From f1b491a15571a43ad69a67d1cc81649e639d1c9c Mon Sep 17 00:00:00 2001 From: rafaelghiorzi Date: Mon, 25 May 2026 10:28:27 -0300 Subject: [PATCH 01/23] gerando framework --- .gitattributes | 9 + .github/dependabot.yml | 12 + .github/workflows/ci.yml | 134 ++++ .gitignore | 35 + project/.dockerignore | 51 ++ project/.kamal/hooks/docker-setup.sample | 3 + project/.kamal/hooks/post-app-boot.sample | 3 + project/.kamal/hooks/post-deploy.sample | 14 + project/.kamal/hooks/post-proxy-reboot.sample | 3 + project/.kamal/hooks/pre-app-boot.sample | 3 + project/.kamal/hooks/pre-build.sample | 51 ++ project/.kamal/hooks/pre-connect.sample | 47 ++ project/.kamal/hooks/pre-deploy.sample | 122 ++++ project/.kamal/hooks/pre-proxy-reboot.sample | 3 + project/.kamal/secrets | 20 + project/.rubocop.yml | 8 + project/.ruby-version | 1 + project/Dockerfile | 77 +++ project/Gemfile | 69 ++ project/Gemfile.lock | 613 ++++++++++++++++++ project/README.md | 24 + project/Rakefile | 6 + project/app/assets/images/.keep | 0 .../app/assets/stylesheets/application.css | 10 + .../app/controllers/application_controller.rb | 7 + project/app/controllers/concerns/.keep | 0 project/app/helpers/application_helper.rb | 2 + project/app/javascript/application.js | 3 + .../app/javascript/controllers/application.js | 9 + .../controllers/hello_controller.js | 7 + project/app/javascript/controllers/index.js | 4 + project/app/jobs/application_job.rb | 7 + project/app/mailers/application_mailer.rb | 4 + project/app/models/application_record.rb | 3 + project/app/models/concerns/.keep | 0 .../app/views/layouts/application.html.erb | 29 + project/app/views/layouts/mailer.html.erb | 13 + project/app/views/layouts/mailer.text.erb | 1 + project/app/views/pwa/manifest.json.erb | 22 + project/app/views/pwa/service-worker.js | 26 + project/bin/brakeman | 7 + project/bin/bundler-audit | 6 + project/bin/ci | 6 + project/bin/dev | 2 + project/bin/docker-entrypoint | 8 + project/bin/importmap | 4 + project/bin/jobs | 6 + project/bin/kamal | 16 + project/bin/rails | 4 + project/bin/rake | 4 + project/bin/rubocop | 8 + project/bin/setup | 35 + project/bin/thrust | 5 + project/config.ru | 6 + project/config/application.rb | 27 + project/config/boot.rb | 4 + project/config/bundler-audit.yml | 5 + project/config/cable.yml | 17 + project/config/cache.yml | 16 + project/config/ci.rb | 24 + project/config/credentials.yml.enc | 1 + project/config/database.yml | 40 ++ project/config/deploy.yml | 119 ++++ project/config/environment.rb | 5 + project/config/environments/development.rb | 78 +++ project/config/environments/production.rb | 90 +++ project/config/environments/test.rb | 53 ++ project/config/importmap.rb | 7 + project/config/initializers/assets.rb | 7 + .../initializers/content_security_policy.rb | 29 + .../initializers/filter_parameter_logging.rb | 8 + project/config/initializers/inflections.rb | 16 + project/config/locales/en.yml | 31 + project/config/puma.rb | 42 ++ project/config/queue.yml | 18 + project/config/recurring.yml | 15 + project/config/routes.rb | 14 + project/config/storage.yml | 27 + project/db/cable_schema.rb | 11 + project/db/cache_schema.rb | 12 + project/db/queue_schema.rb | 129 ++++ project/db/seeds.rb | 9 + project/lib/tasks/.keep | 0 project/log/.keep | 0 project/public/400.html | 135 ++++ project/public/404.html | 135 ++++ project/public/406-unsupported-browser.html | 135 ++++ project/public/422.html | 135 ++++ project/public/500.html | 135 ++++ project/public/icon.png | Bin 0 -> 4166 bytes project/public/icon.svg | 3 + project/public/robots.txt | 1 + project/script/.keep | 0 project/storage/.keep | 0 project/test/controllers/.keep | 0 project/test/fixtures/files/.keep | 0 project/test/helpers/.keep | 0 project/test/integration/.keep | 0 project/test/mailers/.keep | 0 project/test/models/.keep | 0 project/test/test_helper.rb | 15 + project/tmp/.keep | 0 project/tmp/pids/.keep | 0 project/tmp/storage/.keep | 0 project/vendor/.keep | 0 project/vendor/javascript/.keep | 0 106 files changed, 3120 insertions(+) create mode 100644 .gitattributes create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 project/.dockerignore create mode 100755 project/.kamal/hooks/docker-setup.sample create mode 100755 project/.kamal/hooks/post-app-boot.sample create mode 100755 project/.kamal/hooks/post-deploy.sample create mode 100755 project/.kamal/hooks/post-proxy-reboot.sample create mode 100755 project/.kamal/hooks/pre-app-boot.sample create mode 100755 project/.kamal/hooks/pre-build.sample create mode 100755 project/.kamal/hooks/pre-connect.sample create mode 100755 project/.kamal/hooks/pre-deploy.sample create mode 100755 project/.kamal/hooks/pre-proxy-reboot.sample create mode 100644 project/.kamal/secrets create mode 100644 project/.rubocop.yml create mode 100644 project/.ruby-version create mode 100644 project/Dockerfile create mode 100644 project/Gemfile create mode 100644 project/Gemfile.lock create mode 100644 project/README.md create mode 100644 project/Rakefile create mode 100644 project/app/assets/images/.keep create mode 100644 project/app/assets/stylesheets/application.css create mode 100644 project/app/controllers/application_controller.rb create mode 100644 project/app/controllers/concerns/.keep create mode 100644 project/app/helpers/application_helper.rb create mode 100644 project/app/javascript/application.js create mode 100644 project/app/javascript/controllers/application.js create mode 100644 project/app/javascript/controllers/hello_controller.js create mode 100644 project/app/javascript/controllers/index.js create mode 100644 project/app/jobs/application_job.rb create mode 100644 project/app/mailers/application_mailer.rb create mode 100644 project/app/models/application_record.rb create mode 100644 project/app/models/concerns/.keep create mode 100644 project/app/views/layouts/application.html.erb create mode 100644 project/app/views/layouts/mailer.html.erb create mode 100644 project/app/views/layouts/mailer.text.erb create mode 100644 project/app/views/pwa/manifest.json.erb create mode 100644 project/app/views/pwa/service-worker.js create mode 100755 project/bin/brakeman create mode 100755 project/bin/bundler-audit create mode 100755 project/bin/ci create mode 100755 project/bin/dev create mode 100755 project/bin/docker-entrypoint create mode 100755 project/bin/importmap create mode 100755 project/bin/jobs create mode 100755 project/bin/kamal create mode 100755 project/bin/rails create mode 100755 project/bin/rake create mode 100755 project/bin/rubocop create mode 100755 project/bin/setup create mode 100755 project/bin/thrust create mode 100644 project/config.ru create mode 100644 project/config/application.rb create mode 100644 project/config/boot.rb create mode 100644 project/config/bundler-audit.yml create mode 100644 project/config/cable.yml create mode 100644 project/config/cache.yml create mode 100644 project/config/ci.rb create mode 100644 project/config/credentials.yml.enc create mode 100644 project/config/database.yml create mode 100644 project/config/deploy.yml create mode 100644 project/config/environment.rb create mode 100644 project/config/environments/development.rb create mode 100644 project/config/environments/production.rb create mode 100644 project/config/environments/test.rb create mode 100644 project/config/importmap.rb create mode 100644 project/config/initializers/assets.rb create mode 100644 project/config/initializers/content_security_policy.rb create mode 100644 project/config/initializers/filter_parameter_logging.rb create mode 100644 project/config/initializers/inflections.rb create mode 100644 project/config/locales/en.yml create mode 100644 project/config/puma.rb create mode 100644 project/config/queue.yml create mode 100644 project/config/recurring.yml create mode 100644 project/config/routes.rb create mode 100644 project/config/storage.yml create mode 100644 project/db/cable_schema.rb create mode 100644 project/db/cache_schema.rb create mode 100644 project/db/queue_schema.rb create mode 100644 project/db/seeds.rb create mode 100644 project/lib/tasks/.keep create mode 100644 project/log/.keep create mode 100644 project/public/400.html create mode 100644 project/public/404.html create mode 100644 project/public/406-unsupported-browser.html create mode 100644 project/public/422.html create mode 100644 project/public/500.html create mode 100644 project/public/icon.png create mode 100644 project/public/icon.svg create mode 100644 project/public/robots.txt create mode 100644 project/script/.keep create mode 100644 project/storage/.keep create mode 100644 project/test/controllers/.keep create mode 100644 project/test/fixtures/files/.keep create mode 100644 project/test/helpers/.keep create mode 100644 project/test/integration/.keep create mode 100644 project/test/mailers/.keep create mode 100644 project/test/models/.keep create mode 100644 project/test/test_helper.rb create mode 100644 project/tmp/.keep create mode 100644 project/tmp/pids/.keep create mode 100644 project/tmp/storage/.keep create mode 100644 project/vendor/.keep create mode 100644 project/vendor/javascript/.keep diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..6ad66bb7e2 --- /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. +project/db/schema.rb linguist-generated + +# Mark any vendored files as having been vendored. +project/vendor/* linguist-vendored +project/config/credentials/*.yml.enc diff=rails_credentials +project/config/credentials.yml.enc diff=rails_credentials diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..262523cdd0 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: +- package-ecosystem: bundler + directory: "/project" + schedule: + interval: weekly + open-pull-requests-limit: 10 +- package-ecosystem: github-actions + directory: "/project" + 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..07761490f5 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,134 @@ +name: CI + +defaults: + run: + working-directory: project + +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..4c62e618cf --- /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. +project/.bundle + +# Ignore all environment files. +project/.env* + +# Ignore all logfiles and tempfiles. +project/log/* +project/tmp/* +!project/log/.keep +!project/tmp/.keep + +# Ignore pidfiles, but keep the directory. +project/tmp/pids/* +!project/tmp/pids/ +!project/tmp/pids/.keep + +# Ignore storage (uploaded files in development and any SQLite databases). +project/storage/* +!project/storage/.keep +project/tmp/storage/* +!project/tmp/storage/ +!project/tmp/storage/.keep + +project/public/assets + +# Ignore key files for decrypting credentials and more. +project/config/*.key + diff --git a/project/.dockerignore b/project/.dockerignore new file mode 100644 index 0000000000..325bfc036d --- /dev/null +++ b/project/.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/project/.kamal/hooks/docker-setup.sample b/project/.kamal/hooks/docker-setup.sample new file mode 100755 index 0000000000..2fb07d7d7a --- /dev/null +++ b/project/.kamal/hooks/docker-setup.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Docker set up on $KAMAL_HOSTS..." diff --git a/project/.kamal/hooks/post-app-boot.sample b/project/.kamal/hooks/post-app-boot.sample new file mode 100755 index 0000000000..70f9c4bc95 --- /dev/null +++ b/project/.kamal/hooks/post-app-boot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Booted app version $KAMAL_VERSION on $KAMAL_HOSTS..." diff --git a/project/.kamal/hooks/post-deploy.sample b/project/.kamal/hooks/post-deploy.sample new file mode 100755 index 0000000000..fd364c2a77 --- /dev/null +++ b/project/.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/project/.kamal/hooks/post-proxy-reboot.sample b/project/.kamal/hooks/post-proxy-reboot.sample new file mode 100755 index 0000000000..1435a677f2 --- /dev/null +++ b/project/.kamal/hooks/post-proxy-reboot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Rebooted kamal-proxy on $KAMAL_HOSTS" diff --git a/project/.kamal/hooks/pre-app-boot.sample b/project/.kamal/hooks/pre-app-boot.sample new file mode 100755 index 0000000000..45f7355045 --- /dev/null +++ b/project/.kamal/hooks/pre-app-boot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Booting app version $KAMAL_VERSION on $KAMAL_HOSTS..." diff --git a/project/.kamal/hooks/pre-build.sample b/project/.kamal/hooks/pre-build.sample new file mode 100755 index 0000000000..c5a55678b2 --- /dev/null +++ b/project/.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/project/.kamal/hooks/pre-connect.sample b/project/.kamal/hooks/pre-connect.sample new file mode 100755 index 0000000000..77744bdca8 --- /dev/null +++ b/project/.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/project/.kamal/hooks/pre-deploy.sample b/project/.kamal/hooks/pre-deploy.sample new file mode 100755 index 0000000000..05b3055b72 --- /dev/null +++ b/project/.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/project/.kamal/hooks/pre-proxy-reboot.sample b/project/.kamal/hooks/pre-proxy-reboot.sample new file mode 100755 index 0000000000..061f8059e6 --- /dev/null +++ b/project/.kamal/hooks/pre-proxy-reboot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Rebooting kamal-proxy on $KAMAL_HOSTS..." diff --git a/project/.kamal/secrets b/project/.kamal/secrets new file mode 100644 index 0000000000..b3089d6f5a --- /dev/null +++ b/project/.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/project/.rubocop.yml b/project/.rubocop.yml new file mode 100644 index 0000000000..f9d86d4a54 --- /dev/null +++ b/project/.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/project/.ruby-version b/project/.ruby-version new file mode 100644 index 0000000000..61a52c9149 --- /dev/null +++ b/project/.ruby-version @@ -0,0 +1 @@ +ruby-3.4.9 diff --git a/project/Dockerfile b/project/Dockerfile new file mode 100644 index 0000000000..633d3305bc --- /dev/null +++ b/project/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 project . +# docker run -d -p 80:80 -e RAILS_MASTER_KEY= --name project project + +# 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/project/Gemfile b/project/Gemfile new file mode 100644 index 0000000000..7b743a4cab --- /dev/null +++ b/project/Gemfile @@ -0,0 +1,69 @@ +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" + gem "cucumber-rails", require: false + gem "database_cleaner" +end + diff --git a/project/Gemfile.lock b/project/Gemfile.lock new file mode 100644 index 0000000000..aa1ff85d5f --- /dev/null +++ b/project/Gemfile.lock @@ -0,0 +1,613 @@ +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) + 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.1) + 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) + 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-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) + memoist3 (1.0.0) + 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) + multi_test (1.1.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-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 (4.0.0) + 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-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) + 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) + 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.2) + +PLATFORMS + aarch64-linux + aarch64-linux-gnu + aarch64-linux-musl + arm-linux-gnu + arm-linux-musl + x86_64-linux + x86_64-linux-gnu + x86_64-linux-musl + +DEPENDENCIES + bootsnap + brakeman + bundler-audit + capybara + cucumber-rails + database_cleaner + 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 + +CHECKSUMS + action_text-trix (2.1.19) sha256=7012f59421009cf284aa651294896414d653a61a2417c9b8714c8476d2f74009 + actioncable (8.1.3) sha256=e5bc7f75e44e6a22de29c4f43176927c3a9ce4824464b74ed18d8226e75a80f0 + actionmailbox (8.1.3) sha256=df7da474eaa0e70df4ed5a6fef66eb3b3b0f2dbf7f14518deee8d77f1b4aae59 + actionmailer (8.1.3) sha256=831f724891bb70d0aaa4d76581a6321124b6a752cb655c9346aae5479318448d + actionpack (8.1.3) sha256=af998cae4d47c5d581a2cc363b5c77eb718b7c4b45748d81b1887b25621c29a3 + actiontext (8.1.3) sha256=d291019c00e1ea9e6463011fa214f6081a56d7b9a1d224e7d3f6384c1dafc7d2 + actionview (8.1.3) sha256=1347c88c7f3edb38100c5ce0e9fb5e62d7755f3edc1b61cce2eb0b2c6ea2fd5d + activejob (8.1.3) sha256=a149b1766aa8204c3c3da7309e4becd40fcd5529c348cffbf6c9b16b565fe8d3 + activemodel (8.1.3) sha256=90c05cbe4cef3649b8f79f13016191ea94c4525ce4a5c0fb7ef909c4b91c8219 + activerecord (8.1.3) sha256=8003be7b2466ba0a2a670e603eeb0a61dd66058fccecfc49901e775260ac70ab + activestorage (8.1.3) sha256=0564ce9309143951a67615e1bb4e090ee54b8befed417133cae614479b46384d + activesupport (8.1.3) sha256=21a5e0dfbd4c3ddd9e1317ec6a4d782fa226e7867dc70b0743acda81a1dca20e + addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af + ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383 + base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b + bcrypt_pbkdf (1.1.2) sha256=c2414c23ce66869b3eb9f643d6a3374d8322dfb5078125c82792304c10b94cf6 + bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd + bindex (0.8.1) sha256=7b1ecc9dc539ed8bccfc8cb4d2732046227b09d6f37582ff12e50a5047ceb17e + bootsnap (1.24.5) sha256=36b677448524d279b470469aabd5dff4a980e3fa4931a0df68da4a500eb1b6c4 + brakeman (8.0.4) sha256=7bf921fa9638544835df9aa7b3e720a9a72c0267f34f92135955edd80d4dcf6f + builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f + bundler-audit (0.9.3) sha256=81c8766c71e47d0d28a0f98c7eed028539f21a6ea3cd8f685eb6f42333c9b4e9 + capybara (3.40.0) sha256=42dba720578ea1ca65fd7a41d163dd368502c191804558f6e0f71b391054aeef + concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab + connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a + crass (1.0.6) sha256=dc516022a56e7b3b156099abc81b6d2b08ea1ed12676ac7a5657617f012bd45d + cucumber (10.2.0) sha256=fdedbd31ecf40858b60f04853f2aa15c44f5c30bbac29c6a227fa1e7005a8158 + cucumber-ci-environment (11.0.0) sha256=0df79a9e1d0b015b3d9def680f989200d96fef206f4d19ccf86a338c4f71d1e2 + cucumber-core (16.2.0) sha256=592b58a95cf42feef8e5a349f68e363784ba3b6568ffbcf6776e38e136cf970b + cucumber-cucumber-expressions (19.0.1) sha256=648ec09045190d818fb797af46e1648148599fd67a086a34a7f0e647d9e36c8c + cucumber-gherkin (39.1.0) sha256=aed12a0c955d8563d80a012633c1a72075525f4d64d4cc983001df2181b379ed + cucumber-html-formatter (22.3.0) sha256=f9768ed05588dbd73a5f3824c2cc648bd86b00206e6972d743af8051281d0729 + cucumber-messages (32.3.1) sha256=ddc88e4c1cf7afb96c06005b92a4a6f221a2fa435a8b4ca04677d215fd82771c + cucumber-rails (4.0.1) sha256=bd3513ec47dc06188cc05703648cbc3560fb115f3f5cfb8b616065b4d6e8024d + cucumber-tag-expressions (8.1.0) sha256=9bd8c4b6654f8e5bf2a9c99329b6f32136a75e50cd39d4cfb3927d0fa9f52e21 + database_cleaner (2.1.0) sha256=1dcba26e3b1576da692fc6bac10136a4744da5bcc293d248aae19640c65d89cd + database_cleaner-active_record (2.2.2) sha256=88296b9f3088c31f7c0d4fcec10f68e4b71c96698043916de59b04debec10388 + database_cleaner-core (2.0.1) sha256=8646574c32162e59ed7b5258a97a208d3c44551b854e510994f24683865d846c + date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0 + debug (1.11.1) sha256=2e0b0ac6119f2207a6f8ac7d4a73ca8eb4e440f64da0a3136c30343146e952b6 + diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962 + dotenv (3.2.0) sha256=e375b83121ea7ca4ce20f214740076129ab8514cd81378161f11c03853fe619d + drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373 + ed25519 (1.4.0) sha256=16e97f5198689a154247169f3453ef4cfd3f7a47481fde0ae33206cdfdcac506 + erb (6.0.4) sha256=38e3803694be357fe2bfe312487c74beaf9fb4e5beb3e22498952fe1645b95d9 + erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9 + et-orbi (1.4.0) sha256=6c7e3c90779821f9e3b324c5e96fda9767f72995d6ae435b96678a4f3e2de8bc + ffi (1.17.4-aarch64-linux-gnu) sha256=b208f06f91ffd8f5e1193da3cae3d2ccfc27fc36fba577baf698d26d91c080df + ffi (1.17.4-aarch64-linux-musl) sha256=9286b7a615f2676245283aef0a0a3b475ae3aae2bb5448baace630bb77b91f39 + ffi (1.17.4-arm-linux-gnu) sha256=d6dbddf7cb77bf955411af5f187a65b8cd378cb003c15c05697f5feee1cb1564 + ffi (1.17.4-arm-linux-musl) sha256=9d4838ded0465bef6e2426935f6bcc93134b6616785a84ffd2a3d82bc3cf6f95 + ffi (1.17.4-x86_64-linux-gnu) sha256=9d3db14c2eae074b382fa9c083fe95aec6e0a1451da249eab096c34002bc752d + ffi (1.17.4-x86_64-linux-musl) sha256=3fdf9888483de005f8ef8d1cf2d3b20d86626af206cbf780f6a6a12439a9c49e + fugit (1.12.1) sha256=5898f478ede9b415f0804e42b8f3fd53f814bd85eebffceebdbc34e1107aaf68 + globalid (1.3.0) sha256=05c639ad6eb4594522a0b07983022f04aa7254626ab69445a0e493aa3786ff11 + i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5 + image_processing (1.14.0) sha256=754cc169c9c262980889bec6bfd325ed1dafad34f85242b5a07b60af004742fb + importmap-rails (2.2.3) sha256=7101be2a4dc97cf1558fb8f573a718404c5f6bcfe94f304bf1f39e444feeb16a + io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc + irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3 + jbuilder (2.15.0) sha256=fe36cd45b47dd88cb2cbebc3adc4348f041825f580d503578594e8255817f889 + json (2.19.5) sha256=218a18553e4801d579ca7e0f5bc72bafd776d7397238a1fb4e74db5b0a812c59 + kamal (2.11.0) sha256=1408864425e0dec7e0a14d712a3b13f614e9f3a425b7661d3f9d287a51d7dd75 + language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc + lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87 + logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 + loofah (2.25.1) sha256=d436c73dbd0c1147b16c4a41db097942d217303e1f7728704b37e4df9f6d2e04 + mail (2.9.0) sha256=6fa6673ecd71c60c2d996260f9ee3dd387d4673b8169b502134659ece6d34941 + marcel (1.2.1) sha256=1678e9360e32f9eafa917c80029e2f6d10b2715c66a4b87b6d0da9b9cd1f859f + matrix (0.4.3) sha256=a0d5ab7ddcc1973ff690ab361b67f359acbb16958d1dc072b8b956a286564c5b + memoist3 (1.0.0) sha256=686e42402cf150a362050c23143dc57b0ef88f8c344943ff8b7845792b50d56f + mini_magick (5.3.1) sha256=29395dfd76badcabb6403ee5aff6f681e867074f8f28ce08d78661e9e4a351c4 + mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef + minitest (6.0.6) sha256=153ea36d1d987a62942382b61075745042a2b3123b1cd48f4c3675af9cc7d6f1 + msgpack (1.8.0) sha256=e64ce0212000d016809f5048b48eb3a65ffb169db22238fb4b72472fecb2d732 + multi_test (1.1.0) sha256=e9e550cdd863fb72becfe344aefdcd4cbd26ebf307847f4a6c039a4082324d10 + net-imap (0.6.4) sha256=9a5598c67a3022c284d98430ef1d4948e7dbdb62596f61081ea8ca933270a02b + net-pop (0.1.2) sha256=848b4e982013c15b2f0382792268763b748cce91c9e91e36b0f27ed26420dff3 + net-protocol (0.2.2) sha256=aa73e0cba6a125369de9837b8d8ef82a61849360eba0521900e2c3713aa162a8 + net-scp (4.1.0) sha256=a99b0b92a1e5d360b0de4ffbf2dc0c91531502d3d4f56c28b0139a7c093d1a5d + net-sftp (4.0.0) sha256=65bb91c859c2f93b09826757af11b69af931a3a9155050f50d1b06d384526364 + net-smtp (0.5.1) sha256=ed96a0af63c524fceb4b29b0d352195c30d82dd916a42f03c62a3a70e5b70736 + net-ssh (7.3.2) sha256=65029e213c380e20e5fd92ece663934ab0a0fe888e0cd7cc6a5b664074362dd4 + nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1 + nokogiri (1.19.3-aarch64-linux-gnu) sha256=46b89e5d7b9e844c2ee360794240c6ea2a4e6fa0c5892a4ed487db621224b639 + nokogiri (1.19.3-aarch64-linux-musl) sha256=8392dfdcd21be7a94dbbe9ccc138dea01b97b24cb2dc02a114ca98bfb1d9a0b7 + nokogiri (1.19.3-arm-linux-gnu) sha256=3919d5ffc334ad778a4a9eb88fda7dcb8b1fb58c8a52ac640c6dcd2f038e774f + nokogiri (1.19.3-arm-linux-musl) sha256=9ce1cb6346bb9c67b1550eb537aa183ead91e4b6eadb2f36ade02d8dd2a79fb6 + nokogiri (1.19.3-x86_64-linux-gnu) sha256=2f5078620fe12e83669b5b17311b32532a8153d02eee7ad06948b926d6080976 + nokogiri (1.19.3-x86_64-linux-musl) sha256=248c906d2166eca5efb56d52fdee5f9a1f51d69a72e2b64fdac647b4ce39ea3f + ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912 + parallel (2.1.0) sha256=b35258865c2e31134c5ecb708beaaf6772adf9d5efae28e93e99260877b09356 + parser (3.3.11.1) sha256=d17ace7aabe3e72c3cc94043714be27cc6f852f104d81aa284c2281aecc65d54 + pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6 + prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193 + prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85 + propshaft (1.3.2) sha256=1d56a3e56a92c21bfc29caf07406b5386b00d4c47ddf357cf989a5a234b1389e + psych (5.3.1) sha256=eb7a57cef10c9d70173ff74e739d843ac3b2c019a003de48447b2963d81b1974 + public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623 + puma (8.0.1) sha256=7b94e50c07655718c1fb8ae41a11fc06c7d61293208b3aa608ff71a46d3ad37c + raabro (1.4.0) sha256=d4fa9ff5172391edb92b242eed8be802d1934b1464061ae5e70d80962c5da882 + racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f + rack (3.2.6) sha256=5ed78e1f73b2e25679bec7d45ee2d4483cc4146eb1be0264fc4d94cb5ef212c2 + rack-session (2.1.2) sha256=595434f8c0c3473ae7d7ac56ecda6cc6dfd9d37c0b2b5255330aa1576967ffe8 + rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463 + rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868 + rails (8.1.3) sha256=6d017ba5348c98fc909753a8169b21d44de14d2a0b92d140d1a966834c3c9cd3 + rails-dom-testing (2.3.0) sha256=8acc7953a7b911ca44588bf08737bc16719f431a1cc3091a292bca7317925c1d + rails-html-sanitizer (1.7.0) sha256=28b145cceaf9cc214a9874feaa183c3acba036c9592b19886e0e45efc62b1e89 + railties (8.1.3) sha256=913eb0e0cb520aac687ffd74916bd726d48fa21f47833c6292576ef6a286de22 + rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a + rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701 + rdoc (7.2.0) sha256=8650f76cd4009c3b54955eb5d7e3a075c60a57276766ebf36f9085e8c9f23192 + regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb + reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835 + rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142 + rubocop (1.86.2) sha256=bb2e97f635eda42c448f2588f4a6ff78f221b8bdfdf65b1e9b07fbd57521b45d + rubocop-ast (1.49.1) sha256=4412f3ee70f6fe4546cc489548e0f6fcf76cafcfa80fa03af67098ffed755035 + rubocop-performance (1.26.1) sha256=cd19b936ff196df85829d264b522fd4f98b6c89ad271fa52744a8c11b8f71834 + rubocop-rails (2.35.2) sha256=088865be9675922a5c8f13c00055a71ab768ea5eed211437cffd2a8b46b64ac2 + rubocop-rails-omakase (1.1.0) sha256=2af73ac8ee5852de2919abbd2618af9c15c19b512c4cfc1f9a5d3b6ef009109d + ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33 + ruby-vips (2.3.0) sha256=e685ec02c13969912debbd98019e50492e12989282da5f37d05f5471442f5374 + rubyzip (3.3.0) sha256=a372fc67892a4f8c0bc8ec906b720353d8e48807a64b2e63adf99b1e3583a034 + securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1 + selenium-webdriver (4.44.0) sha256=6f1df072529af369589c46f0e01132952aabb250cfd683c274d74dc1eb5d8477 + solid_cable (4.0.0) sha256=8379680ef6bf36e195eb876a6306ea290f87d5fa10bc4a757bc2a918f83229b5 + solid_cache (1.0.10) sha256=bc05a2fb3ac78a6f43cbb5946679cf9db67dd30d22939ededc385cb93e120d41 + solid_queue (1.4.0) sha256=e6a18d196f0b27cb6e3c77c5b31258b05fb634f8ed64fb1866ed164047216c2a + sqlite3 (2.9.4-aarch64-linux-gnu) sha256=ecabed721e6eaad54601d2685f09029d90025efc8d931040dc89cb3f8a2080ec + sqlite3 (2.9.4-aarch64-linux-musl) sha256=ffb4255947fb54c8c3eeca97460c9702b40de91ce390455ef7367ca6a3929a31 + sqlite3 (2.9.4-arm-linux-gnu) sha256=9ee2008b9fbec984c3c165b0d7eedd2bd2a415100b761bfa3a4c6fbec9208bf6 + sqlite3 (2.9.4-arm-linux-musl) sha256=8dc1fe4da6977992cd62decf4a93ccf6cc2e124a5e6a340160d52092f70e837a + sqlite3 (2.9.4-x86_64-linux-gnu) sha256=537a3eda71b1df1336d0055cbebe55a7317c34870c192c7b6b9d8d0be6871847 + sqlite3 (2.9.4-x86_64-linux-musl) sha256=3fc5e865b4be9a85d998203ef8d0c0fdcb92f20acf34a254346ff8a19088efec + sshkit (1.25.0) sha256=c8c6543cdb60f91f1d277306d585dd11b6a064cb44eab0972827e4311ff96744 + stimulus-rails (1.3.4) sha256=765676ffa1f33af64ce026d26b48e8ffb2e0b94e0f50e9119e11d6107d67cb06 + stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1 + sys-uname (1.5.1) sha256=784d7e6491b0393c25cbbe5ac38324ac7be9fda083a6094832648af669386d7b + thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73 + thruster (0.1.21) sha256=dc67928f36e5894844579a95e45637a5091db7a7ea05468ee8c2c6eb0a3f77cf + thruster (0.1.21-aarch64-linux) sha256=f5aff78fb7a6431ed3d6ab4bde03a89c461e9a73981dbc97d6990d85c3db235c + thruster (0.1.21-x86_64-linux) sha256=6e2fbcf826540a72d3710ae4db072c2333287ac2ee57e7e52f35bc10900d74a7 + timeout (0.6.1) sha256=78f57368a7e7bbadec56971f78a3f5ecbcfb59b7fcbb0a3ed6ddc08a5094accb + tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f + turbo-rails (2.0.23) sha256=ee0d90733aafff056cf51ff11e803d65e43cae258cc55f6492020ec1f9f9315f + tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b + unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42 + unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f + uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6 + useragent (0.16.11) sha256=700e6413ad4bb954bb63547fa098dddf7b0ebe75b40cc6f93b8d54255b173844 + web-console (4.3.0) sha256=e13b71301cdfc2093f155b5aa3a622db80b4672d1f2f713119cc7ec7ac6a6da4 + websocket (1.2.11) sha256=b7e7a74e2410b5e85c25858b26b3322f29161e300935f70a0e0d3c35e0462737 + websocket-driver (0.8.0) sha256=ed0dba4b943c22f17f9a734817e808bc84cdce6a7e22045f5315aa57676d4962 + websocket-extensions (0.1.5) sha256=1c6ba63092cda343eb53fc657110c71c754c56484aad42578495227d717a8241 + xpath (3.2.0) sha256=6dfda79d91bb3b949b947ecc5919f042ef2f399b904013eb3ef6d20dd3a4082e + zeitwerk (2.8.2) sha256=7212a61311083c604184b1ea2574b9aa05cd14f855a0841c06985cabe9181d12 + +BUNDLED WITH + 4.0.9 diff --git a/project/README.md b/project/README.md new file mode 100644 index 0000000000..7db80e4ca1 --- /dev/null +++ b/project/README.md @@ -0,0 +1,24 @@ +# 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/project/Rakefile b/project/Rakefile new file mode 100644 index 0000000000..9a5ea7383a --- /dev/null +++ b/project/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/project/app/assets/images/.keep b/project/app/assets/images/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/project/app/assets/stylesheets/application.css b/project/app/assets/stylesheets/application.css new file mode 100644 index 0000000000..fe93333c0f --- /dev/null +++ b/project/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/project/app/controllers/application_controller.rb b/project/app/controllers/application_controller.rb new file mode 100644 index 0000000000..c3537563da --- /dev/null +++ b/project/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/project/app/controllers/concerns/.keep b/project/app/controllers/concerns/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/project/app/helpers/application_helper.rb b/project/app/helpers/application_helper.rb new file mode 100644 index 0000000000..de6be7945c --- /dev/null +++ b/project/app/helpers/application_helper.rb @@ -0,0 +1,2 @@ +module ApplicationHelper +end diff --git a/project/app/javascript/application.js b/project/app/javascript/application.js new file mode 100644 index 0000000000..0d7b49404c --- /dev/null +++ b/project/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/project/app/javascript/controllers/application.js b/project/app/javascript/controllers/application.js new file mode 100644 index 0000000000..1213e85c7a --- /dev/null +++ b/project/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/project/app/javascript/controllers/hello_controller.js b/project/app/javascript/controllers/hello_controller.js new file mode 100644 index 0000000000..5975c0789d --- /dev/null +++ b/project/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/project/app/javascript/controllers/index.js b/project/app/javascript/controllers/index.js new file mode 100644 index 0000000000..1156bf8362 --- /dev/null +++ b/project/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/project/app/jobs/application_job.rb b/project/app/jobs/application_job.rb new file mode 100644 index 0000000000..d394c3d106 --- /dev/null +++ b/project/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/project/app/mailers/application_mailer.rb b/project/app/mailers/application_mailer.rb new file mode 100644 index 0000000000..3c34c8148f --- /dev/null +++ b/project/app/mailers/application_mailer.rb @@ -0,0 +1,4 @@ +class ApplicationMailer < ActionMailer::Base + default from: "from@example.com" + layout "mailer" +end diff --git a/project/app/models/application_record.rb b/project/app/models/application_record.rb new file mode 100644 index 0000000000..b63caeb8a5 --- /dev/null +++ b/project/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + primary_abstract_class +end diff --git a/project/app/models/concerns/.keep b/project/app/models/concerns/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/project/app/views/layouts/application.html.erb b/project/app/views/layouts/application.html.erb new file mode 100644 index 0000000000..6a1d584026 --- /dev/null +++ b/project/app/views/layouts/application.html.erb @@ -0,0 +1,29 @@ + + + + <%= content_for(:title) || "Project" %> + + + + + <%= 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/project/app/views/layouts/mailer.html.erb b/project/app/views/layouts/mailer.html.erb new file mode 100644 index 0000000000..3aac9002ed --- /dev/null +++ b/project/app/views/layouts/mailer.html.erb @@ -0,0 +1,13 @@ + + + + + + + + + <%= yield %> + + diff --git a/project/app/views/layouts/mailer.text.erb b/project/app/views/layouts/mailer.text.erb new file mode 100644 index 0000000000..37f0bddbd7 --- /dev/null +++ b/project/app/views/layouts/mailer.text.erb @@ -0,0 +1 @@ +<%= yield %> diff --git a/project/app/views/pwa/manifest.json.erb b/project/app/views/pwa/manifest.json.erb new file mode 100644 index 0000000000..5cd24347d3 --- /dev/null +++ b/project/app/views/pwa/manifest.json.erb @@ -0,0 +1,22 @@ +{ + "name": "Project", + "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": "Project.", + "theme_color": "red", + "background_color": "red" +} diff --git a/project/app/views/pwa/service-worker.js b/project/app/views/pwa/service-worker.js new file mode 100644 index 0000000000..b3a13fb7bb --- /dev/null +++ b/project/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/project/bin/brakeman b/project/bin/brakeman new file mode 100755 index 0000000000..ace1c9ba08 --- /dev/null +++ b/project/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/project/bin/bundler-audit b/project/bin/bundler-audit new file mode 100755 index 0000000000..e2ef22690c --- /dev/null +++ b/project/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/project/bin/ci b/project/bin/ci new file mode 100755 index 0000000000..4137ad5bb0 --- /dev/null +++ b/project/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/project/bin/dev b/project/bin/dev new file mode 100755 index 0000000000..5f91c20545 --- /dev/null +++ b/project/bin/dev @@ -0,0 +1,2 @@ +#!/usr/bin/env ruby +exec "./bin/rails", "server", *ARGV diff --git a/project/bin/docker-entrypoint b/project/bin/docker-entrypoint new file mode 100755 index 0000000000..ed31659f40 --- /dev/null +++ b/project/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/project/bin/importmap b/project/bin/importmap new file mode 100755 index 0000000000..36502ab16c --- /dev/null +++ b/project/bin/importmap @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby + +require_relative "../config/application" +require "importmap/commands" diff --git a/project/bin/jobs b/project/bin/jobs new file mode 100755 index 0000000000..dcf59f309a --- /dev/null +++ b/project/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/project/bin/kamal b/project/bin/kamal new file mode 100755 index 0000000000..d9ba276702 --- /dev/null +++ b/project/bin/kamal @@ -0,0 +1,16 @@ +#!/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__) + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("kamal", "kamal") diff --git a/project/bin/rails b/project/bin/rails new file mode 100755 index 0000000000..efc0377492 --- /dev/null +++ b/project/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/project/bin/rake b/project/bin/rake new file mode 100755 index 0000000000..4fbf10b960 --- /dev/null +++ b/project/bin/rake @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +require_relative "../config/boot" +require "rake" +Rake.application.run diff --git a/project/bin/rubocop b/project/bin/rubocop new file mode 100755 index 0000000000..5a20504716 --- /dev/null +++ b/project/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/project/bin/setup b/project/bin/setup new file mode 100755 index 0000000000..81be011e87 --- /dev/null +++ b/project/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/project/bin/thrust b/project/bin/thrust new file mode 100755 index 0000000000..36bde2d832 --- /dev/null +++ b/project/bin/thrust @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("thruster", "thrust") diff --git a/project/config.ru b/project/config.ru new file mode 100644 index 0000000000..4a3c09a688 --- /dev/null +++ b/project/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/project/config/application.rb b/project/config/application.rb new file mode 100644 index 0000000000..a69c974960 --- /dev/null +++ b/project/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 Project + 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/project/config/boot.rb b/project/config/boot.rb new file mode 100644 index 0000000000..988a5ddc46 --- /dev/null +++ b/project/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/project/config/bundler-audit.yml b/project/config/bundler-audit.yml new file mode 100644 index 0000000000..e74b3af949 --- /dev/null +++ b/project/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/project/config/cable.yml b/project/config/cable.yml new file mode 100644 index 0000000000..b9adc5aa3a --- /dev/null +++ b/project/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/project/config/cache.yml b/project/config/cache.yml new file mode 100644 index 0000000000..19d490843b --- /dev/null +++ b/project/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/project/config/ci.rb b/project/config/ci.rb new file mode 100644 index 0000000000..1712cc1127 --- /dev/null +++ b/project/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/project/config/credentials.yml.enc b/project/config/credentials.yml.enc new file mode 100644 index 0000000000..6fa056b6fe --- /dev/null +++ b/project/config/credentials.yml.enc @@ -0,0 +1 @@ +zAhTSDhDMGxkP/oq8ep7nzeYW8usCy5ZAxebSamtvX+ePfQSIR7IsVz+7jOMqX2UgLTnGu3zEiPjQ+DESz7Q5J8l71GqGwKXG3WxBmaUAmrY6D3QnMONjJZFKbe5i51SRFyKchJ0h3h1FboH4hYg1D1fsJluy1uFLRlA91YZ70uKFXm9DMIfFysymte1KNx762s/S//88EuH0ov5vkeiuAn/FPS4VmcDuKaKQ63tysC36Vqlyv/N4ZwQ2DD6xbzQZgEFfO5VBDmXeZYc0HqwcfOksHC5FbwsPDzRokwwYOKlbrRV85LOzjNQcJQzaXhcc1r20FzksWEbH7q2Q3vQT90yD/Hx83dmoj5jbgbGJpzxjiwykpvU8upKMjVhrxMV4wrOiAQMtP/2uyLORHXZ7ptQ2XwPsUkIPuzMpCaLxGMpWYujblmkUq/FVNGfZHjIZ2icWU4hKve5LqC2TkJV5NY0qPrgQh1cIPSskSOunisOOu9N+NOkrVm6--EAc8LSksNFtj+8n3--qryypbJabJv0tzjhOgJVkQ== \ No newline at end of file diff --git a/project/config/database.yml b/project/config/database.yml new file mode 100644 index 0000000000..302d638c96 --- /dev/null +++ b/project/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/project/config/deploy.yml b/project/config/deploy.yml new file mode 100644 index 0000000000..167f14168a --- /dev/null +++ b/project/config/deploy.yml @@ -0,0 +1,119 @@ +# Name of your application. Used to uniquely configure containers. +service: project + +# Name of the container image (use your-user/app-name on external registries). +image: project + +# 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 project-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: + - "project_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/project/config/environment.rb b/project/config/environment.rb new file mode 100644 index 0000000000..cac5315775 --- /dev/null +++ b/project/config/environment.rb @@ -0,0 +1,5 @@ +# Load the Rails application. +require_relative "application" + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/project/config/environments/development.rb b/project/config/environments/development.rb new file mode 100644 index 0000000000..75243c3d0f --- /dev/null +++ b/project/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/project/config/environments/production.rb b/project/config/environments/production.rb new file mode 100644 index 0000000000..f5763e04e5 --- /dev/null +++ b/project/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/project/config/environments/test.rb b/project/config/environments/test.rb new file mode 100644 index 0000000000..c2095b1174 --- /dev/null +++ b/project/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/project/config/importmap.rb b/project/config/importmap.rb new file mode 100644 index 0000000000..909dfc542d --- /dev/null +++ b/project/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/project/config/initializers/assets.rb b/project/config/initializers/assets.rb new file mode 100644 index 0000000000..487324424f --- /dev/null +++ b/project/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/project/config/initializers/content_security_policy.rb b/project/config/initializers/content_security_policy.rb new file mode 100644 index 0000000000..d51d713979 --- /dev/null +++ b/project/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/project/config/initializers/filter_parameter_logging.rb b/project/config/initializers/filter_parameter_logging.rb new file mode 100644 index 0000000000..c0b717f7ec --- /dev/null +++ b/project/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/project/config/initializers/inflections.rb b/project/config/initializers/inflections.rb new file mode 100644 index 0000000000..3860f659ea --- /dev/null +++ b/project/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/project/config/locales/en.yml b/project/config/locales/en.yml new file mode 100644 index 0000000000..6c349ae5e3 --- /dev/null +++ b/project/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/project/config/puma.rb b/project/config/puma.rb new file mode 100644 index 0000000000..38c4b86596 --- /dev/null +++ b/project/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/project/config/queue.yml b/project/config/queue.yml new file mode 100644 index 0000000000..9eace59c41 --- /dev/null +++ b/project/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/project/config/recurring.yml b/project/config/recurring.yml new file mode 100644 index 0000000000..b4207f9b07 --- /dev/null +++ b/project/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/project/config/routes.rb b/project/config/routes.rb new file mode 100644 index 0000000000..48254e88ed --- /dev/null +++ b/project/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/project/config/storage.yml b/project/config/storage.yml new file mode 100644 index 0000000000..927dc537c8 --- /dev/null +++ b/project/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/project/db/cable_schema.rb b/project/db/cable_schema.rb new file mode 100644 index 0000000000..23666604a5 --- /dev/null +++ b/project/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/project/db/cache_schema.rb b/project/db/cache_schema.rb new file mode 100644 index 0000000000..81a410d188 --- /dev/null +++ b/project/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/project/db/queue_schema.rb b/project/db/queue_schema.rb new file mode 100644 index 0000000000..85194b6a88 --- /dev/null +++ b/project/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/project/db/seeds.rb b/project/db/seeds.rb new file mode 100644 index 0000000000..4fbd6ed970 --- /dev/null +++ b/project/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/project/lib/tasks/.keep b/project/lib/tasks/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/project/log/.keep b/project/log/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/project/public/400.html b/project/public/400.html new file mode 100644 index 0000000000..640de03397 --- /dev/null +++ b/project/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/project/public/404.html b/project/public/404.html new file mode 100644 index 0000000000..d7f0f14222 --- /dev/null +++ b/project/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/project/public/406-unsupported-browser.html b/project/public/406-unsupported-browser.html new file mode 100644 index 0000000000..43d2811e8c --- /dev/null +++ b/project/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/project/public/422.html b/project/public/422.html new file mode 100644 index 0000000000..f12fb4aa17 --- /dev/null +++ b/project/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/project/public/500.html b/project/public/500.html new file mode 100644 index 0000000000..e4eb18a759 --- /dev/null +++ b/project/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/project/public/icon.png b/project/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/project/public/icon.svg b/project/public/icon.svg new file mode 100644 index 0000000000..04b34bf83f --- /dev/null +++ b/project/public/icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/project/public/robots.txt b/project/public/robots.txt new file mode 100644 index 0000000000..c19f78ab68 --- /dev/null +++ b/project/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/project/script/.keep b/project/script/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/project/storage/.keep b/project/storage/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/project/test/controllers/.keep b/project/test/controllers/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/project/test/fixtures/files/.keep b/project/test/fixtures/files/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/project/test/helpers/.keep b/project/test/helpers/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/project/test/integration/.keep b/project/test/integration/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/project/test/mailers/.keep b/project/test/mailers/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/project/test/models/.keep b/project/test/models/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/project/test/test_helper.rb b/project/test/test_helper.rb new file mode 100644 index 0000000000..0c22470ec1 --- /dev/null +++ b/project/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/project/tmp/.keep b/project/tmp/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/project/tmp/pids/.keep b/project/tmp/pids/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/project/tmp/storage/.keep b/project/tmp/storage/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/project/vendor/.keep b/project/vendor/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/project/vendor/javascript/.keep b/project/vendor/javascript/.keep new file mode 100644 index 0000000000..e69de29bb2 From 19cd386e023b8c8241d47c5352672dcbeed44af8 Mon Sep 17 00:00:00 2001 From: rafaelghiorzi Date: Mon, 25 May 2026 10:29:11 -0300 Subject: [PATCH 02/23] adicionando suporte para cucumber] --- project/bin/cucumber | 11 ++++ project/config/cucumber.yml | 8 +++ project/config/environments/development.rb | 4 ++ project/config/environments/test.rb | 4 ++ project/features/step_definitions/.keep | 0 project/features/support/env.rb | 53 +++++++++++++++++ project/lib/tasks/cucumber.rake | 69 ++++++++++++++++++++++ 7 files changed, 149 insertions(+) create mode 100755 project/bin/cucumber create mode 100644 project/config/cucumber.yml create mode 100644 project/features/step_definitions/.keep create mode 100644 project/features/support/env.rb create mode 100644 project/lib/tasks/cucumber.rake diff --git a/project/bin/cucumber b/project/bin/cucumber new file mode 100755 index 0000000000..eb5e962e86 --- /dev/null +++ b/project/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/project/config/cucumber.yml b/project/config/cucumber.yml new file mode 100644 index 0000000000..47a4663ae2 --- /dev/null +++ b/project/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/project/config/environments/development.rb b/project/config/environments/development.rb index 75243c3d0f..7d1b179ef2 100644 --- a/project/config/environments/development.rb +++ b/project/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/project/config/environments/test.rb b/project/config/environments/test.rb index c2095b1174..e6b5c1b020 100644 --- a/project/config/environments/test.rb +++ b/project/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/project/features/step_definitions/.keep b/project/features/step_definitions/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/project/features/support/env.rb b/project/features/support/env.rb new file mode 100644 index 0000000000..3b97d14087 --- /dev/null +++ b/project/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/project/lib/tasks/cucumber.rake b/project/lib/tasks/cucumber.rake new file mode 100644 index 0000000000..0caa4d2553 --- /dev/null +++ b/project/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 6eb995a1fdb2c4348b1a41777df67dd2a63bb1ba Mon Sep 17 00:00:00 2001 From: rafaelghiorzi Date: Mon, 25 May 2026 10:30:38 -0300 Subject: [PATCH 03/23] remove temp error --- project/tmp/.keep | 0 project/tmp/pids/.keep | 0 project/tmp/storage/.keep | 0 3 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 project/tmp/.keep delete mode 100644 project/tmp/pids/.keep delete mode 100644 project/tmp/storage/.keep diff --git a/project/tmp/.keep b/project/tmp/.keep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/project/tmp/pids/.keep b/project/tmp/pids/.keep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/project/tmp/storage/.keep b/project/tmp/storage/.keep deleted file mode 100644 index e69de29bb2..0000000000 From 8e592a978c930bc5aaf4e6252198a2bd1a1814dc Mon Sep 17 00:00:00 2001 From: Diego Gontijo Date: Mon, 25 May 2026 16:51:03 -0300 Subject: [PATCH 04/23] add AGENTS.md to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 4c62e618cf..db0242945b 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,4 @@ project/public/assets # Ignore key files for decrypting credentials and more. project/config/*.key +AGENTS.md \ No newline at end of file From a0e121175bb2c02b885904dc2a4b262f9c14ab61 Mon Sep 17 00:00:00 2001 From: Diego Gontijo Date: Mon, 25 May 2026 17:20:57 -0300 Subject: [PATCH 05/23] add .md file to gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index db0242945b..9f2fe322d8 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,5 @@ project/public/assets # Ignore key files for decrypting credentials and more. project/config/*.key -AGENTS.md \ No newline at end of file +AGENTS.md +PROJECT_CONTEXT.md \ No newline at end of file From 7add62dd00a09d369553ac53c47d2828053fa945 Mon Sep 17 00:00:00 2001 From: Diego Gontijo Date: Mon, 25 May 2026 17:34:49 -0300 Subject: [PATCH 06/23] feat: add auth BDD scenarios --- .../cadastro_usuarios_importados.feature | 31 +++++++++++++ project/features/definicao_de_senha.feature | 32 ++++++++++++++ project/features/login.feature | 44 +++++++++++++++++++ project/features/redefinicao_de_senha.feature | 38 ++++++++++++++++ 4 files changed, 145 insertions(+) create mode 100644 project/features/cadastro_usuarios_importados.feature create mode 100644 project/features/definicao_de_senha.feature create mode 100644 project/features/login.feature create mode 100644 project/features/redefinicao_de_senha.feature diff --git a/project/features/cadastro_usuarios_importados.feature b/project/features/cadastro_usuarios_importados.feature new file mode 100644 index 0000000000..766967aba9 --- /dev/null +++ b/project/features/cadastro_usuarios_importados.feature @@ -0,0 +1,31 @@ +# language: pt +@auth @issue-100 +Funcionalidade: Cadastrar Usuarios do Sistema + Como administrador + Quero cadastrar usuarios a partir dos dados importados do SIGAA + Para que novos participantes recebam o link de definicao de senha e possam acessar o CAMAAR + + Contexto: + Dado que existe um administrador autenticado do departamento "CIC" + E que o arquivo "class_members.json" contem participantes da turma "CIC0097" no semestre "2021.2" + + Cenario: Administrador importa participantes novos e dispara definicao de senha + Quando o administrador importa os participantes do SIGAA + Entao o sistema deve cadastrar o usuario "Ana Clara Jordao Perna" com email "acjpjvjp@gmail.com" e matricula "190084006" + E deve associar o usuario a turma "CIC0097" como participante discente + E deve marcar o usuario como pendente de definicao de senha + E deve enviar email com link de definicao de senha para "acjpjvjp@gmail.com" + + Cenario: Importacao nao duplica usuario ja cadastrado + Dado que ja existe um usuario cadastrado com email "acjpjvjp@gmail.com" e matricula "190084006" + Quando o administrador importa novamente os participantes do SIGAA + Entao o sistema nao deve criar outro usuario com a matricula "190084006" + E deve manter uma unica conta para o email "acjpjvjp@gmail.com" + E deve atualizar os vinculos de turma quando necessario + + Cenario: Registro importado sem dados obrigatorios nao cria conta + Dado que o arquivo de importacao contem um participante sem email ou matricula + Quando o administrador importa os participantes do SIGAA + Entao o sistema nao deve criar conta para o participante invalido + E deve registrar a inconsistencia encontrada na importacao + E deve continuar processando os demais participantes validos diff --git a/project/features/definicao_de_senha.feature b/project/features/definicao_de_senha.feature new file mode 100644 index 0000000000..6a853fc0c0 --- /dev/null +++ b/project/features/definicao_de_senha.feature @@ -0,0 +1,32 @@ +# language: pt +@auth @issue-105 +Funcionalidade: Sistema de Definicao de Senha + Como usuario recem-cadastrado + Quero definir minha primeira senha por um link recebido por email + Para ativar meu acesso ao CAMAAR + + Contexto: + Dado que existe um participante importado do SIGAA com email "acjpjvjp@gmail.com", matricula "190084006" e sem senha definida + E que o sistema enviou um link valido de definicao de senha para "acjpjvjp@gmail.com" + + Cenario: Usuario define a primeira senha com link valido + Quando o usuario acessa o link valido de definicao de senha + E informa a nova senha "Senha@123" + E confirma a nova senha "Senha@123" + E envia o formulario de definicao de senha + Entao o sistema deve salvar a senha do usuario + E deve ativar o acesso do usuario ao CAMAAR + E deve permitir login com email ou matricula e a nova senha + + Cenario: Usuario nao define senha com link invalido + Quando o usuario acessa um link invalido de definicao de senha + Entao o sistema nao deve permitir a definicao de senha + E deve exibir a mensagem "Link de definicao de senha invalido ou expirado" + + Cenario: Usuario nao define senha quando a confirmacao e diferente + Quando o usuario acessa o link valido de definicao de senha + E informa a nova senha "Senha@123" + E confirma a nova senha "OutraSenha@123" + E envia o formulario de definicao de senha + Entao o sistema nao deve salvar a senha do usuario + E deve exibir a mensagem "Confirmacao de senha nao confere" diff --git a/project/features/login.feature b/project/features/login.feature new file mode 100644 index 0000000000..cde880b5a1 --- /dev/null +++ b/project/features/login.feature @@ -0,0 +1,44 @@ +# language: pt +@auth @issue-104 +Funcionalidade: Sistema de Login + Como usuario cadastrado no CAMAAR + Quero acessar o sistema com email ou matricula e senha + Para visualizar as funcionalidades disponiveis ao meu perfil + + Contexto: + Dado que existe um administrador cadastrado com email "admin.cic@unb.br", matricula "000000001" e senha definida "Senha@123" + E que existe um participante cadastrado com email "acjpjvjp@gmail.com", matricula "190084006" e senha definida "Senha@123" + E que existe um participante cadastrado com email "andreCarvalhoroure@gmail.com", matricula "200033522" e sem senha definida + + Cenario: Administrador acessa com email e ve o menu administrativo + Quando o usuario acessa a pagina de login + E informa o identificador "admin.cic@unb.br" + E informa a senha "Senha@123" + E envia o formulario de login + Entao o sistema deve autenticar o administrador + E deve exibir o menu lateral com opcoes de gerenciamento + + Cenario: Participante acessa com matricula e ve o menu de participante + Quando o usuario acessa a pagina de login + E informa o identificador "190084006" + E informa a senha "Senha@123" + E envia o formulario de login + Entao o sistema deve autenticar o participante + E deve exibir o menu lateral com formularios pendentes + E nao deve exibir opcoes administrativas + + Cenario: Usuario nao acessa com senha incorreta + Quando o usuario acessa a pagina de login + E informa o identificador "acjpjvjp@gmail.com" + E informa a senha "SenhaErrada" + E envia o formulario de login + Entao o sistema nao deve autenticar o usuario + E deve exibir a mensagem "Email, matricula ou senha invalidos" + + Cenario: Usuario ainda sem senha definida nao consegue acessar + Quando o usuario acessa a pagina de login + E informa o identificador "200033522" + E informa a senha "Senha@123" + E envia o formulario de login + Entao o sistema nao deve autenticar o usuario + E deve informar que a senha inicial precisa ser definida antes do acesso diff --git a/project/features/redefinicao_de_senha.feature b/project/features/redefinicao_de_senha.feature new file mode 100644 index 0000000000..b2e79e159e --- /dev/null +++ b/project/features/redefinicao_de_senha.feature @@ -0,0 +1,38 @@ +# language: pt +@auth @issue-107 +Funcionalidade: Redefinicao de Senha + Como usuario cadastrado + Quero solicitar a redefinicao da minha senha por email + Para recuperar meu acesso ao CAMAAR + + Contexto: + Dado que existe um participante cadastrado com email "acjpjvjp@gmail.com", matricula "190084006" e senha definida "Senha@123" + + Cenario: Usuario redefine a senha com token valido + Quando o usuario acessa a pagina de esqueci minha senha + E informa o email "acjpjvjp@gmail.com" + E solicita a redefinicao de senha + Entao o sistema deve enviar um email com link de redefinicao para "acjpjvjp@gmail.com" + Quando o usuario acessa o link valido de redefinicao de senha + E informa a nova senha "NovaSenha@123" + E confirma a nova senha "NovaSenha@123" + E envia o formulario de redefinicao de senha + Entao o sistema deve atualizar a senha do usuario + E deve permitir login com a nova senha + + Cenario: Sistema nao revela se email inexistente esta cadastrado + Quando o usuario acessa a pagina de esqueci minha senha + E informa o email "naoexiste@unb.br" + E solicita a redefinicao de senha + Entao o sistema deve exibir uma mensagem generica de instrucao + E nao deve revelar se o email esta cadastrado + E nao deve criar token de redefinicao para usuario inexistente + + Cenario: Usuario nao redefine senha com token expirado + Dado que existe um link expirado de redefinicao de senha para "acjpjvjp@gmail.com" + Quando o usuario acessa o link expirado de redefinicao de senha + E informa a nova senha "NovaSenha@123" + E confirma a nova senha "NovaSenha@123" + E envia o formulario de redefinicao de senha + Entao o sistema nao deve atualizar a senha do usuario + E deve exibir a mensagem "Link de redefinicao de senha invalido ou expirado" From 11b2d8c098ff54c7c5c13883fa05607f8cbb6f8c Mon Sep 17 00:00:00 2001 From: Diego Gontijo Date: Mon, 25 May 2026 17:35:05 -0300 Subject: [PATCH 07/23] dependencies --- project/Gemfile.lock | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/project/Gemfile.lock b/project/Gemfile.lock index aa1ff85d5f..c8c2c9d4c8 100644 --- a/project/Gemfile.lock +++ b/project/Gemfile.lock @@ -153,6 +153,7 @@ GEM 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-x64-mingw-ucrt) ffi (1.17.4-x86_64-linux-gnu) ffi (1.17.4-x86_64-linux-musl) fugit (1.12.1) @@ -236,6 +237,8 @@ GEM racc (~> 1.4) nokogiri (1.19.3-arm-linux-musl) racc (~> 1.4) + nokogiri (1.19.3-x64-mingw-ucrt) + racc (~> 1.4) nokogiri (1.19.3-x86_64-linux-gnu) racc (~> 1.4) nokogiri (1.19.3-x86_64-linux-musl) @@ -369,6 +372,7 @@ GEM 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-x64-mingw-ucrt) sqlite3 (2.9.4-x86_64-linux-gnu) sqlite3 (2.9.4-x86_64-linux-musl) sshkit (1.25.0) @@ -384,6 +388,10 @@ GEM sys-uname (1.5.1) ffi (~> 1.1) memoist3 (~> 1.0.0) + sys-uname (1.5.1-universal-mingw32) + ffi (~> 1.1) + memoist3 (~> 1.0.0) + win32ole thor (1.5.0) thruster (0.1.21) thruster (0.1.21-aarch64-linux) @@ -395,6 +403,8 @@ GEM railties (>= 7.1.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) + tzinfo-data (1.2026.2) + tzinfo (>= 1.0.0) unicode-display_width (3.2.0) unicode-emoji (~> 4.1) unicode-emoji (4.2.0) @@ -409,6 +419,7 @@ GEM base64 websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) + win32ole (1.9.3) xpath (3.2.0) nokogiri (~> 1.8) zeitwerk (2.8.2) @@ -419,6 +430,7 @@ PLATFORMS aarch64-linux-musl arm-linux-gnu arm-linux-musl + x64-mingw-ucrt x86_64-linux x86_64-linux-gnu x86_64-linux-musl @@ -502,6 +514,7 @@ CHECKSUMS ffi (1.17.4-aarch64-linux-musl) sha256=9286b7a615f2676245283aef0a0a3b475ae3aae2bb5448baace630bb77b91f39 ffi (1.17.4-arm-linux-gnu) sha256=d6dbddf7cb77bf955411af5f187a65b8cd378cb003c15c05697f5feee1cb1564 ffi (1.17.4-arm-linux-musl) sha256=9d4838ded0465bef6e2426935f6bcc93134b6616785a84ffd2a3d82bc3cf6f95 + ffi (1.17.4-x64-mingw-ucrt) sha256=f6ff9618cfccc494138bddade27aa06c74c6c7bc367a1ea1103d80c2fcb9ed35 ffi (1.17.4-x86_64-linux-gnu) sha256=9d3db14c2eae074b382fa9c083fe95aec6e0a1451da249eab096c34002bc752d ffi (1.17.4-x86_64-linux-musl) sha256=3fdf9888483de005f8ef8d1cf2d3b20d86626af206cbf780f6a6a12439a9c49e fugit (1.12.1) sha256=5898f478ede9b415f0804e42b8f3fd53f814bd85eebffceebdbc34e1107aaf68 @@ -539,6 +552,7 @@ CHECKSUMS nokogiri (1.19.3-aarch64-linux-musl) sha256=8392dfdcd21be7a94dbbe9ccc138dea01b97b24cb2dc02a114ca98bfb1d9a0b7 nokogiri (1.19.3-arm-linux-gnu) sha256=3919d5ffc334ad778a4a9eb88fda7dcb8b1fb58c8a52ac640c6dcd2f038e774f nokogiri (1.19.3-arm-linux-musl) sha256=9ce1cb6346bb9c67b1550eb537aa183ead91e4b6eadb2f36ade02d8dd2a79fb6 + nokogiri (1.19.3-x64-mingw-ucrt) sha256=8bb7132cad356c879a1286eaabcb5e68326cb2490317984280fbc62f456d506a nokogiri (1.19.3-x86_64-linux-gnu) sha256=2f5078620fe12e83669b5b17311b32532a8153d02eee7ad06948b926d6080976 nokogiri (1.19.3-x86_64-linux-musl) sha256=248c906d2166eca5efb56d52fdee5f9a1f51d69a72e2b64fdac647b4ce39ea3f ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912 @@ -584,12 +598,14 @@ CHECKSUMS sqlite3 (2.9.4-aarch64-linux-musl) sha256=ffb4255947fb54c8c3eeca97460c9702b40de91ce390455ef7367ca6a3929a31 sqlite3 (2.9.4-arm-linux-gnu) sha256=9ee2008b9fbec984c3c165b0d7eedd2bd2a415100b761bfa3a4c6fbec9208bf6 sqlite3 (2.9.4-arm-linux-musl) sha256=8dc1fe4da6977992cd62decf4a93ccf6cc2e124a5e6a340160d52092f70e837a + sqlite3 (2.9.4-x64-mingw-ucrt) sha256=40997c549b19e2fdfcc5e271f6bdd4d502179742c0bfd678da23d0d09b929848 sqlite3 (2.9.4-x86_64-linux-gnu) sha256=537a3eda71b1df1336d0055cbebe55a7317c34870c192c7b6b9d8d0be6871847 sqlite3 (2.9.4-x86_64-linux-musl) sha256=3fc5e865b4be9a85d998203ef8d0c0fdcb92f20acf34a254346ff8a19088efec sshkit (1.25.0) sha256=c8c6543cdb60f91f1d277306d585dd11b6a064cb44eab0972827e4311ff96744 stimulus-rails (1.3.4) sha256=765676ffa1f33af64ce026d26b48e8ffb2e0b94e0f50e9119e11d6107d67cb06 stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1 sys-uname (1.5.1) sha256=784d7e6491b0393c25cbbe5ac38324ac7be9fda083a6094832648af669386d7b + sys-uname (1.5.1-universal-mingw32) sha256=aceb618e3276da5eae0ce368e9f6fae8c1f3e9ef23a0595cb88db7b6ecd45f62 thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73 thruster (0.1.21) sha256=dc67928f36e5894844579a95e45637a5091db7a7ea05468ee8c2c6eb0a3f77cf thruster (0.1.21-aarch64-linux) sha256=f5aff78fb7a6431ed3d6ab4bde03a89c461e9a73981dbc97d6990d85c3db235c @@ -598,6 +614,7 @@ CHECKSUMS tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f turbo-rails (2.0.23) sha256=ee0d90733aafff056cf51ff11e803d65e43cae258cc55f6492020ec1f9f9315f tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b + tzinfo-data (1.2026.2) sha256=7db0d3d3d53b8d7601fc183fccc8c6d056a3004e14eb59ea995bf6aec4ae10bc unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42 unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6 @@ -606,6 +623,7 @@ CHECKSUMS websocket (1.2.11) sha256=b7e7a74e2410b5e85c25858b26b3322f29161e300935f70a0e0d3c35e0462737 websocket-driver (0.8.0) sha256=ed0dba4b943c22f17f9a734817e808bc84cdce6a7e22045f5315aa57676d4962 websocket-extensions (0.1.5) sha256=1c6ba63092cda343eb53fc657110c71c754c56484aad42578495227d717a8241 + win32ole (1.9.3) sha256=01f43dc5dc13806e6e58204f538b4a28f3d85968ea89074abc9a3cd118e94d96 xpath (3.2.0) sha256=6dfda79d91bb3b949b947ecc5919f042ef2f399b904013eb3ef6d20dd3a4082e zeitwerk (2.8.2) sha256=7212a61311083c604184b1ea2574b9aa05cd14f855a0841c06985cabe9181d12 From 601f7d61ff7e1f757c6658a3c75b3360f00a4d30 Mon Sep 17 00:00:00 2001 From: Rafael Dias Ghiorzi Date: Mon, 25 May 2026 18:02:49 -0300 Subject: [PATCH 08/23] Feat: Adicionar BDDs de admin - templates --- .../features/admin_criar_formulario.feature | 28 +++++++++++++++++ project/features/admin_criar_template.feature | 30 +++++++++++++++++++ .../admin_editar_deletar_templates.feature | 26 ++++++++++++++++ project/features/admin_ver_templates.feature | 17 +++++++++++ 4 files changed, 101 insertions(+) create mode 100644 project/features/admin_criar_formulario.feature create mode 100644 project/features/admin_criar_template.feature create mode 100644 project/features/admin_editar_deletar_templates.feature create mode 100644 project/features/admin_ver_templates.feature diff --git a/project/features/admin_criar_formulario.feature b/project/features/admin_criar_formulario.feature new file mode 100644 index 0000000000..e83278bf75 --- /dev/null +++ b/project/features/admin_criar_formulario.feature @@ -0,0 +1,28 @@ +# language: pt +Funcionalidade: Admin criar formulário a partir de um template para as turmas que escolher + + Eu como Administrador + Quero criar um formulário baseado em um template para as turmas que eu escolher + A fim de avaliar o desempenho das turmas no semestre atual + + Cenário: Administrador cria formulário com sucesso + Dado que sou administrador + E existe pelo menos um template criado por mim + E existe pelo menos uma turma cadastrada + Quando eu preencher o nome do formulário + E selecionar o template e a turma desejada + E clicar no botão 'Enviar' + Então devo ver uma mensagem dizendo 'Formulário gerado com sucesso!' + E as questões do template devem ser clonadas para o novo formulário + + Cenário: Administrador tenta criar formulário sem selecionar um template + Dado que sou um administrador + E estou criando um formulário + Quando eu tentar criar um formulário sem selecionar nenhum template + Então o sistema deve exibir uma mensagem dizendo 'É necessário escolher um template base' + + Cenário: Administrador tenta criar formulário sem selecionar nenhuma turma + Dado que sou um administrador + E estou criando um formulário + Quando eu tentar criar um formulário sem selecionar nenhuma turma + Então o sistema deve exibir uma mensagem dizendo 'É necessário escolher ao menos uma turma' \ No newline at end of file diff --git a/project/features/admin_criar_template.feature b/project/features/admin_criar_template.feature new file mode 100644 index 0000000000..48e8971eb4 --- /dev/null +++ b/project/features/admin_criar_template.feature @@ -0,0 +1,30 @@ +# language: pt +Funcionalidade: Admin cria template para formulários + + Eu como Administrador + Quero criar um template de formulário contendo as questões do formulário + A fim de gerar formulários de avaliações para avaliar o desempenho das turmas + + Cenário: Admin cria template com uma questão com sucesso + Dado que estou na página de criar template + Quando eu preencho o campo 'Nome do template:' + E clico no botão '+' + E seleciono o 'Público-alvo:' + E preencho o campo 'Enunciado da questão:' + E clico no botão 'Criar' + Então o novo template deve aparecer na tela de meus templates + + Cenário: Admin tenta criar template sem preencher o nome + Dado que estou na página de criar template + Quando eu deixo o campo 'Nome do template:' vazio + E adiciono uma questão válida + E clico no botão 'Criar' + Então deve aparecer uma mensagem 'O nome do template é obrigatório' + + Cenário: Admin tenta adicionar questão sem preencher o enunciado + Dado que estou na página de criar template + Quando eu preencho o campo 'Nome do template:' + E clico no botão '+' + E não preencho o campo 'Enunciado da questão:' + E clico no botão 'Criar' + Então deve aparecer uma mensagem 'Questão não possui enunciado' \ No newline at end of file diff --git a/project/features/admin_editar_deletar_templates.feature b/project/features/admin_editar_deletar_templates.feature new file mode 100644 index 0000000000..f7d0dd384d --- /dev/null +++ b/project/features/admin_editar_deletar_templates.feature @@ -0,0 +1,26 @@ +# language: pt +Funcionalidade: Admin editar e deletar templates criados + + Como Administrador + Quero editar e/ou deletar um template que eu criei sem afetar os formulários já criados + A fim de organizar os templates existentes + + Cenário: Edição de um template criado mantendo isolamento + Dado que eu editei informações de um template existente + Quando eu pressionar o botão "Confirmar edição" na tela de edição + Então eu devo visualizar as alterações no template na tela de templates + E os formulários criados usando a versão antiga não devem sofrer alterações nas suas questões + + Cenário: Deleção de um template criado + Dado que eu escolhi um template na minha lista + Quando eu pressionar o botão de "Deletar" + E confirmar a deleção pressionando o botão "Confirmar" + Então esse template não deve aparecer mais na tela de templates + E os formulários gerados por ele devem permanecer intactos no sistema + + Cenário: Tentar adicionar questão com campo vazio durante edição + Dado que estou na página de edição do template + Quando eu clico no botão '+' + E não preencho o campo 'Enunciado da questão:' + E clico no botão 'Confirmar edição' + Então deve aparecer uma mensagem 'A nova questão não possui enunciado' \ No newline at end of file diff --git a/project/features/admin_ver_templates.feature b/project/features/admin_ver_templates.feature new file mode 100644 index 0000000000..4dc52fd3dd --- /dev/null +++ b/project/features/admin_ver_templates.feature @@ -0,0 +1,17 @@ +# language: pt +Funcionalidade: Administrador gerenciar templates criados + + Como Administrador + Quero visualizar os templates criados + A fim de poder editar e/ou deletar um template que eu criei + + Cenário: Visualização dos templates criados + Dado que o administrador está na interface de templates + Quando o sistema carrega os templates + Então ele deverá ver somente os templates criados por ele + E deverá ter a opção de deletar ou editar esses templates + + Cenário: Tentativa de visualizar templates criados por outro administrador + Dado que o administrador está na interface de templates + Quando ele tenta acessar diretamente um template criado por outro administrador + Então ele deverá ver uma mensagem de erro de permissão "Você não tem acesso a esse template" \ No newline at end of file From 41ba6953a03ab9ca4206f98894cafc67ffdf86da Mon Sep 17 00:00:00 2001 From: lfducat <25991752+lfducat@users.noreply.github.com> Date: Tue, 26 May 2026 18:17:20 -0300 Subject: [PATCH 09/23] feat: adicionar BDDs --- .../features/admin_criar_formulario.feature | 63 ++++++++++--------- .../features/admin_gerenciar_turmas.feature | 26 ++++++++ .../admin_importar_dados_sigaa.feature | 23 +++++++ 3 files changed, 84 insertions(+), 28 deletions(-) create mode 100644 project/features/admin_gerenciar_turmas.feature create mode 100644 project/features/admin_importar_dados_sigaa.feature diff --git a/project/features/admin_criar_formulario.feature b/project/features/admin_criar_formulario.feature index e83278bf75..62ef732c42 100644 --- a/project/features/admin_criar_formulario.feature +++ b/project/features/admin_criar_formulario.feature @@ -1,28 +1,35 @@ -# language: pt -Funcionalidade: Admin criar formulário a partir de um template para as turmas que escolher - - Eu como Administrador - Quero criar um formulário baseado em um template para as turmas que eu escolher - A fim de avaliar o desempenho das turmas no semestre atual - - Cenário: Administrador cria formulário com sucesso - Dado que sou administrador - E existe pelo menos um template criado por mim - E existe pelo menos uma turma cadastrada - Quando eu preencher o nome do formulário - E selecionar o template e a turma desejada - E clicar no botão 'Enviar' - Então devo ver uma mensagem dizendo 'Formulário gerado com sucesso!' - E as questões do template devem ser clonadas para o novo formulário - - Cenário: Administrador tenta criar formulário sem selecionar um template - Dado que sou um administrador - E estou criando um formulário - Quando eu tentar criar um formulário sem selecionar nenhum template - Então o sistema deve exibir uma mensagem dizendo 'É necessário escolher um template base' - - Cenário: Administrador tenta criar formulário sem selecionar nenhuma turma - Dado que sou um administrador - E estou criando um formulário - Quando eu tentar criar um formulário sem selecionar nenhuma turma - Então o sistema deve exibir uma mensagem dizendo 'É necessário escolher ao menos uma turma' \ No newline at end of file +# language: pt +Funcionalidade: Criação de Formulário de Avaliação de Turma + + Como um Administrador + Quero escolher criar um formulário para os docentes ou os discentes de uma turma + A fim de avaliar o desempenho de uma matéria + + Cenário: Criação de formulário com sucesso para discentes + Dado que eu acesse a tela de "Criação de Formulários" + Quando eu selecionar a matéria "Cálculo 1" e a turma "1" + E definir o público-alvo como "Discentes" + E preencher as perguntas do formulário de avaliação + E clicar em "Publicar Formulário" + Então o sistema deve salvar o formulário com sucesso + E exibir a mensagem "Formulário de avaliação para discentes publicado com sucesso." + E o formulário deve ficar disponível para os alunos da turma "1" responderem + + Cenário: Tentativa de publicação de formulário sem selecionar o público-alvo + Dado que eu acesse a tela de "Criação de Formulários" + Quando eu selecionar a matéria "Cálculo 1" e a turma "1" + E deixar o campo de público-alvo em branco + E preencher as perguntas do formulário de avaliação + E clicar em "Publicar Formulário" + Então o sistema não deve permitir a publicação + E deve exibir um alerta impeditivo "Por favor, selecione se o formulário é para Docentes ou Discentes." + + Cenário: Tentativa de criação de formulário sem preencher as perguntas + Dado que eu acesse a tela de "Criação de Formulários" + Quando eu selecionar a matéria Cálculo 2" e a turma "3" + E definir o público-alvo como "Docentes" + E não adicionar nenhuma pergunta ao formulário + E clicar em "Publicar Formulário" + Então o sistema deve bloquear a ação + E exibir a mensagem de erro "O formulário não pode ser publicado sem perguntas cadastradas." + diff --git a/project/features/admin_gerenciar_turmas.feature b/project/features/admin_gerenciar_turmas.feature new file mode 100644 index 0000000000..1ae1336648 --- /dev/null +++ b/project/features/admin_gerenciar_turmas.feature @@ -0,0 +1,26 @@ +# 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 + + Cenário: Listagem de turmas filtrada com sucesso pelo departamento do Administrador + Dado que eu acesse a tela de "Gerenciamento de Turmas" + Quando o sistema carregar a página para o semestre atual + Então eu devo visualizar apenas as turmas pertencentes ao "Departamento de Ciências da Computação" + E não devo visualizar turmas de outros departamentos (como "Matemática") + E o sistema deve liberar as opções de avaliação de desempenho para as turmas listadas + + Cenário: Tentativa de acesso direto a uma turma de outro departamento via URL/ID + Dado que eu tente acessar diretamente o painel da turma "Cálculo 1", que pertence ao "Departamento de Matemática" + Quando o sistema validar minhas permissões de Administrador + Então o sistema deve bloquear o meu acesso + E deve redirecionar-me para a página inicial de gerenciamento + E exibir a mensagem de erro: "Acesso negado. Você só pode gerenciar turmas do seu próprio departamento." + + Cenário: Administrador acessa o gerenciamento mas seu departamento não possui turmas no semestre atual + Dado que o "Departamento de Ciências da Computação" não tenha nenhuma turma ofertada ou ativa no semestre atual + Quando eu acessar a tela de "Gerenciamento de Turmas" + Então o sistema deve carregar a página sem registros de turmas + E deve exibir a mensagem informativa: "Nenhuma turma encontrada para o seu departamento no semestre atual." \ No newline at end of file diff --git a/project/features/admin_importar_dados_sigaa.feature b/project/features/admin_importar_dados_sigaa.feature new file mode 100644 index 0000000000..0c8cbb47ed --- /dev/null +++ b/project/features/admin_importar_dados_sigaa.feature @@ -0,0 +1,23 @@ +# language: pt +Funcionalidade: Importação de Dados do SIGAA + + Como um Administrador + Quero importar dados de turmas, matérias e participantes do SIGAA + A fim de alimentar a base de dados do sistema + + Cenário: Sincronização de dados realizada com sucesso (inserção e atualização) + Dado que eu acesse a tela de "Importação de Dados do SIGAA" + Quando eu selecionar o período letivo desejado + E solicitar a sincronização de "Turmas, Matérias e Participantes" + Então o sistema deve consultar o SIGAA (arquivos no repositório original) + E deve inserir os novos registros identificados + E deve atualizar os dados dos registros que sofreram alterações no SIGAA + E exibir a mensagem: "Sincronização concluída. X novos registros adicionados e Y registros atualizados." + + Cenário: Tentativa de importação quando todos os dados já estão atualizados + Dado que todos os dados do SIGAA para o período atual já tenham sido importados previamente + E eu acesse a tela de "Importação de Dados do SIGAA" + Quando eu solicitar a sincronização dos dados + Então o sistema deve verificar a base de dados + E constatar que não há novos registros para inserir + E deve finalizar o processo exibindo a mensagem: "A base de dados já está atualizada com o SIGAA para este período." \ No newline at end of file From 718c242ce53f254161944d3fe67e21e3e73c13ec Mon Sep 17 00:00:00 2001 From: lfducat <25991752+lfducat@users.noreply.github.com> Date: Tue, 26 May 2026 18:31:30 -0300 Subject: [PATCH 10/23] fix: arrumar conflito de nomes em features --- .../features/admin_criar_formulario.feature | 53 ++++++++----------- .../admin_criar_formulario_avaliacao.feature | 35 ++++++++++++ 2 files changed, 58 insertions(+), 30 deletions(-) create mode 100644 project/features/admin_criar_formulario_avaliacao.feature diff --git a/project/features/admin_criar_formulario.feature b/project/features/admin_criar_formulario.feature index 62ef732c42..d8a653e471 100644 --- a/project/features/admin_criar_formulario.feature +++ b/project/features/admin_criar_formulario.feature @@ -1,35 +1,28 @@ # language: pt -Funcionalidade: Criação de Formulário de Avaliação de Turma +Funcionalidade: Admin criar formulário a partir de um template para as turmas que escolher - Como um Administrador - Quero escolher criar um formulário para os docentes ou os discentes de uma turma - A fim de avaliar o desempenho de uma matéria + Eu como Administrador + Quero criar um formulário baseado em um template para as turmas que eu escolher + A fim de avaliar o desempenho das turmas no semestre atual - Cenário: Criação de formulário com sucesso para discentes - Dado que eu acesse a tela de "Criação de Formulários" - Quando eu selecionar a matéria "Cálculo 1" e a turma "1" - E definir o público-alvo como "Discentes" - E preencher as perguntas do formulário de avaliação - E clicar em "Publicar Formulário" - Então o sistema deve salvar o formulário com sucesso - E exibir a mensagem "Formulário de avaliação para discentes publicado com sucesso." - E o formulário deve ficar disponível para os alunos da turma "1" responderem + Cenário: Administrador cria formulário com sucesso + Dado que sou administrador + E existe pelo menos um template criado por mim + E existe pelo menos uma turma cadastrada + Quando eu preencher o nome do formulário + E selecionar o template e a turma desejada + E clicar no botão 'Enviar' + Então devo ver uma mensagem dizendo 'Formulário gerado com sucesso!' + E as questões do template devem ser clonadas para o novo formulário - Cenário: Tentativa de publicação de formulário sem selecionar o público-alvo - Dado que eu acesse a tela de "Criação de Formulários" - Quando eu selecionar a matéria "Cálculo 1" e a turma "1" - E deixar o campo de público-alvo em branco - E preencher as perguntas do formulário de avaliação - E clicar em "Publicar Formulário" - Então o sistema não deve permitir a publicação - E deve exibir um alerta impeditivo "Por favor, selecione se o formulário é para Docentes ou Discentes." - - Cenário: Tentativa de criação de formulário sem preencher as perguntas - Dado que eu acesse a tela de "Criação de Formulários" - Quando eu selecionar a matéria Cálculo 2" e a turma "3" - E definir o público-alvo como "Docentes" - E não adicionar nenhuma pergunta ao formulário - E clicar em "Publicar Formulário" - Então o sistema deve bloquear a ação - E exibir a mensagem de erro "O formulário não pode ser publicado sem perguntas cadastradas." + Cenário: Administrador tenta criar formulário sem selecionar um template + Dado que sou um administrador + E estou criando um formulário + Quando eu tentar criar um formulário sem selecionar nenhum template + Então o sistema deve exibir uma mensagem dizendo 'É necessário escolher um template base' + Cenário: Administrador tenta criar formulário sem selecionar nenhuma turma + Dado que sou um administrador + E estou criando um formulário + Quando eu tentar criar um formulário sem selecionar nenhuma turma + Então o sistema deve exibir uma mensagem dizendo 'É necessário escolher ao menos uma turma' \ No newline at end of file diff --git a/project/features/admin_criar_formulario_avaliacao.feature b/project/features/admin_criar_formulario_avaliacao.feature new file mode 100644 index 0000000000..cd6b1ca50e --- /dev/null +++ b/project/features/admin_criar_formulario_avaliacao.feature @@ -0,0 +1,35 @@ +# language: pt +Funcionalidade: Criação de Formulário de Avaliação de Turma + + Como um Administrador + Quero escolher criar um formulário para os docentes ou os discentes de uma turma + A fim de avaliar o desempenho de uma matéria + + Cenário: Criação de formulário com sucesso para discentes + Dado que eu acesse a tela de "Criação de Formulários" + Quando eu selecionar a matéria "Cálculo 1" e a turma "1" + E definir o público-alvo como "Discentes" + E preencher as perguntas do formulário de avaliação + E clicar em "Publicar Formulário" + Então o sistema deve salvar o formulário com sucesso + E exibir a mensagem "Formulário de avaliação para discentes publicado com sucesso." + E o formulário deve ficar disponível para os alunos da turma "1" responderem + + Cenário: Tentativa de publicação de formulário sem selecionar o público-alvo + Dado que eu acesse a tela de "Criação de Formulários" + Quando eu selecionar a matéria "Cálculo 1" e a turma "1" + E deixar o campo de público-alvo em branco + E preencher as perguntas do formulário de avaliação + E clicar em "Publicar Formulário" + Então o sistema não deve permitir a publicação + E deve exibir um alerta impeditivo "Por favor, selecione se o formulário é para Docentes ou Discentes." + + Cenário: Tentativa de criação de formulário sem preencher as perguntas + Dado que eu acesse a tela de "Criação de Formulários" + Quando eu selecionar a matéria Cálculo 2" e a turma "3" + E definir o público-alvo como "Docentes" + E não adicionar nenhuma pergunta ao formulário + E clicar em "Publicar Formulário" + Então o sistema deve bloquear a ação + E exibir a mensagem de erro "O formulário não pode ser publicado sem perguntas cadastradas." + From af00edca6bb47b89106c91c2d611b5cc67d8872b Mon Sep 17 00:00:00 2001 From: Gabriel Lopes Soares Damasceno Date: Tue, 26 May 2026 21:45:43 -0300 Subject: [PATCH 11/23] feat: BDDs de participante e exportacao de resultados --- .../admin_baixar_resultados_csv.feature | 22 +++++++++++++++++++ .../admin_ver_formularios_criados.feature | 20 +++++++++++++++++ ...articipante_responder_questionario.feature | 22 +++++++++++++++++++ ...te_ver_formularios_nao_respondidos.feature | 20 +++++++++++++++++ 4 files changed, 84 insertions(+) create mode 100644 project/features/admin_baixar_resultados_csv.feature create mode 100644 project/features/admin_ver_formularios_criados.feature create mode 100644 project/features/participante_responder_questionario.feature create mode 100644 project/features/participante_ver_formularios_nao_respondidos.feature diff --git a/project/features/admin_baixar_resultados_csv.feature b/project/features/admin_baixar_resultados_csv.feature new file mode 100644 index 0000000000..5755748657 --- /dev/null +++ b/project/features/admin_baixar_resultados_csv.feature @@ -0,0 +1,22 @@ +# language: pt +Funcionalidade: Exportação de resultados de formulário em CSV + + Como um Administrador + Quero baixar um arquivo csv contendo os resultados de um formulário + A fim de avaliar o desempenho das turmas + + Cenário: Download de resultados em CSV com sucesso + Dado que estou logado no sistema como "Administrador" + E que estou na página de visualização de resultados da turma "Estrutura de Dados" + Quando o formulário selecionado já possui respostas submetidas + E eu clico no botão "Exportar Resultados (CSV)" + Então o sistema deve gerar um arquivo ".csv" com todas as respostas estruturadas + E o download do arquivo deve iniciar automaticamente no meu navegador + + Cenário: Tentativa de download de CSV de um formulário sem respostas + Dado que estou logado no sistema como "Administrador" + E que estou na página de visualização de resultados de um formulário recém-criado + Quando o formulário selecionado ainda não possui nenhuma resposta de participante + E eu clico no botão "Exportar Resultados (CSV)" + Então o sistema não deve gerar o arquivo + E deve exibir um alerta impeditivo "Não é possível exportar o relatório pois nenhuma resposta foi registrada. Aguarde os participantes responderem." \ No newline at end of file diff --git a/project/features/admin_ver_formularios_criados.feature b/project/features/admin_ver_formularios_criados.feature new file mode 100644 index 0000000000..1750b56703 --- /dev/null +++ b/project/features/admin_ver_formularios_criados.feature @@ -0,0 +1,20 @@ +# language: pt +Funcionalidade: Visualização dos formulários criados + + Como um Administrador + Quero visualizar os formulários criados + A fim de poder gerar um relatório a partir das respostas + + Cenário: Visualização de lista de formulários com sucesso + Dado que estou logado no sistema como "Administrador" + Quando eu clico na aba de "Meus Formulários" no menu lateral + E existem formulários previamente criados por mim no sistema + Então eu devo ver uma lista com todos os meus formulários publicados + E a lista deve exibir o título, a turma e o status de cada formulário + + Cenário: Visualização de lista quando nenhum formulário foi criado + Dado que estou logado no sistema como "Administrador" + Quando eu clico na aba de "Meus Formulários" no menu lateral + E eu ainda não criei nenhum formulário no sistema + Então a lista de formulários deve aparecer vazia + E o sistema deve exibir a mensagem "Você ainda não possui formulários criados para as suas turmas." \ No newline at end of file diff --git a/project/features/participante_responder_questionario.feature b/project/features/participante_responder_questionario.feature new file mode 100644 index 0000000000..b60f426cba --- /dev/null +++ b/project/features/participante_responder_questionario.feature @@ -0,0 +1,22 @@ +# language: pt + +Funcionalidade: Responder questionário sobre a turma + Como um 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 + + Cenário: Envio de questionário com sucesso (Caminho Feliz) + Dado que estou autenticado no sistema como participante + E que estou na página do formulário não respondido da minha turma + Quando eu preencho todas as avaliações corretamente + E clico no botão de submeter avaliação + Então o sistema deve salvar os "dados_resposta" vinculados ao meu usuário + E eu devo ser redirecionado com uma mensagem "Avaliação submetida com sucesso" + + Cenário: Tentativa de envio com dados em branco (Caminho Triste) + Dado que estou autenticado no sistema como participante + E que estou na página do formulário não respondido da minha turma + Quando eu deixo questões obrigatórias em branco + E clico no botão de submeter avaliação + Então o sistema não deve processar o envio + E eu devo ver a mensagem de erro "Por favor, preencha todas as questões obrigatórias" \ No newline at end of file diff --git a/project/features/participante_ver_formularios_nao_respondidos.feature b/project/features/participante_ver_formularios_nao_respondidos.feature new file mode 100644 index 0000000000..3a20ec31c4 --- /dev/null +++ b/project/features/participante_ver_formularios_nao_respondidos.feature @@ -0,0 +1,20 @@ +# language: pt +Funcionalidade: Visualização de formulários pendentes do participante + + 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 + + Cenário: Participante possui formulários pendentes para responder + Dado que estou logado no sistema como "Participante" + Quando eu acesso a página inicial do meu painel (Dashboard) + E existem formulários ativos das minhas turmas que eu ainda não respondi + Então o sistema deve listar os formulários pendentes na seção "Avaliações Pendentes" + E cada item da lista deve ter um botão "Responder Avaliação" + + Cenário: Participante já respondeu todos os formulários + Dado que estou logado no sistema como "Participante" + Quando eu acesso a página inicial do meu painel (Dashboard) + E eu já respondi a todos os formulários ativos das minhas turmas + Então a seção "Avaliações Pendentes" não deve listar nenhum formulário + E o sistema deve exibir a mensagem "Você não possui nenhuma avaliação pendente no momento." \ No newline at end of file From 02f7adad1e9711b581b36a004028c2b457a35805 Mon Sep 17 00:00:00 2001 From: rafaelghiorzi Date: Thu, 11 Jun 2026 11:10:09 -0300 Subject: [PATCH 12/23] feat: criando banco de dados --- project/Gemfile | 2 +- project/Gemfile.lock | 3 + project/app/controllers/admins_controller.rb | 70 +++++++++ project/app/controllers/alunos_controller.rb | 70 +++++++++ .../controllers/departamentos_controller.rb | 70 +++++++++ .../app/controllers/formularios_controller.rb | 70 +++++++++ .../app/controllers/professors_controller.rb | 70 +++++++++ .../app/controllers/questaos_controller.rb | 70 +++++++++ .../app/controllers/resposta_controller.rb | 70 +++++++++ .../app/controllers/submissaos_controller.rb | 70 +++++++++ .../app/controllers/templates_controller.rb | 70 +++++++++ project/app/controllers/turmas_controller.rb | 70 +++++++++ project/app/helpers/admins_helper.rb | 2 + project/app/helpers/alunos_helper.rb | 2 + project/app/helpers/departamentos_helper.rb | 2 + project/app/helpers/formularios_helper.rb | 2 + project/app/helpers/professors_helper.rb | 2 + project/app/helpers/questaos_helper.rb | 2 + project/app/helpers/resposta_helper.rb | 2 + project/app/helpers/submissaos_helper.rb | 2 + project/app/helpers/templates_helper.rb | 2 + project/app/helpers/turmas_helper.rb | 2 + project/app/models/admin.rb | 3 + project/app/models/aluno.rb | 6 + project/app/models/departamento.rb | 5 + project/app/models/formulario.rb | 5 + project/app/models/professor.rb | 6 + project/app/models/questao.rb | 6 + project/app/models/respostum.rb | 4 + project/app/models/submissao.rb | 4 + project/app/models/template.rb | 3 + project/app/models/turma.rb | 7 + project/app/models/turma_aluno.rb | 4 + project/app/views/admins/_admin.html.erb | 27 ++++ project/app/views/admins/_admin.json.jbuilder | 2 + project/app/views/admins/_form.html.erb | 42 +++++ project/app/views/admins/edit.html.erb | 12 ++ project/app/views/admins/index.html.erb | 16 ++ project/app/views/admins/index.json.jbuilder | 1 + project/app/views/admins/new.html.erb | 11 ++ project/app/views/admins/show.html.erb | 10 ++ project/app/views/admins/show.json.jbuilder | 1 + project/app/views/alunos/_aluno.html.erb | 27 ++++ project/app/views/alunos/_aluno.json.jbuilder | 2 + project/app/views/alunos/_form.html.erb | 42 +++++ project/app/views/alunos/edit.html.erb | 12 ++ project/app/views/alunos/index.html.erb | 16 ++ project/app/views/alunos/index.json.jbuilder | 1 + project/app/views/alunos/new.html.erb | 11 ++ project/app/views/alunos/show.html.erb | 10 ++ project/app/views/alunos/show.json.jbuilder | 1 + .../departamentos/_departamento.html.erb | 12 ++ .../departamentos/_departamento.json.jbuilder | 2 + .../app/views/departamentos/_form.html.erb | 27 ++++ project/app/views/departamentos/edit.html.erb | 12 ++ .../app/views/departamentos/index.html.erb | 16 ++ .../views/departamentos/index.json.jbuilder | 1 + project/app/views/departamentos/new.html.erb | 11 ++ project/app/views/departamentos/show.html.erb | 10 ++ .../views/departamentos/show.json.jbuilder | 1 + project/app/views/formularios/_form.html.erb | 47 ++++++ .../views/formularios/_formulario.html.erb | 32 ++++ .../formularios/_formulario.json.jbuilder | 2 + project/app/views/formularios/edit.html.erb | 12 ++ project/app/views/formularios/index.html.erb | 16 ++ .../app/views/formularios/index.json.jbuilder | 1 + project/app/views/formularios/new.html.erb | 11 ++ project/app/views/formularios/show.html.erb | 10 ++ .../app/views/formularios/show.json.jbuilder | 1 + project/app/views/professors/_form.html.erb | 47 ++++++ .../app/views/professors/_professor.html.erb | 32 ++++ .../views/professors/_professor.json.jbuilder | 2 + project/app/views/professors/edit.html.erb | 12 ++ project/app/views/professors/index.html.erb | 16 ++ .../app/views/professors/index.json.jbuilder | 1 + project/app/views/professors/new.html.erb | 11 ++ project/app/views/professors/show.html.erb | 10 ++ .../app/views/professors/show.json.jbuilder | 1 + project/app/views/questaos/_form.html.erb | 37 +++++ project/app/views/questaos/_questao.html.erb | 22 +++ .../app/views/questaos/_questao.json.jbuilder | 2 + project/app/views/questaos/edit.html.erb | 12 ++ project/app/views/questaos/index.html.erb | 16 ++ .../app/views/questaos/index.json.jbuilder | 1 + project/app/views/questaos/new.html.erb | 11 ++ project/app/views/questaos/show.html.erb | 10 ++ project/app/views/questaos/show.json.jbuilder | 1 + project/app/views/resposta/_form.html.erb | 37 +++++ .../app/views/resposta/_respostum.html.erb | 22 +++ .../views/resposta/_respostum.json.jbuilder | 2 + project/app/views/resposta/edit.html.erb | 12 ++ project/app/views/resposta/index.html.erb | 16 ++ .../app/views/resposta/index.json.jbuilder | 1 + project/app/views/resposta/new.html.erb | 11 ++ project/app/views/resposta/show.html.erb | 10 ++ project/app/views/resposta/show.json.jbuilder | 1 + project/app/views/submissaos/_form.html.erb | 27 ++++ .../app/views/submissaos/_submissao.html.erb | 12 ++ .../views/submissaos/_submissao.json.jbuilder | 2 + project/app/views/submissaos/edit.html.erb | 12 ++ project/app/views/submissaos/index.html.erb | 16 ++ .../app/views/submissaos/index.json.jbuilder | 1 + project/app/views/submissaos/new.html.erb | 11 ++ project/app/views/submissaos/show.html.erb | 10 ++ .../app/views/submissaos/show.json.jbuilder | 1 + project/app/views/templates/_form.html.erb | 32 ++++ .../app/views/templates/_template.html.erb | 17 ++ .../views/templates/_template.json.jbuilder | 2 + project/app/views/templates/edit.html.erb | 12 ++ project/app/views/templates/index.html.erb | 16 ++ .../app/views/templates/index.json.jbuilder | 1 + project/app/views/templates/new.html.erb | 11 ++ project/app/views/templates/show.html.erb | 10 ++ .../app/views/templates/show.json.jbuilder | 1 + project/app/views/turmas/_form.html.erb | 52 +++++++ project/app/views/turmas/_turma.html.erb | 37 +++++ project/app/views/turmas/_turma.json.jbuilder | 2 + project/app/views/turmas/edit.html.erb | 12 ++ project/app/views/turmas/index.html.erb | 16 ++ project/app/views/turmas/index.json.jbuilder | 1 + project/app/views/turmas/new.html.erb | 11 ++ project/app/views/turmas/show.html.erb | 10 ++ project/app/views/turmas/show.json.jbuilder | 1 + project/config/routes.rb | 10 ++ .../20260611134150_create_departamentos.rb | 10 ++ .../migrate/20260611134210_create_alunos.rb | 13 ++ .../migrate/20260611134235_create_admins.rb | 13 ++ .../20260611134249_create_professors.rb | 14 ++ .../migrate/20260611134304_create_turmas.rb | 15 ++ .../20260611134355_create_turma_alunos.rb | 10 ++ .../20260611134403_create_templates.rb | 11 ++ .../20260611134426_create_formularios.rb | 14 ++ .../migrate/20260611134433_create_questaos.rb | 12 ++ .../20260611134446_create_submissaos.rb | 10 ++ .../migrate/20260611134500_create_resposta.rb | 12 ++ project/db/schema.rb | 147 ++++++++++++++++++ .../controllers/admins_controller_test.rb | 48 ++++++ .../controllers/alunos_controller_test.rb | 48 ++++++ .../departamentos_controller_test.rb | 48 ++++++ .../formularios_controller_test.rb | 48 ++++++ .../controllers/professors_controller_test.rb | 48 ++++++ .../controllers/questaos_controller_test.rb | 48 ++++++ .../controllers/resposta_controller_test.rb | 48 ++++++ .../controllers/submissaos_controller_test.rb | 48 ++++++ .../controllers/templates_controller_test.rb | 48 ++++++ .../controllers/turmas_controller_test.rb | 48 ++++++ project/test/fixtures/admins.yml | 15 ++ project/test/fixtures/alunos.yml | 15 ++ project/test/fixtures/departamentos.yml | 9 ++ project/test/fixtures/formularios.yml | 17 ++ project/test/fixtures/professors.yml | 17 ++ project/test/fixtures/questaos.yml | 13 ++ project/test/fixtures/resposta.yml | 13 ++ project/test/fixtures/submissaos.yml | 11 ++ project/test/fixtures/templates.yml | 11 ++ project/test/fixtures/turma_alunos.yml | 9 ++ project/test/fixtures/turmas.yml | 19 +++ project/test/models/admin_test.rb | 7 + project/test/models/aluno_test.rb | 7 + project/test/models/departamento_test.rb | 7 + project/test/models/formulario_test.rb | 7 + project/test/models/professor_test.rb | 7 + project/test/models/questao_test.rb | 7 + project/test/models/respostum_test.rb | 7 + project/test/models/submissao_test.rb | 7 + project/test/models/template_test.rb | 7 + project/test/models/turma_aluno_test.rb | 7 + project/test/models/turma_test.rb | 7 + 168 files changed, 2934 insertions(+), 1 deletion(-) create mode 100644 project/app/controllers/admins_controller.rb create mode 100644 project/app/controllers/alunos_controller.rb create mode 100644 project/app/controllers/departamentos_controller.rb create mode 100644 project/app/controllers/formularios_controller.rb create mode 100644 project/app/controllers/professors_controller.rb create mode 100644 project/app/controllers/questaos_controller.rb create mode 100644 project/app/controllers/resposta_controller.rb create mode 100644 project/app/controllers/submissaos_controller.rb create mode 100644 project/app/controllers/templates_controller.rb create mode 100644 project/app/controllers/turmas_controller.rb create mode 100644 project/app/helpers/admins_helper.rb create mode 100644 project/app/helpers/alunos_helper.rb create mode 100644 project/app/helpers/departamentos_helper.rb create mode 100644 project/app/helpers/formularios_helper.rb create mode 100644 project/app/helpers/professors_helper.rb create mode 100644 project/app/helpers/questaos_helper.rb create mode 100644 project/app/helpers/resposta_helper.rb create mode 100644 project/app/helpers/submissaos_helper.rb create mode 100644 project/app/helpers/templates_helper.rb create mode 100644 project/app/helpers/turmas_helper.rb create mode 100644 project/app/models/admin.rb create mode 100644 project/app/models/aluno.rb create mode 100644 project/app/models/departamento.rb create mode 100644 project/app/models/formulario.rb create mode 100644 project/app/models/professor.rb create mode 100644 project/app/models/questao.rb create mode 100644 project/app/models/respostum.rb create mode 100644 project/app/models/submissao.rb create mode 100644 project/app/models/template.rb create mode 100644 project/app/models/turma.rb create mode 100644 project/app/models/turma_aluno.rb create mode 100644 project/app/views/admins/_admin.html.erb create mode 100644 project/app/views/admins/_admin.json.jbuilder create mode 100644 project/app/views/admins/_form.html.erb create mode 100644 project/app/views/admins/edit.html.erb create mode 100644 project/app/views/admins/index.html.erb create mode 100644 project/app/views/admins/index.json.jbuilder create mode 100644 project/app/views/admins/new.html.erb create mode 100644 project/app/views/admins/show.html.erb create mode 100644 project/app/views/admins/show.json.jbuilder create mode 100644 project/app/views/alunos/_aluno.html.erb create mode 100644 project/app/views/alunos/_aluno.json.jbuilder create mode 100644 project/app/views/alunos/_form.html.erb create mode 100644 project/app/views/alunos/edit.html.erb create mode 100644 project/app/views/alunos/index.html.erb create mode 100644 project/app/views/alunos/index.json.jbuilder create mode 100644 project/app/views/alunos/new.html.erb create mode 100644 project/app/views/alunos/show.html.erb create mode 100644 project/app/views/alunos/show.json.jbuilder create mode 100644 project/app/views/departamentos/_departamento.html.erb create mode 100644 project/app/views/departamentos/_departamento.json.jbuilder create mode 100644 project/app/views/departamentos/_form.html.erb create mode 100644 project/app/views/departamentos/edit.html.erb create mode 100644 project/app/views/departamentos/index.html.erb create mode 100644 project/app/views/departamentos/index.json.jbuilder create mode 100644 project/app/views/departamentos/new.html.erb create mode 100644 project/app/views/departamentos/show.html.erb create mode 100644 project/app/views/departamentos/show.json.jbuilder create mode 100644 project/app/views/formularios/_form.html.erb create mode 100644 project/app/views/formularios/_formulario.html.erb create mode 100644 project/app/views/formularios/_formulario.json.jbuilder create mode 100644 project/app/views/formularios/edit.html.erb create mode 100644 project/app/views/formularios/index.html.erb create mode 100644 project/app/views/formularios/index.json.jbuilder create mode 100644 project/app/views/formularios/new.html.erb create mode 100644 project/app/views/formularios/show.html.erb create mode 100644 project/app/views/formularios/show.json.jbuilder create mode 100644 project/app/views/professors/_form.html.erb create mode 100644 project/app/views/professors/_professor.html.erb create mode 100644 project/app/views/professors/_professor.json.jbuilder create mode 100644 project/app/views/professors/edit.html.erb create mode 100644 project/app/views/professors/index.html.erb create mode 100644 project/app/views/professors/index.json.jbuilder create mode 100644 project/app/views/professors/new.html.erb create mode 100644 project/app/views/professors/show.html.erb create mode 100644 project/app/views/professors/show.json.jbuilder create mode 100644 project/app/views/questaos/_form.html.erb create mode 100644 project/app/views/questaos/_questao.html.erb create mode 100644 project/app/views/questaos/_questao.json.jbuilder create mode 100644 project/app/views/questaos/edit.html.erb create mode 100644 project/app/views/questaos/index.html.erb create mode 100644 project/app/views/questaos/index.json.jbuilder create mode 100644 project/app/views/questaos/new.html.erb create mode 100644 project/app/views/questaos/show.html.erb create mode 100644 project/app/views/questaos/show.json.jbuilder create mode 100644 project/app/views/resposta/_form.html.erb create mode 100644 project/app/views/resposta/_respostum.html.erb create mode 100644 project/app/views/resposta/_respostum.json.jbuilder create mode 100644 project/app/views/resposta/edit.html.erb create mode 100644 project/app/views/resposta/index.html.erb create mode 100644 project/app/views/resposta/index.json.jbuilder create mode 100644 project/app/views/resposta/new.html.erb create mode 100644 project/app/views/resposta/show.html.erb create mode 100644 project/app/views/resposta/show.json.jbuilder create mode 100644 project/app/views/submissaos/_form.html.erb create mode 100644 project/app/views/submissaos/_submissao.html.erb create mode 100644 project/app/views/submissaos/_submissao.json.jbuilder create mode 100644 project/app/views/submissaos/edit.html.erb create mode 100644 project/app/views/submissaos/index.html.erb create mode 100644 project/app/views/submissaos/index.json.jbuilder create mode 100644 project/app/views/submissaos/new.html.erb create mode 100644 project/app/views/submissaos/show.html.erb create mode 100644 project/app/views/submissaos/show.json.jbuilder create mode 100644 project/app/views/templates/_form.html.erb create mode 100644 project/app/views/templates/_template.html.erb create mode 100644 project/app/views/templates/_template.json.jbuilder create mode 100644 project/app/views/templates/edit.html.erb create mode 100644 project/app/views/templates/index.html.erb create mode 100644 project/app/views/templates/index.json.jbuilder create mode 100644 project/app/views/templates/new.html.erb create mode 100644 project/app/views/templates/show.html.erb create mode 100644 project/app/views/templates/show.json.jbuilder create mode 100644 project/app/views/turmas/_form.html.erb create mode 100644 project/app/views/turmas/_turma.html.erb create mode 100644 project/app/views/turmas/_turma.json.jbuilder create mode 100644 project/app/views/turmas/edit.html.erb create mode 100644 project/app/views/turmas/index.html.erb create mode 100644 project/app/views/turmas/index.json.jbuilder create mode 100644 project/app/views/turmas/new.html.erb create mode 100644 project/app/views/turmas/show.html.erb create mode 100644 project/app/views/turmas/show.json.jbuilder create mode 100644 project/db/migrate/20260611134150_create_departamentos.rb create mode 100644 project/db/migrate/20260611134210_create_alunos.rb create mode 100644 project/db/migrate/20260611134235_create_admins.rb create mode 100644 project/db/migrate/20260611134249_create_professors.rb create mode 100644 project/db/migrate/20260611134304_create_turmas.rb create mode 100644 project/db/migrate/20260611134355_create_turma_alunos.rb create mode 100644 project/db/migrate/20260611134403_create_templates.rb create mode 100644 project/db/migrate/20260611134426_create_formularios.rb create mode 100644 project/db/migrate/20260611134433_create_questaos.rb create mode 100644 project/db/migrate/20260611134446_create_submissaos.rb create mode 100644 project/db/migrate/20260611134500_create_resposta.rb create mode 100644 project/db/schema.rb create mode 100644 project/test/controllers/admins_controller_test.rb create mode 100644 project/test/controllers/alunos_controller_test.rb create mode 100644 project/test/controllers/departamentos_controller_test.rb create mode 100644 project/test/controllers/formularios_controller_test.rb create mode 100644 project/test/controllers/professors_controller_test.rb create mode 100644 project/test/controllers/questaos_controller_test.rb create mode 100644 project/test/controllers/resposta_controller_test.rb create mode 100644 project/test/controllers/submissaos_controller_test.rb create mode 100644 project/test/controllers/templates_controller_test.rb create mode 100644 project/test/controllers/turmas_controller_test.rb create mode 100644 project/test/fixtures/admins.yml create mode 100644 project/test/fixtures/alunos.yml create mode 100644 project/test/fixtures/departamentos.yml create mode 100644 project/test/fixtures/formularios.yml create mode 100644 project/test/fixtures/professors.yml create mode 100644 project/test/fixtures/questaos.yml create mode 100644 project/test/fixtures/resposta.yml create mode 100644 project/test/fixtures/submissaos.yml create mode 100644 project/test/fixtures/templates.yml create mode 100644 project/test/fixtures/turma_alunos.yml create mode 100644 project/test/fixtures/turmas.yml create mode 100644 project/test/models/admin_test.rb create mode 100644 project/test/models/aluno_test.rb create mode 100644 project/test/models/departamento_test.rb create mode 100644 project/test/models/formulario_test.rb create mode 100644 project/test/models/professor_test.rb create mode 100644 project/test/models/questao_test.rb create mode 100644 project/test/models/respostum_test.rb create mode 100644 project/test/models/submissao_test.rb create mode 100644 project/test/models/template_test.rb create mode 100644 project/test/models/turma_aluno_test.rb create mode 100644 project/test/models/turma_test.rb diff --git a/project/Gemfile b/project/Gemfile index 7b743a4cab..193ebaf011 100644 --- a/project/Gemfile +++ b/project/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/project/Gemfile.lock b/project/Gemfile.lock index c8c2c9d4c8..db14dd8aa5 100644 --- a/project/Gemfile.lock +++ b/project/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) @@ -436,6 +437,7 @@ PLATFORMS x86_64-linux-musl DEPENDENCIES + bcrypt (~> 3.1.7) bootsnap brakeman bundler-audit @@ -478,6 +480,7 @@ CHECKSUMS addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383 base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b + bcrypt (3.1.22) sha256=1f0072e88c2d705d94aff7f2c5cb02eb3f1ec4b8368671e19112527489f29032 bcrypt_pbkdf (1.1.2) sha256=c2414c23ce66869b3eb9f643d6a3374d8322dfb5078125c82792304c10b94cf6 bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd bindex (0.8.1) sha256=7b1ecc9dc539ed8bccfc8cb4d2732046227b09d6f37582ff12e50a5047ceb17e diff --git a/project/app/controllers/admins_controller.rb b/project/app/controllers/admins_controller.rb new file mode 100644 index 0000000000..21e196367d --- /dev/null +++ b/project/app/controllers/admins_controller.rb @@ -0,0 +1,70 @@ +class AdminsController < ApplicationController + before_action :set_admin, only: %i[ show edit update destroy ] + + # GET /admins or /admins.json + def index + @admins = Admin.all + end + + # GET /admins/1 or /admins/1.json + def show + end + + # GET /admins/new + def new + @admin = Admin.new + end + + # GET /admins/1/edit + def edit + end + + # POST /admins or /admins.json + def create + @admin = Admin.new(admin_params) + + respond_to do |format| + if @admin.save + format.html { redirect_to @admin, notice: "Admin was successfully created." } + format.json { render :show, status: :created, location: @admin } + else + format.html { render :new, status: :unprocessable_content } + format.json { render json: @admin.errors, status: :unprocessable_content } + end + end + end + + # PATCH/PUT /admins/1 or /admins/1.json + def update + respond_to do |format| + if @admin.update(admin_params) + format.html { redirect_to @admin, notice: "Admin was successfully updated.", status: :see_other } + format.json { render :show, status: :ok, location: @admin } + else + format.html { render :edit, status: :unprocessable_content } + format.json { render json: @admin.errors, status: :unprocessable_content } + end + end + end + + # DELETE /admins/1 or /admins/1.json + def destroy + @admin.destroy! + + respond_to do |format| + format.html { redirect_to admins_path, notice: "Admin was successfully destroyed.", status: :see_other } + format.json { head :no_content } + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_admin + @admin = Admin.find(params.expect(:id)) + end + + # Only allow a list of trusted parameters through. + def admin_params + params.expect(admin: [ :username, :name, :email, :password_digest, :departamento_id ]) + end +end diff --git a/project/app/controllers/alunos_controller.rb b/project/app/controllers/alunos_controller.rb new file mode 100644 index 0000000000..2f4aa29663 --- /dev/null +++ b/project/app/controllers/alunos_controller.rb @@ -0,0 +1,70 @@ +class AlunosController < ApplicationController + before_action :set_aluno, only: %i[ show edit update destroy ] + + # GET /alunos or /alunos.json + def index + @alunos = Aluno.all + end + + # GET /alunos/1 or /alunos/1.json + def show + end + + # GET /alunos/new + def new + @aluno = Aluno.new + end + + # GET /alunos/1/edit + def edit + end + + # POST /alunos or /alunos.json + def create + @aluno = Aluno.new(aluno_params) + + respond_to do |format| + if @aluno.save + format.html { redirect_to @aluno, notice: "Aluno was successfully created." } + format.json { render :show, status: :created, location: @aluno } + else + format.html { render :new, status: :unprocessable_content } + format.json { render json: @aluno.errors, status: :unprocessable_content } + end + end + end + + # PATCH/PUT /alunos/1 or /alunos/1.json + def update + respond_to do |format| + if @aluno.update(aluno_params) + format.html { redirect_to @aluno, notice: "Aluno was successfully updated.", status: :see_other } + format.json { render :show, status: :ok, location: @aluno } + else + format.html { render :edit, status: :unprocessable_content } + format.json { render json: @aluno.errors, status: :unprocessable_content } + end + end + end + + # DELETE /alunos/1 or /alunos/1.json + def destroy + @aluno.destroy! + + respond_to do |format| + format.html { redirect_to alunos_path, notice: "Aluno was successfully destroyed.", status: :see_other } + format.json { head :no_content } + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_aluno + @aluno = Aluno.find(params.expect(:id)) + end + + # Only allow a list of trusted parameters through. + def aluno_params + params.expect(aluno: [ :matricula, :name, :email, :password_digest, :course ]) + end +end diff --git a/project/app/controllers/departamentos_controller.rb b/project/app/controllers/departamentos_controller.rb new file mode 100644 index 0000000000..af808627bf --- /dev/null +++ b/project/app/controllers/departamentos_controller.rb @@ -0,0 +1,70 @@ +class DepartamentosController < ApplicationController + before_action :set_departamento, only: %i[ show edit update destroy ] + + # GET /departamentos or /departamentos.json + def index + @departamentos = Departamento.all + end + + # GET /departamentos/1 or /departamentos/1.json + def show + end + + # GET /departamentos/new + def new + @departamento = Departamento.new + end + + # GET /departamentos/1/edit + def edit + end + + # POST /departamentos or /departamentos.json + def create + @departamento = Departamento.new(departamento_params) + + respond_to do |format| + if @departamento.save + format.html { redirect_to @departamento, notice: "Departamento was successfully created." } + format.json { render :show, status: :created, location: @departamento } + else + format.html { render :new, status: :unprocessable_content } + format.json { render json: @departamento.errors, status: :unprocessable_content } + end + end + end + + # PATCH/PUT /departamentos/1 or /departamentos/1.json + def update + respond_to do |format| + if @departamento.update(departamento_params) + format.html { redirect_to @departamento, notice: "Departamento was successfully updated.", status: :see_other } + format.json { render :show, status: :ok, location: @departamento } + else + format.html { render :edit, status: :unprocessable_content } + format.json { render json: @departamento.errors, status: :unprocessable_content } + end + end + end + + # DELETE /departamentos/1 or /departamentos/1.json + def destroy + @departamento.destroy! + + respond_to do |format| + format.html { redirect_to departamentos_path, notice: "Departamento was successfully destroyed.", status: :see_other } + format.json { head :no_content } + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_departamento + @departamento = Departamento.find(params.expect(:id)) + end + + # Only allow a list of trusted parameters through. + def departamento_params + params.expect(departamento: [ :name, :code ]) + end +end diff --git a/project/app/controllers/formularios_controller.rb b/project/app/controllers/formularios_controller.rb new file mode 100644 index 0000000000..781158cbba --- /dev/null +++ b/project/app/controllers/formularios_controller.rb @@ -0,0 +1,70 @@ +class FormulariosController < ApplicationController + before_action :set_formulario, only: %i[ show edit update destroy ] + + # GET /formularios or /formularios.json + def index + @formularios = Formulario.all + end + + # GET /formularios/1 or /formularios/1.json + def show + end + + # GET /formularios/new + def new + @formulario = Formulario.new + end + + # GET /formularios/1/edit + def edit + end + + # POST /formularios or /formularios.json + def create + @formulario = Formulario.new(formulario_params) + + respond_to do |format| + if @formulario.save + format.html { redirect_to @formulario, notice: "Formulario was successfully created." } + format.json { render :show, status: :created, location: @formulario } + else + format.html { render :new, status: :unprocessable_content } + format.json { render json: @formulario.errors, status: :unprocessable_content } + end + end + end + + # PATCH/PUT /formularios/1 or /formularios/1.json + def update + respond_to do |format| + if @formulario.update(formulario_params) + format.html { redirect_to @formulario, notice: "Formulario was successfully updated.", status: :see_other } + format.json { render :show, status: :ok, location: @formulario } + else + format.html { render :edit, status: :unprocessable_content } + format.json { render json: @formulario.errors, status: :unprocessable_content } + end + end + end + + # DELETE /formularios/1 or /formularios/1.json + def destroy + @formulario.destroy! + + respond_to do |format| + format.html { redirect_to formularios_path, notice: "Formulario was successfully destroyed.", status: :see_other } + format.json { head :no_content } + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_formulario + @formulario = Formulario.find(params.expect(:id)) + end + + # Only allow a list of trusted parameters through. + def formulario_params + params.expect(formulario: [ :title, :target_role, :status, :template_id, :turma_id, :admin_id ]) + end +end diff --git a/project/app/controllers/professors_controller.rb b/project/app/controllers/professors_controller.rb new file mode 100644 index 0000000000..5b5cd839b9 --- /dev/null +++ b/project/app/controllers/professors_controller.rb @@ -0,0 +1,70 @@ +class ProfessorsController < ApplicationController + before_action :set_professor, only: %i[ show edit update destroy ] + + # GET /professors or /professors.json + def index + @professors = Professor.all + end + + # GET /professors/1 or /professors/1.json + def show + end + + # GET /professors/new + def new + @professor = Professor.new + end + + # GET /professors/1/edit + def edit + end + + # POST /professors or /professors.json + def create + @professor = Professor.new(professor_params) + + respond_to do |format| + if @professor.save + format.html { redirect_to @professor, notice: "Professor was successfully created." } + format.json { render :show, status: :created, location: @professor } + else + format.html { render :new, status: :unprocessable_content } + format.json { render json: @professor.errors, status: :unprocessable_content } + end + end + end + + # PATCH/PUT /professors/1 or /professors/1.json + def update + respond_to do |format| + if @professor.update(professor_params) + format.html { redirect_to @professor, notice: "Professor was successfully updated.", status: :see_other } + format.json { render :show, status: :ok, location: @professor } + else + format.html { render :edit, status: :unprocessable_content } + format.json { render json: @professor.errors, status: :unprocessable_content } + end + end + end + + # DELETE /professors/1 or /professors/1.json + def destroy + @professor.destroy! + + respond_to do |format| + format.html { redirect_to professors_path, notice: "Professor was successfully destroyed.", status: :see_other } + format.json { head :no_content } + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_professor + @professor = Professor.find(params.expect(:id)) + end + + # Only allow a list of trusted parameters through. + def professor_params + params.expect(professor: [ :matricula, :name, :email, :password_digest, :formation, :departamento_id ]) + end +end diff --git a/project/app/controllers/questaos_controller.rb b/project/app/controllers/questaos_controller.rb new file mode 100644 index 0000000000..9cfd2366f0 --- /dev/null +++ b/project/app/controllers/questaos_controller.rb @@ -0,0 +1,70 @@ +class QuestaosController < ApplicationController + before_action :set_questao, only: %i[ show edit update destroy ] + + # GET /questaos or /questaos.json + def index + @questaos = Questao.all + end + + # GET /questaos/1 or /questaos/1.json + def show + end + + # GET /questaos/new + def new + @questao = Questao.new + end + + # GET /questaos/1/edit + def edit + end + + # POST /questaos or /questaos.json + def create + @questao = Questao.new(questao_params) + + respond_to do |format| + if @questao.save + format.html { redirect_to @questao, notice: "Questao was successfully created." } + format.json { render :show, status: :created, location: @questao } + else + format.html { render :new, status: :unprocessable_content } + format.json { render json: @questao.errors, status: :unprocessable_content } + end + end + end + + # PATCH/PUT /questaos/1 or /questaos/1.json + def update + respond_to do |format| + if @questao.update(questao_params) + format.html { redirect_to @questao, notice: "Questao was successfully updated.", status: :see_other } + format.json { render :show, status: :ok, location: @questao } + else + format.html { render :edit, status: :unprocessable_content } + format.json { render json: @questao.errors, status: :unprocessable_content } + end + end + end + + # DELETE /questaos/1 or /questaos/1.json + def destroy + @questao.destroy! + + respond_to do |format| + format.html { redirect_to questaos_path, notice: "Questao was successfully destroyed.", status: :see_other } + format.json { head :no_content } + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_questao + @questao = Questao.find(params.expect(:id)) + end + + # Only allow a list of trusted parameters through. + def questao_params + params.expect(questao: [ :enunciado, :tipo, :template_id, :formulario_id ]) + end +end diff --git a/project/app/controllers/resposta_controller.rb b/project/app/controllers/resposta_controller.rb new file mode 100644 index 0000000000..d3daae1769 --- /dev/null +++ b/project/app/controllers/resposta_controller.rb @@ -0,0 +1,70 @@ +class RespostaController < ApplicationController + before_action :set_respostum, only: %i[ show edit update destroy ] + + # GET /resposta or /resposta.json + def index + @resposta = Respostum.all + end + + # GET /resposta/1 or /resposta/1.json + def show + end + + # GET /resposta/new + def new + @respostum = Respostum.new + end + + # GET /resposta/1/edit + def edit + end + + # POST /resposta or /resposta.json + def create + @respostum = Respostum.new(respostum_params) + + respond_to do |format| + if @respostum.save + format.html { redirect_to @respostum, notice: "Respostum was successfully created." } + format.json { render :show, status: :created, location: @respostum } + else + format.html { render :new, status: :unprocessable_content } + format.json { render json: @respostum.errors, status: :unprocessable_content } + end + end + end + + # PATCH/PUT /resposta/1 or /resposta/1.json + def update + respond_to do |format| + if @respostum.update(respostum_params) + format.html { redirect_to @respostum, notice: "Respostum was successfully updated.", status: :see_other } + format.json { render :show, status: :ok, location: @respostum } + else + format.html { render :edit, status: :unprocessable_content } + format.json { render json: @respostum.errors, status: :unprocessable_content } + end + end + end + + # DELETE /resposta/1 or /resposta/1.json + def destroy + @respostum.destroy! + + respond_to do |format| + format.html { redirect_to resposta_path, notice: "Respostum was successfully destroyed.", status: :see_other } + format.json { head :no_content } + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_respostum + @respostum = Respostum.find(params.expect(:id)) + end + + # Only allow a list of trusted parameters through. + def respostum_params + params.expect(respostum: [ :valor_texto, :valor_numerico, :submissao_id, :questao_id ]) + end +end diff --git a/project/app/controllers/submissaos_controller.rb b/project/app/controllers/submissaos_controller.rb new file mode 100644 index 0000000000..afe8dce803 --- /dev/null +++ b/project/app/controllers/submissaos_controller.rb @@ -0,0 +1,70 @@ +class SubmissaosController < ApplicationController + before_action :set_submissao, only: %i[ show edit update destroy ] + + # GET /submissaos or /submissaos.json + def index + @submissaos = Submissao.all + end + + # GET /submissaos/1 or /submissaos/1.json + def show + end + + # GET /submissaos/new + def new + @submissao = Submissao.new + end + + # GET /submissaos/1/edit + def edit + end + + # POST /submissaos or /submissaos.json + def create + @submissao = Submissao.new(submissao_params) + + respond_to do |format| + if @submissao.save + format.html { redirect_to @submissao, notice: "Submissao was successfully created." } + format.json { render :show, status: :created, location: @submissao } + else + format.html { render :new, status: :unprocessable_content } + format.json { render json: @submissao.errors, status: :unprocessable_content } + end + end + end + + # PATCH/PUT /submissaos/1 or /submissaos/1.json + def update + respond_to do |format| + if @submissao.update(submissao_params) + format.html { redirect_to @submissao, notice: "Submissao was successfully updated.", status: :see_other } + format.json { render :show, status: :ok, location: @submissao } + else + format.html { render :edit, status: :unprocessable_content } + format.json { render json: @submissao.errors, status: :unprocessable_content } + end + end + end + + # DELETE /submissaos/1 or /submissaos/1.json + def destroy + @submissao.destroy! + + respond_to do |format| + format.html { redirect_to submissaos_path, notice: "Submissao was successfully destroyed.", status: :see_other } + format.json { head :no_content } + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_submissao + @submissao = Submissao.find(params.expect(:id)) + end + + # Only allow a list of trusted parameters through. + def submissao_params + params.expect(submissao: [ :formulario_id, :participant_id, :participant_type ]) + end +end diff --git a/project/app/controllers/templates_controller.rb b/project/app/controllers/templates_controller.rb new file mode 100644 index 0000000000..cb6a71ea57 --- /dev/null +++ b/project/app/controllers/templates_controller.rb @@ -0,0 +1,70 @@ +class TemplatesController < ApplicationController + before_action :set_template, only: %i[ show edit update destroy ] + + # GET /templates or /templates.json + def index + @templates = Template.all + end + + # GET /templates/1 or /templates/1.json + def show + end + + # GET /templates/new + def new + @template = Template.new + end + + # GET /templates/1/edit + def edit + end + + # POST /templates or /templates.json + def create + @template = Template.new(template_params) + + respond_to do |format| + if @template.save + format.html { redirect_to @template, notice: "Template was successfully created." } + format.json { render :show, status: :created, location: @template } + else + format.html { render :new, status: :unprocessable_content } + format.json { render json: @template.errors, status: :unprocessable_content } + end + end + end + + # PATCH/PUT /templates/1 or /templates/1.json + def update + respond_to do |format| + if @template.update(template_params) + format.html { redirect_to @template, notice: "Template was successfully updated.", status: :see_other } + format.json { render :show, status: :ok, location: @template } + else + format.html { render :edit, status: :unprocessable_content } + format.json { render json: @template.errors, status: :unprocessable_content } + end + end + end + + # DELETE /templates/1 or /templates/1.json + def destroy + @template.destroy! + + respond_to do |format| + format.html { redirect_to templates_path, notice: "Template was successfully destroyed.", status: :see_other } + format.json { head :no_content } + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_template + @template = Template.find(params.expect(:id)) + end + + # Only allow a list of trusted parameters through. + def template_params + params.expect(template: [ :title, :target_role, :admin_id ]) + end +end diff --git a/project/app/controllers/turmas_controller.rb b/project/app/controllers/turmas_controller.rb new file mode 100644 index 0000000000..5e257736e0 --- /dev/null +++ b/project/app/controllers/turmas_controller.rb @@ -0,0 +1,70 @@ +class TurmasController < ApplicationController + before_action :set_turma, only: %i[ show edit update destroy ] + + # GET /turmas or /turmas.json + def index + @turmas = Turma.all + end + + # GET /turmas/1 or /turmas/1.json + def show + end + + # GET /turmas/new + def new + @turma = Turma.new + end + + # GET /turmas/1/edit + def edit + end + + # POST /turmas or /turmas.json + def create + @turma = Turma.new(turma_params) + + respond_to do |format| + if @turma.save + format.html { redirect_to @turma, notice: "Turma was successfully created." } + format.json { render :show, status: :created, location: @turma } + else + format.html { render :new, status: :unprocessable_content } + format.json { render json: @turma.errors, status: :unprocessable_content } + end + end + end + + # PATCH/PUT /turmas/1 or /turmas/1.json + def update + respond_to do |format| + if @turma.update(turma_params) + format.html { redirect_to @turma, notice: "Turma was successfully updated.", status: :see_other } + format.json { render :show, status: :ok, location: @turma } + else + format.html { render :edit, status: :unprocessable_content } + format.json { render json: @turma.errors, status: :unprocessable_content } + end + end + end + + # DELETE /turmas/1 or /turmas/1.json + def destroy + @turma.destroy! + + respond_to do |format| + format.html { redirect_to turmas_path, notice: "Turma was successfully destroyed.", status: :see_other } + format.json { head :no_content } + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_turma + @turma = Turma.find(params.expect(:id)) + end + + # Only allow a list of trusted parameters through. + def turma_params + params.expect(turma: [ :subject_code, :subject_name, :class_code, :semester, :time, :departamento_id, :professor_id ]) + end +end diff --git a/project/app/helpers/admins_helper.rb b/project/app/helpers/admins_helper.rb new file mode 100644 index 0000000000..d4f7b3486b --- /dev/null +++ b/project/app/helpers/admins_helper.rb @@ -0,0 +1,2 @@ +module AdminsHelper +end diff --git a/project/app/helpers/alunos_helper.rb b/project/app/helpers/alunos_helper.rb new file mode 100644 index 0000000000..cdbe19f2dd --- /dev/null +++ b/project/app/helpers/alunos_helper.rb @@ -0,0 +1,2 @@ +module AlunosHelper +end diff --git a/project/app/helpers/departamentos_helper.rb b/project/app/helpers/departamentos_helper.rb new file mode 100644 index 0000000000..df3b94bfda --- /dev/null +++ b/project/app/helpers/departamentos_helper.rb @@ -0,0 +1,2 @@ +module DepartamentosHelper +end diff --git a/project/app/helpers/formularios_helper.rb b/project/app/helpers/formularios_helper.rb new file mode 100644 index 0000000000..3b96bb2209 --- /dev/null +++ b/project/app/helpers/formularios_helper.rb @@ -0,0 +1,2 @@ +module FormulariosHelper +end diff --git a/project/app/helpers/professors_helper.rb b/project/app/helpers/professors_helper.rb new file mode 100644 index 0000000000..15367d63c7 --- /dev/null +++ b/project/app/helpers/professors_helper.rb @@ -0,0 +1,2 @@ +module ProfessorsHelper +end diff --git a/project/app/helpers/questaos_helper.rb b/project/app/helpers/questaos_helper.rb new file mode 100644 index 0000000000..c313f9d4b0 --- /dev/null +++ b/project/app/helpers/questaos_helper.rb @@ -0,0 +1,2 @@ +module QuestaosHelper +end diff --git a/project/app/helpers/resposta_helper.rb b/project/app/helpers/resposta_helper.rb new file mode 100644 index 0000000000..6cc35b5534 --- /dev/null +++ b/project/app/helpers/resposta_helper.rb @@ -0,0 +1,2 @@ +module RespostaHelper +end diff --git a/project/app/helpers/submissaos_helper.rb b/project/app/helpers/submissaos_helper.rb new file mode 100644 index 0000000000..645fdbb6b5 --- /dev/null +++ b/project/app/helpers/submissaos_helper.rb @@ -0,0 +1,2 @@ +module SubmissaosHelper +end diff --git a/project/app/helpers/templates_helper.rb b/project/app/helpers/templates_helper.rb new file mode 100644 index 0000000000..888093b832 --- /dev/null +++ b/project/app/helpers/templates_helper.rb @@ -0,0 +1,2 @@ +module TemplatesHelper +end diff --git a/project/app/helpers/turmas_helper.rb b/project/app/helpers/turmas_helper.rb new file mode 100644 index 0000000000..617e1373e9 --- /dev/null +++ b/project/app/helpers/turmas_helper.rb @@ -0,0 +1,2 @@ +module TurmasHelper +end diff --git a/project/app/models/admin.rb b/project/app/models/admin.rb new file mode 100644 index 0000000000..0d89d934ca --- /dev/null +++ b/project/app/models/admin.rb @@ -0,0 +1,3 @@ +class Admin < ApplicationRecord + belongs_to :departamento +end diff --git a/project/app/models/aluno.rb b/project/app/models/aluno.rb new file mode 100644 index 0000000000..f1d547d638 --- /dev/null +++ b/project/app/models/aluno.rb @@ -0,0 +1,6 @@ +class Aluno < ApplicationRecord + has_secure_password # Habilita a criptografia da senha usando bcrypt + has_many :turma_alunos + has_many :turmas, through: :turma_alunos + has_many :submissoes, as: :participant +end \ No newline at end of file diff --git a/project/app/models/departamento.rb b/project/app/models/departamento.rb new file mode 100644 index 0000000000..7e10c65d86 --- /dev/null +++ b/project/app/models/departamento.rb @@ -0,0 +1,5 @@ +class Departamento < ApplicationRecord + has_many :admins + has_many :professors + has_many :turmas +end \ No newline at end of file diff --git a/project/app/models/formulario.rb b/project/app/models/formulario.rb new file mode 100644 index 0000000000..63b69618ac --- /dev/null +++ b/project/app/models/formulario.rb @@ -0,0 +1,5 @@ +class Formulario < ApplicationRecord + belongs_to :template + belongs_to :turma + belongs_to :admin +end diff --git a/project/app/models/professor.rb b/project/app/models/professor.rb new file mode 100644 index 0000000000..6203f39fe4 --- /dev/null +++ b/project/app/models/professor.rb @@ -0,0 +1,6 @@ +class Professor < ApplicationRecord + belongs_to :departamento + has_secure_password + has_many :turmas + has_many :submissoes, as: :participant +end \ No newline at end of file diff --git a/project/app/models/questao.rb b/project/app/models/questao.rb new file mode 100644 index 0000000000..cfd49e8229 --- /dev/null +++ b/project/app/models/questao.rb @@ -0,0 +1,6 @@ +class Questao < ApplicationRecord + # A chave estrangeira é nula para template se pertencer a formulário, e vice-versa + belongs_to :template, optional: true + belongs_to :formulario, optional: true + has_many :respostas +end \ No newline at end of file diff --git a/project/app/models/respostum.rb b/project/app/models/respostum.rb new file mode 100644 index 0000000000..8f4da0fd44 --- /dev/null +++ b/project/app/models/respostum.rb @@ -0,0 +1,4 @@ +class Respostum < ApplicationRecord + belongs_to :submissao + belongs_to :questao +end diff --git a/project/app/models/submissao.rb b/project/app/models/submissao.rb new file mode 100644 index 0000000000..dad4a9dd3d --- /dev/null +++ b/project/app/models/submissao.rb @@ -0,0 +1,4 @@ +class Submissao < ApplicationRecord + belongs_to :formulario + belongs_to :participant, polymorphic: true +end diff --git a/project/app/models/template.rb b/project/app/models/template.rb new file mode 100644 index 0000000000..d6dea87801 --- /dev/null +++ b/project/app/models/template.rb @@ -0,0 +1,3 @@ +class Template < ApplicationRecord + belongs_to :admin +end diff --git a/project/app/models/turma.rb b/project/app/models/turma.rb new file mode 100644 index 0000000000..49b710c1bc --- /dev/null +++ b/project/app/models/turma.rb @@ -0,0 +1,7 @@ +class Turma < ApplicationRecord + belongs_to :departamento + belongs_to :professor + has_many :turma_alunos + has_many :alunos, through: :turma_alunos # Resolve o relacionamento N:N + has_many :formularios +end \ No newline at end of file diff --git a/project/app/models/turma_aluno.rb b/project/app/models/turma_aluno.rb new file mode 100644 index 0000000000..61478af53c --- /dev/null +++ b/project/app/models/turma_aluno.rb @@ -0,0 +1,4 @@ +class TurmaAluno < ApplicationRecord + belongs_to :aluno + belongs_to :turma +end diff --git a/project/app/views/admins/_admin.html.erb b/project/app/views/admins/_admin.html.erb new file mode 100644 index 0000000000..535042c356 --- /dev/null +++ b/project/app/views/admins/_admin.html.erb @@ -0,0 +1,27 @@ +
+
+ Username: + <%= admin.username %> +
+ +
+ Name: + <%= admin.name %> +
+ +
+ Email: + <%= admin.email %> +
+ +
+ Password digest: + <%= admin.password_digest %> +
+ +
+ Departamento: + <%= admin.departamento_id %> +
+ +
diff --git a/project/app/views/admins/_admin.json.jbuilder b/project/app/views/admins/_admin.json.jbuilder new file mode 100644 index 0000000000..19f73184b3 --- /dev/null +++ b/project/app/views/admins/_admin.json.jbuilder @@ -0,0 +1,2 @@ +json.extract! admin, :id, :username, :name, :email, :password_digest, :departamento_id, :created_at, :updated_at +json.url admin_url(admin, format: :json) diff --git a/project/app/views/admins/_form.html.erb b/project/app/views/admins/_form.html.erb new file mode 100644 index 0000000000..62cffb2b7e --- /dev/null +++ b/project/app/views/admins/_form.html.erb @@ -0,0 +1,42 @@ +<%= form_with(model: admin) do |form| %> + <% if admin.errors.any? %> +
+

<%= pluralize(admin.errors.count, "error") %> prohibited this admin from being saved:

+ +
    + <% admin.errors.each do |error| %> +
  • <%= error.full_message %>
  • + <% end %> +
+
+ <% end %> + +
+ <%= form.label :username, style: "display: block" %> + <%= form.text_field :username %> +
+ +
+ <%= form.label :name, style: "display: block" %> + <%= form.text_field :name %> +
+ +
+ <%= form.label :email, style: "display: block" %> + <%= form.text_field :email %> +
+ +
+ <%= form.label :password_digest, style: "display: block" %> + <%= form.text_field :password_digest %> +
+ +
+ <%= form.label :departamento_id, style: "display: block" %> + <%= form.text_field :departamento_id %> +
+ +
+ <%= form.submit %> +
+<% end %> diff --git a/project/app/views/admins/edit.html.erb b/project/app/views/admins/edit.html.erb new file mode 100644 index 0000000000..d76e123b75 --- /dev/null +++ b/project/app/views/admins/edit.html.erb @@ -0,0 +1,12 @@ +<% content_for :title, "Editing admin" %> + +

Editing admin

+ +<%= render "form", admin: @admin %> + +
+ +
+ <%= link_to "Show this admin", @admin %> | + <%= link_to "Back to admins", admins_path %> +
diff --git a/project/app/views/admins/index.html.erb b/project/app/views/admins/index.html.erb new file mode 100644 index 0000000000..d50845e38e --- /dev/null +++ b/project/app/views/admins/index.html.erb @@ -0,0 +1,16 @@ +

<%= notice %>

+ +<% content_for :title, "Admins" %> + +

Admins

+ +
+ <% @admins.each do |admin| %> + <%= render admin %> +

+ <%= link_to "Show this admin", admin %> +

+ <% end %> +
+ +<%= link_to "New admin", new_admin_path %> diff --git a/project/app/views/admins/index.json.jbuilder b/project/app/views/admins/index.json.jbuilder new file mode 100644 index 0000000000..5efd59b17b --- /dev/null +++ b/project/app/views/admins/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @admins, partial: "admins/admin", as: :admin diff --git a/project/app/views/admins/new.html.erb b/project/app/views/admins/new.html.erb new file mode 100644 index 0000000000..06fd1231e9 --- /dev/null +++ b/project/app/views/admins/new.html.erb @@ -0,0 +1,11 @@ +<% content_for :title, "New admin" %> + +

New admin

+ +<%= render "form", admin: @admin %> + +
+ +
+ <%= link_to "Back to admins", admins_path %> +
diff --git a/project/app/views/admins/show.html.erb b/project/app/views/admins/show.html.erb new file mode 100644 index 0000000000..238d213616 --- /dev/null +++ b/project/app/views/admins/show.html.erb @@ -0,0 +1,10 @@ +

<%= notice %>

+ +<%= render @admin %> + +
+ <%= link_to "Edit this admin", edit_admin_path(@admin) %> | + <%= link_to "Back to admins", admins_path %> + + <%= button_to "Destroy this admin", @admin, method: :delete %> +
diff --git a/project/app/views/admins/show.json.jbuilder b/project/app/views/admins/show.json.jbuilder new file mode 100644 index 0000000000..b62f64f16b --- /dev/null +++ b/project/app/views/admins/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! "admins/admin", admin: @admin diff --git a/project/app/views/alunos/_aluno.html.erb b/project/app/views/alunos/_aluno.html.erb new file mode 100644 index 0000000000..2d93e21748 --- /dev/null +++ b/project/app/views/alunos/_aluno.html.erb @@ -0,0 +1,27 @@ +
+
+ Matricula: + <%= aluno.matricula %> +
+ +
+ Name: + <%= aluno.name %> +
+ +
+ Email: + <%= aluno.email %> +
+ +
+ Password digest: + <%= aluno.password_digest %> +
+ +
+ Course: + <%= aluno.course %> +
+ +
diff --git a/project/app/views/alunos/_aluno.json.jbuilder b/project/app/views/alunos/_aluno.json.jbuilder new file mode 100644 index 0000000000..4e5198ab05 --- /dev/null +++ b/project/app/views/alunos/_aluno.json.jbuilder @@ -0,0 +1,2 @@ +json.extract! aluno, :id, :matricula, :name, :email, :password_digest, :course, :created_at, :updated_at +json.url aluno_url(aluno, format: :json) diff --git a/project/app/views/alunos/_form.html.erb b/project/app/views/alunos/_form.html.erb new file mode 100644 index 0000000000..8cdf068652 --- /dev/null +++ b/project/app/views/alunos/_form.html.erb @@ -0,0 +1,42 @@ +<%= form_with(model: aluno) do |form| %> + <% if aluno.errors.any? %> +
+

<%= pluralize(aluno.errors.count, "error") %> prohibited this aluno from being saved:

+ +
    + <% aluno.errors.each do |error| %> +
  • <%= error.full_message %>
  • + <% end %> +
+
+ <% end %> + +
+ <%= form.label :matricula, style: "display: block" %> + <%= form.text_field :matricula %> +
+ +
+ <%= form.label :name, style: "display: block" %> + <%= form.text_field :name %> +
+ +
+ <%= form.label :email, style: "display: block" %> + <%= form.text_field :email %> +
+ +
+ <%= form.label :password_digest, style: "display: block" %> + <%= form.text_field :password_digest %> +
+ +
+ <%= form.label :course, style: "display: block" %> + <%= form.text_field :course %> +
+ +
+ <%= form.submit %> +
+<% end %> diff --git a/project/app/views/alunos/edit.html.erb b/project/app/views/alunos/edit.html.erb new file mode 100644 index 0000000000..ebf1b5ce77 --- /dev/null +++ b/project/app/views/alunos/edit.html.erb @@ -0,0 +1,12 @@ +<% content_for :title, "Editing aluno" %> + +

Editing aluno

+ +<%= render "form", aluno: @aluno %> + +
+ +
+ <%= link_to "Show this aluno", @aluno %> | + <%= link_to "Back to alunos", alunos_path %> +
diff --git a/project/app/views/alunos/index.html.erb b/project/app/views/alunos/index.html.erb new file mode 100644 index 0000000000..c338a9b9f2 --- /dev/null +++ b/project/app/views/alunos/index.html.erb @@ -0,0 +1,16 @@ +

<%= notice %>

+ +<% content_for :title, "Alunos" %> + +

Alunos

+ +
+ <% @alunos.each do |aluno| %> + <%= render aluno %> +

+ <%= link_to "Show this aluno", aluno %> +

+ <% end %> +
+ +<%= link_to "New aluno", new_aluno_path %> diff --git a/project/app/views/alunos/index.json.jbuilder b/project/app/views/alunos/index.json.jbuilder new file mode 100644 index 0000000000..d8ba6d1b32 --- /dev/null +++ b/project/app/views/alunos/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @alunos, partial: "alunos/aluno", as: :aluno diff --git a/project/app/views/alunos/new.html.erb b/project/app/views/alunos/new.html.erb new file mode 100644 index 0000000000..f004daf571 --- /dev/null +++ b/project/app/views/alunos/new.html.erb @@ -0,0 +1,11 @@ +<% content_for :title, "New aluno" %> + +

New aluno

+ +<%= render "form", aluno: @aluno %> + +
+ +
+ <%= link_to "Back to alunos", alunos_path %> +
diff --git a/project/app/views/alunos/show.html.erb b/project/app/views/alunos/show.html.erb new file mode 100644 index 0000000000..0eea97a1d1 --- /dev/null +++ b/project/app/views/alunos/show.html.erb @@ -0,0 +1,10 @@ +

<%= notice %>

+ +<%= render @aluno %> + +
+ <%= link_to "Edit this aluno", edit_aluno_path(@aluno) %> | + <%= link_to "Back to alunos", alunos_path %> + + <%= button_to "Destroy this aluno", @aluno, method: :delete %> +
diff --git a/project/app/views/alunos/show.json.jbuilder b/project/app/views/alunos/show.json.jbuilder new file mode 100644 index 0000000000..5cb30d27cf --- /dev/null +++ b/project/app/views/alunos/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! "alunos/aluno", aluno: @aluno diff --git a/project/app/views/departamentos/_departamento.html.erb b/project/app/views/departamentos/_departamento.html.erb new file mode 100644 index 0000000000..feee8625cb --- /dev/null +++ b/project/app/views/departamentos/_departamento.html.erb @@ -0,0 +1,12 @@ +
+
+ Name: + <%= departamento.name %> +
+ +
+ Code: + <%= departamento.code %> +
+ +
diff --git a/project/app/views/departamentos/_departamento.json.jbuilder b/project/app/views/departamentos/_departamento.json.jbuilder new file mode 100644 index 0000000000..3c7e1b525c --- /dev/null +++ b/project/app/views/departamentos/_departamento.json.jbuilder @@ -0,0 +1,2 @@ +json.extract! departamento, :id, :name, :code, :created_at, :updated_at +json.url departamento_url(departamento, format: :json) diff --git a/project/app/views/departamentos/_form.html.erb b/project/app/views/departamentos/_form.html.erb new file mode 100644 index 0000000000..31cab715c4 --- /dev/null +++ b/project/app/views/departamentos/_form.html.erb @@ -0,0 +1,27 @@ +<%= form_with(model: departamento) do |form| %> + <% if departamento.errors.any? %> +
+

<%= pluralize(departamento.errors.count, "error") %> prohibited this departamento from being saved:

+ +
    + <% departamento.errors.each do |error| %> +
  • <%= error.full_message %>
  • + <% end %> +
+
+ <% end %> + +
+ <%= form.label :name, style: "display: block" %> + <%= form.text_field :name %> +
+ +
+ <%= form.label :code, style: "display: block" %> + <%= form.text_field :code %> +
+ +
+ <%= form.submit %> +
+<% end %> diff --git a/project/app/views/departamentos/edit.html.erb b/project/app/views/departamentos/edit.html.erb new file mode 100644 index 0000000000..f63ef9cac0 --- /dev/null +++ b/project/app/views/departamentos/edit.html.erb @@ -0,0 +1,12 @@ +<% content_for :title, "Editing departamento" %> + +

Editing departamento

+ +<%= render "form", departamento: @departamento %> + +
+ +
+ <%= link_to "Show this departamento", @departamento %> | + <%= link_to "Back to departamentos", departamentos_path %> +
diff --git a/project/app/views/departamentos/index.html.erb b/project/app/views/departamentos/index.html.erb new file mode 100644 index 0000000000..a05d2b04fa --- /dev/null +++ b/project/app/views/departamentos/index.html.erb @@ -0,0 +1,16 @@ +

<%= notice %>

+ +<% content_for :title, "Departamentos" %> + +

Departamentos

+ +
+ <% @departamentos.each do |departamento| %> + <%= render departamento %> +

+ <%= link_to "Show this departamento", departamento %> +

+ <% end %> +
+ +<%= link_to "New departamento", new_departamento_path %> diff --git a/project/app/views/departamentos/index.json.jbuilder b/project/app/views/departamentos/index.json.jbuilder new file mode 100644 index 0000000000..030ab748d1 --- /dev/null +++ b/project/app/views/departamentos/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @departamentos, partial: "departamentos/departamento", as: :departamento diff --git a/project/app/views/departamentos/new.html.erb b/project/app/views/departamentos/new.html.erb new file mode 100644 index 0000000000..76eefa1cfb --- /dev/null +++ b/project/app/views/departamentos/new.html.erb @@ -0,0 +1,11 @@ +<% content_for :title, "New departamento" %> + +

New departamento

+ +<%= render "form", departamento: @departamento %> + +
+ +
+ <%= link_to "Back to departamentos", departamentos_path %> +
diff --git a/project/app/views/departamentos/show.html.erb b/project/app/views/departamentos/show.html.erb new file mode 100644 index 0000000000..2957799d46 --- /dev/null +++ b/project/app/views/departamentos/show.html.erb @@ -0,0 +1,10 @@ +

<%= notice %>

+ +<%= render @departamento %> + +
+ <%= link_to "Edit this departamento", edit_departamento_path(@departamento) %> | + <%= link_to "Back to departamentos", departamentos_path %> + + <%= button_to "Destroy this departamento", @departamento, method: :delete %> +
diff --git a/project/app/views/departamentos/show.json.jbuilder b/project/app/views/departamentos/show.json.jbuilder new file mode 100644 index 0000000000..13c4a70c0c --- /dev/null +++ b/project/app/views/departamentos/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! "departamentos/departamento", departamento: @departamento diff --git a/project/app/views/formularios/_form.html.erb b/project/app/views/formularios/_form.html.erb new file mode 100644 index 0000000000..e3af3029d0 --- /dev/null +++ b/project/app/views/formularios/_form.html.erb @@ -0,0 +1,47 @@ +<%= form_with(model: formulario) do |form| %> + <% if formulario.errors.any? %> +
+

<%= pluralize(formulario.errors.count, "error") %> prohibited this formulario from being saved:

+ +
    + <% formulario.errors.each do |error| %> +
  • <%= error.full_message %>
  • + <% end %> +
+
+ <% end %> + +
+ <%= form.label :title, style: "display: block" %> + <%= form.text_field :title %> +
+ +
+ <%= form.label :target_role, style: "display: block" %> + <%= form.text_field :target_role %> +
+ +
+ <%= form.label :status, style: "display: block" %> + <%= form.text_field :status %> +
+ +
+ <%= form.label :template_id, style: "display: block" %> + <%= form.text_field :template_id %> +
+ +
+ <%= form.label :turma_id, style: "display: block" %> + <%= form.text_field :turma_id %> +
+ +
+ <%= form.label :admin_id, style: "display: block" %> + <%= form.text_field :admin_id %> +
+ +
+ <%= form.submit %> +
+<% end %> diff --git a/project/app/views/formularios/_formulario.html.erb b/project/app/views/formularios/_formulario.html.erb new file mode 100644 index 0000000000..85efd30d14 --- /dev/null +++ b/project/app/views/formularios/_formulario.html.erb @@ -0,0 +1,32 @@ +
+
+ Title: + <%= formulario.title %> +
+ +
+ Target role: + <%= formulario.target_role %> +
+ +
+ Status: + <%= formulario.status %> +
+ +
+ Template: + <%= formulario.template_id %> +
+ +
+ Turma: + <%= formulario.turma_id %> +
+ +
+ Admin: + <%= formulario.admin_id %> +
+ +
diff --git a/project/app/views/formularios/_formulario.json.jbuilder b/project/app/views/formularios/_formulario.json.jbuilder new file mode 100644 index 0000000000..7572ef155d --- /dev/null +++ b/project/app/views/formularios/_formulario.json.jbuilder @@ -0,0 +1,2 @@ +json.extract! formulario, :id, :title, :target_role, :status, :template_id, :turma_id, :admin_id, :created_at, :updated_at +json.url formulario_url(formulario, format: :json) diff --git a/project/app/views/formularios/edit.html.erb b/project/app/views/formularios/edit.html.erb new file mode 100644 index 0000000000..2fb833aa29 --- /dev/null +++ b/project/app/views/formularios/edit.html.erb @@ -0,0 +1,12 @@ +<% content_for :title, "Editing formulario" %> + +

Editing formulario

+ +<%= render "form", formulario: @formulario %> + +
+ +
+ <%= link_to "Show this formulario", @formulario %> | + <%= link_to "Back to formularios", formularios_path %> +
diff --git a/project/app/views/formularios/index.html.erb b/project/app/views/formularios/index.html.erb new file mode 100644 index 0000000000..99df9d53e3 --- /dev/null +++ b/project/app/views/formularios/index.html.erb @@ -0,0 +1,16 @@ +

<%= notice %>

+ +<% content_for :title, "Formularios" %> + +

Formularios

+ +
+ <% @formularios.each do |formulario| %> + <%= render formulario %> +

+ <%= link_to "Show this formulario", formulario %> +

+ <% end %> +
+ +<%= link_to "New formulario", new_formulario_path %> diff --git a/project/app/views/formularios/index.json.jbuilder b/project/app/views/formularios/index.json.jbuilder new file mode 100644 index 0000000000..70770973a8 --- /dev/null +++ b/project/app/views/formularios/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @formularios, partial: "formularios/formulario", as: :formulario diff --git a/project/app/views/formularios/new.html.erb b/project/app/views/formularios/new.html.erb new file mode 100644 index 0000000000..0dbdfdbd5c --- /dev/null +++ b/project/app/views/formularios/new.html.erb @@ -0,0 +1,11 @@ +<% content_for :title, "New formulario" %> + +

New formulario

+ +<%= render "form", formulario: @formulario %> + +
+ +
+ <%= link_to "Back to formularios", formularios_path %> +
diff --git a/project/app/views/formularios/show.html.erb b/project/app/views/formularios/show.html.erb new file mode 100644 index 0000000000..24709ac66b --- /dev/null +++ b/project/app/views/formularios/show.html.erb @@ -0,0 +1,10 @@ +

<%= notice %>

+ +<%= render @formulario %> + +
+ <%= link_to "Edit this formulario", edit_formulario_path(@formulario) %> | + <%= link_to "Back to formularios", formularios_path %> + + <%= button_to "Destroy this formulario", @formulario, method: :delete %> +
diff --git a/project/app/views/formularios/show.json.jbuilder b/project/app/views/formularios/show.json.jbuilder new file mode 100644 index 0000000000..476f64d624 --- /dev/null +++ b/project/app/views/formularios/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! "formularios/formulario", formulario: @formulario diff --git a/project/app/views/professors/_form.html.erb b/project/app/views/professors/_form.html.erb new file mode 100644 index 0000000000..1caee69146 --- /dev/null +++ b/project/app/views/professors/_form.html.erb @@ -0,0 +1,47 @@ +<%= form_with(model: professor) do |form| %> + <% if professor.errors.any? %> +
+

<%= pluralize(professor.errors.count, "error") %> prohibited this professor from being saved:

+ +
    + <% professor.errors.each do |error| %> +
  • <%= error.full_message %>
  • + <% end %> +
+
+ <% end %> + +
+ <%= form.label :matricula, style: "display: block" %> + <%= form.text_field :matricula %> +
+ +
+ <%= form.label :name, style: "display: block" %> + <%= form.text_field :name %> +
+ +
+ <%= form.label :email, style: "display: block" %> + <%= form.text_field :email %> +
+ +
+ <%= form.label :password_digest, style: "display: block" %> + <%= form.text_field :password_digest %> +
+ +
+ <%= form.label :formation, style: "display: block" %> + <%= form.text_field :formation %> +
+ +
+ <%= form.label :departamento_id, style: "display: block" %> + <%= form.text_field :departamento_id %> +
+ +
+ <%= form.submit %> +
+<% end %> diff --git a/project/app/views/professors/_professor.html.erb b/project/app/views/professors/_professor.html.erb new file mode 100644 index 0000000000..de3fdf7a2c --- /dev/null +++ b/project/app/views/professors/_professor.html.erb @@ -0,0 +1,32 @@ +
+
+ Matricula: + <%= professor.matricula %> +
+ +
+ Name: + <%= professor.name %> +
+ +
+ Email: + <%= professor.email %> +
+ +
+ Password digest: + <%= professor.password_digest %> +
+ +
+ Formation: + <%= professor.formation %> +
+ +
+ Departamento: + <%= professor.departamento_id %> +
+ +
diff --git a/project/app/views/professors/_professor.json.jbuilder b/project/app/views/professors/_professor.json.jbuilder new file mode 100644 index 0000000000..e10f1eda5d --- /dev/null +++ b/project/app/views/professors/_professor.json.jbuilder @@ -0,0 +1,2 @@ +json.extract! professor, :id, :matricula, :name, :email, :password_digest, :formation, :departamento_id, :created_at, :updated_at +json.url professor_url(professor, format: :json) diff --git a/project/app/views/professors/edit.html.erb b/project/app/views/professors/edit.html.erb new file mode 100644 index 0000000000..cb883e2954 --- /dev/null +++ b/project/app/views/professors/edit.html.erb @@ -0,0 +1,12 @@ +<% content_for :title, "Editing professor" %> + +

Editing professor

+ +<%= render "form", professor: @professor %> + +
+ +
+ <%= link_to "Show this professor", @professor %> | + <%= link_to "Back to professors", professors_path %> +
diff --git a/project/app/views/professors/index.html.erb b/project/app/views/professors/index.html.erb new file mode 100644 index 0000000000..1fb166bfec --- /dev/null +++ b/project/app/views/professors/index.html.erb @@ -0,0 +1,16 @@ +

<%= notice %>

+ +<% content_for :title, "Professors" %> + +

Professors

+ +
+ <% @professors.each do |professor| %> + <%= render professor %> +

+ <%= link_to "Show this professor", professor %> +

+ <% end %> +
+ +<%= link_to "New professor", new_professor_path %> diff --git a/project/app/views/professors/index.json.jbuilder b/project/app/views/professors/index.json.jbuilder new file mode 100644 index 0000000000..0403741ce6 --- /dev/null +++ b/project/app/views/professors/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @professors, partial: "professors/professor", as: :professor diff --git a/project/app/views/professors/new.html.erb b/project/app/views/professors/new.html.erb new file mode 100644 index 0000000000..9cea04d8c5 --- /dev/null +++ b/project/app/views/professors/new.html.erb @@ -0,0 +1,11 @@ +<% content_for :title, "New professor" %> + +

New professor

+ +<%= render "form", professor: @professor %> + +
+ +
+ <%= link_to "Back to professors", professors_path %> +
diff --git a/project/app/views/professors/show.html.erb b/project/app/views/professors/show.html.erb new file mode 100644 index 0000000000..65d3961e08 --- /dev/null +++ b/project/app/views/professors/show.html.erb @@ -0,0 +1,10 @@ +

<%= notice %>

+ +<%= render @professor %> + +
+ <%= link_to "Edit this professor", edit_professor_path(@professor) %> | + <%= link_to "Back to professors", professors_path %> + + <%= button_to "Destroy this professor", @professor, method: :delete %> +
diff --git a/project/app/views/professors/show.json.jbuilder b/project/app/views/professors/show.json.jbuilder new file mode 100644 index 0000000000..26daa8ac44 --- /dev/null +++ b/project/app/views/professors/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! "professors/professor", professor: @professor diff --git a/project/app/views/questaos/_form.html.erb b/project/app/views/questaos/_form.html.erb new file mode 100644 index 0000000000..d5c685e11e --- /dev/null +++ b/project/app/views/questaos/_form.html.erb @@ -0,0 +1,37 @@ +<%= form_with(model: questao) do |form| %> + <% if questao.errors.any? %> +
+

<%= pluralize(questao.errors.count, "error") %> prohibited this questao from being saved:

+ +
    + <% questao.errors.each do |error| %> +
  • <%= error.full_message %>
  • + <% end %> +
+
+ <% end %> + +
+ <%= form.label :enunciado, style: "display: block" %> + <%= form.textarea :enunciado %> +
+ +
+ <%= form.label :tipo, style: "display: block" %> + <%= form.text_field :tipo %> +
+ +
+ <%= form.label :template_id, style: "display: block" %> + <%= form.text_field :template_id %> +
+ +
+ <%= form.label :formulario_id, style: "display: block" %> + <%= form.text_field :formulario_id %> +
+ +
+ <%= form.submit %> +
+<% end %> diff --git a/project/app/views/questaos/_questao.html.erb b/project/app/views/questaos/_questao.html.erb new file mode 100644 index 0000000000..9ce03f211e --- /dev/null +++ b/project/app/views/questaos/_questao.html.erb @@ -0,0 +1,22 @@ +
+
+ Enunciado: + <%= questao.enunciado %> +
+ +
+ Tipo: + <%= questao.tipo %> +
+ +
+ Template: + <%= questao.template_id %> +
+ +
+ Formulario: + <%= questao.formulario_id %> +
+ +
diff --git a/project/app/views/questaos/_questao.json.jbuilder b/project/app/views/questaos/_questao.json.jbuilder new file mode 100644 index 0000000000..662859fe77 --- /dev/null +++ b/project/app/views/questaos/_questao.json.jbuilder @@ -0,0 +1,2 @@ +json.extract! questao, :id, :enunciado, :tipo, :template_id, :formulario_id, :created_at, :updated_at +json.url questao_url(questao, format: :json) diff --git a/project/app/views/questaos/edit.html.erb b/project/app/views/questaos/edit.html.erb new file mode 100644 index 0000000000..a204930ccf --- /dev/null +++ b/project/app/views/questaos/edit.html.erb @@ -0,0 +1,12 @@ +<% content_for :title, "Editing questao" %> + +

Editing questao

+ +<%= render "form", questao: @questao %> + +
+ +
+ <%= link_to "Show this questao", @questao %> | + <%= link_to "Back to questaos", questaos_path %> +
diff --git a/project/app/views/questaos/index.html.erb b/project/app/views/questaos/index.html.erb new file mode 100644 index 0000000000..089cc8d99b --- /dev/null +++ b/project/app/views/questaos/index.html.erb @@ -0,0 +1,16 @@ +

<%= notice %>

+ +<% content_for :title, "Questaos" %> + +

Questaos

+ +
+ <% @questaos.each do |questao| %> + <%= render questao %> +

+ <%= link_to "Show this questao", questao %> +

+ <% end %> +
+ +<%= link_to "New questao", new_questao_path %> diff --git a/project/app/views/questaos/index.json.jbuilder b/project/app/views/questaos/index.json.jbuilder new file mode 100644 index 0000000000..e25828a237 --- /dev/null +++ b/project/app/views/questaos/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @questaos, partial: "questaos/questao", as: :questao diff --git a/project/app/views/questaos/new.html.erb b/project/app/views/questaos/new.html.erb new file mode 100644 index 0000000000..34bbbeb61c --- /dev/null +++ b/project/app/views/questaos/new.html.erb @@ -0,0 +1,11 @@ +<% content_for :title, "New questao" %> + +

New questao

+ +<%= render "form", questao: @questao %> + +
+ +
+ <%= link_to "Back to questaos", questaos_path %> +
diff --git a/project/app/views/questaos/show.html.erb b/project/app/views/questaos/show.html.erb new file mode 100644 index 0000000000..f412afb6aa --- /dev/null +++ b/project/app/views/questaos/show.html.erb @@ -0,0 +1,10 @@ +

<%= notice %>

+ +<%= render @questao %> + +
+ <%= link_to "Edit this questao", edit_questao_path(@questao) %> | + <%= link_to "Back to questaos", questaos_path %> + + <%= button_to "Destroy this questao", @questao, method: :delete %> +
diff --git a/project/app/views/questaos/show.json.jbuilder b/project/app/views/questaos/show.json.jbuilder new file mode 100644 index 0000000000..8ba7b1b064 --- /dev/null +++ b/project/app/views/questaos/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! "questaos/questao", questao: @questao diff --git a/project/app/views/resposta/_form.html.erb b/project/app/views/resposta/_form.html.erb new file mode 100644 index 0000000000..68ba2dfbbd --- /dev/null +++ b/project/app/views/resposta/_form.html.erb @@ -0,0 +1,37 @@ +<%= form_with(model: respostum) do |form| %> + <% if respostum.errors.any? %> +
+

<%= pluralize(respostum.errors.count, "error") %> prohibited this respostum from being saved:

+ +
    + <% respostum.errors.each do |error| %> +
  • <%= error.full_message %>
  • + <% end %> +
+
+ <% end %> + +
+ <%= form.label :valor_texto, style: "display: block" %> + <%= form.textarea :valor_texto %> +
+ +
+ <%= form.label :valor_numerico, style: "display: block" %> + <%= form.number_field :valor_numerico %> +
+ +
+ <%= form.label :submissao_id, style: "display: block" %> + <%= form.text_field :submissao_id %> +
+ +
+ <%= form.label :questao_id, style: "display: block" %> + <%= form.text_field :questao_id %> +
+ +
+ <%= form.submit %> +
+<% end %> diff --git a/project/app/views/resposta/_respostum.html.erb b/project/app/views/resposta/_respostum.html.erb new file mode 100644 index 0000000000..cea81e4be5 --- /dev/null +++ b/project/app/views/resposta/_respostum.html.erb @@ -0,0 +1,22 @@ +
+
+ Valor texto: + <%= respostum.valor_texto %> +
+ +
+ Valor numerico: + <%= respostum.valor_numerico %> +
+ +
+ Submissao: + <%= respostum.submissao_id %> +
+ +
+ Questao: + <%= respostum.questao_id %> +
+ +
diff --git a/project/app/views/resposta/_respostum.json.jbuilder b/project/app/views/resposta/_respostum.json.jbuilder new file mode 100644 index 0000000000..8d02d3b179 --- /dev/null +++ b/project/app/views/resposta/_respostum.json.jbuilder @@ -0,0 +1,2 @@ +json.extract! respostum, :id, :valor_texto, :valor_numerico, :submissao_id, :questao_id, :created_at, :updated_at +json.url respostum_url(respostum, format: :json) diff --git a/project/app/views/resposta/edit.html.erb b/project/app/views/resposta/edit.html.erb new file mode 100644 index 0000000000..e84eb42170 --- /dev/null +++ b/project/app/views/resposta/edit.html.erb @@ -0,0 +1,12 @@ +<% content_for :title, "Editing respostum" %> + +

Editing respostum

+ +<%= render "form", respostum: @respostum %> + +
+ +
+ <%= link_to "Show this respostum", @respostum %> | + <%= link_to "Back to resposta", resposta_path %> +
diff --git a/project/app/views/resposta/index.html.erb b/project/app/views/resposta/index.html.erb new file mode 100644 index 0000000000..26759e3546 --- /dev/null +++ b/project/app/views/resposta/index.html.erb @@ -0,0 +1,16 @@ +

<%= notice %>

+ +<% content_for :title, "Resposta" %> + +

Resposta

+ +
+ <% @resposta.each do |respostum| %> + <%= render respostum %> +

+ <%= link_to "Show this respostum", respostum %> +

+ <% end %> +
+ +<%= link_to "New respostum", new_respostum_path %> diff --git a/project/app/views/resposta/index.json.jbuilder b/project/app/views/resposta/index.json.jbuilder new file mode 100644 index 0000000000..fb2638c307 --- /dev/null +++ b/project/app/views/resposta/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @resposta, partial: "resposta/respostum", as: :respostum diff --git a/project/app/views/resposta/new.html.erb b/project/app/views/resposta/new.html.erb new file mode 100644 index 0000000000..2fc7728bc6 --- /dev/null +++ b/project/app/views/resposta/new.html.erb @@ -0,0 +1,11 @@ +<% content_for :title, "New respostum" %> + +

New respostum

+ +<%= render "form", respostum: @respostum %> + +
+ +
+ <%= link_to "Back to resposta", resposta_path %> +
diff --git a/project/app/views/resposta/show.html.erb b/project/app/views/resposta/show.html.erb new file mode 100644 index 0000000000..e486f52189 --- /dev/null +++ b/project/app/views/resposta/show.html.erb @@ -0,0 +1,10 @@ +

<%= notice %>

+ +<%= render @respostum %> + +
+ <%= link_to "Edit this respostum", edit_respostum_path(@respostum) %> | + <%= link_to "Back to resposta", resposta_path %> + + <%= button_to "Destroy this respostum", @respostum, method: :delete %> +
diff --git a/project/app/views/resposta/show.json.jbuilder b/project/app/views/resposta/show.json.jbuilder new file mode 100644 index 0000000000..f1575b75b6 --- /dev/null +++ b/project/app/views/resposta/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! "resposta/respostum", respostum: @respostum diff --git a/project/app/views/submissaos/_form.html.erb b/project/app/views/submissaos/_form.html.erb new file mode 100644 index 0000000000..46a15dc54a --- /dev/null +++ b/project/app/views/submissaos/_form.html.erb @@ -0,0 +1,27 @@ +<%= form_with(model: submissao) do |form| %> + <% if submissao.errors.any? %> +
+

<%= pluralize(submissao.errors.count, "error") %> prohibited this submissao from being saved:

+ +
    + <% submissao.errors.each do |error| %> +
  • <%= error.full_message %>
  • + <% end %> +
+
+ <% end %> + +
+ <%= form.label :formulario_id, style: "display: block" %> + <%= form.text_field :formulario_id %> +
+ +
+ <%= form.label :participant_id, style: "display: block" %> + <%= form.text_field :participant_id %> +
+ +
+ <%= form.submit %> +
+<% end %> diff --git a/project/app/views/submissaos/_submissao.html.erb b/project/app/views/submissaos/_submissao.html.erb new file mode 100644 index 0000000000..4198defe9f --- /dev/null +++ b/project/app/views/submissaos/_submissao.html.erb @@ -0,0 +1,12 @@ +
+
+ Formulario: + <%= submissao.formulario_id %> +
+ +
+ Participant: + <%= submissao.participant_id %> +
+ +
diff --git a/project/app/views/submissaos/_submissao.json.jbuilder b/project/app/views/submissaos/_submissao.json.jbuilder new file mode 100644 index 0000000000..137ba4d6d4 --- /dev/null +++ b/project/app/views/submissaos/_submissao.json.jbuilder @@ -0,0 +1,2 @@ +json.extract! submissao, :id, :formulario_id, :participant_id, :participant_type, :created_at, :updated_at +json.url submissao_url(submissao, format: :json) diff --git a/project/app/views/submissaos/edit.html.erb b/project/app/views/submissaos/edit.html.erb new file mode 100644 index 0000000000..61cab3d323 --- /dev/null +++ b/project/app/views/submissaos/edit.html.erb @@ -0,0 +1,12 @@ +<% content_for :title, "Editing submissao" %> + +

Editing submissao

+ +<%= render "form", submissao: @submissao %> + +
+ +
+ <%= link_to "Show this submissao", @submissao %> | + <%= link_to "Back to submissaos", submissaos_path %> +
diff --git a/project/app/views/submissaos/index.html.erb b/project/app/views/submissaos/index.html.erb new file mode 100644 index 0000000000..a32601b79b --- /dev/null +++ b/project/app/views/submissaos/index.html.erb @@ -0,0 +1,16 @@ +

<%= notice %>

+ +<% content_for :title, "Submissaos" %> + +

Submissaos

+ +
+ <% @submissaos.each do |submissao| %> + <%= render submissao %> +

+ <%= link_to "Show this submissao", submissao %> +

+ <% end %> +
+ +<%= link_to "New submissao", new_submissao_path %> diff --git a/project/app/views/submissaos/index.json.jbuilder b/project/app/views/submissaos/index.json.jbuilder new file mode 100644 index 0000000000..8dd82ca5fe --- /dev/null +++ b/project/app/views/submissaos/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @submissaos, partial: "submissaos/submissao", as: :submissao diff --git a/project/app/views/submissaos/new.html.erb b/project/app/views/submissaos/new.html.erb new file mode 100644 index 0000000000..0b8c26e521 --- /dev/null +++ b/project/app/views/submissaos/new.html.erb @@ -0,0 +1,11 @@ +<% content_for :title, "New submissao" %> + +

New submissao

+ +<%= render "form", submissao: @submissao %> + +
+ +
+ <%= link_to "Back to submissaos", submissaos_path %> +
diff --git a/project/app/views/submissaos/show.html.erb b/project/app/views/submissaos/show.html.erb new file mode 100644 index 0000000000..3eaa6a8675 --- /dev/null +++ b/project/app/views/submissaos/show.html.erb @@ -0,0 +1,10 @@ +

<%= notice %>

+ +<%= render @submissao %> + +
+ <%= link_to "Edit this submissao", edit_submissao_path(@submissao) %> | + <%= link_to "Back to submissaos", submissaos_path %> + + <%= button_to "Destroy this submissao", @submissao, method: :delete %> +
diff --git a/project/app/views/submissaos/show.json.jbuilder b/project/app/views/submissaos/show.json.jbuilder new file mode 100644 index 0000000000..e8d183940c --- /dev/null +++ b/project/app/views/submissaos/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! "submissaos/submissao", submissao: @submissao diff --git a/project/app/views/templates/_form.html.erb b/project/app/views/templates/_form.html.erb new file mode 100644 index 0000000000..1f81fa00b5 --- /dev/null +++ b/project/app/views/templates/_form.html.erb @@ -0,0 +1,32 @@ +<%= form_with(model: template) do |form| %> + <% if template.errors.any? %> +
+

<%= pluralize(template.errors.count, "error") %> prohibited this template from being saved:

+ +
    + <% template.errors.each do |error| %> +
  • <%= error.full_message %>
  • + <% end %> +
+
+ <% end %> + +
+ <%= form.label :title, style: "display: block" %> + <%= form.text_field :title %> +
+ +
+ <%= form.label :target_role, style: "display: block" %> + <%= form.text_field :target_role %> +
+ +
+ <%= form.label :admin_id, style: "display: block" %> + <%= form.text_field :admin_id %> +
+ +
+ <%= form.submit %> +
+<% end %> diff --git a/project/app/views/templates/_template.html.erb b/project/app/views/templates/_template.html.erb new file mode 100644 index 0000000000..0a176012f7 --- /dev/null +++ b/project/app/views/templates/_template.html.erb @@ -0,0 +1,17 @@ +
+
+ Title: + <%= template.title %> +
+ +
+ Target role: + <%= template.target_role %> +
+ +
+ Admin: + <%= template.admin_id %> +
+ +
diff --git a/project/app/views/templates/_template.json.jbuilder b/project/app/views/templates/_template.json.jbuilder new file mode 100644 index 0000000000..aad7a56810 --- /dev/null +++ b/project/app/views/templates/_template.json.jbuilder @@ -0,0 +1,2 @@ +json.extract! template, :id, :title, :target_role, :admin_id, :created_at, :updated_at +json.url template_url(template, format: :json) diff --git a/project/app/views/templates/edit.html.erb b/project/app/views/templates/edit.html.erb new file mode 100644 index 0000000000..2fe355a9c7 --- /dev/null +++ b/project/app/views/templates/edit.html.erb @@ -0,0 +1,12 @@ +<% content_for :title, "Editing template" %> + +

Editing template

+ +<%= render "form", template: @template %> + +
+ +
+ <%= link_to "Show this template", @template %> | + <%= link_to "Back to templates", templates_path %> +
diff --git a/project/app/views/templates/index.html.erb b/project/app/views/templates/index.html.erb new file mode 100644 index 0000000000..93d4cd21c2 --- /dev/null +++ b/project/app/views/templates/index.html.erb @@ -0,0 +1,16 @@ +

<%= notice %>

+ +<% content_for :title, "Templates" %> + +

Templates

+ +
+ <% @templates.each do |template| %> + <%= render template %> +

+ <%= link_to "Show this template", template %> +

+ <% end %> +
+ +<%= link_to "New template", new_template_path %> diff --git a/project/app/views/templates/index.json.jbuilder b/project/app/views/templates/index.json.jbuilder new file mode 100644 index 0000000000..5163fa0e1e --- /dev/null +++ b/project/app/views/templates/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @templates, partial: "templates/template", as: :template diff --git a/project/app/views/templates/new.html.erb b/project/app/views/templates/new.html.erb new file mode 100644 index 0000000000..bd2d47148d --- /dev/null +++ b/project/app/views/templates/new.html.erb @@ -0,0 +1,11 @@ +<% content_for :title, "New template" %> + +

New template

+ +<%= render "form", template: @template %> + +
+ +
+ <%= link_to "Back to templates", templates_path %> +
diff --git a/project/app/views/templates/show.html.erb b/project/app/views/templates/show.html.erb new file mode 100644 index 0000000000..e610456e09 --- /dev/null +++ b/project/app/views/templates/show.html.erb @@ -0,0 +1,10 @@ +

<%= notice %>

+ +<%= render @template %> + +
+ <%= link_to "Edit this template", edit_template_path(@template) %> | + <%= link_to "Back to templates", templates_path %> + + <%= button_to "Destroy this template", @template, method: :delete %> +
diff --git a/project/app/views/templates/show.json.jbuilder b/project/app/views/templates/show.json.jbuilder new file mode 100644 index 0000000000..33110ec93b --- /dev/null +++ b/project/app/views/templates/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! "templates/template", template: @template diff --git a/project/app/views/turmas/_form.html.erb b/project/app/views/turmas/_form.html.erb new file mode 100644 index 0000000000..1b20d18eb5 --- /dev/null +++ b/project/app/views/turmas/_form.html.erb @@ -0,0 +1,52 @@ +<%= form_with(model: turma) do |form| %> + <% if turma.errors.any? %> +
+

<%= pluralize(turma.errors.count, "error") %> prohibited this turma from being saved:

+ +
    + <% turma.errors.each do |error| %> +
  • <%= error.full_message %>
  • + <% end %> +
+
+ <% end %> + +
+ <%= form.label :subject_code, style: "display: block" %> + <%= form.text_field :subject_code %> +
+ +
+ <%= form.label :subject_name, style: "display: block" %> + <%= form.text_field :subject_name %> +
+ +
+ <%= form.label :class_code, style: "display: block" %> + <%= form.text_field :class_code %> +
+ +
+ <%= form.label :semester, style: "display: block" %> + <%= form.text_field :semester %> +
+ +
+ <%= form.label :time, style: "display: block" %> + <%= form.text_field :time %> +
+ +
+ <%= form.label :departamento_id, style: "display: block" %> + <%= form.text_field :departamento_id %> +
+ +
+ <%= form.label :professor_id, style: "display: block" %> + <%= form.text_field :professor_id %> +
+ +
+ <%= form.submit %> +
+<% end %> diff --git a/project/app/views/turmas/_turma.html.erb b/project/app/views/turmas/_turma.html.erb new file mode 100644 index 0000000000..bb0d01b188 --- /dev/null +++ b/project/app/views/turmas/_turma.html.erb @@ -0,0 +1,37 @@ +
+
+ Subject code: + <%= turma.subject_code %> +
+ +
+ Subject name: + <%= turma.subject_name %> +
+ +
+ Class code: + <%= turma.class_code %> +
+ +
+ Semester: + <%= turma.semester %> +
+ +
+ Time: + <%= turma.time %> +
+ +
+ Departamento: + <%= turma.departamento_id %> +
+ +
+ Professor: + <%= turma.professor_id %> +
+ +
diff --git a/project/app/views/turmas/_turma.json.jbuilder b/project/app/views/turmas/_turma.json.jbuilder new file mode 100644 index 0000000000..62063817a2 --- /dev/null +++ b/project/app/views/turmas/_turma.json.jbuilder @@ -0,0 +1,2 @@ +json.extract! turma, :id, :subject_code, :subject_name, :class_code, :semester, :time, :departamento_id, :professor_id, :created_at, :updated_at +json.url turma_url(turma, format: :json) diff --git a/project/app/views/turmas/edit.html.erb b/project/app/views/turmas/edit.html.erb new file mode 100644 index 0000000000..108b99a6c2 --- /dev/null +++ b/project/app/views/turmas/edit.html.erb @@ -0,0 +1,12 @@ +<% content_for :title, "Editing turma" %> + +

Editing turma

+ +<%= render "form", turma: @turma %> + +
+ +
+ <%= link_to "Show this turma", @turma %> | + <%= link_to "Back to turmas", turmas_path %> +
diff --git a/project/app/views/turmas/index.html.erb b/project/app/views/turmas/index.html.erb new file mode 100644 index 0000000000..72ec97a969 --- /dev/null +++ b/project/app/views/turmas/index.html.erb @@ -0,0 +1,16 @@ +

<%= notice %>

+ +<% content_for :title, "Turmas" %> + +

Turmas

+ +
+ <% @turmas.each do |turma| %> + <%= render turma %> +

+ <%= link_to "Show this turma", turma %> +

+ <% end %> +
+ +<%= link_to "New turma", new_turma_path %> diff --git a/project/app/views/turmas/index.json.jbuilder b/project/app/views/turmas/index.json.jbuilder new file mode 100644 index 0000000000..e70b9255db --- /dev/null +++ b/project/app/views/turmas/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @turmas, partial: "turmas/turma", as: :turma diff --git a/project/app/views/turmas/new.html.erb b/project/app/views/turmas/new.html.erb new file mode 100644 index 0000000000..477e3282ff --- /dev/null +++ b/project/app/views/turmas/new.html.erb @@ -0,0 +1,11 @@ +<% content_for :title, "New turma" %> + +

New turma

+ +<%= render "form", turma: @turma %> + +
+ +
+ <%= link_to "Back to turmas", turmas_path %> +
diff --git a/project/app/views/turmas/show.html.erb b/project/app/views/turmas/show.html.erb new file mode 100644 index 0000000000..72fb58239d --- /dev/null +++ b/project/app/views/turmas/show.html.erb @@ -0,0 +1,10 @@ +

<%= notice %>

+ +<%= render @turma %> + +
+ <%= link_to "Edit this turma", edit_turma_path(@turma) %> | + <%= link_to "Back to turmas", turmas_path %> + + <%= button_to "Destroy this turma", @turma, method: :delete %> +
diff --git a/project/app/views/turmas/show.json.jbuilder b/project/app/views/turmas/show.json.jbuilder new file mode 100644 index 0000000000..1d164186aa --- /dev/null +++ b/project/app/views/turmas/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! "turmas/turma", turma: @turma diff --git a/project/config/routes.rb b/project/config/routes.rb index 48254e88ed..77cc61de02 100644 --- a/project/config/routes.rb +++ b/project/config/routes.rb @@ -1,4 +1,14 @@ Rails.application.routes.draw do + resources :resposta + resources :submissaos + resources :questaos + resources :formularios + resources :templates + resources :turmas + resources :professors + resources :admins + resources :alunos + resources :departamentos # 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. diff --git a/project/db/migrate/20260611134150_create_departamentos.rb b/project/db/migrate/20260611134150_create_departamentos.rb new file mode 100644 index 0000000000..486d3458cf --- /dev/null +++ b/project/db/migrate/20260611134150_create_departamentos.rb @@ -0,0 +1,10 @@ +class CreateDepartamentos < ActiveRecord::Migration[8.1] + def change + create_table :departamentos do |t| + t.string :name + t.string :code + + t.timestamps + end + end +end diff --git a/project/db/migrate/20260611134210_create_alunos.rb b/project/db/migrate/20260611134210_create_alunos.rb new file mode 100644 index 0000000000..3975496e21 --- /dev/null +++ b/project/db/migrate/20260611134210_create_alunos.rb @@ -0,0 +1,13 @@ +class CreateAlunos < ActiveRecord::Migration[8.1] + def change + create_table :alunos do |t| + t.string :matricula + t.string :name + t.string :email + t.string :password_digest + t.string :course + + t.timestamps + end + end +end diff --git a/project/db/migrate/20260611134235_create_admins.rb b/project/db/migrate/20260611134235_create_admins.rb new file mode 100644 index 0000000000..08f8aa503f --- /dev/null +++ b/project/db/migrate/20260611134235_create_admins.rb @@ -0,0 +1,13 @@ +class CreateAdmins < ActiveRecord::Migration[8.1] + def change + create_table :admins do |t| + t.string :username + t.string :name + t.string :email + t.string :password_digest + t.references :departamento, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/project/db/migrate/20260611134249_create_professors.rb b/project/db/migrate/20260611134249_create_professors.rb new file mode 100644 index 0000000000..1fe1fec171 --- /dev/null +++ b/project/db/migrate/20260611134249_create_professors.rb @@ -0,0 +1,14 @@ +class CreateProfessors < ActiveRecord::Migration[8.1] + def change + create_table :professors do |t| + t.string :matricula + t.string :name + t.string :email + t.string :password_digest + t.string :formation + t.references :departamento, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/project/db/migrate/20260611134304_create_turmas.rb b/project/db/migrate/20260611134304_create_turmas.rb new file mode 100644 index 0000000000..9c584091c2 --- /dev/null +++ b/project/db/migrate/20260611134304_create_turmas.rb @@ -0,0 +1,15 @@ +class CreateTurmas < ActiveRecord::Migration[8.1] + def change + create_table :turmas do |t| + t.string :subject_code + t.string :subject_name + t.string :class_code + t.string :semester + t.string :time + t.references :departamento, null: false, foreign_key: true + t.references :professor, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/project/db/migrate/20260611134355_create_turma_alunos.rb b/project/db/migrate/20260611134355_create_turma_alunos.rb new file mode 100644 index 0000000000..f5a2c39e31 --- /dev/null +++ b/project/db/migrate/20260611134355_create_turma_alunos.rb @@ -0,0 +1,10 @@ +class CreateTurmaAlunos < ActiveRecord::Migration[8.1] + def change + create_table :turma_alunos do |t| + t.references :aluno, null: false, foreign_key: true + t.references :turma, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/project/db/migrate/20260611134403_create_templates.rb b/project/db/migrate/20260611134403_create_templates.rb new file mode 100644 index 0000000000..e56ae85339 --- /dev/null +++ b/project/db/migrate/20260611134403_create_templates.rb @@ -0,0 +1,11 @@ +class CreateTemplates < ActiveRecord::Migration[8.1] + def change + create_table :templates do |t| + t.string :title + t.string :target_role + t.references :admin, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/project/db/migrate/20260611134426_create_formularios.rb b/project/db/migrate/20260611134426_create_formularios.rb new file mode 100644 index 0000000000..8a2684cf87 --- /dev/null +++ b/project/db/migrate/20260611134426_create_formularios.rb @@ -0,0 +1,14 @@ +class CreateFormularios < ActiveRecord::Migration[8.1] + def change + create_table :formularios do |t| + t.string :title + t.string :target_role + t.string :status + t.references :template, null: false, foreign_key: true + t.references :turma, null: false, foreign_key: true + t.references :admin, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/project/db/migrate/20260611134433_create_questaos.rb b/project/db/migrate/20260611134433_create_questaos.rb new file mode 100644 index 0000000000..5bd2781a26 --- /dev/null +++ b/project/db/migrate/20260611134433_create_questaos.rb @@ -0,0 +1,12 @@ +class CreateQuestaos < ActiveRecord::Migration[8.1] + def change + create_table :questaos do |t| + t.text :enunciado + t.string :tipo + t.references :template, null: true, foreign_key: true + t.references :formulario, null: true, foreign_key: true + + t.timestamps + end + end +end diff --git a/project/db/migrate/20260611134446_create_submissaos.rb b/project/db/migrate/20260611134446_create_submissaos.rb new file mode 100644 index 0000000000..d59930feb7 --- /dev/null +++ b/project/db/migrate/20260611134446_create_submissaos.rb @@ -0,0 +1,10 @@ +class CreateSubmissaos < ActiveRecord::Migration[8.1] + def change + create_table :submissaos do |t| + t.references :formulario, null: false, foreign_key: true + t.references :participant, polymorphic: true, null: false + + t.timestamps + end + end +end diff --git a/project/db/migrate/20260611134500_create_resposta.rb b/project/db/migrate/20260611134500_create_resposta.rb new file mode 100644 index 0000000000..c40ef181da --- /dev/null +++ b/project/db/migrate/20260611134500_create_resposta.rb @@ -0,0 +1,12 @@ +class CreateResposta < ActiveRecord::Migration[8.1] + def change + create_table :resposta do |t| + t.text :valor_texto + t.integer :valor_numerico + t.references :submissao, null: false, foreign_key: true + t.references :questao, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/project/db/schema.rb b/project/db/schema.rb new file mode 100644 index 0000000000..e893bcf628 --- /dev/null +++ b/project/db/schema.rb @@ -0,0 +1,147 @@ +# 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_06_11_134500) do + create_table "admins", force: :cascade do |t| + t.datetime "created_at", null: false + t.integer "departamento_id", null: false + t.string "email" + t.string "name" + t.string "password_digest" + t.datetime "updated_at", null: false + t.string "username" + t.index ["departamento_id"], name: "index_admins_on_departamento_id" + end + + create_table "alunos", force: :cascade do |t| + t.string "course" + t.datetime "created_at", null: false + t.string "email" + t.string "matricula" + t.string "name" + t.string "password_digest" + t.datetime "updated_at", null: false + end + + create_table "departamentos", 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 "formularios", force: :cascade do |t| + t.integer "admin_id", null: false + t.datetime "created_at", null: false + t.string "status" + t.string "target_role" + t.integer "template_id", null: false + t.string "title" + t.integer "turma_id", null: false + t.datetime "updated_at", null: false + t.index ["admin_id"], name: "index_formularios_on_admin_id" + t.index ["template_id"], name: "index_formularios_on_template_id" + t.index ["turma_id"], name: "index_formularios_on_turma_id" + end + + create_table "professors", force: :cascade do |t| + t.datetime "created_at", null: false + t.integer "departamento_id", null: false + t.string "email" + t.string "formation" + t.string "matricula" + t.string "name" + t.string "password_digest" + t.datetime "updated_at", null: false + t.index ["departamento_id"], name: "index_professors_on_departamento_id" + end + + create_table "questaos", force: :cascade do |t| + t.datetime "created_at", null: false + t.text "enunciado" + t.integer "formulario_id" + t.integer "template_id" + t.string "tipo" + t.datetime "updated_at", null: false + t.index ["formulario_id"], name: "index_questaos_on_formulario_id" + t.index ["template_id"], name: "index_questaos_on_template_id" + end + + create_table "resposta", force: :cascade do |t| + t.datetime "created_at", null: false + t.integer "questao_id", null: false + t.integer "submissao_id", null: false + t.datetime "updated_at", null: false + t.integer "valor_numerico" + t.text "valor_texto" + t.index ["questao_id"], name: "index_resposta_on_questao_id" + t.index ["submissao_id"], name: "index_resposta_on_submissao_id" + end + + create_table "submissaos", force: :cascade do |t| + t.datetime "created_at", null: false + t.integer "formulario_id", null: false + t.integer "participant_id", null: false + t.string "participant_type", null: false + t.datetime "updated_at", null: false + t.index ["formulario_id"], name: "index_submissaos_on_formulario_id" + t.index ["participant_type", "participant_id"], name: "index_submissaos_on_participant" + end + + create_table "templates", force: :cascade do |t| + t.integer "admin_id", null: false + t.datetime "created_at", null: false + t.string "target_role" + t.string "title" + t.datetime "updated_at", null: false + t.index ["admin_id"], name: "index_templates_on_admin_id" + end + + create_table "turma_alunos", force: :cascade do |t| + t.integer "aluno_id", null: false + t.datetime "created_at", null: false + t.integer "turma_id", null: false + t.datetime "updated_at", null: false + t.index ["aluno_id"], name: "index_turma_alunos_on_aluno_id" + t.index ["turma_id"], name: "index_turma_alunos_on_turma_id" + end + + create_table "turmas", force: :cascade do |t| + t.string "class_code" + t.datetime "created_at", null: false + t.integer "departamento_id", null: false + t.integer "professor_id", null: false + t.string "semester" + t.string "subject_code" + t.string "subject_name" + t.string "time" + t.datetime "updated_at", null: false + t.index ["departamento_id"], name: "index_turmas_on_departamento_id" + t.index ["professor_id"], name: "index_turmas_on_professor_id" + end + + add_foreign_key "admins", "departamentos" + add_foreign_key "formularios", "admins" + add_foreign_key "formularios", "templates" + add_foreign_key "formularios", "turmas" + add_foreign_key "professors", "departamentos" + add_foreign_key "questaos", "formularios" + add_foreign_key "questaos", "templates" + add_foreign_key "resposta", "questaos" + add_foreign_key "resposta", "submissaos" + add_foreign_key "submissaos", "formularios" + add_foreign_key "templates", "admins" + add_foreign_key "turma_alunos", "alunos" + add_foreign_key "turma_alunos", "turmas" + add_foreign_key "turmas", "departamentos" + add_foreign_key "turmas", "professors" +end diff --git a/project/test/controllers/admins_controller_test.rb b/project/test/controllers/admins_controller_test.rb new file mode 100644 index 0000000000..8580113c65 --- /dev/null +++ b/project/test/controllers/admins_controller_test.rb @@ -0,0 +1,48 @@ +require "test_helper" + +class AdminsControllerTest < ActionDispatch::IntegrationTest + setup do + @admin = admins(:one) + end + + test "should get index" do + get admins_url + assert_response :success + end + + test "should get new" do + get new_admin_url + assert_response :success + end + + test "should create admin" do + assert_difference("Admin.count") do + post admins_url, params: { admin: { departamento_id: @admin.departamento_id, email: @admin.email, name: @admin.name, password_digest: @admin.password_digest, username: @admin.username } } + end + + assert_redirected_to admin_url(Admin.last) + end + + test "should show admin" do + get admin_url(@admin) + assert_response :success + end + + test "should get edit" do + get edit_admin_url(@admin) + assert_response :success + end + + test "should update admin" do + patch admin_url(@admin), params: { admin: { departamento_id: @admin.departamento_id, email: @admin.email, name: @admin.name, password_digest: @admin.password_digest, username: @admin.username } } + assert_redirected_to admin_url(@admin) + end + + test "should destroy admin" do + assert_difference("Admin.count", -1) do + delete admin_url(@admin) + end + + assert_redirected_to admins_url + end +end diff --git a/project/test/controllers/alunos_controller_test.rb b/project/test/controllers/alunos_controller_test.rb new file mode 100644 index 0000000000..b99d6f547a --- /dev/null +++ b/project/test/controllers/alunos_controller_test.rb @@ -0,0 +1,48 @@ +require "test_helper" + +class AlunosControllerTest < ActionDispatch::IntegrationTest + setup do + @aluno = alunos(:one) + end + + test "should get index" do + get alunos_url + assert_response :success + end + + test "should get new" do + get new_aluno_url + assert_response :success + end + + test "should create aluno" do + assert_difference("Aluno.count") do + post alunos_url, params: { aluno: { course: @aluno.course, email: @aluno.email, matricula: @aluno.matricula, name: @aluno.name, password_digest: @aluno.password_digest } } + end + + assert_redirected_to aluno_url(Aluno.last) + end + + test "should show aluno" do + get aluno_url(@aluno) + assert_response :success + end + + test "should get edit" do + get edit_aluno_url(@aluno) + assert_response :success + end + + test "should update aluno" do + patch aluno_url(@aluno), params: { aluno: { course: @aluno.course, email: @aluno.email, matricula: @aluno.matricula, name: @aluno.name, password_digest: @aluno.password_digest } } + assert_redirected_to aluno_url(@aluno) + end + + test "should destroy aluno" do + assert_difference("Aluno.count", -1) do + delete aluno_url(@aluno) + end + + assert_redirected_to alunos_url + end +end diff --git a/project/test/controllers/departamentos_controller_test.rb b/project/test/controllers/departamentos_controller_test.rb new file mode 100644 index 0000000000..bcec0c556e --- /dev/null +++ b/project/test/controllers/departamentos_controller_test.rb @@ -0,0 +1,48 @@ +require "test_helper" + +class DepartamentosControllerTest < ActionDispatch::IntegrationTest + setup do + @departamento = departamentos(:one) + end + + test "should get index" do + get departamentos_url + assert_response :success + end + + test "should get new" do + get new_departamento_url + assert_response :success + end + + test "should create departamento" do + assert_difference("Departamento.count") do + post departamentos_url, params: { departamento: { code: @departamento.code, name: @departamento.name } } + end + + assert_redirected_to departamento_url(Departamento.last) + end + + test "should show departamento" do + get departamento_url(@departamento) + assert_response :success + end + + test "should get edit" do + get edit_departamento_url(@departamento) + assert_response :success + end + + test "should update departamento" do + patch departamento_url(@departamento), params: { departamento: { code: @departamento.code, name: @departamento.name } } + assert_redirected_to departamento_url(@departamento) + end + + test "should destroy departamento" do + assert_difference("Departamento.count", -1) do + delete departamento_url(@departamento) + end + + assert_redirected_to departamentos_url + end +end diff --git a/project/test/controllers/formularios_controller_test.rb b/project/test/controllers/formularios_controller_test.rb new file mode 100644 index 0000000000..f73e8cc6f9 --- /dev/null +++ b/project/test/controllers/formularios_controller_test.rb @@ -0,0 +1,48 @@ +require "test_helper" + +class FormulariosControllerTest < ActionDispatch::IntegrationTest + setup do + @formulario = formularios(:one) + end + + test "should get index" do + get formularios_url + assert_response :success + end + + test "should get new" do + get new_formulario_url + assert_response :success + end + + test "should create formulario" do + assert_difference("Formulario.count") do + post formularios_url, params: { formulario: { admin_id: @formulario.admin_id, status: @formulario.status, target_role: @formulario.target_role, template_id: @formulario.template_id, title: @formulario.title, turma_id: @formulario.turma_id } } + end + + assert_redirected_to formulario_url(Formulario.last) + end + + test "should show formulario" do + get formulario_url(@formulario) + assert_response :success + end + + test "should get edit" do + get edit_formulario_url(@formulario) + assert_response :success + end + + test "should update formulario" do + patch formulario_url(@formulario), params: { formulario: { admin_id: @formulario.admin_id, status: @formulario.status, target_role: @formulario.target_role, template_id: @formulario.template_id, title: @formulario.title, turma_id: @formulario.turma_id } } + assert_redirected_to formulario_url(@formulario) + end + + test "should destroy formulario" do + assert_difference("Formulario.count", -1) do + delete formulario_url(@formulario) + end + + assert_redirected_to formularios_url + end +end diff --git a/project/test/controllers/professors_controller_test.rb b/project/test/controllers/professors_controller_test.rb new file mode 100644 index 0000000000..20f8d030ac --- /dev/null +++ b/project/test/controllers/professors_controller_test.rb @@ -0,0 +1,48 @@ +require "test_helper" + +class ProfessorsControllerTest < ActionDispatch::IntegrationTest + setup do + @professor = professors(:one) + end + + test "should get index" do + get professors_url + assert_response :success + end + + test "should get new" do + get new_professor_url + assert_response :success + end + + test "should create professor" do + assert_difference("Professor.count") do + post professors_url, params: { professor: { departamento_id: @professor.departamento_id, email: @professor.email, formation: @professor.formation, matricula: @professor.matricula, name: @professor.name, password_digest: @professor.password_digest } } + end + + assert_redirected_to professor_url(Professor.last) + end + + test "should show professor" do + get professor_url(@professor) + assert_response :success + end + + test "should get edit" do + get edit_professor_url(@professor) + assert_response :success + end + + test "should update professor" do + patch professor_url(@professor), params: { professor: { departamento_id: @professor.departamento_id, email: @professor.email, formation: @professor.formation, matricula: @professor.matricula, name: @professor.name, password_digest: @professor.password_digest } } + assert_redirected_to professor_url(@professor) + end + + test "should destroy professor" do + assert_difference("Professor.count", -1) do + delete professor_url(@professor) + end + + assert_redirected_to professors_url + end +end diff --git a/project/test/controllers/questaos_controller_test.rb b/project/test/controllers/questaos_controller_test.rb new file mode 100644 index 0000000000..e7048a010b --- /dev/null +++ b/project/test/controllers/questaos_controller_test.rb @@ -0,0 +1,48 @@ +require "test_helper" + +class QuestaosControllerTest < ActionDispatch::IntegrationTest + setup do + @questao = questaos(:one) + end + + test "should get index" do + get questaos_url + assert_response :success + end + + test "should get new" do + get new_questao_url + assert_response :success + end + + test "should create questao" do + assert_difference("Questao.count") do + post questaos_url, params: { questao: { enunciado: @questao.enunciado, formulario_id: @questao.formulario_id, template_id: @questao.template_id, tipo: @questao.tipo } } + end + + assert_redirected_to questao_url(Questao.last) + end + + test "should show questao" do + get questao_url(@questao) + assert_response :success + end + + test "should get edit" do + get edit_questao_url(@questao) + assert_response :success + end + + test "should update questao" do + patch questao_url(@questao), params: { questao: { enunciado: @questao.enunciado, formulario_id: @questao.formulario_id, template_id: @questao.template_id, tipo: @questao.tipo } } + assert_redirected_to questao_url(@questao) + end + + test "should destroy questao" do + assert_difference("Questao.count", -1) do + delete questao_url(@questao) + end + + assert_redirected_to questaos_url + end +end diff --git a/project/test/controllers/resposta_controller_test.rb b/project/test/controllers/resposta_controller_test.rb new file mode 100644 index 0000000000..44d839dbfe --- /dev/null +++ b/project/test/controllers/resposta_controller_test.rb @@ -0,0 +1,48 @@ +require "test_helper" + +class RespostaControllerTest < ActionDispatch::IntegrationTest + setup do + @respostum = resposta(:one) + end + + test "should get index" do + get resposta_url + assert_response :success + end + + test "should get new" do + get new_respostum_url + assert_response :success + end + + test "should create respostum" do + assert_difference("Respostum.count") do + post resposta_url, params: { respostum: { questao_id: @respostum.questao_id, submissao_id: @respostum.submissao_id, valor_numerico: @respostum.valor_numerico, valor_texto: @respostum.valor_texto } } + end + + assert_redirected_to respostum_url(Respostum.last) + end + + test "should show respostum" do + get respostum_url(@respostum) + assert_response :success + end + + test "should get edit" do + get edit_respostum_url(@respostum) + assert_response :success + end + + test "should update respostum" do + patch respostum_url(@respostum), params: { respostum: { questao_id: @respostum.questao_id, submissao_id: @respostum.submissao_id, valor_numerico: @respostum.valor_numerico, valor_texto: @respostum.valor_texto } } + assert_redirected_to respostum_url(@respostum) + end + + test "should destroy respostum" do + assert_difference("Respostum.count", -1) do + delete respostum_url(@respostum) + end + + assert_redirected_to resposta_url + end +end diff --git a/project/test/controllers/submissaos_controller_test.rb b/project/test/controllers/submissaos_controller_test.rb new file mode 100644 index 0000000000..9fcf24d8d3 --- /dev/null +++ b/project/test/controllers/submissaos_controller_test.rb @@ -0,0 +1,48 @@ +require "test_helper" + +class SubmissaosControllerTest < ActionDispatch::IntegrationTest + setup do + @submissao = submissaos(:one) + end + + test "should get index" do + get submissaos_url + assert_response :success + end + + test "should get new" do + get new_submissao_url + assert_response :success + end + + test "should create submissao" do + assert_difference("Submissao.count") do + post submissaos_url, params: { submissao: { formulario_id: @submissao.formulario_id, participant_id: @submissao.participant_id, participant_type: @submissao.participant_type } } + end + + assert_redirected_to submissao_url(Submissao.last) + end + + test "should show submissao" do + get submissao_url(@submissao) + assert_response :success + end + + test "should get edit" do + get edit_submissao_url(@submissao) + assert_response :success + end + + test "should update submissao" do + patch submissao_url(@submissao), params: { submissao: { formulario_id: @submissao.formulario_id, participant_id: @submissao.participant_id, participant_type: @submissao.participant_type } } + assert_redirected_to submissao_url(@submissao) + end + + test "should destroy submissao" do + assert_difference("Submissao.count", -1) do + delete submissao_url(@submissao) + end + + assert_redirected_to submissaos_url + end +end diff --git a/project/test/controllers/templates_controller_test.rb b/project/test/controllers/templates_controller_test.rb new file mode 100644 index 0000000000..0759c34db9 --- /dev/null +++ b/project/test/controllers/templates_controller_test.rb @@ -0,0 +1,48 @@ +require "test_helper" + +class TemplatesControllerTest < ActionDispatch::IntegrationTest + setup do + @template = templates(:one) + end + + test "should get index" do + get templates_url + assert_response :success + end + + test "should get new" do + get new_template_url + assert_response :success + end + + test "should create template" do + assert_difference("Template.count") do + post templates_url, params: { template: { admin_id: @template.admin_id, target_role: @template.target_role, title: @template.title } } + end + + assert_redirected_to template_url(Template.last) + end + + test "should show template" do + get template_url(@template) + assert_response :success + end + + test "should get edit" do + get edit_template_url(@template) + assert_response :success + end + + test "should update template" do + patch template_url(@template), params: { template: { admin_id: @template.admin_id, target_role: @template.target_role, title: @template.title } } + assert_redirected_to template_url(@template) + end + + test "should destroy template" do + assert_difference("Template.count", -1) do + delete template_url(@template) + end + + assert_redirected_to templates_url + end +end diff --git a/project/test/controllers/turmas_controller_test.rb b/project/test/controllers/turmas_controller_test.rb new file mode 100644 index 0000000000..0602a4a51d --- /dev/null +++ b/project/test/controllers/turmas_controller_test.rb @@ -0,0 +1,48 @@ +require "test_helper" + +class TurmasControllerTest < ActionDispatch::IntegrationTest + setup do + @turma = turmas(:one) + end + + test "should get index" do + get turmas_url + assert_response :success + end + + test "should get new" do + get new_turma_url + assert_response :success + end + + test "should create turma" do + assert_difference("Turma.count") do + post turmas_url, params: { turma: { class_code: @turma.class_code, departamento_id: @turma.departamento_id, professor_id: @turma.professor_id, semester: @turma.semester, subject_code: @turma.subject_code, subject_name: @turma.subject_name, time: @turma.time } } + end + + assert_redirected_to turma_url(Turma.last) + end + + test "should show turma" do + get turma_url(@turma) + assert_response :success + end + + test "should get edit" do + get edit_turma_url(@turma) + assert_response :success + end + + test "should update turma" do + patch turma_url(@turma), params: { turma: { class_code: @turma.class_code, departamento_id: @turma.departamento_id, professor_id: @turma.professor_id, semester: @turma.semester, subject_code: @turma.subject_code, subject_name: @turma.subject_name, time: @turma.time } } + assert_redirected_to turma_url(@turma) + end + + test "should destroy turma" do + assert_difference("Turma.count", -1) do + delete turma_url(@turma) + end + + assert_redirected_to turmas_url + end +end diff --git a/project/test/fixtures/admins.yml b/project/test/fixtures/admins.yml new file mode 100644 index 0000000000..ead3a962d6 --- /dev/null +++ b/project/test/fixtures/admins.yml @@ -0,0 +1,15 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + username: MyString + name: MyString + email: MyString + password_digest: MyString + departamento: one + +two: + username: MyString + name: MyString + email: MyString + password_digest: MyString + departamento: two diff --git a/project/test/fixtures/alunos.yml b/project/test/fixtures/alunos.yml new file mode 100644 index 0000000000..436a123e1b --- /dev/null +++ b/project/test/fixtures/alunos.yml @@ -0,0 +1,15 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + matricula: MyString + name: MyString + email: MyString + password_digest: MyString + course: MyString + +two: + matricula: MyString + name: MyString + email: MyString + password_digest: MyString + course: MyString diff --git a/project/test/fixtures/departamentos.yml b/project/test/fixtures/departamentos.yml new file mode 100644 index 0000000000..ee775db7f8 --- /dev/null +++ b/project/test/fixtures/departamentos.yml @@ -0,0 +1,9 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + name: MyString + code: MyString + +two: + name: MyString + code: MyString diff --git a/project/test/fixtures/formularios.yml b/project/test/fixtures/formularios.yml new file mode 100644 index 0000000000..d923f00d76 --- /dev/null +++ b/project/test/fixtures/formularios.yml @@ -0,0 +1,17 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + title: MyString + target_role: MyString + status: MyString + template: one + turma: one + admin: one + +two: + title: MyString + target_role: MyString + status: MyString + template: two + turma: two + admin: two diff --git a/project/test/fixtures/professors.yml b/project/test/fixtures/professors.yml new file mode 100644 index 0000000000..9b9c9fb5d2 --- /dev/null +++ b/project/test/fixtures/professors.yml @@ -0,0 +1,17 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + matricula: MyString + name: MyString + email: MyString + password_digest: MyString + formation: MyString + departamento: one + +two: + matricula: MyString + name: MyString + email: MyString + password_digest: MyString + formation: MyString + departamento: two diff --git a/project/test/fixtures/questaos.yml b/project/test/fixtures/questaos.yml new file mode 100644 index 0000000000..81fe4d9782 --- /dev/null +++ b/project/test/fixtures/questaos.yml @@ -0,0 +1,13 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + enunciado: MyText + tipo: MyString + template: one + formulario: one + +two: + enunciado: MyText + tipo: MyString + template: two + formulario: two diff --git a/project/test/fixtures/resposta.yml b/project/test/fixtures/resposta.yml new file mode 100644 index 0000000000..355094eb02 --- /dev/null +++ b/project/test/fixtures/resposta.yml @@ -0,0 +1,13 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + valor_texto: MyText + valor_numerico: 1 + submissao: one + questao: one + +two: + valor_texto: MyText + valor_numerico: 1 + submissao: two + questao: two diff --git a/project/test/fixtures/submissaos.yml b/project/test/fixtures/submissaos.yml new file mode 100644 index 0000000000..ac34ae408e --- /dev/null +++ b/project/test/fixtures/submissaos.yml @@ -0,0 +1,11 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + formulario: one + participant: one + participant_type: Participant + +two: + formulario: two + participant: two + participant_type: Participant diff --git a/project/test/fixtures/templates.yml b/project/test/fixtures/templates.yml new file mode 100644 index 0000000000..eccf8371b2 --- /dev/null +++ b/project/test/fixtures/templates.yml @@ -0,0 +1,11 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + title: MyString + target_role: MyString + admin: one + +two: + title: MyString + target_role: MyString + admin: two diff --git a/project/test/fixtures/turma_alunos.yml b/project/test/fixtures/turma_alunos.yml new file mode 100644 index 0000000000..bec55c7985 --- /dev/null +++ b/project/test/fixtures/turma_alunos.yml @@ -0,0 +1,9 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + aluno: one + turma: one + +two: + aluno: two + turma: two diff --git a/project/test/fixtures/turmas.yml b/project/test/fixtures/turmas.yml new file mode 100644 index 0000000000..6298f5b41c --- /dev/null +++ b/project/test/fixtures/turmas.yml @@ -0,0 +1,19 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + subject_code: MyString + subject_name: MyString + class_code: MyString + semester: MyString + time: MyString + departamento: one + professor: one + +two: + subject_code: MyString + subject_name: MyString + class_code: MyString + semester: MyString + time: MyString + departamento: two + professor: two diff --git a/project/test/models/admin_test.rb b/project/test/models/admin_test.rb new file mode 100644 index 0000000000..33dd1944a1 --- /dev/null +++ b/project/test/models/admin_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class AdminTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/project/test/models/aluno_test.rb b/project/test/models/aluno_test.rb new file mode 100644 index 0000000000..4e070b6ffc --- /dev/null +++ b/project/test/models/aluno_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class AlunoTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/project/test/models/departamento_test.rb b/project/test/models/departamento_test.rb new file mode 100644 index 0000000000..15a03e49ef --- /dev/null +++ b/project/test/models/departamento_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class DepartamentoTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/project/test/models/formulario_test.rb b/project/test/models/formulario_test.rb new file mode 100644 index 0000000000..b3d3e2cc7f --- /dev/null +++ b/project/test/models/formulario_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class FormularioTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/project/test/models/professor_test.rb b/project/test/models/professor_test.rb new file mode 100644 index 0000000000..42dc05ee40 --- /dev/null +++ b/project/test/models/professor_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class ProfessorTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/project/test/models/questao_test.rb b/project/test/models/questao_test.rb new file mode 100644 index 0000000000..63b5513595 --- /dev/null +++ b/project/test/models/questao_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class QuestaoTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/project/test/models/respostum_test.rb b/project/test/models/respostum_test.rb new file mode 100644 index 0000000000..03ba117c95 --- /dev/null +++ b/project/test/models/respostum_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class RespostumTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/project/test/models/submissao_test.rb b/project/test/models/submissao_test.rb new file mode 100644 index 0000000000..1c54e16944 --- /dev/null +++ b/project/test/models/submissao_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class SubmissaoTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/project/test/models/template_test.rb b/project/test/models/template_test.rb new file mode 100644 index 0000000000..5bc4995c31 --- /dev/null +++ b/project/test/models/template_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class TemplateTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/project/test/models/turma_aluno_test.rb b/project/test/models/turma_aluno_test.rb new file mode 100644 index 0000000000..e2ebd6d30f --- /dev/null +++ b/project/test/models/turma_aluno_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class TurmaAlunoTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/project/test/models/turma_test.rb b/project/test/models/turma_test.rb new file mode 100644 index 0000000000..21806827d6 --- /dev/null +++ b/project/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 8b277273b1345f26885ea29bff87592b009f4a01 Mon Sep 17 00:00:00 2001 From: Gabriel Lopes Soares Damasceno Date: Sun, 14 Jun 2026 18:14:09 -0300 Subject: [PATCH 13/23] feat: respostas e exportacao CSV ( #99 e #101) --- project/app/views/formularios/show.html.erb | 4 ++++ project/app/views/resposta/_form.html.erb | 10 +++++----- project/app/views/resposta/new.html.erb | 6 +++--- project/config/routes.rb | 9 ++++++++- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/project/app/views/formularios/show.html.erb b/project/app/views/formularios/show.html.erb index 24709ac66b..14b47a5b42 100644 --- a/project/app/views/formularios/show.html.erb +++ b/project/app/views/formularios/show.html.erb @@ -2,6 +2,10 @@ <%= render @formulario %> +
+ <%= link_to "Exportar Resultados (CSV)", exportar_csv_formulario_path(@formulario), style: "padding: 10px 15px; background-color: #4CAF50; color: white; text-decoration: none; border-radius: 5px; font-weight: bold;" %> +
+
<%= link_to "Edit this formulario", edit_formulario_path(@formulario) %> | <%= link_to "Back to formularios", formularios_path %> diff --git a/project/app/views/resposta/_form.html.erb b/project/app/views/resposta/_form.html.erb index 68ba2dfbbd..5a3571c3e0 100644 --- a/project/app/views/resposta/_form.html.erb +++ b/project/app/views/resposta/_form.html.erb @@ -12,13 +12,13 @@ <% end %>
- <%= form.label :valor_texto, style: "display: block" %> - <%= form.textarea :valor_texto %> + <%= form.label :valor_texto, "Comentário (Opcional)", style: "display: block" %> + <%= form.text_area :valor_texto %>
- <%= form.label :valor_numerico, style: "display: block" %> - <%= form.number_field :valor_numerico %> + <%= form.label :Score, "Nota da Avaliação", style: "display: block" %> + <%= form.number_field :Score %>
@@ -32,6 +32,6 @@
- <%= form.submit %> + <%= form.submit "Enviar Resposta" %>
<% end %> diff --git a/project/app/views/resposta/new.html.erb b/project/app/views/resposta/new.html.erb index 2fc7728bc6..7d7ea62362 100644 --- a/project/app/views/resposta/new.html.erb +++ b/project/app/views/resposta/new.html.erb @@ -1,11 +1,11 @@ -<% content_for :title, "New respostum" %> +<% content_for :title, "Responder Avaliação" %> -

New respostum

+

Responder Avaliação

<%= render "form", respostum: @respostum %>
- <%= link_to "Back to resposta", resposta_path %> + <%= link_to "Voltar para a lista", resposta_path %>
diff --git a/project/config/routes.rb b/project/config/routes.rb index 77cc61de02..244e7869cd 100644 --- a/project/config/routes.rb +++ b/project/config/routes.rb @@ -2,7 +2,14 @@ resources :resposta resources :submissaos resources :questaos - resources :formularios + + # Aqui adicionamos a rota específica para o download do CSV vinculada ao formulário + resources :formularios do + member do + get :exportar_csv + end + end + resources :templates resources :turmas resources :professors From c546e2077cf00e23ec96dac6f1d58e6ff4ecb932 Mon Sep 17 00:00:00 2001 From: Gabriel Lopes Soares Damasceno Date: Sun, 14 Jun 2026 18:46:48 -0300 Subject: [PATCH 14/23] test: testes de integracao com rspec (#99 e #101) --- project/.rspec | 1 + project/Gemfile | 4 + project/Gemfile.lock | 26 +++++ .../app/controllers/formularios_controller.rb | 16 ++- project/config/routes.rb | 1 - project/spec/rails_helper.rb | 72 ++++++++++++++ project/spec/requests/formularios_spec.rb | 33 +++++++ project/spec/requests/respostas_spec.rb | 61 ++++++++++++ project/spec/spec_helper.rb | 98 +++++++++++++++++++ 9 files changed, 310 insertions(+), 2 deletions(-) create mode 100644 project/.rspec create mode 100644 project/spec/rails_helper.rb create mode 100644 project/spec/requests/formularios_spec.rb create mode 100644 project/spec/requests/respostas_spec.rb create mode 100644 project/spec/spec_helper.rb diff --git a/project/.rspec b/project/.rspec new file mode 100644 index 0000000000..c99d2e7396 --- /dev/null +++ b/project/.rspec @@ -0,0 +1 @@ +--require spec_helper diff --git a/project/Gemfile b/project/Gemfile index 193ebaf011..12c5414ee4 100644 --- a/project/Gemfile +++ b/project/Gemfile @@ -67,3 +67,7 @@ group :test do gem "database_cleaner" end + +gem "rspec-rails", "~> 8.0", groups: [:development, :test] + +gem "csv", "~> 3.3" diff --git a/project/Gemfile.lock b/project/Gemfile.lock index db14dd8aa5..b14c234e28 100644 --- a/project/Gemfile.lock +++ b/project/Gemfile.lock @@ -103,6 +103,7 @@ GEM concurrent-ruby (1.3.6) connection_pool (3.0.2) crass (1.0.6) + csv (3.3.5) cucumber (10.2.0) base64 (~> 0.2) builder (~> 3.2) @@ -313,6 +314,23 @@ GEM reline (0.6.3) io-console (~> 0.5) rexml (3.4.4) + rspec-core (3.13.6) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.8) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-rails (8.0.4) + actionpack (>= 7.2) + activesupport (>= 7.2) + railties (>= 7.2) + rspec-core (>= 3.13.0, < 5.0.0) + rspec-expectations (>= 3.13.0, < 5.0.0) + rspec-mocks (>= 3.13.0, < 5.0.0) + rspec-support (>= 3.13.0, < 5.0.0) + rspec-support (3.13.7) rubocop (1.86.2) json (~> 2.3) language_server-protocol (~> 3.17.0.2) @@ -442,6 +460,7 @@ DEPENDENCIES brakeman bundler-audit capybara + csv (~> 3.3) cucumber-rails database_cleaner debug @@ -452,6 +471,7 @@ DEPENDENCIES propshaft puma (>= 5.0) rails (~> 8.1.3) + rspec-rails (~> 8.0) rubocop-rails-omakase selenium-webdriver solid_cable @@ -492,6 +512,7 @@ CHECKSUMS concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a crass (1.0.6) sha256=dc516022a56e7b3b156099abc81b6d2b08ea1ed12676ac7a5657617f012bd45d + csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f cucumber (10.2.0) sha256=fdedbd31ecf40858b60f04853f2aa15c44f5c30bbac29c6a227fa1e7005a8158 cucumber-ci-environment (11.0.0) sha256=0df79a9e1d0b015b3d9def680f989200d96fef206f4d19ccf86a338c4f71d1e2 cucumber-core (16.2.0) sha256=592b58a95cf42feef8e5a349f68e363784ba3b6568ffbcf6776e38e136cf970b @@ -584,6 +605,11 @@ CHECKSUMS regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835 rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142 + rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d + rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836 + rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47 + rspec-rails (8.0.4) sha256=06235692fc0892683d3d34977e081db867434b3a24ae0dd0c6f3516bad4e22df + rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c rubocop (1.86.2) sha256=bb2e97f635eda42c448f2588f4a6ff78f221b8bdfdf65b1e9b07fbd57521b45d rubocop-ast (1.49.1) sha256=4412f3ee70f6fe4546cc489548e0f6fcf76cafcfa80fa03af67098ffed755035 rubocop-performance (1.26.1) sha256=cd19b936ff196df85829d264b522fd4f98b6c89ad271fa52744a8c11b8f71834 diff --git a/project/app/controllers/formularios_controller.rb b/project/app/controllers/formularios_controller.rb index 781158cbba..933a94f0aa 100644 --- a/project/app/controllers/formularios_controller.rb +++ b/project/app/controllers/formularios_controller.rb @@ -1,5 +1,5 @@ class FormulariosController < ApplicationController - before_action :set_formulario, only: %i[ show edit update destroy ] + before_action :set_formulario, only: %i[ show edit update destroy exportar_csv ] # GET /formularios or /formularios.json def index @@ -57,6 +57,20 @@ def destroy end end + # GET /formularios/1/exportar_csv + def exportar_csv + require 'csv' + + csv_data = CSV.generate(headers: true) do |csv| + csv << ["ID da Pergunta", "Enunciado", "Scores Atribuídos"] + + end + + send_data csv_data, + filename: "respostas_formulario_#{@formulario.id}.csv", + type: "text/csv" + end + private # Use callbacks to share common setup or constraints between actions. def set_formulario diff --git a/project/config/routes.rb b/project/config/routes.rb index 244e7869cd..3838196427 100644 --- a/project/config/routes.rb +++ b/project/config/routes.rb @@ -3,7 +3,6 @@ resources :submissaos resources :questaos - # Aqui adicionamos a rota específica para o download do CSV vinculada ao formulário resources :formularios do member do get :exportar_csv diff --git a/project/spec/rails_helper.rb b/project/spec/rails_helper.rb new file mode 100644 index 0000000000..ef75d46770 --- /dev/null +++ b/project/spec/rails_helper.rb @@ -0,0 +1,72 @@ +# This file is copied to spec/ when you run 'rails generate rspec:install' +require 'spec_helper' +ENV['RAILS_ENV'] ||= 'test' +require_relative '../config/environment' +# Prevent database truncation if the environment is production +abort("The Rails environment is running in production mode!") if Rails.env.production? +# Uncomment the line below in case you have `--require rails_helper` in the `.rspec` file +# that will avoid rails generators crashing because migrations haven't been run yet +# return unless Rails.env.test? +require 'rspec/rails' +# Add additional requires below this line. Rails is not loaded until this point! + +# Requires supporting ruby files with custom matchers and macros, etc, in +# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are +# run as spec files by default. This means that files in spec/support that end +# in _spec.rb will both be required and run as specs, causing the specs to be +# run twice. It is recommended that you do not name files matching this glob to +# end with _spec.rb. You can configure this pattern with the --pattern +# option on the command line or in ~/.rspec, .rspec or `.rspec-local`. +# +# The following line is provided for convenience purposes. It has the downside +# of increasing the boot-up time by auto-requiring all files in the support +# directory. Alternatively, in the individual `*_spec.rb` files, manually +# require only the support files necessary. +# +# Rails.root.glob('spec/support/**/*.rb').sort_by(&:to_s).each { |f| require f } + +# Ensures that the test database schema matches the current schema file. +# If there are pending migrations it will invoke `db:test:prepare` to +# recreate the test database by loading the schema. +# If you are not using ActiveRecord, you can remove these lines. +begin + ActiveRecord::Migration.maintain_test_schema! +rescue ActiveRecord::PendingMigrationError => e + abort e.to_s.strip +end +RSpec.configure do |config| + # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures + config.fixture_paths = [ + Rails.root.join('spec/fixtures') + ] + + # If you're not using ActiveRecord, or you'd prefer not to run each of your + # examples within a transaction, remove the following line or assign false + # instead of true. + config.use_transactional_fixtures = true + + # You can uncomment this line to turn off ActiveRecord support entirely. + # config.use_active_record = false + + # RSpec Rails uses metadata to mix in different behaviours to your tests, + # for example enabling you to call `get` and `post` in request specs. e.g.: + # + # RSpec.describe UsersController, type: :request do + # # ... + # end + # + # The different available types are documented in the features, such as in + # https://rspec.info/features/8-0/rspec-rails + # + # You can also infer these behaviours automatically by location, e.g. + # /spec/models would pull in the same behaviour as `type: :model` but this + # behaviour is considered legacy and will be removed in a future version. + # + # To enable this behaviour uncomment the line below. + # config.infer_spec_type_from_file_location! + + # Filter lines from Rails gems in backtraces. + config.filter_rails_from_backtrace! + # arbitrary gems may also be filtered via: + # config.filter_gems_from_backtrace("gem name") +end diff --git a/project/spec/requests/formularios_spec.rb b/project/spec/requests/formularios_spec.rb new file mode 100644 index 0000000000..b49dfa6cd5 --- /dev/null +++ b/project/spec/requests/formularios_spec.rb @@ -0,0 +1,33 @@ +require 'rails_helper' + +RSpec.describe "Formularios", type: :request do + describe "GET /formularios/:id/exportar_csv" do + it "retorna um arquivo CSV de sucesso com as respostas da avaliação" do + # 1. Base + departamento = Departamento.new + departamento.save(validate: false) + + admin = Admin.new(departamento_id: departamento.id) + admin.save(validate: false) + + professor = Professor.new(departamento_id: departamento.id) + professor.save(validate: false) + + template = Template.new(admin_id: admin.id) + template.save(validate: false) + + turma = Turma.new(departamento_id: departamento.id, professor_id: professor.id) + turma.save(validate: false) + + formulario = Formulario.new(turma_id: turma.id, template_id: template.id, admin_id: admin.id) + formulario.save(validate: false) + + get exportar_csv_formulario_path(formulario, format: :csv) + + # Validações + expect(response).to have_http_status(:success) + expect(response.media_type).to eq('text/csv') + expect(response.body).to include("ID da Pergunta", "Enunciado") + end + end +end \ No newline at end of file diff --git a/project/spec/requests/respostas_spec.rb b/project/spec/requests/respostas_spec.rb new file mode 100644 index 0000000000..b9e91c8041 --- /dev/null +++ b/project/spec/requests/respostas_spec.rb @@ -0,0 +1,61 @@ +require 'rails_helper' + +RSpec.describe "Respostas", type: :request do + describe "POST /resposta" do + it "cria uma nova resposta mapeando a nota corretamente para o atributo do banco" do + # 1. Base da hierarquia + departamento = Departamento.new + departamento.save(validate: false) + + admin = Admin.new(departamento_id: departamento.id) + admin.save(validate: false) + + professor = Professor.new(departamento_id: departamento.id) + professor.save(validate: false) + + template = Template.new(admin_id: admin.id) + template.save(validate: false) + + turma = Turma.new(departamento_id: departamento.id, professor_id: professor.id) + turma.save(validate: false) + + formulario = Formulario.new(turma_id: turma.id, template_id: template.id, admin_id: admin.id) + formulario.save(validate: false) + + # 2. Cria o Aluno para satisfazer o polimorfismo da Submissão + aluno = Aluno.new + aluno.save(validate: false) + + submissao = Submissao.new( + formulario_id: formulario.id, + participant_id: aluno.id, + participant_type: 'Aluno' + ) + submissao.save(validate: false) + + questao = Questao.new(template_id: template.id) + questao.save(validate: false) + + # Parâmetros usando o nome exato do Schema para sincronização perfeita + parametros = { + respostum: { + valor_numerico: 10, + valor_texto: "Ótima aula", + submissao_id: submissao.id, + questao_id: questao.id + } + } + + # Valida se o registro foi criado no banco + expect { + post "/resposta", params: parametros + }.to change(Respostum, :count).by(1) + + expect(response).to be_redirect + + # Garante a sincronização + nova_resposta = Respostum.last + expect(nova_resposta.valor_numerico).to eq(10) + end + end +end \ No newline at end of file diff --git a/project/spec/spec_helper.rb b/project/spec/spec_helper.rb new file mode 100644 index 0000000000..35de9f09fd --- /dev/null +++ b/project/spec/spec_helper.rb @@ -0,0 +1,98 @@ +# This file was generated by the `rails generate rspec:install` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + +# The settings below are suggested to provide a good initial experience +# with RSpec, but feel free to customize to your heart's content. +=begin + # This allows you to limit a spec run to individual examples or groups + # you care about by tagging them with `:focus` metadata. When nothing + # is tagged with `:focus`, all examples get run. RSpec also provides + # aliases for `it`, `describe`, and `context` that include `:focus` + # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + config.filter_run_when_matching :focus + + # Allows RSpec to persist some state between runs in order to support + # the `--only-failures` and `--next-failure` CLI options. We recommend + # you configure your source control system to ignore this file. + config.example_status_persistence_file_path = "spec/examples.txt" + + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/ + config.disable_monkey_patching! + + # This setting enables warnings. It's recommended, but in some cases may + # be too noisy due to issues in dependencies. + config.warnings = true + + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = "doc" + end + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +=end +end From 4e680f8b6af62ef23d7a0bd01bc2d2c6cf7e361d Mon Sep 17 00:00:00 2001 From: Rafael Dias Ghiorzi Date: Sun, 14 Jun 2026 22:26:52 -0300 Subject: [PATCH 15/23] feat: issues #112 #111 #103 #102 feitas --- .gitignore | 5 +- .../app/controllers/application_controller.rb | 15 ++ .../app/controllers/formularios_controller.rb | 29 ++- .../app/controllers/templates_controller.rb | 112 ++++++++--- project/app/models/admin.rb | 2 + project/app/models/formulario.rb | 13 +- project/app/models/questao.rb | 4 +- project/app/models/template.rb | 4 + project/app/views/formularios/_form.html.erb | 37 ++-- project/app/views/formularios/index.html.erb | 9 +- project/app/views/formularios/new.html.erb | 6 +- project/app/views/templates/_form.html.erb | 43 +++-- .../app/views/templates/_template.html.erb | 17 -- project/app/views/templates/edit.html.erb | 9 +- project/app/views/templates/index.html.erb | 21 +- project/app/views/templates/new.html.erb | 8 +- project/app/views/templates/show.html.erb | 27 ++- ...7_allow_null_template_id_on_formularios.rb | 5 + project/db/schema.rb | 4 +- project/db/seeds.rb | 129 ++++++++++++- .../step_definitions/formularios_steps.rb | 70 +++++++ .../templates_edicao_steps.rb | 84 ++++++++ .../step_definitions/templates_steps.rb | 53 +++++ .../templates_visualizacao_steps.rb | 33 ++++ project/features/support/env.rb | 1 + project/features/support/test_helpers.rb | 50 +++++ project/spec/rails_helper.rb | 4 +- project/spec/requests/formularios_spec.rb | 54 ++++++ project/spec/requests/templates_spec.rb | 181 ++++++++++++++++++ project/spec/support/model_helpers.rb | 48 +++++ .../formularios_controller_test.rb | 2 +- .../controllers/templates_controller_test.rb | 4 +- 32 files changed, 956 insertions(+), 127 deletions(-) delete mode 100644 project/app/views/templates/_template.html.erb create mode 100644 project/db/migrate/20260614234457_allow_null_template_id_on_formularios.rb create mode 100644 project/features/step_definitions/formularios_steps.rb create mode 100644 project/features/step_definitions/templates_edicao_steps.rb create mode 100644 project/features/step_definitions/templates_steps.rb create mode 100644 project/features/step_definitions/templates_visualizacao_steps.rb create mode 100644 project/features/support/test_helpers.rb create mode 100644 project/spec/requests/templates_spec.rb create mode 100644 project/spec/support/model_helpers.rb diff --git a/.gitignore b/.gitignore index 9f2fe322d8..d4c518525d 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,7 @@ project/public/assets project/config/*.key AGENTS.md -PROJECT_CONTEXT.md \ No newline at end of file +PROJECT_CONTEXT.md + +CLAUDE.md +.claude/ \ No newline at end of file diff --git a/project/app/controllers/application_controller.rb b/project/app/controllers/application_controller.rb index c3537563da..4286afdc87 100644 --- a/project/app/controllers/application_controller.rb +++ b/project/app/controllers/application_controller.rb @@ -4,4 +4,19 @@ class ApplicationController < ActionController::Base # Changes to the importmap will invalidate the etag for HTML responses stale_when_importmap_changes + + private + + # Login (#104) ainda não foi implementado: por ora o admin autenticado é + # identificado pela sessão, alimentada por um parâmetro admin_id repassado + # nos links/formulários na primeira requisição. Quando nada foi informado + # ainda (ex: acesso direto a /templates), cai para o primeiro admin + # cadastrado, para que a sessão fique consistente a partir da próxima requisição. + def current_admin + return @current_admin if defined?(@current_admin) + + session[:admin_id] = params[:admin_id] if params[:admin_id].present? + @current_admin = Admin.find_by(id: session[:admin_id]) || Admin.first + end + helper_method :current_admin end diff --git a/project/app/controllers/formularios_controller.rb b/project/app/controllers/formularios_controller.rb index 933a94f0aa..d924c3a56a 100644 --- a/project/app/controllers/formularios_controller.rb +++ b/project/app/controllers/formularios_controller.rb @@ -1,9 +1,10 @@ class FormulariosController < ApplicationController before_action :set_formulario, only: %i[ show edit update destroy exportar_csv ] + before_action :set_select_options, only: %i[ new create edit update ] # GET /formularios or /formularios.json def index - @formularios = Formulario.all + @formularios = current_admin ? current_admin.formularios : Formulario.all end # GET /formularios/1 or /formularios/1.json @@ -12,7 +13,7 @@ def show # GET /formularios/new def new - @formulario = Formulario.new + @formulario = Formulario.new(admin_id: current_admin&.id) end # GET /formularios/1/edit @@ -22,15 +23,15 @@ def edit # POST /formularios or /formularios.json def create @formulario = Formulario.new(formulario_params) + @formulario.target_role ||= @formulario.template&.target_role - respond_to do |format| - if @formulario.save - format.html { redirect_to @formulario, notice: "Formulario was successfully created." } - format.json { render :show, status: :created, location: @formulario } - else - format.html { render :new, status: :unprocessable_content } - format.json { render json: @formulario.errors, status: :unprocessable_content } + if @formulario.save + @formulario.template.questaos.each do |questao| + @formulario.questaos.create!(enunciado: questao.enunciado, tipo: questao.tipo) end + redirect_to formularios_path, notice: "Formulário gerado com sucesso!" + else + render :new, status: :unprocessable_content end end @@ -79,6 +80,14 @@ def set_formulario # Only allow a list of trusted parameters through. def formulario_params - params.expect(formulario: [ :title, :target_role, :status, :template_id, :turma_id, :admin_id ]) + attrs = params.expect(formulario: [ :title, :target_role, :status, :template_id, :turma_id, :admin_id ]) + attrs[:admin_id] = current_admin.id if current_admin + attrs + end + + # Opções disponíveis para os selects de template e turma do formulário. + def set_select_options + @templates = current_admin ? current_admin.templates : Template.all + @turmas = Turma.all end end diff --git a/project/app/controllers/templates_controller.rb b/project/app/controllers/templates_controller.rb index cb6a71ea57..7291c738af 100644 --- a/project/app/controllers/templates_controller.rb +++ b/project/app/controllers/templates_controller.rb @@ -1,9 +1,10 @@ class TemplatesController < ApplicationController before_action :set_template, only: %i[ show edit update destroy ] + before_action :authorize_template_access!, only: %i[ show edit update destroy ] # GET /templates or /templates.json def index - @templates = Template.all + @templates = current_admin ? current_admin.templates : Template.all end # GET /templates/1 or /templates/1.json @@ -12,49 +13,73 @@ def show # GET /templates/new def new - @template = Template.new + @template = Template.new(admin_id: current_admin&.id) + @questoes_attrs = [] end # GET /templates/1/edit def edit + @questoes_attrs = @template.questaos.map do |questao| + { id: questao.id, enunciado: questao.enunciado, tipo: questao.tipo } + end end # POST /templates or /templates.json def create @template = Template.new(template_params) + @questoes_attrs = extract_questoes_attrs - respond_to do |format| - if @template.save - format.html { redirect_to @template, notice: "Template was successfully created." } - format.json { render :show, status: :created, location: @template } - else - format.html { render :new, status: :unprocessable_content } - format.json { render json: @template.errors, status: :unprocessable_content } + if params[:add_questao] + @questoes_attrs << blank_questao_attrs + return render :new, status: :ok + end + + if @template.valid? && questoes_attrs_valid? + Template.transaction do + @template.save! + @questoes_attrs.each do |questao| + @template.questaos.create!(enunciado: questao[:enunciado], tipo: questao[:tipo]) + end end + redirect_to templates_path, notice: "Template criado com sucesso." + else + render :new, status: :unprocessable_content end end # PATCH/PUT /templates/1 or /templates/1.json def update - respond_to do |format| - if @template.update(template_params) - format.html { redirect_to @template, notice: "Template was successfully updated.", status: :see_other } - format.json { render :show, status: :ok, location: @template } - else - format.html { render :edit, status: :unprocessable_content } - format.json { render json: @template.errors, status: :unprocessable_content } + @template.assign_attributes(template_params) + @questoes_attrs = extract_questoes_attrs + + if params[:add_questao] + @questoes_attrs << blank_questao_attrs + return render :edit, status: :ok + end + + novas_questoes = @questoes_attrs.reject { |questao| questao[:id].present? } + + if @template.valid? && novas_questoes_validas?(novas_questoes) + Template.transaction do + @template.save! + @questoes_attrs.each do |questao| + if questao[:id].present? + @template.questaos.find(questao[:id]).update!(enunciado: questao[:enunciado], tipo: questao[:tipo]) + else + @template.questaos.create!(enunciado: questao[:enunciado], tipo: questao[:tipo]) + end + end end + redirect_to templates_path, notice: "Template atualizado com sucesso." + else + render :edit, status: :unprocessable_content end end # DELETE /templates/1 or /templates/1.json def destroy @template.destroy! - - respond_to do |format| - format.html { redirect_to templates_path, notice: "Template was successfully destroyed.", status: :see_other } - format.json { head :no_content } - end + redirect_to templates_path, notice: "Template deletado com sucesso.", status: :see_other end private @@ -63,8 +88,51 @@ def set_template @template = Template.find(params.expect(:id)) end + # Garante que o admin só acesse/edite/delete templates criados por ele. + def authorize_template_access! + return if current_admin.nil? || @template.admin_id == current_admin.id + + redirect_to templates_path, alert: "Você não tem acesso a esse template" + end + # Only allow a list of trusted parameters through. def template_params - params.expect(template: [ :title, :target_role, :admin_id ]) + attrs = params.expect(template: [ :title, :target_role, :admin_id ]) + attrs[:admin_id] = current_admin.id if current_admin + attrs + end + + # Extrai as questões enviadas via template[questaos][i][enunciado/tipo/id]. + def extract_questoes_attrs + raw = params.dig(:template, :questaos) + return [] unless raw + + raw.values.map do |questao| + attrs = { enunciado: questao[:enunciado].to_s, tipo: questao[:tipo].presence || "rating" } + attrs[:id] = questao[:id] if questao[:id].present? + attrs + end + end + + def blank_questao_attrs + { enunciado: "", tipo: "rating" } + end + + def questoes_attrs_valid? + if @questoes_attrs.any? { |questao| questao[:enunciado].blank? } + @template.errors.add(:base, "Questão não possui enunciado") + false + else + true + end + end + + def novas_questoes_validas?(novas_questoes) + if novas_questoes.any? { |questao| questao[:enunciado].blank? } + @template.errors.add(:base, "A nova questão não possui enunciado") + false + else + true + end end end diff --git a/project/app/models/admin.rb b/project/app/models/admin.rb index 0d89d934ca..e1038dc686 100644 --- a/project/app/models/admin.rb +++ b/project/app/models/admin.rb @@ -1,3 +1,5 @@ class Admin < ApplicationRecord belongs_to :departamento + has_many :templates, dependent: :destroy + has_many :formularios, dependent: :destroy end diff --git a/project/app/models/formulario.rb b/project/app/models/formulario.rb index 63b69618ac..ea1c330e71 100644 --- a/project/app/models/formulario.rb +++ b/project/app/models/formulario.rb @@ -1,5 +1,14 @@ class Formulario < ApplicationRecord - belongs_to :template - belongs_to :turma + # O template pode ser desvinculado (ficar nulo) se o Admin deletar o template + # de origem; o formulário e suas questões clonadas permanecem intactos. + belongs_to :template, optional: true + belongs_to :turma, optional: true belongs_to :admin + has_many :questaos, dependent: :destroy + + validates :title, presence: { message: "O nome do formulário é obrigatório" } + validates :template_id, presence: { message: "É necessário escolher um template base" }, on: :create + validates :turma_id, presence: { message: "É necessário escolher ao menos uma turma" }, on: :create + + after_initialize { self.status ||= "draft" } end diff --git a/project/app/models/questao.rb b/project/app/models/questao.rb index cfd49e8229..2622bb55f4 100644 --- a/project/app/models/questao.rb +++ b/project/app/models/questao.rb @@ -1,6 +1,8 @@ class Questao < ApplicationRecord - # A chave estrangeira é nula para template se pertencer a formulário, e vice-versa + # A chave estrangeira é nula para template se pertencer a formulário, e vice-versa belongs_to :template, optional: true belongs_to :formulario, optional: true has_many :respostas + + validates :enunciado, presence: true end \ No newline at end of file diff --git a/project/app/models/template.rb b/project/app/models/template.rb index d6dea87801..00b7e595da 100644 --- a/project/app/models/template.rb +++ b/project/app/models/template.rb @@ -1,3 +1,7 @@ class Template < ApplicationRecord belongs_to :admin + has_many :questaos, dependent: :destroy + has_many :formularios, dependent: :nullify + + validates :title, presence: { message: "O nome do template é obrigatório" } end diff --git a/project/app/views/formularios/_form.html.erb b/project/app/views/formularios/_form.html.erb index e3af3029d0..7bddf2eefb 100644 --- a/project/app/views/formularios/_form.html.erb +++ b/project/app/views/formularios/_form.html.erb @@ -1,47 +1,34 @@ <%= form_with(model: formulario) do |form| %> <% if formulario.errors.any? %> -
-

<%= pluralize(formulario.errors.count, "error") %> prohibited this formulario from being saved:

+
+

<%= pluralize(formulario.errors.count, "erro") %> impediu(ram) este formulário de ser salvo:

    - <% formulario.errors.each do |error| %> -
  • <%= error.full_message %>
  • + <% formulario.errors.full_messages.each do |message| %> +
  • <%= message %>
  • <% end %>
<% end %> -
- <%= form.label :title, style: "display: block" %> - <%= form.text_field :title %> -
- -
- <%= form.label :target_role, style: "display: block" %> - <%= form.text_field :target_role %> -
+ <%= hidden_field_tag :admin_id, current_admin&.id %>
- <%= form.label :status, style: "display: block" %> - <%= form.text_field :status %> -
- -
- <%= form.label :template_id, style: "display: block" %> - <%= form.text_field :template_id %> + <%= form.label :title, "Nome do formulário:" %> + <%= form.text_field :title %>
- <%= form.label :turma_id, style: "display: block" %> - <%= form.text_field :turma_id %> + <%= form.label :template_id, "Template:" %> + <%= form.collection_select :template_id, @templates, :id, :title, include_blank: "Selecione um template" %>
- <%= form.label :admin_id, style: "display: block" %> - <%= form.text_field :admin_id %> + <%= form.label :turma_id, "Turma:" %> + <%= form.collection_select :turma_id, @turmas, :id, :subject_name, include_blank: "Selecione uma turma" %>
- <%= form.submit %> + <%= form.submit "Enviar" %>
<% end %> diff --git a/project/app/views/formularios/index.html.erb b/project/app/views/formularios/index.html.erb index 99df9d53e3..00b5cd535e 100644 --- a/project/app/views/formularios/index.html.erb +++ b/project/app/views/formularios/index.html.erb @@ -1,16 +1,17 @@

<%= notice %>

+

<%= alert %>

-<% content_for :title, "Formularios" %> +<% content_for :title, "Formulários" %> -

Formularios

+

Formulários

<% @formularios.each do |formulario| %> <%= render formulario %>

- <%= link_to "Show this formulario", formulario %> + <%= link_to "Ver formulário", formulario %>

<% end %>
-<%= link_to "New formulario", new_formulario_path %> +<%= link_to "Novo formulário", new_formulario_path(admin_id: current_admin&.id) %> diff --git a/project/app/views/formularios/new.html.erb b/project/app/views/formularios/new.html.erb index 0dbdfdbd5c..83b49726ad 100644 --- a/project/app/views/formularios/new.html.erb +++ b/project/app/views/formularios/new.html.erb @@ -1,11 +1,11 @@ -<% content_for :title, "New formulario" %> +<% content_for :title, "Novo formulário" %> -

New formulario

+

Novo formulário

<%= render "form", formulario: @formulario %>
- <%= link_to "Back to formularios", formularios_path %> + <%= link_to "Voltar para formulários", formularios_path(admin_id: current_admin&.id) %>
diff --git a/project/app/views/templates/_form.html.erb b/project/app/views/templates/_form.html.erb index 1f81fa00b5..3e8392d222 100644 --- a/project/app/views/templates/_form.html.erb +++ b/project/app/views/templates/_form.html.erb @@ -1,32 +1,53 @@ <%= form_with(model: template) do |form| %> <% if template.errors.any? %> -
-

<%= pluralize(template.errors.count, "error") %> prohibited this template from being saved:

+
+

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

    - <% template.errors.each do |error| %> -
  • <%= error.full_message %>
  • + <% template.errors.full_messages.each do |message| %> +
  • <%= message %>
  • <% end %>
<% end %> + <%= hidden_field_tag :admin_id, current_admin&.id %> +
- <%= form.label :title, style: "display: block" %> + <%= form.label :title, "Nome do template:" %> <%= form.text_field :title %>
- <%= form.label :target_role, style: "display: block" %> - <%= form.text_field :target_role %> + <%= form.label :target_role, "Público-alvo:" %> + <%= form.select :target_role, [ [ "Discentes", "discente" ], [ "Docentes", "docente" ] ], include_blank: "Selecione" %>
-
- <%= form.label :admin_id, style: "display: block" %> - <%= form.text_field :admin_id %> +

Questões

+ +
+ <% questoes_attrs.each_with_index do |questao, index| %> +
+ <% if questao[:id] %> + <%= hidden_field_tag "template[questaos][#{index}][id]", questao[:id] %> + <% end %> +
+ <%= label_tag "template_questaos_#{index}_enunciado", "Enunciado da questão:" %> + <%= text_area_tag "template[questaos][#{index}][enunciado]", questao[:enunciado], id: "template_questaos_#{index}_enunciado" %> +
+
+ <%= label_tag "template_questaos_#{index}_tipo", "Tipo:" %> + <%= select_tag "template[questaos][#{index}][tipo]", + options_for_select([ [ "Nota (1-5)", "rating" ], [ "Texto", "text" ], [ "Sim/Não", "boolean" ] ], questao[:tipo]), + id: "template_questaos_#{index}_tipo" %> +
+
+ <% end %>
+ <%= submit_tag "+", name: "add_questao", data: { turbo: false } %> +
- <%= form.submit %> + <%= form.submit(template.persisted? ? "Confirmar edição" : "Criar") %>
<% end %> diff --git a/project/app/views/templates/_template.html.erb b/project/app/views/templates/_template.html.erb deleted file mode 100644 index 0a176012f7..0000000000 --- a/project/app/views/templates/_template.html.erb +++ /dev/null @@ -1,17 +0,0 @@ -
-
- Title: - <%= template.title %> -
- -
- Target role: - <%= template.target_role %> -
- -
- Admin: - <%= template.admin_id %> -
- -
diff --git a/project/app/views/templates/edit.html.erb b/project/app/views/templates/edit.html.erb index 2fe355a9c7..3bc0acfae8 100644 --- a/project/app/views/templates/edit.html.erb +++ b/project/app/views/templates/edit.html.erb @@ -1,12 +1,11 @@ -<% content_for :title, "Editing template" %> +<% content_for :title, "Editar template" %> -

Editing template

+

Editar template

-<%= render "form", template: @template %> +<%= render "form", template: @template, questoes_attrs: @questoes_attrs %>
- <%= link_to "Show this template", @template %> | - <%= link_to "Back to templates", templates_path %> + <%= link_to "Voltar para templates", templates_path(admin_id: current_admin&.id) %>
diff --git a/project/app/views/templates/index.html.erb b/project/app/views/templates/index.html.erb index 93d4cd21c2..918bd73903 100644 --- a/project/app/views/templates/index.html.erb +++ b/project/app/views/templates/index.html.erb @@ -1,16 +1,23 @@

<%= notice %>

+

<%= alert %>

-<% content_for :title, "Templates" %> +<% content_for :title, "Meus Templates" %> -

Templates

+

Meus Templates

<% @templates.each do |template| %> - <%= render template %> -

- <%= link_to "Show this template", template %> -

+
+

<%= template.title %>

+

Público-alvo: <%= template.target_role %>

+

Questões: <%= template.questaos.count %>

+ +
+ <%= link_to "Editar", edit_template_path(template, admin_id: current_admin&.id) %> + <%= link_to "Deletar", template_path(template, admin_id: current_admin&.id) %> +
+
<% end %>
-<%= link_to "New template", new_template_path %> +<%= link_to "Novo template", new_template_path(admin_id: current_admin&.id) %> diff --git a/project/app/views/templates/new.html.erb b/project/app/views/templates/new.html.erb index bd2d47148d..85eea3d860 100644 --- a/project/app/views/templates/new.html.erb +++ b/project/app/views/templates/new.html.erb @@ -1,11 +1,11 @@ -<% content_for :title, "New template" %> +<% content_for :title, "Novo template" %> -

New template

+

Novo template

-<%= render "form", template: @template %> +<%= render "form", template: @template, questoes_attrs: @questoes_attrs %>
- <%= link_to "Back to templates", templates_path %> + <%= link_to "Voltar para templates", templates_path(admin_id: current_admin&.id) %>
diff --git a/project/app/views/templates/show.html.erb b/project/app/views/templates/show.html.erb index e610456e09..801ea8ab28 100644 --- a/project/app/views/templates/show.html.erb +++ b/project/app/views/templates/show.html.erb @@ -1,10 +1,29 @@

<%= notice %>

+

<%= alert %>

-<%= render @template %> +<% content_for :title, @template.title %> + +

<%= @template.title %>

+ +

Público-alvo: <%= @template.target_role %>

+ +

Questões

+ +
    + <% @template.questaos.each do |questao| %> +
  • <%= questao.enunciado %> (<%= questao.tipo %>)
  • + <% end %> +
- <%= link_to "Edit this template", edit_template_path(@template) %> | - <%= link_to "Back to templates", templates_path %> + <%= link_to "Editar", edit_template_path(@template, admin_id: current_admin&.id) %> +
- <%= button_to "Destroy this template", @template, method: :delete %> +
+

Tem certeza que deseja deletar este template?

+ <%= button_to "Confirmar", template_path(@template, admin_id: current_admin&.id), method: :delete %> +
+ +
+ <%= link_to "Voltar para templates", templates_path(admin_id: current_admin&.id) %>
diff --git a/project/db/migrate/20260614234457_allow_null_template_id_on_formularios.rb b/project/db/migrate/20260614234457_allow_null_template_id_on_formularios.rb new file mode 100644 index 0000000000..f3ede1c71e --- /dev/null +++ b/project/db/migrate/20260614234457_allow_null_template_id_on_formularios.rb @@ -0,0 +1,5 @@ +class AllowNullTemplateIdOnFormularios < ActiveRecord::Migration[8.1] + def change + change_column_null :formularios, :template_id, true + end +end diff --git a/project/db/schema.rb b/project/db/schema.rb index e893bcf628..799156ff7f 100644 --- a/project/db/schema.rb +++ b/project/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_11_134500) do +ActiveRecord::Schema[8.1].define(version: 2026_06_14_234457) do create_table "admins", force: :cascade do |t| t.datetime "created_at", null: false t.integer "departamento_id", null: false @@ -44,7 +44,7 @@ t.datetime "created_at", null: false t.string "status" t.string "target_role" - t.integer "template_id", null: false + t.integer "template_id" t.string "title" t.integer "turma_id", null: false t.datetime "updated_at", null: false diff --git a/project/db/seeds.rb b/project/db/seeds.rb index 4fbd6ed970..0794ba0808 100644 --- a/project/db/seeds.rb +++ b/project/db/seeds.rb @@ -2,8 +2,127 @@ # 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 +# Seed de desenvolvimento: cria um conjunto mínimo de dados para testar as +# telas (templates, formulários, turmas, alunos, professores, etc) na interface. + +departamento_cic = Departamento.find_or_create_by!(code: "CIC") do |departamento| + departamento.name = "Ciência da Computação" +end + +departamento_ene = Departamento.find_or_create_by!(code: "ENE") do |departamento| + departamento.name = "Engenharia Elétrica" +end + +admin = Admin.find_or_create_by!(username: "admin") do |admin| + admin.name = "Admin CAMAAR" + admin.email = "admin.cic@unb.br" + admin.departamento_id = departamento_cic.id +end + +professor_ana = Professor.find_or_create_by!(matricula: "PROF0001") do |professor| + professor.name = "Ana Souza" + professor.email = "ana.souza@unb.br" + professor.formation = "Doutora em Ciência da Computação" + professor.departamento_id = departamento_cic.id + professor.password = "Senha@123" +end + +professor_bruno = Professor.find_or_create_by!(matricula: "PROF0002") do |professor| + professor.name = "Bruno Lima" + professor.email = "bruno.lima@unb.br" + professor.formation = "Doutor em Engenharia Elétrica" + professor.departamento_id = departamento_ene.id + professor.password = "Senha@123" +end + +alunos = [ + { matricula: "190000001", name: "Carla Mendes", email: "carla.mendes@aluno.unb.br", course: "Engenharia de Software" }, + { matricula: "190000002", name: "Diego Alves", email: "diego.alves@aluno.unb.br", course: "Engenharia de Software" }, + { matricula: "190000003", name: "Eduarda Rocha", email: "eduarda.rocha@aluno.unb.br", course: "Ciência da Computação" }, + { matricula: "190000004", name: "Felipe Castro", email: "felipe.castro@aluno.unb.br", course: "Ciência da Computação" } +].map do |dados| + Aluno.find_or_create_by!(matricula: dados[:matricula]) do |aluno| + aluno.name = dados[:name] + aluno.email = dados[:email] + aluno.course = dados[:course] + aluno.password = "Senha@123" + end +end + +turma_estrutura_dados = Turma.find_or_create_by!(class_code: "CIC0097-T01") do |turma| + turma.subject_code = "CIC0097" + turma.subject_name = "Estrutura de Dados" + turma.semester = "2026.1" + turma.time = "Seg/Qua 08:00-10:00" + turma.departamento_id = departamento_cic.id + turma.professor_id = professor_ana.id +end + +turma_circuitos = Turma.find_or_create_by!(class_code: "ENE0011-T01") do |turma| + turma.subject_code = "ENE0011" + turma.subject_name = "Circuitos Elétricos" + turma.semester = "2026.1" + turma.time = "Ter/Qui 10:00-12:00" + turma.departamento_id = departamento_ene.id + turma.professor_id = professor_bruno.id +end + +[ alunos[0], alunos[1], alunos[2] ].each do |aluno| + TurmaAluno.find_or_create_by!(turma_id: turma_estrutura_dados.id, aluno_id: aluno.id) +end + +[ alunos[2], alunos[3] ].each do |aluno| + TurmaAluno.find_or_create_by!(turma_id: turma_circuitos.id, aluno_id: aluno.id) +end + +template_discente = Template.find_or_create_by!(title: "Avaliação de Disciplina - Discentes") do |template| + template.target_role = "discente" + template.admin_id = admin.id +end + +if template_discente.questaos.empty? + template_discente.questaos.create!(enunciado: "O professor demonstrou domínio do conteúdo?", tipo: "rating") + template_discente.questaos.create!(enunciado: "O material disponibilizado foi adequado?", tipo: "rating") + template_discente.questaos.create!(enunciado: "Comentários e sugestões para a disciplina:", tipo: "text") +end + +template_docente = Template.find_or_create_by!(title: "Autoavaliação - Docentes") do |template| + template.target_role = "docente" + template.admin_id = admin.id +end + +if template_docente.questaos.empty? + template_docente.questaos.create!(enunciado: "Você conseguiu cumprir o plano de ensino proposto?", tipo: "boolean") + template_docente.questaos.create!(enunciado: "Quais dificuldades você encontrou no semestre?", tipo: "text") +end + +formulario = Formulario.find_or_create_by!(title: "Avaliação Estrutura de Dados - 2026.1") do |formulario| + formulario.target_role = template_discente.target_role + formulario.admin_id = admin.id + formulario.template_id = template_discente.id + formulario.turma_id = turma_estrutura_dados.id +end + +if formulario.questaos.empty? + template_discente.questaos.each do |questao| + formulario.questaos.create!(enunciado: questao.enunciado, tipo: questao.tipo) + end +end + +submissao = Submissao.find_or_create_by!(formulario_id: formulario.id, participant: alunos[0]) + +if Respostum.where(submissao_id: submissao.id).empty? + formulario.questaos.each do |questao| + if questao.tipo == "rating" + Respostum.create!(submissao_id: submissao.id, questao_id: questao.id, valor_numerico: 5) + else + Respostum.create!(submissao_id: submissao.id, questao_id: questao.id, valor_texto: "Resposta de exemplo") + end + end +end + +puts "Seed concluída:" +puts " Admin: #{admin.username} (id=#{admin.id}) - use ?admin_id=#{admin.id} enquanto a #104 não existe" +puts " Professores: #{Professor.count} | Alunos: #{Aluno.count} | Turmas: #{Turma.count}" +puts " Templates: #{Template.count} | Formulários: #{Formulario.count}" +puts " Submissões: #{Submissao.count} | Respostas: #{Respostum.count}" diff --git a/project/features/step_definitions/formularios_steps.rb b/project/features/step_definitions/formularios_steps.rb new file mode 100644 index 0000000000..bebdbf3c1f --- /dev/null +++ b/project/features/step_definitions/formularios_steps.rb @@ -0,0 +1,70 @@ +# Step definitions para features/admin_criar_formulario.feature (#103) + +Dado("que sou administrador") do + @admin = criar_admin +end + +Dado("existe pelo menos um template criado por mim") do + @template = criar_template_com_questao(@admin) +end + +Dado("existe pelo menos uma turma cadastrada") do + @turma = criar_turma +end + +Quando("eu preencher o nome do formulário") do + @formulario_titulo = "Formulário Avaliação #{SecureRandom.hex(4)}" + visit new_formulario_path(admin_id: @admin.id) + fill_in "formulario[title]", with: @formulario_titulo +end + +Quando("selecionar o template e a turma desejada") do + select @template.title, from: "formulario[template_id]" + select @turma.subject_name, from: "formulario[turma_id]" +end + +Quando("clicar no botão 'Enviar'") do + click_button "Enviar" +end + +Então("devo ver uma mensagem dizendo 'Formulário gerado com sucesso!'") do + expect(page).to have_text("Formulário gerado com sucesso!") +end + +Então("as questões do template devem ser clonadas para o novo formulário") do + formulario = Formulario.find_by!(title: @formulario_titulo) + + expect(formulario.questaos.count).to eq(@template.questaos.count) + expect(formulario.questaos.pluck(:enunciado)).to eq(@template.questaos.pluck(:enunciado)) + expect(formulario.questaos.pluck(:template_id).uniq).to eq([ nil ]) +end + +Dado("que sou um administrador") do + @admin = criar_admin +end + +Dado("estou criando um formulário") do + @template = criar_template_com_questao(@admin) + @turma = criar_turma + visit new_formulario_path(admin_id: @admin.id) +end + +Quando("eu tentar criar um formulário sem selecionar nenhum template") do + fill_in "formulario[title]", with: "Formulário sem template" + select @turma.subject_name, from: "formulario[turma_id]" + click_button "Enviar" +end + +Então("o sistema deve exibir uma mensagem dizendo 'É necessário escolher um template base'") do + expect(page).to have_text("É necessário escolher um template base") +end + +Quando("eu tentar criar um formulário sem selecionar nenhuma turma") do + fill_in "formulario[title]", with: "Formulário sem turma" + select @template.title, from: "formulario[template_id]" + click_button "Enviar" +end + +Então("o sistema deve exibir uma mensagem dizendo 'É necessário escolher ao menos uma turma'") do + expect(page).to have_text("É necessário escolher ao menos uma turma") +end diff --git a/project/features/step_definitions/templates_edicao_steps.rb b/project/features/step_definitions/templates_edicao_steps.rb new file mode 100644 index 0000000000..3a4a5e6c26 --- /dev/null +++ b/project/features/step_definitions/templates_edicao_steps.rb @@ -0,0 +1,84 @@ +# Step definitions para features/admin_editar_deletar_templates.feature (#112) + +Dado("que eu editei informações de um template existente") do + @admin = criar_admin + @template = criar_template_com_questao(@admin) + + # Formulário gerado a partir da versão ATUAL do template, com questões já clonadas. + turma = criar_turma + @formulario = @admin.formularios.create!(title: "Formulário Antigo", template_id: @template.id, turma_id: turma.id, target_role: @template.target_role) + @template.questaos.each do |questao| + @formulario.questaos.create!(enunciado: questao.enunciado, tipo: questao.tipo) + end + @enunciado_original_formulario = @formulario.questaos.first.enunciado + + @novo_titulo = "Template Editado #{SecureRandom.hex(4)}" + + visit edit_template_path(@template, admin_id: @admin.id) + fill_in "template[title]", with: @novo_titulo +end + +Quando("eu pressionar o botão {string} na tela de edição") do |texto_botao| + click_button texto_botao +end + +Então("eu devo visualizar as alterações no template na tela de templates") do + expect(page).to have_text(@novo_titulo) +end + +Então("os formulários criados usando a versão antiga não devem sofrer alterações nas suas questões") do + @formulario.reload + expect(@formulario.questaos.first.enunciado).to eq(@enunciado_original_formulario) + expect(@formulario.questaos.first.template_id).to be_nil +end + +Dado("que eu escolhi um template na minha lista") do + @admin = criar_admin + @template = criar_template_com_questao(@admin) + + turma = criar_turma + @formulario = @admin.formularios.create!(title: "Formulário Gerado", template_id: @template.id, turma_id: turma.id, target_role: @template.target_role) + @template.questaos.each do |questao| + @formulario.questaos.create!(enunciado: questao.enunciado, tipo: questao.tipo) + end + + @template_title = @template.title + visit templates_path(admin_id: @admin.id) +end + +Quando("eu pressionar o botão de {string}") do |texto| + click_link texto +end + +Então("confirmar a deleção pressionando o botão {string}") do |texto| + click_button texto +end + +Então("esse template não deve aparecer mais na tela de templates") do + expect(page).not_to have_text(@template_title) +end + +Então("os formulários gerados por ele devem permanecer intactos no sistema") do + @formulario.reload + expect(@formulario.title).to eq("Formulário Gerado") + expect(@formulario.template_id).to be_nil + expect(@formulario.questaos.count).to eq(1) +end + +Dado("que estou na página de edição do template") do + @admin = criar_admin + @template = criar_template_com_questao(@admin) + visit edit_template_path(@template, admin_id: @admin.id) +end + +Quando("eu clico no botão '+'") do + click_button "+" +end + +Quando("clico no botão 'Confirmar edição'") do + click_button "Confirmar edição" +end + +Então("deve aparecer uma mensagem 'A nova questão não possui enunciado'") do + expect(page).to have_text("A nova questão não possui enunciado") +end diff --git a/project/features/step_definitions/templates_steps.rb b/project/features/step_definitions/templates_steps.rb new file mode 100644 index 0000000000..88feac1612 --- /dev/null +++ b/project/features/step_definitions/templates_steps.rb @@ -0,0 +1,53 @@ +# Step definitions para features/admin_criar_template.feature (#102) +# Alguns passos abaixo são reutilizados por features/admin_editar_deletar_templates.feature (#112). + +Dado("que estou na página de criar template") do + @admin = criar_admin + visit new_template_path(admin_id: @admin.id) +end + +Quando("eu preencho o campo 'Nome do template:'") do + @template_title = "Template Avaliação #{SecureRandom.hex(4)}" + fill_in "template[title]", with: @template_title +end + +Quando("eu deixo o campo 'Nome do template:' vazio") do + # O campo é deliberadamente deixado em branco. +end + +Quando("clico no botão '+'") do + click_button "+" +end + +Quando("seleciono o 'Público-alvo:'") do + select "Discentes", from: "template[target_role]" +end + +Quando("preencho o campo 'Enunciado da questão:'") do + fill_in "template[questaos][0][enunciado]", with: "Qual é a capital do Brasil?" +end + +Quando("não preencho o campo 'Enunciado da questão:'") do + # O campo é deliberadamente deixado em branco. +end + +Quando("adiciono uma questão válida") do + click_button "+" + fill_in "template[questaos][0][enunciado]", with: "Qual é a maior montanha do mundo?" +end + +Quando("clico no botão 'Criar'") do + click_button "Criar" +end + +Então("o novo template deve aparecer na tela de meus templates") do + expect(page).to have_text(@template_title) +end + +Então("deve aparecer uma mensagem 'O nome do template é obrigatório'") do + expect(page).to have_text("O nome do template é obrigatório") +end + +Então("deve aparecer uma mensagem 'Questão não possui enunciado'") do + expect(page).to have_text("Questão não possui enunciado") +end diff --git a/project/features/step_definitions/templates_visualizacao_steps.rb b/project/features/step_definitions/templates_visualizacao_steps.rb new file mode 100644 index 0000000000..38521de335 --- /dev/null +++ b/project/features/step_definitions/templates_visualizacao_steps.rb @@ -0,0 +1,33 @@ +# Step definitions para features/admin_ver_templates.feature (#111) + +Dado("que o administrador está na interface de templates") do + @admin = criar_admin + @template = criar_template_com_questao(@admin, titulo: "Template do Admin #{SecureRandom.hex(4)}") + + @outro_admin = criar_admin + @template_outro_admin = criar_template_com_questao(@outro_admin, titulo: "Template de Outro Admin #{SecureRandom.hex(4)}") + + visit templates_path(admin_id: @admin.id) +end + +Quando("o sistema carrega os templates") do + # A página já foi carregada no passo "Dado". +end + +Então("ele deverá ver somente os templates criados por ele") do + expect(page).to have_text(@template.title) + expect(page).not_to have_text(@template_outro_admin.title) +end + +Então("deverá ter a opção de deletar ou editar esses templates") do + expect(page).to have_link("Editar") + expect(page).to have_link("Deletar") +end + +Quando("ele tenta acessar diretamente um template criado por outro administrador") do + visit edit_template_path(@template_outro_admin, admin_id: @admin.id) +end + +Então("ele deverá ver uma mensagem de erro de permissão {string}") do |mensagem| + expect(page).to have_text(mensagem) +end diff --git a/project/features/support/env.rb b/project/features/support/env.rb index 3b97d14087..1b06a3c7b0 100644 --- a/project/features/support/env.rb +++ b/project/features/support/env.rb @@ -6,6 +6,7 @@ require 'cucumber/rails' +require 'rspec/expectations' # 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 diff --git a/project/features/support/test_helpers.rb b/project/features/support/test_helpers.rb new file mode 100644 index 0000000000..e67f9fb011 --- /dev/null +++ b/project/features/support/test_helpers.rb @@ -0,0 +1,50 @@ +module TestHelpers + def departamento_de_teste + Departamento.find_or_create_by!(code: "DEPT001") do |departamento| + departamento.name = "Departamento de Teste" + end + end + + def criar_admin + departamento = departamento_de_teste + sufixo = SecureRandom.hex(4) + + Admin.create!( + name: "Admin Teste #{sufixo}", + email: "admin#{sufixo}@teste.com", + username: "admin#{sufixo}", + departamento_id: departamento.id + ) + end + + def criar_template_com_questao(admin, titulo: "Template #{SecureRandom.hex(4)}", target_role: "discente", enunciado: "Qual é a resposta correta?") + template = admin.templates.create!(title: titulo, target_role: target_role) + template.questaos.create!(enunciado: enunciado, tipo: "text") + template + end + + def criar_turma + departamento = departamento_de_teste + sufixo = SecureRandom.hex(4) + + professor = Professor.create!( + name: "Professor Teste #{sufixo}", + email: "professor#{sufixo}@teste.com", + matricula: "PROF#{sufixo}", + password: "senha12345", + departamento_id: departamento.id + ) + + Turma.create!( + class_code: "TURMA#{sufixo}", + subject_code: "DISC#{sufixo}", + subject_name: "Disciplina Teste", + semester: "2026.1", + time: "10:00", + departamento_id: departamento.id, + professor_id: professor.id + ) + end +end + +World(TestHelpers) diff --git a/project/spec/rails_helper.rb b/project/spec/rails_helper.rb index ef75d46770..b1765001cd 100644 --- a/project/spec/rails_helper.rb +++ b/project/spec/rails_helper.rb @@ -23,7 +23,7 @@ # directory. Alternatively, in the individual `*_spec.rb` files, manually # require only the support files necessary. # -# Rails.root.glob('spec/support/**/*.rb').sort_by(&:to_s).each { |f| require f } +Rails.root.glob('spec/support/**/*.rb').sort_by(&:to_s).each { |f| require f } # Ensures that the test database schema matches the current schema file. # If there are pending migrations it will invoke `db:test:prepare` to @@ -69,4 +69,6 @@ config.filter_rails_from_backtrace! # arbitrary gems may also be filtered via: # config.filter_gems_from_backtrace("gem name") + + config.include ModelHelpers, type: :request end diff --git a/project/spec/requests/formularios_spec.rb b/project/spec/requests/formularios_spec.rb index b49dfa6cd5..6bc0c1d09b 100644 --- a/project/spec/requests/formularios_spec.rb +++ b/project/spec/requests/formularios_spec.rb @@ -30,4 +30,58 @@ expect(response.body).to include("ID da Pergunta", "Enunciado") end end + + describe "POST /formularios" do + it "cria um formulário a partir de um template e clona as questões do template" do + admin = criar_admin + template = criar_template_com_questao(admin, enunciado: "O professor foi claro?") + turma = criar_turma + + expect { + post formularios_path, params: { + admin_id: admin.id, + formulario: { title: "Avaliação do Semestre", template_id: template.id, turma_id: turma.id } + } + }.to change(Formulario, :count).by(1).and change(Questao, :count).by(1) + + expect(response).to redirect_to(formularios_path) + follow_redirect! + expect(response.body).to include("Formulário gerado com sucesso!") + + formulario = Formulario.last + expect(formulario.admin_id).to eq(admin.id) + expect(formulario.questaos.first.enunciado).to eq("O professor foi claro?") + expect(formulario.questaos.first.template_id).to be_nil + end + + it "não cria o formulário quando nenhum template é selecionado" do + admin = criar_admin + turma = criar_turma + + expect { + post formularios_path, params: { + admin_id: admin.id, + formulario: { title: "Sem template", turma_id: turma.id } + } + }.not_to change(Formulario, :count) + + expect(response).to have_http_status(:unprocessable_content) + expect(response.body).to include("É necessário escolher um template base") + end + + it "não cria o formulário quando nenhuma turma é selecionada" do + admin = criar_admin + template = criar_template_com_questao(admin) + + expect { + post formularios_path, params: { + admin_id: admin.id, + formulario: { title: "Sem turma", template_id: template.id } + } + }.not_to change(Formulario, :count) + + expect(response).to have_http_status(:unprocessable_content) + expect(response.body).to include("É necessário escolher ao menos uma turma") + end + end end \ No newline at end of file diff --git a/project/spec/requests/templates_spec.rb b/project/spec/requests/templates_spec.rb new file mode 100644 index 0000000000..c5b6a5fd9c --- /dev/null +++ b/project/spec/requests/templates_spec.rb @@ -0,0 +1,181 @@ +require "rails_helper" + +RSpec.describe "Templates", type: :request do + describe "POST /templates" do + it "cria um template com uma questão e redireciona para a lista de templates" do + admin = criar_admin + + expect { + post templates_path, params: { + admin_id: admin.id, + template: { + title: "Avaliação de Disciplina", + target_role: "discente", + questaos: { "0" => { enunciado: "O conteúdo foi claro?", tipo: "rating" } } + } + } + }.to change(Template, :count).by(1).and change(Questao, :count).by(1) + + expect(response).to redirect_to(templates_path) + + template = Template.last + expect(template.title).to eq("Avaliação de Disciplina") + expect(template.admin_id).to eq(admin.id) + expect(template.questaos.first.enunciado).to eq("O conteúdo foi claro?") + end + + it "não cria o template e exibe erro quando o nome está em branco" do + admin = criar_admin + + expect { + post templates_path, params: { + admin_id: admin.id, + template: { + title: "", + target_role: "discente", + questaos: { "0" => { enunciado: "Pergunta válida", tipo: "rating" } } + } + } + }.not_to change(Template, :count) + + expect(response).to have_http_status(:unprocessable_content) + expect(response.body).to include("O nome do template é obrigatório") + end + + it "não cria o template e exibe erro quando a questão não possui enunciado" do + admin = criar_admin + + expect { + post templates_path, params: { + admin_id: admin.id, + template: { + title: "Avaliação sem enunciado", + target_role: "discente", + questaos: { "0" => { enunciado: "", tipo: "rating" } } + } + } + }.not_to change(Template, :count) + + expect(response).to have_http_status(:unprocessable_content) + expect(response.body).to include("Questão não possui enunciado") + end + + it "adiciona um novo campo de questão sem persistir nada ao clicar em '+'" do + admin = criar_admin + template_count_before = Template.count + questao_count_before = Questao.count + + post templates_path, params: { + admin_id: admin.id, + add_questao: "+", + template: { title: "Rascunho", target_role: "discente" } + } + + expect(response).to have_http_status(:ok) + expect(Template.count).to eq(template_count_before) + expect(Questao.count).to eq(questao_count_before) + expect(response.body).to include("template[questaos][0][enunciado]") + end + end + + describe "GET /templates" do + it "lista apenas os templates do administrador autenticado" do + admin = criar_admin + outro_admin = criar_admin + + meu_template = criar_template_com_questao(admin, titulo: "Meu Template") + criar_template_com_questao(outro_admin, titulo: "Template de Outro Admin") + + get templates_path(admin_id: admin.id) + + expect(response).to have_http_status(:success) + expect(response.body).to include(meu_template.title) + expect(response.body).not_to include("Template de Outro Admin") + end + end + + describe "GET /templates/:id/edit" do + it "bloqueia o acesso a um template de outro administrador" do + admin = criar_admin + outro_admin = criar_admin + template_outro_admin = criar_template_com_questao(outro_admin) + + get edit_template_path(template_outro_admin, admin_id: admin.id) + + expect(response).to redirect_to(templates_path) + follow_redirect! + expect(response.body).to include("Você não tem acesso a esse template") + end + end + + describe "PATCH /templates/:id" do + it "atualiza o template sem afetar as questões dos formulários já gerados a partir dele" do + admin = criar_admin + template = criar_template_com_questao(admin, enunciado: "Pergunta original") + turma = criar_turma + + formulario = admin.formularios.create!(title: "Formulário Gerado", template_id: template.id, turma_id: turma.id, target_role: template.target_role) + template.questaos.each { |questao| formulario.questaos.create!(enunciado: questao.enunciado, tipo: questao.tipo) } + + questao_existente = template.questaos.first + + patch template_path(template), params: { + admin_id: admin.id, + template: { + title: "Título atualizado", + target_role: template.target_role, + questaos: { "0" => { id: questao_existente.id, enunciado: questao_existente.enunciado, tipo: questao_existente.tipo } } + } + } + + expect(response).to redirect_to(templates_path) + expect(template.reload.title).to eq("Título atualizado") + + formulario.reload + expect(formulario.questaos.first.enunciado).to eq("Pergunta original") + expect(formulario.questaos.first.template_id).to be_nil + end + + it "exige enunciado para novas questões adicionadas durante a edição" do + admin = criar_admin + template = criar_template_com_questao(admin) + questao_existente = template.questaos.first + + patch template_path(template), params: { + admin_id: admin.id, + template: { + title: template.title, + target_role: template.target_role, + questaos: { + "0" => { id: questao_existente.id, enunciado: questao_existente.enunciado, tipo: questao_existente.tipo }, + "1" => { enunciado: "", tipo: "rating" } + } + } + } + + expect(response).to have_http_status(:unprocessable_content) + expect(response.body).to include("A nova questão não possui enunciado") + end + end + + describe "DELETE /templates/:id" do + it "remove o template, desvincula formulários gerados e mantém suas questões clonadas" do + admin = criar_admin + template = criar_template_com_questao(admin) + turma = criar_turma + + formulario = admin.formularios.create!(title: "Formulário Gerado", template_id: template.id, turma_id: turma.id, target_role: template.target_role) + template.questaos.each { |questao| formulario.questaos.create!(enunciado: questao.enunciado, tipo: questao.tipo) } + + expect { + delete template_path(template), params: { admin_id: admin.id } + }.to change(Template, :count).by(-1) + + expect(response).to redirect_to(templates_path) + + formulario.reload + expect(formulario.template_id).to be_nil + expect(formulario.questaos.count).to eq(1) + end + end +end diff --git a/project/spec/support/model_helpers.rb b/project/spec/support/model_helpers.rb new file mode 100644 index 0000000000..bf8621c021 --- /dev/null +++ b/project/spec/support/model_helpers.rb @@ -0,0 +1,48 @@ +module ModelHelpers + def departamento_de_teste + Departamento.find_or_create_by!(code: "DEPT001") do |departamento| + departamento.name = "Departamento de Teste" + end + end + + def criar_admin + departamento = departamento_de_teste + sufixo = SecureRandom.hex(4) + + Admin.create!( + name: "Admin Teste #{sufixo}", + email: "admin#{sufixo}@teste.com", + username: "admin#{sufixo}", + departamento_id: departamento.id + ) + end + + def criar_template_com_questao(admin, titulo: "Template #{SecureRandom.hex(4)}", target_role: "discente", enunciado: "Qual é a resposta correta?") + template = admin.templates.create!(title: titulo, target_role: target_role) + template.questaos.create!(enunciado: enunciado, tipo: "text") + template + end + + def criar_turma + departamento = departamento_de_teste + sufixo = SecureRandom.hex(4) + + professor = Professor.create!( + name: "Professor Teste #{sufixo}", + email: "professor#{sufixo}@teste.com", + matricula: "PROF#{sufixo}", + password: "senha12345", + departamento_id: departamento.id + ) + + Turma.create!( + class_code: "TURMA#{sufixo}", + subject_code: "DISC#{sufixo}", + subject_name: "Disciplina Teste", + semester: "2026.1", + time: "10:00", + departamento_id: departamento.id, + professor_id: professor.id + ) + end +end diff --git a/project/test/controllers/formularios_controller_test.rb b/project/test/controllers/formularios_controller_test.rb index f73e8cc6f9..26ad815e85 100644 --- a/project/test/controllers/formularios_controller_test.rb +++ b/project/test/controllers/formularios_controller_test.rb @@ -20,7 +20,7 @@ class FormulariosControllerTest < ActionDispatch::IntegrationTest post formularios_url, params: { formulario: { admin_id: @formulario.admin_id, status: @formulario.status, target_role: @formulario.target_role, template_id: @formulario.template_id, title: @formulario.title, turma_id: @formulario.turma_id } } end - assert_redirected_to formulario_url(Formulario.last) + assert_redirected_to formularios_url end test "should show formulario" do diff --git a/project/test/controllers/templates_controller_test.rb b/project/test/controllers/templates_controller_test.rb index 0759c34db9..4568eadea4 100644 --- a/project/test/controllers/templates_controller_test.rb +++ b/project/test/controllers/templates_controller_test.rb @@ -20,7 +20,7 @@ class TemplatesControllerTest < ActionDispatch::IntegrationTest post templates_url, params: { template: { admin_id: @template.admin_id, target_role: @template.target_role, title: @template.title } } end - assert_redirected_to template_url(Template.last) + assert_redirected_to templates_url end test "should show template" do @@ -35,7 +35,7 @@ class TemplatesControllerTest < ActionDispatch::IntegrationTest test "should update template" do patch template_url(@template), params: { template: { admin_id: @template.admin_id, target_role: @template.target_role, title: @template.title } } - assert_redirected_to template_url(@template) + assert_redirected_to templates_url end test "should destroy template" do From 34e52ea4a2c7dca4af335a370918c88e77025016 Mon Sep 17 00:00:00 2001 From: Rafael Dias Ghiorzi Date: Sun, 14 Jun 2026 23:17:16 -0300 Subject: [PATCH 16/23] refac: Adicionando factories para RSpec --- project/Gemfile | 2 + project/Gemfile.lock | 8 +++ project/spec/factories/admins.rb | 8 +++ project/spec/factories/alunos.rb | 9 +++ project/spec/factories/departamentos.rb | 6 ++ project/spec/factories/formularios.rb | 15 +++++ project/spec/factories/professors.rb | 10 ++++ project/spec/factories/questaos.rb | 16 ++++++ project/spec/factories/respostas.rb | 13 +++++ project/spec/factories/submissaos.rb | 10 ++++ project/spec/factories/templates.rb | 17 ++++++ project/spec/factories/turma_alunos.rb | 6 ++ project/spec/factories/turmas.rb | 11 ++++ project/spec/models/formulario_spec.rb | 67 +++++++++++++++++++++++ project/spec/models/questao_spec.rb | 39 +++++++++++++ project/spec/models/template_spec.rb | 45 +++++++++++++++ project/spec/rails_helper.rb | 2 +- project/spec/requests/formularios_spec.rb | 34 +++--------- project/spec/requests/respostas_spec.rb | 34 +----------- project/spec/requests/templates_spec.rb | 43 ++++++++------- project/spec/support/model_helpers.rb | 48 ---------------- 21 files changed, 316 insertions(+), 127 deletions(-) create mode 100644 project/spec/factories/admins.rb create mode 100644 project/spec/factories/alunos.rb create mode 100644 project/spec/factories/departamentos.rb create mode 100644 project/spec/factories/formularios.rb create mode 100644 project/spec/factories/professors.rb create mode 100644 project/spec/factories/questaos.rb create mode 100644 project/spec/factories/respostas.rb create mode 100644 project/spec/factories/submissaos.rb create mode 100644 project/spec/factories/templates.rb create mode 100644 project/spec/factories/turma_alunos.rb create mode 100644 project/spec/factories/turmas.rb create mode 100644 project/spec/models/formulario_spec.rb create mode 100644 project/spec/models/questao_spec.rb create mode 100644 project/spec/models/template_spec.rb delete mode 100644 project/spec/support/model_helpers.rb diff --git a/project/Gemfile b/project/Gemfile index 12c5414ee4..7597f349b8 100644 --- a/project/Gemfile +++ b/project/Gemfile @@ -70,4 +70,6 @@ end gem "rspec-rails", "~> 8.0", groups: [:development, :test] +gem "factory_bot_rails", groups: [:development, :test] + gem "csv", "~> 3.3" diff --git a/project/Gemfile.lock b/project/Gemfile.lock index b14c234e28..56a1d0a96e 100644 --- a/project/Gemfile.lock +++ b/project/Gemfile.lock @@ -151,6 +151,11 @@ GEM erubi (1.13.1) et-orbi (1.4.0) tzinfo + factory_bot (6.6.0) + activesupport (>= 6.1.0) + factory_bot_rails (6.5.1) + factory_bot (~> 6.5) + railties (>= 6.1.0) ffi (1.17.4-aarch64-linux-gnu) ffi (1.17.4-aarch64-linux-musl) ffi (1.17.4-arm-linux-gnu) @@ -464,6 +469,7 @@ DEPENDENCIES cucumber-rails database_cleaner debug + factory_bot_rails image_processing (~> 1.2) importmap-rails jbuilder @@ -534,6 +540,8 @@ CHECKSUMS erb (6.0.4) sha256=38e3803694be357fe2bfe312487c74beaf9fb4e5beb3e22498952fe1645b95d9 erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9 et-orbi (1.4.0) sha256=6c7e3c90779821f9e3b324c5e96fda9767f72995d6ae435b96678a4f3e2de8bc + factory_bot (6.6.0) sha256=1fc1b3b5620ec980a6a27aec1b6ec8c250ca82962e970e8a40f93e8d388d4b89 + factory_bot_rails (6.5.1) sha256=d3cc4851eae4dea8a665ec4a4516895045e710554d2b5ac9e68b94d351bc6d68 ffi (1.17.4-aarch64-linux-gnu) sha256=b208f06f91ffd8f5e1193da3cae3d2ccfc27fc36fba577baf698d26d91c080df ffi (1.17.4-aarch64-linux-musl) sha256=9286b7a615f2676245283aef0a0a3b475ae3aae2bb5448baace630bb77b91f39 ffi (1.17.4-arm-linux-gnu) sha256=d6dbddf7cb77bf955411af5f187a65b8cd378cb003c15c05697f5feee1cb1564 diff --git a/project/spec/factories/admins.rb b/project/spec/factories/admins.rb new file mode 100644 index 0000000000..7f132bf962 --- /dev/null +++ b/project/spec/factories/admins.rb @@ -0,0 +1,8 @@ +FactoryBot.define do + factory :admin do + departamento + sequence(:username) { |n| "admin#{n}" } + sequence(:email) { |n| "admin#{n}@teste.com" } + name { "Admin Teste" } + end +end diff --git a/project/spec/factories/alunos.rb b/project/spec/factories/alunos.rb new file mode 100644 index 0000000000..d59b7976df --- /dev/null +++ b/project/spec/factories/alunos.rb @@ -0,0 +1,9 @@ +FactoryBot.define do + factory :aluno do + sequence(:matricula) { |n| "19#{n.to_s.rjust(7, '0')}" } + sequence(:email) { |n| "aluno#{n}@teste.com" } + name { "Aluno Teste" } + course { "Engenharia de Software" } + password { "senha12345" } + end +end diff --git a/project/spec/factories/departamentos.rb b/project/spec/factories/departamentos.rb new file mode 100644 index 0000000000..eb651b90de --- /dev/null +++ b/project/spec/factories/departamentos.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :departamento do + sequence(:code) { |n| "DEPT#{n.to_s.rjust(3, '0')}" } + name { "Departamento de Teste" } + end +end diff --git a/project/spec/factories/formularios.rb b/project/spec/factories/formularios.rb new file mode 100644 index 0000000000..1cede36139 --- /dev/null +++ b/project/spec/factories/formularios.rb @@ -0,0 +1,15 @@ +FactoryBot.define do + factory :formulario do + admin + template + turma + sequence(:title) { |n| "Formulário de Teste #{n}" } + target_role { "discente" } + + trait :com_questao do + after(:create) do |formulario| + create(:questao, :de_formulario, formulario: formulario) + end + end + end +end diff --git a/project/spec/factories/professors.rb b/project/spec/factories/professors.rb new file mode 100644 index 0000000000..7110ec051c --- /dev/null +++ b/project/spec/factories/professors.rb @@ -0,0 +1,10 @@ +FactoryBot.define do + factory :professor do + departamento + sequence(:matricula) { |n| "PROF#{n.to_s.rjust(4, '0')}" } + sequence(:email) { |n| "professor#{n}@teste.com" } + name { "Professor Teste" } + formation { "Doutor em Ciência da Computação" } + password { "senha12345" } + end +end diff --git a/project/spec/factories/questaos.rb b/project/spec/factories/questaos.rb new file mode 100644 index 0000000000..a23cffa03c --- /dev/null +++ b/project/spec/factories/questaos.rb @@ -0,0 +1,16 @@ +FactoryBot.define do + factory :questao do + template + sequence(:enunciado) { |n| "Pergunta de teste #{n}?" } + tipo { "text" } + + trait :rating do + tipo { "rating" } + end + + trait :de_formulario do + template { nil } + formulario + end + end +end diff --git a/project/spec/factories/respostas.rb b/project/spec/factories/respostas.rb new file mode 100644 index 0000000000..ea45109848 --- /dev/null +++ b/project/spec/factories/respostas.rb @@ -0,0 +1,13 @@ +FactoryBot.define do + factory :respostum do + submissao + questao + valor_texto { "Resposta de teste" } + valor_numerico { nil } + + trait :numerica do + valor_texto { nil } + valor_numerico { 5 } + end + end +end diff --git a/project/spec/factories/submissaos.rb b/project/spec/factories/submissaos.rb new file mode 100644 index 0000000000..2e227ef033 --- /dev/null +++ b/project/spec/factories/submissaos.rb @@ -0,0 +1,10 @@ +FactoryBot.define do + factory :submissao do + formulario + association :participant, factory: :aluno + + trait :de_professor do + association :participant, factory: :professor + end + end +end diff --git a/project/spec/factories/templates.rb b/project/spec/factories/templates.rb new file mode 100644 index 0000000000..76c74e7a48 --- /dev/null +++ b/project/spec/factories/templates.rb @@ -0,0 +1,17 @@ +FactoryBot.define do + factory :template do + admin + sequence(:title) { |n| "Template #{n}" } + target_role { "discente" } + + trait :docente do + target_role { "docente" } + end + + trait :com_questao do + after(:create) do |template| + create(:questao, template: template) + end + end + end +end diff --git a/project/spec/factories/turma_alunos.rb b/project/spec/factories/turma_alunos.rb new file mode 100644 index 0000000000..89c3155677 --- /dev/null +++ b/project/spec/factories/turma_alunos.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :turma_aluno do + turma + aluno + end +end diff --git a/project/spec/factories/turmas.rb b/project/spec/factories/turmas.rb new file mode 100644 index 0000000000..266bc2ce1a --- /dev/null +++ b/project/spec/factories/turmas.rb @@ -0,0 +1,11 @@ +FactoryBot.define do + factory :turma do + departamento + professor + sequence(:class_code) { |n| "TURMA#{n}" } + sequence(:subject_code) { |n| "DISC#{n}" } + subject_name { "Disciplina Teste" } + semester { "2026.1" } + time { "10:00" } + end +end diff --git a/project/spec/models/formulario_spec.rb b/project/spec/models/formulario_spec.rb new file mode 100644 index 0000000000..1ad7e7a724 --- /dev/null +++ b/project/spec/models/formulario_spec.rb @@ -0,0 +1,67 @@ +require "rails_helper" + +RSpec.describe Formulario, type: :model do + describe "validações" do + it "é válido e persistido com os atributos da factory" do + expect(create(:formulario)).to be_persisted + end + + it "exige um título" do + formulario = build(:formulario, title: "") + + expect(formulario).not_to be_valid + expect(formulario.errors[:title]).to include("O nome do formulário é obrigatório") + end + + it "exige um template ao ser criado (#103)" do + formulario = build(:formulario, template: nil) + + expect(formulario).not_to be_valid + expect(formulario.errors[:template_id]).to include("É necessário escolher um template base") + end + + it "exige uma turma ao ser criado (#103)" do + formulario = build(:formulario, turma: nil) + + expect(formulario).not_to be_valid + expect(formulario.errors[:turma_id]).to include("É necessário escolher ao menos uma turma") + end + + it "não exige template nem turma em atualizações (#112)" do + formulario = create(:formulario) + + formulario.template_id = nil + formulario.turma_id = nil + + expect(formulario).to be_valid + end + end + + describe "valores padrão" do + it "define status como 'draft' quando não informado" do + expect(Formulario.new.status).to eq("draft") + end + + it "não sobrescreve o status quando já informado" do + formulario = Formulario.new(status: "concluido") + + expect(formulario.status).to eq("concluido") + end + end + + describe "associações" do + it "pertence a um admin, um template e uma turma" do + formulario = create(:formulario) + + expect(formulario.admin).to be_present + expect(formulario.template).to be_present + expect(formulario.turma).to be_present + end + + it "destrói suas próprias questões (clonadas do template) ao ser destruído" do + formulario = create(:formulario, :com_questao) + + expect { formulario.destroy }.to change(Questao, :count).by(-1) + end + end +end diff --git a/project/spec/models/questao_spec.rb b/project/spec/models/questao_spec.rb new file mode 100644 index 0000000000..766d1490cb --- /dev/null +++ b/project/spec/models/questao_spec.rb @@ -0,0 +1,39 @@ +require "rails_helper" + +RSpec.describe Questao, type: :model do + describe "validações" do + it "é válida com os atributos da factory" do + expect(build(:questao)).to be_valid + end + + it "exige um enunciado" do + questao = build(:questao, enunciado: nil) + + expect(questao).not_to be_valid + expect(questao.errors[:enunciado]).to include("can't be blank") + end + end + + describe "associações" do + it "pode pertencer a um template (questão de um template, #102)" do + template = create(:template) + questao = create(:questao, template: template) + + expect(questao.template).to eq(template) + expect(questao.formulario_id).to be_nil + end + + it "pode pertencer a um formulário sem template (questão clonada, #103)" do + questao = create(:questao, :de_formulario) + + expect(questao.template_id).to be_nil + expect(questao.formulario).to be_present + end + + it "é válida sem template e sem formulário associados" do + questao = build(:questao, template: nil, formulario: nil) + + expect(questao).to be_valid + end + end +end diff --git a/project/spec/models/template_spec.rb b/project/spec/models/template_spec.rb new file mode 100644 index 0000000000..4ab7f734ab --- /dev/null +++ b/project/spec/models/template_spec.rb @@ -0,0 +1,45 @@ +require "rails_helper" + +RSpec.describe Template, type: :model do + describe "validações" do + it "é válido com os atributos da factory" do + expect(build(:template)).to be_valid + end + + it "exige um título" do + template = build(:template, title: "") + + expect(template).not_to be_valid + expect(template.errors[:title]).to include("O nome do template é obrigatório") + end + end + + describe "associações" do + it "pertence a um admin" do + template = create(:template) + + expect(template.admin).to be_present + end + + it "destrói suas questões ao ser destruído (#102)" do + template = create(:template, :com_questao) + + expect { template.destroy }.to change(Questao, :count).by(-1) + end + + it "desvincula, mas não destrói, formulários gerados ao ser destruído (#112)" do + template = create(:template) + formulario = create(:formulario, template: template) + + expect { template.destroy }.not_to change(Formulario, :count) + expect(formulario.reload.template_id).to be_nil + end + + it "não afeta as questões já clonadas para um formulário ao ser destruído (#112)" do + template = create(:template) + create(:formulario, :com_questao, template: template) + + expect { template.destroy }.not_to change(Questao, :count) + end + end +end diff --git a/project/spec/rails_helper.rb b/project/spec/rails_helper.rb index b1765001cd..63a676c97a 100644 --- a/project/spec/rails_helper.rb +++ b/project/spec/rails_helper.rb @@ -70,5 +70,5 @@ # arbitrary gems may also be filtered via: # config.filter_gems_from_backtrace("gem name") - config.include ModelHelpers, type: :request + config.include FactoryBot::Syntax::Methods end diff --git a/project/spec/requests/formularios_spec.rb b/project/spec/requests/formularios_spec.rb index 6bc0c1d09b..19dc8afba5 100644 --- a/project/spec/requests/formularios_spec.rb +++ b/project/spec/requests/formularios_spec.rb @@ -3,24 +3,7 @@ RSpec.describe "Formularios", type: :request do describe "GET /formularios/:id/exportar_csv" do it "retorna um arquivo CSV de sucesso com as respostas da avaliação" do - # 1. Base - departamento = Departamento.new - departamento.save(validate: false) - - admin = Admin.new(departamento_id: departamento.id) - admin.save(validate: false) - - professor = Professor.new(departamento_id: departamento.id) - professor.save(validate: false) - - template = Template.new(admin_id: admin.id) - template.save(validate: false) - - turma = Turma.new(departamento_id: departamento.id, professor_id: professor.id) - turma.save(validate: false) - - formulario = Formulario.new(turma_id: turma.id, template_id: template.id, admin_id: admin.id) - formulario.save(validate: false) + formulario = create(:formulario) get exportar_csv_formulario_path(formulario, format: :csv) @@ -33,9 +16,10 @@ describe "POST /formularios" do it "cria um formulário a partir de um template e clona as questões do template" do - admin = criar_admin - template = criar_template_com_questao(admin, enunciado: "O professor foi claro?") - turma = criar_turma + admin = create(:admin) + template = create(:template, admin: admin) + create(:questao, template: template, enunciado: "O professor foi claro?") + turma = create(:turma) expect { post formularios_path, params: { @@ -55,8 +39,8 @@ end it "não cria o formulário quando nenhum template é selecionado" do - admin = criar_admin - turma = criar_turma + admin = create(:admin) + turma = create(:turma) expect { post formularios_path, params: { @@ -70,8 +54,8 @@ end it "não cria o formulário quando nenhuma turma é selecionada" do - admin = criar_admin - template = criar_template_com_questao(admin) + admin = create(:admin) + template = create(:template, :com_questao, admin: admin) expect { post formularios_path, params: { diff --git a/project/spec/requests/respostas_spec.rb b/project/spec/requests/respostas_spec.rb index b9e91c8041..7b395cfdf0 100644 --- a/project/spec/requests/respostas_spec.rb +++ b/project/spec/requests/respostas_spec.rb @@ -3,38 +3,8 @@ RSpec.describe "Respostas", type: :request do describe "POST /resposta" do it "cria uma nova resposta mapeando a nota corretamente para o atributo do banco" do - # 1. Base da hierarquia - departamento = Departamento.new - departamento.save(validate: false) - - admin = Admin.new(departamento_id: departamento.id) - admin.save(validate: false) - - professor = Professor.new(departamento_id: departamento.id) - professor.save(validate: false) - - template = Template.new(admin_id: admin.id) - template.save(validate: false) - - turma = Turma.new(departamento_id: departamento.id, professor_id: professor.id) - turma.save(validate: false) - - formulario = Formulario.new(turma_id: turma.id, template_id: template.id, admin_id: admin.id) - formulario.save(validate: false) - - # 2. Cria o Aluno para satisfazer o polimorfismo da Submissão - aluno = Aluno.new - aluno.save(validate: false) - - submissao = Submissao.new( - formulario_id: formulario.id, - participant_id: aluno.id, - participant_type: 'Aluno' - ) - submissao.save(validate: false) - - questao = Questao.new(template_id: template.id) - questao.save(validate: false) + submissao = create(:submissao) + questao = create(:questao, :de_formulario, formulario: submissao.formulario) # Parâmetros usando o nome exato do Schema para sincronização perfeita parametros = { diff --git a/project/spec/requests/templates_spec.rb b/project/spec/requests/templates_spec.rb index c5b6a5fd9c..4cc4efe4db 100644 --- a/project/spec/requests/templates_spec.rb +++ b/project/spec/requests/templates_spec.rb @@ -3,7 +3,7 @@ RSpec.describe "Templates", type: :request do describe "POST /templates" do it "cria um template com uma questão e redireciona para a lista de templates" do - admin = criar_admin + admin = create(:admin) expect { post templates_path, params: { @@ -25,7 +25,7 @@ end it "não cria o template e exibe erro quando o nome está em branco" do - admin = criar_admin + admin = create(:admin) expect { post templates_path, params: { @@ -43,7 +43,7 @@ end it "não cria o template e exibe erro quando a questão não possui enunciado" do - admin = criar_admin + admin = create(:admin) expect { post templates_path, params: { @@ -61,7 +61,7 @@ end it "adiciona um novo campo de questão sem persistir nada ao clicar em '+'" do - admin = criar_admin + admin = create(:admin) template_count_before = Template.count questao_count_before = Questao.count @@ -80,11 +80,11 @@ describe "GET /templates" do it "lista apenas os templates do administrador autenticado" do - admin = criar_admin - outro_admin = criar_admin + admin = create(:admin) + outro_admin = create(:admin) - meu_template = criar_template_com_questao(admin, titulo: "Meu Template") - criar_template_com_questao(outro_admin, titulo: "Template de Outro Admin") + meu_template = create(:template, :com_questao, admin: admin, title: "Meu Template") + create(:template, :com_questao, admin: outro_admin, title: "Template de Outro Admin") get templates_path(admin_id: admin.id) @@ -96,9 +96,9 @@ describe "GET /templates/:id/edit" do it "bloqueia o acesso a um template de outro administrador" do - admin = criar_admin - outro_admin = criar_admin - template_outro_admin = criar_template_com_questao(outro_admin) + admin = create(:admin) + outro_admin = create(:admin) + template_outro_admin = create(:template, :com_questao, admin: outro_admin) get edit_template_path(template_outro_admin, admin_id: admin.id) @@ -110,11 +110,12 @@ describe "PATCH /templates/:id" do it "atualiza o template sem afetar as questões dos formulários já gerados a partir dele" do - admin = criar_admin - template = criar_template_com_questao(admin, enunciado: "Pergunta original") - turma = criar_turma + admin = create(:admin) + template = create(:template, admin: admin) + create(:questao, template: template, enunciado: "Pergunta original") + turma = create(:turma) - formulario = admin.formularios.create!(title: "Formulário Gerado", template_id: template.id, turma_id: turma.id, target_role: template.target_role) + formulario = create(:formulario, admin: admin, template: template, turma: turma, title: "Formulário Gerado", target_role: template.target_role) template.questaos.each { |questao| formulario.questaos.create!(enunciado: questao.enunciado, tipo: questao.tipo) } questao_existente = template.questaos.first @@ -137,8 +138,8 @@ end it "exige enunciado para novas questões adicionadas durante a edição" do - admin = criar_admin - template = criar_template_com_questao(admin) + admin = create(:admin) + template = create(:template, :com_questao, admin: admin) questao_existente = template.questaos.first patch template_path(template), params: { @@ -160,11 +161,11 @@ describe "DELETE /templates/:id" do it "remove o template, desvincula formulários gerados e mantém suas questões clonadas" do - admin = criar_admin - template = criar_template_com_questao(admin) - turma = criar_turma + admin = create(:admin) + template = create(:template, :com_questao, admin: admin) + turma = create(:turma) - formulario = admin.formularios.create!(title: "Formulário Gerado", template_id: template.id, turma_id: turma.id, target_role: template.target_role) + formulario = create(:formulario, admin: admin, template: template, turma: turma, title: "Formulário Gerado", target_role: template.target_role) template.questaos.each { |questao| formulario.questaos.create!(enunciado: questao.enunciado, tipo: questao.tipo) } expect { diff --git a/project/spec/support/model_helpers.rb b/project/spec/support/model_helpers.rb deleted file mode 100644 index bf8621c021..0000000000 --- a/project/spec/support/model_helpers.rb +++ /dev/null @@ -1,48 +0,0 @@ -module ModelHelpers - def departamento_de_teste - Departamento.find_or_create_by!(code: "DEPT001") do |departamento| - departamento.name = "Departamento de Teste" - end - end - - def criar_admin - departamento = departamento_de_teste - sufixo = SecureRandom.hex(4) - - Admin.create!( - name: "Admin Teste #{sufixo}", - email: "admin#{sufixo}@teste.com", - username: "admin#{sufixo}", - departamento_id: departamento.id - ) - end - - def criar_template_com_questao(admin, titulo: "Template #{SecureRandom.hex(4)}", target_role: "discente", enunciado: "Qual é a resposta correta?") - template = admin.templates.create!(title: titulo, target_role: target_role) - template.questaos.create!(enunciado: enunciado, tipo: "text") - template - end - - def criar_turma - departamento = departamento_de_teste - sufixo = SecureRandom.hex(4) - - professor = Professor.create!( - name: "Professor Teste #{sufixo}", - email: "professor#{sufixo}@teste.com", - matricula: "PROF#{sufixo}", - password: "senha12345", - departamento_id: departamento.id - ) - - Turma.create!( - class_code: "TURMA#{sufixo}", - subject_code: "DISC#{sufixo}", - subject_name: "Disciplina Teste", - semester: "2026.1", - time: "10:00", - departamento_id: departamento.id, - professor_id: professor.id - ) - end -end From fee8a517ff63d9a10b95a453282e6c0746cc9861 Mon Sep 17 00:00:00 2001 From: Diego Gontijo Date: Mon, 15 Jun 2026 18:33:02 -0300 Subject: [PATCH 17/23] =?UTF-8?q?feat:=20implementar=20fluxo=20de=20autent?= =?UTF-8?q?ica=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- project/.rspec | 1 + project/.rubocop.yml | 4 + project/Gemfile | 10 +- project/Gemfile.lock | 3 + .../app/controllers/application_controller.rb | 26 +- .../app/controllers/dashboard_controller.rb | 6 + .../imports/class_members_controller.rb | 10 + .../controllers/password_resets_controller.rb | 51 +++ .../controllers/password_setups_controller.rb | 35 ++ .../app/controllers/sessions_controller.rb | 30 ++ project/app/mailers/user_mailer.rb | 15 + project/app/models/class_membership.rb | 9 + project/app/models/course_class.rb | 8 + project/app/models/department.rb | 7 + project/app/models/import_inconsistency.rb | 3 + project/app/models/user.rb | 76 +++++ .../services/sigaa/class_members_importer.rb | 119 +++++++ project/app/views/dashboard/show.html.erb | 20 ++ .../app/views/password_resets/edit.html.erb | 17 + .../views/password_resets/invalid.html.erb | 3 + .../app/views/password_resets/new.html.erb | 12 + .../app/views/password_setups/edit.html.erb | 17 + .../views/password_setups/invalid.html.erb | 3 + project/app/views/sessions/new.html.erb | 19 ++ project/app/views/shared/_flash.html.erb | 3 + .../views/user_mailer/password_reset.html.erb | 5 + .../views/user_mailer/password_reset.text.erb | 5 + .../views/user_mailer/password_setup.html.erb | 5 + .../views/user_mailer/password_setup.text.erb | 5 + project/config/environments/development.rb | 4 +- project/config/environments/test.rb | 4 +- project/config/routes.rb | 19 +- .../20260615000100_create_departments.rb | 12 + .../20260615000200_create_course_classes.rb | 15 + .../db/migrate/20260615000300_create_users.rb | 23 ++ ...20260615000400_create_class_memberships.rb | 13 + ...615000500_create_import_inconsistencies.rb | 11 + project/db/schema.rb | 65 +++- .../features/step_definitions/auth_steps.rb | 319 ++++++++++++++++++ project/spec/mailers/user_mailer_spec.rb | 21 ++ project/spec/rails_helper.rb | 74 +--- project/spec/requests/password_resets_spec.rb | 57 ++++ project/spec/requests/password_setups_spec.rb | 44 +++ project/spec/requests/sessions_spec.rb | 50 +++ .../services/class_members_importer_spec.rb | 104 ++++++ project/spec/spec_helper.rb | 87 ----- project/spec/support/auth_helpers.rb | 30 ++ 47 files changed, 1313 insertions(+), 166 deletions(-) create mode 100644 project/app/controllers/dashboard_controller.rb create mode 100644 project/app/controllers/imports/class_members_controller.rb create mode 100644 project/app/controllers/password_resets_controller.rb create mode 100644 project/app/controllers/password_setups_controller.rb create mode 100644 project/app/controllers/sessions_controller.rb create mode 100644 project/app/mailers/user_mailer.rb create mode 100644 project/app/models/class_membership.rb create mode 100644 project/app/models/course_class.rb create mode 100644 project/app/models/department.rb create mode 100644 project/app/models/import_inconsistency.rb create mode 100644 project/app/models/user.rb create mode 100644 project/app/services/sigaa/class_members_importer.rb create mode 100644 project/app/views/dashboard/show.html.erb create mode 100644 project/app/views/password_resets/edit.html.erb create mode 100644 project/app/views/password_resets/invalid.html.erb create mode 100644 project/app/views/password_resets/new.html.erb create mode 100644 project/app/views/password_setups/edit.html.erb create mode 100644 project/app/views/password_setups/invalid.html.erb create mode 100644 project/app/views/sessions/new.html.erb create mode 100644 project/app/views/shared/_flash.html.erb create mode 100644 project/app/views/user_mailer/password_reset.html.erb create mode 100644 project/app/views/user_mailer/password_reset.text.erb create mode 100644 project/app/views/user_mailer/password_setup.html.erb create mode 100644 project/app/views/user_mailer/password_setup.text.erb create mode 100644 project/db/migrate/20260615000100_create_departments.rb create mode 100644 project/db/migrate/20260615000200_create_course_classes.rb create mode 100644 project/db/migrate/20260615000300_create_users.rb create mode 100644 project/db/migrate/20260615000400_create_class_memberships.rb create mode 100644 project/db/migrate/20260615000500_create_import_inconsistencies.rb create mode 100644 project/features/step_definitions/auth_steps.rb create mode 100644 project/spec/mailers/user_mailer_spec.rb create mode 100644 project/spec/requests/password_resets_spec.rb create mode 100644 project/spec/requests/password_setups_spec.rb create mode 100644 project/spec/requests/sessions_spec.rb create mode 100644 project/spec/services/class_members_importer_spec.rb create mode 100644 project/spec/support/auth_helpers.rb diff --git a/project/.rspec b/project/.rspec index c99d2e7396..5be63fcb08 100644 --- a/project/.rspec +++ b/project/.rspec @@ -1 +1,2 @@ --require spec_helper +--format documentation diff --git a/project/.rubocop.yml b/project/.rubocop.yml index f9d86d4a54..1391b9893d 100644 --- a/project/.rubocop.yml +++ b/project/.rubocop.yml @@ -6,3 +6,7 @@ inherit_gem: { rubocop-rails-omakase: rubocop.yml } # # Use `[a, [b, c]]` not `[ a, [ b, c ] ]` # Layout/SpaceInsideArrayLiteralBrackets: # Enabled: false + +AllCops: + Exclude: + - "lib/tasks/cucumber.rake" diff --git a/project/Gemfile b/project/Gemfile index 7597f349b8..98005f64d2 100644 --- a/project/Gemfile +++ b/project/Gemfile @@ -22,6 +22,8 @@ gem "bcrypt", "~> 3.1.7" # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem "tzinfo-data", platforms: %i[ windows jruby ] +# Ruby 4 no longer ships fiddle as a default gem; Rails console uses it through reline on Windows. +gem "fiddle" # Use the database-backed adapters for Rails.cache, Active Job, and Action Cable gem "solid_cache" @@ -52,6 +54,9 @@ group :development, :test do # Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/] gem "rubocop-rails-omakase", require: false + + gem "factory_bot_rails" + gem "rspec-rails", "~> 8.0" end group :development do @@ -67,9 +72,4 @@ group :test do gem "database_cleaner" end - -gem "rspec-rails", "~> 8.0", groups: [:development, :test] - -gem "factory_bot_rails", groups: [:development, :test] - gem "csv", "~> 3.3" diff --git a/project/Gemfile.lock b/project/Gemfile.lock index 56a1d0a96e..ac72d3b254 100644 --- a/project/Gemfile.lock +++ b/project/Gemfile.lock @@ -163,6 +163,7 @@ GEM ffi (1.17.4-x64-mingw-ucrt) ffi (1.17.4-x86_64-linux-gnu) ffi (1.17.4-x86_64-linux-musl) + fiddle (1.1.8) fugit (1.12.1) et-orbi (~> 1.4) raabro (~> 1.4) @@ -470,6 +471,7 @@ DEPENDENCIES database_cleaner debug factory_bot_rails + fiddle image_processing (~> 1.2) importmap-rails jbuilder @@ -549,6 +551,7 @@ CHECKSUMS ffi (1.17.4-x64-mingw-ucrt) sha256=f6ff9618cfccc494138bddade27aa06c74c6c7bc367a1ea1103d80c2fcb9ed35 ffi (1.17.4-x86_64-linux-gnu) sha256=9d3db14c2eae074b382fa9c083fe95aec6e0a1451da249eab096c34002bc752d ffi (1.17.4-x86_64-linux-musl) sha256=3fdf9888483de005f8ef8d1cf2d3b20d86626af206cbf780f6a6a12439a9c49e + fiddle (1.1.8) sha256=7fa8ee3627271497f3add5503acdbc3f40b32f610fc1cf49634f083ef3f32eee fugit (1.12.1) sha256=5898f478ede9b415f0804e42b8f3fd53f814bd85eebffceebdbc34e1107aaf68 globalid (1.3.0) sha256=05c639ad6eb4594522a0b07983022f04aa7254626ab69445a0e493aa3786ff11 i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5 diff --git a/project/app/controllers/application_controller.rb b/project/app/controllers/application_controller.rb index 4286afdc87..4f8a11dcd3 100644 --- a/project/app/controllers/application_controller.rb +++ b/project/app/controllers/application_controller.rb @@ -5,18 +5,32 @@ class ApplicationController < ActionController::Base # Changes to the importmap will invalidate the etag for HTML responses stale_when_importmap_changes + helper_method :current_admin, :current_user, :user_signed_in? + private - # Login (#104) ainda não foi implementado: por ora o admin autenticado é - # identificado pela sessão, alimentada por um parâmetro admin_id repassado - # nos links/formulários na primeira requisição. Quando nada foi informado - # ainda (ex: acesso direto a /templates), cai para o primeiro admin - # cadastrado, para que a sessão fique consistente a partir da próxima requisição. def current_admin return @current_admin if defined?(@current_admin) session[:admin_id] = params[:admin_id] if params[:admin_id].present? @current_admin = Admin.find_by(id: session[:admin_id]) || Admin.first end - helper_method :current_admin + + def current_user + @current_user ||= User.find_by(id: session[:user_id]) if session[:user_id].present? + end + + def user_signed_in? + current_user.present? + end + + def require_login + redirect_to login_path, alert: "Faca login para continuar" unless user_signed_in? + end + + def require_admin + return if user_signed_in? && current_user.admin? + + redirect_to root_path, alert: "Acesso restrito a administradores" + end end diff --git a/project/app/controllers/dashboard_controller.rb b/project/app/controllers/dashboard_controller.rb new file mode 100644 index 0000000000..2000f0424c --- /dev/null +++ b/project/app/controllers/dashboard_controller.rb @@ -0,0 +1,6 @@ +class DashboardController < ApplicationController + before_action :require_login + + def show + end +end diff --git a/project/app/controllers/imports/class_members_controller.rb b/project/app/controllers/imports/class_members_controller.rb new file mode 100644 index 0000000000..290edab245 --- /dev/null +++ b/project/app/controllers/imports/class_members_controller.rb @@ -0,0 +1,10 @@ +module Imports + class ClassMembersController < ApplicationController + before_action :require_admin + + def create + result = Sigaa::ClassMembersImporter.new(imported_by: current_user).call + redirect_to root_path, notice: "Importacao concluida: #{result.created_users} usuarios criados" + end + end +end diff --git a/project/app/controllers/password_resets_controller.rb b/project/app/controllers/password_resets_controller.rb new file mode 100644 index 0000000000..cf76ccda2f --- /dev/null +++ b/project/app/controllers/password_resets_controller.rb @@ -0,0 +1,51 @@ +class PasswordResetsController < ApplicationController + GENERIC_MESSAGE = "Se o email estiver cadastrado, voce recebera instrucoes para redefinir sua senha" + + def new + end + + def create + user = User.find_by(email: reset_request_params[:email].to_s.strip.downcase) + UserMailer.password_reset(user, user.generate_password_reset_token!).deliver_now if user + + redirect_to new_password_reset_path, notice: GENERIC_MESSAGE + end + + def edit + load_user + render_invalid_token unless @user + end + + def update + load_user + return render_invalid_token unless @user + + if password_params[:password] != password_params[:password_confirmation] + flash.now[:alert] = "Confirmacao de senha nao confere" + render :edit, status: :unprocessable_entity + else + @user.apply_new_password!(password_params[:password]) + redirect_to login_path, notice: "Senha redefinida com sucesso" + end + end + + private + + def load_user + @token = params[:token] + @user = User.find_by_reset_token(@token) + end + + def reset_request_params + params.require(:password_reset).permit(:email) + end + + def password_params + params.require(:user).permit(:password, :password_confirmation) + end + + def render_invalid_token + flash.now[:alert] = "Link de redefinicao de senha invalido ou expirado" + render :invalid, status: :not_found + end +end diff --git a/project/app/controllers/password_setups_controller.rb b/project/app/controllers/password_setups_controller.rb new file mode 100644 index 0000000000..10916d7281 --- /dev/null +++ b/project/app/controllers/password_setups_controller.rb @@ -0,0 +1,35 @@ +class PasswordSetupsController < ApplicationController + before_action :load_user + + def edit + render_invalid_token unless @user + end + + def update + return render_invalid_token unless @user + + if password_params[:password] != password_params[:password_confirmation] + flash.now[:alert] = "Confirmacao de senha nao confere" + render :edit, status: :unprocessable_entity + else + @user.apply_new_password!(password_params[:password]) + redirect_to login_path, notice: "Senha definida com sucesso" + end + end + + private + + def load_user + @token = params[:token] + @user = User.find_by_setup_token(@token) + end + + def password_params + params.require(:user).permit(:password, :password_confirmation) + end + + def render_invalid_token + flash.now[:alert] = "Link de definicao de senha invalido ou expirado" + render :invalid, status: :not_found + end +end diff --git a/project/app/controllers/sessions_controller.rb b/project/app/controllers/sessions_controller.rb new file mode 100644 index 0000000000..f83f8ed224 --- /dev/null +++ b/project/app/controllers/sessions_controller.rb @@ -0,0 +1,30 @@ +class SessionsController < ApplicationController + def new + end + + def create + user = User.find_for_login(session_params[:identifier]) + + if user&.pending_password_setup? + flash.now[:alert] = "Senha inicial precisa ser definida antes do acesso" + render :new, status: :unprocessable_entity + elsif user&.authenticate(session_params[:password]) + session[:user_id] = user.id + redirect_to root_path, notice: "Login realizado com sucesso" + else + flash.now[:alert] = "Email, matricula ou senha invalidos" + render :new, status: :unprocessable_entity + end + end + + def destroy + reset_session + redirect_to login_path, notice: "Logout realizado com sucesso" + end + + private + + def session_params + params.require(:session).permit(:identifier, :password) + end +end diff --git a/project/app/mailers/user_mailer.rb b/project/app/mailers/user_mailer.rb new file mode 100644 index 0000000000..8ce249be84 --- /dev/null +++ b/project/app/mailers/user_mailer.rb @@ -0,0 +1,15 @@ +class UserMailer < ApplicationMailer + default from: "noreply@camaar.local" + + def password_setup(user, token) + @user = user + @url = edit_password_setup_url(token) + mail(to: user.email, subject: "Defina sua senha no CAMAAR") + end + + def password_reset(user, token) + @user = user + @url = edit_password_reset_url(token) + mail(to: user.email, subject: "Redefinicao de senha do CAMAAR") + end +end diff --git a/project/app/models/class_membership.rb b/project/app/models/class_membership.rb new file mode 100644 index 0000000000..0b94fdd2a0 --- /dev/null +++ b/project/app/models/class_membership.rb @@ -0,0 +1,9 @@ +class ClassMembership < ApplicationRecord + belongs_to :user + belongs_to :course_class + + enum :role, { discente: 0, docente: 1 } + + validates :role, presence: true + validates :user_id, uniqueness: { scope: [ :course_class_id, :role ] } +end diff --git a/project/app/models/course_class.rb b/project/app/models/course_class.rb new file mode 100644 index 0000000000..39b0442005 --- /dev/null +++ b/project/app/models/course_class.rb @@ -0,0 +1,8 @@ +class CourseClass < ApplicationRecord + belongs_to :department + has_many :class_memberships, dependent: :destroy + has_many :users, through: :class_memberships + + validates :code, :class_code, :semester, presence: true + validates :code, uniqueness: { scope: [ :class_code, :semester ] } +end diff --git a/project/app/models/department.rb b/project/app/models/department.rb new file mode 100644 index 0000000000..a1025cc54e --- /dev/null +++ b/project/app/models/department.rb @@ -0,0 +1,7 @@ +class Department < ApplicationRecord + has_many :course_classes, dependent: :restrict_with_exception + has_many :users, dependent: :nullify + + validates :name, :code, presence: true + validates :code, uniqueness: true +end diff --git a/project/app/models/import_inconsistency.rb b/project/app/models/import_inconsistency.rb new file mode 100644 index 0000000000..5da38ddd65 --- /dev/null +++ b/project/app/models/import_inconsistency.rb @@ -0,0 +1,3 @@ +class ImportInconsistency < ApplicationRecord + validates :source, :message, presence: true +end diff --git a/project/app/models/user.rb b/project/app/models/user.rb new file mode 100644 index 0000000000..0297022e07 --- /dev/null +++ b/project/app/models/user.rb @@ -0,0 +1,76 @@ +class User < ApplicationRecord + PASSWORD_SETUP_TTL = 7.days + PASSWORD_RESET_TTL = 2.hours + + belongs_to :department, optional: true + has_many :class_memberships, dependent: :destroy + has_many :course_classes, through: :class_memberships + + has_secure_password validations: false + + enum :role, { admin: 0, participant: 1 } + + normalizes :email, with: ->(email) { email.strip.downcase } + normalizes :registration, with: ->(registration) { registration.strip } + + validates :name, :email, :registration, :role, presence: true + validates :email, uniqueness: true + validates :registration, uniqueness: true + + def self.find_for_login(identifier) + normalized = identifier.to_s.strip + find_by(email: normalized.downcase) || find_by(registration: normalized) + end + + def self.find_by_setup_token(token) + find_valid_token(:password_setup_token_digest, :password_setup_sent_at, token, PASSWORD_SETUP_TTL) + end + + def self.find_by_reset_token(token) + find_valid_token(:password_reset_token_digest, :password_reset_sent_at, token, PASSWORD_RESET_TTL) + end + + def password_defined? + password_digest.present? + end + + def pending_password_setup? + !password_defined? + end + + def generate_password_setup_token! + token = SecureRandom.urlsafe_base64(32) + update!(password_setup_token_digest: self.class.token_digest(token), password_setup_sent_at: Time.current) + token + end + + def generate_password_reset_token! + token = SecureRandom.urlsafe_base64(32) + update!(password_reset_token_digest: self.class.token_digest(token), password_reset_sent_at: Time.current) + token + end + + def apply_new_password!(new_password) + update!( + password: new_password, + password_setup_token_digest: nil, + password_setup_sent_at: nil, + password_reset_token_digest: nil, + password_reset_sent_at: nil + ) + end + + def self.token_digest(token) + Digest::SHA256.hexdigest(token.to_s) + end + + def self.find_valid_token(digest_column, sent_at_column, token, ttl) + user = find_by(digest_column => token_digest(token)) + return unless user + return if user.public_send(sent_at_column).blank? + return if user.public_send(sent_at_column) < ttl.ago + + user + end + private_class_method :find_valid_token +end diff --git a/project/app/services/sigaa/class_members_importer.rb b/project/app/services/sigaa/class_members_importer.rb new file mode 100644 index 0000000000..8f31bb747b --- /dev/null +++ b/project/app/services/sigaa/class_members_importer.rb @@ -0,0 +1,119 @@ +module Sigaa + class ClassMembersImporter + Result = Data.define(:created_users, :updated_memberships, :inconsistencies) + + DEFAULT_PATH = Rails.root.join("..", "class_members.json") + + def initialize(path: DEFAULT_PATH, imported_by: nil) + @path = path + @imported_by = imported_by + end + + def call + created_users = 0 + updated_memberships = 0 + inconsistencies = 0 + + parsed_data.each do |class_payload| + course_class = find_or_create_course_class(class_payload) + + members_for(class_payload).each do |member_payload, membership_role| + if invalid_member?(member_payload) + register_inconsistency(member_payload, "Participante sem email ou matricula") + inconsistencies += 1 + next + end + + user, created = find_or_create_user(member_payload, course_class.department) + created_users += 1 if created + send_password_setup(user) if created && user.pending_password_setup? + updated_memberships += 1 if ensure_membership(user, course_class, membership_role) + end + end + + Result.new(created_users:, updated_memberships:, inconsistencies:) + end + + private + + attr_reader :path, :imported_by + + def parsed_data + JSON.parse(File.read(path)) + end + + def find_or_create_course_class(class_payload) + code = class_payload.fetch("code") + department = department_for(code) + + CourseClass.find_or_create_by!( + code:, + class_code: class_payload.fetch("classCode"), + semester: class_payload.fetch("semester") + ) do |course_class| + course_class.department = department + course_class.name = class_payload["name"] + end + end + + def department_for(code) + department_code = imported_by&.department&.code || code.to_s[/\A[A-Za-z]+/]&.upcase || "GERAL" + Department.find_or_create_by!(code: department_code) { |department| department.name = department_code } + end + + def members_for(class_payload) + class_payload.flat_map do |key, value| + next [] unless value.is_a?(Array) + + role = key == "docente" ? :docente : :discente + value.map { |member_payload| [ member_payload, role ] } + end + end + + def invalid_member?(member_payload) + member_payload["email"].blank? || member_payload["matricula"].blank? + end + + def register_inconsistency(member_payload, message) + ImportInconsistency.create!(source: "class_members.json", message:, payload: member_payload) + end + + def find_or_create_user(member_payload, department) + user = User.find_by(email: normalized_email(member_payload)) || + User.find_by(registration: member_payload["matricula"].to_s.strip) + + if user + user.update!( + name: member_payload["nome"], + email: normalized_email(member_payload), + registration: member_payload["matricula"].to_s.strip, + department: + ) + [ user, false ] + else + [ + User.create!( + name: member_payload["nome"], + email: normalized_email(member_payload), + registration: member_payload["matricula"].to_s.strip, + role: :participant, + department: + ), + true + ] + end + end + + def normalized_email(member_payload) + member_payload["email"].to_s.strip.downcase + end + + def send_password_setup(user) + UserMailer.password_setup(user, user.generate_password_setup_token!).deliver_now + end + + def ensure_membership(user, course_class, role) + ClassMembership.find_or_create_by!(user:, course_class:, role:).previously_new_record? + end + end +end diff --git a/project/app/views/dashboard/show.html.erb b/project/app/views/dashboard/show.html.erb new file mode 100644 index 0000000000..9a164b8c6b --- /dev/null +++ b/project/app/views/dashboard/show.html.erb @@ -0,0 +1,20 @@ +

CAMAAR

+ +<%= render "shared/flash" %> + +
+ +

Usuario autenticado: <%= current_user.name %>

diff --git a/project/app/views/password_resets/edit.html.erb b/project/app/views/password_resets/edit.html.erb new file mode 100644 index 0000000000..1f5eb36ee3 --- /dev/null +++ b/project/app/views/password_resets/edit.html.erb @@ -0,0 +1,17 @@ +

Redefinir senha

+ +<%= render "shared/flash" %> + +<%= form_with model: @user, url: password_reset_path(@token), method: :patch do |form| %> +
+ <%= form.label :password, "Nova senha" %> + <%= form.password_field :password %> +
+ +
+ <%= form.label :password_confirmation, "Confirmacao de senha" %> + <%= form.password_field :password_confirmation %> +
+ + <%= form.submit "Redefinir senha" %> +<% end %> diff --git a/project/app/views/password_resets/invalid.html.erb b/project/app/views/password_resets/invalid.html.erb new file mode 100644 index 0000000000..33312e48cc --- /dev/null +++ b/project/app/views/password_resets/invalid.html.erb @@ -0,0 +1,3 @@ +

Link invalido

+ +<%= render "shared/flash" %> diff --git a/project/app/views/password_resets/new.html.erb b/project/app/views/password_resets/new.html.erb new file mode 100644 index 0000000000..74fff9e155 --- /dev/null +++ b/project/app/views/password_resets/new.html.erb @@ -0,0 +1,12 @@ +

Esqueci minha senha

+ +<%= render "shared/flash" %> + +<%= form_with scope: :password_reset, url: password_resets_path do |form| %> +
+ <%= form.label :email, "Email" %> + <%= form.email_field :email %> +
+ + <%= form.submit "Solicitar redefinicao de senha" %> +<% end %> diff --git a/project/app/views/password_setups/edit.html.erb b/project/app/views/password_setups/edit.html.erb new file mode 100644 index 0000000000..6136e7c676 --- /dev/null +++ b/project/app/views/password_setups/edit.html.erb @@ -0,0 +1,17 @@ +

Definir senha

+ +<%= render "shared/flash" %> + +<%= form_with model: @user, url: password_setup_path(@token), method: :patch do |form| %> +
+ <%= form.label :password, "Nova senha" %> + <%= form.password_field :password %> +
+ +
+ <%= form.label :password_confirmation, "Confirmacao de senha" %> + <%= form.password_field :password_confirmation %> +
+ + <%= form.submit "Definir senha" %> +<% end %> diff --git a/project/app/views/password_setups/invalid.html.erb b/project/app/views/password_setups/invalid.html.erb new file mode 100644 index 0000000000..33312e48cc --- /dev/null +++ b/project/app/views/password_setups/invalid.html.erb @@ -0,0 +1,3 @@ +

Link invalido

+ +<%= render "shared/flash" %> diff --git a/project/app/views/sessions/new.html.erb b/project/app/views/sessions/new.html.erb new file mode 100644 index 0000000000..9d877e010f --- /dev/null +++ b/project/app/views/sessions/new.html.erb @@ -0,0 +1,19 @@ +

Login

+ +<%= render "shared/flash" %> + +<%= form_with scope: :session, url: login_path do |form| %> +
+ <%= form.label :identifier, "Email ou matricula" %> + <%= form.text_field :identifier %> +
+ +
+ <%= form.label :password, "Senha" %> + <%= form.password_field :password %> +
+ + <%= form.submit "Entrar" %> +<% end %> + +<%= link_to "Esqueci minha senha", new_password_reset_path %> diff --git a/project/app/views/shared/_flash.html.erb b/project/app/views/shared/_flash.html.erb new file mode 100644 index 0000000000..4b6a0a8f37 --- /dev/null +++ b/project/app/views/shared/_flash.html.erb @@ -0,0 +1,3 @@ +<% flash.each do |type, message| %> +

<%= message %>

+<% end %> diff --git a/project/app/views/user_mailer/password_reset.html.erb b/project/app/views/user_mailer/password_reset.html.erb new file mode 100644 index 0000000000..e72841469d --- /dev/null +++ b/project/app/views/user_mailer/password_reset.html.erb @@ -0,0 +1,5 @@ +

Ola, <%= @user.name %>.

+ +

Use o link abaixo para redefinir sua senha no CAMAAR.

+ +

<%= link_to "Redefinir senha", @url %>

diff --git a/project/app/views/user_mailer/password_reset.text.erb b/project/app/views/user_mailer/password_reset.text.erb new file mode 100644 index 0000000000..c5d1f99044 --- /dev/null +++ b/project/app/views/user_mailer/password_reset.text.erb @@ -0,0 +1,5 @@ +Ola, <%= @user.name %>. + +Use o link abaixo para redefinir sua senha no CAMAAR. + +<%= @url %> diff --git a/project/app/views/user_mailer/password_setup.html.erb b/project/app/views/user_mailer/password_setup.html.erb new file mode 100644 index 0000000000..cc45bf0a47 --- /dev/null +++ b/project/app/views/user_mailer/password_setup.html.erb @@ -0,0 +1,5 @@ +

Ola, <%= @user.name %>.

+ +

Use o link abaixo para definir sua primeira senha no CAMAAR.

+ +

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

diff --git a/project/app/views/user_mailer/password_setup.text.erb b/project/app/views/user_mailer/password_setup.text.erb new file mode 100644 index 0000000000..5db9bce1f9 --- /dev/null +++ b/project/app/views/user_mailer/password_setup.text.erb @@ -0,0 +1,5 @@ +Ola, <%= @user.name %>. + +Use o link abaixo para definir sua primeira senha no CAMAAR. + +<%= @url %> diff --git a/project/config/environments/development.rb b/project/config/environments/development.rb index 7d1b179ef2..4a6ea19c9c 100644 --- a/project/config/environments/development.rb +++ b/project/config/environments/development.rb @@ -2,8 +2,8 @@ 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*(.*)$/ } + 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. diff --git a/project/config/environments/test.rb b/project/config/environments/test.rb index e6b5c1b020..29d195b837 100644 --- a/project/config/environments/test.rb +++ b/project/config/environments/test.rb @@ -5,8 +5,8 @@ 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*(.*)$/ } + 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. diff --git a/project/config/routes.rb b/project/config/routes.rb index 3838196427..b157ef7efd 100644 --- a/project/config/routes.rb +++ b/project/config/routes.rb @@ -25,6 +25,21 @@ # 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 "dashboard#show" + + get "login", to: "sessions#new" + post "login", to: "sessions#create" + delete "logout", to: "sessions#destroy" + + get "senha/definir/:token", to: "password_setups#edit", as: :edit_password_setup + patch "senha/definir/:token", to: "password_setups#update", as: :password_setup + + get "senha/esqueci", to: "password_resets#new", as: :new_password_reset + post "senha/esqueci", to: "password_resets#create", as: :password_resets + get "senha/redefinir/:token", to: "password_resets#edit", as: :edit_password_reset + patch "senha/redefinir/:token", to: "password_resets#update", as: :password_reset + + namespace :imports do + post "class_members", to: "class_members#create" + end end diff --git a/project/db/migrate/20260615000100_create_departments.rb b/project/db/migrate/20260615000100_create_departments.rb new file mode 100644 index 0000000000..98ffe30b3a --- /dev/null +++ b/project/db/migrate/20260615000100_create_departments.rb @@ -0,0 +1,12 @@ +class CreateDepartments < ActiveRecord::Migration[8.1] + def change + create_table :departments do |t| + t.string :name, null: false + t.string :code, null: false + + t.timestamps + end + + add_index :departments, :code, unique: true + end +end diff --git a/project/db/migrate/20260615000200_create_course_classes.rb b/project/db/migrate/20260615000200_create_course_classes.rb new file mode 100644 index 0000000000..a5eebe34d7 --- /dev/null +++ b/project/db/migrate/20260615000200_create_course_classes.rb @@ -0,0 +1,15 @@ +class CreateCourseClasses < ActiveRecord::Migration[8.1] + def change + create_table :course_classes do |t| + t.references :department, null: false, foreign_key: true + t.string :code, null: false + t.string :name + t.string :class_code, null: false + t.string :semester, null: false + + t.timestamps + end + + add_index :course_classes, [ :code, :class_code, :semester ], unique: true + end +end diff --git a/project/db/migrate/20260615000300_create_users.rb b/project/db/migrate/20260615000300_create_users.rb new file mode 100644 index 0000000000..2f77e553db --- /dev/null +++ b/project/db/migrate/20260615000300_create_users.rb @@ -0,0 +1,23 @@ +class CreateUsers < ActiveRecord::Migration[8.1] + def change + create_table :users do |t| + t.references :department, foreign_key: true + t.string :name, null: false + t.string :email, null: false + t.string :registration, null: false + t.integer :role, null: false, default: 1 + t.string :password_digest + t.string :password_setup_token_digest + t.datetime :password_setup_sent_at + t.string :password_reset_token_digest + t.datetime :password_reset_sent_at + + t.timestamps + end + + add_index :users, :email, unique: true + add_index :users, :registration, unique: true + add_index :users, :password_setup_token_digest, unique: true + add_index :users, :password_reset_token_digest, unique: true + end +end diff --git a/project/db/migrate/20260615000400_create_class_memberships.rb b/project/db/migrate/20260615000400_create_class_memberships.rb new file mode 100644 index 0000000000..aadc8a7cd2 --- /dev/null +++ b/project/db/migrate/20260615000400_create_class_memberships.rb @@ -0,0 +1,13 @@ +class CreateClassMemberships < ActiveRecord::Migration[8.1] + def change + create_table :class_memberships do |t| + t.references :user, null: false, foreign_key: true + t.references :course_class, null: false, foreign_key: true + t.integer :role, null: false, default: 0 + + t.timestamps + end + + add_index :class_memberships, [ :user_id, :course_class_id, :role ], unique: true + end +end diff --git a/project/db/migrate/20260615000500_create_import_inconsistencies.rb b/project/db/migrate/20260615000500_create_import_inconsistencies.rb new file mode 100644 index 0000000000..821c18b1ad --- /dev/null +++ b/project/db/migrate/20260615000500_create_import_inconsistencies.rb @@ -0,0 +1,11 @@ +class CreateImportInconsistencies < ActiveRecord::Migration[8.1] + def change + create_table :import_inconsistencies do |t| + t.string :source, null: false + t.string :message, null: false + t.json :payload + + t.timestamps + end + end +end diff --git a/project/db/schema.rb b/project/db/schema.rb index 799156ff7f..295550474d 100644 --- a/project/db/schema.rb +++ b/project/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_14_234457) do +ActiveRecord::Schema[8.1].define(version: 2026_06_15_000500) do create_table "admins", force: :cascade do |t| t.datetime "created_at", null: false t.integer "departamento_id", null: false @@ -32,6 +32,29 @@ t.datetime "updated_at", null: false end + create_table "class_memberships", force: :cascade do |t| + t.integer "course_class_id", null: false + t.datetime "created_at", null: false + t.integer "role", default: 0, null: false + t.datetime "updated_at", null: false + t.integer "user_id", null: false + t.index ["course_class_id"], name: "index_class_memberships_on_course_class_id" + t.index ["user_id", "course_class_id", "role"], name: "idx_on_user_id_course_class_id_role_c0460d53c1", unique: true + t.index ["user_id"], name: "index_class_memberships_on_user_id" + end + + create_table "course_classes", force: :cascade do |t| + t.string "class_code", null: false + t.string "code", null: false + t.datetime "created_at", null: false + t.integer "department_id", null: false + t.string "name" + t.string "semester", null: false + t.datetime "updated_at", null: false + t.index ["code", "class_code", "semester"], name: "index_course_classes_on_code_and_class_code_and_semester", unique: true + t.index ["department_id"], name: "index_course_classes_on_department_id" + end + create_table "departamentos", force: :cascade do |t| t.string "code" t.datetime "created_at", null: false @@ -39,6 +62,14 @@ t.datetime "updated_at", null: false end + create_table "departments", force: :cascade do |t| + t.string "code", null: false + t.datetime "created_at", null: false + t.string "name", null: false + t.datetime "updated_at", null: false + t.index ["code"], name: "index_departments_on_code", unique: true + end + create_table "formularios", force: :cascade do |t| t.integer "admin_id", null: false t.datetime "created_at", null: false @@ -53,6 +84,14 @@ t.index ["turma_id"], name: "index_formularios_on_turma_id" end + create_table "import_inconsistencies", force: :cascade do |t| + t.datetime "created_at", null: false + t.string "message", null: false + t.json "payload" + t.string "source", null: false + t.datetime "updated_at", null: false + end + create_table "professors", force: :cascade do |t| t.datetime "created_at", null: false t.integer "departamento_id", null: false @@ -129,7 +168,30 @@ t.index ["professor_id"], name: "index_turmas_on_professor_id" end + create_table "users", force: :cascade do |t| + t.datetime "created_at", null: false + t.integer "department_id" + t.string "email", null: false + t.string "name", null: false + t.string "password_digest" + t.datetime "password_reset_sent_at" + t.string "password_reset_token_digest" + t.datetime "password_setup_sent_at" + t.string "password_setup_token_digest" + t.string "registration", null: false + t.integer "role", default: 1, null: false + t.datetime "updated_at", null: false + t.index ["department_id"], name: "index_users_on_department_id" + t.index ["email"], name: "index_users_on_email", unique: true + t.index ["password_reset_token_digest"], name: "index_users_on_password_reset_token_digest", unique: true + t.index ["password_setup_token_digest"], name: "index_users_on_password_setup_token_digest", unique: true + t.index ["registration"], name: "index_users_on_registration", unique: true + end + add_foreign_key "admins", "departamentos" + add_foreign_key "class_memberships", "course_classes" + add_foreign_key "class_memberships", "users" + add_foreign_key "course_classes", "departments" add_foreign_key "formularios", "admins" add_foreign_key "formularios", "templates" add_foreign_key "formularios", "turmas" @@ -144,4 +206,5 @@ add_foreign_key "turma_alunos", "turmas" add_foreign_key "turmas", "departamentos" add_foreign_key "turmas", "professors" + add_foreign_key "users", "departments" end diff --git a/project/features/step_definitions/auth_steps.rb b/project/features/step_definitions/auth_steps.rb new file mode 100644 index 0000000000..c09d2a7f4c --- /dev/null +++ b/project/features/step_definitions/auth_steps.rb @@ -0,0 +1,319 @@ +Before("@auth") do + ActionMailer::Base.deliveries.clear +end + +After("@auth") do + File.delete(@temporary_import_path) if @temporary_import_path.present? && File.exist?(@temporary_import_path) +end + +Dado("que existe um administrador autenticado do departamento {string}") do |department_code| + department = Department.find_or_create_by!(code: department_code) { |record| record.name = department_code } + @current_admin = User.create!( + name: "Administrador #{department_code}", + email: "admin.#{department_code.downcase}@unb.br", + registration: "000000001", + role: :admin, + department:, + password: "Senha@123" + ) +end + +Dado("que o arquivo {string} contem participantes da turma {string} no semestre {string}") do |file_name, class_code, semester| + @import_path = Rails.root.join("..", file_name) + expect(JSON.parse(File.read(@import_path))).to include(a_hash_including("code" => class_code, "semester" => semester)) +end + +Quando("o administrador importa os participantes do SIGAA") do + @import_result = Sigaa::ClassMembersImporter.new(path: @import_path, imported_by: @current_admin).call +end + +Quando("o administrador importa novamente os participantes do SIGAA") do + step "o administrador importa os participantes do SIGAA" +end + +Entao("o sistema deve cadastrar o usuario {string} com email {string} e matricula {string}") do |name, email, registration| + @last_user = User.find_by!(email:, registration:) + expect(@last_user.name).to eq(name) +end + +Entao("deve associar o usuario a turma {string} como participante discente") do |class_code| + expect(@last_user.course_classes.where(code: class_code)).to exist + expect(@last_user.class_memberships.discente).to exist +end + +Entao("deve marcar o usuario como pendente de definicao de senha") do + expect(@last_user).to be_pending_password_setup +end + +Entao("deve enviar email com link de definicao de senha para {string}") do |email| + message = ActionMailer::Base.deliveries.find { |delivery| delivery.to.include?(email) } + expect(message).to be_present + expect(message.body.encoded).to include("senha/definir") +end + +Dado("que ja existe um usuario cadastrado com email {string} e matricula {string}") do |email, registration| + User.create!( + name: "Usuario Existente", + email:, + registration:, + role: :participant, + department: Department.find_or_create_by!(code: "CIC") { |record| record.name = "CIC" }, + password: "Senha@123" + ) +end + +Entao("o sistema nao deve criar outro usuario com a matricula {string}") do |registration| + expect(User.where(registration:).count).to eq(1) +end + +Entao("deve manter uma unica conta para o email {string}") do |email| + expect(User.where(email:).count).to eq(1) +end + +Entao("deve atualizar os vinculos de turma quando necessario") do + expect(User.find_by!(email: "acjpjvjp@gmail.com").course_classes).not_to be_empty +end + +Dado("que o arquivo de importacao contem um participante sem email ou matricula") do + @temporary_import_path = Rails.root.join("tmp", "cucumber_class_members.json") + @import_path = @temporary_import_path + payload = [ + { + "code" => "CIC0097", + "classCode" => "TA", + "semester" => "2021.2", + "dicente" => [ + { "nome" => "Participante Invalido", "matricula" => "", "email" => "" }, + { "nome" => "Ana Clara Jordao Perna", "matricula" => "190084006", "email" => "acjpjvjp@gmail.com" } + ] + } + ] + File.write(@temporary_import_path, JSON.pretty_generate(payload)) +end + +Entao("o sistema nao deve criar conta para o participante invalido") do + expect(User.where(name: "Participante Invalido")).not_to exist +end + +Entao("deve registrar a inconsistencia encontrada na importacao") do + expect(ImportInconsistency.where(message: "Participante sem email ou matricula")).to exist +end + +Entao("deve continuar processando os demais participantes validos") do + expect(User.where(registration: "190084006")).to exist +end + +Dado("que existe um administrador cadastrado com email {string}, matricula {string} e senha definida {string}") do |email, registration, password| + User.create!( + name: "Administrador CIC", + email:, + registration:, + role: :admin, + department: Department.find_or_create_by!(code: "CIC") { |record| record.name = "CIC" }, + password: + ) +end + +Dado("que existe um participante cadastrado com email {string}, matricula {string} e senha definida {string}") do |email, registration, password| + @user = User.create!( + name: "Participante", + email:, + registration:, + role: :participant, + department: Department.find_or_create_by!(code: "CIC") { |record| record.name = "CIC" }, + password: + ) +end + +Dado("que existe um participante cadastrado com email {string}, matricula {string} e sem senha definida") do |email, registration| + User.create!( + name: "Participante sem senha", + email:, + registration:, + role: :participant, + department: Department.find_or_create_by!(code: "CIC") { |record| record.name = "CIC" } + ) +end + +Quando("o usuario acessa a pagina de login") do + visit login_path +end + +Quando("informa o identificador {string}") do |identifier| + fill_in "Email ou matricula", with: identifier +end + +Quando("informa a senha {string}") do |password| + fill_in "Senha", with: password +end + +Quando("envia o formulario de login") do + click_button "Entrar" +end + +Entao("o sistema deve autenticar o administrador") do + expect(page).to have_content("Login realizado com sucesso") + expect(page).to have_content("Administrador CIC") +end + +Entao("deve exibir o menu lateral com opcoes de gerenciamento") do + expect(page).to have_content("Gerenciar templates") + expect(page).to have_content("Gerenciar formularios") +end + +Entao("o sistema deve autenticar o participante") do + expect(page).to have_content("Login realizado com sucesso") +end + +Entao("deve exibir o menu lateral com formularios pendentes") do + expect(page).to have_content("Formularios pendentes") +end + +Entao("nao deve exibir opcoes administrativas") do + expect(page).not_to have_content("Gerenciar templates") +end + +Entao("o sistema nao deve autenticar o usuario") do + expect(page).not_to have_content("Login realizado com sucesso") +end + +Entao("deve exibir a mensagem {string}") do |message| + expect(page).to have_content(message) +end + +Entao("deve informar que a senha inicial precisa ser definida antes do acesso") do + expect(page).to have_content("Senha inicial precisa ser definida antes do acesso") +end + +Dado("que existe um participante importado do SIGAA com email {string}, matricula {string} e sem senha definida") do |email, registration| + @user = User.create!( + name: "Ana Clara Jordao Perna", + email:, + registration:, + role: :participant, + department: Department.find_or_create_by!(code: "CIC") { |record| record.name = "CIC" } + ) +end + +Dado("que o sistema enviou um link valido de definicao de senha para {string}") do |email| + user = User.find_by!(email:) + @setup_token = user.generate_password_setup_token! + UserMailer.password_setup(user, @setup_token).deliver_now +end + +Quando("o usuario acessa o link valido de definicao de senha") do + @old_password_digest = @user.reload.password_digest + visit edit_password_setup_path(@setup_token) +end + +Quando("informa a nova senha {string}") do |password| + @new_password = password + fill_in "Nova senha", with: password if page.has_field?("Nova senha") +end + +Quando("confirma a nova senha {string}") do |password| + @new_password_confirmation = password + fill_in "Confirmacao de senha", with: password if page.has_field?("Confirmacao de senha") +end + +Quando("envia o formulario de definicao de senha") do + click_button "Definir senha" +end + +Entao("o sistema deve salvar a senha do usuario") do + expect(@user.reload.password_digest).to be_present + expect(@user.password_digest).not_to eq(@old_password_digest) +end + +Entao("deve ativar o acesso do usuario ao CAMAAR") do + expect(@user.reload).not_to be_pending_password_setup +end + +Entao("deve permitir login com email ou matricula e a nova senha") do + visit login_path + fill_in "Email ou matricula", with: @user.email + fill_in "Senha", with: "Senha@123" + click_button "Entrar" + expect(page).to have_content("Login realizado com sucesso") +end + +Quando("o usuario acessa um link invalido de definicao de senha") do + visit edit_password_setup_path("token-invalido") +end + +Entao("o sistema nao deve permitir a definicao de senha") do + expect(page).to have_content("Link de definicao de senha invalido ou expirado") +end + +Entao("o sistema nao deve salvar a senha do usuario") do + expect(@user.reload.password_digest).to eq(@old_password_digest) +end + +Quando("o usuario acessa a pagina de esqueci minha senha") do + visit new_password_reset_path +end + +Quando("informa o email {string}") do |email| + fill_in "Email", with: email +end + +Quando("solicita a redefinicao de senha") do + click_button "Solicitar redefinicao de senha" +end + +Entao("o sistema deve enviar um email com link de redefinicao para {string}") do |email| + @reset_email = ActionMailer::Base.deliveries.find { |delivery| delivery.to.include?(email) } + expect(@reset_email).to be_present + expect(@reset_email.body.encoded).to include("senha/redefinir") +end + +Quando("o usuario acessa o link valido de redefinicao de senha") do + @old_password_digest = @user.reload.password_digest + @reset_token = @reset_email.body.encoded.match(%r{senha/redefinir/([^"\s<]+)})[1] + visit edit_password_reset_path(@reset_token) +end + +Quando("envia o formulario de redefinicao de senha") do + click_button "Redefinir senha" if page.has_button?("Redefinir senha") +end + +Entao("o sistema deve atualizar a senha do usuario") do + expect(@user.reload.password_digest).to be_present + expect(@user.password_digest).not_to eq(@old_password_digest) +end + +Entao("deve permitir login com a nova senha") do + visit login_path + fill_in "Email ou matricula", with: @user.email + fill_in "Senha", with: "NovaSenha@123" + click_button "Entrar" + expect(page).to have_content("Login realizado com sucesso") +end + +Entao("o sistema deve exibir uma mensagem generica de instrucao") do + expect(page).to have_content(PasswordResetsController::GENERIC_MESSAGE) +end + +Entao("nao deve revelar se o email esta cadastrado") do + expect(page).not_to have_content("nao cadastrado") + expect(page).not_to have_content("inexistente") +end + +Entao("nao deve criar token de redefinicao para usuario inexistente") do + expect(User.where.not(password_reset_token_digest: nil)).not_to exist +end + +Dado("que existe um link expirado de redefinicao de senha para {string}") do |email| + @user = User.find_by!(email:) + @old_password_digest = @user.password_digest + @reset_token = @user.generate_password_reset_token! + @user.update!(password_reset_sent_at: 3.hours.ago) +end + +Quando("o usuario acessa o link expirado de redefinicao de senha") do + visit edit_password_reset_path(@reset_token) +end + +Entao("o sistema nao deve atualizar a senha do usuario") do + expect(@user.reload.password_digest).to eq(@old_password_digest) +end diff --git a/project/spec/mailers/user_mailer_spec.rb b/project/spec/mailers/user_mailer_spec.rb new file mode 100644 index 0000000000..1049e41719 --- /dev/null +++ b/project/spec/mailers/user_mailer_spec.rb @@ -0,0 +1,21 @@ +require "rails_helper" + +RSpec.describe UserMailer do + it "envia link de definicao de senha" do + user = create_user(email: "acjpjvjp@gmail.com", registration: "190084006") + email = described_class.password_setup(user, "token-123") + + expect(email.to).to include("acjpjvjp@gmail.com") + expect(email.subject).to eq("Defina sua senha no CAMAAR") + expect(email.body.encoded).to include("senha/definir/token-123") + end + + it "envia link de redefinicao de senha" do + user = create_user(email: "acjpjvjp@gmail.com", registration: "190084006", password: "Senha@123") + email = described_class.password_reset(user, "token-123") + + expect(email.to).to include("acjpjvjp@gmail.com") + expect(email.subject).to eq("Redefinicao de senha do CAMAAR") + expect(email.body.encoded).to include("senha/redefinir/token-123") + end +end diff --git a/project/spec/rails_helper.rb b/project/spec/rails_helper.rb index 63a676c97a..acb1445723 100644 --- a/project/spec/rails_helper.rb +++ b/project/spec/rails_helper.rb @@ -1,74 +1,26 @@ -# This file is copied to spec/ when you run 'rails generate rspec:install' -require 'spec_helper' -ENV['RAILS_ENV'] ||= 'test' -require_relative '../config/environment' -# Prevent database truncation if the environment is production +ENV["RAILS_ENV"] ||= "test" +require_relative "../config/environment" abort("The Rails environment is running in production mode!") if Rails.env.production? -# Uncomment the line below in case you have `--require rails_helper` in the `.rspec` file -# that will avoid rails generators crashing because migrations haven't been run yet -# return unless Rails.env.test? -require 'rspec/rails' -# Add additional requires below this line. Rails is not loaded until this point! -# Requires supporting ruby files with custom matchers and macros, etc, in -# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are -# run as spec files by default. This means that files in spec/support that end -# in _spec.rb will both be required and run as specs, causing the specs to be -# run twice. It is recommended that you do not name files matching this glob to -# end with _spec.rb. You can configure this pattern with the --pattern -# option on the command line or in ~/.rspec, .rspec or `.rspec-local`. -# -# The following line is provided for convenience purposes. It has the downside -# of increasing the boot-up time by auto-requiring all files in the support -# directory. Alternatively, in the individual `*_spec.rb` files, manually -# require only the support files necessary. -# -Rails.root.glob('spec/support/**/*.rb').sort_by(&:to_s).each { |f| require f } +require "rspec/rails" + +Rails.root.glob("spec/support/**/*.rb").sort_by(&:to_s).each { |file| require file } -# Ensures that the test database schema matches the current schema file. -# If there are pending migrations it will invoke `db:test:prepare` to -# recreate the test database by loading the schema. -# If you are not using ActiveRecord, you can remove these lines. begin ActiveRecord::Migration.maintain_test_schema! rescue ActiveRecord::PendingMigrationError => e abort e.to_s.strip end -RSpec.configure do |config| - # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures - config.fixture_paths = [ - Rails.root.join('spec/fixtures') - ] - # If you're not using ActiveRecord, or you'd prefer not to run each of your - # examples within a transaction, remove the following line or assign false - # instead of true. +RSpec.configure do |config| + config.fixture_paths = [ Rails.root.join("spec/fixtures") ] config.use_transactional_fixtures = true - - # You can uncomment this line to turn off ActiveRecord support entirely. - # config.use_active_record = false - - # RSpec Rails uses metadata to mix in different behaviours to your tests, - # for example enabling you to call `get` and `post` in request specs. e.g.: - # - # RSpec.describe UsersController, type: :request do - # # ... - # end - # - # The different available types are documented in the features, such as in - # https://rspec.info/features/8-0/rspec-rails - # - # You can also infer these behaviours automatically by location, e.g. - # /spec/models would pull in the same behaviour as `type: :model` but this - # behaviour is considered legacy and will be removed in a future version. - # - # To enable this behaviour uncomment the line below. - # config.infer_spec_type_from_file_location! - - # Filter lines from Rails gems in backtraces. + config.infer_spec_type_from_file_location! config.filter_rails_from_backtrace! - # arbitrary gems may also be filtered via: - # config.filter_gems_from_backtrace("gem name") - + config.include ActiveSupport::Testing::TimeHelpers config.include FactoryBot::Syntax::Methods + + config.before do + ActionMailer::Base.deliveries.clear + end end diff --git a/project/spec/requests/password_resets_spec.rb b/project/spec/requests/password_resets_spec.rb new file mode 100644 index 0000000000..dca4aafc02 --- /dev/null +++ b/project/spec/requests/password_resets_spec.rb @@ -0,0 +1,57 @@ +require "rails_helper" + +RSpec.describe "Redefinicao de senha" do + let!(:user) do + create_user( + name: "Ana Clara Jordao Perna", + email: "acjpjvjp@gmail.com", + registration: "190084006", + password: "Senha@123" + ) + end + + it "redefine a senha com token valido e permite login com a nova senha" do + post password_resets_path, params: { password_reset: { email: "acjpjvjp@gmail.com" } } + + expect(response).to redirect_to(new_password_reset_path) + expect(ActionMailer::Base.deliveries.last.to).to include("acjpjvjp@gmail.com") + + token = extract_token_from(ActionMailer::Base.deliveries.last.body.encoded, "senha/redefinir") + patch password_reset_path(token), + params: { user: { password: "NovaSenha@123", password_confirmation: "NovaSenha@123" } } + + expect(response).to redirect_to(login_path) + expect(user.reload.authenticate("NovaSenha@123")).to eq(user) + + post login_path, params: { session: { identifier: "acjpjvjp@gmail.com", password: "NovaSenha@123" } } + expect(response).to redirect_to(root_path) + end + + it "nao revela se email inexistente esta cadastrado" do + expect do + post password_resets_path, params: { password_reset: { email: "naoexiste@unb.br" } } + end.not_to change { User.where.not(password_reset_token_digest: nil).count } + + expect(response).to redirect_to(new_password_reset_path) + follow_redirect! + expect(response.body).to include(PasswordResetsController::GENERIC_MESSAGE) + expect(ActionMailer::Base.deliveries).to be_empty + end + + it "nao redefine senha com token expirado" do + token = user.generate_password_reset_token! + user.update!(password_reset_sent_at: 3.hours.ago) + + patch password_reset_path(token), + params: { user: { password: "NovaSenha@123", password_confirmation: "NovaSenha@123" } } + + expect(response).to have_http_status(:not_found) + expect(response.body).to include("Link de redefinicao de senha invalido ou expirado") + expect(user.reload.authenticate("Senha@123")).to eq(user) + expect(user.authenticate("NovaSenha@123")).to be(false) + end + + def extract_token_from(body, segment) + body.match(%r{#{segment}/([^"\s<]+)})[1] + end +end diff --git a/project/spec/requests/password_setups_spec.rb b/project/spec/requests/password_setups_spec.rb new file mode 100644 index 0000000000..20f3bad7af --- /dev/null +++ b/project/spec/requests/password_setups_spec.rb @@ -0,0 +1,44 @@ +require "rails_helper" + +RSpec.describe "Definicao de senha" do + let(:user) do + create_user( + name: "Ana Clara Jordao Perna", + email: "acjpjvjp@gmail.com", + registration: "190084006" + ) + end + + it "define a primeira senha com link valido e permite login" do + token = user.generate_password_setup_token! + + patch password_setup_path(token), + params: { user: { password: "Senha@123", password_confirmation: "Senha@123" } } + + expect(response).to redirect_to(login_path) + user.reload + expect(user).not_to be_pending_password_setup + expect(user.authenticate("Senha@123")).to eq(user) + + post login_path, params: { session: { identifier: "190084006", password: "Senha@123" } } + expect(response).to redirect_to(root_path) + end + + it "nao permite definicao com link invalido" do + get edit_password_setup_path("token-invalido") + + expect(response).to have_http_status(:not_found) + expect(response.body).to include("Link de definicao de senha invalido ou expirado") + end + + it "nao salva senha quando a confirmacao e diferente" do + token = user.generate_password_setup_token! + + patch password_setup_path(token), + params: { user: { password: "Senha@123", password_confirmation: "OutraSenha@123" } } + + expect(response).to have_http_status(:unprocessable_content) + expect(response.body).to include("Confirmacao de senha nao confere") + expect(user.reload).to be_pending_password_setup + end +end diff --git a/project/spec/requests/sessions_spec.rb b/project/spec/requests/sessions_spec.rb new file mode 100644 index 0000000000..4574a3f627 --- /dev/null +++ b/project/spec/requests/sessions_spec.rb @@ -0,0 +1,50 @@ +require "rails_helper" + +RSpec.describe "Login" do + before do + create_admin + create_user( + name: "Ana Clara Jordao Perna", + email: "acjpjvjp@gmail.com", + registration: "190084006", + password: "Senha@123" + ) + create_user( + name: "Andre Carvalho de Roure", + email: "andreCarvalhoroure@gmail.com", + registration: "200033522" + ) + end + + it "autentica administrador com email e exibe menu administrativo" do + post login_path, params: { session: { identifier: "admin.cic@unb.br", password: "Senha@123" } } + + expect(response).to redirect_to(root_path) + follow_redirect! + expect(response.body).to include("Gerenciar templates") + expect(response.body).to include("Gerenciar formularios") + end + + it "autentica participante com matricula e exibe menu de participante" do + post login_path, params: { session: { identifier: "190084006", password: "Senha@123" } } + + expect(response).to redirect_to(root_path) + follow_redirect! + expect(response.body).to include("Formularios pendentes") + expect(response.body).not_to include("Gerenciar templates") + end + + it "nao autentica usuario com senha incorreta" do + post login_path, params: { session: { identifier: "acjpjvjp@gmail.com", password: "SenhaErrada" } } + + expect(response).to have_http_status(:unprocessable_content) + expect(response.body).to include("Email, matricula ou senha invalidos") + end + + it "nao autentica usuario ainda sem senha definida" do + post login_path, params: { session: { identifier: "200033522", password: "Senha@123" } } + + expect(response).to have_http_status(:unprocessable_content) + expect(response.body).to include("Senha inicial precisa ser definida antes do acesso") + end +end diff --git a/project/spec/services/class_members_importer_spec.rb b/project/spec/services/class_members_importer_spec.rb new file mode 100644 index 0000000000..3b77969f88 --- /dev/null +++ b/project/spec/services/class_members_importer_spec.rb @@ -0,0 +1,104 @@ +require "rails_helper" + +RSpec.describe Sigaa::ClassMembersImporter do + let!(:admin) { create_admin } + let(:path) { Rails.root.join("tmp", "spec_class_members.json") } + + after do + File.delete(path) if File.exist?(path) + end + + it "cadastra participantes novos, associa turma e envia link de definicao de senha" do + write_import_file([ + { + "code" => "CIC0097", + "classCode" => "TA", + "semester" => "2021.2", + "dicente" => [ + { + "nome" => "Ana Clara Jordao Perna", + "matricula" => "190084006", + "email" => "acjpjvjp@gmail.com" + } + ] + } + ]) + + result = described_class.new(path:, imported_by: admin).call + + user = User.find_by!(registration: "190084006") + expect(result.created_users).to eq(1) + expect(user.name).to eq("Ana Clara Jordao Perna") + expect(user.email).to eq("acjpjvjp@gmail.com") + expect(user).to be_pending_password_setup + expect(user.password_setup_token_digest).to be_present + expect(user.class_memberships.first).to be_discente + expect(user.course_classes.first.code).to eq("CIC0097") + expect(ActionMailer::Base.deliveries.last.to).to include("acjpjvjp@gmail.com") + expect(ActionMailer::Base.deliveries.last.body.encoded).to include("senha/definir") + end + + it "nao duplica usuario ja cadastrado e mantem uma unica conta por matricula e email" do + create_user( + name: "Ana", + email: "acjpjvjp@gmail.com", + registration: "190084006", + password: "Senha@123" + ) + write_import_file([ + { + "code" => "CIC0097", + "classCode" => "TA", + "semester" => "2021.2", + "dicente" => [ + { + "nome" => "Ana Clara Jordao Perna", + "matricula" => "190084006", + "email" => "acjpjvjp@gmail.com" + } + ] + } + ]) + + expect do + described_class.new(path:, imported_by: admin).call + end.not_to change(User, :count) + + expect(User.where(registration: "190084006").count).to eq(1) + expect(User.where(email: "acjpjvjp@gmail.com").count).to eq(1) + expect(User.find_by!(registration: "190084006").course_classes.count).to eq(1) + end + + it "registra inconsistencia e continua processando os participantes validos" do + write_import_file([ + { + "code" => "CIC0097", + "classCode" => "TA", + "semester" => "2021.2", + "dicente" => [ + { + "nome" => "Participante Invalido", + "matricula" => "", + "email" => "" + }, + { + "nome" => "Ana Clara Jordao Perna", + "matricula" => "190084006", + "email" => "acjpjvjp@gmail.com" + } + ] + } + ]) + + result = described_class.new(path:, imported_by: admin).call + + expect(result.inconsistencies).to eq(1) + expect(ImportInconsistency.last.message).to eq("Participante sem email ou matricula") + expect(User.exists?(name: "Participante Invalido")).to be(false) + expect(User.exists?(registration: "190084006")).to be(true) + end + + def write_import_file(payload) + File.write(path, JSON.pretty_generate(payload)) + end +end diff --git a/project/spec/spec_helper.rb b/project/spec/spec_helper.rb index 35de9f09fd..06e09a3093 100644 --- a/project/spec/spec_helper.rb +++ b/project/spec/spec_helper.rb @@ -1,98 +1,11 @@ -# This file was generated by the `rails generate rspec:install` command. Conventionally, all -# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. -# The generated `.rspec` file contains `--require spec_helper` which will cause -# this file to always be loaded, without a need to explicitly require it in any -# files. -# -# Given that it is always loaded, you are encouraged to keep this file as -# light-weight as possible. Requiring heavyweight dependencies from this file -# will add to the boot time of your test suite on EVERY test run, even for an -# individual file that may not need all of that loaded. Instead, consider making -# a separate helper file that requires the additional dependencies and performs -# the additional setup, and require it from the spec files that actually need -# it. -# -# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration RSpec.configure do |config| - # rspec-expectations config goes here. You can use an alternate - # assertion/expectation library such as wrong or the stdlib/minitest - # assertions if you prefer. config.expect_with :rspec do |expectations| - # This option will default to `true` in RSpec 4. It makes the `description` - # and `failure_message` of custom matchers include text for helper methods - # defined using `chain`, e.g.: - # be_bigger_than(2).and_smaller_than(4).description - # # => "be bigger than 2 and smaller than 4" - # ...rather than: - # # => "be bigger than 2" expectations.include_chain_clauses_in_custom_matcher_descriptions = true end - # rspec-mocks config goes here. You can use an alternate test double - # library (such as bogus or mocha) by changing the `mock_with` option here. config.mock_with :rspec do |mocks| - # Prevents you from mocking or stubbing a method that does not exist on - # a real object. This is generally recommended, and will default to - # `true` in RSpec 4. mocks.verify_partial_doubles = true end - # This option will default to `:apply_to_host_groups` in RSpec 4 (and will - # have no way to turn it off -- the option exists only for backwards - # compatibility in RSpec 3). It causes shared context metadata to be - # inherited by the metadata hash of host groups and examples, rather than - # triggering implicit auto-inclusion in groups with matching metadata. config.shared_context_metadata_behavior = :apply_to_host_groups - -# The settings below are suggested to provide a good initial experience -# with RSpec, but feel free to customize to your heart's content. -=begin - # This allows you to limit a spec run to individual examples or groups - # you care about by tagging them with `:focus` metadata. When nothing - # is tagged with `:focus`, all examples get run. RSpec also provides - # aliases for `it`, `describe`, and `context` that include `:focus` - # metadata: `fit`, `fdescribe` and `fcontext`, respectively. - config.filter_run_when_matching :focus - - # Allows RSpec to persist some state between runs in order to support - # the `--only-failures` and `--next-failure` CLI options. We recommend - # you configure your source control system to ignore this file. - config.example_status_persistence_file_path = "spec/examples.txt" - - # Limits the available syntax to the non-monkey patched syntax that is - # recommended. For more details, see: - # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/ - config.disable_monkey_patching! - - # This setting enables warnings. It's recommended, but in some cases may - # be too noisy due to issues in dependencies. - config.warnings = true - - # Many RSpec users commonly either run the entire suite or an individual - # file, and it's useful to allow more verbose output when running an - # individual spec file. - if config.files_to_run.one? - # Use the documentation formatter for detailed output, - # unless a formatter has already been configured - # (e.g. via a command-line flag). - config.default_formatter = "doc" - end - - # Print the 10 slowest examples and example groups at the - # end of the spec run, to help surface which specs are running - # particularly slow. - config.profile_examples = 10 - - # Run specs in random order to surface order dependencies. If you find an - # order dependency and want to debug it, you can fix the order by providing - # the seed, which is printed after each run. - # --seed 1234 - config.order = :random - - # Seed global randomization in this process using the `--seed` CLI option. - # Setting this allows you to use `--seed` to deterministically reproduce - # test failures related to randomization by passing the same `--seed` value - # as the one that triggered the failure. - Kernel.srand config.seed -=end end diff --git a/project/spec/support/auth_helpers.rb b/project/spec/support/auth_helpers.rb new file mode 100644 index 0000000000..2c6e0c8e33 --- /dev/null +++ b/project/spec/support/auth_helpers.rb @@ -0,0 +1,30 @@ +module AuthHelpers + def create_department(code: "CIC") + Department.find_or_create_by!(code:) { |department| department.name = code } + end + + def create_user(name: "Usuario Teste", email: "user@example.com", registration: "123", role: :participant, password: nil) + User.create!( + name:, + email:, + registration:, + role:, + department: create_department, + password: + ) + end + + def create_admin + create_user( + name: "Admin CIC", + email: "admin.cic@unb.br", + registration: "000000001", + role: :admin, + password: "Senha@123" + ) + end +end + +RSpec.configure do |config| + config.include AuthHelpers +end From 35b9f5547d57c4426ed2b314e4aa9cd700d8df6f Mon Sep 17 00:00:00 2001 From: Rafael Dias Ghiorzi Date: Tue, 16 Jun 2026 01:52:14 -0300 Subject: [PATCH 18/23] fix: correcting tests and seed files --- .../app/controllers/application_controller.rb | 15 +- .../app/controllers/formularios_controller.rb | 22 ++- .../app/controllers/templates_controller.rb | 17 +- project/app/models/admin.rb | 2 - project/app/models/formulario.rb | 2 +- project/app/models/template.rb | 2 +- project/app/models/user.rb | 2 + project/app/views/formularios/_form.html.erb | 2 - project/app/views/formularios/index.html.erb | 2 +- project/app/views/formularios/new.html.erb | 2 +- project/app/views/templates/_form.html.erb | 2 - project/app/views/templates/edit.html.erb | 2 +- project/app/views/templates/index.html.erb | 6 +- project/app/views/templates/new.html.erb | 2 +- project/app/views/templates/show.html.erb | 6 +- ...60616000001_change_admin_id_fk_to_users.rb | 9 ++ project/db/schema.rb | 6 +- project/db/seeds.rb | 149 +++++++++++------- .../step_definitions/formularios_steps.rb | 6 +- .../templates_edicao_steps.rb | 9 +- .../step_definitions/templates_steps.rb | 3 +- .../templates_visualizacao_steps.rb | 5 +- project/features/support/test_helpers.rb | 25 +-- project/spec/factories/formularios.rb | 2 +- project/spec/factories/templates.rb | 2 +- project/spec/factories/users.rb | 13 ++ project/spec/requests/formularios_spec.rb | 19 +-- project/spec/requests/templates_spec.rb | 43 ++--- project/spec/support/auth_helpers.rb | 4 + 29 files changed, 221 insertions(+), 160 deletions(-) create mode 100644 project/db/migrate/20260616000001_change_admin_id_fk_to_users.rb create mode 100644 project/spec/factories/users.rb diff --git a/project/app/controllers/application_controller.rb b/project/app/controllers/application_controller.rb index 4f8a11dcd3..4c87d12aef 100644 --- a/project/app/controllers/application_controller.rb +++ b/project/app/controllers/application_controller.rb @@ -9,17 +9,14 @@ class ApplicationController < ActionController::Base private - def current_admin - return @current_admin if defined?(@current_admin) - - session[:admin_id] = params[:admin_id] if params[:admin_id].present? - @current_admin = Admin.find_by(id: session[:admin_id]) || Admin.first - end - def current_user @current_user ||= User.find_by(id: session[:user_id]) if session[:user_id].present? end + def current_admin + current_user&.admin? ? current_user : nil + end + def user_signed_in? current_user.present? end @@ -28,6 +25,10 @@ def require_login redirect_to login_path, alert: "Faca login para continuar" unless user_signed_in? end + def require_admin! + redirect_to login_path, alert: "Acesso restrito a administradores" unless current_admin + end + def require_admin return if user_signed_in? && current_user.admin? diff --git a/project/app/controllers/formularios_controller.rb b/project/app/controllers/formularios_controller.rb index d924c3a56a..1c8e8fd7dc 100644 --- a/project/app/controllers/formularios_controller.rb +++ b/project/app/controllers/formularios_controller.rb @@ -1,10 +1,11 @@ class FormulariosController < ApplicationController + before_action :require_admin! before_action :set_formulario, only: %i[ show edit update destroy exportar_csv ] before_action :set_select_options, only: %i[ new create edit update ] # GET /formularios or /formularios.json def index - @formularios = current_admin ? current_admin.formularios : Formulario.all + @formularios = current_admin.formularios end # GET /formularios/1 or /formularios/1.json @@ -13,7 +14,7 @@ def show # GET /formularios/new def new - @formulario = Formulario.new(admin_id: current_admin&.id) + @formulario = Formulario.new end # GET /formularios/1/edit @@ -61,33 +62,30 @@ def destroy # GET /formularios/1/exportar_csv def exportar_csv require 'csv' - + csv_data = CSV.generate(headers: true) do |csv| csv << ["ID da Pergunta", "Enunciado", "Scores Atribuídos"] - + end - send_data csv_data, - filename: "respostas_formulario_#{@formulario.id}.csv", + send_data csv_data, + filename: "respostas_formulario_#{@formulario.id}.csv", type: "text/csv" end private - # Use callbacks to share common setup or constraints between actions. def set_formulario @formulario = Formulario.find(params.expect(:id)) end - # Only allow a list of trusted parameters through. def formulario_params - attrs = params.expect(formulario: [ :title, :target_role, :status, :template_id, :turma_id, :admin_id ]) - attrs[:admin_id] = current_admin.id if current_admin + attrs = params.expect(formulario: [ :title, :target_role, :status, :template_id, :turma_id ]) + attrs[:admin_id] = current_admin.id attrs end - # Opções disponíveis para os selects de template e turma do formulário. def set_select_options - @templates = current_admin ? current_admin.templates : Template.all + @templates = current_admin.templates @turmas = Turma.all end end diff --git a/project/app/controllers/templates_controller.rb b/project/app/controllers/templates_controller.rb index 7291c738af..a61bd94ed0 100644 --- a/project/app/controllers/templates_controller.rb +++ b/project/app/controllers/templates_controller.rb @@ -1,10 +1,11 @@ class TemplatesController < ApplicationController + before_action :require_admin! before_action :set_template, only: %i[ show edit update destroy ] before_action :authorize_template_access!, only: %i[ show edit update destroy ] # GET /templates or /templates.json def index - @templates = current_admin ? current_admin.templates : Template.all + @templates = current_admin.templates end # GET /templates/1 or /templates/1.json @@ -13,7 +14,7 @@ def show # GET /templates/new def new - @template = Template.new(admin_id: current_admin&.id) + @template = Template.new @questoes_attrs = [] end @@ -83,26 +84,20 @@ def destroy end private - # Use callbacks to share common setup or constraints between actions. def set_template @template = Template.find(params.expect(:id)) end - # Garante que o admin só acesse/edite/delete templates criados por ele. def authorize_template_access! - return if current_admin.nil? || @template.admin_id == current_admin.id - - redirect_to templates_path, alert: "Você não tem acesso a esse template" + redirect_to templates_path, alert: "Você não tem acesso a esse template" unless @template.admin_id == current_admin.id end - # Only allow a list of trusted parameters through. def template_params - attrs = params.expect(template: [ :title, :target_role, :admin_id ]) - attrs[:admin_id] = current_admin.id if current_admin + attrs = params.expect(template: [ :title, :target_role ]) + attrs[:admin_id] = current_admin.id attrs end - # Extrai as questões enviadas via template[questaos][i][enunciado/tipo/id]. def extract_questoes_attrs raw = params.dig(:template, :questaos) return [] unless raw diff --git a/project/app/models/admin.rb b/project/app/models/admin.rb index e1038dc686..0d89d934ca 100644 --- a/project/app/models/admin.rb +++ b/project/app/models/admin.rb @@ -1,5 +1,3 @@ class Admin < ApplicationRecord belongs_to :departamento - has_many :templates, dependent: :destroy - has_many :formularios, dependent: :destroy end diff --git a/project/app/models/formulario.rb b/project/app/models/formulario.rb index ea1c330e71..03750ad1ba 100644 --- a/project/app/models/formulario.rb +++ b/project/app/models/formulario.rb @@ -3,7 +3,7 @@ class Formulario < ApplicationRecord # de origem; o formulário e suas questões clonadas permanecem intactos. belongs_to :template, optional: true belongs_to :turma, optional: true - belongs_to :admin + belongs_to :admin, class_name: "User" has_many :questaos, dependent: :destroy validates :title, presence: { message: "O nome do formulário é obrigatório" } diff --git a/project/app/models/template.rb b/project/app/models/template.rb index 00b7e595da..1a0a6c2a8e 100644 --- a/project/app/models/template.rb +++ b/project/app/models/template.rb @@ -1,5 +1,5 @@ class Template < ApplicationRecord - belongs_to :admin + belongs_to :admin, class_name: "User" has_many :questaos, dependent: :destroy has_many :formularios, dependent: :nullify diff --git a/project/app/models/user.rb b/project/app/models/user.rb index 0297022e07..5f1c6d0e7e 100644 --- a/project/app/models/user.rb +++ b/project/app/models/user.rb @@ -5,6 +5,8 @@ class User < ApplicationRecord belongs_to :department, optional: true has_many :class_memberships, dependent: :destroy has_many :course_classes, through: :class_memberships + has_many :templates, foreign_key: :admin_id, dependent: :destroy + has_many :formularios, foreign_key: :admin_id, dependent: :destroy has_secure_password validations: false diff --git a/project/app/views/formularios/_form.html.erb b/project/app/views/formularios/_form.html.erb index 7bddf2eefb..883f0a2bb1 100644 --- a/project/app/views/formularios/_form.html.erb +++ b/project/app/views/formularios/_form.html.erb @@ -11,8 +11,6 @@
<% end %> - <%= hidden_field_tag :admin_id, current_admin&.id %> -
<%= form.label :title, "Nome do formulário:" %> <%= form.text_field :title %> diff --git a/project/app/views/formularios/index.html.erb b/project/app/views/formularios/index.html.erb index 00b5cd535e..2eae679670 100644 --- a/project/app/views/formularios/index.html.erb +++ b/project/app/views/formularios/index.html.erb @@ -14,4 +14,4 @@ <% end %>
-<%= link_to "Novo formulário", new_formulario_path(admin_id: current_admin&.id) %> +<%= link_to "Novo formulário", new_formulario_path %> diff --git a/project/app/views/formularios/new.html.erb b/project/app/views/formularios/new.html.erb index 83b49726ad..5f3d969fa3 100644 --- a/project/app/views/formularios/new.html.erb +++ b/project/app/views/formularios/new.html.erb @@ -7,5 +7,5 @@
- <%= link_to "Voltar para formulários", formularios_path(admin_id: current_admin&.id) %> + <%= link_to "Voltar para formulários", formularios_path %>
diff --git a/project/app/views/templates/_form.html.erb b/project/app/views/templates/_form.html.erb index 3e8392d222..8bce670a00 100644 --- a/project/app/views/templates/_form.html.erb +++ b/project/app/views/templates/_form.html.erb @@ -11,8 +11,6 @@
<% end %> - <%= hidden_field_tag :admin_id, current_admin&.id %> -
<%= form.label :title, "Nome do template:" %> <%= form.text_field :title %> diff --git a/project/app/views/templates/edit.html.erb b/project/app/views/templates/edit.html.erb index 3bc0acfae8..2ae4c24299 100644 --- a/project/app/views/templates/edit.html.erb +++ b/project/app/views/templates/edit.html.erb @@ -7,5 +7,5 @@
- <%= link_to "Voltar para templates", templates_path(admin_id: current_admin&.id) %> + <%= link_to "Voltar para templates", templates_path %>
diff --git a/project/app/views/templates/index.html.erb b/project/app/views/templates/index.html.erb index 918bd73903..88c379f5b2 100644 --- a/project/app/views/templates/index.html.erb +++ b/project/app/views/templates/index.html.erb @@ -13,11 +13,11 @@

Questões: <%= template.questaos.count %>

- <%= link_to "Editar", edit_template_path(template, admin_id: current_admin&.id) %> - <%= link_to "Deletar", template_path(template, admin_id: current_admin&.id) %> + <%= link_to "Editar", edit_template_path(template) %> + <%= link_to "Deletar", template_path(template) %>
<% end %>
-<%= link_to "Novo template", new_template_path(admin_id: current_admin&.id) %> +<%= link_to "Novo template", new_template_path %> diff --git a/project/app/views/templates/new.html.erb b/project/app/views/templates/new.html.erb index 85eea3d860..3f2e1c3582 100644 --- a/project/app/views/templates/new.html.erb +++ b/project/app/views/templates/new.html.erb @@ -7,5 +7,5 @@
- <%= link_to "Voltar para templates", templates_path(admin_id: current_admin&.id) %> + <%= link_to "Voltar para templates", templates_path %>
diff --git a/project/app/views/templates/show.html.erb b/project/app/views/templates/show.html.erb index 801ea8ab28..ad72e3ff33 100644 --- a/project/app/views/templates/show.html.erb +++ b/project/app/views/templates/show.html.erb @@ -16,14 +16,14 @@
- <%= link_to "Editar", edit_template_path(@template, admin_id: current_admin&.id) %> + <%= link_to "Editar", edit_template_path(@template) %>

Tem certeza que deseja deletar este template?

- <%= button_to "Confirmar", template_path(@template, admin_id: current_admin&.id), method: :delete %> + <%= button_to "Confirmar", template_path(@template), method: :delete %>
- <%= link_to "Voltar para templates", templates_path(admin_id: current_admin&.id) %> + <%= link_to "Voltar para templates", templates_path %>
diff --git a/project/db/migrate/20260616000001_change_admin_id_fk_to_users.rb b/project/db/migrate/20260616000001_change_admin_id_fk_to_users.rb new file mode 100644 index 0000000000..464c411bda --- /dev/null +++ b/project/db/migrate/20260616000001_change_admin_id_fk_to_users.rb @@ -0,0 +1,9 @@ +class ChangeAdminIdFkToUsers < ActiveRecord::Migration[8.1] + def change + remove_foreign_key :templates, :admins + remove_foreign_key :formularios, :admins + + add_foreign_key :templates, :users, column: :admin_id + add_foreign_key :formularios, :users, column: :admin_id + end +end diff --git a/project/db/schema.rb b/project/db/schema.rb index 295550474d..111f1d6ea3 100644 --- a/project/db/schema.rb +++ b/project/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_15_000500) do +ActiveRecord::Schema[8.1].define(version: 2026_06_16_000001) do create_table "admins", force: :cascade do |t| t.datetime "created_at", null: false t.integer "departamento_id", null: false @@ -192,16 +192,16 @@ add_foreign_key "class_memberships", "course_classes" add_foreign_key "class_memberships", "users" add_foreign_key "course_classes", "departments" - add_foreign_key "formularios", "admins" add_foreign_key "formularios", "templates" add_foreign_key "formularios", "turmas" + add_foreign_key "formularios", "users", column: "admin_id" add_foreign_key "professors", "departamentos" add_foreign_key "questaos", "formularios" add_foreign_key "questaos", "templates" add_foreign_key "resposta", "questaos" add_foreign_key "resposta", "submissaos" add_foreign_key "submissaos", "formularios" - add_foreign_key "templates", "admins" + add_foreign_key "templates", "users", column: "admin_id" add_foreign_key "turma_alunos", "alunos" add_foreign_key "turma_alunos", "turmas" add_foreign_key "turmas", "departamentos" diff --git a/project/db/seeds.rb b/project/db/seeds.rb index 0794ba0808..38b35f1db0 100644 --- a/project/db/seeds.rb +++ b/project/db/seeds.rb @@ -1,70 +1,91 @@ -# 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). -# # Seed de desenvolvimento: cria um conjunto mínimo de dados para testar as # telas (templates, formulários, turmas, alunos, professores, etc) na interface. +# Idempotente — pode ser rodado múltiplas vezes sem duplicar dados. -departamento_cic = Departamento.find_or_create_by!(code: "CIC") do |departamento| - departamento.name = "Ciência da Computação" +# --------------------------------------------------------------------------- +# Departamentos (domínio legado) +# --------------------------------------------------------------------------- +departamento_cic = Departamento.find_or_create_by!(code: "CIC") do |d| + d.name = "Ciência da Computação" end -departamento_ene = Departamento.find_or_create_by!(code: "ENE") do |departamento| - departamento.name = "Engenharia Elétrica" +departamento_ene = Departamento.find_or_create_by!(code: "ENE") do |d| + d.name = "Engenharia Elétrica" end -admin = Admin.find_or_create_by!(username: "admin") do |admin| - admin.name = "Admin CAMAAR" - admin.email = "admin.cic@unb.br" - admin.departamento_id = departamento_cic.id +# --------------------------------------------------------------------------- +# Department (novo sistema de auth — tabela separada) +# --------------------------------------------------------------------------- +department_cic = Department.find_or_create_by!(code: "CIC") do |d| + d.name = "Ciência da Computação" end -professor_ana = Professor.find_or_create_by!(matricula: "PROF0001") do |professor| - professor.name = "Ana Souza" - professor.email = "ana.souza@unb.br" - professor.formation = "Doutora em Ciência da Computação" - professor.departamento_id = departamento_cic.id - professor.password = "Senha@123" +# --------------------------------------------------------------------------- +# Usuário administrador (novo sistema — usado para login e posse de templates) +# --------------------------------------------------------------------------- +admin_user = User.find_or_create_by!(email: "admin.cic@unb.br") do |u| + u.name = "Admin CAMAAR" + u.registration = "000000001" + u.role = :admin + u.password = "Senha@123" + u.department = department_cic end -professor_bruno = Professor.find_or_create_by!(matricula: "PROF0002") do |professor| - professor.name = "Bruno Lima" - professor.email = "bruno.lima@unb.br" - professor.formation = "Doutor em Engenharia Elétrica" - professor.departamento_id = departamento_ene.id - professor.password = "Senha@123" +# --------------------------------------------------------------------------- +# Professores (domínio legado — relacionamento com Turma) +# --------------------------------------------------------------------------- +professor_ana = Professor.find_or_create_by!(matricula: "PROF0001") do |p| + p.name = "Ana Souza" + p.email = "ana.souza@unb.br" + p.formation = "Doutora em Ciência da Computação" + p.departamento_id = departamento_cic.id + p.password = "Senha@123" end +professor_bruno = Professor.find_or_create_by!(matricula: "PROF0002") do |p| + p.name = "Bruno Lima" + p.email = "bruno.lima@unb.br" + p.formation = "Doutor em Engenharia Elétrica" + p.departamento_id = departamento_ene.id + p.password = "Senha@123" +end + +# --------------------------------------------------------------------------- +# Alunos (domínio legado — relacionamento com TurmaAluno e Submissao) +# --------------------------------------------------------------------------- alunos = [ - { matricula: "190000001", name: "Carla Mendes", email: "carla.mendes@aluno.unb.br", course: "Engenharia de Software" }, - { matricula: "190000002", name: "Diego Alves", email: "diego.alves@aluno.unb.br", course: "Engenharia de Software" }, - { matricula: "190000003", name: "Eduarda Rocha", email: "eduarda.rocha@aluno.unb.br", course: "Ciência da Computação" }, - { matricula: "190000004", name: "Felipe Castro", email: "felipe.castro@aluno.unb.br", course: "Ciência da Computação" } + { matricula: "190000001", name: "Carla Mendes", email: "carla.mendes@aluno.unb.br", course: "Engenharia de Software" }, + { matricula: "190000002", name: "Diego Alves", email: "diego.alves@aluno.unb.br", course: "Engenharia de Software" }, + { matricula: "190000003", name: "Eduarda Rocha", email: "eduarda.rocha@aluno.unb.br", course: "Ciência da Computação" }, + { matricula: "190000004", name: "Felipe Castro", email: "felipe.castro@aluno.unb.br", course: "Ciência da Computação" } ].map do |dados| - Aluno.find_or_create_by!(matricula: dados[:matricula]) do |aluno| - aluno.name = dados[:name] - aluno.email = dados[:email] - aluno.course = dados[:course] - aluno.password = "Senha@123" + Aluno.find_or_create_by!(matricula: dados[:matricula]) do |a| + a.name = dados[:name] + a.email = dados[:email] + a.course = dados[:course] + a.password = "Senha@123" end end -turma_estrutura_dados = Turma.find_or_create_by!(class_code: "CIC0097-T01") do |turma| - turma.subject_code = "CIC0097" - turma.subject_name = "Estrutura de Dados" - turma.semester = "2026.1" - turma.time = "Seg/Qua 08:00-10:00" - turma.departamento_id = departamento_cic.id - turma.professor_id = professor_ana.id +# --------------------------------------------------------------------------- +# Turmas +# --------------------------------------------------------------------------- +turma_estrutura_dados = Turma.find_or_create_by!(class_code: "CIC0097-T01") do |t| + t.subject_code = "CIC0097" + t.subject_name = "Estrutura de Dados" + t.semester = "2026.1" + t.time = "Seg/Qua 08:00-10:00" + t.departamento_id = departamento_cic.id + t.professor_id = professor_ana.id end -turma_circuitos = Turma.find_or_create_by!(class_code: "ENE0011-T01") do |turma| - turma.subject_code = "ENE0011" - turma.subject_name = "Circuitos Elétricos" - turma.semester = "2026.1" - turma.time = "Ter/Qui 10:00-12:00" - turma.departamento_id = departamento_ene.id - turma.professor_id = professor_bruno.id +turma_circuitos = Turma.find_or_create_by!(class_code: "ENE0011-T01") do |t| + t.subject_code = "ENE0011" + t.subject_name = "Circuitos Elétricos" + t.semester = "2026.1" + t.time = "Ter/Qui 10:00-12:00" + t.departamento_id = departamento_ene.id + t.professor_id = professor_bruno.id end [ alunos[0], alunos[1], alunos[2] ].each do |aluno| @@ -75,9 +96,12 @@ TurmaAluno.find_or_create_by!(turma_id: turma_circuitos.id, aluno_id: aluno.id) end -template_discente = Template.find_or_create_by!(title: "Avaliação de Disciplina - Discentes") do |template| - template.target_role = "discente" - template.admin_id = admin.id +# --------------------------------------------------------------------------- +# Templates (admin_id agora referencia users.id) +# --------------------------------------------------------------------------- +template_discente = Template.find_or_create_by!(title: "Avaliação de Disciplina - Discentes") do |t| + t.target_role = "discente" + t.admin_id = admin_user.id end if template_discente.questaos.empty? @@ -86,9 +110,9 @@ template_discente.questaos.create!(enunciado: "Comentários e sugestões para a disciplina:", tipo: "text") end -template_docente = Template.find_or_create_by!(title: "Autoavaliação - Docentes") do |template| - template.target_role = "docente" - template.admin_id = admin.id +template_docente = Template.find_or_create_by!(title: "Autoavaliação - Docentes") do |t| + t.target_role = "docente" + t.admin_id = admin_user.id end if template_docente.questaos.empty? @@ -96,11 +120,14 @@ template_docente.questaos.create!(enunciado: "Quais dificuldades você encontrou no semestre?", tipo: "text") end -formulario = Formulario.find_or_create_by!(title: "Avaliação Estrutura de Dados - 2026.1") do |formulario| - formulario.target_role = template_discente.target_role - formulario.admin_id = admin.id - formulario.template_id = template_discente.id - formulario.turma_id = turma_estrutura_dados.id +# --------------------------------------------------------------------------- +# Formulário de exemplo +# --------------------------------------------------------------------------- +formulario = Formulario.find_or_create_by!(title: "Avaliação Estrutura de Dados - 2026.1") do |f| + f.target_role = template_discente.target_role + f.admin_id = admin_user.id + f.template_id = template_discente.id + f.turma_id = turma_estrutura_dados.id end if formulario.questaos.empty? @@ -109,6 +136,9 @@ end end +# --------------------------------------------------------------------------- +# Submissão de exemplo +# --------------------------------------------------------------------------- submissao = Submissao.find_or_create_by!(formulario_id: formulario.id, participant: alunos[0]) if Respostum.where(submissao_id: submissao.id).empty? @@ -121,8 +151,9 @@ end end +# --------------------------------------------------------------------------- puts "Seed concluída:" -puts " Admin: #{admin.username} (id=#{admin.id}) - use ?admin_id=#{admin.id} enquanto a #104 não existe" +puts " Login admin: email=#{admin_user.email} / senha=Senha@123" puts " Professores: #{Professor.count} | Alunos: #{Aluno.count} | Turmas: #{Turma.count}" puts " Templates: #{Template.count} | Formulários: #{Formulario.count}" puts " Submissões: #{Submissao.count} | Respostas: #{Respostum.count}" diff --git a/project/features/step_definitions/formularios_steps.rb b/project/features/step_definitions/formularios_steps.rb index bebdbf3c1f..7f39d03915 100644 --- a/project/features/step_definitions/formularios_steps.rb +++ b/project/features/step_definitions/formularios_steps.rb @@ -14,7 +14,8 @@ Quando("eu preencher o nome do formulário") do @formulario_titulo = "Formulário Avaliação #{SecureRandom.hex(4)}" - visit new_formulario_path(admin_id: @admin.id) + fazer_login_como(@admin) + visit new_formulario_path fill_in "formulario[title]", with: @formulario_titulo end @@ -46,7 +47,8 @@ Dado("estou criando um formulário") do @template = criar_template_com_questao(@admin) @turma = criar_turma - visit new_formulario_path(admin_id: @admin.id) + fazer_login_como(@admin) + visit new_formulario_path end Quando("eu tentar criar um formulário sem selecionar nenhum template") do diff --git a/project/features/step_definitions/templates_edicao_steps.rb b/project/features/step_definitions/templates_edicao_steps.rb index 3a4a5e6c26..d83842a53d 100644 --- a/project/features/step_definitions/templates_edicao_steps.rb +++ b/project/features/step_definitions/templates_edicao_steps.rb @@ -14,7 +14,8 @@ @novo_titulo = "Template Editado #{SecureRandom.hex(4)}" - visit edit_template_path(@template, admin_id: @admin.id) + fazer_login_como(@admin) + visit edit_template_path(@template) fill_in "template[title]", with: @novo_titulo end @@ -43,7 +44,8 @@ end @template_title = @template.title - visit templates_path(admin_id: @admin.id) + fazer_login_como(@admin) + visit templates_path end Quando("eu pressionar o botão de {string}") do |texto| @@ -68,7 +70,8 @@ Dado("que estou na página de edição do template") do @admin = criar_admin @template = criar_template_com_questao(@admin) - visit edit_template_path(@template, admin_id: @admin.id) + fazer_login_como(@admin) + visit edit_template_path(@template) end Quando("eu clico no botão '+'") do diff --git a/project/features/step_definitions/templates_steps.rb b/project/features/step_definitions/templates_steps.rb index 88feac1612..165bbd3f6f 100644 --- a/project/features/step_definitions/templates_steps.rb +++ b/project/features/step_definitions/templates_steps.rb @@ -3,7 +3,8 @@ Dado("que estou na página de criar template") do @admin = criar_admin - visit new_template_path(admin_id: @admin.id) + fazer_login_como(@admin) + visit new_template_path end Quando("eu preencho o campo 'Nome do template:'") do diff --git a/project/features/step_definitions/templates_visualizacao_steps.rb b/project/features/step_definitions/templates_visualizacao_steps.rb index 38521de335..79dc40ce72 100644 --- a/project/features/step_definitions/templates_visualizacao_steps.rb +++ b/project/features/step_definitions/templates_visualizacao_steps.rb @@ -7,7 +7,8 @@ @outro_admin = criar_admin @template_outro_admin = criar_template_com_questao(@outro_admin, titulo: "Template de Outro Admin #{SecureRandom.hex(4)}") - visit templates_path(admin_id: @admin.id) + fazer_login_como(@admin) + visit templates_path end Quando("o sistema carrega os templates") do @@ -25,7 +26,7 @@ end Quando("ele tenta acessar diretamente um template criado por outro administrador") do - visit edit_template_path(@template_outro_admin, admin_id: @admin.id) + visit edit_template_path(@template_outro_admin) end Então("ele deverá ver uma mensagem de erro de permissão {string}") do |mensagem| diff --git a/project/features/support/test_helpers.rb b/project/features/support/test_helpers.rb index e67f9fb011..38cce725cd 100644 --- a/project/features/support/test_helpers.rb +++ b/project/features/support/test_helpers.rb @@ -1,22 +1,23 @@ module TestHelpers - def departamento_de_teste - Departamento.find_or_create_by!(code: "DEPT001") do |departamento| - departamento.name = "Departamento de Teste" - end - end - def criar_admin - departamento = departamento_de_teste sufixo = SecureRandom.hex(4) - Admin.create!( + User.create!( name: "Admin Teste #{sufixo}", email: "admin#{sufixo}@teste.com", - username: "admin#{sufixo}", - departamento_id: departamento.id + registration: "ADM#{sufixo}", + role: :admin, + password: "Senha@123" ) end + def fazer_login_como(user, password: "Senha@123") + visit login_path + fill_in "Email ou matricula", with: user.email + fill_in "Senha", with: password + click_button "Entrar" + end + def criar_template_com_questao(admin, titulo: "Template #{SecureRandom.hex(4)}", target_role: "discente", enunciado: "Qual é a resposta correta?") template = admin.templates.create!(title: titulo, target_role: target_role) template.questaos.create!(enunciado: enunciado, tipo: "text") @@ -24,7 +25,9 @@ def criar_template_com_questao(admin, titulo: "Template #{SecureRandom.hex(4)}", end def criar_turma - departamento = departamento_de_teste + departamento = Departamento.find_or_create_by!(code: "DEPT001") do |d| + d.name = "Departamento de Teste" + end sufixo = SecureRandom.hex(4) professor = Professor.create!( diff --git a/project/spec/factories/formularios.rb b/project/spec/factories/formularios.rb index 1cede36139..94543243c3 100644 --- a/project/spec/factories/formularios.rb +++ b/project/spec/factories/formularios.rb @@ -1,6 +1,6 @@ FactoryBot.define do factory :formulario do - admin + association :admin, factory: %i[user admin] template turma sequence(:title) { |n| "Formulário de Teste #{n}" } diff --git a/project/spec/factories/templates.rb b/project/spec/factories/templates.rb index 76c74e7a48..1cc98c61d7 100644 --- a/project/spec/factories/templates.rb +++ b/project/spec/factories/templates.rb @@ -1,6 +1,6 @@ FactoryBot.define do factory :template do - admin + association :admin, factory: %i[user admin] sequence(:title) { |n| "Template #{n}" } target_role { "discente" } diff --git a/project/spec/factories/users.rb b/project/spec/factories/users.rb new file mode 100644 index 0000000000..01a52101b6 --- /dev/null +++ b/project/spec/factories/users.rb @@ -0,0 +1,13 @@ +FactoryBot.define do + factory :user do + sequence(:name) { |n| "Usuario #{n}" } + sequence(:email) { |n| "user#{n}@example.com" } + sequence(:registration) { |n| "U#{1000 + n}" } + role { :participant } + password { "Senha@123" } + + trait :admin do + role { :admin } + end + end +end diff --git a/project/spec/requests/formularios_spec.rb b/project/spec/requests/formularios_spec.rb index 19dc8afba5..5d3fe670fc 100644 --- a/project/spec/requests/formularios_spec.rb +++ b/project/spec/requests/formularios_spec.rb @@ -3,11 +3,12 @@ RSpec.describe "Formularios", type: :request do describe "GET /formularios/:id/exportar_csv" do it "retorna um arquivo CSV de sucesso com as respostas da avaliação" do - formulario = create(:formulario) + admin = create(:user, :admin) + formulario = create(:formulario, admin: admin) + login_as(admin) get exportar_csv_formulario_path(formulario, format: :csv) - # Validações expect(response).to have_http_status(:success) expect(response.media_type).to eq('text/csv') expect(response.body).to include("ID da Pergunta", "Enunciado") @@ -16,14 +17,14 @@ describe "POST /formularios" do it "cria um formulário a partir de um template e clona as questões do template" do - admin = create(:admin) + admin = create(:user, :admin) template = create(:template, admin: admin) create(:questao, template: template, enunciado: "O professor foi claro?") turma = create(:turma) + login_as(admin) expect { post formularios_path, params: { - admin_id: admin.id, formulario: { title: "Avaliação do Semestre", template_id: template.id, turma_id: turma.id } } }.to change(Formulario, :count).by(1).and change(Questao, :count).by(1) @@ -39,12 +40,12 @@ end it "não cria o formulário quando nenhum template é selecionado" do - admin = create(:admin) + admin = create(:user, :admin) turma = create(:turma) + login_as(admin) expect { post formularios_path, params: { - admin_id: admin.id, formulario: { title: "Sem template", turma_id: turma.id } } }.not_to change(Formulario, :count) @@ -54,12 +55,12 @@ end it "não cria o formulário quando nenhuma turma é selecionada" do - admin = create(:admin) + admin = create(:user, :admin) template = create(:template, :com_questao, admin: admin) + login_as(admin) expect { post formularios_path, params: { - admin_id: admin.id, formulario: { title: "Sem turma", template_id: template.id } } }.not_to change(Formulario, :count) @@ -68,4 +69,4 @@ expect(response.body).to include("É necessário escolher ao menos uma turma") end end -end \ No newline at end of file +end diff --git a/project/spec/requests/templates_spec.rb b/project/spec/requests/templates_spec.rb index 4cc4efe4db..3e3ebe0340 100644 --- a/project/spec/requests/templates_spec.rb +++ b/project/spec/requests/templates_spec.rb @@ -3,11 +3,11 @@ RSpec.describe "Templates", type: :request do describe "POST /templates" do it "cria um template com uma questão e redireciona para a lista de templates" do - admin = create(:admin) + admin = create(:user, :admin) + login_as(admin) expect { post templates_path, params: { - admin_id: admin.id, template: { title: "Avaliação de Disciplina", target_role: "discente", @@ -25,11 +25,11 @@ end it "não cria o template e exibe erro quando o nome está em branco" do - admin = create(:admin) + admin = create(:user, :admin) + login_as(admin) expect { post templates_path, params: { - admin_id: admin.id, template: { title: "", target_role: "discente", @@ -43,11 +43,11 @@ end it "não cria o template e exibe erro quando a questão não possui enunciado" do - admin = create(:admin) + admin = create(:user, :admin) + login_as(admin) expect { post templates_path, params: { - admin_id: admin.id, template: { title: "Avaliação sem enunciado", target_role: "discente", @@ -61,12 +61,12 @@ end it "adiciona um novo campo de questão sem persistir nada ao clicar em '+'" do - admin = create(:admin) + admin = create(:user, :admin) + login_as(admin) template_count_before = Template.count questao_count_before = Questao.count post templates_path, params: { - admin_id: admin.id, add_questao: "+", template: { title: "Rascunho", target_role: "discente" } } @@ -80,13 +80,14 @@ describe "GET /templates" do it "lista apenas os templates do administrador autenticado" do - admin = create(:admin) - outro_admin = create(:admin) + admin = create(:user, :admin) + outro_admin = create(:user, :admin) meu_template = create(:template, :com_questao, admin: admin, title: "Meu Template") create(:template, :com_questao, admin: outro_admin, title: "Template de Outro Admin") - get templates_path(admin_id: admin.id) + login_as(admin) + get templates_path expect(response).to have_http_status(:success) expect(response.body).to include(meu_template.title) @@ -96,11 +97,12 @@ describe "GET /templates/:id/edit" do it "bloqueia o acesso a um template de outro administrador" do - admin = create(:admin) - outro_admin = create(:admin) + admin = create(:user, :admin) + outro_admin = create(:user, :admin) template_outro_admin = create(:template, :com_questao, admin: outro_admin) - get edit_template_path(template_outro_admin, admin_id: admin.id) + login_as(admin) + get edit_template_path(template_outro_admin) expect(response).to redirect_to(templates_path) follow_redirect! @@ -110,7 +112,7 @@ describe "PATCH /templates/:id" do it "atualiza o template sem afetar as questões dos formulários já gerados a partir dele" do - admin = create(:admin) + admin = create(:user, :admin) template = create(:template, admin: admin) create(:questao, template: template, enunciado: "Pergunta original") turma = create(:turma) @@ -120,8 +122,8 @@ questao_existente = template.questaos.first + login_as(admin) patch template_path(template), params: { - admin_id: admin.id, template: { title: "Título atualizado", target_role: template.target_role, @@ -138,12 +140,12 @@ end it "exige enunciado para novas questões adicionadas durante a edição" do - admin = create(:admin) + admin = create(:user, :admin) template = create(:template, :com_questao, admin: admin) questao_existente = template.questaos.first + login_as(admin) patch template_path(template), params: { - admin_id: admin.id, template: { title: template.title, target_role: template.target_role, @@ -161,15 +163,16 @@ describe "DELETE /templates/:id" do it "remove o template, desvincula formulários gerados e mantém suas questões clonadas" do - admin = create(:admin) + admin = create(:user, :admin) template = create(:template, :com_questao, admin: admin) turma = create(:turma) formulario = create(:formulario, admin: admin, template: template, turma: turma, title: "Formulário Gerado", target_role: template.target_role) template.questaos.each { |questao| formulario.questaos.create!(enunciado: questao.enunciado, tipo: questao.tipo) } + login_as(admin) expect { - delete template_path(template), params: { admin_id: admin.id } + delete template_path(template) }.to change(Template, :count).by(-1) expect(response).to redirect_to(templates_path) diff --git a/project/spec/support/auth_helpers.rb b/project/spec/support/auth_helpers.rb index 2c6e0c8e33..7d0dfc917c 100644 --- a/project/spec/support/auth_helpers.rb +++ b/project/spec/support/auth_helpers.rb @@ -23,6 +23,10 @@ def create_admin password: "Senha@123" ) end + + def login_as(user, password: "Senha@123") + post login_path, params: { session: { identifier: user.email, password: } } + end end RSpec.configure do |config| From bc31226ba4a02332301b93ed6ce017b3165e2265 Mon Sep 17 00:00:00 2001 From: Rafael Dias Ghiorzi Date: Tue, 16 Jun 2026 02:20:38 -0300 Subject: [PATCH 19/23] refac: consolidating users into a single model --- project/app/controllers/admins_controller.rb | 70 -------- project/app/controllers/alunos_controller.rb | 70 -------- .../controllers/departamentos_controller.rb | 70 -------- .../app/controllers/formularios_controller.rb | 37 ++-- .../app/controllers/professors_controller.rb | 70 -------- .../app/controllers/submissaos_controller.rb | 24 +-- project/app/controllers/turmas_controller.rb | 70 -------- project/app/models/admin.rb | 3 - project/app/models/aluno.rb | 6 - project/app/models/course_class.rb | 1 + project/app/models/departamento.rb | 5 - project/app/models/formulario.rb | 6 +- project/app/models/professor.rb | 6 - project/app/models/submissao.rb | 2 +- project/app/models/turma.rb | 7 - project/app/models/turma_aluno.rb | 4 - project/app/views/admins/_admin.html.erb | 27 --- project/app/views/admins/_admin.json.jbuilder | 2 - project/app/views/admins/_form.html.erb | 42 ----- project/app/views/admins/edit.html.erb | 12 -- project/app/views/admins/index.html.erb | 16 -- project/app/views/admins/index.json.jbuilder | 1 - project/app/views/admins/new.html.erb | 11 -- project/app/views/admins/show.html.erb | 10 -- project/app/views/admins/show.json.jbuilder | 1 - project/app/views/alunos/_aluno.html.erb | 27 --- project/app/views/alunos/_aluno.json.jbuilder | 2 - project/app/views/alunos/_form.html.erb | 42 ----- project/app/views/alunos/edit.html.erb | 12 -- project/app/views/alunos/index.html.erb | 16 -- project/app/views/alunos/index.json.jbuilder | 1 - project/app/views/alunos/new.html.erb | 11 -- project/app/views/alunos/show.html.erb | 10 -- project/app/views/alunos/show.json.jbuilder | 1 - .../departamentos/_departamento.html.erb | 12 -- .../departamentos/_departamento.json.jbuilder | 2 - .../app/views/departamentos/_form.html.erb | 27 --- project/app/views/departamentos/edit.html.erb | 12 -- .../app/views/departamentos/index.html.erb | 16 -- .../views/departamentos/index.json.jbuilder | 1 - project/app/views/departamentos/new.html.erb | 11 -- project/app/views/departamentos/show.html.erb | 10 -- .../views/departamentos/show.json.jbuilder | 1 - project/app/views/formularios/_form.html.erb | 5 +- .../views/formularios/_formulario.html.erb | 3 +- project/app/views/professors/_form.html.erb | 47 ----- .../app/views/professors/_professor.html.erb | 32 ---- .../views/professors/_professor.json.jbuilder | 2 - project/app/views/professors/edit.html.erb | 12 -- project/app/views/professors/index.html.erb | 16 -- .../app/views/professors/index.json.jbuilder | 1 - project/app/views/professors/new.html.erb | 11 -- project/app/views/professors/show.html.erb | 10 -- .../app/views/professors/show.json.jbuilder | 1 - project/app/views/turmas/_form.html.erb | 52 ------ project/app/views/turmas/_turma.html.erb | 37 ---- project/app/views/turmas/_turma.json.jbuilder | 2 - project/app/views/turmas/edit.html.erb | 12 -- project/app/views/turmas/index.html.erb | 16 -- project/app/views/turmas/index.json.jbuilder | 1 - project/app/views/turmas/new.html.erb | 11 -- project/app/views/turmas/show.html.erb | 10 -- project/app/views/turmas/show.json.jbuilder | 1 - project/config/routes.rb | 20 +-- ...0260616000002_consolidate_to_user_model.rb | 28 +++ project/db/schema.rb | 84 +-------- project/db/seeds.rb | 163 ++++++------------ .../step_definitions/formularios_steps.rb | 4 +- .../templates_edicao_steps.rb | 4 +- project/features/support/test_helpers.rb | 25 +-- project/spec/factories/admins.rb | 8 - project/spec/factories/alunos.rb | 9 - project/spec/factories/course_classes.rb | 10 ++ project/spec/factories/departamentos.rb | 6 - project/spec/factories/departments.rb | 6 + project/spec/factories/formularios.rb | 2 +- project/spec/factories/professors.rb | 10 -- project/spec/factories/submissaos.rb | 6 +- project/spec/factories/turma_alunos.rb | 6 - project/spec/factories/turmas.rb | 11 -- project/spec/mailers/user_mailer_spec.rb | 4 +- project/spec/models/formulario_spec.rb | 8 +- .../class_members_importer_spec.rb | 10 +- project/spec/requests/formularios_spec.rb | 8 +- project/spec/requests/password_resets_spec.rb | 7 +- project/spec/requests/password_setups_spec.rb | 6 +- project/spec/requests/sessions_spec.rb | 15 +- project/spec/requests/templates_spec.rb | 8 +- project/spec/support/auth_helpers.rb | 25 --- 89 files changed, 171 insertions(+), 1391 deletions(-) delete mode 100644 project/app/controllers/admins_controller.rb delete mode 100644 project/app/controllers/alunos_controller.rb delete mode 100644 project/app/controllers/departamentos_controller.rb delete mode 100644 project/app/controllers/professors_controller.rb delete mode 100644 project/app/controllers/turmas_controller.rb delete mode 100644 project/app/models/admin.rb delete mode 100644 project/app/models/aluno.rb delete mode 100644 project/app/models/departamento.rb delete mode 100644 project/app/models/professor.rb delete mode 100644 project/app/models/turma.rb delete mode 100644 project/app/models/turma_aluno.rb delete mode 100644 project/app/views/admins/_admin.html.erb delete mode 100644 project/app/views/admins/_admin.json.jbuilder delete mode 100644 project/app/views/admins/_form.html.erb delete mode 100644 project/app/views/admins/edit.html.erb delete mode 100644 project/app/views/admins/index.html.erb delete mode 100644 project/app/views/admins/index.json.jbuilder delete mode 100644 project/app/views/admins/new.html.erb delete mode 100644 project/app/views/admins/show.html.erb delete mode 100644 project/app/views/admins/show.json.jbuilder delete mode 100644 project/app/views/alunos/_aluno.html.erb delete mode 100644 project/app/views/alunos/_aluno.json.jbuilder delete mode 100644 project/app/views/alunos/_form.html.erb delete mode 100644 project/app/views/alunos/edit.html.erb delete mode 100644 project/app/views/alunos/index.html.erb delete mode 100644 project/app/views/alunos/index.json.jbuilder delete mode 100644 project/app/views/alunos/new.html.erb delete mode 100644 project/app/views/alunos/show.html.erb delete mode 100644 project/app/views/alunos/show.json.jbuilder delete mode 100644 project/app/views/departamentos/_departamento.html.erb delete mode 100644 project/app/views/departamentos/_departamento.json.jbuilder delete mode 100644 project/app/views/departamentos/_form.html.erb delete mode 100644 project/app/views/departamentos/edit.html.erb delete mode 100644 project/app/views/departamentos/index.html.erb delete mode 100644 project/app/views/departamentos/index.json.jbuilder delete mode 100644 project/app/views/departamentos/new.html.erb delete mode 100644 project/app/views/departamentos/show.html.erb delete mode 100644 project/app/views/departamentos/show.json.jbuilder delete mode 100644 project/app/views/professors/_form.html.erb delete mode 100644 project/app/views/professors/_professor.html.erb delete mode 100644 project/app/views/professors/_professor.json.jbuilder delete mode 100644 project/app/views/professors/edit.html.erb delete mode 100644 project/app/views/professors/index.html.erb delete mode 100644 project/app/views/professors/index.json.jbuilder delete mode 100644 project/app/views/professors/new.html.erb delete mode 100644 project/app/views/professors/show.html.erb delete mode 100644 project/app/views/professors/show.json.jbuilder delete mode 100644 project/app/views/turmas/_form.html.erb delete mode 100644 project/app/views/turmas/_turma.html.erb delete mode 100644 project/app/views/turmas/_turma.json.jbuilder delete mode 100644 project/app/views/turmas/edit.html.erb delete mode 100644 project/app/views/turmas/index.html.erb delete mode 100644 project/app/views/turmas/index.json.jbuilder delete mode 100644 project/app/views/turmas/new.html.erb delete mode 100644 project/app/views/turmas/show.html.erb delete mode 100644 project/app/views/turmas/show.json.jbuilder create mode 100644 project/db/migrate/20260616000002_consolidate_to_user_model.rb delete mode 100644 project/spec/factories/admins.rb delete mode 100644 project/spec/factories/alunos.rb create mode 100644 project/spec/factories/course_classes.rb delete mode 100644 project/spec/factories/departamentos.rb create mode 100644 project/spec/factories/departments.rb delete mode 100644 project/spec/factories/professors.rb delete mode 100644 project/spec/factories/turma_alunos.rb delete mode 100644 project/spec/factories/turmas.rb rename project/spec/{services => requests}/class_members_importer_spec.rb (94%) diff --git a/project/app/controllers/admins_controller.rb b/project/app/controllers/admins_controller.rb deleted file mode 100644 index 21e196367d..0000000000 --- a/project/app/controllers/admins_controller.rb +++ /dev/null @@ -1,70 +0,0 @@ -class AdminsController < ApplicationController - before_action :set_admin, only: %i[ show edit update destroy ] - - # GET /admins or /admins.json - def index - @admins = Admin.all - end - - # GET /admins/1 or /admins/1.json - def show - end - - # GET /admins/new - def new - @admin = Admin.new - end - - # GET /admins/1/edit - def edit - end - - # POST /admins or /admins.json - def create - @admin = Admin.new(admin_params) - - respond_to do |format| - if @admin.save - format.html { redirect_to @admin, notice: "Admin was successfully created." } - format.json { render :show, status: :created, location: @admin } - else - format.html { render :new, status: :unprocessable_content } - format.json { render json: @admin.errors, status: :unprocessable_content } - end - end - end - - # PATCH/PUT /admins/1 or /admins/1.json - def update - respond_to do |format| - if @admin.update(admin_params) - format.html { redirect_to @admin, notice: "Admin was successfully updated.", status: :see_other } - format.json { render :show, status: :ok, location: @admin } - else - format.html { render :edit, status: :unprocessable_content } - format.json { render json: @admin.errors, status: :unprocessable_content } - end - end - end - - # DELETE /admins/1 or /admins/1.json - def destroy - @admin.destroy! - - respond_to do |format| - format.html { redirect_to admins_path, notice: "Admin was successfully destroyed.", status: :see_other } - format.json { head :no_content } - end - end - - private - # Use callbacks to share common setup or constraints between actions. - def set_admin - @admin = Admin.find(params.expect(:id)) - end - - # Only allow a list of trusted parameters through. - def admin_params - params.expect(admin: [ :username, :name, :email, :password_digest, :departamento_id ]) - end -end diff --git a/project/app/controllers/alunos_controller.rb b/project/app/controllers/alunos_controller.rb deleted file mode 100644 index 2f4aa29663..0000000000 --- a/project/app/controllers/alunos_controller.rb +++ /dev/null @@ -1,70 +0,0 @@ -class AlunosController < ApplicationController - before_action :set_aluno, only: %i[ show edit update destroy ] - - # GET /alunos or /alunos.json - def index - @alunos = Aluno.all - end - - # GET /alunos/1 or /alunos/1.json - def show - end - - # GET /alunos/new - def new - @aluno = Aluno.new - end - - # GET /alunos/1/edit - def edit - end - - # POST /alunos or /alunos.json - def create - @aluno = Aluno.new(aluno_params) - - respond_to do |format| - if @aluno.save - format.html { redirect_to @aluno, notice: "Aluno was successfully created." } - format.json { render :show, status: :created, location: @aluno } - else - format.html { render :new, status: :unprocessable_content } - format.json { render json: @aluno.errors, status: :unprocessable_content } - end - end - end - - # PATCH/PUT /alunos/1 or /alunos/1.json - def update - respond_to do |format| - if @aluno.update(aluno_params) - format.html { redirect_to @aluno, notice: "Aluno was successfully updated.", status: :see_other } - format.json { render :show, status: :ok, location: @aluno } - else - format.html { render :edit, status: :unprocessable_content } - format.json { render json: @aluno.errors, status: :unprocessable_content } - end - end - end - - # DELETE /alunos/1 or /alunos/1.json - def destroy - @aluno.destroy! - - respond_to do |format| - format.html { redirect_to alunos_path, notice: "Aluno was successfully destroyed.", status: :see_other } - format.json { head :no_content } - end - end - - private - # Use callbacks to share common setup or constraints between actions. - def set_aluno - @aluno = Aluno.find(params.expect(:id)) - end - - # Only allow a list of trusted parameters through. - def aluno_params - params.expect(aluno: [ :matricula, :name, :email, :password_digest, :course ]) - end -end diff --git a/project/app/controllers/departamentos_controller.rb b/project/app/controllers/departamentos_controller.rb deleted file mode 100644 index af808627bf..0000000000 --- a/project/app/controllers/departamentos_controller.rb +++ /dev/null @@ -1,70 +0,0 @@ -class DepartamentosController < ApplicationController - before_action :set_departamento, only: %i[ show edit update destroy ] - - # GET /departamentos or /departamentos.json - def index - @departamentos = Departamento.all - end - - # GET /departamentos/1 or /departamentos/1.json - def show - end - - # GET /departamentos/new - def new - @departamento = Departamento.new - end - - # GET /departamentos/1/edit - def edit - end - - # POST /departamentos or /departamentos.json - def create - @departamento = Departamento.new(departamento_params) - - respond_to do |format| - if @departamento.save - format.html { redirect_to @departamento, notice: "Departamento was successfully created." } - format.json { render :show, status: :created, location: @departamento } - else - format.html { render :new, status: :unprocessable_content } - format.json { render json: @departamento.errors, status: :unprocessable_content } - end - end - end - - # PATCH/PUT /departamentos/1 or /departamentos/1.json - def update - respond_to do |format| - if @departamento.update(departamento_params) - format.html { redirect_to @departamento, notice: "Departamento was successfully updated.", status: :see_other } - format.json { render :show, status: :ok, location: @departamento } - else - format.html { render :edit, status: :unprocessable_content } - format.json { render json: @departamento.errors, status: :unprocessable_content } - end - end - end - - # DELETE /departamentos/1 or /departamentos/1.json - def destroy - @departamento.destroy! - - respond_to do |format| - format.html { redirect_to departamentos_path, notice: "Departamento was successfully destroyed.", status: :see_other } - format.json { head :no_content } - end - end - - private - # Use callbacks to share common setup or constraints between actions. - def set_departamento - @departamento = Departamento.find(params.expect(:id)) - end - - # Only allow a list of trusted parameters through. - def departamento_params - params.expect(departamento: [ :name, :code ]) - end -end diff --git a/project/app/controllers/formularios_controller.rb b/project/app/controllers/formularios_controller.rb index 1c8e8fd7dc..7acfd0c6dc 100644 --- a/project/app/controllers/formularios_controller.rb +++ b/project/app/controllers/formularios_controller.rb @@ -3,25 +3,20 @@ class FormulariosController < ApplicationController before_action :set_formulario, only: %i[ show edit update destroy exportar_csv ] before_action :set_select_options, only: %i[ new create edit update ] - # GET /formularios or /formularios.json def index @formularios = current_admin.formularios end - # GET /formularios/1 or /formularios/1.json def show end - # GET /formularios/new def new @formulario = Formulario.new end - # GET /formularios/1/edit def edit end - # POST /formularios or /formularios.json def create @formulario = Formulario.new(formulario_params) @formulario.target_role ||= @formulario.template&.target_role @@ -36,7 +31,6 @@ def create end end - # PATCH/PUT /formularios/1 or /formularios/1.json def update respond_to do |format| if @formulario.update(formulario_params) @@ -49,43 +43,38 @@ def update end end - # DELETE /formularios/1 or /formularios/1.json def destroy @formulario.destroy! - respond_to do |format| format.html { redirect_to formularios_path, notice: "Formulario was successfully destroyed.", status: :see_other } format.json { head :no_content } end end - # GET /formularios/1/exportar_csv def exportar_csv require 'csv' - csv_data = CSV.generate(headers: true) do |csv| csv << ["ID da Pergunta", "Enunciado", "Scores Atribuídos"] - end - send_data csv_data, filename: "respostas_formulario_#{@formulario.id}.csv", type: "text/csv" end private - def set_formulario - @formulario = Formulario.find(params.expect(:id)) - end - def formulario_params - attrs = params.expect(formulario: [ :title, :target_role, :status, :template_id, :turma_id ]) - attrs[:admin_id] = current_admin.id - attrs - end + def set_formulario + @formulario = Formulario.find(params.expect(:id)) + end - def set_select_options - @templates = current_admin.templates - @turmas = Turma.all - end + def formulario_params + attrs = params.expect(formulario: [ :title, :target_role, :status, :template_id, :course_class_id ]) + attrs[:admin_id] = current_admin.id + attrs + end + + def set_select_options + @templates = current_admin.templates + @course_classes = CourseClass.all + end end diff --git a/project/app/controllers/professors_controller.rb b/project/app/controllers/professors_controller.rb deleted file mode 100644 index 5b5cd839b9..0000000000 --- a/project/app/controllers/professors_controller.rb +++ /dev/null @@ -1,70 +0,0 @@ -class ProfessorsController < ApplicationController - before_action :set_professor, only: %i[ show edit update destroy ] - - # GET /professors or /professors.json - def index - @professors = Professor.all - end - - # GET /professors/1 or /professors/1.json - def show - end - - # GET /professors/new - def new - @professor = Professor.new - end - - # GET /professors/1/edit - def edit - end - - # POST /professors or /professors.json - def create - @professor = Professor.new(professor_params) - - respond_to do |format| - if @professor.save - format.html { redirect_to @professor, notice: "Professor was successfully created." } - format.json { render :show, status: :created, location: @professor } - else - format.html { render :new, status: :unprocessable_content } - format.json { render json: @professor.errors, status: :unprocessable_content } - end - end - end - - # PATCH/PUT /professors/1 or /professors/1.json - def update - respond_to do |format| - if @professor.update(professor_params) - format.html { redirect_to @professor, notice: "Professor was successfully updated.", status: :see_other } - format.json { render :show, status: :ok, location: @professor } - else - format.html { render :edit, status: :unprocessable_content } - format.json { render json: @professor.errors, status: :unprocessable_content } - end - end - end - - # DELETE /professors/1 or /professors/1.json - def destroy - @professor.destroy! - - respond_to do |format| - format.html { redirect_to professors_path, notice: "Professor was successfully destroyed.", status: :see_other } - format.json { head :no_content } - end - end - - private - # Use callbacks to share common setup or constraints between actions. - def set_professor - @professor = Professor.find(params.expect(:id)) - end - - # Only allow a list of trusted parameters through. - def professor_params - params.expect(professor: [ :matricula, :name, :email, :password_digest, :formation, :departamento_id ]) - end -end diff --git a/project/app/controllers/submissaos_controller.rb b/project/app/controllers/submissaos_controller.rb index afe8dce803..f8b8e050b5 100644 --- a/project/app/controllers/submissaos_controller.rb +++ b/project/app/controllers/submissaos_controller.rb @@ -1,28 +1,22 @@ class SubmissaosController < ApplicationController before_action :set_submissao, only: %i[ show edit update destroy ] - # GET /submissaos or /submissaos.json def index @submissaos = Submissao.all end - # GET /submissaos/1 or /submissaos/1.json def show end - # GET /submissaos/new def new @submissao = Submissao.new end - # GET /submissaos/1/edit def edit end - # POST /submissaos or /submissaos.json def create @submissao = Submissao.new(submissao_params) - respond_to do |format| if @submissao.save format.html { redirect_to @submissao, notice: "Submissao was successfully created." } @@ -34,7 +28,6 @@ def create end end - # PATCH/PUT /submissaos/1 or /submissaos/1.json def update respond_to do |format| if @submissao.update(submissao_params) @@ -47,10 +40,8 @@ def update end end - # DELETE /submissaos/1 or /submissaos/1.json def destroy @submissao.destroy! - respond_to do |format| format.html { redirect_to submissaos_path, notice: "Submissao was successfully destroyed.", status: :see_other } format.json { head :no_content } @@ -58,13 +49,12 @@ def destroy end private - # Use callbacks to share common setup or constraints between actions. - def set_submissao - @submissao = Submissao.find(params.expect(:id)) - end - # Only allow a list of trusted parameters through. - def submissao_params - params.expect(submissao: [ :formulario_id, :participant_id, :participant_type ]) - end + def set_submissao + @submissao = Submissao.find(params.expect(:id)) + end + + def submissao_params + params.expect(submissao: [ :formulario_id, :user_id ]) + end end diff --git a/project/app/controllers/turmas_controller.rb b/project/app/controllers/turmas_controller.rb deleted file mode 100644 index 5e257736e0..0000000000 --- a/project/app/controllers/turmas_controller.rb +++ /dev/null @@ -1,70 +0,0 @@ -class TurmasController < ApplicationController - before_action :set_turma, only: %i[ show edit update destroy ] - - # GET /turmas or /turmas.json - def index - @turmas = Turma.all - end - - # GET /turmas/1 or /turmas/1.json - def show - end - - # GET /turmas/new - def new - @turma = Turma.new - end - - # GET /turmas/1/edit - def edit - end - - # POST /turmas or /turmas.json - def create - @turma = Turma.new(turma_params) - - respond_to do |format| - if @turma.save - format.html { redirect_to @turma, notice: "Turma was successfully created." } - format.json { render :show, status: :created, location: @turma } - else - format.html { render :new, status: :unprocessable_content } - format.json { render json: @turma.errors, status: :unprocessable_content } - end - end - end - - # PATCH/PUT /turmas/1 or /turmas/1.json - def update - respond_to do |format| - if @turma.update(turma_params) - format.html { redirect_to @turma, notice: "Turma was successfully updated.", status: :see_other } - format.json { render :show, status: :ok, location: @turma } - else - format.html { render :edit, status: :unprocessable_content } - format.json { render json: @turma.errors, status: :unprocessable_content } - end - end - end - - # DELETE /turmas/1 or /turmas/1.json - def destroy - @turma.destroy! - - respond_to do |format| - format.html { redirect_to turmas_path, notice: "Turma was successfully destroyed.", status: :see_other } - format.json { head :no_content } - end - end - - private - # Use callbacks to share common setup or constraints between actions. - def set_turma - @turma = Turma.find(params.expect(:id)) - end - - # Only allow a list of trusted parameters through. - def turma_params - params.expect(turma: [ :subject_code, :subject_name, :class_code, :semester, :time, :departamento_id, :professor_id ]) - end -end diff --git a/project/app/models/admin.rb b/project/app/models/admin.rb deleted file mode 100644 index 0d89d934ca..0000000000 --- a/project/app/models/admin.rb +++ /dev/null @@ -1,3 +0,0 @@ -class Admin < ApplicationRecord - belongs_to :departamento -end diff --git a/project/app/models/aluno.rb b/project/app/models/aluno.rb deleted file mode 100644 index f1d547d638..0000000000 --- a/project/app/models/aluno.rb +++ /dev/null @@ -1,6 +0,0 @@ -class Aluno < ApplicationRecord - has_secure_password # Habilita a criptografia da senha usando bcrypt - has_many :turma_alunos - has_many :turmas, through: :turma_alunos - has_many :submissoes, as: :participant -end \ No newline at end of file diff --git a/project/app/models/course_class.rb b/project/app/models/course_class.rb index 39b0442005..b88367db50 100644 --- a/project/app/models/course_class.rb +++ b/project/app/models/course_class.rb @@ -2,6 +2,7 @@ class CourseClass < ApplicationRecord belongs_to :department has_many :class_memberships, dependent: :destroy has_many :users, through: :class_memberships + has_many :formularios, foreign_key: :course_class_id, dependent: :destroy validates :code, :class_code, :semester, presence: true validates :code, uniqueness: { scope: [ :class_code, :semester ] } diff --git a/project/app/models/departamento.rb b/project/app/models/departamento.rb deleted file mode 100644 index 7e10c65d86..0000000000 --- a/project/app/models/departamento.rb +++ /dev/null @@ -1,5 +0,0 @@ -class Departamento < ApplicationRecord - has_many :admins - has_many :professors - has_many :turmas -end \ No newline at end of file diff --git a/project/app/models/formulario.rb b/project/app/models/formulario.rb index 03750ad1ba..b280b18956 100644 --- a/project/app/models/formulario.rb +++ b/project/app/models/formulario.rb @@ -1,14 +1,12 @@ class Formulario < ApplicationRecord - # O template pode ser desvinculado (ficar nulo) se o Admin deletar o template - # de origem; o formulário e suas questões clonadas permanecem intactos. belongs_to :template, optional: true - belongs_to :turma, optional: true + belongs_to :course_class, optional: true belongs_to :admin, class_name: "User" has_many :questaos, dependent: :destroy validates :title, presence: { message: "O nome do formulário é obrigatório" } validates :template_id, presence: { message: "É necessário escolher um template base" }, on: :create - validates :turma_id, presence: { message: "É necessário escolher ao menos uma turma" }, on: :create + validates :course_class_id, presence: { message: "É necessário escolher ao menos uma turma" }, on: :create after_initialize { self.status ||= "draft" } end diff --git a/project/app/models/professor.rb b/project/app/models/professor.rb deleted file mode 100644 index 6203f39fe4..0000000000 --- a/project/app/models/professor.rb +++ /dev/null @@ -1,6 +0,0 @@ -class Professor < ApplicationRecord - belongs_to :departamento - has_secure_password - has_many :turmas - has_many :submissoes, as: :participant -end \ No newline at end of file diff --git a/project/app/models/submissao.rb b/project/app/models/submissao.rb index dad4a9dd3d..c532cf6595 100644 --- a/project/app/models/submissao.rb +++ b/project/app/models/submissao.rb @@ -1,4 +1,4 @@ class Submissao < ApplicationRecord belongs_to :formulario - belongs_to :participant, polymorphic: true + belongs_to :user end diff --git a/project/app/models/turma.rb b/project/app/models/turma.rb deleted file mode 100644 index 49b710c1bc..0000000000 --- a/project/app/models/turma.rb +++ /dev/null @@ -1,7 +0,0 @@ -class Turma < ApplicationRecord - belongs_to :departamento - belongs_to :professor - has_many :turma_alunos - has_many :alunos, through: :turma_alunos # Resolve o relacionamento N:N - has_many :formularios -end \ No newline at end of file diff --git a/project/app/models/turma_aluno.rb b/project/app/models/turma_aluno.rb deleted file mode 100644 index 61478af53c..0000000000 --- a/project/app/models/turma_aluno.rb +++ /dev/null @@ -1,4 +0,0 @@ -class TurmaAluno < ApplicationRecord - belongs_to :aluno - belongs_to :turma -end diff --git a/project/app/views/admins/_admin.html.erb b/project/app/views/admins/_admin.html.erb deleted file mode 100644 index 535042c356..0000000000 --- a/project/app/views/admins/_admin.html.erb +++ /dev/null @@ -1,27 +0,0 @@ -
-
- Username: - <%= admin.username %> -
- -
- Name: - <%= admin.name %> -
- -
- Email: - <%= admin.email %> -
- -
- Password digest: - <%= admin.password_digest %> -
- -
- Departamento: - <%= admin.departamento_id %> -
- -
diff --git a/project/app/views/admins/_admin.json.jbuilder b/project/app/views/admins/_admin.json.jbuilder deleted file mode 100644 index 19f73184b3..0000000000 --- a/project/app/views/admins/_admin.json.jbuilder +++ /dev/null @@ -1,2 +0,0 @@ -json.extract! admin, :id, :username, :name, :email, :password_digest, :departamento_id, :created_at, :updated_at -json.url admin_url(admin, format: :json) diff --git a/project/app/views/admins/_form.html.erb b/project/app/views/admins/_form.html.erb deleted file mode 100644 index 62cffb2b7e..0000000000 --- a/project/app/views/admins/_form.html.erb +++ /dev/null @@ -1,42 +0,0 @@ -<%= form_with(model: admin) do |form| %> - <% if admin.errors.any? %> -
-

<%= pluralize(admin.errors.count, "error") %> prohibited this admin from being saved:

- -
    - <% admin.errors.each do |error| %> -
  • <%= error.full_message %>
  • - <% end %> -
-
- <% end %> - -
- <%= form.label :username, style: "display: block" %> - <%= form.text_field :username %> -
- -
- <%= form.label :name, style: "display: block" %> - <%= form.text_field :name %> -
- -
- <%= form.label :email, style: "display: block" %> - <%= form.text_field :email %> -
- -
- <%= form.label :password_digest, style: "display: block" %> - <%= form.text_field :password_digest %> -
- -
- <%= form.label :departamento_id, style: "display: block" %> - <%= form.text_field :departamento_id %> -
- -
- <%= form.submit %> -
-<% end %> diff --git a/project/app/views/admins/edit.html.erb b/project/app/views/admins/edit.html.erb deleted file mode 100644 index d76e123b75..0000000000 --- a/project/app/views/admins/edit.html.erb +++ /dev/null @@ -1,12 +0,0 @@ -<% content_for :title, "Editing admin" %> - -

Editing admin

- -<%= render "form", admin: @admin %> - -
- -
- <%= link_to "Show this admin", @admin %> | - <%= link_to "Back to admins", admins_path %> -
diff --git a/project/app/views/admins/index.html.erb b/project/app/views/admins/index.html.erb deleted file mode 100644 index d50845e38e..0000000000 --- a/project/app/views/admins/index.html.erb +++ /dev/null @@ -1,16 +0,0 @@ -

<%= notice %>

- -<% content_for :title, "Admins" %> - -

Admins

- -
- <% @admins.each do |admin| %> - <%= render admin %> -

- <%= link_to "Show this admin", admin %> -

- <% end %> -
- -<%= link_to "New admin", new_admin_path %> diff --git a/project/app/views/admins/index.json.jbuilder b/project/app/views/admins/index.json.jbuilder deleted file mode 100644 index 5efd59b17b..0000000000 --- a/project/app/views/admins/index.json.jbuilder +++ /dev/null @@ -1 +0,0 @@ -json.array! @admins, partial: "admins/admin", as: :admin diff --git a/project/app/views/admins/new.html.erb b/project/app/views/admins/new.html.erb deleted file mode 100644 index 06fd1231e9..0000000000 --- a/project/app/views/admins/new.html.erb +++ /dev/null @@ -1,11 +0,0 @@ -<% content_for :title, "New admin" %> - -

New admin

- -<%= render "form", admin: @admin %> - -
- -
- <%= link_to "Back to admins", admins_path %> -
diff --git a/project/app/views/admins/show.html.erb b/project/app/views/admins/show.html.erb deleted file mode 100644 index 238d213616..0000000000 --- a/project/app/views/admins/show.html.erb +++ /dev/null @@ -1,10 +0,0 @@ -

<%= notice %>

- -<%= render @admin %> - -
- <%= link_to "Edit this admin", edit_admin_path(@admin) %> | - <%= link_to "Back to admins", admins_path %> - - <%= button_to "Destroy this admin", @admin, method: :delete %> -
diff --git a/project/app/views/admins/show.json.jbuilder b/project/app/views/admins/show.json.jbuilder deleted file mode 100644 index b62f64f16b..0000000000 --- a/project/app/views/admins/show.json.jbuilder +++ /dev/null @@ -1 +0,0 @@ -json.partial! "admins/admin", admin: @admin diff --git a/project/app/views/alunos/_aluno.html.erb b/project/app/views/alunos/_aluno.html.erb deleted file mode 100644 index 2d93e21748..0000000000 --- a/project/app/views/alunos/_aluno.html.erb +++ /dev/null @@ -1,27 +0,0 @@ -
-
- Matricula: - <%= aluno.matricula %> -
- -
- Name: - <%= aluno.name %> -
- -
- Email: - <%= aluno.email %> -
- -
- Password digest: - <%= aluno.password_digest %> -
- -
- Course: - <%= aluno.course %> -
- -
diff --git a/project/app/views/alunos/_aluno.json.jbuilder b/project/app/views/alunos/_aluno.json.jbuilder deleted file mode 100644 index 4e5198ab05..0000000000 --- a/project/app/views/alunos/_aluno.json.jbuilder +++ /dev/null @@ -1,2 +0,0 @@ -json.extract! aluno, :id, :matricula, :name, :email, :password_digest, :course, :created_at, :updated_at -json.url aluno_url(aluno, format: :json) diff --git a/project/app/views/alunos/_form.html.erb b/project/app/views/alunos/_form.html.erb deleted file mode 100644 index 8cdf068652..0000000000 --- a/project/app/views/alunos/_form.html.erb +++ /dev/null @@ -1,42 +0,0 @@ -<%= form_with(model: aluno) do |form| %> - <% if aluno.errors.any? %> -
-

<%= pluralize(aluno.errors.count, "error") %> prohibited this aluno from being saved:

- -
    - <% aluno.errors.each do |error| %> -
  • <%= error.full_message %>
  • - <% end %> -
-
- <% end %> - -
- <%= form.label :matricula, style: "display: block" %> - <%= form.text_field :matricula %> -
- -
- <%= form.label :name, style: "display: block" %> - <%= form.text_field :name %> -
- -
- <%= form.label :email, style: "display: block" %> - <%= form.text_field :email %> -
- -
- <%= form.label :password_digest, style: "display: block" %> - <%= form.text_field :password_digest %> -
- -
- <%= form.label :course, style: "display: block" %> - <%= form.text_field :course %> -
- -
- <%= form.submit %> -
-<% end %> diff --git a/project/app/views/alunos/edit.html.erb b/project/app/views/alunos/edit.html.erb deleted file mode 100644 index ebf1b5ce77..0000000000 --- a/project/app/views/alunos/edit.html.erb +++ /dev/null @@ -1,12 +0,0 @@ -<% content_for :title, "Editing aluno" %> - -

Editing aluno

- -<%= render "form", aluno: @aluno %> - -
- -
- <%= link_to "Show this aluno", @aluno %> | - <%= link_to "Back to alunos", alunos_path %> -
diff --git a/project/app/views/alunos/index.html.erb b/project/app/views/alunos/index.html.erb deleted file mode 100644 index c338a9b9f2..0000000000 --- a/project/app/views/alunos/index.html.erb +++ /dev/null @@ -1,16 +0,0 @@ -

<%= notice %>

- -<% content_for :title, "Alunos" %> - -

Alunos

- -
- <% @alunos.each do |aluno| %> - <%= render aluno %> -

- <%= link_to "Show this aluno", aluno %> -

- <% end %> -
- -<%= link_to "New aluno", new_aluno_path %> diff --git a/project/app/views/alunos/index.json.jbuilder b/project/app/views/alunos/index.json.jbuilder deleted file mode 100644 index d8ba6d1b32..0000000000 --- a/project/app/views/alunos/index.json.jbuilder +++ /dev/null @@ -1 +0,0 @@ -json.array! @alunos, partial: "alunos/aluno", as: :aluno diff --git a/project/app/views/alunos/new.html.erb b/project/app/views/alunos/new.html.erb deleted file mode 100644 index f004daf571..0000000000 --- a/project/app/views/alunos/new.html.erb +++ /dev/null @@ -1,11 +0,0 @@ -<% content_for :title, "New aluno" %> - -

New aluno

- -<%= render "form", aluno: @aluno %> - -
- -
- <%= link_to "Back to alunos", alunos_path %> -
diff --git a/project/app/views/alunos/show.html.erb b/project/app/views/alunos/show.html.erb deleted file mode 100644 index 0eea97a1d1..0000000000 --- a/project/app/views/alunos/show.html.erb +++ /dev/null @@ -1,10 +0,0 @@ -

<%= notice %>

- -<%= render @aluno %> - -
- <%= link_to "Edit this aluno", edit_aluno_path(@aluno) %> | - <%= link_to "Back to alunos", alunos_path %> - - <%= button_to "Destroy this aluno", @aluno, method: :delete %> -
diff --git a/project/app/views/alunos/show.json.jbuilder b/project/app/views/alunos/show.json.jbuilder deleted file mode 100644 index 5cb30d27cf..0000000000 --- a/project/app/views/alunos/show.json.jbuilder +++ /dev/null @@ -1 +0,0 @@ -json.partial! "alunos/aluno", aluno: @aluno diff --git a/project/app/views/departamentos/_departamento.html.erb b/project/app/views/departamentos/_departamento.html.erb deleted file mode 100644 index feee8625cb..0000000000 --- a/project/app/views/departamentos/_departamento.html.erb +++ /dev/null @@ -1,12 +0,0 @@ -
-
- Name: - <%= departamento.name %> -
- -
- Code: - <%= departamento.code %> -
- -
diff --git a/project/app/views/departamentos/_departamento.json.jbuilder b/project/app/views/departamentos/_departamento.json.jbuilder deleted file mode 100644 index 3c7e1b525c..0000000000 --- a/project/app/views/departamentos/_departamento.json.jbuilder +++ /dev/null @@ -1,2 +0,0 @@ -json.extract! departamento, :id, :name, :code, :created_at, :updated_at -json.url departamento_url(departamento, format: :json) diff --git a/project/app/views/departamentos/_form.html.erb b/project/app/views/departamentos/_form.html.erb deleted file mode 100644 index 31cab715c4..0000000000 --- a/project/app/views/departamentos/_form.html.erb +++ /dev/null @@ -1,27 +0,0 @@ -<%= form_with(model: departamento) do |form| %> - <% if departamento.errors.any? %> -
-

<%= pluralize(departamento.errors.count, "error") %> prohibited this departamento from being saved:

- -
    - <% departamento.errors.each do |error| %> -
  • <%= error.full_message %>
  • - <% end %> -
-
- <% end %> - -
- <%= form.label :name, style: "display: block" %> - <%= form.text_field :name %> -
- -
- <%= form.label :code, style: "display: block" %> - <%= form.text_field :code %> -
- -
- <%= form.submit %> -
-<% end %> diff --git a/project/app/views/departamentos/edit.html.erb b/project/app/views/departamentos/edit.html.erb deleted file mode 100644 index f63ef9cac0..0000000000 --- a/project/app/views/departamentos/edit.html.erb +++ /dev/null @@ -1,12 +0,0 @@ -<% content_for :title, "Editing departamento" %> - -

Editing departamento

- -<%= render "form", departamento: @departamento %> - -
- -
- <%= link_to "Show this departamento", @departamento %> | - <%= link_to "Back to departamentos", departamentos_path %> -
diff --git a/project/app/views/departamentos/index.html.erb b/project/app/views/departamentos/index.html.erb deleted file mode 100644 index a05d2b04fa..0000000000 --- a/project/app/views/departamentos/index.html.erb +++ /dev/null @@ -1,16 +0,0 @@ -

<%= notice %>

- -<% content_for :title, "Departamentos" %> - -

Departamentos

- -
- <% @departamentos.each do |departamento| %> - <%= render departamento %> -

- <%= link_to "Show this departamento", departamento %> -

- <% end %> -
- -<%= link_to "New departamento", new_departamento_path %> diff --git a/project/app/views/departamentos/index.json.jbuilder b/project/app/views/departamentos/index.json.jbuilder deleted file mode 100644 index 030ab748d1..0000000000 --- a/project/app/views/departamentos/index.json.jbuilder +++ /dev/null @@ -1 +0,0 @@ -json.array! @departamentos, partial: "departamentos/departamento", as: :departamento diff --git a/project/app/views/departamentos/new.html.erb b/project/app/views/departamentos/new.html.erb deleted file mode 100644 index 76eefa1cfb..0000000000 --- a/project/app/views/departamentos/new.html.erb +++ /dev/null @@ -1,11 +0,0 @@ -<% content_for :title, "New departamento" %> - -

New departamento

- -<%= render "form", departamento: @departamento %> - -
- -
- <%= link_to "Back to departamentos", departamentos_path %> -
diff --git a/project/app/views/departamentos/show.html.erb b/project/app/views/departamentos/show.html.erb deleted file mode 100644 index 2957799d46..0000000000 --- a/project/app/views/departamentos/show.html.erb +++ /dev/null @@ -1,10 +0,0 @@ -

<%= notice %>

- -<%= render @departamento %> - -
- <%= link_to "Edit this departamento", edit_departamento_path(@departamento) %> | - <%= link_to "Back to departamentos", departamentos_path %> - - <%= button_to "Destroy this departamento", @departamento, method: :delete %> -
diff --git a/project/app/views/departamentos/show.json.jbuilder b/project/app/views/departamentos/show.json.jbuilder deleted file mode 100644 index 13c4a70c0c..0000000000 --- a/project/app/views/departamentos/show.json.jbuilder +++ /dev/null @@ -1 +0,0 @@ -json.partial! "departamentos/departamento", departamento: @departamento diff --git a/project/app/views/formularios/_form.html.erb b/project/app/views/formularios/_form.html.erb index 883f0a2bb1..707ad1d3b3 100644 --- a/project/app/views/formularios/_form.html.erb +++ b/project/app/views/formularios/_form.html.erb @@ -2,7 +2,6 @@ <% if formulario.errors.any? %>

<%= pluralize(formulario.errors.count, "erro") %> impediu(ram) este formulário de ser salvo:

-
    <% formulario.errors.full_messages.each do |message| %>
  • <%= message %>
  • @@ -22,8 +21,8 @@
- <%= form.label :turma_id, "Turma:" %> - <%= form.collection_select :turma_id, @turmas, :id, :subject_name, include_blank: "Selecione uma turma" %> + <%= form.label :course_class_id, "Turma:" %> + <%= form.collection_select :course_class_id, @course_classes, :id, :name, include_blank: "Selecione uma turma" %>
diff --git a/project/app/views/formularios/_formulario.html.erb b/project/app/views/formularios/_formulario.html.erb index 85efd30d14..aa0adb521c 100644 --- a/project/app/views/formularios/_formulario.html.erb +++ b/project/app/views/formularios/_formulario.html.erb @@ -21,12 +21,11 @@
Turma: - <%= formulario.turma_id %> + <%= formulario.course_class_id %>
Admin: <%= formulario.admin_id %>
-
diff --git a/project/app/views/professors/_form.html.erb b/project/app/views/professors/_form.html.erb deleted file mode 100644 index 1caee69146..0000000000 --- a/project/app/views/professors/_form.html.erb +++ /dev/null @@ -1,47 +0,0 @@ -<%= form_with(model: professor) do |form| %> - <% if professor.errors.any? %> -
-

<%= pluralize(professor.errors.count, "error") %> prohibited this professor from being saved:

- -
    - <% professor.errors.each do |error| %> -
  • <%= error.full_message %>
  • - <% end %> -
-
- <% end %> - -
- <%= form.label :matricula, style: "display: block" %> - <%= form.text_field :matricula %> -
- -
- <%= form.label :name, style: "display: block" %> - <%= form.text_field :name %> -
- -
- <%= form.label :email, style: "display: block" %> - <%= form.text_field :email %> -
- -
- <%= form.label :password_digest, style: "display: block" %> - <%= form.text_field :password_digest %> -
- -
- <%= form.label :formation, style: "display: block" %> - <%= form.text_field :formation %> -
- -
- <%= form.label :departamento_id, style: "display: block" %> - <%= form.text_field :departamento_id %> -
- -
- <%= form.submit %> -
-<% end %> diff --git a/project/app/views/professors/_professor.html.erb b/project/app/views/professors/_professor.html.erb deleted file mode 100644 index de3fdf7a2c..0000000000 --- a/project/app/views/professors/_professor.html.erb +++ /dev/null @@ -1,32 +0,0 @@ -
-
- Matricula: - <%= professor.matricula %> -
- -
- Name: - <%= professor.name %> -
- -
- Email: - <%= professor.email %> -
- -
- Password digest: - <%= professor.password_digest %> -
- -
- Formation: - <%= professor.formation %> -
- -
- Departamento: - <%= professor.departamento_id %> -
- -
diff --git a/project/app/views/professors/_professor.json.jbuilder b/project/app/views/professors/_professor.json.jbuilder deleted file mode 100644 index e10f1eda5d..0000000000 --- a/project/app/views/professors/_professor.json.jbuilder +++ /dev/null @@ -1,2 +0,0 @@ -json.extract! professor, :id, :matricula, :name, :email, :password_digest, :formation, :departamento_id, :created_at, :updated_at -json.url professor_url(professor, format: :json) diff --git a/project/app/views/professors/edit.html.erb b/project/app/views/professors/edit.html.erb deleted file mode 100644 index cb883e2954..0000000000 --- a/project/app/views/professors/edit.html.erb +++ /dev/null @@ -1,12 +0,0 @@ -<% content_for :title, "Editing professor" %> - -

Editing professor

- -<%= render "form", professor: @professor %> - -
- -
- <%= link_to "Show this professor", @professor %> | - <%= link_to "Back to professors", professors_path %> -
diff --git a/project/app/views/professors/index.html.erb b/project/app/views/professors/index.html.erb deleted file mode 100644 index 1fb166bfec..0000000000 --- a/project/app/views/professors/index.html.erb +++ /dev/null @@ -1,16 +0,0 @@ -

<%= notice %>

- -<% content_for :title, "Professors" %> - -

Professors

- -
- <% @professors.each do |professor| %> - <%= render professor %> -

- <%= link_to "Show this professor", professor %> -

- <% end %> -
- -<%= link_to "New professor", new_professor_path %> diff --git a/project/app/views/professors/index.json.jbuilder b/project/app/views/professors/index.json.jbuilder deleted file mode 100644 index 0403741ce6..0000000000 --- a/project/app/views/professors/index.json.jbuilder +++ /dev/null @@ -1 +0,0 @@ -json.array! @professors, partial: "professors/professor", as: :professor diff --git a/project/app/views/professors/new.html.erb b/project/app/views/professors/new.html.erb deleted file mode 100644 index 9cea04d8c5..0000000000 --- a/project/app/views/professors/new.html.erb +++ /dev/null @@ -1,11 +0,0 @@ -<% content_for :title, "New professor" %> - -

New professor

- -<%= render "form", professor: @professor %> - -
- -
- <%= link_to "Back to professors", professors_path %> -
diff --git a/project/app/views/professors/show.html.erb b/project/app/views/professors/show.html.erb deleted file mode 100644 index 65d3961e08..0000000000 --- a/project/app/views/professors/show.html.erb +++ /dev/null @@ -1,10 +0,0 @@ -

<%= notice %>

- -<%= render @professor %> - -
- <%= link_to "Edit this professor", edit_professor_path(@professor) %> | - <%= link_to "Back to professors", professors_path %> - - <%= button_to "Destroy this professor", @professor, method: :delete %> -
diff --git a/project/app/views/professors/show.json.jbuilder b/project/app/views/professors/show.json.jbuilder deleted file mode 100644 index 26daa8ac44..0000000000 --- a/project/app/views/professors/show.json.jbuilder +++ /dev/null @@ -1 +0,0 @@ -json.partial! "professors/professor", professor: @professor diff --git a/project/app/views/turmas/_form.html.erb b/project/app/views/turmas/_form.html.erb deleted file mode 100644 index 1b20d18eb5..0000000000 --- a/project/app/views/turmas/_form.html.erb +++ /dev/null @@ -1,52 +0,0 @@ -<%= form_with(model: turma) do |form| %> - <% if turma.errors.any? %> -
-

<%= pluralize(turma.errors.count, "error") %> prohibited this turma from being saved:

- -
    - <% turma.errors.each do |error| %> -
  • <%= error.full_message %>
  • - <% end %> -
-
- <% end %> - -
- <%= form.label :subject_code, style: "display: block" %> - <%= form.text_field :subject_code %> -
- -
- <%= form.label :subject_name, style: "display: block" %> - <%= form.text_field :subject_name %> -
- -
- <%= form.label :class_code, style: "display: block" %> - <%= form.text_field :class_code %> -
- -
- <%= form.label :semester, style: "display: block" %> - <%= form.text_field :semester %> -
- -
- <%= form.label :time, style: "display: block" %> - <%= form.text_field :time %> -
- -
- <%= form.label :departamento_id, style: "display: block" %> - <%= form.text_field :departamento_id %> -
- -
- <%= form.label :professor_id, style: "display: block" %> - <%= form.text_field :professor_id %> -
- -
- <%= form.submit %> -
-<% end %> diff --git a/project/app/views/turmas/_turma.html.erb b/project/app/views/turmas/_turma.html.erb deleted file mode 100644 index bb0d01b188..0000000000 --- a/project/app/views/turmas/_turma.html.erb +++ /dev/null @@ -1,37 +0,0 @@ -
-
- Subject code: - <%= turma.subject_code %> -
- -
- Subject name: - <%= turma.subject_name %> -
- -
- Class code: - <%= turma.class_code %> -
- -
- Semester: - <%= turma.semester %> -
- -
- Time: - <%= turma.time %> -
- -
- Departamento: - <%= turma.departamento_id %> -
- -
- Professor: - <%= turma.professor_id %> -
- -
diff --git a/project/app/views/turmas/_turma.json.jbuilder b/project/app/views/turmas/_turma.json.jbuilder deleted file mode 100644 index 62063817a2..0000000000 --- a/project/app/views/turmas/_turma.json.jbuilder +++ /dev/null @@ -1,2 +0,0 @@ -json.extract! turma, :id, :subject_code, :subject_name, :class_code, :semester, :time, :departamento_id, :professor_id, :created_at, :updated_at -json.url turma_url(turma, format: :json) diff --git a/project/app/views/turmas/edit.html.erb b/project/app/views/turmas/edit.html.erb deleted file mode 100644 index 108b99a6c2..0000000000 --- a/project/app/views/turmas/edit.html.erb +++ /dev/null @@ -1,12 +0,0 @@ -<% content_for :title, "Editing turma" %> - -

Editing turma

- -<%= render "form", turma: @turma %> - -
- -
- <%= link_to "Show this turma", @turma %> | - <%= link_to "Back to turmas", turmas_path %> -
diff --git a/project/app/views/turmas/index.html.erb b/project/app/views/turmas/index.html.erb deleted file mode 100644 index 72ec97a969..0000000000 --- a/project/app/views/turmas/index.html.erb +++ /dev/null @@ -1,16 +0,0 @@ -

<%= notice %>

- -<% content_for :title, "Turmas" %> - -

Turmas

- -
- <% @turmas.each do |turma| %> - <%= render turma %> -

- <%= link_to "Show this turma", turma %> -

- <% end %> -
- -<%= link_to "New turma", new_turma_path %> diff --git a/project/app/views/turmas/index.json.jbuilder b/project/app/views/turmas/index.json.jbuilder deleted file mode 100644 index e70b9255db..0000000000 --- a/project/app/views/turmas/index.json.jbuilder +++ /dev/null @@ -1 +0,0 @@ -json.array! @turmas, partial: "turmas/turma", as: :turma diff --git a/project/app/views/turmas/new.html.erb b/project/app/views/turmas/new.html.erb deleted file mode 100644 index 477e3282ff..0000000000 --- a/project/app/views/turmas/new.html.erb +++ /dev/null @@ -1,11 +0,0 @@ -<% content_for :title, "New turma" %> - -

New turma

- -<%= render "form", turma: @turma %> - -
- -
- <%= link_to "Back to turmas", turmas_path %> -
diff --git a/project/app/views/turmas/show.html.erb b/project/app/views/turmas/show.html.erb deleted file mode 100644 index 72fb58239d..0000000000 --- a/project/app/views/turmas/show.html.erb +++ /dev/null @@ -1,10 +0,0 @@ -

<%= notice %>

- -<%= render @turma %> - -
- <%= link_to "Edit this turma", edit_turma_path(@turma) %> | - <%= link_to "Back to turmas", turmas_path %> - - <%= button_to "Destroy this turma", @turma, method: :delete %> -
diff --git a/project/app/views/turmas/show.json.jbuilder b/project/app/views/turmas/show.json.jbuilder deleted file mode 100644 index 1d164186aa..0000000000 --- a/project/app/views/turmas/show.json.jbuilder +++ /dev/null @@ -1 +0,0 @@ -json.partial! "turmas/turma", turma: @turma diff --git a/project/config/routes.rb b/project/config/routes.rb index b157ef7efd..75422f463b 100644 --- a/project/config/routes.rb +++ b/project/config/routes.rb @@ -2,32 +2,20 @@ resources :resposta resources :submissaos resources :questaos - + resources :formularios do member do get :exportar_csv end end - + resources :templates - resources :turmas - resources :professors - resources :admins - resources :alunos - resources :departamentos - # 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 + get "up" => "rails/health#show", as: :rails_health_check root "dashboard#show" - get "login", to: "sessions#new" + get "login", to: "sessions#new", as: :login post "login", to: "sessions#create" delete "logout", to: "sessions#destroy" diff --git a/project/db/migrate/20260616000002_consolidate_to_user_model.rb b/project/db/migrate/20260616000002_consolidate_to_user_model.rb new file mode 100644 index 0000000000..5fb6c44b5d --- /dev/null +++ b/project/db/migrate/20260616000002_consolidate_to_user_model.rb @@ -0,0 +1,28 @@ +class ConsolidateToUserModel < ActiveRecord::Migration[8.1] + def up + # --- course_classes: horário para exibição --- + add_column :course_classes, :time, :string + + # --- formularios: turma_id → course_class_id --- + remove_foreign_key :formularios, column: :turma_id + rename_column :formularios, :turma_id, :course_class_id + add_foreign_key :formularios, :course_classes, column: :course_class_id + + # --- submissaos: substituir polimórfico por user_id --- + remove_column :submissaos, :participant_id + remove_column :submissaos, :participant_type + add_reference :submissaos, :user, null: false, foreign_key: true + + # --- remover tabelas legadas (ordem respeita FKs) --- + drop_table :turma_alunos + drop_table :turmas + drop_table :professors + drop_table :admins + drop_table :alunos + drop_table :departamentos + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/project/db/schema.rb b/project/db/schema.rb index 111f1d6ea3..d4f420563d 100644 --- a/project/db/schema.rb +++ b/project/db/schema.rb @@ -10,28 +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_000001) do - create_table "admins", force: :cascade do |t| - t.datetime "created_at", null: false - t.integer "departamento_id", null: false - t.string "email" - t.string "name" - t.string "password_digest" - t.datetime "updated_at", null: false - t.string "username" - t.index ["departamento_id"], name: "index_admins_on_departamento_id" - end - - create_table "alunos", force: :cascade do |t| - t.string "course" - t.datetime "created_at", null: false - t.string "email" - t.string "matricula" - t.string "name" - t.string "password_digest" - t.datetime "updated_at", null: false - end - +ActiveRecord::Schema[8.1].define(version: 2026_06_16_000002) do create_table "class_memberships", force: :cascade do |t| t.integer "course_class_id", null: false t.datetime "created_at", null: false @@ -50,18 +29,12 @@ t.integer "department_id", null: false t.string "name" t.string "semester", null: false + t.string "time" t.datetime "updated_at", null: false t.index ["code", "class_code", "semester"], name: "index_course_classes_on_code_and_class_code_and_semester", unique: true t.index ["department_id"], name: "index_course_classes_on_department_id" end - create_table "departamentos", 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 "departments", force: :cascade do |t| t.string "code", null: false t.datetime "created_at", null: false @@ -72,16 +45,16 @@ create_table "formularios", force: :cascade do |t| t.integer "admin_id", null: false + t.integer "course_class_id", null: false t.datetime "created_at", null: false t.string "status" t.string "target_role" t.integer "template_id" t.string "title" - t.integer "turma_id", null: false t.datetime "updated_at", null: false t.index ["admin_id"], name: "index_formularios_on_admin_id" + t.index ["course_class_id"], name: "index_formularios_on_course_class_id" t.index ["template_id"], name: "index_formularios_on_template_id" - t.index ["turma_id"], name: "index_formularios_on_turma_id" end create_table "import_inconsistencies", force: :cascade do |t| @@ -92,18 +65,6 @@ t.datetime "updated_at", null: false end - create_table "professors", force: :cascade do |t| - t.datetime "created_at", null: false - t.integer "departamento_id", null: false - t.string "email" - t.string "formation" - t.string "matricula" - t.string "name" - t.string "password_digest" - t.datetime "updated_at", null: false - t.index ["departamento_id"], name: "index_professors_on_departamento_id" - end - create_table "questaos", force: :cascade do |t| t.datetime "created_at", null: false t.text "enunciado" @@ -129,11 +90,10 @@ create_table "submissaos", force: :cascade do |t| t.datetime "created_at", null: false t.integer "formulario_id", null: false - t.integer "participant_id", null: false - t.string "participant_type", null: false t.datetime "updated_at", null: false + t.integer "user_id", null: false t.index ["formulario_id"], name: "index_submissaos_on_formulario_id" - t.index ["participant_type", "participant_id"], name: "index_submissaos_on_participant" + t.index ["user_id"], name: "index_submissaos_on_user_id" end create_table "templates", force: :cascade do |t| @@ -145,29 +105,6 @@ t.index ["admin_id"], name: "index_templates_on_admin_id" end - create_table "turma_alunos", force: :cascade do |t| - t.integer "aluno_id", null: false - t.datetime "created_at", null: false - t.integer "turma_id", null: false - t.datetime "updated_at", null: false - t.index ["aluno_id"], name: "index_turma_alunos_on_aluno_id" - t.index ["turma_id"], name: "index_turma_alunos_on_turma_id" - end - - create_table "turmas", force: :cascade do |t| - t.string "class_code" - t.datetime "created_at", null: false - t.integer "departamento_id", null: false - t.integer "professor_id", null: false - t.string "semester" - t.string "subject_code" - t.string "subject_name" - t.string "time" - t.datetime "updated_at", null: false - t.index ["departamento_id"], name: "index_turmas_on_departamento_id" - t.index ["professor_id"], name: "index_turmas_on_professor_id" - end - create_table "users", force: :cascade do |t| t.datetime "created_at", null: false t.integer "department_id" @@ -188,23 +125,18 @@ t.index ["registration"], name: "index_users_on_registration", unique: true end - add_foreign_key "admins", "departamentos" add_foreign_key "class_memberships", "course_classes" add_foreign_key "class_memberships", "users" add_foreign_key "course_classes", "departments" + add_foreign_key "formularios", "course_classes" add_foreign_key "formularios", "templates" - add_foreign_key "formularios", "turmas" add_foreign_key "formularios", "users", column: "admin_id" - add_foreign_key "professors", "departamentos" add_foreign_key "questaos", "formularios" add_foreign_key "questaos", "templates" add_foreign_key "resposta", "questaos" add_foreign_key "resposta", "submissaos" add_foreign_key "submissaos", "formularios" + add_foreign_key "submissaos", "users" add_foreign_key "templates", "users", column: "admin_id" - add_foreign_key "turma_alunos", "alunos" - add_foreign_key "turma_alunos", "turmas" - add_foreign_key "turmas", "departamentos" - add_foreign_key "turmas", "professors" add_foreign_key "users", "departments" end diff --git a/project/db/seeds.rb b/project/db/seeds.rb index 38b35f1db0..fcda9a8f2c 100644 --- a/project/db/seeds.rb +++ b/project/db/seeds.rb @@ -1,29 +1,11 @@ -# Seed de desenvolvimento: cria um conjunto mínimo de dados para testar as -# telas (templates, formulários, turmas, alunos, professores, etc) na interface. -# Idempotente — pode ser rodado múltiplas vezes sem duplicar dados. - -# --------------------------------------------------------------------------- -# Departamentos (domínio legado) -# --------------------------------------------------------------------------- -departamento_cic = Departamento.find_or_create_by!(code: "CIC") do |d| - d.name = "Ciência da Computação" -end - -departamento_ene = Departamento.find_or_create_by!(code: "ENE") do |d| - d.name = "Engenharia Elétrica" -end +# Seed de desenvolvimento — pode ser executado múltiplas vezes (idempotente). -# --------------------------------------------------------------------------- -# Department (novo sistema de auth — tabela separada) -# --------------------------------------------------------------------------- -department_cic = Department.find_or_create_by!(code: "CIC") do |d| - d.name = "Ciência da Computação" -end +# --- Departamento (sistema novo) --- +department_cic = Department.find_or_create_by!(code: "CIC") { |d| d.name = "Ciência da Computação" } +department_ene = Department.find_or_create_by!(code: "ENE") { |d| d.name = "Engenharia Elétrica" } -# --------------------------------------------------------------------------- -# Usuário administrador (novo sistema — usado para login e posse de templates) -# --------------------------------------------------------------------------- -admin_user = User.find_or_create_by!(email: "admin.cic@unb.br") do |u| +# --- Usuário administrador --- +admin = User.find_or_create_by!(email: "admin.cic@unb.br") do |u| u.name = "Admin CAMAAR" u.registration = "000000001" u.role = :admin @@ -31,77 +13,48 @@ u.department = department_cic end -# --------------------------------------------------------------------------- -# Professores (domínio legado — relacionamento com Turma) -# --------------------------------------------------------------------------- -professor_ana = Professor.find_or_create_by!(matricula: "PROF0001") do |p| - p.name = "Ana Souza" - p.email = "ana.souza@unb.br" - p.formation = "Doutora em Ciência da Computação" - p.departamento_id = departamento_cic.id - p.password = "Senha@123" -end - -professor_bruno = Professor.find_or_create_by!(matricula: "PROF0002") do |p| - p.name = "Bruno Lima" - p.email = "bruno.lima@unb.br" - p.formation = "Doutor em Engenharia Elétrica" - p.departamento_id = departamento_ene.id - p.password = "Senha@123" -end - -# --------------------------------------------------------------------------- -# Alunos (domínio legado — relacionamento com TurmaAluno e Submissao) -# --------------------------------------------------------------------------- -alunos = [ - { matricula: "190000001", name: "Carla Mendes", email: "carla.mendes@aluno.unb.br", course: "Engenharia de Software" }, - { matricula: "190000002", name: "Diego Alves", email: "diego.alves@aluno.unb.br", course: "Engenharia de Software" }, - { matricula: "190000003", name: "Eduarda Rocha", email: "eduarda.rocha@aluno.unb.br", course: "Ciência da Computação" }, - { matricula: "190000004", name: "Felipe Castro", email: "felipe.castro@aluno.unb.br", course: "Ciência da Computação" } +# --- Usuários participantes --- +participantes = [ + { name: "Carla Mendes", email: "carla.mendes@aluno.unb.br", registration: "190000001", role: :participant }, + { name: "Diego Alves", email: "diego.alves@aluno.unb.br", registration: "190000002", role: :participant }, + { name: "Ana Souza", email: "ana.souza@unb.br", registration: "PROF0001", role: :participant }, + { name: "Bruno Lima", email: "bruno.lima@unb.br", registration: "PROF0002", role: :participant } ].map do |dados| - Aluno.find_or_create_by!(matricula: dados[:matricula]) do |a| - a.name = dados[:name] - a.email = dados[:email] - a.course = dados[:course] - a.password = "Senha@123" + User.find_or_create_by!(email: dados[:email]) do |u| + u.name = dados[:name] + u.registration = dados[:registration] + u.role = dados[:role] + u.password = "Senha@123" + u.department = department_cic end end -# --------------------------------------------------------------------------- -# Turmas -# --------------------------------------------------------------------------- -turma_estrutura_dados = Turma.find_or_create_by!(class_code: "CIC0097-T01") do |t| - t.subject_code = "CIC0097" - t.subject_name = "Estrutura de Dados" - t.semester = "2026.1" - t.time = "Seg/Qua 08:00-10:00" - t.departamento_id = departamento_cic.id - t.professor_id = professor_ana.id -end +carla, diego, ana_prof, bruno_prof = participantes -turma_circuitos = Turma.find_or_create_by!(class_code: "ENE0011-T01") do |t| - t.subject_code = "ENE0011" - t.subject_name = "Circuitos Elétricos" - t.semester = "2026.1" - t.time = "Ter/Qui 10:00-12:00" - t.departamento_id = departamento_ene.id - t.professor_id = professor_bruno.id +# --- Turmas (CourseClass) --- +turma_ed = CourseClass.find_or_create_by!(code: "CIC0097", class_code: "T01", semester: "2026.1") do |t| + t.name = "Estrutura de Dados" + t.time = "Seg/Qua 08:00-10:00" + t.department = department_cic end -[ alunos[0], alunos[1], alunos[2] ].each do |aluno| - TurmaAluno.find_or_create_by!(turma_id: turma_estrutura_dados.id, aluno_id: aluno.id) +turma_circ = CourseClass.find_or_create_by!(code: "ENE0011", class_code: "T01", semester: "2026.1") do |t| + t.name = "Circuitos Elétricos" + t.time = "Ter/Qui 10:00-12:00" + t.department = department_ene end -[ alunos[2], alunos[3] ].each do |aluno| - TurmaAluno.find_or_create_by!(turma_id: turma_circuitos.id, aluno_id: aluno.id) -end +# --- Vínculos de membros (ClassMembership) --- +ClassMembership.find_or_create_by!(user: ana_prof, course_class: turma_ed, role: :docente) +ClassMembership.find_or_create_by!(user: bruno_prof, course_class: turma_circ, role: :docente) +ClassMembership.find_or_create_by!(user: carla, course_class: turma_ed, role: :discente) +ClassMembership.find_or_create_by!(user: diego, course_class: turma_ed, role: :discente) +ClassMembership.find_or_create_by!(user: carla, course_class: turma_circ, role: :discente) -# --------------------------------------------------------------------------- -# Templates (admin_id agora referencia users.id) -# --------------------------------------------------------------------------- +# --- Templates --- template_discente = Template.find_or_create_by!(title: "Avaliação de Disciplina - Discentes") do |t| t.target_role = "discente" - t.admin_id = admin_user.id + t.admin_id = admin.id end if template_discente.questaos.empty? @@ -112,7 +65,7 @@ template_docente = Template.find_or_create_by!(title: "Autoavaliação - Docentes") do |t| t.target_role = "docente" - t.admin_id = admin_user.id + t.admin_id = admin.id end if template_docente.questaos.empty? @@ -120,40 +73,32 @@ template_docente.questaos.create!(enunciado: "Quais dificuldades você encontrou no semestre?", tipo: "text") end -# --------------------------------------------------------------------------- -# Formulário de exemplo -# --------------------------------------------------------------------------- +# --- Formulário de exemplo --- formulario = Formulario.find_or_create_by!(title: "Avaliação Estrutura de Dados - 2026.1") do |f| - f.target_role = template_discente.target_role - f.admin_id = admin_user.id - f.template_id = template_discente.id - f.turma_id = turma_estrutura_dados.id + f.target_role = template_discente.target_role + f.admin_id = admin.id + f.template_id = template_discente.id + f.course_class_id = turma_ed.id end if formulario.questaos.empty? - template_discente.questaos.each do |questao| - formulario.questaos.create!(enunciado: questao.enunciado, tipo: questao.tipo) - end + template_discente.questaos.each { |q| formulario.questaos.create!(enunciado: q.enunciado, tipo: q.tipo) } end -# --------------------------------------------------------------------------- -# Submissão de exemplo -# --------------------------------------------------------------------------- -submissao = Submissao.find_or_create_by!(formulario_id: formulario.id, participant: alunos[0]) +# --- Submissão de exemplo --- +submissao = Submissao.find_or_create_by!(formulario: formulario, user: carla) if Respostum.where(submissao_id: submissao.id).empty? - formulario.questaos.each do |questao| - if questao.tipo == "rating" - Respostum.create!(submissao_id: submissao.id, questao_id: questao.id, valor_numerico: 5) - else - Respostum.create!(submissao_id: submissao.id, questao_id: questao.id, valor_texto: "Resposta de exemplo") - end + formulario.questaos.each do |q| + Respostum.create!(submissao_id: submissao.id, questao_id: q.id, + valor_numerico: (q.tipo == "rating" ? 5 : nil), + valor_texto: (q.tipo != "rating" ? "Resposta de exemplo" : nil)) end end -# --------------------------------------------------------------------------- puts "Seed concluída:" -puts " Login admin: email=#{admin_user.email} / senha=Senha@123" -puts " Professores: #{Professor.count} | Alunos: #{Aluno.count} | Turmas: #{Turma.count}" -puts " Templates: #{Template.count} | Formulários: #{Formulario.count}" -puts " Submissões: #{Submissao.count} | Respostas: #{Respostum.count}" +puts " Admin: #{admin.email} / Senha@123" +puts " Participantes: #{User.participant.count} (#{User.participant.pluck(:email).join(', ')})" +puts " Turmas: #{CourseClass.count}" +puts " Templates: #{Template.count} | Formulários: #{Formulario.count}" +puts " Submissões: #{Submissao.count} | Respostas: #{Respostum.count}" diff --git a/project/features/step_definitions/formularios_steps.rb b/project/features/step_definitions/formularios_steps.rb index 7f39d03915..766bb90b66 100644 --- a/project/features/step_definitions/formularios_steps.rb +++ b/project/features/step_definitions/formularios_steps.rb @@ -21,7 +21,7 @@ Quando("selecionar o template e a turma desejada") do select @template.title, from: "formulario[template_id]" - select @turma.subject_name, from: "formulario[turma_id]" + select @turma.name, from: "formulario[course_class_id]" end Quando("clicar no botão 'Enviar'") do @@ -53,7 +53,7 @@ Quando("eu tentar criar um formulário sem selecionar nenhum template") do fill_in "formulario[title]", with: "Formulário sem template" - select @turma.subject_name, from: "formulario[turma_id]" + select @turma.name, from: "formulario[course_class_id]" click_button "Enviar" end diff --git a/project/features/step_definitions/templates_edicao_steps.rb b/project/features/step_definitions/templates_edicao_steps.rb index d83842a53d..d2a07f3d26 100644 --- a/project/features/step_definitions/templates_edicao_steps.rb +++ b/project/features/step_definitions/templates_edicao_steps.rb @@ -6,7 +6,7 @@ # Formulário gerado a partir da versão ATUAL do template, com questões já clonadas. turma = criar_turma - @formulario = @admin.formularios.create!(title: "Formulário Antigo", template_id: @template.id, turma_id: turma.id, target_role: @template.target_role) + @formulario = @admin.formularios.create!(title: "Formulário Antigo", template_id: @template.id, course_class_id: turma.id, target_role: @template.target_role) @template.questaos.each do |questao| @formulario.questaos.create!(enunciado: questao.enunciado, tipo: questao.tipo) end @@ -38,7 +38,7 @@ @template = criar_template_com_questao(@admin) turma = criar_turma - @formulario = @admin.formularios.create!(title: "Formulário Gerado", template_id: @template.id, turma_id: turma.id, target_role: @template.target_role) + @formulario = @admin.formularios.create!(title: "Formulário Gerado", template_id: @template.id, course_class_id: turma.id, target_role: @template.target_role) @template.questaos.each do |questao| @formulario.questaos.create!(enunciado: questao.enunciado, tipo: questao.tipo) end diff --git a/project/features/support/test_helpers.rb b/project/features/support/test_helpers.rb index 38cce725cd..2de7787736 100644 --- a/project/features/support/test_helpers.rb +++ b/project/features/support/test_helpers.rb @@ -1,7 +1,6 @@ module TestHelpers def criar_admin sufixo = SecureRandom.hex(4) - User.create!( name: "Admin Teste #{sufixo}", email: "admin#{sufixo}@teste.com", @@ -25,27 +24,15 @@ def criar_template_com_questao(admin, titulo: "Template #{SecureRandom.hex(4)}", end def criar_turma - departamento = Departamento.find_or_create_by!(code: "DEPT001") do |d| - d.name = "Departamento de Teste" - end + department = Department.find_or_create_by!(code: "DEPT001") { |d| d.name = "Departamento de Teste" } sufixo = SecureRandom.hex(4) - - professor = Professor.create!( - name: "Professor Teste #{sufixo}", - email: "professor#{sufixo}@teste.com", - matricula: "PROF#{sufixo}", - password: "senha12345", - departamento_id: departamento.id - ) - - Turma.create!( - class_code: "TURMA#{sufixo}", - subject_code: "DISC#{sufixo}", - subject_name: "Disciplina Teste", + CourseClass.create!( + code: "CIC#{sufixo}", + class_code: "T#{sufixo}", + name: "Disciplina Teste #{sufixo}", semester: "2026.1", time: "10:00", - departamento_id: departamento.id, - professor_id: professor.id + department: department ) end end diff --git a/project/spec/factories/admins.rb b/project/spec/factories/admins.rb deleted file mode 100644 index 7f132bf962..0000000000 --- a/project/spec/factories/admins.rb +++ /dev/null @@ -1,8 +0,0 @@ -FactoryBot.define do - factory :admin do - departamento - sequence(:username) { |n| "admin#{n}" } - sequence(:email) { |n| "admin#{n}@teste.com" } - name { "Admin Teste" } - end -end diff --git a/project/spec/factories/alunos.rb b/project/spec/factories/alunos.rb deleted file mode 100644 index d59b7976df..0000000000 --- a/project/spec/factories/alunos.rb +++ /dev/null @@ -1,9 +0,0 @@ -FactoryBot.define do - factory :aluno do - sequence(:matricula) { |n| "19#{n.to_s.rjust(7, '0')}" } - sequence(:email) { |n| "aluno#{n}@teste.com" } - name { "Aluno Teste" } - course { "Engenharia de Software" } - password { "senha12345" } - end -end diff --git a/project/spec/factories/course_classes.rb b/project/spec/factories/course_classes.rb new file mode 100644 index 0000000000..b30bcb8aff --- /dev/null +++ b/project/spec/factories/course_classes.rb @@ -0,0 +1,10 @@ +FactoryBot.define do + factory :course_class do + association :department + sequence(:code) { |n| "CIC#{n.to_s.rjust(4, '0')}" } + sequence(:class_code) { |n| "T#{n}" } + name { "Disciplina Teste" } + semester { "2026.1" } + time { "10:00" } + end +end diff --git a/project/spec/factories/departamentos.rb b/project/spec/factories/departamentos.rb deleted file mode 100644 index eb651b90de..0000000000 --- a/project/spec/factories/departamentos.rb +++ /dev/null @@ -1,6 +0,0 @@ -FactoryBot.define do - factory :departamento do - sequence(:code) { |n| "DEPT#{n.to_s.rjust(3, '0')}" } - name { "Departamento de Teste" } - end -end diff --git a/project/spec/factories/departments.rb b/project/spec/factories/departments.rb new file mode 100644 index 0000000000..f65cc6885c --- /dev/null +++ b/project/spec/factories/departments.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :department do + sequence(:code) { |n| "DEPT#{n}" } + sequence(:name) { |n| "Departamento #{n}" } + end +end diff --git a/project/spec/factories/formularios.rb b/project/spec/factories/formularios.rb index 94543243c3..a8015a9544 100644 --- a/project/spec/factories/formularios.rb +++ b/project/spec/factories/formularios.rb @@ -2,7 +2,7 @@ factory :formulario do association :admin, factory: %i[user admin] template - turma + association :course_class sequence(:title) { |n| "Formulário de Teste #{n}" } target_role { "discente" } diff --git a/project/spec/factories/professors.rb b/project/spec/factories/professors.rb deleted file mode 100644 index 7110ec051c..0000000000 --- a/project/spec/factories/professors.rb +++ /dev/null @@ -1,10 +0,0 @@ -FactoryBot.define do - factory :professor do - departamento - sequence(:matricula) { |n| "PROF#{n.to_s.rjust(4, '0')}" } - sequence(:email) { |n| "professor#{n}@teste.com" } - name { "Professor Teste" } - formation { "Doutor em Ciência da Computação" } - password { "senha12345" } - end -end diff --git a/project/spec/factories/submissaos.rb b/project/spec/factories/submissaos.rb index 2e227ef033..4ee953fde5 100644 --- a/project/spec/factories/submissaos.rb +++ b/project/spec/factories/submissaos.rb @@ -1,10 +1,6 @@ FactoryBot.define do factory :submissao do formulario - association :participant, factory: :aluno - - trait :de_professor do - association :participant, factory: :professor - end + association :user end end diff --git a/project/spec/factories/turma_alunos.rb b/project/spec/factories/turma_alunos.rb deleted file mode 100644 index 89c3155677..0000000000 --- a/project/spec/factories/turma_alunos.rb +++ /dev/null @@ -1,6 +0,0 @@ -FactoryBot.define do - factory :turma_aluno do - turma - aluno - end -end diff --git a/project/spec/factories/turmas.rb b/project/spec/factories/turmas.rb deleted file mode 100644 index 266bc2ce1a..0000000000 --- a/project/spec/factories/turmas.rb +++ /dev/null @@ -1,11 +0,0 @@ -FactoryBot.define do - factory :turma do - departamento - professor - sequence(:class_code) { |n| "TURMA#{n}" } - sequence(:subject_code) { |n| "DISC#{n}" } - subject_name { "Disciplina Teste" } - semester { "2026.1" } - time { "10:00" } - end -end diff --git a/project/spec/mailers/user_mailer_spec.rb b/project/spec/mailers/user_mailer_spec.rb index 1049e41719..808e1d9ff7 100644 --- a/project/spec/mailers/user_mailer_spec.rb +++ b/project/spec/mailers/user_mailer_spec.rb @@ -2,7 +2,7 @@ RSpec.describe UserMailer do it "envia link de definicao de senha" do - user = create_user(email: "acjpjvjp@gmail.com", registration: "190084006") + user = create(:user, email: "acjpjvjp@gmail.com", registration: "190084006", password: nil) email = described_class.password_setup(user, "token-123") expect(email.to).to include("acjpjvjp@gmail.com") @@ -11,7 +11,7 @@ end it "envia link de redefinicao de senha" do - user = create_user(email: "acjpjvjp@gmail.com", registration: "190084006", password: "Senha@123") + user = create(:user, email: "acjpjvjp@gmail.com", registration: "190084006", password: "Senha@123") email = described_class.password_reset(user, "token-123") expect(email.to).to include("acjpjvjp@gmail.com") diff --git a/project/spec/models/formulario_spec.rb b/project/spec/models/formulario_spec.rb index 1ad7e7a724..96ed594bbb 100644 --- a/project/spec/models/formulario_spec.rb +++ b/project/spec/models/formulario_spec.rb @@ -21,17 +21,17 @@ end it "exige uma turma ao ser criado (#103)" do - formulario = build(:formulario, turma: nil) + formulario = build(:formulario, course_class: nil) expect(formulario).not_to be_valid - expect(formulario.errors[:turma_id]).to include("É necessário escolher ao menos uma turma") + expect(formulario.errors[:course_class_id]).to include("É necessário escolher ao menos uma turma") end it "não exige template nem turma em atualizações (#112)" do formulario = create(:formulario) formulario.template_id = nil - formulario.turma_id = nil + formulario.course_class_id = nil expect(formulario).to be_valid end @@ -55,7 +55,7 @@ expect(formulario.admin).to be_present expect(formulario.template).to be_present - expect(formulario.turma).to be_present + expect(formulario.course_class).to be_present end it "destrói suas próprias questões (clonadas do template) ao ser destruído" do diff --git a/project/spec/services/class_members_importer_spec.rb b/project/spec/requests/class_members_importer_spec.rb similarity index 94% rename from project/spec/services/class_members_importer_spec.rb rename to project/spec/requests/class_members_importer_spec.rb index 3b77969f88..7b4bdf1b39 100644 --- a/project/spec/services/class_members_importer_spec.rb +++ b/project/spec/requests/class_members_importer_spec.rb @@ -1,7 +1,7 @@ require "rails_helper" RSpec.describe Sigaa::ClassMembersImporter do - let!(:admin) { create_admin } + let!(:admin) { create(:user, :admin) } let(:path) { Rails.root.join("tmp", "spec_class_members.json") } after do @@ -39,12 +39,8 @@ end it "nao duplica usuario ja cadastrado e mantem uma unica conta por matricula e email" do - create_user( - name: "Ana", - email: "acjpjvjp@gmail.com", - registration: "190084006", - password: "Senha@123" - ) + create(:user, name: "Ana", email: "acjpjvjp@gmail.com", registration: "190084006", password: "Senha@123") + write_import_file([ { "code" => "CIC0097", diff --git a/project/spec/requests/formularios_spec.rb b/project/spec/requests/formularios_spec.rb index 5d3fe670fc..60deedd7ba 100644 --- a/project/spec/requests/formularios_spec.rb +++ b/project/spec/requests/formularios_spec.rb @@ -20,12 +20,12 @@ admin = create(:user, :admin) template = create(:template, admin: admin) create(:questao, template: template, enunciado: "O professor foi claro?") - turma = create(:turma) + course_class = create(:course_class) login_as(admin) expect { post formularios_path, params: { - formulario: { title: "Avaliação do Semestre", template_id: template.id, turma_id: turma.id } + formulario: { title: "Avaliação do Semestre", template_id: template.id, course_class_id: course_class.id } } }.to change(Formulario, :count).by(1).and change(Questao, :count).by(1) @@ -41,12 +41,12 @@ it "não cria o formulário quando nenhum template é selecionado" do admin = create(:user, :admin) - turma = create(:turma) + course_class = create(:course_class) login_as(admin) expect { post formularios_path, params: { - formulario: { title: "Sem template", turma_id: turma.id } + formulario: { title: "Sem template", course_class_id: course_class.id } } }.not_to change(Formulario, :count) diff --git a/project/spec/requests/password_resets_spec.rb b/project/spec/requests/password_resets_spec.rb index dca4aafc02..db768336cb 100644 --- a/project/spec/requests/password_resets_spec.rb +++ b/project/spec/requests/password_resets_spec.rb @@ -2,12 +2,7 @@ RSpec.describe "Redefinicao de senha" do let!(:user) do - create_user( - name: "Ana Clara Jordao Perna", - email: "acjpjvjp@gmail.com", - registration: "190084006", - password: "Senha@123" - ) + create(:user, name: "Ana Clara Jordao Perna", email: "acjpjvjp@gmail.com", registration: "190084006", password: "Senha@123") end it "redefine a senha com token valido e permite login com a nova senha" do diff --git a/project/spec/requests/password_setups_spec.rb b/project/spec/requests/password_setups_spec.rb index 20f3bad7af..0a6c5ef19a 100644 --- a/project/spec/requests/password_setups_spec.rb +++ b/project/spec/requests/password_setups_spec.rb @@ -2,11 +2,7 @@ RSpec.describe "Definicao de senha" do let(:user) do - create_user( - name: "Ana Clara Jordao Perna", - email: "acjpjvjp@gmail.com", - registration: "190084006" - ) + create(:user, name: "Ana Clara Jordao Perna", email: "acjpjvjp@gmail.com", registration: "190084006", password: nil) end it "define a primeira senha com link valido e permite login" do diff --git a/project/spec/requests/sessions_spec.rb b/project/spec/requests/sessions_spec.rb index 4574a3f627..da5d8bbe3b 100644 --- a/project/spec/requests/sessions_spec.rb +++ b/project/spec/requests/sessions_spec.rb @@ -2,18 +2,9 @@ RSpec.describe "Login" do before do - create_admin - create_user( - name: "Ana Clara Jordao Perna", - email: "acjpjvjp@gmail.com", - registration: "190084006", - password: "Senha@123" - ) - create_user( - name: "Andre Carvalho de Roure", - email: "andreCarvalhoroure@gmail.com", - registration: "200033522" - ) + create(:user, :admin, name: "Admin CIC", email: "admin.cic@unb.br", registration: "000000001") + create(:user, name: "Ana Clara Jordao Perna", email: "acjpjvjp@gmail.com", registration: "190084006", password: "Senha@123") + create(:user, name: "Andre Carvalho de Roure", email: "andreCarvalhoroure@gmail.com", registration: "200033522", password: nil) end it "autentica administrador com email e exibe menu administrativo" do diff --git a/project/spec/requests/templates_spec.rb b/project/spec/requests/templates_spec.rb index 3e3ebe0340..17293e3a54 100644 --- a/project/spec/requests/templates_spec.rb +++ b/project/spec/requests/templates_spec.rb @@ -115,9 +115,9 @@ admin = create(:user, :admin) template = create(:template, admin: admin) create(:questao, template: template, enunciado: "Pergunta original") - turma = create(:turma) + course_class = create(:course_class) - formulario = create(:formulario, admin: admin, template: template, turma: turma, title: "Formulário Gerado", target_role: template.target_role) + formulario = create(:formulario, admin: admin, template: template, course_class: course_class, title: "Formulário Gerado", target_role: template.target_role) template.questaos.each { |questao| formulario.questaos.create!(enunciado: questao.enunciado, tipo: questao.tipo) } questao_existente = template.questaos.first @@ -165,9 +165,9 @@ it "remove o template, desvincula formulários gerados e mantém suas questões clonadas" do admin = create(:user, :admin) template = create(:template, :com_questao, admin: admin) - turma = create(:turma) + course_class = create(:course_class) - formulario = create(:formulario, admin: admin, template: template, turma: turma, title: "Formulário Gerado", target_role: template.target_role) + formulario = create(:formulario, admin: admin, template: template, course_class: course_class, title: "Formulário Gerado", target_role: template.target_role) template.questaos.each { |questao| formulario.questaos.create!(enunciado: questao.enunciado, tipo: questao.tipo) } login_as(admin) diff --git a/project/spec/support/auth_helpers.rb b/project/spec/support/auth_helpers.rb index 7d0dfc917c..3cdb08ea4b 100644 --- a/project/spec/support/auth_helpers.rb +++ b/project/spec/support/auth_helpers.rb @@ -1,29 +1,4 @@ module AuthHelpers - def create_department(code: "CIC") - Department.find_or_create_by!(code:) { |department| department.name = code } - end - - def create_user(name: "Usuario Teste", email: "user@example.com", registration: "123", role: :participant, password: nil) - User.create!( - name:, - email:, - registration:, - role:, - department: create_department, - password: - ) - end - - def create_admin - create_user( - name: "Admin CIC", - email: "admin.cic@unb.br", - registration: "000000001", - role: :admin, - password: "Senha@123" - ) - end - def login_as(user, password: "Senha@123") post login_path, params: { session: { identifier: user.email, password: } } end From 331ae534b9d67ccdcf68efb9c83fa3a04875c2c0 Mon Sep 17 00:00:00 2001 From: Gabriel Lopes Soares Damasceno Date: Tue, 16 Jun 2026 21:22:59 -0300 Subject: [PATCH 20/23] =?UTF-8?q?Feat:=20listagem,=20resposta=20de=20aluno?= =?UTF-8?q?s=20e=20exporta=C3=A7=C3=A3o=20CSV?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/controllers/formularios_controller.rb | 67 +++--- .../app/controllers/resposta_controller.rb | 31 ++- project/app/views/dashboard/show.html.erb | 6 +- project/app/views/formularios/index.html.erb | 47 +++- project/app/views/formularios/show.html.erb | 46 +++- .../step_definitions/formularios_steps.rb | 202 ++++++++++++++---- project/features/support/env.rb | 3 + 7 files changed, 302 insertions(+), 100 deletions(-) diff --git a/project/app/controllers/formularios_controller.rb b/project/app/controllers/formularios_controller.rb index 7acfd0c6dc..5010e2ff8c 100644 --- a/project/app/controllers/formularios_controller.rb +++ b/project/app/controllers/formularios_controller.rb @@ -1,36 +1,54 @@ class FormulariosController < ApplicationController - before_action :require_admin! before_action :set_formulario, only: %i[ show edit update destroy exportar_csv ] - before_action :set_select_options, only: %i[ new create edit update ] + # GET /formularios or /formularios.json def index - @formularios = current_admin.formularios + # Issue 110: Visualizar os formulários criados (Listagem geral) + @formularios = Formulario.all + + # Issue 109: Visualizar os formulários não respondidos das turmas + if current_user + # 1. Mapeia quais formulários o usuário atual já respondeu + formularios_respondidos_ids = Submissao.where(user_id: current_user.id).select(:formulario_id) + + # 2. Busca os formulários que pertencem às turmas do usuário E que não estão na lista de respondidos + @formularios_pendentes = Formulario + .where(course_class_id: current_user.course_classes.select(:id)) + .where.not(id: formularios_respondidos_ids) + else + @formularios_pendentes = [] + end end + # GET /formularios/1 or /formularios/1.json def show end + # GET /formularios/new def new @formulario = Formulario.new end + # GET /formularios/1/edit def edit end + # POST /formularios or /formularios.json def create @formulario = Formulario.new(formulario_params) - @formulario.target_role ||= @formulario.template&.target_role - if @formulario.save - @formulario.template.questaos.each do |questao| - @formulario.questaos.create!(enunciado: questao.enunciado, tipo: questao.tipo) + respond_to do |format| + if @formulario.save + format.html { redirect_to @formulario, notice: "Formulario was successfully created." } + format.json { render :show, status: :created, location: @formulario } + else + format.html { render :new, status: :unprocessable_content } + format.json { render json: @formulario.errors, status: :unprocessable_content } end - redirect_to formularios_path, notice: "Formulário gerado com sucesso!" - else - render :new, status: :unprocessable_content end end + # PATCH/PUT /formularios/1 or /formularios/1.json def update respond_to do |format| if @formulario.update(formulario_params) @@ -43,38 +61,35 @@ def update end end + # DELETE /formularios/1 or /formularios/1.json def destroy @formulario.destroy! + respond_to do |format| format.html { redirect_to formularios_path, notice: "Formulario was successfully destroyed.", status: :see_other } format.json { head :no_content } end end + # GET /formularios/1/exportar_csv def exportar_csv require 'csv' + csv_data = CSV.generate(headers: true) do |csv| csv << ["ID da Pergunta", "Enunciado", "Scores Atribuídos"] end - send_data csv_data, - filename: "respostas_formulario_#{@formulario.id}.csv", + + send_data csv_data, + filename: "respostas_formulario_#{@formulario.id}.csv", type: "text/csv" end private + def set_formulario + @formulario = Formulario.find(params.expect(:id)) + end - def set_formulario - @formulario = Formulario.find(params.expect(:id)) - end - - def formulario_params - attrs = params.expect(formulario: [ :title, :target_role, :status, :template_id, :course_class_id ]) - attrs[:admin_id] = current_admin.id - attrs - end - - def set_select_options - @templates = current_admin.templates - @course_classes = CourseClass.all - end + def formulario_params + params.expect(formulario: [ :title, :target_role, :status, :template_id, :course_class_id, :admin_id ]) + end end diff --git a/project/app/controllers/resposta_controller.rb b/project/app/controllers/resposta_controller.rb index d3daae1769..f7be379ec1 100644 --- a/project/app/controllers/resposta_controller.rb +++ b/project/app/controllers/resposta_controller.rb @@ -21,16 +21,29 @@ def edit # POST /resposta or /resposta.json def create - @respostum = Respostum.new(respostum_params) + # Caminho Triste: Valida se o campo de nota foi deixado em branco pelo robô/usuário + if params.dig(:respostum, :valor_numerico).blank? + redirect_to formulario_path(params[:respostum][:formulario_id]), alert: "Por favor, preencha todas as questões obrigatórias" + return + end - respond_to do |format| - if @respostum.save - format.html { redirect_to @respostum, notice: "Respostum was successfully created." } - format.json { render :show, status: :created, location: @respostum } - else - format.html { render :new, status: :unprocessable_content } - format.json { render json: @respostum.errors, status: :unprocessable_content } - end + # Caminho Feliz: Cria a submissão vinculando o usuário e o formulário (idêntico ao banco) + @submissao = Submissao.find_or_create_by!( + user_id: current_user.id, + formulario_id: params[:respostum][:formulario_id] + ) + + # Cria a resposta vinculando à questão e à submissão recém-criada + @respostum = Respostum.new( + valor_numerico: params[:respostum][:valor_numerico], + questao_id: params[:respostum][:questao_id], + submissao_id: @submissao.id + ) + + if @respostum.save + redirect_to formularios_path, notice: "Avaliação submetida com sucesso" + else + redirect_to formulario_path(params[:respostum][:formulario_id]), alert: "Por favor, preencha todas as questões obrigatórias" end end diff --git a/project/app/views/dashboard/show.html.erb b/project/app/views/dashboard/show.html.erb index 9a164b8c6b..73a580049e 100644 --- a/project/app/views/dashboard/show.html.erb +++ b/project/app/views/dashboard/show.html.erb @@ -6,13 +6,13 @@ <% if current_user.admin? %> <% else %> <% end %> diff --git a/project/app/views/formularios/index.html.erb b/project/app/views/formularios/index.html.erb index 2eae679670..6be0f9760a 100644 --- a/project/app/views/formularios/index.html.erb +++ b/project/app/views/formularios/index.html.erb @@ -1,17 +1,44 @@

<%= notice %>

<%= alert %>

-<% content_for :title, "Formulários" %> -

Formulários

-
- <% @formularios.each do |formulario| %> - <%= render formulario %> -

- <%= link_to "Ver formulário", formulario %> -

+<% if current_user.admin? %> + <% if @formularios.empty? %> +

Você ainda não possui formulários criados para as suas turmas.

+ <% else %> +
+ <% @formularios.each do |formulario| %> +
+ <%= formulario.title %> | + Turma: <%= formulario.course_class&.name %> | + Status: <%= formulario.status %> +
+ <%= link_to "Ver Detalhes", formulario %> +
+
+ <% end %> +
<% end %> -
+ +
+ <%= link_to "Novo formulário", new_formulario_path %> -<%= link_to "Novo formulário", new_formulario_path %> +<% else %> +

Avaliações Pendentes

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

Você não possui nenhuma avaliação pendente no momento.

+ <% else %> +
+ <% @formularios_pendentes.each do |formulario| %> +
+ <%= formulario.title %> +
+ <%= link_to "Responder Avaliação", formulario %> +
+
+ <% end %> +
+ <% end %> +<% end %> diff --git a/project/app/views/formularios/show.html.erb b/project/app/views/formularios/show.html.erb index 14b47a5b42..74a78c837e 100644 --- a/project/app/views/formularios/show.html.erb +++ b/project/app/views/formularios/show.html.erb @@ -1,14 +1,42 @@

<%= notice %>

+

<%= alert %>

-<%= render @formulario %> +

<%= @formulario.title %>

+

Turma: <%= @formulario.course_class&.name %>

-
- <%= link_to "Exportar Resultados (CSV)", exportar_csv_formulario_path(@formulario), style: "padding: 10px 15px; background-color: #4CAF50; color: white; text-decoration: none; border-radius: 5px; font-weight: bold;" %> -
+
-
- <%= link_to "Edit this formulario", edit_formulario_path(@formulario) %> | - <%= link_to "Back to formularios", formularios_path %> +<% if current_user.admin? %> + <%# -- VISÃO DO ADMINISTRADOR (Issue 4) -- %> +

Resultados

+ + <% if Submissao.where(formulario_id: @formulario.id).any? %> + <%= link_to "Exportar Resultados (CSV)", exportar_csv_formulario_path(@formulario, format: :csv) %> + <% else %> +

Não é possível exportar o relatório pois nenhuma resposta foi registrada. Aguarde os participantes responderem.

+ <% end %> + +

+ <%= link_to "Voltar para listagem", formularios_path %> - <%= button_to "Destroy this formulario", @formulario, method: :delete %> -
+<% else %> + <%# -- VISÃO DO PARTICIPANTE RESPONDENDO (Issue 3) -- %> +

Responda o Questionário

+ + <%= form_with scope: :respostum, url: resposta_path, method: :post do |f| %> + <%# Atributos ocultos necessários para sincronizar com as colunas do banco %> + <%= f.hidden_field :formulario_id, value: @formulario.id %> + <%= f.hidden_field :questao_id, value: @formulario.questaos.first&.id %> + +
+ <%= f.label :valor_numerico, "Score" %>
+ <%= f.number_field :valor_numerico %> +
+ +
+ <%= f.submit "Enviar Resposta" %> + <% end %> + +
+ <%= link_to "Voltar", formularios_path %> +<% end %> \ No newline at end of file diff --git a/project/features/step_definitions/formularios_steps.rb b/project/features/step_definitions/formularios_steps.rb index 766bb90b66..9a2b18a986 100644 --- a/project/features/step_definitions/formularios_steps.rb +++ b/project/features/step_definitions/formularios_steps.rb @@ -1,72 +1,188 @@ -# Step definitions para features/admin_criar_formulario.feature (#103) +# frozen_string_literal: true -Dado("que sou administrador") do - @admin = criar_admin + +# CONTEXTO GERAL E AUTENTICAÇÃO + + +Dado('que estou logado no sistema como {string}') do |perfil| + trait = perfil == 'Administrador' ? :admin : nil + @user = trait ? create(:user, trait) : create(:user) + + visit '/login' + fill_in 'Email ou matricula', with: @user.email + fill_in 'Senha', with: 'Senha@123' + click_button 'Entrar' +end + +Dado('que estou autenticado no sistema como participante') do + step 'que estou logado no sistema como "Participante"' +end + + +# ISSUE 110: VISUALIZAÇÃO DE FORMULÁRIOS (ADMINISTRADOR) + + +Quando('eu clico na aba de {string} no menu lateral') do |aba| + nome_do_link = aba == "Meus Formulários" ? "Gerenciar formularios" : aba + click_link nome_do_link +end + +E('existem formulários previamente criados por mim no sistema') do + @formularios = create_list(:formulario, 3, admin: @user) + visit current_path +end + +E('eu ainda não criei nenhum formulário no sistema') do + expect(Formulario.where(admin: @user).count).to eq(0) +end + +Então('eu devo ver uma lista com todos os meus formulários publicados') do + @formularios.each do |formulario| + expect(page).to have_content(formulario.title) + end +end + +E('a lista deve exibir o título, a turma e o status de cada formulário') do + @formularios.each do |formulario| + expect(page).to have_content(formulario.title) + expect(page).to have_content(formulario.course_class.name) + expect(page).to have_content(formulario.status || 'Ativo') + end +end + +Então('a lista de formulários deve aparecer vazia') do + expect(page).not_to have_css('.formulario-item') +end + + +# ISSUE 109: FORMULÁRIOS PENDENTES (PARTICIPANTE) + + +Quando('eu acesso a página inicial do meu painel \(Dashboard)') do + visit formularios_path end -Dado("existe pelo menos um template criado por mim") do - @template = criar_template_com_questao(@admin) +E('existem formulários ativos das minhas turmas que eu ainda não respondi') do + @turma = create(:course_class) + @user.course_classes << @turma + @formulario_pendente = create(:formulario, course_class: @turma) + visit current_path end -Dado("existe pelo menos uma turma cadastrada") do - @turma = criar_turma +E('eu já respondi a todos os formulários ativos das minhas turmas') do + @turma = create(:course_class) + @user.course_classes << @turma + formulario = create(:formulario, course_class: @turma) + + create(:submissao, formulario: formulario, user: @user) + visit current_path end -Quando("eu preencher o nome do formulário") do - @formulario_titulo = "Formulário Avaliação #{SecureRandom.hex(4)}" - fazer_login_como(@admin) - visit new_formulario_path - fill_in "formulario[title]", with: @formulario_titulo +Então('o sistema deve listar os formulários pendentes na seção {string}') do |secao| + expect(page).to have_content(secao) + expect(page).to have_content(@formulario_pendente.title) end -Quando("selecionar o template e a turma desejada") do - select @template.title, from: "formulario[template_id]" - select @turma.name, from: "formulario[course_class_id]" +Então('a seção {string} não deve listar nenhum formulário') do |secao| + expect(page).to have_content(secao) end -Quando("clicar no botão 'Enviar'") do - click_button "Enviar" +E('cada item da lista deve ter um botão {string}') do |nome_botao| + expect(page).to have_link(nome_botao) end -Então("devo ver uma mensagem dizendo 'Formulário gerado com sucesso!'") do - expect(page).to have_text("Formulário gerado com sucesso!") + +# ISSUE 99: RESPONDER QUESTIONÁRIO + + +E('que estou na página do formulário não respondido da minha turma') do + @turma = create(:course_class) + @user.course_classes << @turma + @formulario = create(:formulario, :com_questao, course_class: @turma) + + visit formulario_path(@formulario) end -Então("as questões do template devem ser clonadas para o novo formulário") do - formulario = Formulario.find_by!(title: @formulario_titulo) +Quando('eu preencho todas as avaliações corretamente') do + fill_in 'Score', with: '10' +end - expect(formulario.questaos.count).to eq(@template.questaos.count) - expect(formulario.questaos.pluck(:enunciado)).to eq(@template.questaos.pluck(:enunciado)) - expect(formulario.questaos.pluck(:template_id).uniq).to eq([ nil ]) +Quando('eu deixo questões obrigatórias em branco') do + fill_in 'Score', with: '' end -Dado("que sou um administrador") do - @admin = criar_admin +E('clico no botão de submeter avaliação') do + click_button 'Enviar Resposta' end -Dado("estou criando um formulário") do - @template = criar_template_com_questao(@admin) - @turma = criar_turma - fazer_login_como(@admin) - visit new_formulario_path +Então('o sistema deve salvar os {string} vinculados ao meu usuário') do |_| + expect(Submissao.where(user: @user, formulario: @formulario).count).to eq(1) + expect(Respostum.count).to eq(1) end -Quando("eu tentar criar um formulário sem selecionar nenhum template") do - fill_in "formulario[title]", with: "Formulário sem template" - select @turma.name, from: "formulario[course_class_id]" - click_button "Enviar" +Então('o sistema não deve processar o envio') do + expect(Submissao.count).to eq(0) end -Então("o sistema deve exibir uma mensagem dizendo 'É necessário escolher um template base'") do - expect(page).to have_text("É necessário escolher um template base") +E('eu devo ser redirecionado com uma mensagem {string}') do |mensagem| + expect(page).to have_content(mensagem) end -Quando("eu tentar criar um formulário sem selecionar nenhuma turma") do - fill_in "formulario[title]", with: "Formulário sem turma" - select @template.title, from: "formulario[template_id]" - click_button "Enviar" +E('eu devo ver a mensagem de erro {string}') do |mensagem| + expect(Submissao.count).to eq(0) + expect(page).to have_content(mensagem) end -Então("o sistema deve exibir uma mensagem dizendo 'É necessário escolher ao menos uma turma'") do - expect(page).to have_text("É necessário escolher ao menos uma turma") +E('o sistema deve exibir a mensagem {string}') do |mensagem| + expect(page).to have_content(mensagem) end + + +# ISSUE 100: EXPORTAÇÃO DE CSV (ADMIN) + + +E('que estou na página de visualização de resultados da turma {string}') do |nome_turma| + @turma = create(:course_class, name: nome_turma) + @formulario = create(:formulario, admin: @user, course_class: @turma) + visit formulario_path(@formulario) +end + +E('que estou na página de visualização de resultados de um formulário recém-criado') do + @formulario = create(:formulario, admin: @user) + visit formulario_path(@formulario) +end + +Quando('o formulário selecionado já possui respostas submetidas') do + aluno = create(:user) + create(:submissao, formulario: @formulario, user: aluno) + visit formulario_path(@formulario) # <--- Força o recarregamento explícito da página +end + +Quando('o formulário selecionado ainda não possui nenhuma resposta de participante') do + expect(Submissao.where(formulario_id: @formulario.id).count).to eq(0) +end + +E('eu clico no botão {string}') do |texto_botao| + if page.has_link?(texto_botao) + click_link texto_botao + elsif page.has_button?(texto_botao) + click_button texto_botao + end +end + +Então('o sistema deve gerar um arquivo {string} com todas as respostas estruturadas') do |_| + expect(page.response_headers['Content-Type']).to include('text/csv') +end + +E('o download do arquivo deve iniciar automaticamente no meu navegador') do + expect(page.response_headers['Content-Disposition']).to include('attachment') + expect(page.response_headers['Content-Disposition']).to include(".csv") +end + +Então('o sistema não deve gerar o arquivo') do + expect(page.response_headers['Content-Type']).not_to include('text/csv') +end + +E('deve exibir um alerta impeditivo {string}') do |mensagem| + expect(page).to have_content(mensagem) +end \ No newline at end of file diff --git a/project/features/support/env.rb b/project/features/support/env.rb index 1b06a3c7b0..c64903a620 100644 --- a/project/features/support/env.rb +++ b/project/features/support/env.rb @@ -52,3 +52,6 @@ # 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 + +require 'factory_bot_rails' +World(FactoryBot::Syntax::Methods) \ No newline at end of file From 42f2b04e0508e2b2f1393b17612f97f61c200e7e Mon Sep 17 00:00:00 2001 From: lfducat <25991752+lfducat@users.noreply.github.com> Date: Tue, 16 Jun 2026 21:56:04 -0300 Subject: [PATCH 21/23] feat: importar/atualizar com dados do SIGAA --- .../controllers/imports/sigaa_controller.rb | 27 ++++++ .../services/sigaa/class_members_importer.rb | 92 ++++++++++++------- .../app/services/sigaa/classes_importer.rb | 67 ++++++++++++++ .../app/services/sigaa/data_synchronizer.rb | 38 ++++++++ project/app/views/dashboard/show.html.erb | 2 +- project/app/views/imports/sigaa/new.html.erb | 19 ++++ project/config/routes.rb | 3 +- .../step_definitions/sigaa_import_steps.rb | 63 +++++++++++++ .../requests/class_members_importer_spec.rb | 2 +- project/spec/requests/sigaa_import_spec.rb | 47 ++++++++++ .../services/sigaa/classes_importer_spec.rb | 74 +++++++++++++++ .../services/sigaa/data_synchronizer_spec.rb | 83 +++++++++++++++++ 12 files changed, 480 insertions(+), 37 deletions(-) create mode 100644 project/app/controllers/imports/sigaa_controller.rb create mode 100644 project/app/services/sigaa/classes_importer.rb create mode 100644 project/app/services/sigaa/data_synchronizer.rb create mode 100644 project/app/views/imports/sigaa/new.html.erb create mode 100644 project/features/step_definitions/sigaa_import_steps.rb create mode 100644 project/spec/requests/sigaa_import_spec.rb create mode 100644 project/spec/services/sigaa/classes_importer_spec.rb create mode 100644 project/spec/services/sigaa/data_synchronizer_spec.rb diff --git a/project/app/controllers/imports/sigaa_controller.rb b/project/app/controllers/imports/sigaa_controller.rb new file mode 100644 index 0000000000..2ddbe240be --- /dev/null +++ b/project/app/controllers/imports/sigaa_controller.rb @@ -0,0 +1,27 @@ +module Imports + class SigaaController < ApplicationController + before_action :require_admin + + def new + @semesters = Sigaa::DataSynchronizer.available_semesters + @semester = params[:semester].presence || @semesters.first + end + + def create + result = Sigaa::DataSynchronizer.new(semester: sync_params[:semester], imported_by: current_user).call + + if result.already_up_to_date? + redirect_to imports_sigaa_path, notice: "A base de dados ja esta atualizada com o SIGAA para este periodo." + else + redirect_to imports_sigaa_path, + notice: "Sincronizacao concluida. #{result.created_count} novos registros adicionados e #{result.updated_count} registros atualizados." + end + end + + private + + def sync_params + params.require(:sigaa_sync).permit(:semester) + end + end +end diff --git a/project/app/services/sigaa/class_members_importer.rb b/project/app/services/sigaa/class_members_importer.rb index 8f31bb747b..23420eabb9 100644 --- a/project/app/services/sigaa/class_members_importer.rb +++ b/project/app/services/sigaa/class_members_importer.rb @@ -1,17 +1,18 @@ module Sigaa class ClassMembersImporter - Result = Data.define(:created_users, :updated_memberships, :inconsistencies) + Result = Data.define(:created_count, :updated_count, :inconsistencies) DEFAULT_PATH = Rails.root.join("..", "class_members.json") - def initialize(path: DEFAULT_PATH, imported_by: nil) + def initialize(path: DEFAULT_PATH, semester: nil, imported_by: nil) @path = path + @semester = semester @imported_by = imported_by end def call - created_users = 0 - updated_memberships = 0 + created_count = 0 + updated_count = 0 inconsistencies = 0 parsed_data.each do |class_payload| @@ -24,22 +25,27 @@ def call next end - user, created = find_or_create_user(member_payload, course_class.department) - created_users += 1 if created - send_password_setup(user) if created && user.pending_password_setup? - updated_memberships += 1 if ensure_membership(user, course_class, membership_role) + user_status = sync_user(member_payload, course_class.department) + created_count += 1 if user_status == :created + updated_count += 1 if user_status == :updated + + membership_status = ensure_membership(user_for(member_payload), course_class, membership_role) + created_count += 1 if membership_status == :created end end - Result.new(created_users:, updated_memberships:, inconsistencies:) + Result.new(created_count:, updated_count:, inconsistencies:) end private - attr_reader :path, :imported_by + attr_reader :path, :semester, :imported_by def parsed_data - JSON.parse(File.read(path)) + data = JSON.parse(File.read(path)) + return data if semester.blank? + + data.select { |entry| entry["semester"] == semester } end def find_or_create_course_class(class_payload) @@ -63,47 +69,64 @@ def department_for(code) def members_for(class_payload) class_payload.flat_map do |key, value| - next [] unless value.is_a?(Array) + next [] unless %w[dicente docente].include?(key) role = key == "docente" ? :docente : :discente - value.map { |member_payload| [ member_payload, role ] } + members = value.is_a?(Array) ? value : (value.is_a?(Hash) ? [ value ] : []) + members.map { |member_payload| [ member_payload, role ] } end end def invalid_member?(member_payload) - member_payload["email"].blank? || member_payload["matricula"].blank? + member_payload["email"].blank? || member_registration(member_payload).blank? + end + + def member_registration(member_payload) + member_payload["matricula"].presence || member_payload["usuario"].presence end def register_inconsistency(member_payload, message) ImportInconsistency.create!(source: "class_members.json", message:, payload: member_payload) end - def find_or_create_user(member_payload, department) - user = User.find_by(email: normalized_email(member_payload)) || - User.find_by(registration: member_payload["matricula"].to_s.strip) + def sync_user(member_payload, department) + registration = member_registration(member_payload).to_s.strip + email = normalized_email(member_payload) + name = member_payload["nome"] + + user = User.find_by(email:) || User.find_by(registration:) - if user - user.update!( - name: member_payload["nome"], - email: normalized_email(member_payload), - registration: member_payload["matricula"].to_s.strip, + if user.nil? + user = User.create!( + name:, + email:, + registration:, + role: :participant, department: ) - [ user, false ] + send_password_setup(user) if user.pending_password_setup? + return :created + end + + updates = {} + updates[:name] = name if user.name != name + updates[:email] = email if user.email != email + updates[:registration] = registration if user.registration != registration + updates[:department] = department if user.department_id != department.id + + if updates.any? + user.update!(updates) + :updated else - [ - User.create!( - name: member_payload["nome"], - email: normalized_email(member_payload), - registration: member_payload["matricula"].to_s.strip, - role: :participant, - department: - ), - true - ] + :unchanged end end + def user_for(member_payload) + registration = member_registration(member_payload).to_s.strip + User.find_by(email: normalized_email(member_payload)) || User.find_by!(registration:) + end + def normalized_email(member_payload) member_payload["email"].to_s.strip.downcase end @@ -113,7 +136,8 @@ def send_password_setup(user) end def ensure_membership(user, course_class, role) - ClassMembership.find_or_create_by!(user:, course_class:, role:).previously_new_record? + membership = ClassMembership.find_or_create_by!(user:, course_class:, role:) + membership.previously_new_record? ? :created : :unchanged end end end diff --git a/project/app/services/sigaa/classes_importer.rb b/project/app/services/sigaa/classes_importer.rb new file mode 100644 index 0000000000..c0706c9511 --- /dev/null +++ b/project/app/services/sigaa/classes_importer.rb @@ -0,0 +1,67 @@ +module Sigaa + class ClassesImporter + Result = Data.define(:created_count, :updated_count) + + DEFAULT_PATH = Rails.root.join("..", "classes.json") + + def initialize(path: DEFAULT_PATH, semester:, imported_by: nil) + @path = path + @semester = semester + @imported_by = imported_by + end + + def call + created_count = 0 + updated_count = 0 + + entries_for_semester.each do |entry| + attrs = extract_class_attrs(entry) + course_class = CourseClass.find_by( + code: attrs[:code], + class_code: attrs[:class_code], + semester: attrs[:semester] + ) + + if course_class.nil? + CourseClass.create!(attrs.merge(department: department_for(attrs[:code]))) + created_count += 1 + elsif course_class_attributes_changed?(course_class, attrs) + course_class.update!(attrs.slice(:name, :time)) + updated_count += 1 + end + end + + Result.new(created_count:, updated_count:) + end + + private + + attr_reader :path, :semester, :imported_by + + def entries_for_semester + JSON.parse(File.read(path)).select do |entry| + entry.dig("class", "semester") == semester + end + end + + def extract_class_attrs(entry) + class_info = entry.fetch("class") + { + code: entry.fetch("code"), + name: entry.fetch("name"), + class_code: class_info.fetch("classCode"), + semester: class_info.fetch("semester"), + time: class_info["time"] + } + end + + def department_for(code) + department_code = imported_by&.department&.code || code.to_s[/\A[A-Za-z]+/]&.upcase || "GERAL" + Department.find_or_create_by!(code: department_code) { |department| department.name = department_code } + end + + def course_class_attributes_changed?(course_class, attrs) + course_class.name != attrs[:name] || course_class.time != attrs[:time] + end + end +end diff --git a/project/app/services/sigaa/data_synchronizer.rb b/project/app/services/sigaa/data_synchronizer.rb new file mode 100644 index 0000000000..1644dc2aa8 --- /dev/null +++ b/project/app/services/sigaa/data_synchronizer.rb @@ -0,0 +1,38 @@ +module Sigaa + class DataSynchronizer + Result = Data.define(:created_count, :updated_count, :inconsistencies, :already_up_to_date?) + + def initialize(semester:, imported_by:, classes_path: ClassesImporter::DEFAULT_PATH, members_path: ClassMembersImporter::DEFAULT_PATH) + @semester = semester + @imported_by = imported_by + @classes_path = classes_path + @members_path = members_path + end + + def call + classes_result = ClassesImporter.new(path: classes_path, semester:, imported_by:).call + members_result = ClassMembersImporter.new(path: members_path, semester:, imported_by:).call + + created_count = classes_result.created_count + members_result.created_count + updated_count = classes_result.updated_count + members_result.updated_count + + Result.new( + created_count:, + updated_count:, + inconsistencies: members_result.inconsistencies, + already_up_to_date?: created_count.zero? && updated_count.zero? + ) + end + + def self.available_semesters(classes_path: ClassesImporter::DEFAULT_PATH, members_path: ClassMembersImporter::DEFAULT_PATH) + from_classes = JSON.parse(File.read(classes_path)).filter_map { |entry| entry.dig("class", "semester") } + from_members = JSON.parse(File.read(members_path)).filter_map { |entry| entry["semester"] } + + (from_classes + from_members).uniq.sort.reverse + end + + private + + attr_reader :semester, :imported_by, :classes_path, :members_path + end +end diff --git a/project/app/views/dashboard/show.html.erb b/project/app/views/dashboard/show.html.erb index 73a580049e..167ad71ba0 100644 --- a/project/app/views/dashboard/show.html.erb +++ b/project/app/views/dashboard/show.html.erb @@ -5,7 +5,7 @@