From c41c4891a5ff86c38d31360a77b22c45d1b5ab39 Mon Sep 17 00:00:00 2001 From: mariosantos-05 Date: Wed, 12 Nov 2025 16:22:18 -0300 Subject: [PATCH 001/100] App creation --- .dockerignore | 51 +++ .gitattributes | 9 + .github/dependabot.yml | 12 + .github/workflows/ci.yml | 90 ++++ .gitignore | 34 ++ .kamal/hooks/docker-setup.sample | 3 + .kamal/hooks/post-app-boot.sample | 3 + .kamal/hooks/post-deploy.sample | 14 + .kamal/hooks/post-proxy-reboot.sample | 3 + .kamal/hooks/pre-app-boot.sample | 3 + .kamal/hooks/pre-build.sample | 51 +++ .kamal/hooks/pre-connect.sample | 47 ++ .kamal/hooks/pre-deploy.sample | 122 ++++++ .kamal/hooks/pre-proxy-reboot.sample | 3 + .kamal/secrets | 17 + .rubocop.yml | 8 + .ruby-version | 1 + Dockerfile | 72 ++++ Gemfile | 63 +++ Gemfile.lock | 400 ++++++++++++++++++ README.md | 26 +- Rakefile | 6 + app/assets/images/.keep | 0 app/assets/stylesheets/application.css | 10 + app/controllers/application_controller.rb | 4 + app/controllers/concerns/.keep | 0 app/helpers/application_helper.rb | 2 + app/javascript/application.js | 3 + app/javascript/controllers/application.js | 9 + .../controllers/hello_controller.js | 7 + app/javascript/controllers/index.js | 4 + app/jobs/application_job.rb | 7 + app/mailers/application_mailer.rb | 4 + app/models/application_record.rb | 3 + app/models/concerns/.keep | 0 app/views/layouts/application.html.erb | 28 ++ app/views/layouts/mailer.html.erb | 13 + app/views/layouts/mailer.text.erb | 1 + app/views/pwa/manifest.json.erb | 22 + app/views/pwa/service-worker.js | 26 ++ bin/brakeman | 7 + bin/dev | 2 + bin/docker-entrypoint | 14 + bin/importmap | 4 + bin/jobs | 6 + bin/kamal | 16 + bin/rails | 4 + bin/rake | 4 + bin/rubocop | 8 + bin/setup | 34 ++ bin/thrust | 5 + config.ru | 6 + config/application.rb | 27 ++ config/boot.rb | 4 + config/cable.yml | 17 + config/cache.yml | 16 + config/credentials.yml.enc | 1 + config/database.yml | 41 ++ config/deploy.yml | 116 +++++ config/environment.rb | 5 + config/environments/development.rb | 72 ++++ config/environments/production.rb | 90 ++++ config/environments/test.rb | 53 +++ config/importmap.rb | 7 + config/initializers/assets.rb | 7 + .../initializers/content_security_policy.rb | 25 ++ .../initializers/filter_parameter_logging.rb | 8 + config/initializers/inflections.rb | 16 + config/locales/en.yml | 31 ++ config/puma.rb | 41 ++ config/queue.yml | 18 + config/recurring.yml | 15 + config/routes.rb | 14 + config/storage.yml | 34 ++ db/cable_schema.rb | 11 + db/cache_schema.rb | 12 + db/queue_schema.rb | 129 ++++++ db/seeds.rb | 9 + lib/tasks/.keep | 0 log/.keep | 0 public/400.html | 114 +++++ public/404.html | 114 +++++ public/406-unsupported-browser.html | 114 +++++ public/422.html | 114 +++++ public/500.html | 114 +++++ public/icon.png | Bin 0 -> 4166 bytes public/icon.svg | 3 + public/robots.txt | 1 + script/.keep | 0 storage/.keep | 0 test/application_system_test_case.rb | 5 + test/controllers/.keep | 0 test/fixtures/files/.keep | 0 test/helpers/.keep | 0 test/integration/.keep | 0 test/mailers/.keep | 0 test/models/.keep | 0 test/system/.keep | 0 test/test_helper.rb | 15 + tmp/.keep | 0 tmp/pids/.keep | 0 tmp/storage/.keep | 0 vendor/.keep | 0 vendor/javascript/.keep | 0 104 files changed, 2702 insertions(+), 2 deletions(-) create mode 100644 .dockerignore create mode 100644 .gitattributes create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100755 .kamal/hooks/docker-setup.sample create mode 100755 .kamal/hooks/post-app-boot.sample create mode 100755 .kamal/hooks/post-deploy.sample create mode 100755 .kamal/hooks/post-proxy-reboot.sample create mode 100755 .kamal/hooks/pre-app-boot.sample create mode 100755 .kamal/hooks/pre-build.sample create mode 100755 .kamal/hooks/pre-connect.sample create mode 100755 .kamal/hooks/pre-deploy.sample create mode 100755 .kamal/hooks/pre-proxy-reboot.sample create mode 100644 .kamal/secrets create mode 100644 .rubocop.yml create mode 100644 .ruby-version create mode 100644 Dockerfile create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 Rakefile create mode 100644 app/assets/images/.keep create mode 100644 app/assets/stylesheets/application.css create mode 100644 app/controllers/application_controller.rb create mode 100644 app/controllers/concerns/.keep create mode 100644 app/helpers/application_helper.rb create mode 100644 app/javascript/application.js create mode 100644 app/javascript/controllers/application.js create mode 100644 app/javascript/controllers/hello_controller.js create mode 100644 app/javascript/controllers/index.js create mode 100644 app/jobs/application_job.rb create mode 100644 app/mailers/application_mailer.rb create mode 100644 app/models/application_record.rb create mode 100644 app/models/concerns/.keep create mode 100644 app/views/layouts/application.html.erb create mode 100644 app/views/layouts/mailer.html.erb create mode 100644 app/views/layouts/mailer.text.erb create mode 100644 app/views/pwa/manifest.json.erb create mode 100644 app/views/pwa/service-worker.js create mode 100755 bin/brakeman create mode 100755 bin/dev create mode 100755 bin/docker-entrypoint create mode 100755 bin/importmap create mode 100755 bin/jobs create mode 100755 bin/kamal create mode 100755 bin/rails create mode 100755 bin/rake create mode 100755 bin/rubocop create mode 100755 bin/setup create mode 100755 bin/thrust create mode 100644 config.ru create mode 100644 config/application.rb create mode 100644 config/boot.rb create mode 100644 config/cable.yml create mode 100644 config/cache.yml create mode 100644 config/credentials.yml.enc create mode 100644 config/database.yml create mode 100644 config/deploy.yml create mode 100644 config/environment.rb create mode 100644 config/environments/development.rb create mode 100644 config/environments/production.rb create mode 100644 config/environments/test.rb create mode 100644 config/importmap.rb create mode 100644 config/initializers/assets.rb create mode 100644 config/initializers/content_security_policy.rb create mode 100644 config/initializers/filter_parameter_logging.rb create mode 100644 config/initializers/inflections.rb create mode 100644 config/locales/en.yml create mode 100644 config/puma.rb create mode 100644 config/queue.yml create mode 100644 config/recurring.yml create mode 100644 config/routes.rb create mode 100644 config/storage.yml create mode 100644 db/cable_schema.rb create mode 100644 db/cache_schema.rb create mode 100644 db/queue_schema.rb create mode 100644 db/seeds.rb create mode 100644 lib/tasks/.keep create mode 100644 log/.keep create mode 100644 public/400.html create mode 100644 public/404.html create mode 100644 public/406-unsupported-browser.html create mode 100644 public/422.html create mode 100644 public/500.html create mode 100644 public/icon.png create mode 100644 public/icon.svg create mode 100644 public/robots.txt create mode 100644 script/.keep create mode 100644 storage/.keep create mode 100644 test/application_system_test_case.rb create mode 100644 test/controllers/.keep create mode 100644 test/fixtures/files/.keep create mode 100644 test/helpers/.keep create mode 100644 test/integration/.keep create mode 100644 test/mailers/.keep create mode 100644 test/models/.keep create mode 100644 test/system/.keep create mode 100644 test/test_helper.rb create mode 100644 tmp/.keep create mode 100644 tmp/pids/.keep create mode 100644 tmp/storage/.keep create mode 100644 vendor/.keep create mode 100644 vendor/javascript/.keep diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..325bfc036d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,51 @@ +# See https://docs.docker.com/engine/reference/builder/#dockerignore-file for more about ignoring files. + +# Ignore git directory. +/.git/ +/.gitignore + +# Ignore bundler config. +/.bundle + +# Ignore all environment files. +/.env* + +# Ignore all default key files. +/config/master.key +/config/credentials/*.key + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/.keep + +# Ignore storage (uploaded files in development and any SQLite databases). +/storage/* +!/storage/.keep +/tmp/storage/* +!/tmp/storage/.keep + +# Ignore assets. +/node_modules/ +/app/assets/builds/* +!/app/assets/builds/.keep +/public/assets + +# Ignore CI service files. +/.github + +# Ignore Kamal files. +/config/deploy*.yml +/.kamal + +# Ignore development files +/.devcontainer + +# Ignore Docker-related files +/.dockerignore +/Dockerfile* diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..8dc4323435 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +# See https://git-scm.com/docs/gitattributes for more about git attribute files. + +# Mark the database schema as having been generated. +db/schema.rb linguist-generated + +# Mark any vendored files as having been vendored. +vendor/* linguist-vendored +config/credentials/*.yml.enc diff=rails_credentials +config/credentials.yml.enc diff=rails_credentials diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..f0527e6be1 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: +- package-ecosystem: bundler + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 +- package-ecosystem: github-actions + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000..7b7c0c59b3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,90 @@ +name: CI + +on: + pull_request: + push: + branches: [ main ] + +jobs: + scan_ruby: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + - name: Scan for common Rails security vulnerabilities using static analysis + run: bin/brakeman --no-pager + + scan_js: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + - name: Scan for security vulnerabilities in JavaScript dependencies + run: bin/importmap audit + + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + - name: Lint code for consistent style + run: bin/rubocop -f github + + test: + runs-on: ubuntu-latest + + # services: + # redis: + # image: redis + # 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 build-essential git libyaml-dev pkg-config google-chrome-stable + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + - name: Run tests + env: + RAILS_ENV: test + # REDIS_URL: redis://localhost:6379/0 + run: bin/rails db:test:prepare test 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..f92525ca5e --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# See https://help.github.com/articles/ignoring-files for more about ignoring files. +# +# Temporary files generated by your text editor or operating system +# belong in git's global ignore instead: +# `$XDG_CONFIG_HOME/git/ignore` or `~/.config/git/ignore` + +# Ignore bundler config. +/.bundle + +# Ignore all environment files. +/.env* + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/ +!/tmp/pids/.keep + +# Ignore storage (uploaded files in development and any SQLite databases). +/storage/* +!/storage/.keep +/tmp/storage/* +!/tmp/storage/ +!/tmp/storage/.keep + +/public/assets + +# Ignore master key for decrypting credentials and more. +/config/master.key diff --git a/.kamal/hooks/docker-setup.sample b/.kamal/hooks/docker-setup.sample new file mode 100755 index 0000000000..2fb07d7d7a --- /dev/null +++ b/.kamal/hooks/docker-setup.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Docker set up on $KAMAL_HOSTS..." diff --git a/.kamal/hooks/post-app-boot.sample b/.kamal/hooks/post-app-boot.sample new file mode 100755 index 0000000000..70f9c4bc95 --- /dev/null +++ b/.kamal/hooks/post-app-boot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Booted app version $KAMAL_VERSION on $KAMAL_HOSTS..." diff --git a/.kamal/hooks/post-deploy.sample b/.kamal/hooks/post-deploy.sample new file mode 100755 index 0000000000..fd364c2a77 --- /dev/null +++ b/.kamal/hooks/post-deploy.sample @@ -0,0 +1,14 @@ +#!/bin/sh + +# A sample post-deploy hook +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_ROLES (if set) +# KAMAL_DESTINATION (if set) +# KAMAL_RUNTIME + +echo "$KAMAL_PERFORMER deployed $KAMAL_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds" diff --git a/.kamal/hooks/post-proxy-reboot.sample b/.kamal/hooks/post-proxy-reboot.sample new file mode 100755 index 0000000000..1435a677f2 --- /dev/null +++ b/.kamal/hooks/post-proxy-reboot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Rebooted kamal-proxy on $KAMAL_HOSTS" diff --git a/.kamal/hooks/pre-app-boot.sample b/.kamal/hooks/pre-app-boot.sample new file mode 100755 index 0000000000..45f7355045 --- /dev/null +++ b/.kamal/hooks/pre-app-boot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Booting app version $KAMAL_VERSION on $KAMAL_HOSTS..." diff --git a/.kamal/hooks/pre-build.sample b/.kamal/hooks/pre-build.sample new file mode 100755 index 0000000000..c5a55678b2 --- /dev/null +++ b/.kamal/hooks/pre-build.sample @@ -0,0 +1,51 @@ +#!/bin/sh + +# A sample pre-build hook +# +# Checks: +# 1. We have a clean checkout +# 2. A remote is configured +# 3. The branch has been pushed to the remote +# 4. The version we are deploying matches the remote +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_ROLES (if set) +# KAMAL_DESTINATION (if set) + +if [ -n "$(git status --porcelain)" ]; then + echo "Git checkout is not clean, aborting..." >&2 + git status --porcelain >&2 + exit 1 +fi + +first_remote=$(git remote) + +if [ -z "$first_remote" ]; then + echo "No git remote set, aborting..." >&2 + exit 1 +fi + +current_branch=$(git branch --show-current) + +if [ -z "$current_branch" ]; then + echo "Not on a git branch, aborting..." >&2 + exit 1 +fi + +remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1) + +if [ -z "$remote_head" ]; then + echo "Branch not pushed to remote, aborting..." >&2 + exit 1 +fi + +if [ "$KAMAL_VERSION" != "$remote_head" ]; then + echo "Version ($KAMAL_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2 + exit 1 +fi + +exit 0 diff --git a/.kamal/hooks/pre-connect.sample b/.kamal/hooks/pre-connect.sample new file mode 100755 index 0000000000..77744bdca8 --- /dev/null +++ b/.kamal/hooks/pre-connect.sample @@ -0,0 +1,47 @@ +#!/usr/bin/env ruby + +# A sample pre-connect check +# +# Warms DNS before connecting to hosts in parallel +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_ROLES (if set) +# KAMAL_DESTINATION (if set) +# KAMAL_RUNTIME + +hosts = ENV["KAMAL_HOSTS"].split(",") +results = nil +max = 3 + +elapsed = Benchmark.realtime do + results = hosts.map do |host| + Thread.new do + tries = 1 + + begin + Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME) + rescue SocketError + if tries < max + puts "Retrying DNS warmup: #{host}" + tries += 1 + sleep rand + retry + else + puts "DNS warmup failed: #{host}" + host + end + end + + tries + end + end.map(&:value) +end + +retries = results.sum - hosts.size +nopes = results.count { |r| r == max } + +puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ] diff --git a/.kamal/hooks/pre-deploy.sample b/.kamal/hooks/pre-deploy.sample new file mode 100755 index 0000000000..05b3055b72 --- /dev/null +++ b/.kamal/hooks/pre-deploy.sample @@ -0,0 +1,122 @@ +#!/usr/bin/env ruby + +# A sample pre-deploy hook +# +# Checks the Github status of the build, waiting for a pending build to complete for up to 720 seconds. +# +# Fails unless the combined status is "success" +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_COMMAND +# KAMAL_SUBCOMMAND +# KAMAL_ROLES (if set) +# KAMAL_DESTINATION (if set) + +# Only check the build status for production deployments +if ENV["KAMAL_COMMAND"] == "rollback" || ENV["KAMAL_DESTINATION"] != "production" + exit 0 +end + +require "bundler/inline" + +# true = install gems so this is fast on repeat invocations +gemfile(true, quiet: true) do + source "https://rubygems.org" + + gem "octokit" + gem "faraday-retry" +end + +MAX_ATTEMPTS = 72 +ATTEMPTS_GAP = 10 + +def exit_with_error(message) + $stderr.puts message + exit 1 +end + +class GithubStatusChecks + attr_reader :remote_url, :git_sha, :github_client, :combined_status + + def initialize + @remote_url = github_repo_from_remote_url + @git_sha = `git rev-parse HEAD`.strip + @github_client = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"]) + refresh! + end + + def refresh! + @combined_status = github_client.combined_status(remote_url, git_sha) + end + + def state + combined_status[:state] + end + + def first_status_url + first_status = combined_status[:statuses].find { |status| status[:state] == state } + first_status && first_status[:target_url] + end + + def complete_count + combined_status[:statuses].count { |status| status[:state] != "pending"} + end + + def total_count + combined_status[:statuses].count + end + + def current_status + if total_count > 0 + "Completed #{complete_count}/#{total_count} checks, see #{first_status_url} ..." + else + "Build not started..." + end + end + + private + def github_repo_from_remote_url + url = `git config --get remote.origin.url`.strip.delete_suffix(".git") + if url.start_with?("https://github.com/") + url.delete_prefix("https://github.com/") + elsif url.start_with?("git@github.com:") + url.delete_prefix("git@github.com:") + else + url + end + end +end + + +$stdout.sync = true + +begin + puts "Checking build status..." + + attempts = 0 + checks = GithubStatusChecks.new + + loop do + case checks.state + when "success" + puts "Checks passed, see #{checks.first_status_url}" + exit 0 + when "failure" + exit_with_error "Checks failed, see #{checks.first_status_url}" + when "pending" + attempts += 1 + end + + exit_with_error "Checks are still pending, gave up after #{MAX_ATTEMPTS * ATTEMPTS_GAP} seconds" if attempts == MAX_ATTEMPTS + + puts checks.current_status + sleep(ATTEMPTS_GAP) + checks.refresh! + end +rescue Octokit::NotFound + exit_with_error "Build status could not be found" +end diff --git a/.kamal/hooks/pre-proxy-reboot.sample b/.kamal/hooks/pre-proxy-reboot.sample new file mode 100755 index 0000000000..061f8059e6 --- /dev/null +++ b/.kamal/hooks/pre-proxy-reboot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Rebooting kamal-proxy on $KAMAL_HOSTS..." diff --git a/.kamal/secrets b/.kamal/secrets new file mode 100644 index 0000000000..9a771a3985 --- /dev/null +++ b/.kamal/secrets @@ -0,0 +1,17 @@ +# Secrets defined here are available for reference under registry/password, env/secret, builder/secrets, +# and accessories/*/env/secret in config/deploy.yml. All secrets should be pulled from either +# password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git. + +# Example of extracting secrets from 1password (or another compatible pw manager) +# SECRETS=$(kamal secrets fetch --adapter 1password --account your-account --from Vault/Item KAMAL_REGISTRY_PASSWORD RAILS_MASTER_KEY) +# KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD ${SECRETS}) +# RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY ${SECRETS}) + +# Use a GITHUB_TOKEN if private repositories are needed for the image +# GITHUB_TOKEN=$(gh config get -h github.com oauth_token) + +# Grab the registry password from ENV +KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD + +# Improve security by using a password manager. Never check config/master.key into git! +RAILS_MASTER_KEY=$(cat config/master.key) diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000000..f9d86d4a54 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,8 @@ +# Omakase Ruby styling for Rails +inherit_gem: { rubocop-rails-omakase: rubocop.yml } + +# Overwrite or add rules to create your own house style +# +# # Use `[a, [b, c]]` not `[ a, [ b, c ] ]` +# Layout/SpaceInsideArrayLiteralBrackets: +# Enabled: false diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000000..3b47f2e4f8 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.3.9 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..4ba3d017fd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,72 @@ +# syntax=docker/dockerfile:1 +# check=error=true + +# This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand: +# docker build -t camaar_g1 . +# docker run -d -p 80:80 -e RAILS_MASTER_KEY= --name camaar_g1 camaar_g1 + +# 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.3.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 && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Set production environment +ENV RAILS_ENV="production" \ + BUNDLE_DEPLOYMENT="1" \ + BUNDLE_PATH="/usr/local/bundle" \ + BUNDLE_WITHOUT="development" + +# Throw-away build stage to reduce size of final image +FROM base AS build + +# Install packages needed to build gems +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y build-essential git libyaml-dev pkg-config && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Install application gems +COPY Gemfile Gemfile.lock ./ +RUN bundle install && \ + rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \ + bundle exec bootsnap precompile --gemfile + +# Copy application code +COPY . . + +# Precompile bootsnap code for faster boot times +RUN bundle exec bootsnap precompile app/ lib/ + +# Precompiling assets for production without requiring secret RAILS_MASTER_KEY +RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile + + + + +# Final stage for app image +FROM base + +# Copy built artifacts: gems, application +COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}" +COPY --from=build /rails /rails + +# Run and own only the runtime files as a non-root user for security +RUN groupadd --system --gid 1000 rails && \ + useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \ + chown -R rails:rails db log storage tmp +USER 1000:1000 + +# Entrypoint prepares the database. +ENTRYPOINT ["/rails/bin/docker-entrypoint"] + +# Start server via Thruster by default, this can be overwritten at runtime +EXPOSE 80 +CMD ["./bin/thrust", "./bin/rails", "server"] diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000000..ed45b5b828 --- /dev/null +++ b/Gemfile @@ -0,0 +1,63 @@ +source "https://rubygems.org" + +# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" +gem "rails", "~> 8.0.2", ">= 8.0.2.1" +# 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" + + # Static analysis for security vulnerabilities [https://brakemanscanner.org/] + gem "brakeman", require: false + + # Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/] + gem "rubocop-rails-omakase", require: false +end + +group :development do + # Use console on exceptions pages [https://github.com/rails/web-console] + gem "web-console" +end + +group :test do + # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing] + gem "capybara" + gem "selenium-webdriver" +end diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000000..75d2d41720 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,400 @@ +GEM + remote: https://rubygems.org/ + specs: + actioncable (8.0.4) + actionpack (= 8.0.4) + activesupport (= 8.0.4) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + zeitwerk (~> 2.6) + actionmailbox (8.0.4) + actionpack (= 8.0.4) + activejob (= 8.0.4) + activerecord (= 8.0.4) + activestorage (= 8.0.4) + activesupport (= 8.0.4) + mail (>= 2.8.0) + actionmailer (8.0.4) + actionpack (= 8.0.4) + actionview (= 8.0.4) + activejob (= 8.0.4) + activesupport (= 8.0.4) + mail (>= 2.8.0) + rails-dom-testing (~> 2.2) + actionpack (8.0.4) + actionview (= 8.0.4) + activesupport (= 8.0.4) + nokogiri (>= 1.8.5) + rack (>= 2.2.4) + rack-session (>= 1.0.1) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) + actiontext (8.0.4) + actionpack (= 8.0.4) + activerecord (= 8.0.4) + activestorage (= 8.0.4) + activesupport (= 8.0.4) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (8.0.4) + activesupport (= 8.0.4) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (8.0.4) + activesupport (= 8.0.4) + globalid (>= 0.3.6) + activemodel (8.0.4) + activesupport (= 8.0.4) + activerecord (8.0.4) + activemodel (= 8.0.4) + activesupport (= 8.0.4) + timeout (>= 0.4.0) + activestorage (8.0.4) + actionpack (= 8.0.4) + activejob (= 8.0.4) + activerecord (= 8.0.4) + activesupport (= 8.0.4) + marcel (~> 1.0) + activesupport (8.0.4) + base64 + benchmark (>= 0.3) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + ast (2.4.3) + base64 (0.3.0) + bcrypt_pbkdf (1.1.1) + bcrypt_pbkdf (1.1.1-arm64-darwin) + bcrypt_pbkdf (1.1.1-x86_64-darwin) + benchmark (0.5.0) + bigdecimal (3.3.1) + bindex (0.8.1) + bootsnap (1.18.6) + msgpack (~> 1.2) + brakeman (7.1.1) + racc + builder (3.3.0) + capybara (3.40.0) + addressable + matrix + mini_mime (>= 0.1.3) + nokogiri (~> 1.11) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + regexp_parser (>= 1.5, < 3.0) + xpath (~> 3.2) + concurrent-ruby (1.3.5) + connection_pool (2.5.4) + crass (1.0.6) + date (3.5.0) + debug (1.11.0) + irb (~> 1.10) + reline (>= 0.3.8) + dotenv (3.1.8) + drb (2.2.3) + ed25519 (1.4.0) + erb (5.1.3) + erubi (1.13.1) + et-orbi (1.4.0) + tzinfo + fugit (1.12.1) + et-orbi (~> 1.4) + raabro (~> 1.4) + globalid (1.3.0) + activesupport (>= 6.1) + i18n (1.14.7) + concurrent-ruby (~> 1.0) + importmap-rails (2.2.2) + actionpack (>= 6.0.0) + activesupport (>= 6.0.0) + railties (>= 6.0.0) + io-console (0.8.1) + irb (1.15.3) + pp (>= 0.6.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + jbuilder (2.14.1) + actionview (>= 7.0.0) + activesupport (>= 7.0.0) + json (2.16.0) + kamal (2.8.2) + activesupport (>= 7.0) + base64 (~> 0.2) + bcrypt_pbkdf (~> 1.0) + concurrent-ruby (~> 1.2) + dotenv (~> 3.1) + ed25519 (~> 1.4) + net-ssh (~> 7.3) + sshkit (>= 1.23.0, < 2.0) + thor (~> 1.3) + zeitwerk (>= 2.6.18, < 3.0) + language_server-protocol (3.17.0.5) + lint_roller (1.1.0) + logger (1.7.0) + loofah (2.24.1) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mail (2.9.0) + logger + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + marcel (1.1.0) + matrix (0.4.3) + mini_mime (1.1.5) + minitest (5.26.1) + msgpack (1.8.0) + net-imap (0.5.12) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-scp (4.1.0) + net-ssh (>= 2.6.5, < 8.0.0) + net-sftp (4.0.0) + net-ssh (>= 5.0.0, < 8.0.0) + net-smtp (0.5.1) + net-protocol + net-ssh (7.3.0) + nio4r (2.7.5) + nokogiri (1.18.10-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.10-aarch64-linux-musl) + racc (~> 1.4) + nokogiri (1.18.10-arm-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.10-arm-linux-musl) + racc (~> 1.4) + nokogiri (1.18.10-arm64-darwin) + racc (~> 1.4) + nokogiri (1.18.10-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.18.10-x86_64-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.10-x86_64-linux-musl) + racc (~> 1.4) + ostruct (0.6.3) + parallel (1.27.0) + parser (3.3.10.0) + ast (~> 2.4.1) + racc + pp (0.6.3) + prettyprint + prettyprint (0.2.0) + prism (1.6.0) + propshaft (1.3.1) + actionpack (>= 7.0.0) + activesupport (>= 7.0.0) + rack + psych (5.2.6) + date + stringio + public_suffix (6.0.2) + puma (7.1.0) + nio4r (~> 2.0) + raabro (1.4.0) + racc (1.8.1) + rack (3.2.4) + rack-session (2.1.1) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rack-test (2.2.0) + rack (>= 1.3) + rackup (2.2.1) + rack (>= 3) + rails (8.0.4) + actioncable (= 8.0.4) + actionmailbox (= 8.0.4) + actionmailer (= 8.0.4) + actionpack (= 8.0.4) + actiontext (= 8.0.4) + actionview (= 8.0.4) + activejob (= 8.0.4) + activemodel (= 8.0.4) + activerecord (= 8.0.4) + activestorage (= 8.0.4) + activesupport (= 8.0.4) + bundler (>= 1.15.0) + railties (= 8.0.4) + rails-dom-testing (2.3.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.6.2) + loofah (~> 2.21) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + railties (8.0.4) + actionpack (= 8.0.4) + activesupport (= 8.0.4) + irb (~> 1.13) + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + tsort (>= 0.2) + zeitwerk (~> 2.6) + rainbow (3.1.1) + rake (13.3.1) + rdoc (6.15.1) + erb + psych (>= 4.0.0) + tsort + regexp_parser (2.11.3) + reline (0.6.3) + io-console (~> 0.5) + rexml (3.4.4) + rubocop (1.81.7) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.47.1, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.48.0) + parser (>= 3.3.7.2) + prism (~> 1.4) + rubocop-performance (1.26.1) + lint_roller (~> 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.47.1, < 2.0) + rubocop-rails (2.33.4) + activesupport (>= 4.2.0) + lint_roller (~> 1.1) + rack (>= 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.44.0, < 2.0) + rubocop-rails-omakase (1.1.0) + rubocop (>= 1.72) + rubocop-performance (>= 1.24) + rubocop-rails (>= 2.30) + ruby-progressbar (1.13.0) + rubyzip (3.2.2) + securerandom (0.4.1) + selenium-webdriver (4.38.0) + base64 (~> 0.2) + logger (~> 1.4) + rexml (~> 3.2, >= 3.2.5) + rubyzip (>= 1.2.2, < 4.0) + websocket (~> 1.0) + solid_cable (3.0.12) + actioncable (>= 7.2) + activejob (>= 7.2) + activerecord (>= 7.2) + railties (>= 7.2) + solid_cache (1.0.10) + activejob (>= 7.2) + activerecord (>= 7.2) + railties (>= 7.2) + solid_queue (1.2.4) + activejob (>= 7.1) + activerecord (>= 7.1) + concurrent-ruby (>= 1.3.1) + fugit (~> 1.11) + railties (>= 7.1) + thor (>= 1.3.1) + sqlite3 (2.8.0-aarch64-linux-gnu) + sqlite3 (2.8.0-aarch64-linux-musl) + sqlite3 (2.8.0-arm-linux-gnu) + sqlite3 (2.8.0-arm-linux-musl) + sqlite3 (2.8.0-arm64-darwin) + sqlite3 (2.8.0-x86_64-darwin) + sqlite3 (2.8.0-x86_64-linux-gnu) + sqlite3 (2.8.0-x86_64-linux-musl) + sshkit (1.24.0) + base64 + logger + net-scp (>= 1.1.2) + net-sftp (>= 2.1.2) + net-ssh (>= 2.8.0) + ostruct + stimulus-rails (1.3.4) + railties (>= 6.0.0) + stringio (3.1.8) + thor (1.4.0) + thruster (0.1.16) + thruster (0.1.16-aarch64-linux) + thruster (0.1.16-arm64-darwin) + thruster (0.1.16-x86_64-darwin) + thruster (0.1.16-x86_64-linux) + timeout (0.4.4) + tsort (0.2.0) + turbo-rails (2.0.20) + 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.1.0) + uri (1.1.1) + useragent (0.16.11) + web-console (4.2.1) + actionview (>= 6.0.0) + activemodel (>= 6.0.0) + bindex (>= 0.4.0) + railties (>= 6.0.0) + websocket (1.2.11) + websocket-driver (0.8.0) + base64 + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + xpath (3.2.0) + nokogiri (~> 1.8) + zeitwerk (2.7.3) + +PLATFORMS + aarch64-linux + aarch64-linux-gnu + aarch64-linux-musl + arm-linux-gnu + arm-linux-musl + arm64-darwin + x86_64-darwin + x86_64-linux + x86_64-linux-gnu + x86_64-linux-musl + +DEPENDENCIES + bootsnap + brakeman + capybara + debug + importmap-rails + jbuilder + kamal + propshaft + puma (>= 5.0) + rails (~> 8.0.2, >= 8.0.2.1) + rubocop-rails-omakase + selenium-webdriver + solid_cable + solid_cache + solid_queue + sqlite3 (>= 2.1) + stimulus-rails + thruster + turbo-rails + tzinfo-data + web-console + +BUNDLED WITH + 2.7.1 diff --git a/README.md b/README.md index 9d7fe1bf53..7db80e4ca1 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,24 @@ -# CAMAAR -Sistema para avaliação de atividades acadêmicas remotas do CIC +# README + +This README would normally document whatever steps are necessary to get the +application up and running. + +Things you may want to cover: + +* Ruby version + +* System dependencies + +* Configuration + +* Database creation + +* Database initialization + +* How to run the test suite + +* Services (job queues, cache servers, search engines, etc.) + +* Deployment instructions + +* ... diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000000..9a5ea7383a --- /dev/null +++ b/Rakefile @@ -0,0 +1,6 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require_relative "config/application" + +Rails.application.load_tasks diff --git a/app/assets/images/.keep b/app/assets/images/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css new file mode 100644 index 0000000000..fe93333c0f --- /dev/null +++ b/app/assets/stylesheets/application.css @@ -0,0 +1,10 @@ +/* + * This is a manifest file that'll be compiled into application.css. + * + * With Propshaft, assets are served efficiently without preprocessing steps. You can still include + * application-wide styles in this file, but keep in mind that CSS precedence will follow the standard + * cascading order, meaning styles declared later in the document or manifest will override earlier ones, + * depending on specificity. + * + * Consider organizing styles into separate files for maintainability. + */ diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb new file mode 100644 index 0000000000..0d95db22b4 --- /dev/null +++ b/app/controllers/application_controller.rb @@ -0,0 +1,4 @@ +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 +end diff --git a/app/controllers/concerns/.keep b/app/controllers/concerns/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb new file mode 100644 index 0000000000..de6be7945c --- /dev/null +++ b/app/helpers/application_helper.rb @@ -0,0 +1,2 @@ +module ApplicationHelper +end diff --git a/app/javascript/application.js b/app/javascript/application.js new file mode 100644 index 0000000000..0d7b49404c --- /dev/null +++ b/app/javascript/application.js @@ -0,0 +1,3 @@ +// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails +import "@hotwired/turbo-rails" +import "controllers" diff --git a/app/javascript/controllers/application.js b/app/javascript/controllers/application.js new file mode 100644 index 0000000000..1213e85c7a --- /dev/null +++ b/app/javascript/controllers/application.js @@ -0,0 +1,9 @@ +import { Application } from "@hotwired/stimulus" + +const application = Application.start() + +// Configure Stimulus development experience +application.debug = false +window.Stimulus = application + +export { application } diff --git a/app/javascript/controllers/hello_controller.js b/app/javascript/controllers/hello_controller.js new file mode 100644 index 0000000000..5975c0789d --- /dev/null +++ b/app/javascript/controllers/hello_controller.js @@ -0,0 +1,7 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + connect() { + this.element.textContent = "Hello World!" + } +} diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js new file mode 100644 index 0000000000..1156bf8362 --- /dev/null +++ b/app/javascript/controllers/index.js @@ -0,0 +1,4 @@ +// Import and register all your controllers from the importmap via controllers/**/*_controller +import { application } from "controllers/application" +import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading" +eagerLoadControllersFrom("controllers", application) diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb new file mode 100644 index 0000000000..d394c3d106 --- /dev/null +++ b/app/jobs/application_job.rb @@ -0,0 +1,7 @@ +class ApplicationJob < ActiveJob::Base + # Automatically retry jobs that encountered a deadlock + # retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + # discard_on ActiveJob::DeserializationError +end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb new file mode 100644 index 0000000000..3c34c8148f --- /dev/null +++ b/app/mailers/application_mailer.rb @@ -0,0 +1,4 @@ +class ApplicationMailer < ActionMailer::Base + default from: "from@example.com" + layout "mailer" +end diff --git a/app/models/application_record.rb b/app/models/application_record.rb new file mode 100644 index 0000000000..b63caeb8a5 --- /dev/null +++ b/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + primary_abstract_class +end diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb new file mode 100644 index 0000000000..1455450e14 --- /dev/null +++ b/app/views/layouts/application.html.erb @@ -0,0 +1,28 @@ + + + + <%= content_for(:title) || "Camaar G1" %> + + + + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + + <%= yield :head %> + + <%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %> + <%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %> + + + + + + <%# Includes all stylesheet files in app/assets/stylesheets %> + <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %> + <%= javascript_importmap_tags %> + + + + <%= yield %> + + diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb new file mode 100644 index 0000000000..3aac9002ed --- /dev/null +++ b/app/views/layouts/mailer.html.erb @@ -0,0 +1,13 @@ + + + + + + + + + <%= yield %> + + diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb new file mode 100644 index 0000000000..37f0bddbd7 --- /dev/null +++ b/app/views/layouts/mailer.text.erb @@ -0,0 +1 @@ +<%= yield %> diff --git a/app/views/pwa/manifest.json.erb b/app/views/pwa/manifest.json.erb new file mode 100644 index 0000000000..d6cf3bbd6c --- /dev/null +++ b/app/views/pwa/manifest.json.erb @@ -0,0 +1,22 @@ +{ + "name": "CamaarG1", + "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": "CamaarG1.", + "theme_color": "red", + "background_color": "red" +} diff --git a/app/views/pwa/service-worker.js b/app/views/pwa/service-worker.js new file mode 100644 index 0000000000..b3a13fb7bb --- /dev/null +++ b/app/views/pwa/service-worker.js @@ -0,0 +1,26 @@ +// Add a service worker for processing Web Push notifications: +// +// self.addEventListener("push", async (event) => { +// const { title, options } = await event.data.json() +// event.waitUntil(self.registration.showNotification(title, options)) +// }) +// +// self.addEventListener("notificationclick", function(event) { +// event.notification.close() +// event.waitUntil( +// clients.matchAll({ type: "window" }).then((clientList) => { +// for (let i = 0; i < clientList.length; i++) { +// let client = clientList[i] +// let clientPath = (new URL(client.url)).pathname +// +// if (clientPath == event.notification.data.path && "focus" in client) { +// return client.focus() +// } +// } +// +// if (clients.openWindow) { +// return clients.openWindow(event.notification.data.path) +// } +// }) +// ) +// }) diff --git a/bin/brakeman b/bin/brakeman new file mode 100755 index 0000000000..ace1c9ba08 --- /dev/null +++ b/bin/brakeman @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby +require "rubygems" +require "bundler/setup" + +ARGV.unshift("--ensure-latest") + +load Gem.bin_path("brakeman", "brakeman") diff --git a/bin/dev b/bin/dev new file mode 100755 index 0000000000..5f91c20545 --- /dev/null +++ b/bin/dev @@ -0,0 +1,2 @@ +#!/usr/bin/env ruby +exec "./bin/rails", "server", *ARGV diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint new file mode 100755 index 0000000000..57567d69b4 --- /dev/null +++ b/bin/docker-entrypoint @@ -0,0 +1,14 @@ +#!/bin/bash -e + +# Enable jemalloc for reduced memory usage and latency. +if [ -z "${LD_PRELOAD+x}" ]; then + LD_PRELOAD=$(find /usr/lib -name libjemalloc.so.2 -print -quit) + export LD_PRELOAD +fi + +# If running the rails server then create or migrate existing database +if [ "${@: -2:1}" == "./bin/rails" ] && [ "${@: -1:1}" == "server" ]; then + ./bin/rails db:prepare +fi + +exec "${@}" diff --git a/bin/importmap b/bin/importmap new file mode 100755 index 0000000000..36502ab16c --- /dev/null +++ b/bin/importmap @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby + +require_relative "../config/application" +require "importmap/commands" diff --git a/bin/jobs b/bin/jobs new file mode 100755 index 0000000000..dcf59f309a --- /dev/null +++ b/bin/jobs @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby + +require_relative "../config/environment" +require "solid_queue/cli" + +SolidQueue::Cli.start(ARGV) diff --git a/bin/kamal b/bin/kamal new file mode 100755 index 0000000000..d9ba276702 --- /dev/null +++ b/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/bin/rails b/bin/rails new file mode 100755 index 0000000000..efc0377492 --- /dev/null +++ b/bin/rails @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +APP_PATH = File.expand_path("../config/application", __dir__) +require_relative "../config/boot" +require "rails/commands" diff --git a/bin/rake b/bin/rake new file mode 100755 index 0000000000..4fbf10b960 --- /dev/null +++ b/bin/rake @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +require_relative "../config/boot" +require "rake" +Rake.application.run diff --git a/bin/rubocop b/bin/rubocop new file mode 100755 index 0000000000..40330c0ff1 --- /dev/null +++ b/bin/rubocop @@ -0,0 +1,8 @@ +#!/usr/bin/env ruby +require "rubygems" +require "bundler/setup" + +# explicit rubocop config increases performance slightly while avoiding config confusion. +ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__)) + +load Gem.bin_path("rubocop", "rubocop") diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000000..be3db3c0d6 --- /dev/null +++ b/bin/setup @@ -0,0 +1,34 @@ +#!/usr/bin/env ruby +require "fileutils" + +APP_ROOT = File.expand_path("..", __dir__) + +def system!(*args) + system(*args, exception: true) +end + +FileUtils.chdir APP_ROOT do + # This script is a way to set up or update your development environment automatically. + # This script is idempotent, so that you can run it at any time and get an expectable outcome. + # Add necessary setup steps to this file. + + puts "== Installing dependencies ==" + system("bundle check") || system!("bundle install") + + # puts "\n== Copying sample files ==" + # unless File.exist?("config/database.yml") + # FileUtils.cp "config/database.yml.sample", "config/database.yml" + # end + + puts "\n== Preparing database ==" + system! "bin/rails db:prepare" + + puts "\n== Removing old logs and tempfiles ==" + system! "bin/rails log:clear tmp:clear" + + unless ARGV.include?("--skip-server") + puts "\n== Starting development server ==" + STDOUT.flush # flush the output before exec(2) so that it displays + exec "bin/dev" + end +end diff --git a/bin/thrust b/bin/thrust new file mode 100755 index 0000000000..36bde2d832 --- /dev/null +++ b/bin/thrust @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("thruster", "thrust") diff --git a/config.ru b/config.ru new file mode 100644 index 0000000000..4a3c09a688 --- /dev/null +++ b/config.ru @@ -0,0 +1,6 @@ +# This file is used by Rack-based servers to start the application. + +require_relative "config/environment" + +run Rails.application +Rails.application.load_server diff --git a/config/application.rb b/config/application.rb new file mode 100644 index 0000000000..72b52e0f11 --- /dev/null +++ b/config/application.rb @@ -0,0 +1,27 @@ +require_relative "boot" + +require "rails/all" + +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(*Rails.groups) + +module CamaarG1 + class Application < Rails::Application + # Initialize configuration defaults for originally generated Rails version. + config.load_defaults 8.0 + + # Please, add to the `ignore` list any other `lib` subdirectories that do + # not contain `.rb` files, or that should not be reloaded or eager loaded. + # Common ones are `templates`, `generators`, or `middleware`, for example. + config.autoload_lib(ignore: %w[assets tasks]) + + # Configuration for the application, engines, and railties goes here. + # + # These settings can be overridden in specific environments using the files + # in config/environments, which are processed later. + # + # config.time_zone = "Central Time (US & Canada)" + # config.eager_load_paths << Rails.root.join("extras") + end +end diff --git a/config/boot.rb b/config/boot.rb new file mode 100644 index 0000000000..988a5ddc46 --- /dev/null +++ b/config/boot.rb @@ -0,0 +1,4 @@ +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +require "bundler/setup" # Set up gems listed in the Gemfile. +require "bootsnap/setup" # Speed up boot time by caching expensive operations. diff --git a/config/cable.yml b/config/cable.yml new file mode 100644 index 0000000000..b9adc5aa3a --- /dev/null +++ b/config/cable.yml @@ -0,0 +1,17 @@ +# Async adapter only works within the same process, so for manually triggering cable updates from a console, +# and seeing results in the browser, you must do so from the web console (running inside the dev process), +# not a terminal started via bin/rails console! Add "console" to any action or any ERB template view +# to make the web console appear. +development: + adapter: async + +test: + adapter: test + +production: + adapter: solid_cable + connects_to: + database: + writing: cable + polling_interval: 0.1.seconds + message_retention: 1.day diff --git a/config/cache.yml b/config/cache.yml new file mode 100644 index 0000000000..19d490843b --- /dev/null +++ b/config/cache.yml @@ -0,0 +1,16 @@ +default: &default + store_options: + # Cap age of oldest cache entry to fulfill retention policies + # max_age: <%= 60.days.to_i %> + max_size: <%= 256.megabytes %> + namespace: <%= Rails.env %> + +development: + <<: *default + +test: + <<: *default + +production: + database: cache + <<: *default diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc new file mode 100644 index 0000000000..5e95166c07 --- /dev/null +++ b/config/credentials.yml.enc @@ -0,0 +1 @@ +dG82hj0yB5jlVXL3V13vnZl7iIWRvVPx4qMlMg5yWkOEd6QuE6HpPOjWcx8acScjxkTLiSdpoUV3dGk+R8BTa+gyX8VkVuJM4UzcLUMQ4YlFd5craaf7jlqH4oUul0VEuFpH4387REN8JdaHIZdtal4r0+5bOAnnxzCDs2twIbSAyl13Nns0QS+/kAzj4Ge8oowivPsLtax/HloUO5Ovi+U/fUjgHZFdpfVbl0M4YA5y2HUUEEw/uRbvvi8KVM+ZE4Oa3OU0d+I5N0i4Suj+CBUOeHN//wACcP1QIbHo3y5h4NSLYJpFNkwg4MIOIcJXmJf/VJLoYsXKsohF03ioYBbbb5SCAJHX8mCHzjk5KMfbSYZMJDeOGGKZ+ZfT+dSB3dFjLKtoV9VNYTQ+Ll+Cbk4iBLn5X9M4vojcM+Q+MuPPTXEw6k2+oR8wlRt49Dez1Ar2cye9/eMRoiTsQu60GlSlDpbg9zeQ9vP/igmBOOGmv9ZIlJevO5P8--tkNmt0c6GGbUYEKp--YnPJEHyaQ1KPNB4TkhYHjA== \ No newline at end of file diff --git a/config/database.yml b/config/database.yml new file mode 100644 index 0000000000..2640cb5f30 --- /dev/null +++ b/config/database.yml @@ -0,0 +1,41 @@ +# SQLite. Versions 3.8.0 and up are supported. +# gem install sqlite3 +# +# Ensure the SQLite 3 gem is defined in your Gemfile +# gem "sqlite3" +# +default: &default + adapter: sqlite3 + pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + timeout: 5000 + +development: + <<: *default + database: storage/development.sqlite3 + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + <<: *default + database: storage/test.sqlite3 + + +# Store production database in the storage/ directory, which by default +# is mounted as a persistent Docker volume in config/deploy.yml. +production: + primary: + <<: *default + database: storage/production.sqlite3 + cache: + <<: *default + database: storage/production_cache.sqlite3 + migrations_paths: db/cache_migrate + queue: + <<: *default + database: storage/production_queue.sqlite3 + migrations_paths: db/queue_migrate + cable: + <<: *default + database: storage/production_cable.sqlite3 + migrations_paths: db/cable_migrate diff --git a/config/deploy.yml b/config/deploy.yml new file mode 100644 index 0000000000..6188baa0b2 --- /dev/null +++ b/config/deploy.yml @@ -0,0 +1,116 @@ +# Name of your application. Used to uniquely configure containers. +service: camaar_g1 + +# Name of the container image. +image: your-user/camaar_g1 + +# Deploy to these servers. +servers: + web: + - 192.168.0.1 + # job: + # hosts: + # - 192.168.0.1 + # cmd: bin/jobs + +# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server. +# Remove this section when using multiple web servers and ensure you terminate SSL at your load balancer. +# +# Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption. +proxy: + ssl: true + host: app.example.com + +# Credentials for your image host. +registry: + # Specify the registry server, if you're not using Docker Hub + # server: registry.digitalocean.com / ghcr.io / ... + username: your-user + + # Always use an access token rather than real password when possible. + password: + - KAMAL_REGISTRY_PASSWORD + +# Inject ENV variables into containers (secrets come from .kamal/secrets). +env: + secret: + - RAILS_MASTER_KEY + clear: + # Run the Solid Queue Supervisor inside the web server's Puma process to do jobs. + # When you start using multiple servers, you should split out job processing to a dedicated machine. + SOLID_QUEUE_IN_PUMA: true + + # Set number of processes dedicated to Solid Queue (default: 1) + # JOB_CONCURRENCY: 3 + + # Set number of cores available to the application on each server (default: 1). + # WEB_CONCURRENCY: 2 + + # Match this to any external database server to configure Active Record correctly + # Use camaar_g1-db for a db accessory server on same machine via local kamal docker network. + # DB_HOST: 192.168.0.2 + + # Log everything from Rails + # RAILS_LOG_LEVEL: debug + +# Aliases are triggered with "bin/kamal ". You can overwrite arguments on invocation: +# "bin/kamal logs -r job" will tail logs from the first server in the job section. +aliases: + console: app exec --interactive --reuse "bin/rails console" + shell: app exec --interactive --reuse "bash" + logs: app logs -f + dbc: app exec --interactive --reuse "bin/rails dbconsole" + + +# Use a persistent storage volume for sqlite database files and local Active Storage files. +# Recommended to change this to a mounted volume path that is backed up off server. +volumes: + - "camaar_g1_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: 3.3.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: redis:7.0 +# host: 192.168.0.2 +# port: 6379 +# directories: +# - data:/data diff --git a/config/environment.rb b/config/environment.rb new file mode 100644 index 0000000000..cac5315775 --- /dev/null +++ b/config/environment.rb @@ -0,0 +1,5 @@ +# Load the Rails application. +require_relative "application" + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/config/environments/development.rb b/config/environments/development.rb new file mode 100644 index 0000000000..4cc21c4ebe --- /dev/null +++ b/config/environments/development.rb @@ -0,0 +1,72 @@ +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 + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + config.action_view.annotate_rendered_view_with_filenames = true + + # Uncomment if you wish to allow Action Cable access from any origin. + # config.action_cable.disable_request_forgery_protection = true + + # Raise error when a before_action's only/except options reference missing actions. + config.action_controller.raise_on_missing_callback_actions = true + + # Apply autocorrection by RuboCop to files generated by `bin/rails generate`. + # config.generators.apply_rubocop_autocorrect_after_generate! +end diff --git a/config/environments/production.rb b/config/environments/production.rb new file mode 100644 index 0000000000..bdcd01d1bf --- /dev/null +++ b/config/environments/production.rb @@ -0,0 +1,90 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Code is not reloaded between requests. + config.enable_reloading = false + + # Eager load code on boot for better performance and memory savings (ignored by Rake tasks). + config.eager_load = true + + # Full error reports are disabled. + config.consider_all_requests_local = false + + # Turn on fragment caching in view templates. + config.action_controller.perform_caching = true + + # Cache assets for far-future expiry since they are all digest stamped. + config.public_file_server.headers = { "cache-control" => "public, max-age=#{1.year.to_i}" } + + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.asset_host = "http://assets.example.com" + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + + # Assume all access to the app is happening through a SSL-terminating reverse proxy. + config.assume_ssl = true + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + config.force_ssl = true + + # Skip http-to-https redirect for the default health check endpoint. + # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } } + + # Log to STDOUT with the current request id as a default log tag. + config.log_tags = [ :request_id ] + config.logger = ActiveSupport::TaggedLogging.logger(STDOUT) + + # Change to "debug" to log everything (including potentially personally-identifiable information!) + config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") + + # Prevent health checks from clogging up the logs. + config.silence_healthcheck_path = "/up" + + # Don't log any deprecations. + config.active_support.report_deprecations = false + + # Replace the default in-process memory cache store with a durable alternative. + config.cache_store = :solid_cache_store + + # Replace the default in-process and non-durable queuing backend for Active Job. + config.active_job.queue_adapter = :solid_queue + config.solid_queue.connects_to = { database: { writing: :queue } } + + # Ignore bad email addresses and do not raise email delivery errors. + # Set this to true and configure the email server for immediate delivery to raise delivery errors. + # config.action_mailer.raise_delivery_errors = false + + # Set host to be used by links generated in mailer templates. + config.action_mailer.default_url_options = { host: "example.com" } + + # Specify outgoing SMTP server. Remember to add smtp/* credentials via rails credentials:edit. + # config.action_mailer.smtp_settings = { + # user_name: Rails.application.credentials.dig(:smtp, :user_name), + # password: Rails.application.credentials.dig(:smtp, :password), + # address: "smtp.example.com", + # port: 587, + # authentication: :plain + # } + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true + + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false + + # Only use :id for inspections in production. + config.active_record.attributes_for_inspect = [ :id ] + + # Enable DNS rebinding protection and other `Host` header attacks. + # config.hosts = [ + # "example.com", # Allow requests from example.com + # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` + # ] + # + # Skip DNS rebinding protection for the default health check endpoint. + # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } +end diff --git a/config/environments/test.rb b/config/environments/test.rb new file mode 100644 index 0000000000..c2095b1174 --- /dev/null +++ b/config/environments/test.rb @@ -0,0 +1,53 @@ +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # While tests run files are not watched, reloading is not necessary. + config.enable_reloading = false + + # Eager loading loads your entire application. When running a single test locally, + # this is usually not necessary, and can slow down your test suite. However, it's + # recommended that you enable it in continuous integration systems to ensure eager + # loading is working properly before deploying your code. + config.eager_load = ENV["CI"].present? + + # Configure public file server for tests with cache-control for performance. + config.public_file_server.headers = { "cache-control" => "public, max-age=3600" } + + # Show full error reports. + config.consider_all_requests_local = true + config.cache_store = :null_store + + # Render exception templates for rescuable exceptions and raise for other exceptions. + config.action_dispatch.show_exceptions = :rescuable + + # Disable request forgery protection in test environment. + config.action_controller.allow_forgery_protection = false + + # Store uploaded files on the local file system in a temporary directory. + config.active_storage.service = :test + + # Tell Action Mailer not to deliver emails to the real world. + # The :test delivery method accumulates sent emails in the + # ActionMailer::Base.deliveries array. + config.action_mailer.delivery_method = :test + + # Set host to be used by links generated in mailer templates. + config.action_mailer.default_url_options = { host: "example.com" } + + # Print deprecation notices to the stderr. + config.active_support.deprecation = :stderr + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true + + # Raise error when a before_action's only/except options reference missing actions. + config.action_controller.raise_on_missing_callback_actions = true +end diff --git a/config/importmap.rb b/config/importmap.rb new file mode 100644 index 0000000000..909dfc542d --- /dev/null +++ b/config/importmap.rb @@ -0,0 +1,7 @@ +# Pin npm packages by running ./bin/importmap + +pin "application" +pin "@hotwired/turbo-rails", to: "turbo.min.js" +pin "@hotwired/stimulus", to: "stimulus.min.js" +pin "@hotwired/stimulus-loading", to: "stimulus-loading.js" +pin_all_from "app/javascript/controllers", under: "controllers" diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb new file mode 100644 index 0000000000..487324424f --- /dev/null +++ b/config/initializers/assets.rb @@ -0,0 +1,7 @@ +# Be sure to restart your server when you modify this file. + +# Version of your assets, change this if you want to expire all your assets. +Rails.application.config.assets.version = "1.0" + +# Add additional assets to the asset load path. +# Rails.application.config.assets.paths << Emoji.images_path diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb new file mode 100644 index 0000000000..b3076b38fe --- /dev/null +++ b/config/initializers/content_security_policy.rb @@ -0,0 +1,25 @@ +# Be sure to restart your server when you modify this file. + +# Define an application-wide content security policy. +# See the Securing Rails Applications Guide for more information: +# https://guides.rubyonrails.org/security.html#content-security-policy-header + +# Rails.application.configure do +# config.content_security_policy do |policy| +# policy.default_src :self, :https +# policy.font_src :self, :https, :data +# policy.img_src :self, :https, :data +# policy.object_src :none +# policy.script_src :self, :https +# policy.style_src :self, :https +# # Specify URI for violation reports +# # policy.report_uri "/csp-violation-report-endpoint" +# end +# +# # Generate session nonces for permitted importmap, inline scripts, and inline styles. +# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } +# config.content_security_policy_nonce_directives = %w(script-src style-src) +# +# # Report violations without enforcing the policy. +# # config.content_security_policy_report_only = true +# end diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb new file mode 100644 index 0000000000..c0b717f7ec --- /dev/null +++ b/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. +# Use this to limit dissemination of sensitive information. +# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. +Rails.application.config.filter_parameters += [ + :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc +] diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb new file mode 100644 index 0000000000..3860f659ea --- /dev/null +++ b/config/initializers/inflections.rb @@ -0,0 +1,16 @@ +# Be sure to restart your server when you modify this file. + +# Add new inflection rules using the following format. Inflections +# are locale specific, and you may define rules for as many different +# locales as you wish. All of these examples are active by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.plural /^(ox)$/i, "\\1en" +# inflect.singular /^(ox)en/i, "\\1" +# inflect.irregular "person", "people" +# inflect.uncountable %w( fish sheep ) +# end + +# These inflection rules are supported but not enabled by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.acronym "RESTful" +# end diff --git a/config/locales/en.yml b/config/locales/en.yml new file mode 100644 index 0000000000..6c349ae5e3 --- /dev/null +++ b/config/locales/en.yml @@ -0,0 +1,31 @@ +# Files in the config/locales directory are used for internationalization and +# are automatically loaded by Rails. If you want to use locales other than +# English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t "hello" +# +# In views, this is aliased to just `t`: +# +# <%= t("hello") %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# To learn more about the API, please read the Rails Internationalization guide +# at https://guides.rubyonrails.org/i18n.html. +# +# Be aware that YAML interprets the following case-insensitive strings as +# booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings +# must be quoted to be interpreted as strings. For example: +# +# en: +# "yes": yup +# enabled: "ON" + +en: + hello: "Hello world" diff --git a/config/puma.rb b/config/puma.rb new file mode 100644 index 0000000000..a248513b24 --- /dev/null +++ b/config/puma.rb @@ -0,0 +1,41 @@ +# This configuration file will be evaluated by Puma. The top-level methods that +# are invoked here are part of Puma's configuration DSL. For more information +# about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. +# +# Puma starts a configurable number of processes (workers) and each process +# serves each request in a thread from an internal thread pool. +# +# You can control the number of workers using ENV["WEB_CONCURRENCY"]. You +# should only set this value when you want to run 2 or more workers. The +# default is already 1. +# +# The ideal number of threads per worker depends both on how much time the +# application spends waiting for IO operations and on how much you wish to +# prioritize throughput over latency. +# +# As a rule of thumb, increasing the number of threads will increase how much +# traffic a given process can handle (throughput), but due to CRuby's +# Global VM Lock (GVL) it has diminishing returns and will degrade the +# response time (latency) of the application. +# +# The default is set to 3 threads as it's deemed a decent compromise between +# throughput and latency for the average Rails application. +# +# Any libraries that use a connection pool or another resource pool should +# be configured to provide at least as many connections as the number of +# threads. This includes Active Record's `pool` parameter in `database.yml`. +threads_count = ENV.fetch("RAILS_MAX_THREADS", 3) +threads threads_count, threads_count + +# Specifies the `port` that Puma will listen on to receive requests; default is 3000. +port ENV.fetch("PORT", 3000) + +# Allow puma to be restarted by `bin/rails restart` command. +plugin :tmp_restart + +# Run the Solid Queue supervisor inside of Puma for single-server deployments +plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"] + +# Specify the PID file. Defaults to tmp/pids/server.pid in development. +# In other environments, only set the PID file if requested. +pidfile ENV["PIDFILE"] if ENV["PIDFILE"] diff --git a/config/queue.yml b/config/queue.yml new file mode 100644 index 0000000000..9eace59c41 --- /dev/null +++ b/config/queue.yml @@ -0,0 +1,18 @@ +default: &default + dispatchers: + - polling_interval: 1 + batch_size: 500 + workers: + - queues: "*" + threads: 3 + processes: <%= ENV.fetch("JOB_CONCURRENCY", 1) %> + polling_interval: 0.1 + +development: + <<: *default + +test: + <<: *default + +production: + <<: *default diff --git a/config/recurring.yml b/config/recurring.yml new file mode 100644 index 0000000000..b4207f9b07 --- /dev/null +++ b/config/recurring.yml @@ -0,0 +1,15 @@ +# examples: +# periodic_cleanup: +# class: CleanSoftDeletedRecordsJob +# queue: background +# args: [ 1000, { batch_size: 500 } ] +# schedule: every hour +# periodic_cleanup_with_command: +# command: "SoftDeletedRecord.due.delete_all" +# priority: 2 +# schedule: at 5am every day + +production: + clear_solid_queue_finished_jobs: + command: "SolidQueue::Job.clear_finished_in_batches(sleep_between_batches: 0.3)" + schedule: every hour at minute 12 diff --git a/config/routes.rb b/config/routes.rb new file mode 100644 index 0000000000..48254e88ed --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,14 @@ +Rails.application.routes.draw do + # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html + + # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. + # Can be used by load balancers and uptime monitors to verify that the app is live. + get "up" => "rails/health#show", as: :rails_health_check + + # Render dynamic PWA files from app/views/pwa/* (remember to link manifest in application.html.erb) + # get "manifest" => "rails/pwa#manifest", as: :pwa_manifest + # get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker + + # Defines the root path route ("/") + # root "posts#index" +end diff --git a/config/storage.yml b/config/storage.yml new file mode 100644 index 0000000000..4942ab6694 --- /dev/null +++ b/config/storage.yml @@ -0,0 +1,34 @@ +test: + service: Disk + root: <%= Rails.root.join("tmp/storage") %> + +local: + service: Disk + root: <%= Rails.root.join("storage") %> + +# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) +# amazon: +# service: S3 +# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> +# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> +# region: us-east-1 +# bucket: your_own_bucket-<%= Rails.env %> + +# Remember not to checkin your GCS keyfile to a repository +# google: +# service: GCS +# project: your_project +# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> +# bucket: your_own_bucket-<%= Rails.env %> + +# Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) +# microsoft: +# service: AzureStorage +# storage_account_name: your_account_name +# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> +# container: your_container_name-<%= Rails.env %> + +# mirror: +# service: Mirror +# primary: local +# mirrors: [ amazon, google, microsoft ] diff --git a/db/cable_schema.rb b/db/cable_schema.rb new file mode 100644 index 0000000000..23666604a5 --- /dev/null +++ b/db/cable_schema.rb @@ -0,0 +1,11 @@ +ActiveRecord::Schema[7.1].define(version: 1) do + create_table "solid_cable_messages", force: :cascade do |t| + t.binary "channel", limit: 1024, null: false + t.binary "payload", limit: 536870912, null: false + t.datetime "created_at", null: false + t.integer "channel_hash", limit: 8, null: false + t.index ["channel"], name: "index_solid_cable_messages_on_channel" + t.index ["channel_hash"], name: "index_solid_cable_messages_on_channel_hash" + t.index ["created_at"], name: "index_solid_cable_messages_on_created_at" + end +end diff --git a/db/cache_schema.rb b/db/cache_schema.rb new file mode 100644 index 0000000000..81a410d188 --- /dev/null +++ b/db/cache_schema.rb @@ -0,0 +1,12 @@ +ActiveRecord::Schema[7.2].define(version: 1) do + create_table "solid_cache_entries", force: :cascade do |t| + t.binary "key", limit: 1024, null: false + t.binary "value", limit: 536870912, null: false + t.datetime "created_at", null: false + t.integer "key_hash", limit: 8, null: false + t.integer "byte_size", limit: 4, null: false + t.index ["byte_size"], name: "index_solid_cache_entries_on_byte_size" + t.index ["key_hash", "byte_size"], name: "index_solid_cache_entries_on_key_hash_and_byte_size" + t.index ["key_hash"], name: "index_solid_cache_entries_on_key_hash", unique: true + end +end diff --git a/db/queue_schema.rb b/db/queue_schema.rb new file mode 100644 index 0000000000..85194b6a88 --- /dev/null +++ b/db/queue_schema.rb @@ -0,0 +1,129 @@ +ActiveRecord::Schema[7.1].define(version: 1) do + create_table "solid_queue_blocked_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.string "queue_name", null: false + t.integer "priority", default: 0, null: false + t.string "concurrency_key", null: false + t.datetime "expires_at", null: false + t.datetime "created_at", null: false + t.index [ "concurrency_key", "priority", "job_id" ], name: "index_solid_queue_blocked_executions_for_release" + t.index [ "expires_at", "concurrency_key" ], name: "index_solid_queue_blocked_executions_for_maintenance" + t.index [ "job_id" ], name: "index_solid_queue_blocked_executions_on_job_id", unique: true + end + + create_table "solid_queue_claimed_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.bigint "process_id" + t.datetime "created_at", null: false + t.index [ "job_id" ], name: "index_solid_queue_claimed_executions_on_job_id", unique: true + t.index [ "process_id", "job_id" ], name: "index_solid_queue_claimed_executions_on_process_id_and_job_id" + end + + create_table "solid_queue_failed_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.text "error" + t.datetime "created_at", null: false + t.index [ "job_id" ], name: "index_solid_queue_failed_executions_on_job_id", unique: true + end + + create_table "solid_queue_jobs", force: :cascade do |t| + t.string "queue_name", null: false + t.string "class_name", null: false + t.text "arguments" + t.integer "priority", default: 0, null: false + t.string "active_job_id" + t.datetime "scheduled_at" + t.datetime "finished_at" + t.string "concurrency_key" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index [ "active_job_id" ], name: "index_solid_queue_jobs_on_active_job_id" + t.index [ "class_name" ], name: "index_solid_queue_jobs_on_class_name" + t.index [ "finished_at" ], name: "index_solid_queue_jobs_on_finished_at" + t.index [ "queue_name", "finished_at" ], name: "index_solid_queue_jobs_for_filtering" + t.index [ "scheduled_at", "finished_at" ], name: "index_solid_queue_jobs_for_alerting" + end + + create_table "solid_queue_pauses", force: :cascade do |t| + t.string "queue_name", null: false + t.datetime "created_at", null: false + t.index [ "queue_name" ], name: "index_solid_queue_pauses_on_queue_name", unique: true + end + + create_table "solid_queue_processes", force: :cascade do |t| + t.string "kind", null: false + t.datetime "last_heartbeat_at", null: false + t.bigint "supervisor_id" + t.integer "pid", null: false + t.string "hostname" + t.text "metadata" + t.datetime "created_at", null: false + t.string "name", null: false + t.index [ "last_heartbeat_at" ], name: "index_solid_queue_processes_on_last_heartbeat_at" + t.index [ "name", "supervisor_id" ], name: "index_solid_queue_processes_on_name_and_supervisor_id", unique: true + t.index [ "supervisor_id" ], name: "index_solid_queue_processes_on_supervisor_id" + end + + create_table "solid_queue_ready_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.string "queue_name", null: false + t.integer "priority", default: 0, null: false + t.datetime "created_at", null: false + t.index [ "job_id" ], name: "index_solid_queue_ready_executions_on_job_id", unique: true + t.index [ "priority", "job_id" ], name: "index_solid_queue_poll_all" + t.index [ "queue_name", "priority", "job_id" ], name: "index_solid_queue_poll_by_queue" + end + + create_table "solid_queue_recurring_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.string "task_key", null: false + t.datetime "run_at", null: false + t.datetime "created_at", null: false + t.index [ "job_id" ], name: "index_solid_queue_recurring_executions_on_job_id", unique: true + t.index [ "task_key", "run_at" ], name: "index_solid_queue_recurring_executions_on_task_key_and_run_at", unique: true + end + + create_table "solid_queue_recurring_tasks", force: :cascade do |t| + t.string "key", null: false + t.string "schedule", null: false + t.string "command", limit: 2048 + t.string "class_name" + t.text "arguments" + t.string "queue_name" + t.integer "priority", default: 0 + t.boolean "static", default: true, null: false + t.text "description" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index [ "key" ], name: "index_solid_queue_recurring_tasks_on_key", unique: true + t.index [ "static" ], name: "index_solid_queue_recurring_tasks_on_static" + end + + create_table "solid_queue_scheduled_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.string "queue_name", null: false + t.integer "priority", default: 0, null: false + t.datetime "scheduled_at", null: false + t.datetime "created_at", null: false + t.index [ "job_id" ], name: "index_solid_queue_scheduled_executions_on_job_id", unique: true + t.index [ "scheduled_at", "priority", "job_id" ], name: "index_solid_queue_dispatch_all" + end + + create_table "solid_queue_semaphores", force: :cascade do |t| + t.string "key", null: false + t.integer "value", default: 1, null: false + t.datetime "expires_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index [ "expires_at" ], name: "index_solid_queue_semaphores_on_expires_at" + t.index [ "key", "value" ], name: "index_solid_queue_semaphores_on_key_and_value" + t.index [ "key" ], name: "index_solid_queue_semaphores_on_key", unique: true + end + + add_foreign_key "solid_queue_blocked_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_claimed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_failed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_ready_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_recurring_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_scheduled_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade +end diff --git a/db/seeds.rb b/db/seeds.rb new file mode 100644 index 0000000000..4fbd6ed970 --- /dev/null +++ b/db/seeds.rb @@ -0,0 +1,9 @@ +# This file should ensure the existence of records required to run the application in every environment (production, +# development, test). The code here should be idempotent so that it can be executed at any point in every environment. +# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). +# +# Example: +# +# ["Action", "Comedy", "Drama", "Horror"].each do |genre_name| +# MovieGenre.find_or_create_by!(name: genre_name) +# end diff --git a/lib/tasks/.keep b/lib/tasks/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/log/.keep b/log/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/public/400.html b/public/400.html new file mode 100644 index 0000000000..282dbc8cc9 --- /dev/null +++ b/public/400.html @@ -0,0 +1,114 @@ + + + + + + + The server cannot process the request due to a client error (400 Bad Request) + + + + + + + + + + + + + +
+
+ +
+
+

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

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

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

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

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

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

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

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

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

+
+
+ + + + diff --git a/public/icon.png b/public/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c4c9dbfbbd2f7c1421ffd5727188146213abbcef GIT binary patch literal 4166 zcmd6qU;WFw?|v@m)Sk^&NvB8tcujdV-r1b=i(NJxn&7{KTb zX$3(M+3TP2o^#KAo{#tIjl&t~(8D-k004kqPglzn0HFG(Q~(I*AKsD#M*g7!XK0T7 zN6P7j>HcT8rZgKl$v!xr806dyN19Bd4C0x_R*I-a?#zsTvb_89cyhuC&T**i|Rc zq5b8M;+{8KvoJ~uj9`u~d_f6`V&3+&ZX9x5pc8s)d175;@pjm(?dapmBcm0&vl9+W zx1ZD2o^nuyUHWj|^A8r>lUorO`wFF;>9XL-Jy!P}UXC{(z!FO%SH~8k`#|9;Q|eue zqWL0^Bp(fg_+Pkm!fDKRSY;+^@BF?AJE zCUWpXPst~hi_~u)SzYBDZroR+Z4xeHIlm_3Yc_9nZ(o_gg!jDgVa=E}Y8uDgem9`b zf=mfJ_@(BXSkW53B)F2s!&?_R4ptb1fYXlF++@vPhd=marQgEGRZS@B4g1Mu?euknL= z67P~tZ?*>-Hmi7GwlisNHHJDku-dSm7g@!=a}9cSL6Pa^w^2?&?$Oi8ibrr>w)xqx zOH_EMU@m05)9kuNR>>4@H%|){U$^yvVQ(YgOlh;5oU_-vivG-p4=LrN-k7D?*?u1u zsWly%tfAzKd6Fb=`eU2un_uaTXmcT#tlOL+aRS=kZZf}A7qT8lvcTx~7j` z*b>=z)mwg7%B2_!D0!1IZ?Nq{^Y$uI4Qx*6T!E2Col&2{k?ImCO=dD~A&9f9diXy^$x{6CwkBimn|1E09 zAMSezYtiL?O6hS37KpvDM?22&d{l)7h-!F)C-d3j8Z`c@($?mfd{R82)H>Qe`h{~G z!I}(2j(|49{LR?w4Jspl_i!(4T{31|dqCOpI52r5NhxYV+cDAu(xp*4iqZ2e-$YP= zoFOPmm|u*7C?S{Fp43y+V;>~@FFR76bCl@pTtyB93vNWy5yf;HKr8^0d7&GVIslYm zo3Tgt@M!`8B6IW&lK{Xk>%zp41G%`(DR&^u z5^pwD4>E6-w<8Kl2DzJ%a@~QDE$(e87lNhy?-Qgep!$b?5f7+&EM7$e>|WrX+=zCb z=!f5P>MxFyy;mIRxjc(H*}mceXw5a*IpC0PEYJ8Y3{JdoIW)@t97{wcUB@u+$FCCO z;s2Qe(d~oJC^`m$7DE-dsha`glrtu&v&93IZadvl_yjp!c89>zo;Krk+d&DEG4?x$ zufC1n+c1XD7dolX1q|7}uelR$`pT0Z)1jun<39$Sn2V5g&|(j~Z!wOddfYiZo7)A< z!dK`aBHOOk+-E_xbWCA3VR-+o$i5eO9`rMI#p_0xQ}rjEpGW;U!&&PKnivOcG(|m9 z!C8?WC6nCXw25WVa*eew)zQ=h45k8jSIPbq&?VE{oG%?4>9rwEeB4&qe#?-y_es4c|7ufw%+H5EY#oCgv!Lzv291#-oNlX~X+Jl5(riC~r z=0M|wMOP)Tt8@hNg&%V@Z9@J|Q#K*hE>sr6@oguas9&6^-=~$*2Gs%h#GF@h)i=Im z^iKk~ipWJg1VrvKS;_2lgs3n1zvNvxb27nGM=NXE!D4C!U`f*K2B@^^&ij9y}DTLB*FI zEnBL6y{jc?JqXWbkIZd7I16hA>(f9T!iwbIxJj~bKPfrO;>%*5nk&Lf?G@c2wvGrY&41$W{7HM9+b@&XY@>NZM5s|EK_Dp zQX60CBuantx>|d#DsaZ*8MW(we|#KTYZ=vNa#d*DJQe6hr~J6{_rI#?wi@s|&O}FR zG$kfPxheXh1?IZ{bDT-CWB4FTvO-k5scW^mi8?iY5Q`f8JcnnCxiy@m@D-%lO;y0pTLhh6i6l@x52j=#^$5_U^os}OFg zzdHbo(QI`%9#o*r8GCW~T3UdV`szO#~)^&X_(VW>o~umY9-ns9-V4lf~j z`QBD~pJ4a#b`*6bJ^3RS5y?RAgF7K5$ll97Y8#WZduZ`j?IEY~H(s^doZg>7-tk*t z4_QE1%%bb^p~4F5SB$t2i1>DBG1cIo;2(xTaj*Y~hlM{tSDHojL-QPg%Mo%6^7FrpB*{ z4G0@T{-77Por4DCMF zB_5Y~Phv%EQ64W8^GS6h?x6xh;w2{z3$rhC;m+;uD&pR74j+i22P5DS-tE8ABvH(U~indEbBUTAAAXfHZg5QpB@TgV9eI<)JrAkOI z8!TSOgfAJiWAXeM&vR4Glh;VxH}WG&V$bVb`a`g}GSpwggti*&)taV1@Ak|{WrV|5 zmNYx)Ans=S{c52qv@+jmGQ&vd6>6yX6IKq9O$3r&0xUTdZ!m1!irzn`SY+F23Rl6# zFRxws&gV-kM1NX(3(gnKpGi0Q)Dxi~#?nyzOR9!en;Ij>YJZVFAL*=R%7y%Mz9hU% zs>+ZB?qRmZ)nISx7wxY)y#cd$iaC~{k0avD>BjyF1q^mNQ1QcwsxiTySe<6C&cC6P zE`vwO9^k-d`9hZ!+r@Jnr+MF*2;2l8WjZ}DrwDUHzSF{WoG zucbSWguA!3KgB3MU%HH`R;XqVv0CcaGq?+;v_A5A2kpmk5V%qZE3yzQ7R5XWhq=eR zyUezH=@V)y>L9T-M-?tW(PQYTRBKZSVb_!$^H-Pn%ea;!vS_?M<~Tm>_rWIW43sPW z=!lY&fWc1g7+r?R)0p8(%zp&vl+FK4HRkns%BW+Up&wK8!lQ2~bja|9bD12WrKn#M zK)Yl9*8$SI7MAwSK$%)dMd>o+1UD<2&aQMhyjS5R{-vV+M;Q4bzl~Z~=4HFj_#2V9 zB)Gfzx3ncy@uzx?yzi}6>d%-?WE}h7v*w)Jr_gBl!2P&F3DX>j_1#--yjpL%<;JMR z*b70Gr)MMIBWDo~#<5F^Q0$VKI;SBIRneuR7)yVsN~A9I@gZTXe)E?iVII+X5h0~H zx^c(fP&4>!*q>fb6dAOC?MI>Cz3kld#J*;uik+Ps49cwm1B4 zZc1|ZxYyTv;{Z!?qS=D)sgRKx^1AYf%;y_V&VgZglfU>d+Ufk5&LV$sKv}Hoj+s; xK3FZRYdhbXT_@RW*ff3@`D1#ps#~H)p+y&j#(J|vk^lW{fF9OJt5(B-_&*Xgn9~3N literal 0 HcmV?d00001 diff --git a/public/icon.svg b/public/icon.svg new file mode 100644 index 0000000000..04b34bf83f --- /dev/null +++ b/public/icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000000..c19f78ab68 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1 @@ +# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file diff --git a/script/.keep b/script/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/storage/.keep b/storage/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb new file mode 100644 index 0000000000..cee29fd214 --- /dev/null +++ b/test/application_system_test_case.rb @@ -0,0 +1,5 @@ +require "test_helper" + +class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + driven_by :selenium, using: :headless_chrome, screen_size: [ 1400, 1400 ] +end diff --git a/test/controllers/.keep b/test/controllers/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/fixtures/files/.keep b/test/fixtures/files/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/helpers/.keep b/test/helpers/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/integration/.keep b/test/integration/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/mailers/.keep b/test/mailers/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/models/.keep b/test/models/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/system/.keep b/test/system/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 0000000000..0c22470ec1 --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,15 @@ +ENV["RAILS_ENV"] ||= "test" +require_relative "../config/environment" +require "rails/test_help" + +module ActiveSupport + class TestCase + # Run tests in parallel with specified workers + parallelize(workers: :number_of_processors) + + # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. + fixtures :all + + # Add more helper methods to be used by all tests here... + end +end diff --git a/tmp/.keep b/tmp/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tmp/pids/.keep b/tmp/pids/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tmp/storage/.keep b/tmp/storage/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/vendor/.keep b/vendor/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/vendor/javascript/.keep b/vendor/javascript/.keep new file mode 100644 index 0000000000..e69de29bb2 From 22a463cf4aad1371308c66c7a34cfd9daaa2c3d6 Mon Sep 17 00:00:00 2001 From: mariosantos-05 Date: Thu, 13 Nov 2025 19:34:38 -0300 Subject: [PATCH 002/100] FEAT(add): Cucumber config --- Gemfile | 9 +++- Gemfile.lock | 49 +++++++++++++++++++++ bin/cucumber | 11 +++++ config/cucumber.yml | 8 ++++ config/environments/development.rb | 4 ++ config/environments/test.rb | 4 ++ features/step_definitions/.keep | 0 features/support/env.rb | 53 +++++++++++++++++++++++ lib/tasks/cucumber.rake | 69 ++++++++++++++++++++++++++++++ 9 files changed, 205 insertions(+), 2 deletions(-) create mode 100755 bin/cucumber create mode 100644 config/cucumber.yml create mode 100644 features/step_definitions/.keep create mode 100644 features/support/env.rb create mode 100644 lib/tasks/cucumber.rake diff --git a/Gemfile b/Gemfile index ed45b5b828..03a0aba365 100644 --- a/Gemfile +++ b/Gemfile @@ -56,8 +56,13 @@ group :development do gem "web-console" end +# Testing gems group :test do - # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing] + # Rails default system testing gem "capybara" gem "selenium-webdriver" -end + + # Cucumber support + gem "cucumber-rails", require: false + gem "database_cleaner-active_record" +end \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index 75d2d41720..aedc81f092 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -99,10 +99,44 @@ GEM concurrent-ruby (1.3.5) connection_pool (2.5.4) crass (1.0.6) + cucumber (10.1.1) + base64 (~> 0.2) + builder (~> 3.2) + cucumber-ci-environment (> 9, < 11) + cucumber-core (> 15, < 17) + cucumber-cucumber-expressions (> 17, < 19) + cucumber-html-formatter (> 20.3, < 22) + diff-lcs (~> 1.5) + logger (~> 1.6) + mini_mime (~> 1.1) + multi_test (~> 1.1) + sys-uname (~> 1.3) + cucumber-ci-environment (10.0.1) + cucumber-core (15.3.0) + cucumber-gherkin (> 27, < 35) + cucumber-messages (> 26, < 30) + cucumber-tag-expressions (> 5, < 9) + cucumber-cucumber-expressions (18.0.1) + bigdecimal + cucumber-gherkin (34.0.0) + cucumber-messages (> 25, < 29) + cucumber-html-formatter (21.15.1) + cucumber-messages (> 19, < 28) + cucumber-messages (27.2.0) + cucumber-rails (4.0.0) + capybara (>= 3.25, < 4) + cucumber (>= 7, < 11) + railties (>= 6.1, < 9) + cucumber-tag-expressions (8.0.0) + database_cleaner-active_record (2.2.2) + activerecord (>= 5.a) + database_cleaner-core (~> 2.0) + database_cleaner-core (2.0.1) date (3.5.0) debug (1.11.0) irb (~> 1.10) reline (>= 0.3.8) + diff-lcs (1.6.2) dotenv (3.1.8) drb (2.2.3) ed25519 (1.4.0) @@ -110,6 +144,14 @@ GEM erubi (1.13.1) et-orbi (1.4.0) tzinfo + ffi (1.17.2-aarch64-linux-gnu) + ffi (1.17.2-aarch64-linux-musl) + ffi (1.17.2-arm-linux-gnu) + ffi (1.17.2-arm-linux-musl) + ffi (1.17.2-arm64-darwin) + ffi (1.17.2-x86_64-darwin) + ffi (1.17.2-x86_64-linux-gnu) + ffi (1.17.2-x86_64-linux-musl) fugit (1.12.1) et-orbi (~> 1.4) raabro (~> 1.4) @@ -155,9 +197,11 @@ GEM net-smtp marcel (1.1.0) matrix (0.4.3) + memoist3 (1.0.0) mini_mime (1.1.5) minitest (5.26.1) msgpack (1.8.0) + multi_test (1.1.0) net-imap (0.5.12) date net-protocol @@ -329,6 +373,9 @@ GEM stimulus-rails (1.3.4) railties (>= 6.0.0) stringio (3.1.8) + sys-uname (1.4.1) + ffi (~> 1.1) + memoist3 (~> 1.0.0) thor (1.4.0) thruster (0.1.16) thruster (0.1.16-aarch64-linux) @@ -377,6 +424,8 @@ DEPENDENCIES bootsnap brakeman capybara + cucumber-rails + database_cleaner-active_record debug importmap-rails jbuilder diff --git a/bin/cucumber b/bin/cucumber new file mode 100755 index 0000000000..eb5e962e86 --- /dev/null +++ b/bin/cucumber @@ -0,0 +1,11 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +vendored_cucumber_bin = Dir["#{File.dirname(__FILE__)}/../vendor/{gems,plugins}/cucumber*/bin/cucumber"].first +if vendored_cucumber_bin + load File.expand_path(vendored_cucumber_bin) +else + require 'rubygems' unless ENV['NO_RUBYGEMS'] + require 'cucumber' + load Cucumber::BINARY +end diff --git a/config/cucumber.yml b/config/cucumber.yml new file mode 100644 index 0000000000..47a4663ae2 --- /dev/null +++ b/config/cucumber.yml @@ -0,0 +1,8 @@ +<% +rerun = File.file?('rerun.txt') ? IO.read('rerun.txt') : "" +rerun = rerun.strip.gsub /\s/, ' ' +rerun_opts = rerun.empty? ? "--format #{ENV['CUCUMBER_FORMAT'] || 'progress'} features" : "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} #{rerun}" +std_opts = "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} --strict --tags 'not @wip'" +%> +default: <%= std_opts %> features +rerun: <%= rerun_opts %> --format rerun --out rerun.txt --strict --tags 'not @wip' diff --git a/config/environments/development.rb b/config/environments/development.rb index 4cc21c4ebe..1a0bf09e40 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -1,6 +1,10 @@ require "active_support/core_ext/integer/time" Rails.application.configure do + # Configure 'rails notes' to inspect Cucumber files + config.annotations.register_directories('features') + config.annotations.register_extensions('feature') { |tag| /#\s*(#{tag}):?\s*(.*)$/ } + # Settings specified here will take precedence over those in config/application.rb. # Make code changes take effect immediately without server restart. diff --git a/config/environments/test.rb b/config/environments/test.rb index c2095b1174..e6b5c1b020 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -4,6 +4,10 @@ # and recreated between test runs. Don't rely on the data there! Rails.application.configure do + # Configure 'rails notes' to inspect Cucumber files + config.annotations.register_directories('features') + config.annotations.register_extensions('feature') { |tag| /#\s*(#{tag}):?\s*(.*)$/ } + # Settings specified here will take precedence over those in config/application.rb. # While tests run files are not watched, reloading is not necessary. diff --git a/features/step_definitions/.keep b/features/step_definitions/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/features/support/env.rb b/features/support/env.rb new file mode 100644 index 0000000000..3b97d14087 --- /dev/null +++ b/features/support/env.rb @@ -0,0 +1,53 @@ +# IMPORTANT: This file is generated by cucumber-rails - edit at your own peril. +# It is recommended to regenerate this file in the future when you upgrade to a +# newer version of cucumber-rails. Consider adding your own code to a new file +# instead of editing this one. Cucumber will automatically load all features/**/*.rb +# files. + + +require 'cucumber/rails' + +# By default, any exception happening in your Rails application will bubble up +# to Cucumber so that your scenario will fail. This is a different from how +# your application behaves in the production environment, where an error page will +# be rendered instead. +# +# Sometimes we want to override this default behaviour and allow Rails to rescue +# exceptions and display an error page (just like when the app is running in production). +# Typical scenarios where you want to do this is when you test your error pages. +# There are two ways to allow Rails to rescue exceptions: +# +# 1) Tag your scenario (or feature) with @allow-rescue +# +# 2) Set the value below to true. Beware that doing this globally is not +# recommended as it will mask a lot of errors for you! +# +ActionController::Base.allow_rescue = false + +# Remove/comment out the lines below if your app doesn't have a database. +# For some databases (like MongoDB and CouchDB) you may need to use :truncation instead. +begin + DatabaseCleaner.strategy = :transaction +rescue NameError + raise "You need to add database_cleaner to your Gemfile (in the :test group) if you wish to use it." +end + +# You may also want to configure DatabaseCleaner to use different strategies for certain features and scenarios. +# See the DatabaseCleaner documentation for details. Example: +# +# Before('@no-txn,@selenium,@culerity,@celerity,@javascript') do +# # { except: [:widgets] } may not do what you expect here +# # as Cucumber::Rails::Database.javascript_strategy overrides +# # this setting. +# DatabaseCleaner.strategy = :truncation +# end +# +# Before('not @no-txn', 'not @selenium', 'not @culerity', 'not @celerity', 'not @javascript') do +# DatabaseCleaner.strategy = :transaction +# end +# + +# Possible values are :truncation and :transaction +# The :transaction strategy is faster, but might give you threading problems. +# See https://github.com/cucumber/cucumber-rails/blob/master/features/choose_javascript_database_strategy.feature +Cucumber::Rails::Database.javascript_strategy = :truncation diff --git a/lib/tasks/cucumber.rake b/lib/tasks/cucumber.rake new file mode 100644 index 0000000000..0caa4d2553 --- /dev/null +++ b/lib/tasks/cucumber.rake @@ -0,0 +1,69 @@ +# IMPORTANT: This file is generated by cucumber-rails - edit at your own peril. +# It is recommended to regenerate this file in the future when you upgrade to a +# newer version of cucumber-rails. Consider adding your own code to a new file +# instead of editing this one. Cucumber will automatically load all features/**/*.rb +# files. + + +unless ARGV.any? {|a| a =~ /^gems/} # Don't load anything when running the gems:* tasks + +vendored_cucumber_bin = Dir["#{Rails.root}/vendor/{gems,plugins}/cucumber*/bin/cucumber"].first +$LOAD_PATH.unshift(File.dirname(vendored_cucumber_bin) + '/../lib') unless vendored_cucumber_bin.nil? + +begin + require 'cucumber/rake/task' + + namespace :cucumber do + Cucumber::Rake::Task.new({ok: 'test:prepare'}, 'Run features that should pass') do |t| + t.binary = vendored_cucumber_bin # If nil, the gem's binary is used. + t.fork = true # You may get faster startup if you set this to false + t.profile = 'default' + end + + Cucumber::Rake::Task.new({wip: 'test:prepare'}, 'Run features that are being worked on') do |t| + t.binary = vendored_cucumber_bin + t.fork = true # You may get faster startup if you set this to false + t.profile = 'wip' + end + + Cucumber::Rake::Task.new({rerun: 'test:prepare'}, 'Record failing features and run only them if any exist') do |t| + t.binary = vendored_cucumber_bin + t.fork = true # You may get faster startup if you set this to false + t.profile = 'rerun' + end + + desc 'Run all features' + task all: [:ok, :wip] + + task :statsetup do + require 'rails/code_statistics' + ::STATS_DIRECTORIES << %w(Cucumber\ features features) if File.exist?('features') + ::CodeStatistics::TEST_TYPES << "Cucumber features" if File.exist?('features') + end + + end + + desc 'Alias for cucumber:ok' + task cucumber: 'cucumber:ok' + + task default: :cucumber + + task features: :cucumber do + STDERR.puts "*** The 'features' task is deprecated. See rake -T cucumber ***" + end + + # In case we don't have the generic Rails test:prepare hook, append a no-op task that we can depend upon. + task 'test:prepare' do + end + + task stats: 'cucumber:statsetup' + + +rescue LoadError + desc 'cucumber rake task not available (cucumber not installed)' + task :cucumber do + abort 'Cucumber rake task is not available. Be sure to install cucumber as a gem or plugin' + end +end + +end From b336748ebb24ec168032ac818e7102ac6cc9cd58 Mon Sep 17 00:00:00 2001 From: mariosantos-05 Date: Thu, 13 Nov 2025 19:38:43 -0300 Subject: [PATCH 003/100] Feat(add) docker compose file --- docker-compose.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000..da38d35673 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,10 @@ +version: "3.9" + +services: + web: + build: . + command: ./bin/dev + volumes: + - .:/rails + ports: + - "3000:3000" From 5b500274c6d63a95a9fc1160a57f899e4e709a6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Sun, 16 Nov 2025 11:55:14 -0300 Subject: [PATCH 004/100] Add files via upload --- Wiki.md | 107 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 Wiki.md diff --git a/Wiki.md b/Wiki.md new file mode 100644 index 0000000000..1e009d7e9e --- /dev/null +++ b/Wiki.md @@ -0,0 +1,107 @@ +# 📝 Wiki do Projeto – Sprint 1 +**Grupo 1 – Engenharia de Software** +**Integrantes:** + +| Nome | Matrícula | +| :--- | :--- | +| Caroline | 232050975 | +| Célio | 211010350 | +| Luís Filipe | 190091975 | +| Mário | 231035778 | + +--- + +# 📌 Nome do Projeto +**CAMAAR – Sistema para avaliação de atividades acadêmicas remotas do CIC Pipipipópópó(ainda a ser editado)** + +--- + +# 📌 Escopo do Projeto +O sistema **CAMAAR** tem como objetivo auxiliar na avaliação acadêmica de atividades, tarefas e outras atividades remotas do CIC. +O projeto contempla funcionalidades de cadastro de usuários, criação de tarefas, acompanhamento de entregas e visualização de desempenho. + +--- + +# 🔰 Papéis na Sprint 1 + +## 🧑‍💼 Scrum Master +> **Função:** Responsável por garantir que a equipe siga a metodologia Scrum. Remove impedimentos que possam atrapalhar os desenvolvedores, facilita as cerimônias (Reuniões Diárias, Planejamento, Revisão e Retrospectiva) e protege o time de interrupções externas. +**Fulano** +### Funções: +- Facilitar as cerimônias da Sprint (Planning, Review, Retrospective). +- Remover impedimentos enfrentados pela equipe. +- Assegurar que o time siga os princípios ágeis. + +--- + +## 🧑‍💻 Product Owner +> **Função:** O "dono do produto". É a voz do cliente/usuário. Sua principal responsabilidade é definir *o que* será construído, criar e priorizar os itens do Product Backlog (as funcionalidades) para maximizar o valor entregue pela equipe a cada Sprint. + +**Fulana (00000000)** +### Funções: +- Definir e priorizar o Product Backlog. +- Garantir que as funcionalidades entreguem valor ao usuário final. +- Esclarecer dúvidas sobre requisitos. + +--- + +# 🚀 Funcionalidades da Sprint 1 + +A Sprint 1 tem como foco as **funcionalidades essenciais** relacionadas ao cadastro e acesso de usuários. + +## 📦 Funcionalidade 1 – Cadastro de Usuário +**Regra de Negócio:** +- O sistema deve validar e-mail único. +- Senha deve ter pelo menos 8 caracteres. +- O usuário só pode acessar o sistema após confirmar o e-mail. + +**Responsável:** Bombardilno crocodilo +**Histórias:** +- **US01**: “Como usuário, quero criar uma conta usando e-mail e senha para acessar o sistema.” + - **Story Points:** x +- **US02**: “Como usuário, quero receber um e-mail de confirmação para ativar minha conta.” + - **Story Points:** x + +--- + +## 📦 Funcionalidade 2 – Login no Sistema +**Regra de Negócio:** +- O sistema deve bloquear após 5 tentativas falhas consecutivas. +- O login só será autorizado caso o e-mail esteja verificado. + +**Responsável:** Bombardilno crocodilo +**Histórias:** +- **US03**: “Como usuário, quero fazer login com meu e-mail e senha para acessar o sistema.” + - **Story Points:** x +- **US04**: “Como usuário, quero recuperar minha senha caso eu a esqueça.” + - **Story Points:** x + +--- + +# 📊 Métrica Velocity da Sprint 1 + +| História | Pontos | +|----------|--------| +| US01 | X | +| US02 | X | +| US03 | X | +| US04 | X | +| **Total** | **x story points** | + +A *velocity* da Sprint 1 é **x pontos**. + +--- + +# 🌿 Política de Branching Utilizada pelo Grupo + + +--- + +# 📌 Resumo +Esta página da Wiki apresenta: +- Integrantes do grupo e papéis na Sprint +- Funcionalidades planejadas +- Regras de negócio +- Responsáveis +- Pontuação das histórias (Velocity) +- Política de branching adotada From fb37d77ef63264f35e249290218087bf2247cfa6 Mon Sep 17 00:00:00 2001 From: Carol <232050975@aluno.unb.br> Date: Mon, 17 Nov 2025 10:09:44 -0300 Subject: [PATCH 005/100] =?UTF-8?q?Especifica=C3=A7=C3=A3o=20dos=20BDDs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- features/admin/atualizar_dados.feature | 66 +++++++++++++++++++++ features/admin/gerenciar_relatorios.feature | 28 +++++++++ features/admin/importar_dados.feature | 48 +++++++++++++++ 3 files changed, 142 insertions(+) create mode 100644 features/admin/atualizar_dados.feature create mode 100644 features/admin/gerenciar_relatorios.feature create mode 100644 features/admin/importar_dados.feature diff --git a/features/admin/atualizar_dados.feature b/features/admin/atualizar_dados.feature new file mode 100644 index 0000000000..1e10d4ba9a --- /dev/null +++ b/features/admin/atualizar_dados.feature @@ -0,0 +1,66 @@ +# language: pt-br + +Funcionalidade: Atualização da base de dados por importação de JSON do SIGAA + + Como um administrador do sistema + Eu quero fazer o upload de um (ou mais) arquivo JSON contendo os dados das disciplinas, turmas e alunos matriculados + A fim de atualizar o sistema com as turmas do semestre e garantir que apenas os alunos corretos possam avaliar seus respectivos professores. + +Cenário: Admin importa JSON de um novo semestre + Dado que eu estou logado como "admin@sistema.com" + E eu estou na página que contém o botão "Importar dados" + E eu tenho os arquivos "disciplinas_2021.2.json" e "turmas_2021.2.json" + Quando eu seleciono os arquivos "disciplinas_2021.2.json" e "turmas_2021.2.json" + E importo-os para o sistema + Então eu devo ver a mensagem "Importação concluída." + E a disciplina "BANCOS DE DADOS" (CIC0097) deve existir no sistema + E a aluna "Ana Clara Jordao Perna" (190084006) deve estar matriculada na turma "TA" de "CIC0097" + + +Cenário: Admin importa JSON que atualiza o email de um aluno + Dado que eu estou logado como "admin@sistema.com" + E eu estou na página que contém o botão "Importar dados" + E existe uma aluna "Ana Clara Jordao Perna" (190084006) com o email "acjpjvjp@gmail.com" + E eu tenho um arquivo "turmas_correcao.json" onde o aluno "190484006" agora tem o email "ana.clara@aluno.unb.br" + Quando eu seleciono o arquivo "turmas_correcao.json" + E importo-o ao sistema + Então eu devo ver a mensagem "Importação concluída." + E a aluna (190084006) deve ter o email "ana.clara@aluno.unb.br" no banco de dados + E não deve existir um novo aluno no sistema + +Cenário: Admin tenta importar um arquivo que não é JSON + Dado que eu estou logado como "admin@sistema.com" + E eu estou na página que contém o botão "Importar dados" + Quando eu seleciono um arquivo "documento.csv" para importaçao de dados + E importo-o ao sistema + Então eu devo continuar na página que contém o botão "Importar dados" + E eu devo ver a mensagem de erro "Formato de arquivo inválido. Por favor, envie um arquivo .json." + +Cenário: Admin importa JSON de turma com chave "matricula" faltando + Dado que eu estou logado como "admin@sistema.com" + E eu estou na página que contém o botão "Importar dados" + E eu tenho um arquivo "turmas_sem_matricula.json" onde um objeto "discente" não possui a chave "matricula" + Quando eu seleciono o arquivo "turmas_sem_matricula.json" para importação de dados + E importo-o ao sistema + Então eu devo continuar na página que contém o botão "Importar dados" + E eu devo ver a mensagem de erro "Erro na importação: O arquivo JSON é inválido." + + +Cenário: Admin importa um arquivo JSON com sintaxe quebrada + Dado que eu estou logado como "admin@sistema.com" + E eu estou na página que contém o botão "Importar dados" + E eu tenho um arquivo "json_errado.json" (que possui um erro de sintaxe, como uma vírgula extra) + Quando eu seleciono o arquivo "json_errado.json" para importação de dados + E importo-o ao sistema + Então eu devo continuar na página que contém o botão "Importar dados" + E eu devo ver a mensagem de erro "Erro: O arquivo não é um JSON válido." + + +Cenário: Admin importa JSON com tipo de dado inválido para matrícula + Dado que eu estou logado como "admin@sistema.com" + E eu estou na página que contém o botão "Importar dados" + E eu tenho um arquivo "turmas_tipo_errado.json" onde a "matricula" de um aluno é o texto "matricula" em vez de um número + Quando eu seleciono o arquivo "turmas_tipo_errado.json" para importação de dados + E importo-o ao sistema + Então eu devo continuar na página que contém o botão "Importar dados" + E eu devo ver a mensagem de erro "Erro na importação." \ No newline at end of file diff --git a/features/admin/gerenciar_relatorios.feature b/features/admin/gerenciar_relatorios.feature new file mode 100644 index 0000000000..56b2b263ff --- /dev/null +++ b/features/admin/gerenciar_relatorios.feature @@ -0,0 +1,28 @@ +# language: pt-br + +Funcionalidade: Visualização e Download de Resultados de Avaliação + Como um administrador + Eu quero baixar um arquivo CSV contendo os resultados de um formulário + A fim de avaliar o desempenho das turmas + +Cenário: Admin baixa CSV de uma turma com respostas + Dado que eu estou logado como "admin@sistema.com" + E eu navego para a página "Gerenciamento" + E eu clico em "Resultados" + E eu estou na tela "Gerenciamento - Resultados" + Quando eu clico no bloco da turma "BANCOS DE DADOS (CIC0097) - Prof. Joao" + Então eu devo baixar um arquivo chamado "resultados_CIC0097_ProfJoao.csv" + +Cenário: Admin tenta ver resultados de uma turma sem respostas + Dado que eu estou logado como "admin@sistema.com" + E eu estou na tela "Gerenciamento - Resultados" + E a turma "ENGENHARIA DE SOFTWARE (CIC0105) - Prof. Genaina" possui 0 respostas de avaliação + Quando eu clico no bloco da turma "ENGENHARIA DE SOFTWARE (CIC0105) - Prof. Genaina" + Então deve exibir a mensagem "Este formulário ainda não possui respostas" + E não efetuar nenhum download de aruivo CSV. + +Cenário: Professor (não-admin) tenta acessar a página de resultados diretamente pela URL + Dado que eu estou logado como "professor.comum@sistema.com" (que não é admin) + E eu não vejo o link para a página "Gerenciamento" no meu menu + Quando eu tento acessar a URL "/gerenciamento/resultados" diretamente no meu navegador + Então eu devo ser redirecionado para a minha página inicial (ou "Dashboard") \ No newline at end of file diff --git a/features/admin/importar_dados.feature b/features/admin/importar_dados.feature new file mode 100644 index 0000000000..a31421e252 --- /dev/null +++ b/features/admin/importar_dados.feature @@ -0,0 +1,48 @@ +# language: pt-br + +Funcionalidade: Alimentação inicial da base de dados com JSON do SIGAA + + Como um administrador + Eu quero importar dados de turmas, matérias e participantes do SIGAA (caso não existam na base de dados atual) + A fim de alimentar a base de dados do sistema. + +Cenário: Admin importa dados em um sistema vazio + Dado que eu estou logado como "admin@sistema.com" + E eu estou na página que contém o botão "Importar dados" + E o banco de dados não contém nenhuma disciplina ou aluno + E eu tenho um arquivo "disciplinas_2021.2.json" (com a disciplina "BANCOS DE DADOS") + E eu tenho um arquivo "turmas_2021.2.json" (com o aluno "Ana Clara Jordao Perna") + Quando eu seleciono os arquivos "disciplinas_2021.2.json" e "turmas_2021.2.json" + E importo-os ao sistema + Então eu devo ver a mensagem "Importação concluída." + E a disciplina "BANCOS DE DADOS" (CIC0097) deve existir no sistema + E o aluno "Ana Clara Jordao Perna" (190084006) deve existir no sistema + +Cenário: Admin importa dados que já existem na base + Dado que eu estou logado como "admin@sistema.com" + E eu estou na página que contém o botão "Importar dados" + E já existe uma disciplina "BANCOS DE DADOS" (CIC0097) no sistema + E já existe o aluno "Ana Clara Jordao Perna" (190084006) + E eu tenho um arquivo "turmas_2021.2_repetido.json" que contém os mesmos dados + Quando eu seleciono o arquivo "turmas_2021.2_repetido.json" + E impoto-o ao sistema + Então eu devo ver a mensagem "Importação concluída." + E deve haver apenas 1 aluno com a matrícula "190084006" no sistema + +Cenário: Admin tenta alimentar a base com um arquivo que não é JSON + Dado que eu estou logado como "admin@sistema.com" + E eu estou na página que contém o botão "Importar dados" + E o banco de dados está vazio + Quando eu seleciono um arquivo "lista_de_alunos.pdf" + E importo-o ao sistema + Então devo ver a mensagem de erro "Formato de arquivo inválido. Por favor, envie um arquivo .json." + +Cenário: Admin importa JSON de turma com a chave "matricula" faltando + Dado que eu estou logado como "admin@sistema.com" + E eu estou na página que contém o botão "Importar dados" + E eu tenho um arquivo "turmas_sem_matricula.json" onde um "discente" não possui a chave "matricula" + Quando eu seleciono o arquivo "turmas_sem_matricula.json" + E importo-o ao sistema + Então eu devo ver a mensagem de erro "Erro na importação: O arquivo JSON é inválido." + + From ad8f67a37ae21d85b8f2a08b3e53820026f0822e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9lio=20Eduardo?= <59587358+celio-eduardo@users.noreply.github.com> Date: Mon, 17 Nov 2025 11:29:15 -0300 Subject: [PATCH 006/100] =?UTF-8?q?Add=20-=20redefini=C3=A7=C3=A3o=20de=20?= =?UTF-8?q?senha=20-=20feature=20scenario?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- features/login/redefinicao_senha.feature | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 features/login/redefinicao_senha.feature diff --git a/features/login/redefinicao_senha.feature b/features/login/redefinicao_senha.feature new file mode 100644 index 0000000000..0f9af74dc8 --- /dev/null +++ b/features/login/redefinicao_senha.feature @@ -0,0 +1,22 @@ +Funcionalidade: Redefinição de senha + Como um Usuário + Eu quero redefinir uma senha para o meu usuário a partir do e-mail recebido + A fim de recuperar o meu acesso ao sistema + +Cenário: Usuário solicita redefinição com e-mail válido (Caminho Feliz) + Dado que eu estou na página de "Login" + E existe um usuário cadastrado com o e-mail "usuario_valido@alias" + Quando eu clico no link "Esqueci minha senha" + E eu preencho o campo "E-mail" com "usuario_valido@alias" + E clico no botão "Solicitar redefinição" + Então eu devo ver a mensagem "Um e-mail de redefinição foi enviado para sua caixa de entrada." + E um e-mail com um link de redefinição deve ser enviado para "usuario_valido@alias" + +Cenário: Usuário solicita redefinição com e-mail não cadastrado (Caminho Triste) + Dado que eu estou na página de "Login" + E não existe um usuário cadastrado com o e-mail "usuario_valido@alias" + Quando eu clico no link "Esqueci minha senha" + E eu preencho o campo "E-mail" com "usuario_valido@alias" + E clico no botão "Solicitar redefinição" + Então eu devo ver a mensagem "Se este e-mail estiver cadastrado, um link será enviado." + E nenhum e-mail de redefinição deve ser enviado From 62d88c9c551a5b39f71a6dc6aae39c4d3d2272fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9lio=20Eduardo?= <59587358+celio-eduardo@users.noreply.github.com> Date: Mon, 17 Nov 2025 11:33:14 -0300 Subject: [PATCH 007/100] Add scenarios for weak password validations --- features/login/definicao_senha.feature | 36 ++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 features/login/definicao_senha.feature diff --git a/features/login/definicao_senha.feature b/features/login/definicao_senha.feature new file mode 100644 index 0000000000..91cd821fd4 --- /dev/null +++ b/features/login/definicao_senha.feature @@ -0,0 +1,36 @@ +Funcionalidade: Definição inicial de senha + Como um Usuário + Eu quero definir uma senha para o meu usuário a partir do e-mail do sistema de solicitação de cadastro + A fim de acessar o sistema + +Cenário: Usuário define uma senha válida pela primeira vez (Caminho Feliz) + Dado que eu acessei a página "Definir Senha" através de um link de cadastro válido + Quando eu preencho o campo "Nova Senha" com "SenhaForte@123" + E eu preencho o campo "Confirmar Senha" com "SenhaForte@123" + E eu clico no botão "Definir Senha" + Então eu devo ser redirecionado para a página de "Login" + E eu devo ver a mensagem "Senha definida com sucesso! Você já pode fazer o login." + +Cenário: Usuário tenta definir senhas que não são idênticas (Caminho Triste) + Dado que eu acessei a página "Definir Senha" através de um link de cadastro válido + Quando eu preencho o campo "Nova Senha" com "SenhaForte@123" + E eu preencho o campo "Confirmar Senha" com "SenhaDiferente@123" + E eu clico no botão "Definir Senha" + Então eu devo continuar na página "Definir Senha" + E eu devo ver a mensagem de erro "As senhas não conferem." + +Cenário: Usuário tenta definir uma senha muito curta (Caminho Triste) + Dado que eu acessei a página "Definir Senha" através de um link de cadastro válido + Quando eu preencho o campo "Nova Senha" com "123" + E eu preencho o campo "Confirmar Senha" com "123" + E eu clico no botão "Definir Senha" + Então eu devo continuar na página "Definir Senha" + E eu devo ver a mensagem de erro "A senha deve ter no mínimo 8 caracteres." + +Cenário: Usuário tenta definir uma senha fraca (Caminho Triste) + Dado que eu acessei a página "Definir Senha" através de um link de cadastro válido + Quando eu preencho o campo "Nova Senha" com "SenhaFraca" + E eu preencho o campo "Confirmar Senha" com "SenhaFraca" + E eu clico no botão "Definir Senha" + Então eu devo continuar na página "Definir Senha" + E eu devo ver a mensagem de erro "Não atende aos requisitos mínimos: mínimo 8 caracteres, sendo obrigatório 1 caractere especial, 1 número, 1 letra maiúscula e 1 letra minúscula!" From 939c5ec8450bc996330145c6b0d29a1eeaacebd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9lio=20Eduardo?= <59587358+celio-eduardo@users.noreply.github.com> Date: Mon, 17 Nov 2025 11:37:46 -0300 Subject: [PATCH 008/100] =?UTF-8?q?Add=20login=20feature=20scenarios=20par?= =?UTF-8?q?a=20usu=C3=A1rios=20autenticados?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- features/login/sistema_login.feature | 40 ++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 features/login/sistema_login.feature diff --git a/features/login/sistema_login.feature b/features/login/sistema_login.feature new file mode 100644 index 0000000000..2cc137728d --- /dev/null +++ b/features/login/sistema_login.feature @@ -0,0 +1,40 @@ +Funcionalidade: Autenticação de usuário no sistema + Como um Usuário do sistema + Eu quero acessar o sistema utilizando um e-mail ou matrícula e uma senha já cadastrada + A fim de responder formulários ou gerenciar o sistema + +Cenário: Administrador faz login com sucesso (Caminho Feliz) + Dado que eu estou na página de "Login" + E existe um usuário "usuario_valido@alias" com a senha "senha123" e perfil "Admin" + Quando eu preencho "E-mail ou Matrícula" com "usuario_valido@alias" ou "Matricula" + E preencho "Senha" com "senha123" + E clico em "Entrar" + Então eu devo ser redirecionado para a página "Dashboard Admin" + E eu devo ver o link "Gerenciamento" no menu lateral + +Cenário: Aluno (usuário comum) faz login com sucesso (Caminho Feliz) + Dado que eu estou na página de "Login" + E existe um usuário "usuario_valido@alias" com a senha "senha456" e perfil "Aluno" + Quando eu preencho "E-mail ou Matrícula" com "usuario_valido@alias" ou "Matricula" + E preencho "Senha" com "senha456" + E clico em "Entrar" + Então eu devo ser redirecionado para a página "Dashboard Aluno" + E eu não devo ver o link "Gerenciamento" no menu lateral + +Cenário: Professor (usuário comum) faz login com sucesso (Caminho Feliz) + Dado que eu estou na página de "Login" + E existe um usuário "usuario_valido@alias" com a senha "senha456" e perfil "Aluno" + Quando eu preencho "E-mail ou Matrícula" com "usuario_valido@alias" ou "Matricula" + E preencho "Senha" com "senha456" + E clico em "Entrar" + Então eu devo ser redirecionado para a página "Dashboard Professor" + E eu não devo ver o link "Gerenciamento" no menu lateral + +Cenário: Usuário tenta fazer login com senha incorreta (Caminho Triste) + Dado que eu estou na página de "Login" + E existe um usuário "usuario_valido@alias" com a senha "senha456" + Quando eu preencho "E-mail ou Matrícula" com "usuario_valido@alias" ou "Matricula" + E preencho "Senha" com "senhaErrada" + E clico em "Entrar" + Então eu devo continuar na página de "Login" + E eu devo ver a mensagem de erro "E-mail ou senha inválidos." From 3b8f7954f0994c0a00eaa11a30cb63584be79313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9lio=20Eduardo?= <59587358+celio-eduardo@users.noreply.github.com> Date: Mon, 17 Nov 2025 11:47:53 -0300 Subject: [PATCH 009/100] =?UTF-8?q?Add=20feature=20para=20registro=20de=20?= =?UTF-8?q?usu=C3=A1rios=20via=20importa=C3=A7=C3=A3o=20JSON?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- features/cadastro/cadastrar_usuarios.feature | 35 ++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 features/cadastro/cadastrar_usuarios.feature diff --git a/features/cadastro/cadastrar_usuarios.feature b/features/cadastro/cadastrar_usuarios.feature new file mode 100644 index 0000000000..05645330a7 --- /dev/null +++ b/features/cadastro/cadastrar_usuarios.feature @@ -0,0 +1,35 @@ +Funcionalidade: Cadastro de usuários do SIGAA via importação de dados + Como um Administrador + Eu quero cadastrar participantes de turmas do SIGAA ao importar dados de usuarios novos + A fim de que eles acessem o sistema CAMAAR + +Cenário: Admin importa JSON com um novo aluno (Caminho Feliz) + Dado que eu estou logado como "admin_valido@alias" + E eu estou na página que contém o botão "Importar dados" + E o banco de dados não contém o aluno "Novo Aluno" (matrícula "200012345") + E eu tenho um arquivo "turmas_novo_aluno.json" que contém "Novo Aluno" (200012345) com o email "novo.aluno@alias" + Quando eu seleciono o arquivo "turmas_novo_aluno.json" + E importo-o ao sistema + Então eu devo ver a mensagem "Importação concluída." + E o aluno "Novo Aluno" (200012345) deve existir no sistema com status "Pendente" + E um e-mail de definição de senha deve ser enviado para "novo.aluno@alias" + +Cenário: Admin importa JSON com um novo professor (Caminho Feliz) + Dado que eu estou logado como "admin_valido@alias" + E eu estou na página que contém o botão "Importar dados" + E o banco de dados não contém o professor "Novo Professor" (matrícula "XXXXXXXXX") + E eu tenho um arquivo "departamento_novo_professor.json" que contém "Novo Professor" (XXXXXXXXX) com o email "novo.professor@alias" + Quando eu seleciono o arquivo "departamento_novo_professor.json" + E importo-o ao sistema + Então eu devo ver a mensagem "Importação concluída." + E o professor "Novo Professor" (XXXXXXXXX) deve existir no sistema com status "Pendente" + E um e-mail de definição de senha deve ser enviado para "novo.professor@alias" + +Cenário: Admin importa JSON de turma com aluno novo sem a chave "email" (Caminho Triste) + Dado que eu estou logado como "admin_valido@alias" + E eu estou na página que contém o botão "Importar dados" + E eu tenho um arquivo "turmas_sem_email.json" onde um "discente" novo não possui a chave "email" + Quando eu seleciono o arquivo "turmas_sem_email.json" + E importo-o ao sistema + Então eu devo ver a mensagem de erro "Erro na importação: O arquivo JSON é inválido." + E nenhum novo usuário deve ser criado From cee2de9f04726338f74bf532c288267490ac06de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Mon, 17 Nov 2025 18:35:57 -0300 Subject: [PATCH 010/100] Create bdd_templates.feature --- .../bdd_templates.feature | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 features/ template_formularios /bdd_templates.feature diff --git a/features/ template_formularios /bdd_templates.feature b/features/ template_formularios /bdd_templates.feature new file mode 100644 index 0000000000..54d0a1425a --- /dev/null +++ b/features/ template_formularios /bdd_templates.feature @@ -0,0 +1,86 @@ +Funcionalidade: Criar template de formulário + +Cenário: Criação de template com dados válidos (Feliz) + Dado que estou na página de criação de templates + E preencho o campo "Nome do template" com "Avaliação 2024" + E adiciono um tipo de questão de múltipla escolha + E adiciono um tipo de questão discursiva + Quando clico no botão "Salvar Template" + Então o sistema deve exibir a mensagem "Template criado com sucesso" + E o template deve ser salvo no banco de dados + +Cenário: Tentativa de criação com título vazio (Triste) + Dado que estou na página de criação de templates + E deixo o campo "Título" vazio + E adiciono tipos de questões válidas + Quando clico no botão "Salvar Template" + Então o sistema deve exibir a mensagem de erro "O campo Título é obrigatório" + E o template não deve ser salvo + +Cenário: Tentativa de criação sem questões (Triste) + Dado que preencho o título corretamente + E não adiciono nenhum tipo de questão ao template + Quando clico no botão "Salvar Template" + Então o sistema deve exibir o erro "O template deve conter pelo menos uma questão" + E o template não deve ser salvo + + +Funcionalidade: Visualização dos templates criados + +Cenário: Visualizar lista de templates existentes (Feliz) + Dado que existem templates salvos no sistema + Quando eu acesso a página "Templates" + Então devo ver uma lista contendo todos os templates criados + E devo ver as opções de "Editar" e "Deletar" para cada template da lista + +Cenário: Visualizar lista vazia (Triste) + Dado que não existe nenhum template salvo no sistema + Quando eu acesso a página "Templates" + Então o sistema deve exibir a mensagem "Nenhum template foi criado" + E a lista de visualização deve estar vazia + + Funcionalidade: Edição e Deleção de Templates + +Cenário: Edição de nome de template usado em formulário antigo (Feliz) + Dado que tenho o template "Avaliação A" associado a um formulário já respondido + Quando eu edito o nome do template para "Avaliação A - Revisada" + E clico no botão "Salvar" + Então o sistema deve atualizar o template para "Avaliação A - Revisada" + E os formulários antigos criados com o template "Avaliação A" não devem sofrer alteração + +Cenário: Deleção de um template existente (Feliz) + Dado que seleciono um template existente na lista + Quando clico em "Deletar" + E aparece um pop-up com o texto "Você tem certeza que deseja deletar este template?" + E confirmo a ação clicando no botão "Confirmar" + Então o template deve ser removido da lista de visualização + E não devo ver ele no gerenciamento de templates + +Cenário: Edição com caracteres inválidos no título (Triste) + Dado que estou na tela de edição de um template + Quando preencho o campo "Título" com caracteres não permitidos + Então o sistema deve exibir a mensagem "Formato de título inválido" + E aparece uma mensagem de texto indicando quais caracteres não são permitidos + E as alterações não devem ser salvas + + Funcionalidade: Criação de formulário para docentes ou dicentes + +Cenário: Enviar formulário para Discentes de uma turma específica (Feliz) + Dado que estou criando um novo formulário a partir de um template + Quando seleciono a opção "Discentes" no campo de público-alvo + E seleciono uma turma válida "Turma A - Engenharia" + E clico no botão "Enviar" + Então o formulário deve ser enviado especificamente para os alunos daquela turma + +Cenário: Enviar formulário para Docentes (Feliz) + Dado que estou criando um novo formulário + Quando seleciono a opção "Docentes" no campo de público-alvo + Então o formulário deve ser enviado especificamente para os professores + E o campo de seleção de turma deve ficar oculto ou opcional + +Cenário: Tentativa de criar sem selecionar público (Triste) + Dado que preenchi os dados básicos do formulário + E não selecionei nem "Docente" nem "Discente" + Quando tento clicar em "Enviar" + Então o sistema deve exibir o erro "Selecione o público-alvo da avaliação" + E não deve enviar o formulário From df0e23eae83f92f16ad61875639ace90008c6071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Mon, 17 Nov 2025 18:38:15 -0300 Subject: [PATCH 011/100] Update bdd_templates.feature Especificando BDD dos templates --- features/ template_formularios /bdd_templates.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/ template_formularios /bdd_templates.feature b/features/ template_formularios /bdd_templates.feature index 54d0a1425a..9de823804f 100644 --- a/features/ template_formularios /bdd_templates.feature +++ b/features/ template_formularios /bdd_templates.feature @@ -1,4 +1,4 @@ -Funcionalidade: Criar template de formulário +Funcionalidade: Criar template de formulário Cenário: Criação de template com dados válidos (Feliz) Dado que estou na página de criação de templates From f27da391f766bc525cd046d20ec219ad8ed3ff92 Mon Sep 17 00:00:00 2001 From: mariosantos-05 Date: Mon, 17 Nov 2025 19:38:40 -0300 Subject: [PATCH 012/100] gerenciamento forms --- .dockerignore | 85 ++++++++----------- README.Docker.md | 17 ++++ docker-compose.yml | 58 +++++++++++-- .../criar_formulario.feature | 30 +++++++ .../responder_formulario.feature | 36 ++++++++ .../visualizar_formulario.feature | 19 +++++ .../visualizar_resposta_formulario.feature | 17 ++++ 7 files changed, 203 insertions(+), 59 deletions(-) create mode 100644 README.Docker.md create mode 100644 features/step_definitions/Gerenciamento_forms/criar_formulario.feature create mode 100644 features/step_definitions/Gerenciamento_forms/responder_formulario.feature create mode 100644 features/step_definitions/Gerenciamento_forms/visualizar_formulario.feature create mode 100644 features/step_definitions/Gerenciamento_forms/visualizar_resposta_formulario.feature diff --git a/.dockerignore b/.dockerignore index 325bfc036d..03a268b8ba 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,51 +1,34 @@ -# 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* +# Include any files or directories that you don't want to be copied to your +# container here (e.g., local build artifacts, temporary files, etc.). +# +# For more help, visit the .dockerignore file reference guide at +# https://docs.docker.com/go/build-context-dockerignore/ + +**/.DS_Store +**/__pycache__ +**/.venv +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/bin +**/charts +**/docker-compose* +**/compose.y*ml +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md diff --git a/README.Docker.md b/README.Docker.md new file mode 100644 index 0000000000..ffca340863 --- /dev/null +++ b/README.Docker.md @@ -0,0 +1,17 @@ +### Building and running your application + +When you're ready, start your application by running: +`docker compose up --build`. + +### Deploying your application to the cloud + +First, build your image, e.g.: `docker build -t myapp .`. +If your cloud uses a different CPU architecture than your development +machine (e.g., you are on a Mac M1 and your cloud provider is amd64), +you'll want to build the image for that platform, e.g.: +`docker build --platform=linux/amd64 -t myapp .`. + +Then, push it to your registry, e.g. `docker push myregistry.com/myapp`. + +Consult Docker's [getting started](https://docs.docker.com/go/get-started-sharing/) +docs for more detail on building and pushing. \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index da38d35673..6a19e9a592 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,10 +1,52 @@ -version: "3.9" +# Comments are provided throughout this file to help you get started. +# If you need more help, visit the Docker Compose reference guide at +# https://docs.docker.com/go/compose-spec-reference/ +# Here the instructions define your application as a service called "app". +# This service is built from the Dockerfile in the current directory. +# You can add other services your application may depend on here, such as a +# database or a cache. For examples, see the Awesome Compose repository: +# https://github.com/docker/awesome-compose services: - web: - build: . - command: ./bin/dev - volumes: - - .:/rails - ports: - - "3000:3000" + app: + build: + context: . + target: final + # If your application exposes a port, uncomment the following lines and change + # the port numbers as needed. The first number is the host port and the second + # is the port inside the container. + # ports: + # - 8080:8080 + + # The commented out section below is an example of how to define a PostgreSQL + # database that your application can use. `depends_on` tells Docker Compose to + # start the database before your application. The `db-data` volume persists the + # database data between container restarts. The `db-password` secret is used + # to set the database password. You must create `db/password.txt` and add + # a password of your choosing to it before running `docker compose up`. + # depends_on: + # db: + # condition: service_healthy + # db: + # image: postgres + # restart: always + # user: postgres + # secrets: + # - db-password + # volumes: + # - db-data:/var/lib/postgresql/data + # environment: + # - POSTGRES_DB=example + # - POSTGRES_PASSWORD_FILE=/run/secrets/db-password + # expose: + # - 5432 + # healthcheck: + # test: [ "CMD", "pg_isready" ] + # interval: 10s + # timeout: 5s + # retries: 5 + # volumes: + # db-data: + # secrets: + # db-password: + # file: db/password.txt diff --git a/features/step_definitions/Gerenciamento_forms/criar_formulario.feature b/features/step_definitions/Gerenciamento_forms/criar_formulario.feature new file mode 100644 index 0000000000..ca024eb585 --- /dev/null +++ b/features/step_definitions/Gerenciamento_forms/criar_formulario.feature @@ -0,0 +1,30 @@ + Funcionalidade: Criar formulário de avaliação + Eu como Administrador + Quero criar um formulário baseado em um template para as turmas que eu escolher + A fim de avaliar o desempenho das turmas no semestre atual + + Cenário: Criar formulário de avaliação para turmas selecionadas + Dado que eu estou logado como Administrador + E que eu tenho acesso ao template de formulário "Avaliação Semestral" + Quando eu seleciono as turmas "Turma A", "Turma B" e "Turma C" + E eu escolho o template "Avaliação Semestral" + E eu clico em "Criar Formulário" + Então o sistema deve criar formulários de avaliação para as turmas selecionadas + E eu devo ver uma mensagem de sucesso "Formulários criados com sucesso para as turmas selecionadas." + + Cenário: Tentativa de criação sem selecionar turmas + Dado que eu estou logado como Administrador + E que existe um template de formulário chamado "Avaliação Semestral" + Quando eu escolho o template "Avaliação Semestral" + E eu clico em "Criar Formulário" + Então eu devo ver uma mensagem de erro "Nenhuma turma selecionada." + E nenhum formulário deve ser criado + + Cenário: Tentativa de criação sem selecionar template + Dado que eu estou logado como Administrador + E que existem turmas cadastradas no sistema + Quando eu seleciono as turmas "Turma A" e "Turma B" + E eu clico em "Criar Formulário" + Então eu devo ver uma mensagem de erro "Nenhum template selecionado." + E nenhum formulário deve ser criado + diff --git a/features/step_definitions/Gerenciamento_forms/responder_formulario.feature b/features/step_definitions/Gerenciamento_forms/responder_formulario.feature new file mode 100644 index 0000000000..c4cfbe21a6 --- /dev/null +++ b/features/step_definitions/Gerenciamento_forms/responder_formulario.feature @@ -0,0 +1,36 @@ + Funcionalidade: Responder formulário + Eu como Participante de uma turma + Quero responder o questionário sobre a turma em que estou matriculado + A fim de submeter minha avaliação da turma + + Cenário: Responder formulário de avaliação para uma turma + Dado que eu estou logado como Participante + E que eu estou matriculado na turma "Turma A" + E que existe um formulário de avaliação disponível para "Turma A" + Quando eu acesso o formulário de avaliação para "Turma A" + E eu preencho o formulário com as respostas necessárias + E eu clico em "Enviar Formulário" + Então o sistema deve registrar minhas respostas para o formulário de "Turma A" + E eu devo ver uma mensagem de confirmação "Seu formulário foi enviado com sucesso." + + Cenário: Tentativa de enviar formulário sem preencher campos obrigatórios + Dado que eu estou logado como Participante + E que existe um formulário de avaliação ativo para "Turma A" + Quando eu acesso o formulário de avaliação de "Turma A" + E eu envio o formulário sem preencher os campos obrigatórios + Então eu devo ver uma mensagem de erro indicando os campos faltantes + E as respostas não devem ser registradas + + Cenário: Tentativa de enviar formulário sem preencher campos obrigatórios + Dado que eu estou logado como Participante + E que existe um formulário de avaliação ativo para "Turma A" + Quando eu acesso o formulário de avaliação de "Turma A" + E eu envio o formulário sem preencher os campos obrigatórios + Então eu devo ver uma mensagem de erro indicando os campos faltantes + E as respostas não devem ser registradas + + + + + + diff --git a/features/step_definitions/Gerenciamento_forms/visualizar_formulario.feature b/features/step_definitions/Gerenciamento_forms/visualizar_formulario.feature new file mode 100644 index 0000000000..97da17fed1 --- /dev/null +++ b/features/step_definitions/Gerenciamento_forms/visualizar_formulario.feature @@ -0,0 +1,19 @@ +Funcionalidade: Visualização de formulários para responder + Eu como 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: Visualizar formulários não respondidos para turmas matriculadas + Dado que eu estou logado como Participante + E que eu estou matriculado nas turmas "Turma A" e "Turma B" + E que existem formulários não respondidos para essas turmas + Quando eu acesso a seção de formulários disponíveis + Então eu devo ver os formulários não respondidos para "Turma A" e "Turma B" + E eu devo ver uma mensagem indicando o número de formulários pendentes "Você tem 2 formulários para responder." + + Cenário: Nenhum formulário pendente + Dado que eu estou logado como Participante + E que eu estou matriculado nas turmas "Turma A" e "Turma B" + E que não existem formulários pendentes + Quando eu acesso a seção "Formulários Disponíveis" + Então eu devo ver a mensagem "Você não possui formulários pendentes." diff --git a/features/step_definitions/Gerenciamento_forms/visualizar_resposta_formulario.feature b/features/step_definitions/Gerenciamento_forms/visualizar_resposta_formulario.feature new file mode 100644 index 0000000000..1793ae6cf5 --- /dev/null +++ b/features/step_definitions/Gerenciamento_forms/visualizar_resposta_formulario.feature @@ -0,0 +1,17 @@ + Funcionalidade: Visualização de resultados dos formulários + Eu como Administrador + Quero visualizar os formulários criados + A fim de poder gerar um relatório a partir das respostas + + Cenário: Visualizar formulários criados para turmas selecionadas + Dado que eu estou logado como Administrador + E que eu tenho formulários criados para as turmas "Turma A" e "Turma B" + Quando eu acesso a seção de formulários criados + Então eu devo ver os formulários criados para "Turma A" e "Turma B" + E eu devo ver uma mensagem indicando o número de formulários criados "Existem 2 formulários criados." s + + Cenário: Visualizar detalhes de um formulários + Dado que eu estou logado como Administrador + E que existe um formulário criado para a turma "Turma A" + Quando eu seleciono o formulário de "Turma A" na lista + Então eu devo visualizar as perguntas, respostas e estatísticas do formulário From f4b7c1e71b8aef24aee4e1ced0fa8a58502c9c5a Mon Sep 17 00:00:00 2001 From: mariosantos-05 Date: Mon, 17 Nov 2025 19:46:07 -0300 Subject: [PATCH 013/100] Fix(local) ajuste de local BDD formularios --- .../Gerenciamento_forms/criar_formulario.feature | 0 .../Gerenciamento_forms/responder_formulario.feature | 0 .../Gerenciamento_forms/visualizar_formulario.feature | 0 .../Gerenciamento_forms/visualizar_resposta_formulario.feature | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename features/{step_definitions => }/Gerenciamento_forms/criar_formulario.feature (100%) rename features/{step_definitions => }/Gerenciamento_forms/responder_formulario.feature (100%) rename features/{step_definitions => }/Gerenciamento_forms/visualizar_formulario.feature (100%) rename features/{step_definitions => }/Gerenciamento_forms/visualizar_resposta_formulario.feature (100%) diff --git a/features/step_definitions/Gerenciamento_forms/criar_formulario.feature b/features/Gerenciamento_forms/criar_formulario.feature similarity index 100% rename from features/step_definitions/Gerenciamento_forms/criar_formulario.feature rename to features/Gerenciamento_forms/criar_formulario.feature diff --git a/features/step_definitions/Gerenciamento_forms/responder_formulario.feature b/features/Gerenciamento_forms/responder_formulario.feature similarity index 100% rename from features/step_definitions/Gerenciamento_forms/responder_formulario.feature rename to features/Gerenciamento_forms/responder_formulario.feature diff --git a/features/step_definitions/Gerenciamento_forms/visualizar_formulario.feature b/features/Gerenciamento_forms/visualizar_formulario.feature similarity index 100% rename from features/step_definitions/Gerenciamento_forms/visualizar_formulario.feature rename to features/Gerenciamento_forms/visualizar_formulario.feature diff --git a/features/step_definitions/Gerenciamento_forms/visualizar_resposta_formulario.feature b/features/Gerenciamento_forms/visualizar_resposta_formulario.feature similarity index 100% rename from features/step_definitions/Gerenciamento_forms/visualizar_resposta_formulario.feature rename to features/Gerenciamento_forms/visualizar_resposta_formulario.feature From cc49383bfcf45bf1909dbdb14d46116ef4ebc243 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Mon, 17 Nov 2025 19:57:12 -0300 Subject: [PATCH 014/100] Create criar_template.feature --- .../criar_template.feature | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 features/ template_formularios /criar_template.feature diff --git a/features/ template_formularios /criar_template.feature b/features/ template_formularios /criar_template.feature new file mode 100644 index 0000000000..dac7934038 --- /dev/null +++ b/features/ template_formularios /criar_template.feature @@ -0,0 +1,25 @@ +Funcionalidade: Criar template de formulário + +Cenário: Criação de template com dados válidos (Feliz) + Dado que estou na página de criação de templates + E preencho o campo "Nome do template" com "Avaliação 2024" + E adiciono um tipo de questão de múltipla escolha + E adiciono um tipo de questão discursiva + Quando clico no botão "Salvar Template" + Então o sistema deve exibir a mensagem "Template criado com sucesso" + E o template deve ser salvo no banco de dados + +Cenário: Tentativa de criação com título vazio (Triste) + Dado que estou na página de criação de templates + E deixo o campo "Título" vazio + E adiciono tipos de questões válidas + Quando clico no botão "Salvar Template" + Então o sistema deve exibir a mensagem de erro "O campo Título é obrigatório" + E o template não deve ser salvo + +Cenário: Tentativa de criação sem questões (Triste) + Dado que preencho o título corretamente + E não adiciono nenhum tipo de questão ao template + Quando clico no botão "Salvar Template" + Então o sistema deve exibir o erro "O template deve conter pelo menos uma questão" + E o template não deve ser salvo From e2604402c37b3e25c717ade0b1c7dab03ad9d741 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Mon, 17 Nov 2025 19:57:51 -0300 Subject: [PATCH 015/100] Add files via upload --- .../criar_template.feature | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 features/ template_formularios/criar_template.feature diff --git a/features/ template_formularios/criar_template.feature b/features/ template_formularios/criar_template.feature new file mode 100644 index 0000000000..dac7934038 --- /dev/null +++ b/features/ template_formularios/criar_template.feature @@ -0,0 +1,25 @@ +Funcionalidade: Criar template de formulário + +Cenário: Criação de template com dados válidos (Feliz) + Dado que estou na página de criação de templates + E preencho o campo "Nome do template" com "Avaliação 2024" + E adiciono um tipo de questão de múltipla escolha + E adiciono um tipo de questão discursiva + Quando clico no botão "Salvar Template" + Então o sistema deve exibir a mensagem "Template criado com sucesso" + E o template deve ser salvo no banco de dados + +Cenário: Tentativa de criação com título vazio (Triste) + Dado que estou na página de criação de templates + E deixo o campo "Título" vazio + E adiciono tipos de questões válidas + Quando clico no botão "Salvar Template" + Então o sistema deve exibir a mensagem de erro "O campo Título é obrigatório" + E o template não deve ser salvo + +Cenário: Tentativa de criação sem questões (Triste) + Dado que preencho o título corretamente + E não adiciono nenhum tipo de questão ao template + Quando clico no botão "Salvar Template" + Então o sistema deve exibir o erro "O template deve conter pelo menos uma questão" + E o template não deve ser salvo From 59acddccf382a22a174e1da03bcdf52b33157528 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Mon, 17 Nov 2025 19:58:49 -0300 Subject: [PATCH 016/100] Create criar_template.feature --- .../criar_template.feature | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 features/ template_formularios /criar_template.feature diff --git a/features/ template_formularios /criar_template.feature b/features/ template_formularios /criar_template.feature new file mode 100644 index 0000000000..dac7934038 --- /dev/null +++ b/features/ template_formularios /criar_template.feature @@ -0,0 +1,25 @@ +Funcionalidade: Criar template de formulário + +Cenário: Criação de template com dados válidos (Feliz) + Dado que estou na página de criação de templates + E preencho o campo "Nome do template" com "Avaliação 2024" + E adiciono um tipo de questão de múltipla escolha + E adiciono um tipo de questão discursiva + Quando clico no botão "Salvar Template" + Então o sistema deve exibir a mensagem "Template criado com sucesso" + E o template deve ser salvo no banco de dados + +Cenário: Tentativa de criação com título vazio (Triste) + Dado que estou na página de criação de templates + E deixo o campo "Título" vazio + E adiciono tipos de questões válidas + Quando clico no botão "Salvar Template" + Então o sistema deve exibir a mensagem de erro "O campo Título é obrigatório" + E o template não deve ser salvo + +Cenário: Tentativa de criação sem questões (Triste) + Dado que preencho o título corretamente + E não adiciono nenhum tipo de questão ao template + Quando clico no botão "Salvar Template" + Então o sistema deve exibir o erro "O template deve conter pelo menos uma questão" + E o template não deve ser salvo From 0b2f6f204a527db3378a62b89d20b5c4ca7754fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Mon, 17 Nov 2025 19:59:31 -0300 Subject: [PATCH 017/100] Create visualizar_template.feature --- .../visualizar_template.feature | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 features/ template_formularios /visualizar_template.feature diff --git a/features/ template_formularios /visualizar_template.feature b/features/ template_formularios /visualizar_template.feature new file mode 100644 index 0000000000..e878d2ace3 --- /dev/null +++ b/features/ template_formularios /visualizar_template.feature @@ -0,0 +1,37 @@ +Funcionalidade: Visualização dos templates criados + +Cenário: Visualizar lista de templates existentes (Feliz) + Dado que existem templates salvos no sistema + Quando eu acesso a página "Templates" + Então devo ver uma lista contendo todos os templates criados + E devo ver as opções de "Editar" e "Deletar" para cada template da lista + +Cenário: Visualizar lista vazia (Triste) + Dado que não existe nenhum template salvo no sistema + Quando eu acesso a página "Templates" + Então o sistema deve exibir a mensagem "Nenhum template foi criado" + E a lista de visualização deve estar vazia + + Funcionalidade: Edição e Deleção de Templates + +Cenário: Edição de nome de template usado em formulário antigo (Feliz) + Dado que tenho o template "Avaliação A" associado a um formulário já respondido + Quando eu edito o nome do template para "Avaliação A - Revisada" + E clico no botão "Salvar" + Então o sistema deve atualizar o template para "Avaliação A - Revisada" + E os formulários antigos criados com o template "Avaliação A" não devem sofrer alteração + +Cenário: Deleção de um template existente (Feliz) + Dado que seleciono um template existente na lista + Quando clico em "Deletar" + E aparece um pop-up com o texto "Você tem certeza que deseja deletar este template?" + E confirmo a ação clicando no botão "Confirmar" + Então o template deve ser removido da lista de visualização + E não devo ver ele no gerenciamento de templates + +Cenário: Edição com caracteres inválidos no título (Triste) + Dado que estou na tela de edição de um template + Quando preencho o campo "Título" com caracteres não permitidos + Então o sistema deve exibir a mensagem "Formato de título inválido" + E aparece uma mensagem de texto indicando quais caracteres não são permitidos + E as alterações não devem ser salvas From eed4c8375aa40efa6e7886fc24da4606044c6bbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Mon, 17 Nov 2025 20:00:11 -0300 Subject: [PATCH 018/100] Create criar_formulario.feature --- .../criar_formulario.feature | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 features/ template_formularios /criar_formulario.feature diff --git a/features/ template_formularios /criar_formulario.feature b/features/ template_formularios /criar_formulario.feature new file mode 100644 index 0000000000..0ba3dd1750 --- /dev/null +++ b/features/ template_formularios /criar_formulario.feature @@ -0,0 +1,21 @@ +Funcionalidade: Criação de formulário para docentes ou dicentes + +Cenário: Enviar formulário para Discentes de uma turma específica (Feliz) + Dado que estou criando um novo formulário a partir de um template + Quando seleciono a opção "Discentes" no campo de público-alvo + E seleciono uma turma válida "Turma A - Engenharia" + E clico no botão "Enviar" + Então o formulário deve ser enviado especificamente para os alunos daquela turma + +Cenário: Enviar formulário para Docentes (Feliz) + Dado que estou criando um novo formulário + Quando seleciono a opção "Docentes" no campo de público-alvo + Então o formulário deve ser enviado especificamente para os professores + E o campo de seleção de turma deve ficar oculto ou opcional + +Cenário: Tentativa de criar sem selecionar público (Triste) + Dado que preenchi os dados básicos do formulário + E não selecionei nem "Docente" nem "Discente" + Quando tento clicar em "Enviar" + Então o sistema deve exibir o erro "Selecione o público-alvo da avaliação" + E não deve enviar o formulário From 413c93aae9bc63bf8faa6578b9f9615e61479aca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Mon, 17 Nov 2025 20:00:42 -0300 Subject: [PATCH 019/100] Update visualizar_template.feature --- .../visualizar_template.feature | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/features/ template_formularios /visualizar_template.feature b/features/ template_formularios /visualizar_template.feature index e878d2ace3..439840ebc0 100644 --- a/features/ template_formularios /visualizar_template.feature +++ b/features/ template_formularios /visualizar_template.feature @@ -11,27 +11,3 @@ Cenário: Visualizar lista vazia (Triste) Quando eu acesso a página "Templates" Então o sistema deve exibir a mensagem "Nenhum template foi criado" E a lista de visualização deve estar vazia - - Funcionalidade: Edição e Deleção de Templates - -Cenário: Edição de nome de template usado em formulário antigo (Feliz) - Dado que tenho o template "Avaliação A" associado a um formulário já respondido - Quando eu edito o nome do template para "Avaliação A - Revisada" - E clico no botão "Salvar" - Então o sistema deve atualizar o template para "Avaliação A - Revisada" - E os formulários antigos criados com o template "Avaliação A" não devem sofrer alteração - -Cenário: Deleção de um template existente (Feliz) - Dado que seleciono um template existente na lista - Quando clico em "Deletar" - E aparece um pop-up com o texto "Você tem certeza que deseja deletar este template?" - E confirmo a ação clicando no botão "Confirmar" - Então o template deve ser removido da lista de visualização - E não devo ver ele no gerenciamento de templates - -Cenário: Edição com caracteres inválidos no título (Triste) - Dado que estou na tela de edição de um template - Quando preencho o campo "Título" com caracteres não permitidos - Então o sistema deve exibir a mensagem "Formato de título inválido" - E aparece uma mensagem de texto indicando quais caracteres não são permitidos - E as alterações não devem ser salvas From 655409bcc6408be9670994501022fcd29382857e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Mon, 17 Nov 2025 20:01:07 -0300 Subject: [PATCH 020/100] Create editar_template.feature --- .../editar_template.feature | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 features/ template_formularios /editar_template.feature diff --git a/features/ template_formularios /editar_template.feature b/features/ template_formularios /editar_template.feature new file mode 100644 index 0000000000..76dd378cfb --- /dev/null +++ b/features/ template_formularios /editar_template.feature @@ -0,0 +1,23 @@ +Funcionalidade: Edição e Deleção de Templates + +Cenário: Edição de nome de template usado em formulário antigo (Feliz) + Dado que tenho o template "Avaliação A" associado a um formulário já respondido + Quando eu edito o nome do template para "Avaliação A - Revisada" + E clico no botão "Salvar" + Então o sistema deve atualizar o template para "Avaliação A - Revisada" + E os formulários antigos criados com o template "Avaliação A" não devem sofrer alteração + +Cenário: Deleção de um template existente (Feliz) + Dado que seleciono um template existente na lista + Quando clico em "Deletar" + E aparece um pop-up com o texto "Você tem certeza que deseja deletar este template?" + E confirmo a ação clicando no botão "Confirmar" + Então o template deve ser removido da lista de visualização + E não devo ver ele no gerenciamento de templates + +Cenário: Edição com caracteres inválidos no título (Triste) + Dado que estou na tela de edição de um template + Quando preencho o campo "Título" com caracteres não permitidos + Então o sistema deve exibir a mensagem "Formato de título inválido" + E aparece uma mensagem de texto indicando quais caracteres não são permitidos + E as alterações não devem ser salvas From 169c9368c6559eb25abd04c4de4716e3887b97c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Mon, 17 Nov 2025 20:01:21 -0300 Subject: [PATCH 021/100] Delete features/ template_formularios /bdd_templates.feature --- .../bdd_templates.feature | 86 ------------------- 1 file changed, 86 deletions(-) delete mode 100644 features/ template_formularios /bdd_templates.feature diff --git a/features/ template_formularios /bdd_templates.feature b/features/ template_formularios /bdd_templates.feature deleted file mode 100644 index 9de823804f..0000000000 --- a/features/ template_formularios /bdd_templates.feature +++ /dev/null @@ -1,86 +0,0 @@ -Funcionalidade: Criar template de formulário - -Cenário: Criação de template com dados válidos (Feliz) - Dado que estou na página de criação de templates - E preencho o campo "Nome do template" com "Avaliação 2024" - E adiciono um tipo de questão de múltipla escolha - E adiciono um tipo de questão discursiva - Quando clico no botão "Salvar Template" - Então o sistema deve exibir a mensagem "Template criado com sucesso" - E o template deve ser salvo no banco de dados - -Cenário: Tentativa de criação com título vazio (Triste) - Dado que estou na página de criação de templates - E deixo o campo "Título" vazio - E adiciono tipos de questões válidas - Quando clico no botão "Salvar Template" - Então o sistema deve exibir a mensagem de erro "O campo Título é obrigatório" - E o template não deve ser salvo - -Cenário: Tentativa de criação sem questões (Triste) - Dado que preencho o título corretamente - E não adiciono nenhum tipo de questão ao template - Quando clico no botão "Salvar Template" - Então o sistema deve exibir o erro "O template deve conter pelo menos uma questão" - E o template não deve ser salvo - - -Funcionalidade: Visualização dos templates criados - -Cenário: Visualizar lista de templates existentes (Feliz) - Dado que existem templates salvos no sistema - Quando eu acesso a página "Templates" - Então devo ver uma lista contendo todos os templates criados - E devo ver as opções de "Editar" e "Deletar" para cada template da lista - -Cenário: Visualizar lista vazia (Triste) - Dado que não existe nenhum template salvo no sistema - Quando eu acesso a página "Templates" - Então o sistema deve exibir a mensagem "Nenhum template foi criado" - E a lista de visualização deve estar vazia - - Funcionalidade: Edição e Deleção de Templates - -Cenário: Edição de nome de template usado em formulário antigo (Feliz) - Dado que tenho o template "Avaliação A" associado a um formulário já respondido - Quando eu edito o nome do template para "Avaliação A - Revisada" - E clico no botão "Salvar" - Então o sistema deve atualizar o template para "Avaliação A - Revisada" - E os formulários antigos criados com o template "Avaliação A" não devem sofrer alteração - -Cenário: Deleção de um template existente (Feliz) - Dado que seleciono um template existente na lista - Quando clico em "Deletar" - E aparece um pop-up com o texto "Você tem certeza que deseja deletar este template?" - E confirmo a ação clicando no botão "Confirmar" - Então o template deve ser removido da lista de visualização - E não devo ver ele no gerenciamento de templates - -Cenário: Edição com caracteres inválidos no título (Triste) - Dado que estou na tela de edição de um template - Quando preencho o campo "Título" com caracteres não permitidos - Então o sistema deve exibir a mensagem "Formato de título inválido" - E aparece uma mensagem de texto indicando quais caracteres não são permitidos - E as alterações não devem ser salvas - - Funcionalidade: Criação de formulário para docentes ou dicentes - -Cenário: Enviar formulário para Discentes de uma turma específica (Feliz) - Dado que estou criando um novo formulário a partir de um template - Quando seleciono a opção "Discentes" no campo de público-alvo - E seleciono uma turma válida "Turma A - Engenharia" - E clico no botão "Enviar" - Então o formulário deve ser enviado especificamente para os alunos daquela turma - -Cenário: Enviar formulário para Docentes (Feliz) - Dado que estou criando um novo formulário - Quando seleciono a opção "Docentes" no campo de público-alvo - Então o formulário deve ser enviado especificamente para os professores - E o campo de seleção de turma deve ficar oculto ou opcional - -Cenário: Tentativa de criar sem selecionar público (Triste) - Dado que preenchi os dados básicos do formulário - E não selecionei nem "Docente" nem "Discente" - Quando tento clicar em "Enviar" - Então o sistema deve exibir o erro "Selecione o público-alvo da avaliação" - E não deve enviar o formulário From 009ef78e4f1779db356985ae3abfa0c457b45ada Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Mon, 17 Nov 2025 20:01:34 -0300 Subject: [PATCH 022/100] Delete features/ template_formularios directory --- .../criar_template.feature | 25 ------------------- 1 file changed, 25 deletions(-) delete mode 100644 features/ template_formularios/criar_template.feature diff --git a/features/ template_formularios/criar_template.feature b/features/ template_formularios/criar_template.feature deleted file mode 100644 index dac7934038..0000000000 --- a/features/ template_formularios/criar_template.feature +++ /dev/null @@ -1,25 +0,0 @@ -Funcionalidade: Criar template de formulário - -Cenário: Criação de template com dados válidos (Feliz) - Dado que estou na página de criação de templates - E preencho o campo "Nome do template" com "Avaliação 2024" - E adiciono um tipo de questão de múltipla escolha - E adiciono um tipo de questão discursiva - Quando clico no botão "Salvar Template" - Então o sistema deve exibir a mensagem "Template criado com sucesso" - E o template deve ser salvo no banco de dados - -Cenário: Tentativa de criação com título vazio (Triste) - Dado que estou na página de criação de templates - E deixo o campo "Título" vazio - E adiciono tipos de questões válidas - Quando clico no botão "Salvar Template" - Então o sistema deve exibir a mensagem de erro "O campo Título é obrigatório" - E o template não deve ser salvo - -Cenário: Tentativa de criação sem questões (Triste) - Dado que preencho o título corretamente - E não adiciono nenhum tipo de questão ao template - Quando clico no botão "Salvar Template" - Então o sistema deve exibir o erro "O template deve conter pelo menos uma questão" - E o template não deve ser salvo From 8be98ee8bb2ce5a8624b272aa5b5c7c465fd7ade Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Mon, 17 Nov 2025 20:01:47 -0300 Subject: [PATCH 023/100] Delete features/ template_formularios directory --- .../criar_template.feature | 25 ------------------- 1 file changed, 25 deletions(-) delete mode 100644 features/ template_formularios /criar_template.feature diff --git a/features/ template_formularios /criar_template.feature b/features/ template_formularios /criar_template.feature deleted file mode 100644 index dac7934038..0000000000 --- a/features/ template_formularios /criar_template.feature +++ /dev/null @@ -1,25 +0,0 @@ -Funcionalidade: Criar template de formulário - -Cenário: Criação de template com dados válidos (Feliz) - Dado que estou na página de criação de templates - E preencho o campo "Nome do template" com "Avaliação 2024" - E adiciono um tipo de questão de múltipla escolha - E adiciono um tipo de questão discursiva - Quando clico no botão "Salvar Template" - Então o sistema deve exibir a mensagem "Template criado com sucesso" - E o template deve ser salvo no banco de dados - -Cenário: Tentativa de criação com título vazio (Triste) - Dado que estou na página de criação de templates - E deixo o campo "Título" vazio - E adiciono tipos de questões válidas - Quando clico no botão "Salvar Template" - Então o sistema deve exibir a mensagem de erro "O campo Título é obrigatório" - E o template não deve ser salvo - -Cenário: Tentativa de criação sem questões (Triste) - Dado que preencho o título corretamente - E não adiciono nenhum tipo de questão ao template - Quando clico no botão "Salvar Template" - Então o sistema deve exibir o erro "O template deve conter pelo menos uma questão" - E o template não deve ser salvo From 2a78174d790e0563c6acbfa241d44c2a039476f9 Mon Sep 17 00:00:00 2001 From: Carol <232050975@aluno.unb.br> Date: Mon, 17 Nov 2025 20:04:40 -0300 Subject: [PATCH 024/100] =?UTF-8?q?FEAT(add):=20adi=C3=A7=C3=A3o=20do=20BD?= =?UTF-8?q?D=20da=20issue=20#12?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/gerenciamento_departamento.feature | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 features/admin/gerenciamento_departamento.feature diff --git a/features/admin/gerenciamento_departamento.feature b/features/admin/gerenciamento_departamento.feature new file mode 100644 index 0000000000..96c1b3bec6 --- /dev/null +++ b/features/admin/gerenciamento_departamento.feature @@ -0,0 +1,34 @@ +# language: pt-br + +Funcionalidade: Visualização de resultados de avaliação restrita por departamento + + Como um administrador + Eu quero gerenciar (ver e avaliar) somente as turmas do departamento ao qual eu pertenço + A fim de avaliar o desempenho das turmas no semestre atual + +Cenário: Admin do CIC vê apenas as turmas do CIC + Dado que eu estou logado como "admin@sistema.com" + E sou professor do depatamento "CIC" + E eu navego para a página "Gerenciamento" e clico em "Resultados" + E existe uma turma "Bancos de Dados (CIC0097)" que pertence ao departamento "CIC" + E existe uma turma "CÁLCULO 1 (MAT0025)" que pertence ao departamento "MAT" + Quando eu olho a lista de turmas na tela "Gerenciamento - Resultados" + Então eu devo ver o bloco da turma "Bancos de Dados (CIC0097)" + E eu não devo ver o bloco da turma "CÁLCULO 1 (MAT0025)" + +Cenário: Admin do MAT vê apenas as turmas do MAT + Dado que eu estou logado como "admin@sistema.com" + E sou professor do depatamento "MAT" + E eu navego para a página "Gerenciamento" e clico em "Resultados" + E existe uma turma "Bancos de Dados (CIC0097)" que pertence ao departamento "CIC" + E existe uma turma "CÁLCULO 1 (MAT0025)" que pertence ao departamento "MAT" + Quando eu olho a lista de turmas na tela "Gerenciamento - Resultados" + Então eu não devo ver o bloco da turma "Bancos de Dados (CIC0097)" + E eu devo ver o bloco da turma "CÁLCULO 1 (MAT0025)" + +Cenário: Admin do CIC tenta acessar resultados do MAT diretamente pela URL + Dado que eu estou logado como "admin@sistema.com" + E sou professor do depatamento "CIC" + E existe a turma "Cálculo 1 (MAT0025)" (de id: 42) que pertence ao departamento "MAT" + Quando eu tento acessar a URL "/gerenciamento/resultados/42" diretamente no meu navegador + Então eu devo ser redirecionado para a minha página inicial (ou "Dashboard") \ No newline at end of file From 4032504aaa02b2c5b18f66864fa3ebee8a612404 Mon Sep 17 00:00:00 2001 From: Carol <232050975@aluno.unb.br> Date: Thu, 27 Nov 2025 21:47:36 -0300 Subject: [PATCH 025/100] =?UTF-8?q?Implementa=C3=A7=C3=A3o=20completa=20da?= =?UTF-8?q?=20US=20'Importar=20Dados=20do=20SIGAA'=20testado=20no=20Cucumb?= =?UTF-8?q?er.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .rspec | 1 + Gemfile | 2 + Gemfile.lock | 18 + app/assets/stylesheets/application.css | 192 +++++++- app/controllers/admins_controller.rb | 30 ++ app/models/member.rb | 3 + app/models/subject.rb | 3 + app/models/turma.rb | 4 + app/services/sigaa_import_service.rb | 109 +++++ app/views/admins/management.html.erb | 31 ++ app/views/admins/new_import.html.erb | 33 ++ app/views/layouts/application.html.erb | 29 ++ config/routes.rb | 6 +- ...184821_create_join_table_turmas_members.rb | 8 + db/migrate/20251127213250_create_subjects.rb | 10 + db/migrate/20251127213308_create_members.rb | 15 + db/migrate/20251127213521_create_turmas.rb | 12 + db/schema.rb | 49 +++ features/admin/atualizar_dados.feature | 2 +- ...feature => gerenciar_departamento.feature} | 2 +- features/admin/gerenciar_relatorios.feature | 2 +- features/admin/importar_dados.feature | 95 ++-- features/step_definitions/import_steps.rb | 84 ++++ spec/fixtures/files/arquivo_texto.txt | 1 + spec/fixtures/files/disciplinas_2021.2.json | 29 ++ spec/fixtures/files/json_aleatorio.json | 1 + spec/fixtures/files/turmas_2021.2.json | 413 ++++++++++++++++++ spec/fixtures/files/turmas_sem_matricula.json | 24 + spec/rails_helper.rb | 72 +++ spec/spec_helper.rb | 94 ++++ test/fixtures/members.yml | 19 + test/fixtures/subjects.yml | 9 + test/fixtures/turmas.yml | 13 + test/models/member_test.rb | 7 + test/models/subject_test.rb | 7 + test/models/turma_test.rb | 7 + 36 files changed, 1379 insertions(+), 57 deletions(-) create mode 100644 .rspec create mode 100644 app/controllers/admins_controller.rb create mode 100644 app/models/member.rb create mode 100644 app/models/subject.rb create mode 100644 app/models/turma.rb create mode 100644 app/services/sigaa_import_service.rb create mode 100644 app/views/admins/management.html.erb create mode 100644 app/views/admins/new_import.html.erb create mode 100644 db/migrate/20251127184821_create_join_table_turmas_members.rb create mode 100644 db/migrate/20251127213250_create_subjects.rb create mode 100644 db/migrate/20251127213308_create_members.rb create mode 100644 db/migrate/20251127213521_create_turmas.rb create mode 100644 db/schema.rb rename features/admin/{gerenciamento_departamento.feature => gerenciar_departamento.feature} (99%) create mode 100644 features/step_definitions/import_steps.rb create mode 100644 spec/fixtures/files/arquivo_texto.txt create mode 100755 spec/fixtures/files/disciplinas_2021.2.json create mode 100644 spec/fixtures/files/json_aleatorio.json create mode 100755 spec/fixtures/files/turmas_2021.2.json create mode 100644 spec/fixtures/files/turmas_sem_matricula.json create mode 100644 spec/rails_helper.rb create mode 100644 spec/spec_helper.rb create mode 100644 test/fixtures/members.yml create mode 100644 test/fixtures/subjects.yml create mode 100644 test/fixtures/turmas.yml create mode 100644 test/models/member_test.rb create mode 100644 test/models/subject_test.rb create mode 100644 test/models/turma_test.rb diff --git a/.rspec b/.rspec new file mode 100644 index 0000000000..c99d2e7396 --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--require spec_helper diff --git a/Gemfile b/Gemfile index 03a0aba365..364bd47248 100644 --- a/Gemfile +++ b/Gemfile @@ -54,6 +54,7 @@ end group :development do # Use console on exceptions pages [https://github.com/rails/web-console] gem "web-console" + gem "rspec-rails" end # Testing gems @@ -61,6 +62,7 @@ group :test do # Rails default system testing gem "capybara" gem "selenium-webdriver" + gem "rspec-rails" # Cucumber support gem "cucumber-rails", require: false diff --git a/Gemfile.lock b/Gemfile.lock index aedc81f092..54cbd8c129 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -302,6 +302,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.7) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-rails (8.0.2) + actionpack (>= 7.2) + activesupport (>= 7.2) + railties (>= 7.2) + rspec-core (~> 3.13) + rspec-expectations (~> 3.13) + rspec-mocks (~> 3.13) + rspec-support (~> 3.13) + rspec-support (3.13.6) rubocop (1.81.7) json (~> 2.3) language_server-protocol (~> 3.17.0.2) @@ -433,6 +450,7 @@ DEPENDENCIES propshaft puma (>= 5.0) rails (~> 8.0.2, >= 8.0.2.1) + rspec-rails rubocop-rails-omakase selenium-webdriver solid_cable diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index fe93333c0f..a9bb2b5672 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -1,10 +1,182 @@ -/* - * 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. - */ +* { + box_sizing: border-box; +} + +body { + margin: 0; + padding: 0; + font-family: 'Arial', sans-serif; + height: 100vh; + overflow: hidden; +} + +.admin-container { + display: flex; + height: 100vh; + width: 100vw; +} + +.sidebar { + width: 250px; + background-color: #fff; + border-right: 1px solid #ddd; + display: flex; + flex-direction: column; + flex-shrink: 0; +} + +.sidebar-header { + padding: 20px; + font-size: 1.1rem; + font-weight: bold; + display: flex; + align-items: center; + gap: 10px; +} + +.sidebar-menu { + display: flex; + flex-direction: column; +} + +.sidebar-menu a { + padding: 15px 20px; + text-decoration: none; + color: #333; + font-size: 0.9rem; +} + +.sidebar-menu a.active { + background-color: #6a1b57; + color: white; + border-left: 5px solid #4a0d3b; +} + +.main-content { + flex: 1; + background-color: #e0e0e0; + display: flex; + flex-direction: column; +} + +.top-navbar { + height: 60px; + background-color: white; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 30px; + box-shadow: 0 2px 4px rgba(0,0,0,0.05); +} + +.nav-search { + display: flex; + align-items: center; + gap: 20px; +} + +.search-input { + padding: 8px 15px; + border-radius: 20px; + border: 1px solid #ccc; + width: 250px; + outline: none; +} + +.profile-circle { + width: 40px; + height: 40px; + background-color: #6a1b57; + color: white; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + font-size: 1.2rem; +} + +.content-area { + flex: 1; + display: flex; + justify-content: center; + align-items: center; + width: 100%; +} +.action-card { + background-color: white; + width: 450px; + padding: 40px; + border-radius: 8px; + box-shadow: 0 4px 10px rgba(0,0,0,0.1); + display: flex; + flex-direction: column; + gap: 15px; +} + +.btn-action { + width: 100%; + padding: 15px 0; + border-radius: 6px; + text-decoration: none; + font-weight: bold; + text-align: center; + border: none; + cursor: pointer; + font-size: 1rem; + font-family: inherit; + display: block; +} + +.btn-primary-green { + background-color: #2ecc71; + color: white; +} +.btn-primary-green:hover { background-color: #27ae60; } + +.btn-secondary-green { + background-color: #8ceabb; + color: white; +} +.btn-secondary-green:hover { background-color: #7bd3a6; } + +.flash-container { + position: fixed; + bottom: 20px; + right: 20px; + z-index: 9999; + display: flex; + flex-direction: column; + gap: 10px; +} + +.flash-message { + width: auto; + min-width: 200px; + max-width: 280px; + padding: 10px 15px; + border-radius: 6px; + text-align: center; + font-size: 0.85rem; + font-weight: bold; + box-shadow: 0 2px 5px rgba(0,0,0,0.2); + opacity: 1; + transition: opacity 1s ease-out, visibility 1s ease-out; +} + +.flash-success { + background-color: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; +} + +.flash-error { + background-color: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; +} + +.fade-out { + opacity: 0; + visibility: hidden; +} \ No newline at end of file diff --git a/app/controllers/admins_controller.rb b/app/controllers/admins_controller.rb new file mode 100644 index 0000000000..c4d8bca003 --- /dev/null +++ b/app/controllers/admins_controller.rb @@ -0,0 +1,30 @@ +class AdminsController < ApplicationController + def management + end + + def new_import + end + + def create_import + uploaded_file = params[:file] + + if uploaded_file.nil? + redirect_to import_sigaa_path, alert: 'Nenhum arquivo selecionado.' + return + end + + begin + SigaaImportService.new(uploaded_file.path).call + redirect_to admin_management_path, notice: 'Dados importados com sucesso!' + + rescue JSON::ParserError + redirect_to import_sigaa_path, alert: 'Erro: O arquivo enviado não é um JSON válido.' + + rescue SigaaImportService::InvalidFileError => e + redirect_to import_sigaa_path, alert: "Erro de Validação: #{e.message}" + + rescue StandardError => e + redirect_to import_sigaa_path, alert: "Ocorreu um erro inesperado: #{e.message}" + end + end +end \ No newline at end of file diff --git a/app/models/member.rb b/app/models/member.rb new file mode 100644 index 0000000000..cf21ce3dee --- /dev/null +++ b/app/models/member.rb @@ -0,0 +1,3 @@ +class Member < ApplicationRecord + has_and_belongs_to_many :turmas +end \ No newline at end of file diff --git a/app/models/subject.rb b/app/models/subject.rb new file mode 100644 index 0000000000..a594dac65a --- /dev/null +++ b/app/models/subject.rb @@ -0,0 +1,3 @@ +class Subject < ApplicationRecord + has_many :turmas +end \ No newline at end of file diff --git a/app/models/turma.rb b/app/models/turma.rb new file mode 100644 index 0000000000..9dad348497 --- /dev/null +++ b/app/models/turma.rb @@ -0,0 +1,4 @@ +class Turma < ApplicationRecord + belongs_to :subject + has_and_belongs_to_many :members +end \ No newline at end of file diff --git a/app/services/sigaa_import_service.rb b/app/services/sigaa_import_service.rb new file mode 100644 index 0000000000..f05afa48b2 --- /dev/null +++ b/app/services/sigaa_import_service.rb @@ -0,0 +1,109 @@ +require 'json' + +class SigaaImportService + class InvalidFileError < StandardError; end + + REQUIRED_SUBJECT_KEYS = %w[code name class] + + REQUIRED_CLASS_KEYS = %w[classCode semester time] + + REQUIRED_MEMBER_FILE_KEYS = %w[code classCode semester dicente] + + REQUIRED_STUDENT_KEYS = %w[nome matricula curso usuario email ocupacao formacao] + + def initialize(file_path) + @file_path = file_path + end + + def call + file_content = File.read(@file_path) + + begin + json_data = JSON.parse(file_content) + rescue JSON::ParserError + raise InvalidFileError, "O arquivo não é um JSON válido." + end + + unless json_data.is_a?(Array) + raise InvalidFileError, "O JSON deve ser uma lista (Array) de objetos." + end + + ActiveRecord::Base.transaction do + json_data.each_with_index do |entry, index| + if entry.key?('class') + process_classes_file(entry, index) + elsif entry.key?('dicente') + process_members_file(entry, index) + else + raise InvalidFileError, "O objeto na linha #{index + 1} não foi reconhecido (não possui chave 'class' nem 'dicente')." + end + end + end + end + + private + + def validate_keys!(data, required_keys, context_message) + required_keys.each do |key| + value = data[key] + if value.nil? || (value.respond_to?(:strip) && value.strip.empty?) + raise InvalidFileError, "Erro #{context_message}: O campo obrigatório '#{key}' está ausente ou vazio." + end + end + end + + def process_classes_file(entry, index) + validate_keys!(entry, REQUIRED_SUBJECT_KEYS, "na Matéria (item #{index + 1})") + + validate_keys!(entry['class'], REQUIRED_CLASS_KEYS, "na Turma da matéria '#{entry['code']}'") + + subject = Subject.find_or_create_by(code: entry['code']) do |s| + s.name = entry['name'] + end + subject.update(name: entry['name']) + + class_info = entry['class'] + Turma.find_or_create_by( + class_code: class_info['classCode'], + semester: class_info['semester'], + subject: subject + ) do |t| + t.time = class_info['time'] + end + end + + def process_members_file(entry, index) + validate_keys!(entry, REQUIRED_MEMBER_FILE_KEYS, "no cabeçalho da Turma (item #{index + 1})") + + if entry['dicente'] + entry['dicente'].each_with_index do |student_data, student_index| + validate_keys!(student_data, REQUIRED_STUDENT_KEYS, "no Aluno ##{student_index + 1} da turma '#{entry['code']}'") + end + end + + subject = Subject.find_by(code: entry['code']) + unless subject + return + end + + turma = Turma.find_by( + subject: subject, + class_code: entry['classCode'], + semester: entry['semester'] + ) + return unless turma + + entry['dicente'].each do |student_data| + member = Member.find_or_create_by(matricula: student_data['matricula']) do |m| + m.nome = student_data['nome'] + m.curso = student_data['curso'] + m.usuario = student_data['usuario'] + m.email = student_data['email'] + m.ocupacao = student_data['ocupacao'] + m.formacao = student_data['formacao'] + end + + turma.members << member unless turma.members.exists?(member.id) + end + end +end \ No newline at end of file diff --git a/app/views/admins/management.html.erb b/app/views/admins/management.html.erb new file mode 100644 index 0000000000..4d8a2ccb7e --- /dev/null +++ b/app/views/admins/management.html.erb @@ -0,0 +1,31 @@ +
+ + + +
+
+

Gerenciamento

+ +
+ +
+
+ <%= link_to "Importar dados", import_sigaa_path, class: "btn-action btn-primary-green" %> + + + +
+
+
+
\ No newline at end of file diff --git a/app/views/admins/new_import.html.erb b/app/views/admins/new_import.html.erb new file mode 100644 index 0000000000..470e795299 --- /dev/null +++ b/app/views/admins/new_import.html.erb @@ -0,0 +1,33 @@ +
+ +
+

Importar Dados do SIGAA

+

+ Selecione o arquivo JSON de Turmas ou Alunos. +

+ + <%= form_with url: import_sigaa_path, local: true, multipart: true do |form| %> + + <% if flash[:alert] %> +
+ <%= flash[:alert] %> +
+ <% end %> + +
+ <%= form.label :file, "Arquivo JSON:", style: "display: block; margin-bottom: 8px; font-weight: bold; color: #333;" %> + + <%= form.file_field :file, accept: 'application/json', + style: "width: 100%; box-sizing: border-box; display: block; padding: 12px; border: 1px solid #ccc; border-radius: 6px; font-size: 1rem;" %> +
+ + <%= form.submit "Enviar Arquivo", class: "btn-action btn-primary-green", style: "margin-bottom: 15px;" %> + +
+ <%= link_to "Voltar para o Menu", admin_management_path, style: "color: #666; text-decoration: none; font-size: 0.9rem;" %> +
+ + <% end %> +
+ +
\ No newline at end of file diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 1455450e14..173428e2b5 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -24,5 +24,34 @@ <%= yield %> +
+ <% if notice %> +
+ <%= notice %> +
+ <% end %> + + <% if alert %> +
+ <%= alert %> +
+ <% end %> +
+ + diff --git a/config/routes.rb b/config/routes.rb index 48254e88ed..7a6bb6f5b7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -3,7 +3,11 @@ # 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 + + get 'gerenciamento', to: 'admins#management', as: 'admin_management' + get 'importar_sigaa', to: 'admins#new_import', as: 'import_sigaa' + post 'importar_sigaa', to: 'admins#create_import' + # Render dynamic PWA files from app/views/pwa/* (remember to link manifest in application.html.erb) # get "manifest" => "rails/pwa#manifest", as: :pwa_manifest diff --git a/db/migrate/20251127184821_create_join_table_turmas_members.rb b/db/migrate/20251127184821_create_join_table_turmas_members.rb new file mode 100644 index 0000000000..03bf649580 --- /dev/null +++ b/db/migrate/20251127184821_create_join_table_turmas_members.rb @@ -0,0 +1,8 @@ +class CreateJoinTableTurmasMembers < ActiveRecord::Migration[8.0] + def change + create_join_table :turmas, :members do |t| + # t.index [:turma_id, :member_id] + # t.index [:member_id, :turma_id] + end + end +end diff --git a/db/migrate/20251127213250_create_subjects.rb b/db/migrate/20251127213250_create_subjects.rb new file mode 100644 index 0000000000..3c584232cd --- /dev/null +++ b/db/migrate/20251127213250_create_subjects.rb @@ -0,0 +1,10 @@ +class CreateSubjects < ActiveRecord::Migration[8.0] + def change + create_table :subjects do |t| + t.string :code + t.string :name + + t.timestamps + end + end +end diff --git a/db/migrate/20251127213308_create_members.rb b/db/migrate/20251127213308_create_members.rb new file mode 100644 index 0000000000..dfd4fed068 --- /dev/null +++ b/db/migrate/20251127213308_create_members.rb @@ -0,0 +1,15 @@ +class CreateMembers < ActiveRecord::Migration[8.0] + def change + create_table :members do |t| + t.string :nome + t.string :matricula + t.string :curso + t.string :usuario + t.string :formacao + t.string :ocupacao + t.string :email + + t.timestamps + end + end +end diff --git a/db/migrate/20251127213521_create_turmas.rb b/db/migrate/20251127213521_create_turmas.rb new file mode 100644 index 0000000000..dc1179521a --- /dev/null +++ b/db/migrate/20251127213521_create_turmas.rb @@ -0,0 +1,12 @@ +class CreateTurmas < ActiveRecord::Migration[8.0] + def change + create_table :turmas do |t| + t.string :class_code + t.string :semester + t.string :time + t.references :subject, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb new file mode 100644 index 0000000000..f500d7df6a --- /dev/null +++ b/db/schema.rb @@ -0,0 +1,49 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema[8.0].define(version: 2025_11_27_213521) do + create_table "members", force: :cascade do |t| + t.string "nome" + t.string "matricula" + t.string "curso" + t.string "usuario" + t.string "formacao" + t.string "ocupacao" + t.string "email" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "members_turmas", id: false, force: :cascade do |t| + t.integer "turma_id", null: false + t.integer "member_id", null: false + end + + create_table "subjects", force: :cascade do |t| + t.string "code" + t.string "name" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "turmas", force: :cascade do |t| + t.string "class_code" + t.string "semester" + t.string "time" + t.integer "subject_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["subject_id"], name: "index_turmas_on_subject_id" + end + + add_foreign_key "turmas", "subjects" +end diff --git a/features/admin/atualizar_dados.feature b/features/admin/atualizar_dados.feature index 1e10d4ba9a..625c9553bc 100644 --- a/features/admin/atualizar_dados.feature +++ b/features/admin/atualizar_dados.feature @@ -1,4 +1,4 @@ -# language: pt-br +# language: pt Funcionalidade: Atualização da base de dados por importação de JSON do SIGAA diff --git a/features/admin/gerenciamento_departamento.feature b/features/admin/gerenciar_departamento.feature similarity index 99% rename from features/admin/gerenciamento_departamento.feature rename to features/admin/gerenciar_departamento.feature index 96c1b3bec6..f06d741405 100644 --- a/features/admin/gerenciamento_departamento.feature +++ b/features/admin/gerenciar_departamento.feature @@ -1,4 +1,4 @@ -# language: pt-br +# language: pt Funcionalidade: Visualização de resultados de avaliação restrita por departamento diff --git a/features/admin/gerenciar_relatorios.feature b/features/admin/gerenciar_relatorios.feature index 56b2b263ff..910115e961 100644 --- a/features/admin/gerenciar_relatorios.feature +++ b/features/admin/gerenciar_relatorios.feature @@ -1,4 +1,4 @@ -# language: pt-br +# language: pt Funcionalidade: Visualização e Download de Resultados de Avaliação Como um administrador diff --git a/features/admin/importar_dados.feature b/features/admin/importar_dados.feature index a31421e252..4ab54d8e3b 100644 --- a/features/admin/importar_dados.feature +++ b/features/admin/importar_dados.feature @@ -1,48 +1,57 @@ -# language: pt-br - +# language: pt Funcionalidade: Alimentação inicial da base de dados com JSON do SIGAA - Como um administrador - Eu quero importar dados de turmas, matérias e participantes do SIGAA (caso não existam na base de dados atual) - A fim de alimentar a base de dados do sistema. + Eu quero importar arquivos JSON (Turmas e Alunos) + A fim de alimentar a base de dados do sistema com segurança e integridade. + + Contexto: + Dado que eu estou logado como "admin@sistema.com" + E que eu estou na página "Importar Dados do SIGAA" + + Cenário: Admin importa Turmas e depois Alunos em um sistema vazio + Dado o banco de dados não contém nenhuma disciplina ou aluno + E eu tenho um arquivo "disciplinas_2021.2.json" + E eu tenho um arquivo "turmas_2021.2.json" + Quando eu seleciono o arquivo "disciplinas_2021.2.json" + E eu clico no botão "Enviar Arquivo" + Então eu devo ver a mensagem "Dados importados com sucesso!" + E a disciplina "BANCOS DE DADOS" ("CIC0097") deve existir no sistema + E que eu estou na página "Importar Dados do SIGAA" + Quando eu seleciono o arquivo "turmas_2021.2.json" + E eu clico no botão "Enviar Arquivo" + Então eu devo ver a mensagem "Dados importados com sucesso!" + E o aluno "Ana Clara Jordao Perna" (190084006) deve existir no sistema + + Cenário: Admin reenvia arquivo e o sistema ignora duplicatas + Dado já existe uma disciplina "BANCOS DE DADOS" ("CIC0097") no sistema + E eu tenho um arquivo "disciplinas_2021.2.json" + Quando eu seleciono o arquivo "disciplinas_2021.2.json" + E eu clico no botão "Enviar Arquivo" + Então eu devo ver a mensagem "Dados importados com sucesso!" + # Verifica se não duplicou (continua existindo apenas 1 registro) + E a disciplina "BANCOS DE DADOS" ("CIC0097") deve existir no sistema -Cenário: Admin importa dados em um sistema vazio - Dado que eu estou logado como "admin@sistema.com" - E eu estou na página que contém o botão "Importar dados" - E o banco de dados não contém nenhuma disciplina ou aluno - E eu tenho um arquivo "disciplinas_2021.2.json" (com a disciplina "BANCOS DE DADOS") - E eu tenho um arquivo "turmas_2021.2.json" (com o aluno "Ana Clara Jordao Perna") - Quando eu seleciono os arquivos "disciplinas_2021.2.json" e "turmas_2021.2.json" - E importo-os ao sistema - Então eu devo ver a mensagem "Importação concluída." - E a disciplina "BANCOS DE DADOS" (CIC0097) deve existir no sistema - E o aluno "Ana Clara Jordao Perna" (190084006) deve existir no sistema + Cenário: Admin tenta enviar um arquivo que não é JSON (Sintaxe Inválida) + # Aqui usamos um arquivo de texto renomeado ou pdf + Dado eu tenho um arquivo "arquivo_texto.txt" + Quando eu seleciono o arquivo "arquivo_texto.txt" + E eu clico no botão "Enviar Arquivo" + Então eu devo ver a mensagem "O arquivo não é um JSON válido" + E eu devo continuar na página "Importar Dados do SIGAA" -Cenário: Admin importa dados que já existem na base - Dado que eu estou logado como "admin@sistema.com" - E eu estou na página que contém o botão "Importar dados" - E já existe uma disciplina "BANCOS DE DADOS" (CIC0097) no sistema - E já existe o aluno "Ana Clara Jordao Perna" (190084006) - E eu tenho um arquivo "turmas_2021.2_repetido.json" que contém os mesmos dados - Quando eu seleciono o arquivo "turmas_2021.2_repetido.json" - E impoto-o ao sistema - Então eu devo ver a mensagem "Importação concluída." - E deve haver apenas 1 aluno com a matrícula "190084006" no sistema - -Cenário: Admin tenta alimentar a base com um arquivo que não é JSON - Dado que eu estou logado como "admin@sistema.com" - E eu estou na página que contém o botão "Importar dados" - E o banco de dados está vazio - Quando eu seleciono um arquivo "lista_de_alunos.pdf" - E importo-o ao sistema - Então devo ver a mensagem de erro "Formato de arquivo inválido. Por favor, envie um arquivo .json." + Cenário: Admin envia JSON com estrutura incorreta (Falta campo obrigatório) + # Este arquivo tem um aluno sem a chave "matricula" + Dado eu tenho um arquivo "turmas_sem_matricula.json" + Quando eu seleciono o arquivo "turmas_sem_matricula.json" + E eu clico no botão "Enviar Arquivo" + Então eu devo ver a mensagem "Erro de Validação" + E eu devo ver a mensagem "O campo obrigatório 'matricula' está ausente ou vazio" + E o aluno "Aluno Sem Matricula" (0) não deve existir no sistema -Cenário: Admin importa JSON de turma com a chave "matricula" faltando - Dado que eu estou logado como "admin@sistema.com" - E eu estou na página que contém o botão "Importar dados" - E eu tenho um arquivo "turmas_sem_matricula.json" onde um "discente" não possui a chave "matricula" - Quando eu seleciono o arquivo "turmas_sem_matricula.json" - E importo-o ao sistema - Então eu devo ver a mensagem de erro "Erro na importação: O arquivo JSON é inválido." - - + Cenário: Admin envia JSON desconhecido (Nem turma, nem aluno) + # Um JSON válido, mas com dados aleatórios + Dado eu tenho um arquivo "json_aleatorio.json" + Quando eu seleciono o arquivo "json_aleatorio.json" + E eu clico no botão "Enviar Arquivo" + Então eu devo ver a mensagem "Erro de Validação" + E eu devo ver a mensagem "O JSON deve ser uma lista (Array) de objetos" \ No newline at end of file diff --git a/features/step_definitions/import_steps.rb b/features/step_definitions/import_steps.rb new file mode 100644 index 0000000000..8b60d2b9ed --- /dev/null +++ b/features/step_definitions/import_steps.rb @@ -0,0 +1,84 @@ +Dado('que eu estou logado como {string}') do |email| + @current_user = Member.create!(email: email, nome: "Admin", ocupacao: "docente", matricula: "000000") +end + +Dado('que eu estou na página {string}') do |pagina| + if pagina == "Importar Dados do SIGAA" || pagina.include?("Importar") + visit '/importar_sigaa' + else + visit '/gerenciamento' + end +end + +Dado('o banco de dados não contém nenhuma disciplina ou aluno') do + Member.destroy_all + Turma.destroy_all + Subject.destroy_all +end + +Dado('já existe uma disciplina {string} \({string}\) no sistema') do |nome, codigo| + Subject.create!(name: nome, code: codigo) +end + +Dado('eu tenho um arquivo {string} {string}') do |nome_arquivo, _descricao| + @nome_arquivo = nome_arquivo +end + +Dado('eu tenho um arquivo {string}') do |nome_arquivo| + @nome_arquivo = nome_arquivo +end + +Quando('eu seleciono o arquivo {string} para {string}') do |nome_arquivo, _campo| + path = Rails.root.join('spec/fixtures/files', nome_arquivo) + attach_file('file', path) +end + +Quando('eu seleciono o arquivo {string}') do |nome_arquivo| + path = Rails.root.join('spec/fixtures/files', nome_arquivo) + attach_file('file', path) +end + +Quando('eu clico no botão {string}') do |texto_botao| + click_button texto_botao +end + +Quando('importo-o ao sistema') do + click_button "Enviar Arquivo" +end + +Então('eu devo ser redirecionado para a {string}') do |pagina| + expect(current_path).to eq(admin_management_path) +end + +Então('eu devo ver a mensagem {string}') do |mensagem| + expect(page).to have_content(mensagem) +end + +Então('eu devo continuar na página {string}') do |_pagina| + expect(current_path).to eq(import_sigaa_path) +end + +Então('eu devo ver a mensagem de erro {string}') do |mensagem| + expect(page).to have_content(mensagem) +end + +Então('a disciplina {string} \({string}\) deve existir no sistema') do |nome, codigo| + subject = Subject.find_by(code: codigo) + expect(subject).to be_present + expect(subject.name).to eq(nome) +end + +Então('o aluno {string} \({int}\) deve existir no sistema') do |nome, matricula| + member = Member.find_by(matricula: matricula.to_s) + expect(member).to be_present + expect(member.nome).to eq(nome) +end + +Então('deve haver apenas {int} aluno com a matrícula {string} no sistema') do |quantidade, matricula| + expect(Member.where(matricula: matricula).count).to eq(quantidade) +end + +Então(/^o aluno "([^"]*)" \(([^)]*)\) não deve existir no sistema$/) do |nome, matricula| + member = Member.find_by(matricula: matricula.to_s) + expect(member).to be_nil +end \ No newline at end of file diff --git a/spec/fixtures/files/arquivo_texto.txt b/spec/fixtures/files/arquivo_texto.txt new file mode 100644 index 0000000000..8eb6bbd7f0 --- /dev/null +++ b/spec/fixtures/files/arquivo_texto.txt @@ -0,0 +1 @@ +Oiiii!!! \ No newline at end of file diff --git a/spec/fixtures/files/disciplinas_2021.2.json b/spec/fixtures/files/disciplinas_2021.2.json new file mode 100755 index 0000000000..c42c269750 --- /dev/null +++ b/spec/fixtures/files/disciplinas_2021.2.json @@ -0,0 +1,29 @@ +[ + { + "code": "CIC0097", + "name": "BANCOS DE DADOS", + "class": { + "classCode": "TA", + "semester": "2021.2", + "time": "35T45" + } + }, + { + "code": "CIC0105", + "name": "ENGENHARIA DE SOFTWARE", + "class": { + "classCode": "TA", + "semester": "2021.2", + "time": "35M12" + } + }, + { + "code": "CIC0202", + "name": "PROGRAMAÇÃO CONCORRENTE", + "class": { + "classCode": "TA", + "semester": "2021.2", + "time": "35M34" + } + } +] diff --git a/spec/fixtures/files/json_aleatorio.json b/spec/fixtures/files/json_aleatorio.json new file mode 100644 index 0000000000..37d6db60de --- /dev/null +++ b/spec/fixtures/files/json_aleatorio.json @@ -0,0 +1 @@ +{"K-pop": "BLACKPINK"} \ No newline at end of file diff --git a/spec/fixtures/files/turmas_2021.2.json b/spec/fixtures/files/turmas_2021.2.json new file mode 100755 index 0000000000..733560ef03 --- /dev/null +++ b/spec/fixtures/files/turmas_2021.2.json @@ -0,0 +1,413 @@ +[ + { + "code": "CIC0097", + "classCode": "TA", + "semester": "2021.2", + "dicente": [ + { + "nome": "Ana Clara Jordao Perna", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190084006", + "usuario": "190084006", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "acjpjvjp@gmail.com" + }, + { + "nome": "Andre Carvalho de Roure", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "200033522", + "usuario": "200033522", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "andreCarvalhoroure@gmail.com" + }, + { + "nome": "André Carvalho Marques", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "150005491", + "usuario": "150005491", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "andre.acm97@outlook.com" + }, + { + "nome": "Antonio Vinicius de Moura Rodrigues", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190084502", + "usuario": "190084502", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "antoniovmoura.r@gmail.com" + }, + { + "nome": "Arthur Barreiros de Oliveira Mota", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190102829", + "usuario": "190102829", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "arthurbarreirosmota@gmail.com" + }, + { + "nome": "ARTHUR RODRIGUES NEVES", + "curso": "ENGENHARIA DE COMPUTAÇÃO/CIC", + "matricula": "202014403", + "usuario": "202014403", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "arthurcontroleambiental@gmail.com" + }, + { + "nome": "Bianca Glycia Boueri", + "curso": "ENGENHARIA MECATRÔNICA - CONTROLE E AUTOMAÇÃO/FTD", + "matricula": "170161561", + "usuario": "170161561", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "biancaglyciaboueri@gmail.com" + }, + { + "nome": "Caio Otávio Peluti Alencar", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190085312", + "usuario": "190085312", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "peluticaio@gmail.com" + }, + { + "nome": "Camila Frealdo Fraga", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "170007561", + "usuario": "170007561", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "camilizx2021@gmail.com" + }, + { + "nome": "Claudio Roberto Oliveira Peres de Barros", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190097591", + "usuario": "190097591", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "dinhobarros15@gmail.com" + }, + { + "nome": "Daltro Oliveira Vinuto", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "160025966", + "usuario": "160025966", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "daltroov777@gmail.com" + }, + { + "nome": "Davi de Moura Amaral", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "200016750", + "usuario": "200016750", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "davimouraamaral@gmail.com" + }, + { + "nome": "Eduardo Xavier Dantas", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190086530", + "usuario": "190086530", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "eduardoxdantas@gmail.com" + }, + { + "nome": "Enzo Nunes Leal Sampaio", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190062789", + "usuario": "190062789", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "enzonleal2016@hotmail.com" + }, + { + "nome": "Enzo Yoshio Niho", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190027304", + "usuario": "190027304", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "enzoyn@hotmail.com" + }, + { + "nome": "Gabriel Faustino Lima da Rocha", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190013249", + "usuario": "190013249", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "gabrielfaustino99@gmail.com" + }, + { + "nome": "Gabriel Ligoski", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190087498", + "usuario": "190087498", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "gabriel.ligoski@gmail.com" + }, + { + "nome": "GABRIEL MENDES CIRIATICO GUIMARÃES", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "202033202", + "usuario": "202033202", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "gabrielciriatico@gmail.com" + }, + { + "nome": "Gustavo Rodrigues dos Santos", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190014121", + "usuario": "190014121", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "190014121@aluno.unb.br" + }, + { + "nome": "Gustavo Rodrigues Gualberto", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190108266", + "usuario": "190108266", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "gustavorgualberto@gmail.com" + }, + { + "nome": "Igor David Morais", + "curso": "ENGENHARIA MECATRÔNICA - CONTROLE E AUTOMAÇÃO/FTD", + "matricula": "180102141", + "usuario": "180102141", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "igordavid13@gmail.com" + }, + { + "nome": "Jefte Augusto Gomes Batista", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "180057570", + "usuario": "180057570", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "ndaffte@gmail.com" + }, + { + "nome": "Karolina de Souza Silva", + "curso": "ENGENHARIA DE COMPUTAÇÃO/CIC", + "matricula": "190046791", + "usuario": "190046791", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "karolinasouza@outlook.com" + }, + { + "nome": "Kléber Rodrigues da Costa Júnior", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "200053680", + "usuario": "200053680", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "kleberrjr7@gmail.com" + }, + { + "nome": "Luca Delpino Barbabella", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "180125559", + "usuario": "180125559", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "barbadluca@gmail.com" + }, + { + "nome": "Lucas de Almeida Abreu Faria", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "170016668", + "usuario": "170016668", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "lucasaafaria@gmail.com" + }, + { + "nome": "Lucas Gonçalves Ramalho", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190098091", + "usuario": "190098091", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "lucasramalho29@gmail.com" + }, + { + "nome": "Lucas Monteiro Miranda", + "curso": "ENGENHARIA MECATRÔNICA - CONTROLE E AUTOMAÇÃO/FTD", + "matricula": "170149684", + "usuario": "170149684", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "luquinha_miranda@hotmail.com" + }, + { + "nome": "Lucas Resende Silveira Reis", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "180144421", + "usuario": "180144421", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "180144421@aluno.unb.br" + }, + { + "nome": "Luis Fernando Freitas Lamellas", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190016841", + "usuario": "190016841", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "lflamellas@icloud.com" + }, + { + "nome": "Luiza de Araujo Nunes Gomes", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190112794", + "usuario": "190112794", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "luizangomes@outlook.com" + }, + { + "nome": "Marcelo Aiache Postiglione", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "180126652", + "usuario": "180126652", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "180126652@aluno.unb.br" + }, + { + "nome": "Marcelo Junqueira Ferreira", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "200023624", + "usuario": "200023624", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "marcelojunqueiraf@gmail.com" + }, + { + "nome": "MARIA EDUARDA CARVALHO SANTOS", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190092556", + "usuario": "190092556", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "auntduda@gmail.com" + }, + { + "nome": "Maria Eduarda Lacerda Dantas", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "200067184", + "usuario": "200067184", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "lacwerda@gmail.com" + }, + { + "nome": "Maylla Krislainy de Sousa Silva", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190043873", + "usuario": "190043873", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "mayllak@hotmail.com" + }, + { + "nome": "Pedro Cesar Ribeiro Passos", + "curso": "ENGENHARIA MECATRÔNICA - CONTROLE E AUTOMAÇÃO/FTD", + "matricula": "180139312", + "usuario": "180139312", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "pedrocesarribeiro2013@gmail.com" + }, + { + "nome": "Rafael Mascarenhas Dal Moro", + "curso": "ENGENHARIA DE COMPUTAÇÃO/CIC", + "matricula": "170021041", + "usuario": "170021041", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "170021041@aluno.unb.br" + }, + { + "nome": "Rodrigo Mamedio Arrelaro", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190095164", + "usuario": "190095164", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "arrelaro1@hotmail.com" + }, + { + "nome": "Thiago de Oliveira Albuquerque", + "curso": "ENGENHARIA DE COMPUTAÇÃO/CIC", + "matricula": "140177442", + "usuario": "140177442", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "thiago.work.ti@outlook.com" + }, + { + "nome": "Thiago Elias dos Reis", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190126892", + "usuario": "190126892", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "thiagoeliasdosreis01@gmail.com" + }, + { + "nome": "Victor Hugo Rodrigues Fernandes", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "180132041", + "usuario": "180132041", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "aluno0sem.luz@gmail.com" + }, + { + "nome": "Vinicius Lima Passos", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "200028545", + "usuario": "200028545", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "viniciuslimapassos@gmail.com" + }, + { + "nome": "William Xavier dos Santos", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190075384", + "usuario": "190075384", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "wilxavier@me.com" + } + ], + "docente": { + "nome": "MARISTELA TERTO DE HOLANDA", + "departamento": "DEPTO CIÊNCIAS DA COMPUTAÇÃO", + "formacao": "DOUTORADO", + "usuario": "83807519491", + "email": "mholanda@unb.br", + "ocupacao": "docente" + } + } +] diff --git a/spec/fixtures/files/turmas_sem_matricula.json b/spec/fixtures/files/turmas_sem_matricula.json new file mode 100644 index 0000000000..9960d8948b --- /dev/null +++ b/spec/fixtures/files/turmas_sem_matricula.json @@ -0,0 +1,24 @@ +[ + { + "code": "CIC0097", + "classCode": "TA", + "semester": "2021.2", + "dicente": [ + { + "nome": "Ana Clara Jordao Perna", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "usuario": "190084006", + "formacao": "graduando", + "ocupacao": "dicente" + } + ], + "docente": { + "nome": "MARISTELA TERTO DE HOLANDA", + "departamento": "DEPTO CIÊNCIAS DA COMPUTAÇÃO", + "formacao": "DOUTORADO", + "usuario": "83807519491", + "email": "mholanda@unb.br", + "ocupacao": "docente" + } + } +] diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb new file mode 100644 index 0000000000..79811b8f6b --- /dev/null +++ b/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 this infer these behaviours automatically by location, e.g. + # /spec/models would pull in the same behaviour as `type: :model` but this + # behaviour is considered legacy and will be removed in a future version. + # + # To enable this behaviour uncomment the line below. + # config.infer_spec_type_from_file_location! + + # Filter lines from Rails gems in backtraces. + config.filter_rails_from_backtrace! + # arbitrary gems may also be filtered via: + # config.filter_gems_from_backtrace("gem name") +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000000..327b58ea1f --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,94 @@ +# 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! + + # 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/test/fixtures/members.yml b/test/fixtures/members.yml new file mode 100644 index 0000000000..a200e9d71b --- /dev/null +++ b/test/fixtures/members.yml @@ -0,0 +1,19 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + nome: MyString + matricula: MyString + curso: MyString + usuario: MyString + formacao: MyString + ocupacao: MyString + email: MyString + +two: + nome: MyString + matricula: MyString + curso: MyString + usuario: MyString + formacao: MyString + ocupacao: MyString + email: MyString diff --git a/test/fixtures/subjects.yml b/test/fixtures/subjects.yml new file mode 100644 index 0000000000..59387ea1a2 --- /dev/null +++ b/test/fixtures/subjects.yml @@ -0,0 +1,9 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + code: MyString + name: MyString + +two: + code: MyString + name: MyString diff --git a/test/fixtures/turmas.yml b/test/fixtures/turmas.yml new file mode 100644 index 0000000000..8c4e9c3cc1 --- /dev/null +++ b/test/fixtures/turmas.yml @@ -0,0 +1,13 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + class_code: MyString + semester: MyString + time: MyString + subject: one + +two: + class_code: MyString + semester: MyString + time: MyString + subject: two diff --git a/test/models/member_test.rb b/test/models/member_test.rb new file mode 100644 index 0000000000..4fec230203 --- /dev/null +++ b/test/models/member_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class MemberTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/subject_test.rb b/test/models/subject_test.rb new file mode 100644 index 0000000000..d9e374931b --- /dev/null +++ b/test/models/subject_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class SubjectTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/turma_test.rb b/test/models/turma_test.rb new file mode 100644 index 0000000000..21806827d6 --- /dev/null +++ b/test/models/turma_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class TurmaTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end From 6432bdb62fb1d2b5b949bba7d73d0f26640d0f7c Mon Sep 17 00:00:00 2001 From: Carol <232050975@aluno.unb.br> Date: Thu, 27 Nov 2025 21:50:11 -0300 Subject: [PATCH 026/100] =?UTF-8?q?Altera=C3=A7=C3=A3o=20do=20README.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 289 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 276 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 7db80e4ca1..5e7f70bd14 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,287 @@ -# README +# 📝 Wiki do Projeto – Sprint 1: Etapa 1 -This README would normally document whatever steps are necessary to get the -application up and running. +**Grupo 1 – Engenharia de Software** +**Integrantes:** -Things you may want to cover: +| Nome | Matrícula | +| :---------- | :-------- | +| Caroline | 232050975 | +| Célio | 211010350 | +| Luís Filipe | 190091975 | +| Mário | 231035778 | -* Ruby version +# 📌 Nome do Projeto -* System dependencies +**CAMAAR – Sistema para avaliação de atividades acadêmicas remotas do CIC** -* Configuration +# 📌 Escopo do Projeto -* Database creation +O sistema **CAMAAR** tem como objetivo auxiliar na avaliação acadêmica de atividades, tarefas e outras atividades remotas do CIC. +O projeto contempla funcionalidades de cadastro de usuários, redefinição de senha, importação de base de dados do SIGAA, visualização de formulários, criação de formulários, criação de templates para formulários e download de resultados dos relatórios. -* Database initialization +# 🔰 Papéis na Sprint 1 -* How to run the test suite +● Especificar os cenários BDD das histórias de usuário usando o +Cucumber. +Responsáveis: Caroline, Célio, Luís Filipe e Mário. -* Services (job queues, cache servers, search engines, etc.) +● Abrir uma Pull Request com as especificações dos testes de aceitação +(BDD) no repositório principal. +Responsável: Mário -* Deployment instructions +● Entregar arquivo .txt contendo um link para o repositório, o nome e a +matrícula dos integrantes. +Responsável: Mário -* ... +● Criar um arquivo Markdown como Wiki, contendo +as informações sobre a Sprint 1. +Responsáveis: Caroline, Célio, Luís Filipe e Mário. + + +## 🧑‍💼 Scrum Master + +Caroline, Célio, Luís Filipe e Mário. + +## 🧑‍💻 Product Owner + +Caroline, Célio, Luís Filipe e Mário. + +# Quais funcionalidades serão desenvolvidas? + +Nesta Sprint 1: 1° Etapa os integrantes irão Especificar os cenários BDD de acordo com as histórias de usuários, cada cenário BDD deve possuir pelo menos um cenário feliz e um triste. +As US desta sprint são: +[02 - Edição e deleção de templates](https://github.com/mariosantos-05/CAMAAR-G1/issues/2) +[03 - Visualização dos templates criados](https://github.com/mariosantos-05/CAMAAR-G1/issues/3) +[04 - Importar dados do SIGAA](https://github.com/mariosantos-05/CAMAAR-G1/issues/4) +[05 - Responder formulário](https://github.com/mariosantos-05/CAMAAR-G1/issues/5) +[06 - Cadastrar usuários do sistema](https://github.com/mariosantos-05/CAMAAR-G1/issues/6) +[07 - Gerar relatório do Administrador](https://github.com/mariosantos-05/CAMAAR-G1/issues/7) +[08 - Criar template de formulário](https://github.com/mariosantos-05/CAMAAR-G1/issues/8) +[09 - Criar formulários de avaliação](https://github.com/mariosantos-05/CAMAAR-G1/issues/9) +[10 - Sistema de login](https://github.com/mariosantos-05/CAMAAR-G1/issues/10) +[11 - Sistema de definição de senha](https://github.com/mariosantos-05/CAMAAR-G1/issues/11) +[12 - Sistema de gerenciamento por departamento](https://github.com/mariosantos-05/CAMAAR-G1/issues/12) +[13 - Redefinição de senha](https://github.com/mariosantos-05/CAMAAR-G1/issues/13) +[14 - Atualizar base de dados com os dados do SIGAA](https://github.com/mariosantos-05/CAMAAR-G1/issues/14) +[15 - Visualização de formlários para responder](https://github.com/mariosantos-05/CAMAAR-G1/issues/15) +[16 - Visualização de Resultado dos Formulários](https://github.com/mariosantos-05/CAMAAR-G1/issues/16) +[17 - Criação de formulário para docentes ou discentes](https://github.com/mariosantos-05/CAMAAR-G1/issues/17) + +# Quais serão as regras de negócio para cada funcionalidade? +## Regras de Negócio - Redefinição de Senha +(Issue 13: "Quero redefinir uma senha... a partir do e-mail recebido...") + +| Código | Descrição | +|------------|---------------------------------------------------------------------------------------------------------------------| +| RN-RS-01 | O usuário deve informar um e-mail cadastrado para solicitar a redefinição de senha. | +| RN-RS-02 | Caso o e-mail **não exista** no sistema, exibir mensagem de sucesso genérica ("Se este e-mail estiver cadastrado...") para evitar enumeração de usuários. | +| RN-RS-03 | Se o e-mail existir, gerar um **token único** de redefinição e enviá-lo por e-mail ao usuário. | +| RN-RS-04 | O token de redefinição deve expirar em **60 minutos**. | +| RN-RS-05 | O token é de **uso único** – após a redefinição da senha, torna-se inválido. | +| RN-RS-06 | A nova senha deve obedecer às regras de complexidade do sistema (ver RN-DS-02). | + +## Regras de Negócio - Definição de Senha (Primeiro Acesso) +(Issue 11: "Quero definir uma senha... a partir do e-mail do sistema de solicitação de cadastro...") + +| Código | Descrição | +|------------|---------------------------------------------------------------------------------------------------------------------| +| RN-DS-01 | O link de definição de senha é de **uso único**. | +| RN-DS-02 | A senha deve ter no mínimo **8 caracteres**, contendo letras maiúsculas, minúsculas e números. | +| RN-DS-03 | Os campos **"Nova Senha"** e **"Confirmar Senha"** devem ser idênticos; caso contrário, exibir erro. | +| RN-DS-04 | A conta só muda de status **"Pendente" → "Ativo"** após a definição bem-sucedida da senha (cadastro efetivado). | + +## Regras de Negócio - Cadastro de Usuários via Importação +(Issue 06: "Quero cadastrar participantes... ao importar dados de usuarios novos...") + +| Código | Descrição | +|------------|---------------------------------------------------------------------------------------------------------------------| +| RN-C-01 | A funcionalidade de importação só está acessível para usuários com perfil **"Admin"**. | +| RN-C-02 | Aceitar **apenas** arquivos no formato **.json**. Qualquer outro formato (ex: .pdf, .csv) → "Formato de arquivo inválido". | +| RN-C-03 | O arquivo deve ser um **JSON válido** (sintaxe correta). Erros de sintaxe → "O arquivo não é um JSON válido". | +| RN-C-04 | Cada objeto de usuário deve conter **obrigatoriamente** as chaves `"matricula"` e `"email"`. Falta de qualquer uma → rejeitar importação. | +| RN-C-05 | As chaves devem ter tipos corretos (ex: `"matricula"` deve ser número/string numérica válida). | +| RN-C-06 | Se a matrícula **não existir** no banco, criar novo usuário com status **"Pendente"**. | +| RN-C-07 | **Não** disparar automaticamente o e-mail de definição de senha ao criar usuário "Pendente" via importação. | +| RN-C-08 | Se a matrícula já existir, **não criar duplicata**. | +| RN-C-09 | Se a matrícula já existir, **atualizar** os dados do usuário (ex: atualizar e-mail se diferente no JSON). | + +## Regras de Negócio - Sistema de Login +(Issue 10: "Quero acessar o sistema utilizando um e-mail ou matrícula...") + +| Código | Descrição | +|------------|---------------------------------------------------------------------------------------------------------------------| +| RN-L-01 | O usuário deve poder se autenticar usando **e-mail** ou **número de matrícula** no mesmo campo de login. | +| RN-L-02 | Os campos **"E-mail ou Matrícula"** e **"Senha"** são de preenchimento obrigatório. | +| RN-L-03 | Em caso de e-mail/matrícula ou senha incorretos, exibir mensagem genérica **"E-mail ou senha inválidos"** (nunca informar qual dos dois está errado). | +| RN-L-04 | Usuários com perfil **"Admin"** devem ter a opção **"Gerenciamento"** exibida no menu lateral. | +| RN-L-05 | Usuários com perfil diferente de "Admin" (ex: Aluno, Professor) **não devem** ver a opção "Gerenciamento". | +| RN-L-06 | O login só é permitido se o status da conta do usuário for **"Ativo"** (ou seja, após a primeira definição de senha). | + +## Regras de Negócio - Criar Formulário (Template de Questões) +(Issue 09: "Quero criar um template de formulário contendo as questões do formulário...") + +| Código | Descrição | +|------------|----------------------------------------------------------------------------------------------------| +| RN-CF-01 | **Confirmação de Exclusão**: A ação de deletar um template exige confirmação explícita (pop-up) antes de ser executada. | +| RN-CF-02 | **Tipos de Questões**: O sistema deve permitir incluir e persistir diferentes tipos de perguntas (múltipla escolha, discursiva, etc.) no mesmo template. | + +## Regras de Negócio - Criar Template de Formulário +(Issue 17: "Quero escolher criar um formulário para os docentes ou os discentes...") + +| Código | Descrição | +|-----------|----------------------------------------------------------------------------------------------------| +| RN-CTF-01 | **Obrigatoriedade de Título**: Não é permitido criar ou salvar um template com o campo "Nome/Título" vazio. | +| RN-CTF-02 | **Imutabilidade Histórica**: A edição de um template **não pode** alterar a estrutura ou os dados de formulários já respondidos (instâncias antigas permanecem inalteradas). | + +## Regras de Negócio - Visualizar Templates +(Issue 03: "Quero visualizar os templates criados") + +| Código | Descrição | +|-----------|----------------------------------------------------------------------------------------------------| +| RN-VT-01 | **Condicionalidade de Campos**: O campo "Turma" deve ser **obrigatório** quando o público-alvo for "Discentes" e **oculto** quando for "Docentes". | +| RN-VT-02 | **Segmentação de Envio**: O formulário gerado deve ser enviado **apenas** para os usuários vinculados à turma selecionada. | + +## Regras de Negócio - Editar e Deletar Template +(Issue 02: "Quero editar e/ou deletar um template que eu criei sem afetar...") + +| Código | Descrição | +|-----------|----------------------------------------------------------------------------------------------------| +| RN-ET-01 | **Estado de Lista Vazia**: Quando não houver templates cadastrados, exibir a mensagem "Nenhum template foi criado" em vez de uma lista em branco. | +| RN-ET-02 | **Ações de Gerenciamento**: Cada item da lista deve exibir botões individuais de **"Editar"** e **"Deletar"**. | + +## Regras de Negócio - Importação de Dados do SIGAA (Apenas Adicionar) +(Issue 04: Importar dados do SIGAA) +(Quero importar dados de turmas, matérias e participantes do SIGAA caso não existam na base de dados atual) + +| Código | Descrição | +|-----------|---------------------------------------------------------------------------------------------------------------------| +| RN-IDS-13 | Para cada item do JSON, verificar a chave única (ex: matrícula do aluno ou código da disciplina):
• Se **não existir** → criar o registro.
• Se **já existir** → ignorar o item (não atualizar nem duplicar). | +| RN-IDS-14 | Esta é uma operação de **"apenas adicionar"**, usada para alimentar a base sem risco de sobrescrever dados já alterados manualmente. | +| RN-IDS-15 | Aceitar somente arquivos com extensão **.json**. Qualquer outro formato deve ser rejeitado com mensagem de erro. | +| RN-IDS-16 | O arquivo .json deve ser sintaticamente válido. Erro de sintaxe → rejeição imediata com mensagem de erro clara. | + +## Regras de Negócio - Gerenciamento de Relatórios e Resultados +(Issue 07: Gerar relatório do administrador – Quero baixar um arquivo CSV contendo os resultados de um formulário) + +| Código | Descrição | +|-----------|---------------------------------------------------------------------------------------------------------------------| +| RN-GR-01 | O acesso à página **"Gerenciamento → Resultados"** e todas as suas funcionalidades é restrito exclusivamente a usuários com papel **"Administrador"**. | +| RN-GR-02 | Usuários sem perfil Administrador **não devem ver** o link da página. Caso tentem acessar diretamente a URL, devem ser bloqueados e redirecionados ao seu dashboard. | +| RN-GR-03 | Ao solicitar o download dos resultados, o sistema deve gerar e oferecer um arquivo no formato **CSV**. | + +## Regras de Negócio - Atualizar Dados Existentes (via SIGAA) +(Issue 14: Quero atualizar a base de dados já existente com os dados atuais do SIGAA) + +| Código | Descrição | +|-----------|---------------------------------------------------------------------------------------------------------------------| +| RN-ADE-01 | Se o item (aluno, turma, etc.) do JSON **não existir** no banco, o sistema deve criá-lo. | +| RN-ADE-02 | Se o item do JSON **já existir** no banco, o sistema deve atualizar o registro existente com os dados do JSON. | +| RN-ADE-03 | O sistema **nunca** deve criar duplicatas – a ação é sempre de correção/atualização do registro existente. | +| RN-ADE-04 | Aceitar apenas arquivos com extensão **.json**. Qualquer outro formato deve ser rejeitado. | +| RN-ADE-05 | O arquivo .json deve ser sintaticamente válido. Caso contrário, a importação deve ser rejeitada. | +| RN-ADE-06 | O JSON deve conter todas as chaves obrigatórias esperadas. Se algum item estiver sem chave obrigatória (ex: matrícula), a importação deve falhar. | +| RN-ADE-07 | Após importação bem-sucedida, exibir mensagem de sucesso clara ao administrador. | +| RN-ADE-08 | Após falha na importação (qualquer motivo), exibir mensagem de erro detalhando o problema. | + +## Regras de Negócio - Gerenciamento de Turmas por Departamento +(Issue 12: Quero gerenciar somente as turmas do departamento o qual eu pertenço) + +| Código | Descrição | +|-----------|---------------------------------------------------------------------------------------------------------------------| +| RN-GTD-01 | Se um usuário (administrador de departamento ou não) tentar acessar diretamente via URL os dados de turmas de outro departamento, o sistema deve redirecioná-lo imediatamente para sua página principal (Dashboard). | + +## Regras de Negócio - Criar Formulário de Avaliação +(Issue 09: "Criar um formulário de avaliação baseado em um template para turmas selecionadas") + +| Código | Descrição | +|--------|-----------| +| RN-CFA-01 | **Seleção Obrigatória de Turmas**: Não é permitido criar formulários se nenhuma turma for selecionada; o sistema deve exibir a mensagem de erro "Nenhuma turma selecionada." | +| RN-CFA-02 | **Seleção Obrigatória de Template**: O sistema deve impedir a criação de formulários caso nenhum template seja selecionado, exibindo a mensagem "Nenhum template selecionado." | +| RN-CFA-03 | **Criação em Lote por Turma**: O sistema deve gerar individualmente um formulário para cada turma selecionada quando solicitado. | +| RN-CFA-04 | **Confirmação de Sucesso**: Após criar os formulários, o sistema deve exibir a mensagem "Formulários criados com sucesso para as turmas selecionadas." | + +## Regras de Negócio - Responder Formulário +(Issue 05: "Responder o formulário de avaliação como Participante") + +| Código | Descrição | +|--------|-----------| +| RN-RF-01 | **Disponibilidade Vinculada à Turma**: O participante só pode responder formulários das turmas em que está matriculado. | +| RN-RF-02 | **Existência de Formulário Ativo**: O sistema só permite acesso e envio se houver um formulário ativo disponível para a turma. | +| RN-RF-03 | **Validação de Campos Obrigatórios**: O sistema deve impedir o envio caso campos obrigatórios não sejam preenchidos, exibindo erro. | +| RN-RF-04 | **Registro de Respostas**: O sistema deve registrar todas as respostas submetidas pelo participante. | +| RN-RF-05 | **Confirmação de Envio**: Após envio bem-sucedido, o sistema deve exibir "Seu formulário foi enviado com sucesso." | + +## Regras de Negócio - Visualização de Formulários Pendentes +(Issue 15: "Visualizar os formulários não respondidos das turmas em que o participante está matriculado") + +| Código | Descrição | +|--------|-----------| +| RN-VFP-01 | **Exibição Apenas do que Não foi Respondido**: O sistema deve listar somente formulários pendentes. | +| RN-VFP-01 | **Segmentação por Turma Matriculada**: O participante só visualiza formulários das turmas nas quais está matriculado. | +| RN-VFP-01 | **Contador de Formulários Pendentes**: O sistema deve exibir o total de formulários pendentes com mensagem informativa. | +| RN-VFP-01 | **Estado de Lista Vazia**: Na ausência de formulários pendentes, exibir "Você não possui formulários pendentes." | + +## Regras de Negócio - Visualizar Resultados dos Formulários +(Issue 16: "Visualização dos formulários criados pelo Administrador") + +| Código | Descrição | +|--------|-----------| +| RN-VRF-01 | **Lista de Formulários Criados**: O administrador deve visualizar os formulários criados organizados por turma. | +| RN-VRF-02 | **Contador de Formulários Criados**: O sistema deve exibir o total de formulários criados, como "Existem X formulários criados." | +| RN-VRF-03 | **Acesso aos Detalhes**: O administrador deve poder visualizar perguntas, respostas e estatísticas de cada formulário selecionado. | + +## Quem ficou responsável por cada cenário BDD em relação as US/Issues? + +#[02](https://github.com/mariosantos-05/CAMAAR-G1/issues/2) Luís Filipe +#[03](https://github.com/mariosantos-05/CAMAAR-G1/issues/3) Luís Filipe +#[04](https://github.com/mariosantos-05/CAMAAR-G1/issues/4) Caroline +#[05](https://github.com/mariosantos-05/CAMAAR-G1/issues/5) Mário +#[06](https://github.com/mariosantos-05/CAMAAR-G1/issues/6) Célio +#[07](https://github.com/mariosantos-05/CAMAAR-G1/issues/7) Caroline +#[08](https://github.com/mariosantos-05/CAMAAR-G1/issues/8) Luís Filipe +#[09](https://github.com/mariosantos-05/CAMAAR-G1/issues/9) Mário +#[10](https://github.com/mariosantos-05/CAMAAR-G1/issues/10) Célio +#[11](https://github.com/mariosantos-05/CAMAAR-G1/issues/11) Célio +#[12](https://github.com/mariosantos-05/CAMAAR-G1/issues/12) (Caroline ou Luís Filipe) +#[13](https://github.com/mariosantos-05/CAMAAR-G1/issues/13) Célio +#[14](https://github.com/mariosantos-05/CAMAAR-G1/issues/14) Caroline +#[15](https://github.com/mariosantos-05/CAMAAR-G1/issues/15) Mário +#[16](https://github.com/mariosantos-05/CAMAAR-G1/issues/16) Mário +#[17](https://github.com/mariosantos-05/CAMAAR-G1/issues/17) (Caroline ou Luís Filipe) + +--- + +# 📊 Métrica Velocity da Sprint 1 + +| História/Issue | Pontos | +| ---------------- | ------------------- | +| US / #02 | 2 | +| US / #03 | 1 | +| US / #04 | 3 | +| US / #05 | 2 | +| US / #06 | 3 | +| US / #07 | 2 | +| US / #08 | 3 | +| US / #09 | 3 | +| US / #10 | 2 | +| US / #11 | 2 | +| US / #12 | 3 | +| US / #13 | 3 | +| US / #14 | 3 | +| US / #15 | 2 | +| US / #16 | 3 | +| US / #17 | 3 | +| **Total** | **Story Points** | +| **16 US/Issues** | **40 Story Points** | + +--- + +# 🌿 Política de Branching Utilizada pelo Grupo + +Sprint Branching + Feature Branching (variação do GitLab Flow): + +- A equipe cria uma branch representando a sprint a partir da main. + +- Todas as feature branches da sprint nascem a partir dela. + +- No final da sprint, tudo é consolidado e mergeado para a branch da sprint. \ No newline at end of file From 28c964e1c06ed6f2799a9dc10e7f83becf08f06d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9lio=20Eduardo?= <59587358+celio-eduardo@users.noreply.github.com> Date: Fri, 28 Nov 2025 00:33:11 -0300 Subject: [PATCH 027/100] Rename criar_formulario.feature to criar_formulario.feature --- .../criar_formulario.feature | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename features/{ template_formularios => template_formularios}/criar_formulario.feature (100%) diff --git a/features/ template_formularios /criar_formulario.feature b/features/template_formularios/criar_formulario.feature similarity index 100% rename from features/ template_formularios /criar_formulario.feature rename to features/template_formularios/criar_formulario.feature From a6384b6a2101df46c19c1d5fc48a890761075994 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9lio=20Eduardo?= <59587358+celio-eduardo@users.noreply.github.com> Date: Fri, 28 Nov 2025 00:34:12 -0300 Subject: [PATCH 028/100] Rename criar_template.feature to criar_template.feature --- .../criar_template.feature | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename features/{ template_formularios => template_formularios}/criar_template.feature (100%) diff --git a/features/ template_formularios /criar_template.feature b/features/template_formularios/criar_template.feature similarity index 100% rename from features/ template_formularios /criar_template.feature rename to features/template_formularios/criar_template.feature From 7810b6c63ece712cfb269963c12d206ef80099cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9lio=20Eduardo?= <59587358+celio-eduardo@users.noreply.github.com> Date: Fri, 28 Nov 2025 00:34:57 -0300 Subject: [PATCH 029/100] Rename editar_template.feature to editar_template.feature --- .../editar_template.feature | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename features/{ template_formularios => template_formularios}/editar_template.feature (100%) diff --git a/features/ template_formularios /editar_template.feature b/features/template_formularios/editar_template.feature similarity index 100% rename from features/ template_formularios /editar_template.feature rename to features/template_formularios/editar_template.feature From e5522eec6d03eebc4c417f0ad00a664e76789249 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9lio=20Eduardo?= <59587358+celio-eduardo@users.noreply.github.com> Date: Fri, 28 Nov 2025 00:35:26 -0300 Subject: [PATCH 030/100] Rename visualizar_template.feature to visualizar_template.feature --- .../visualizar_template.feature | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename features/{ template_formularios => template_formularios}/visualizar_template.feature (100%) diff --git a/features/ template_formularios /visualizar_template.feature b/features/template_formularios/visualizar_template.feature similarity index 100% rename from features/ template_formularios /visualizar_template.feature rename to features/template_formularios/visualizar_template.feature From d5fc342d97caf69e35fc936a5ed5dbfd73f392a3 Mon Sep 17 00:00:00 2001 From: Carol <232050975@aluno.unb.br> Date: Fri, 28 Nov 2025 10:53:14 -0300 Subject: [PATCH 031/100] =?UTF-8?q?Altera=C3=A7=C3=A3o=20do=20Banco=20de?= =?UTF-8?q?=20Dados=20para=20ficar=20igual=20=C3=A0=20especifica=C3=A7?= =?UTF-8?q?=C3=A3o=20do=20PDF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/form.rb | 4 + app/models/respostum.rb | 4 + app/models/template.rb | 3 + app/models/turma.rb | 4 +- app/models/usuario.rb | 2 + app/models/vinculo.rb | 4 + app/services/sigaa_import_service.rb | 70 +++++++--------- ...184821_create_join_table_turmas_members.rb | 8 -- db/migrate/20251127213250_create_subjects.rb | 10 --- db/migrate/20251127213308_create_members.rb | 15 ---- db/migrate/20251128134201_create_usuarios.rb | 14 ++++ ...mas.rb => 20251128134211_create_turmas.rb} | 7 +- db/migrate/20251128134223_create_vinculos.rb | 11 +++ db/migrate/20251128134232_create_templates.rb | 11 +++ db/migrate/20251128134241_create_forms.rb | 11 +++ db/migrate/20251128134249_create_resposta.rb | 10 +++ db/schema.rb | 71 +++++++++++----- features/admin/importar_dados.feature | 1 + features/step_definitions/import_steps.rb | 84 +++++++++---------- spec/models/form_spec.rb | 5 ++ spec/models/respostum_spec.rb | 5 ++ spec/models/template_spec.rb | 5 ++ spec/models/turma_spec.rb | 5 ++ spec/models/usuario_spec.rb | 5 ++ spec/models/vinculo_spec.rb | 5 ++ 25 files changed, 230 insertions(+), 144 deletions(-) create mode 100644 app/models/form.rb create mode 100644 app/models/respostum.rb create mode 100644 app/models/template.rb create mode 100644 app/models/usuario.rb create mode 100644 app/models/vinculo.rb delete mode 100644 db/migrate/20251127184821_create_join_table_turmas_members.rb delete mode 100644 db/migrate/20251127213250_create_subjects.rb delete mode 100644 db/migrate/20251127213308_create_members.rb create mode 100644 db/migrate/20251128134201_create_usuarios.rb rename db/migrate/{20251127213521_create_turmas.rb => 20251128134211_create_turmas.rb} (50%) create mode 100644 db/migrate/20251128134223_create_vinculos.rb create mode 100644 db/migrate/20251128134232_create_templates.rb create mode 100644 db/migrate/20251128134241_create_forms.rb create mode 100644 db/migrate/20251128134249_create_resposta.rb create mode 100644 spec/models/form_spec.rb create mode 100644 spec/models/respostum_spec.rb create mode 100644 spec/models/template_spec.rb create mode 100644 spec/models/turma_spec.rb create mode 100644 spec/models/usuario_spec.rb create mode 100644 spec/models/vinculo_spec.rb diff --git a/app/models/form.rb b/app/models/form.rb new file mode 100644 index 0000000000..f605754de6 --- /dev/null +++ b/app/models/form.rb @@ -0,0 +1,4 @@ +class Form < ApplicationRecord + belongs_to :template + belongs_to :turma +end diff --git a/app/models/respostum.rb b/app/models/respostum.rb new file mode 100644 index 0000000000..d80c57eb33 --- /dev/null +++ b/app/models/respostum.rb @@ -0,0 +1,4 @@ +class Respostum < ApplicationRecord + belongs_to :form + belongs_to :usuario +end diff --git a/app/models/template.rb b/app/models/template.rb new file mode 100644 index 0000000000..9db120bdbf --- /dev/null +++ b/app/models/template.rb @@ -0,0 +1,3 @@ +class Template < ApplicationRecord + belongs_to :criado_por +end diff --git a/app/models/turma.rb b/app/models/turma.rb index 9dad348497..41fd637f68 100644 --- a/app/models/turma.rb +++ b/app/models/turma.rb @@ -1,4 +1,2 @@ class Turma < ApplicationRecord - belongs_to :subject - has_and_belongs_to_many :members -end \ No newline at end of file +end diff --git a/app/models/usuario.rb b/app/models/usuario.rb new file mode 100644 index 0000000000..c21d504781 --- /dev/null +++ b/app/models/usuario.rb @@ -0,0 +1,2 @@ +class Usuario < ApplicationRecord +end diff --git a/app/models/vinculo.rb b/app/models/vinculo.rb new file mode 100644 index 0000000000..21e563a333 --- /dev/null +++ b/app/models/vinculo.rb @@ -0,0 +1,4 @@ +class Vinculo < ApplicationRecord + belongs_to :usuario + belongs_to :turma +end diff --git a/app/services/sigaa_import_service.rb b/app/services/sigaa_import_service.rb index f05afa48b2..ecd9243945 100644 --- a/app/services/sigaa_import_service.rb +++ b/app/services/sigaa_import_service.rb @@ -4,12 +4,11 @@ class SigaaImportService class InvalidFileError < StandardError; end REQUIRED_SUBJECT_KEYS = %w[code name class] - REQUIRED_CLASS_KEYS = %w[classCode semester time] REQUIRED_MEMBER_FILE_KEYS = %w[code classCode semester dicente] - REQUIRED_STUDENT_KEYS = %w[nome matricula curso usuario email ocupacao formacao] + REQUIRED_STUDENT_KEYS = %w[nome matricula curso email ocupacao formacao] def initialize(file_path) @file_path = file_path @@ -35,7 +34,7 @@ def call elsif entry.key?('dicente') process_members_file(entry, index) else - raise InvalidFileError, "O objeto na linha #{index + 1} não foi reconhecido (não possui chave 'class' nem 'dicente')." + raise InvalidFileError, "Objeto não reconhecido na linha #{index + 1}." end end end @@ -54,56 +53,49 @@ def validate_keys!(data, required_keys, context_message) def process_classes_file(entry, index) validate_keys!(entry, REQUIRED_SUBJECT_KEYS, "na Matéria (item #{index + 1})") + validate_keys!(entry['class'], REQUIRED_CLASS_KEYS, "na Turma") - validate_keys!(entry['class'], REQUIRED_CLASS_KEYS, "na Turma da matéria '#{entry['code']}'") + nome_completo = "#{entry['name']} (#{entry['code']} - #{entry['class']['classCode']})" + semestre = entry['class']['semester'] - subject = Subject.find_or_create_by(code: entry['code']) do |s| - s.name = entry['name'] - end - subject.update(name: entry['name']) - - class_info = entry['class'] - Turma.find_or_create_by( - class_code: class_info['classCode'], - semester: class_info['semester'], - subject: subject - ) do |t| - t.time = class_info['time'] + turma = Turma.find_or_create_by(nome: nome_completo, semestre: semestre) do |t| + t.is_active = true end + + turma.update(nome: nome_completo) end def process_members_file(entry, index) - validate_keys!(entry, REQUIRED_MEMBER_FILE_KEYS, "no cabeçalho da Turma (item #{index + 1})") + validate_keys!(entry, REQUIRED_MEMBER_FILE_KEYS, "no cabeçalho da Turma") if entry['dicente'] - entry['dicente'].each_with_index do |student_data, student_index| - validate_keys!(student_data, REQUIRED_STUDENT_KEYS, "no Aluno ##{student_index + 1} da turma '#{entry['code']}'") + entry['dicente'].each_with_index do |student_data, s_index| + validate_keys!(student_data, REQUIRED_STUDENT_KEYS, "no Aluno ##{s_index + 1}") end end - subject = Subject.find_by(code: entry['code']) - unless subject - return + turma = Turma.where(semestre: entry['semester']).find do |t| + t.nome.include?(entry['code']) && t.nome.include?(entry['classCode']) + end + + unless turma + return end - turma = Turma.find_by( - subject: subject, - class_code: entry['classCode'], - semester: entry['semester'] - ) - return unless turma - - entry['dicente'].each do |student_data| - member = Member.find_or_create_by(matricula: student_data['matricula']) do |m| - m.nome = student_data['nome'] - m.curso = student_data['curso'] - m.usuario = student_data['usuario'] - m.email = student_data['email'] - m.ocupacao = student_data['ocupacao'] - m.formacao = student_data['formacao'] + if entry['dicente'] + entry['dicente'].each do |student_data| + usuario = Usuario.find_or_create_by(matricula: student_data['matricula']) do |u| + u.nome = student_data['nome'] + u.email = student_data['email'] + u.profile = 'Aluno' + u.status = true + u.departamento_id = 1 + end + + Vinculo.find_or_create_by(usuario: usuario, turma: turma) do |v| + v.papel_turma = 0 + end end - - turma.members << member unless turma.members.exists?(member.id) end end end \ No newline at end of file diff --git a/db/migrate/20251127184821_create_join_table_turmas_members.rb b/db/migrate/20251127184821_create_join_table_turmas_members.rb deleted file mode 100644 index 03bf649580..0000000000 --- a/db/migrate/20251127184821_create_join_table_turmas_members.rb +++ /dev/null @@ -1,8 +0,0 @@ -class CreateJoinTableTurmasMembers < ActiveRecord::Migration[8.0] - def change - create_join_table :turmas, :members do |t| - # t.index [:turma_id, :member_id] - # t.index [:member_id, :turma_id] - end - end -end diff --git a/db/migrate/20251127213250_create_subjects.rb b/db/migrate/20251127213250_create_subjects.rb deleted file mode 100644 index 3c584232cd..0000000000 --- a/db/migrate/20251127213250_create_subjects.rb +++ /dev/null @@ -1,10 +0,0 @@ -class CreateSubjects < ActiveRecord::Migration[8.0] - def change - create_table :subjects do |t| - t.string :code - t.string :name - - t.timestamps - end - end -end diff --git a/db/migrate/20251127213308_create_members.rb b/db/migrate/20251127213308_create_members.rb deleted file mode 100644 index dfd4fed068..0000000000 --- a/db/migrate/20251127213308_create_members.rb +++ /dev/null @@ -1,15 +0,0 @@ -class CreateMembers < ActiveRecord::Migration[8.0] - def change - create_table :members do |t| - t.string :nome - t.string :matricula - t.string :curso - t.string :usuario - t.string :formacao - t.string :ocupacao - t.string :email - - t.timestamps - end - end -end diff --git a/db/migrate/20251128134201_create_usuarios.rb b/db/migrate/20251128134201_create_usuarios.rb new file mode 100644 index 0000000000..b0736b146c --- /dev/null +++ b/db/migrate/20251128134201_create_usuarios.rb @@ -0,0 +1,14 @@ +class CreateUsuarios < ActiveRecord::Migration[8.0] + def change + create_table :usuarios do |t| + t.string :nome + t.string :matricula + t.string :email + t.boolean :status + t.string :profile + t.integer :departamento_id + + t.timestamps + end + end +end diff --git a/db/migrate/20251127213521_create_turmas.rb b/db/migrate/20251128134211_create_turmas.rb similarity index 50% rename from db/migrate/20251127213521_create_turmas.rb rename to db/migrate/20251128134211_create_turmas.rb index dc1179521a..0b9d3d9ecd 100644 --- a/db/migrate/20251127213521_create_turmas.rb +++ b/db/migrate/20251128134211_create_turmas.rb @@ -1,10 +1,9 @@ class CreateTurmas < ActiveRecord::Migration[8.0] def change create_table :turmas do |t| - t.string :class_code - t.string :semester - t.string :time - t.references :subject, null: false, foreign_key: true + t.string :nome + t.string :semestre + t.boolean :is_active t.timestamps end diff --git a/db/migrate/20251128134223_create_vinculos.rb b/db/migrate/20251128134223_create_vinculos.rb new file mode 100644 index 0000000000..226643760b --- /dev/null +++ b/db/migrate/20251128134223_create_vinculos.rb @@ -0,0 +1,11 @@ +class CreateVinculos < ActiveRecord::Migration[8.0] + def change + create_table :vinculos do |t| + t.references :usuario, null: false, foreign_key: true + t.references :turma, null: false, foreign_key: true + t.integer :papel_turma + + t.timestamps + end + end +end diff --git a/db/migrate/20251128134232_create_templates.rb b/db/migrate/20251128134232_create_templates.rb new file mode 100644 index 0000000000..2bb8714377 --- /dev/null +++ b/db/migrate/20251128134232_create_templates.rb @@ -0,0 +1,11 @@ +class CreateTemplates < ActiveRecord::Migration[8.0] + def change + create_table :templates do |t| + t.string :titulo + t.string :target_audience + t.references :criado_por, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/db/migrate/20251128134241_create_forms.rb b/db/migrate/20251128134241_create_forms.rb new file mode 100644 index 0000000000..1019d1c504 --- /dev/null +++ b/db/migrate/20251128134241_create_forms.rb @@ -0,0 +1,11 @@ +class CreateForms < ActiveRecord::Migration[8.0] + def change + create_table :forms do |t| + t.references :template, null: false, foreign_key: true + t.references :turma, null: false, foreign_key: true + t.boolean :is_active + + t.timestamps + end + end +end diff --git a/db/migrate/20251128134249_create_resposta.rb b/db/migrate/20251128134249_create_resposta.rb new file mode 100644 index 0000000000..dbc4cf675f --- /dev/null +++ b/db/migrate/20251128134249_create_resposta.rb @@ -0,0 +1,10 @@ +class CreateResposta < ActiveRecord::Migration[8.0] + def change + create_table :resposta do |t| + t.references :form, null: false, foreign_key: true + t.references :usuario, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index f500d7df6a..bce5668f83 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,40 +10,69 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_11_27_213521) do - create_table "members", force: :cascade do |t| - t.string "nome" - t.string "matricula" - t.string "curso" - t.string "usuario" - t.string "formacao" - t.string "ocupacao" - t.string "email" +ActiveRecord::Schema[8.0].define(version: 2025_11_28_134249) do + create_table "forms", force: :cascade do |t| + t.integer "template_id", null: false + t.integer "turma_id", null: false + t.boolean "is_active" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.index ["template_id"], name: "index_forms_on_template_id" + t.index ["turma_id"], name: "index_forms_on_turma_id" end - create_table "members_turmas", id: false, force: :cascade do |t| - t.integer "turma_id", null: false - t.integer "member_id", null: false + create_table "resposta", force: :cascade do |t| + t.integer "form_id", null: false + t.integer "usuario_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["form_id"], name: "index_resposta_on_form_id" + t.index ["usuario_id"], name: "index_resposta_on_usuario_id" end - create_table "subjects", force: :cascade do |t| - t.string "code" - t.string "name" + create_table "templates", force: :cascade do |t| + t.string "titulo" + t.string "target_audience" + t.integer "criado_por_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.index ["criado_por_id"], name: "index_templates_on_criado_por_id" end create_table "turmas", force: :cascade do |t| - t.string "class_code" - t.string "semester" - t.string "time" - t.integer "subject_id", null: false + t.string "nome" + t.string "semestre" + t.boolean "is_active" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "usuarios", force: :cascade do |t| + t.string "nome" + t.string "matricula" + t.string "email" + t.boolean "status" + t.string "profile" + t.integer "departamento_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "vinculos", force: :cascade do |t| + t.integer "usuario_id", null: false + t.integer "turma_id", null: false + t.integer "papel_turma" t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.index ["subject_id"], name: "index_turmas_on_subject_id" + t.index ["turma_id"], name: "index_vinculos_on_turma_id" + t.index ["usuario_id"], name: "index_vinculos_on_usuario_id" end - add_foreign_key "turmas", "subjects" + add_foreign_key "forms", "templates" + add_foreign_key "forms", "turmas" + add_foreign_key "resposta", "forms" + add_foreign_key "resposta", "usuarios" + add_foreign_key "templates", "criado_pors" + add_foreign_key "vinculos", "turmas" + add_foreign_key "vinculos", "usuarios" end diff --git a/features/admin/importar_dados.feature b/features/admin/importar_dados.feature index 4ab54d8e3b..f580e4d5c8 100644 --- a/features/admin/importar_dados.feature +++ b/features/admin/importar_dados.feature @@ -1,4 +1,5 @@ # language: pt + Funcionalidade: Alimentação inicial da base de dados com JSON do SIGAA Como um administrador Eu quero importar arquivos JSON (Turmas e Alunos) diff --git a/features/step_definitions/import_steps.rb b/features/step_definitions/import_steps.rb index 8b60d2b9ed..d2ea482337 100644 --- a/features/step_definitions/import_steps.rb +++ b/features/step_definitions/import_steps.rb @@ -1,9 +1,15 @@ Dado('que eu estou logado como {string}') do |email| - @current_user = Member.create!(email: email, nome: "Admin", ocupacao: "docente", matricula: "000000") + @current_user = Usuario.create!( + email: email, + nome: "Administrador", + matricula: "000000", + profile: "Admin", + status: true + ) end Dado('que eu estou na página {string}') do |pagina| - if pagina == "Importar Dados do SIGAA" || pagina.include?("Importar") + if pagina.include?("Importar") visit '/importar_sigaa' else visit '/gerenciamento' @@ -11,26 +17,18 @@ end Dado('o banco de dados não contém nenhuma disciplina ou aluno') do - Member.destroy_all + Vinculo.destroy_all + Usuario.destroy_all Turma.destroy_all - Subject.destroy_all -end - -Dado('já existe uma disciplina {string} \({string}\) no sistema') do |nome, codigo| - Subject.create!(name: nome, code: codigo) -end - -Dado('eu tenho um arquivo {string} {string}') do |nome_arquivo, _descricao| - @nome_arquivo = nome_arquivo end Dado('eu tenho um arquivo {string}') do |nome_arquivo| @nome_arquivo = nome_arquivo end -Quando('eu seleciono o arquivo {string} para {string}') do |nome_arquivo, _campo| - path = Rails.root.join('spec/fixtures/files', nome_arquivo) - attach_file('file', path) +Dado('já existe uma disciplina {string} \({string}\) no sistema') do |nome, codigo| + nome_completo = "#{nome} (#{codigo} - TA)" + Turma.create!(nome: nome_completo, semestre: "2021.2", is_active: true) end Quando('eu seleciono o arquivo {string}') do |nome_arquivo| @@ -42,43 +40,41 @@ click_button texto_botao end -Quando('importo-o ao sistema') do - click_button "Enviar Arquivo" -end - -Então('eu devo ser redirecionado para a {string}') do |pagina| - expect(current_path).to eq(admin_management_path) -end - Então('eu devo ver a mensagem {string}') do |mensagem| expect(page).to have_content(mensagem) end -Então('eu devo continuar na página {string}') do |_pagina| - expect(current_path).to eq(import_sigaa_path) -end - -Então('eu devo ver a mensagem de erro {string}') do |mensagem| - expect(page).to have_content(mensagem) +Então(/^a disciplina "([^"]*)" \(([^)]*)\) deve existir no sistema$/) do |nome, codigo| + codigo_limpo = codigo.gsub('"', '') + + turma = Turma.where("nome LIKE ?", "%#{nome}%").first + + expect(turma).to be_present, "A disciplina '#{nome}' não foi encontrada na tabela Turmas." + + expect(turma.nome).to include(codigo_limpo) end -Então('a disciplina {string} \({string}\) deve existir no sistema') do |nome, codigo| - subject = Subject.find_by(code: codigo) - expect(subject).to be_present - expect(subject.name).to eq(nome) +Então(/^o aluno "([^"]*)" \(([^)]*)\) deve existir no sistema$/) do |nome, matricula| + matricula_limpa = matricula.to_s.gsub('"', '') + + usuario = Usuario.find_by(matricula: matricula_limpa) + + expect(usuario).to be_present, "O aluno '#{nome}' (Matrícula: #{matricula_limpa}) não foi encontrado." + expect(usuario.nome).to eq(nome) + expect(usuario.profile).to eq('Aluno') end -Então('o aluno {string} \({int}\) deve existir no sistema') do |nome, matricula| - member = Member.find_by(matricula: matricula.to_s) - expect(member).to be_present - expect(member.nome).to eq(nome) -end - -Então('deve haver apenas {int} aluno com a matrícula {string} no sistema') do |quantidade, matricula| - expect(Member.where(matricula: matricula).count).to eq(quantidade) +Então(/^o aluno "([^"]*)" \(([^)]*)\) não deve existir no sistema$/) do |nome, matricula| + matricula_limpa = matricula.to_s.gsub('"', '') + + usuario = Usuario.find_by(matricula: matricula_limpa) + expect(usuario).to be_nil end -Então(/^o aluno "([^"]*)" \(([^)]*)\) não deve existir no sistema$/) do |nome, matricula| - member = Member.find_by(matricula: matricula.to_s) - expect(member).to be_nil +Então('eu devo continuar na página {string}') do |nome_pagina| + if nome_pagina.include?("Importar") + expect(current_path).to eq(import_sigaa_path) + else + expect(current_path).to eq(admin_management_path) + end end \ No newline at end of file diff --git a/spec/models/form_spec.rb b/spec/models/form_spec.rb new file mode 100644 index 0000000000..4607aad445 --- /dev/null +++ b/spec/models/form_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Form, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/respostum_spec.rb b/spec/models/respostum_spec.rb new file mode 100644 index 0000000000..050980cbac --- /dev/null +++ b/spec/models/respostum_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Respostum, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/template_spec.rb b/spec/models/template_spec.rb new file mode 100644 index 0000000000..068ef0ee3c --- /dev/null +++ b/spec/models/template_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Template, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/turma_spec.rb b/spec/models/turma_spec.rb new file mode 100644 index 0000000000..b7629b1e16 --- /dev/null +++ b/spec/models/turma_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Turma, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/usuario_spec.rb b/spec/models/usuario_spec.rb new file mode 100644 index 0000000000..12591f4841 --- /dev/null +++ b/spec/models/usuario_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Usuario, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/vinculo_spec.rb b/spec/models/vinculo_spec.rb new file mode 100644 index 0000000000..b58bcf4fe5 --- /dev/null +++ b/spec/models/vinculo_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Vinculo, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end From fadebcbec6088a2c67a0b9bb87413e21f0ba8f1a Mon Sep 17 00:00:00 2001 From: Carol <232050975@aluno.unb.br> Date: Fri, 28 Nov 2025 11:54:22 -0300 Subject: [PATCH 032/100] =?UTF-8?q?Ajustes=20nas=20configura=C3=A7=C3=B5es?= =?UTF-8?q?=20do=20Banco=20de=20Dados=20e=20Implementa=C3=A7=C3=A3o=20da?= =?UTF-8?q?=20US=20'Gerar=20relat=C3=B3rio=20do=20administrador'=20-=20Dev?= =?UTF-8?q?e-se=20alterar=20os=20par=C3=A2metros=20do=20arquivo=20.csv=20p?= =?UTF-8?q?ara=20quando=20o=20preenchimento=20do=20formul=C3=A1rio=20de=20?= =?UTF-8?q?avalia=C3=A7=C3=B5es=20estiver=20implementado.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Gemfile | 1 + Gemfile.lock | 2 + app/controllers/admins_controller.rb | 42 +++++++ app/models/turma.rb | 2 + app/models/usuario.rb | 2 + app/views/admins/management.html.erb | 3 +- app/views/admins/new_import.html.erb | 68 +++++++---- app/views/admins/results.html.erb | 39 ++++++ config/routes.rb | 4 + features/admin/atualizar_dados.feature | 112 +++++++++--------- features/admin/gerenciar_departamento.feature | 48 ++++---- features/admin/gerenciar_relatorios.feature | 44 +++---- features/step_definitions/results_steps.rb | 71 +++++++++++ features/support/env.rb | 11 ++ 14 files changed, 321 insertions(+), 128 deletions(-) create mode 100644 app/views/admins/results.html.erb create mode 100644 features/step_definitions/results_steps.rb diff --git a/Gemfile b/Gemfile index 364bd47248..8453384cc3 100644 --- a/Gemfile +++ b/Gemfile @@ -19,6 +19,7 @@ gem "jbuilder" # Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] # gem "bcrypt", "~> 3.1.7" +gem "csv" # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem "tzinfo-data", platforms: %i[ windows jruby ] diff --git a/Gemfile.lock b/Gemfile.lock index 54cbd8c129..448b2e49ec 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -99,6 +99,7 @@ GEM concurrent-ruby (1.3.5) connection_pool (2.5.4) crass (1.0.6) + csv (3.3.5) cucumber (10.1.1) base64 (~> 0.2) builder (~> 3.2) @@ -441,6 +442,7 @@ DEPENDENCIES bootsnap brakeman capybara + csv cucumber-rails database_cleaner-active_record debug diff --git a/app/controllers/admins_controller.rb b/app/controllers/admins_controller.rb index c4d8bca003..370681f766 100644 --- a/app/controllers/admins_controller.rb +++ b/app/controllers/admins_controller.rb @@ -1,4 +1,8 @@ +require 'csv' + class AdminsController < ApplicationController + before_action :require_admin, except: [:management] + def management end @@ -27,4 +31,42 @@ def create_import redirect_to import_sigaa_path, alert: "Ocorreu um erro inesperado: #{e.message}" end end + + def results + @turmas = Turma.all + end + + def export_csv # TEM QUE MUDAR ESSA LÓGICA PARA GERAR O FORMULÁRIO REAL + @turma = Turma.find(params[:turma_id]) + + if @turma.vinculos.count == 0 + redirect_to admin_results_path, alert: "Este formulário ainda não possui respostas" + return + end + + codigo_materia = @turma.nome.match(/\((.*?)\s-/)&.captures&.first || "TURMA_#{@turma.id}" + + filename = "resultados_#{codigo_materia}.csv" + + csv_data = CSV.generate(headers: true) do |csv| + csv << ["Matricula", "Nome", "Email", "Perfil"] + + @turma.vinculos.each do |vinculo| + u = vinculo.usuario + csv << [u.matricula, u.nome, u.email, u.profile] + end + end + + send_data csv_data, filename: filename, type: 'text/csv' + end + + private + + def require_admin + user = try(:current_user) + + if user.present? && user.profile != 'Admin' + redirect_to admin_management_path, alert: "Acesso negado" + end + end end \ No newline at end of file diff --git a/app/models/turma.rb b/app/models/turma.rb index 41fd637f68..6b10422401 100644 --- a/app/models/turma.rb +++ b/app/models/turma.rb @@ -1,2 +1,4 @@ class Turma < ApplicationRecord + has_many :vinculos, dependent: :destroy + has_many :usuarios, through: :vinculos end diff --git a/app/models/usuario.rb b/app/models/usuario.rb index c21d504781..7cad11a3a4 100644 --- a/app/models/usuario.rb +++ b/app/models/usuario.rb @@ -1,2 +1,4 @@ class Usuario < ApplicationRecord + has_many :vinculos, dependent: :destroy + has_many :turmas, through: :vinculos end diff --git a/app/views/admins/management.html.erb b/app/views/admins/management.html.erb index 4d8a2ccb7e..ebd7d80e50 100644 --- a/app/views/admins/management.html.erb +++ b/app/views/admins/management.html.erb @@ -1,5 +1,4 @@
-
diff --git a/app/views/admins/new_import.html.erb b/app/views/admins/new_import.html.erb index 470e795299..f4d6c05256 100644 --- a/app/views/admins/new_import.html.erb +++ b/app/views/admins/new_import.html.erb @@ -1,33 +1,51 @@ -
+
+ + +
+
+

Importar Dados

+ +
-
-

Importar Dados do SIGAA

-

- Selecione o arquivo JSON de Turmas ou Alunos. -

+
+
+

Importar Dados do SIGAA

+

+ Selecione o arquivo JSON de Turmas ou Alunos. +

+ + <%= form_with url: import_sigaa_path, local: true, multipart: true do |form| %> + + <% if flash[:alert] %> +
+ <%= flash[:alert] %> +
+ <% end %> - <%= form_with url: import_sigaa_path, local: true, multipart: true do |form| %> - - <% if flash[:alert] %> -
- <%= flash[:alert] %> +
+ <%= form.label :file, "Arquivo JSON:", style: "display: block; margin-bottom: 8px; font-weight: bold; color: #333;" %> + + <%= form.file_field :file, accept: 'application/json', + style: "width: 100%; box-sizing: border-box; display: block; padding: 12px; border: 1px solid #ccc; border-radius: 6px; font-size: 1rem;" %>
- <% end %> -
- <%= form.label :file, "Arquivo JSON:", style: "display: block; margin-bottom: 8px; font-weight: bold; color: #333;" %> + <%= form.submit "Enviar Arquivo", class: "btn-action btn-primary-green", style: "margin-bottom: 15px;" %> - <%= form.file_field :file, accept: 'application/json', - style: "width: 100%; box-sizing: border-box; display: block; padding: 12px; border: 1px solid #ccc; border-radius: 6px; font-size: 1rem;" %> -
- - <%= form.submit "Enviar Arquivo", class: "btn-action btn-primary-green", style: "margin-bottom: 15px;" %> - -
- <%= link_to "Voltar para o Menu", admin_management_path, style: "color: #666; text-decoration: none; font-size: 0.9rem;" %> -
+
+ <%= link_to "Voltar para o Menu", admin_management_path, style: "color: #666; text-decoration: none; font-size: 0.9rem;" %> +
- <% end %> + <% end %> +
- +
\ No newline at end of file diff --git a/app/views/admins/results.html.erb b/app/views/admins/results.html.erb new file mode 100644 index 0000000000..09e5235997 --- /dev/null +++ b/app/views/admins/results.html.erb @@ -0,0 +1,39 @@ +
+ + +
+
+

Resultados

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

<%= turma.nome %>

+ +

+ Respostas: <%= turma.vinculos.count %> +

+ + <%= link_to export_results_csv_path(turma.id), class: "btn-action btn-primary-green", style: "width: auto; display: inline-block; padding: 8px 15px; font-size: 0.9rem;" do %> + Baixar CSV + <% end %> +
+ <% end %> +
+ +
+
+
\ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 7a6bb6f5b7..5fde70fb3b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -8,6 +8,10 @@ get 'importar_sigaa', to: 'admins#new_import', as: 'import_sigaa' post 'importar_sigaa', to: 'admins#create_import' + get 'resultados', to: 'admins#results', as: 'admin_results' + get 'resultados/:turma_id/csv', to: 'admins#export_csv', as: 'export_results_csv' + + # Render dynamic PWA files from app/views/pwa/* (remember to link manifest in application.html.erb) # get "manifest" => "rails/pwa#manifest", as: :pwa_manifest diff --git a/features/admin/atualizar_dados.feature b/features/admin/atualizar_dados.feature index 625c9553bc..24ac19ad0a 100644 --- a/features/admin/atualizar_dados.feature +++ b/features/admin/atualizar_dados.feature @@ -6,61 +6,61 @@ Funcionalidade: Atualização da base de dados por importação de JSON do SIGAA Eu quero fazer o upload de um (ou mais) arquivo JSON contendo os dados das disciplinas, turmas e alunos matriculados A fim de atualizar o sistema com as turmas do semestre e garantir que apenas os alunos corretos possam avaliar seus respectivos professores. -Cenário: Admin importa JSON de um novo semestre - Dado que eu estou logado como "admin@sistema.com" - E eu estou na página que contém o botão "Importar dados" - E eu tenho os arquivos "disciplinas_2021.2.json" e "turmas_2021.2.json" - Quando eu seleciono os arquivos "disciplinas_2021.2.json" e "turmas_2021.2.json" - E importo-os para o sistema - Então eu devo ver a mensagem "Importação concluída." - E a disciplina "BANCOS DE DADOS" (CIC0097) deve existir no sistema - E a aluna "Ana Clara Jordao Perna" (190084006) deve estar matriculada na turma "TA" de "CIC0097" - - -Cenário: Admin importa JSON que atualiza o email de um aluno - Dado que eu estou logado como "admin@sistema.com" - E eu estou na página que contém o botão "Importar dados" - E existe uma aluna "Ana Clara Jordao Perna" (190084006) com o email "acjpjvjp@gmail.com" - E eu tenho um arquivo "turmas_correcao.json" onde o aluno "190484006" agora tem o email "ana.clara@aluno.unb.br" - Quando eu seleciono o arquivo "turmas_correcao.json" - E importo-o ao sistema - Então eu devo ver a mensagem "Importação concluída." - E a aluna (190084006) deve ter o email "ana.clara@aluno.unb.br" no banco de dados - E não deve existir um novo aluno no sistema - -Cenário: Admin tenta importar um arquivo que não é JSON - Dado que eu estou logado como "admin@sistema.com" - E eu estou na página que contém o botão "Importar dados" - Quando eu seleciono um arquivo "documento.csv" para importaçao de dados - E importo-o ao sistema - Então eu devo continuar na página que contém o botão "Importar dados" - E eu devo ver a mensagem de erro "Formato de arquivo inválido. Por favor, envie um arquivo .json." - -Cenário: Admin importa JSON de turma com chave "matricula" faltando - Dado que eu estou logado como "admin@sistema.com" - E eu estou na página que contém o botão "Importar dados" - E eu tenho um arquivo "turmas_sem_matricula.json" onde um objeto "discente" não possui a chave "matricula" - Quando eu seleciono o arquivo "turmas_sem_matricula.json" para importação de dados - E importo-o ao sistema - Então eu devo continuar na página que contém o botão "Importar dados" - E eu devo ver a mensagem de erro "Erro na importação: O arquivo JSON é inválido." - - -Cenário: Admin importa um arquivo JSON com sintaxe quebrada - Dado que eu estou logado como "admin@sistema.com" - E eu estou na página que contém o botão "Importar dados" - E eu tenho um arquivo "json_errado.json" (que possui um erro de sintaxe, como uma vírgula extra) - Quando eu seleciono o arquivo "json_errado.json" para importação de dados - E importo-o ao sistema - Então eu devo continuar na página que contém o botão "Importar dados" - E eu devo ver a mensagem de erro "Erro: O arquivo não é um JSON válido." + Cenário: Admin importa JSON de um novo semestre + Dado que eu estou logado como "admin@sistema.com" + E eu estou na página que contém o botão "Importar dados" + E eu tenho os arquivos "disciplinas_2021.2.json" e "turmas_2021.2.json" + Quando eu seleciono os arquivos "disciplinas_2021.2.json" e "turmas_2021.2.json" + E importo-os para o sistema + Então eu devo ver a mensagem "Importação concluída." + E a disciplina "BANCOS DE DADOS" (CIC0097) deve existir no sistema + E a aluna "Ana Clara Jordao Perna" (190084006) deve estar matriculada na turma "TA" de "CIC0097" + + + Cenário: Admin importa JSON que atualiza o email de um aluno + Dado que eu estou logado como "admin@sistema.com" + E eu estou na página que contém o botão "Importar dados" + E existe uma aluna "Ana Clara Jordao Perna" (190084006) com o email "acjpjvjp@gmail.com" + E eu tenho um arquivo "turmas_correcao.json" onde o aluno "190484006" agora tem o email "ana.clara@aluno.unb.br" + Quando eu seleciono o arquivo "turmas_correcao.json" + E importo-o ao sistema + Então eu devo ver a mensagem "Importação concluída." + E a aluna (190084006) deve ter o email "ana.clara@aluno.unb.br" no banco de dados + E não deve existir um novo aluno no sistema + + Cenário: Admin tenta importar um arquivo que não é JSON + Dado que eu estou logado como "admin@sistema.com" + E eu estou na página que contém o botão "Importar dados" + Quando eu seleciono um arquivo "documento.csv" para importaçao de dados + E importo-o ao sistema + Então eu devo continuar na página que contém o botão "Importar dados" + E eu devo ver a mensagem de erro "Formato de arquivo inválido. Por favor, envie um arquivo .json." + + Cenário: Admin importa JSON de turma com chave "matricula" faltando + Dado que eu estou logado como "admin@sistema.com" + E eu estou na página que contém o botão "Importar dados" + E eu tenho um arquivo "turmas_sem_matricula.json" onde um objeto "discente" não possui a chave "matricula" + Quando eu seleciono o arquivo "turmas_sem_matricula.json" para importação de dados + E importo-o ao sistema + Então eu devo continuar na página que contém o botão "Importar dados" + E eu devo ver a mensagem de erro "Erro na importação: O arquivo JSON é inválido." + + + Cenário: Admin importa um arquivo JSON com sintaxe quebrada + Dado que eu estou logado como "admin@sistema.com" + E eu estou na página que contém o botão "Importar dados" + E eu tenho um arquivo "json_errado.json" (que possui um erro de sintaxe, como uma vírgula extra) + Quando eu seleciono o arquivo "json_errado.json" para importação de dados + E importo-o ao sistema + Então eu devo continuar na página que contém o botão "Importar dados" + E eu devo ver a mensagem de erro "Erro: O arquivo não é um JSON válido." -Cenário: Admin importa JSON com tipo de dado inválido para matrícula - Dado que eu estou logado como "admin@sistema.com" - E eu estou na página que contém o botão "Importar dados" - E eu tenho um arquivo "turmas_tipo_errado.json" onde a "matricula" de um aluno é o texto "matricula" em vez de um número - Quando eu seleciono o arquivo "turmas_tipo_errado.json" para importação de dados - E importo-o ao sistema - Então eu devo continuar na página que contém o botão "Importar dados" - E eu devo ver a mensagem de erro "Erro na importação." \ No newline at end of file + Cenário: Admin importa JSON com tipo de dado inválido para matrícula + Dado que eu estou logado como "admin@sistema.com" + E eu estou na página que contém o botão "Importar dados" + E eu tenho um arquivo "turmas_tipo_errado.json" onde a "matricula" de um aluno é o texto "matricula" em vez de um número + Quando eu seleciono o arquivo "turmas_tipo_errado.json" para importação de dados + E importo-o ao sistema + Então eu devo continuar na página que contém o botão "Importar dados" + E eu devo ver a mensagem de erro "Erro na importação." \ No newline at end of file diff --git a/features/admin/gerenciar_departamento.feature b/features/admin/gerenciar_departamento.feature index f06d741405..fa70a7b6aa 100644 --- a/features/admin/gerenciar_departamento.feature +++ b/features/admin/gerenciar_departamento.feature @@ -6,29 +6,29 @@ Funcionalidade: Visualização de resultados de avaliação restrita por departa Eu quero gerenciar (ver e avaliar) somente as turmas do departamento ao qual eu pertenço A fim de avaliar o desempenho das turmas no semestre atual -Cenário: Admin do CIC vê apenas as turmas do CIC - Dado que eu estou logado como "admin@sistema.com" - E sou professor do depatamento "CIC" - E eu navego para a página "Gerenciamento" e clico em "Resultados" - E existe uma turma "Bancos de Dados (CIC0097)" que pertence ao departamento "CIC" - E existe uma turma "CÁLCULO 1 (MAT0025)" que pertence ao departamento "MAT" - Quando eu olho a lista de turmas na tela "Gerenciamento - Resultados" - Então eu devo ver o bloco da turma "Bancos de Dados (CIC0097)" - E eu não devo ver o bloco da turma "CÁLCULO 1 (MAT0025)" + Cenário: Admin do CIC vê apenas as turmas do CIC + Dado que eu estou logado como "admin@sistema.com" + E sou professor do depatamento "CIC" + E eu navego para a página "Gerenciamento" e clico em "Resultados" + E existe uma turma "Bancos de Dados (CIC0097)" que pertence ao departamento "CIC" + E existe uma turma "CÁLCULO 1 (MAT0025)" que pertence ao departamento "MAT" + Quando eu olho a lista de turmas na tela "Gerenciamento - Resultados" + Então eu devo ver o bloco da turma "Bancos de Dados (CIC0097)" + E eu não devo ver o bloco da turma "CÁLCULO 1 (MAT0025)" -Cenário: Admin do MAT vê apenas as turmas do MAT - Dado que eu estou logado como "admin@sistema.com" - E sou professor do depatamento "MAT" - E eu navego para a página "Gerenciamento" e clico em "Resultados" - E existe uma turma "Bancos de Dados (CIC0097)" que pertence ao departamento "CIC" - E existe uma turma "CÁLCULO 1 (MAT0025)" que pertence ao departamento "MAT" - Quando eu olho a lista de turmas na tela "Gerenciamento - Resultados" - Então eu não devo ver o bloco da turma "Bancos de Dados (CIC0097)" - E eu devo ver o bloco da turma "CÁLCULO 1 (MAT0025)" + Cenário: Admin do MAT vê apenas as turmas do MAT + Dado que eu estou logado como "admin@sistema.com" + E sou professor do depatamento "MAT" + E eu navego para a página "Gerenciamento" e clico em "Resultados" + E existe uma turma "Bancos de Dados (CIC0097)" que pertence ao departamento "CIC" + E existe uma turma "CÁLCULO 1 (MAT0025)" que pertence ao departamento "MAT" + Quando eu olho a lista de turmas na tela "Gerenciamento - Resultados" + Então eu não devo ver o bloco da turma "Bancos de Dados (CIC0097)" + E eu devo ver o bloco da turma "CÁLCULO 1 (MAT0025)" -Cenário: Admin do CIC tenta acessar resultados do MAT diretamente pela URL - Dado que eu estou logado como "admin@sistema.com" - E sou professor do depatamento "CIC" - E existe a turma "Cálculo 1 (MAT0025)" (de id: 42) que pertence ao departamento "MAT" - Quando eu tento acessar a URL "/gerenciamento/resultados/42" diretamente no meu navegador - Então eu devo ser redirecionado para a minha página inicial (ou "Dashboard") \ No newline at end of file + Cenário: Admin do CIC tenta acessar resultados do MAT diretamente pela URL + Dado que eu estou logado como "admin@sistema.com" + E sou professor do depatamento "CIC" + E existe a turma "Cálculo 1 (MAT0025)" (de id: 42) que pertence ao departamento "MAT" + Quando eu tento acessar a URL "/gerenciamento/resultados/42" diretamente no meu navegador + Então eu devo ser redirecionado para a minha página inicial (ou "Dashboard") \ No newline at end of file diff --git a/features/admin/gerenciar_relatorios.feature b/features/admin/gerenciar_relatorios.feature index 910115e961..7b4b3b5b4e 100644 --- a/features/admin/gerenciar_relatorios.feature +++ b/features/admin/gerenciar_relatorios.feature @@ -1,28 +1,30 @@ # language: pt Funcionalidade: Visualização e Download de Resultados de Avaliação - Como um administrador - Eu quero baixar um arquivo CSV contendo os resultados de um formulário - A fim de avaliar o desempenho das turmas + Como um administrador + Eu quero baixar um arquivo CSV contendo os resultados de um formulário + A fim de avaliar o desempenho das turmas -Cenário: Admin baixa CSV de uma turma com respostas + Contexto: Dado que eu estou logado como "admin@sistema.com" - E eu navego para a página "Gerenciamento" - E eu clico em "Resultados" - E eu estou na tela "Gerenciamento - Resultados" - Quando eu clico no bloco da turma "BANCOS DE DADOS (CIC0097) - Prof. Joao" - Então eu devo baixar um arquivo chamado "resultados_CIC0097_ProfJoao.csv" + + Cenário: Admin baixa CSV de uma turma com respostas + Dado que existe uma turma "BANCOS DE DADOS (CIC0097) - Prof. Joao" com 5 alunos + E eu estou na página "Resultados" + Quando eu clico no botão "Baixar CSV" da turma "BANCOS DE DADOS (CIC0097) - Prof. Joao" + Então o download do arquivo deve ser iniciado + E o nome do arquivo deve conter "resultados" e "CIC0097" -Cenário: Admin tenta ver resultados de uma turma sem respostas - Dado que eu estou logado como "admin@sistema.com" - E eu estou na tela "Gerenciamento - Resultados" - E a turma "ENGENHARIA DE SOFTWARE (CIC0105) - Prof. Genaina" possui 0 respostas de avaliação - Quando eu clico no bloco da turma "ENGENHARIA DE SOFTWARE (CIC0105) - Prof. Genaina" - Então deve exibir a mensagem "Este formulário ainda não possui respostas" - E não efetuar nenhum download de aruivo CSV. + Cenário: Admin tenta ver resultados de uma turma sem respostas + Dado que existe uma turma "ENGENHARIA DE SOFTWARE (CIC0105) - Prof. Genaina" com 0 alunos + E eu estou na página "Resultados" + Quando eu clico no botão "Baixar CSV" da turma "ENGENHARIA DE SOFTWARE (CIC0105) - Prof. Genaina" + Então eu devo ver a mensagem "Este formulário ainda não possui respostas" + E a página não deve fazer download -Cenário: Professor (não-admin) tenta acessar a página de resultados diretamente pela URL - Dado que eu estou logado como "professor.comum@sistema.com" (que não é admin) - E eu não vejo o link para a página "Gerenciamento" no meu menu - Quando eu tento acessar a URL "/gerenciamento/resultados" diretamente no meu navegador - Então eu devo ser redirecionado para a minha página inicial (ou "Dashboard") \ No newline at end of file + Cenário: Professor (não-admin) tenta acessar a página de resultados diretamente pela URL + Dado que eu estou logado como "professor@sistema.com" + E o meu perfil é "Professor" + Quando eu tento acessar a URL "/resultados" diretamente no meu navegador + Então eu devo ser redirecionado para a "Dashboard" + E eu devo ver a mensagem "Acesso negado" \ No newline at end of file diff --git a/features/step_definitions/results_steps.rb b/features/step_definitions/results_steps.rb new file mode 100644 index 0000000000..4e38e2fdc3 --- /dev/null +++ b/features/step_definitions/results_steps.rb @@ -0,0 +1,71 @@ +# features/step_definitions/results_steps.rb + +Dado('que existe uma turma {string} com {int} alunos') do |nome_turma, qtd_alunos| + # Cria a turma + turma = Turma.create!(nome: nome_turma, semestre: "2021.2", is_active: true) + + # Cria os alunos (Vínculos) + qtd_alunos.times do |i| + u = Usuario.create!( + nome: "Aluno #{i}", + email: "aluno#{i}@teste.com", + matricula: "100#{i}", + profile: "Aluno", + status: true + ) + Vinculo.create!(usuario: u, turma: turma, papel_turma: 0) + end +end + +Dado('eu estou na página {string}') do |pagina| + if pagina == "Resultados" + visit '/resultados' + else + visit '/gerenciamento' + end +end + +Quando('eu clico no botão "Baixar CSV" da turma {string}') do |nome_turma| + # Acha o bloco da turma pelo texto do título + turma_element = find('h4', text: nome_turma).find(:xpath, '..') + + # Clica no link dentro desse bloco + within(turma_element) do + click_link "Baixar CSV" + end +end + +Então('o download do arquivo deve ser iniciado') do + # Verifica se o cabeçalho da resposta indica um anexo CSV + expect(page.response_headers['Content-Type']).to include('text/csv') +end + +Então('o nome do arquivo deve conter {string} e {string}') do |parte1, parte2| + disposition = page.response_headers['Content-Disposition'] + expect(disposition).to include(parte1) + expect(disposition).to include(parte2) +end + +Então('a página não deve fazer download') do + # Se não fez download, o content-type continua sendo HTML + expect(page.response_headers['Content-Type']).to include('text/html') +end + +# --- Lógica de Acesso Negado --- + +Dado('o meu perfil é {string}') do |perfil| + # Cria um objeto falso (Mock) que responde a .profile + user_mock = double("User", profile: perfil) + + # Injeta esse objeto falso no controller APENAS para este cenário + allow_any_instance_of(AdminsController).to receive(:current_user).and_return(user_mock) +end + +Quando('eu tento acessar a URL {string} diretamente no meu navegador') do |url| + visit url +end + +Então('eu devo ser redirecionado para a {string}') do |pagina| + path = pagina == "Dashboard" ? '/gerenciamento' : pagina + expect(current_path).to eq(path) +end \ No newline at end of file diff --git a/features/support/env.rb b/features/support/env.rb index 3b97d14087..12271e9b79 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -6,6 +6,7 @@ require 'cucumber/rails' +require 'rspec/mocks' # 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 @@ -22,6 +23,16 @@ # 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! # +World(RSpec::Mocks::ExampleMethods) + +Before do + RSpec::Mocks.setup +end + +After do + RSpec::Mocks.teardown +end + ActionController::Base.allow_rescue = false # Remove/comment out the lines below if your app doesn't have a database. From e47e5e0b03a7a15d79a0a6ac7fb8164616a02ba3 Mon Sep 17 00:00:00 2001 From: Carol <232050975@aluno.unb.br> Date: Fri, 28 Nov 2025 21:39:06 -0300 Subject: [PATCH 033/100] =?UTF-8?q?Implementa=C3=A7=C3=A3o=20da=20US=20#14?= =?UTF-8?q?=20-=20Atualizar=20base=20de=20dados=20com=20os=20dados=20do=20?= =?UTF-8?q?SIGAA=20testado=20no=20Cucumber?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/services/sigaa_import_service.rb | 45 ++- features/admin/atualizar_dados.feature | 104 +++-- features/step_definitions/import_steps.rb | 27 ++ spec/fixtures/files/documento.csv | 2 + spec/fixtures/files/json_errado.json | 1 + spec/fixtures/files/turmas_correcao.json | 413 ++++++++++++++++++++ spec/fixtures/files/turmas_tipo_errado.json | 413 ++++++++++++++++++++ 7 files changed, 928 insertions(+), 77 deletions(-) create mode 100644 spec/fixtures/files/documento.csv create mode 100644 spec/fixtures/files/json_errado.json create mode 100755 spec/fixtures/files/turmas_correcao.json create mode 100755 spec/fixtures/files/turmas_tipo_errado.json diff --git a/app/services/sigaa_import_service.rb b/app/services/sigaa_import_service.rb index ecd9243945..0b7d40ef48 100644 --- a/app/services/sigaa_import_service.rb +++ b/app/services/sigaa_import_service.rb @@ -5,9 +5,7 @@ class InvalidFileError < StandardError; end REQUIRED_SUBJECT_KEYS = %w[code name class] REQUIRED_CLASS_KEYS = %w[classCode semester time] - REQUIRED_MEMBER_FILE_KEYS = %w[code classCode semester dicente] - REQUIRED_STUDENT_KEYS = %w[nome matricula curso email ocupacao formacao] def initialize(file_path) @@ -58,11 +56,9 @@ def process_classes_file(entry, index) nome_completo = "#{entry['name']} (#{entry['code']} - #{entry['class']['classCode']})" semestre = entry['class']['semester'] - turma = Turma.find_or_create_by(nome: nome_completo, semestre: semestre) do |t| - t.is_active = true - end - - turma.update(nome: nome_completo) + turma = Turma.find_or_initialize_by(nome: nome_completo, semestre: semestre) + turma.is_active = true + turma.save! end def process_members_file(entry, index) @@ -71,6 +67,10 @@ def process_members_file(entry, index) if entry['dicente'] entry['dicente'].each_with_index do |student_data, s_index| validate_keys!(student_data, REQUIRED_STUDENT_KEYS, "no Aluno ##{s_index + 1}") + + unless student_data['matricula'].to_s.match?(/^\d+$/) + raise InvalidFileError, "Erro no Aluno ##{s_index + 1}: A matrícula deve conter apenas números (Valor recebido: '#{student_data['matricula']}')." + end end end @@ -78,23 +78,22 @@ def process_members_file(entry, index) t.nome.include?(entry['code']) && t.nome.include?(entry['classCode']) end - unless turma - return - end + return unless turma - if entry['dicente'] - entry['dicente'].each do |student_data| - usuario = Usuario.find_or_create_by(matricula: student_data['matricula']) do |u| - u.nome = student_data['nome'] - u.email = student_data['email'] - u.profile = 'Aluno' - u.status = true - u.departamento_id = 1 - end - - Vinculo.find_or_create_by(usuario: usuario, turma: turma) do |v| - v.papel_turma = 0 - end + entry['dicente'].each do |student_data| + + usuario = Usuario.find_or_initialize_by(matricula: student_data['matricula'].to_s) + + usuario.update!( + nome: student_data['nome'], + email: student_data['email'], + profile: 'Aluno', + status: true, + departamento_id: 1 + ) + + Vinculo.find_or_create_by(usuario: usuario, turma: turma) do |v| + v.papel_turma = 0 end end end diff --git a/features/admin/atualizar_dados.feature b/features/admin/atualizar_dados.feature index 24ac19ad0a..f6809fc401 100644 --- a/features/admin/atualizar_dados.feature +++ b/features/admin/atualizar_dados.feature @@ -1,66 +1,62 @@ # language: pt Funcionalidade: Atualização da base de dados por importação de JSON do SIGAA + Como um administrador do sistema + Eu quero fazer o upload de arquivos JSON para atualizar turmas e alunos + A fim de manter os dados sincronizados e corrigir informações (como e-mails). - Como um administrador do sistema - Eu quero fazer o upload de um (ou mais) arquivo JSON contendo os dados das disciplinas, turmas e alunos matriculados - A fim de atualizar o sistema com as turmas do semestre e garantir que apenas os alunos corretos possam avaliar seus respectivos professores. - - Cenário: Admin importa JSON de um novo semestre + Contexto: Dado que eu estou logado como "admin@sistema.com" - E eu estou na página que contém o botão "Importar dados" - E eu tenho os arquivos "disciplinas_2021.2.json" e "turmas_2021.2.json" - Quando eu seleciono os arquivos "disciplinas_2021.2.json" e "turmas_2021.2.json" - E importo-os para o sistema - Então eu devo ver a mensagem "Importação concluída." - E a disciplina "BANCOS DE DADOS" (CIC0097) deve existir no sistema - E a aluna "Ana Clara Jordao Perna" (190084006) deve estar matriculada na turma "TA" de "CIC0097" - - + E que eu estou na página "Importar Dados do SIGAA" + + Cenário: Admin importa JSON de um novo semestre (Funcionamento Padrão) + Dado o banco de dados não contém nenhuma disciplina ou aluno + E eu tenho um arquivo "disciplinas_2021.2.json" + E eu tenho um arquivo "turmas_2021.2.json" + Quando eu seleciono o arquivo "disciplinas_2021.2.json" + E eu clico no botão "Enviar Arquivo" + E que eu estou na página "Importar Dados do SIGAA" + E eu seleciono o arquivo "turmas_2021.2.json" + E eu clico no botão "Enviar Arquivo" + Então eu devo ver a mensagem "Dados importados com sucesso!" + E a disciplina "BANCOS DE DADOS" ("CIC0097") deve existir no sistema + E o aluno "Ana Clara Jordao Perna" ("190084006") deve existir no sistema + Cenário: Admin importa JSON que atualiza o email de um aluno - Dado que eu estou logado como "admin@sistema.com" - E eu estou na página que contém o botão "Importar dados" - E existe uma aluna "Ana Clara Jordao Perna" (190084006) com o email "acjpjvjp@gmail.com" - E eu tenho um arquivo "turmas_correcao.json" onde o aluno "190484006" agora tem o email "ana.clara@aluno.unb.br" + Dado já existe uma disciplina "BANCOS DE DADOS" ("CIC0097") no sistema + Dado existe uma aluna "Ana Clara Jordao Perna" com matrícula "190084006" e email "email.antigo@gmail.com" + E eu tenho um arquivo "turmas_correcao.json" Quando eu seleciono o arquivo "turmas_correcao.json" - E importo-o ao sistema - Então eu devo ver a mensagem "Importação concluída." - E a aluna (190084006) deve ter o email "ana.clara@aluno.unb.br" no banco de dados - E não deve existir um novo aluno no sistema - + E eu clico no botão "Enviar Arquivo" + Então eu devo ver a mensagem "Dados importados com sucesso!" + E a aluna com matrícula "190084006" deve ter o email "ana.clara@aluno.unb.br" + # Verifica que não duplicou + E deve haver apenas 1 aluno com a matrícula "190084006" no sistema + Cenário: Admin tenta importar um arquivo que não é JSON - Dado que eu estou logado como "admin@sistema.com" - E eu estou na página que contém o botão "Importar dados" - Quando eu seleciono um arquivo "documento.csv" para importaçao de dados - E importo-o ao sistema - Então eu devo continuar na página que contém o botão "Importar dados" - E eu devo ver a mensagem de erro "Formato de arquivo inválido. Por favor, envie um arquivo .json." - + Dado eu tenho um arquivo "documento.csv" + Quando eu seleciono o arquivo "documento.csv" + E eu clico no botão "Enviar Arquivo" + Então eu devo continuar na página "Importar Dados do SIGAA" + E eu devo ver a mensagem "O arquivo não é um JSON válido" + Cenário: Admin importa JSON de turma com chave "matricula" faltando - Dado que eu estou logado como "admin@sistema.com" - E eu estou na página que contém o botão "Importar dados" - E eu tenho um arquivo "turmas_sem_matricula.json" onde um objeto "discente" não possui a chave "matricula" - Quando eu seleciono o arquivo "turmas_sem_matricula.json" para importação de dados - E importo-o ao sistema - Então eu devo continuar na página que contém o botão "Importar dados" - E eu devo ver a mensagem de erro "Erro na importação: O arquivo JSON é inválido." - - - Cenário: Admin importa um arquivo JSON com sintaxe quebrada - Dado que eu estou logado como "admin@sistema.com" - E eu estou na página que contém o botão "Importar dados" - E eu tenho um arquivo "json_errado.json" (que possui um erro de sintaxe, como uma vírgula extra) - Quando eu seleciono o arquivo "json_errado.json" para importação de dados - E importo-o ao sistema - Então eu devo continuar na página que contém o botão "Importar dados" - E eu devo ver a mensagem de erro "Erro: O arquivo não é um JSON válido." + Dado eu tenho um arquivo "turmas_sem_matricula.json" + Quando eu seleciono o arquivo "turmas_sem_matricula.json" + E eu clico no botão "Enviar Arquivo" + Então eu devo continuar na página "Importar Dados do SIGAA" + E eu devo ver a mensagem "O campo obrigatório 'matricula' está ausente ou vazio" + Cenário: Admin importa JSON com sintaxe quebrada + Dado eu tenho um arquivo "json_errado.json" + Quando eu seleciono o arquivo "json_errado.json" + E eu clico no botão "Enviar Arquivo" + Então eu devo continuar na página "Importar Dados do SIGAA" + E eu devo ver a mensagem "O arquivo não é um JSON válido" Cenário: Admin importa JSON com tipo de dado inválido para matrícula - Dado que eu estou logado como "admin@sistema.com" - E eu estou na página que contém o botão "Importar dados" - E eu tenho um arquivo "turmas_tipo_errado.json" onde a "matricula" de um aluno é o texto "matricula" em vez de um número - Quando eu seleciono o arquivo "turmas_tipo_errado.json" para importação de dados - E importo-o ao sistema - Então eu devo continuar na página que contém o botão "Importar dados" - E eu devo ver a mensagem de erro "Erro na importação." \ No newline at end of file + Dado eu tenho um arquivo "turmas_tipo_errado.json" + Quando eu seleciono o arquivo "turmas_tipo_errado.json" + E eu clico no botão "Enviar Arquivo" + Então eu devo continuar na página "Importar Dados do SIGAA" + E eu devo ver a mensagem "A matrícula deve conter apenas números" \ No newline at end of file diff --git a/features/step_definitions/import_steps.rb b/features/step_definitions/import_steps.rb index d2ea482337..47c434fc32 100644 --- a/features/step_definitions/import_steps.rb +++ b/features/step_definitions/import_steps.rb @@ -77,4 +77,31 @@ else expect(current_path).to eq(admin_management_path) end +end + +Dado('existe uma aluna {string} com matrícula {string} e email {string}') do |nome, matricula, email| + Usuario.create!( + nome: nome, + matricula: matricula, + email: email, + profile: "Aluno", + status: true, + departamento_id: 1 + ) +end + +Então('a aluna com matrícula {string} deve ter o email {string} no banco de dados') do |matricula, novo_email| + usuario = Usuario.find_by(matricula: matricula) + expect(usuario.email).to eq(novo_email) +end + +Então('deve haver apenas {int} aluno com a matrícula {string} no sistema') do |quantidade, matricula| + expect(Usuario.where(matricula: matricula).count).to eq(quantidade) +end + +Então('a aluna com matrícula {string} deve ter o email {string}') do |matricula, novo_email| + usuario = Usuario.find_by(matricula: matricula) + + expect(usuario).to be_present, "Usuário com matrícula #{matricula} não encontrado." + expect(usuario.email).to eq(novo_email) end \ No newline at end of file diff --git a/spec/fixtures/files/documento.csv b/spec/fixtures/files/documento.csv new file mode 100644 index 0000000000..6576dd12fb --- /dev/null +++ b/spec/fixtures/files/documento.csv @@ -0,0 +1,2 @@ +1, 2, 3 e BLACKÍNK IN YOU AREA +Ouça o novo album da Kim Taeyeon! \ No newline at end of file diff --git a/spec/fixtures/files/json_errado.json b/spec/fixtures/files/json_errado.json new file mode 100644 index 0000000000..5a29895113 --- /dev/null +++ b/spec/fixtures/files/json_errado.json @@ -0,0 +1 @@ +[ {"chave": "OUÇA O NOVO ÁLBUM DA KIM TAEYEON!!!!!" \ No newline at end of file diff --git a/spec/fixtures/files/turmas_correcao.json b/spec/fixtures/files/turmas_correcao.json new file mode 100755 index 0000000000..eddf4e0bb2 --- /dev/null +++ b/spec/fixtures/files/turmas_correcao.json @@ -0,0 +1,413 @@ +[ + { + "code": "CIC0097", + "classCode": "TA", + "semester": "2021.2", + "dicente": [ + { + "nome": "Ana Clara Jordao Perna", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190084006", + "usuario": "190084006", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "ana.clara@aluno.unb.br" + }, + { + "nome": "Andre Carvalho de Roure", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "200033522", + "usuario": "200033522", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "andreCarvalhoroure@gmail.com" + }, + { + "nome": "André Carvalho Marques", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "150005491", + "usuario": "150005491", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "andre.acm97@outlook.com" + }, + { + "nome": "Antonio Vinicius de Moura Rodrigues", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190084502", + "usuario": "190084502", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "antoniovmoura.r@gmail.com" + }, + { + "nome": "Arthur Barreiros de Oliveira Mota", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190102829", + "usuario": "190102829", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "arthurbarreirosmota@gmail.com" + }, + { + "nome": "ARTHUR RODRIGUES NEVES", + "curso": "ENGENHARIA DE COMPUTAÇÃO/CIC", + "matricula": "202014403", + "usuario": "202014403", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "arthurcontroleambiental@gmail.com" + }, + { + "nome": "Bianca Glycia Boueri", + "curso": "ENGENHARIA MECATRÔNICA - CONTROLE E AUTOMAÇÃO/FTD", + "matricula": "170161561", + "usuario": "170161561", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "biancaglyciaboueri@gmail.com" + }, + { + "nome": "Caio Otávio Peluti Alencar", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190085312", + "usuario": "190085312", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "peluticaio@gmail.com" + }, + { + "nome": "Camila Frealdo Fraga", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "170007561", + "usuario": "170007561", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "camilizx2021@gmail.com" + }, + { + "nome": "Claudio Roberto Oliveira Peres de Barros", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190097591", + "usuario": "190097591", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "dinhobarros15@gmail.com" + }, + { + "nome": "Daltro Oliveira Vinuto", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "160025966", + "usuario": "160025966", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "daltroov777@gmail.com" + }, + { + "nome": "Davi de Moura Amaral", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "200016750", + "usuario": "200016750", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "davimouraamaral@gmail.com" + }, + { + "nome": "Eduardo Xavier Dantas", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190086530", + "usuario": "190086530", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "eduardoxdantas@gmail.com" + }, + { + "nome": "Enzo Nunes Leal Sampaio", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190062789", + "usuario": "190062789", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "enzonleal2016@hotmail.com" + }, + { + "nome": "Enzo Yoshio Niho", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190027304", + "usuario": "190027304", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "enzoyn@hotmail.com" + }, + { + "nome": "Gabriel Faustino Lima da Rocha", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190013249", + "usuario": "190013249", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "gabrielfaustino99@gmail.com" + }, + { + "nome": "Gabriel Ligoski", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190087498", + "usuario": "190087498", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "gabriel.ligoski@gmail.com" + }, + { + "nome": "GABRIEL MENDES CIRIATICO GUIMARÃES", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "202033202", + "usuario": "202033202", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "gabrielciriatico@gmail.com" + }, + { + "nome": "Gustavo Rodrigues dos Santos", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190014121", + "usuario": "190014121", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "190014121@aluno.unb.br" + }, + { + "nome": "Gustavo Rodrigues Gualberto", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190108266", + "usuario": "190108266", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "gustavorgualberto@gmail.com" + }, + { + "nome": "Igor David Morais", + "curso": "ENGENHARIA MECATRÔNICA - CONTROLE E AUTOMAÇÃO/FTD", + "matricula": "180102141", + "usuario": "180102141", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "igordavid13@gmail.com" + }, + { + "nome": "Jefte Augusto Gomes Batista", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "180057570", + "usuario": "180057570", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "ndaffte@gmail.com" + }, + { + "nome": "Karolina de Souza Silva", + "curso": "ENGENHARIA DE COMPUTAÇÃO/CIC", + "matricula": "190046791", + "usuario": "190046791", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "karolinasouza@outlook.com" + }, + { + "nome": "Kléber Rodrigues da Costa Júnior", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "200053680", + "usuario": "200053680", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "kleberrjr7@gmail.com" + }, + { + "nome": "Luca Delpino Barbabella", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "180125559", + "usuario": "180125559", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "barbadluca@gmail.com" + }, + { + "nome": "Lucas de Almeida Abreu Faria", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "170016668", + "usuario": "170016668", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "lucasaafaria@gmail.com" + }, + { + "nome": "Lucas Gonçalves Ramalho", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190098091", + "usuario": "190098091", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "lucasramalho29@gmail.com" + }, + { + "nome": "Lucas Monteiro Miranda", + "curso": "ENGENHARIA MECATRÔNICA - CONTROLE E AUTOMAÇÃO/FTD", + "matricula": "170149684", + "usuario": "170149684", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "luquinha_miranda@hotmail.com" + }, + { + "nome": "Lucas Resende Silveira Reis", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "180144421", + "usuario": "180144421", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "180144421@aluno.unb.br" + }, + { + "nome": "Luis Fernando Freitas Lamellas", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190016841", + "usuario": "190016841", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "lflamellas@icloud.com" + }, + { + "nome": "Luiza de Araujo Nunes Gomes", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190112794", + "usuario": "190112794", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "luizangomes@outlook.com" + }, + { + "nome": "Marcelo Aiache Postiglione", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "180126652", + "usuario": "180126652", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "180126652@aluno.unb.br" + }, + { + "nome": "Marcelo Junqueira Ferreira", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "200023624", + "usuario": "200023624", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "marcelojunqueiraf@gmail.com" + }, + { + "nome": "MARIA EDUARDA CARVALHO SANTOS", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190092556", + "usuario": "190092556", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "auntduda@gmail.com" + }, + { + "nome": "Maria Eduarda Lacerda Dantas", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "200067184", + "usuario": "200067184", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "lacwerda@gmail.com" + }, + { + "nome": "Maylla Krislainy de Sousa Silva", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190043873", + "usuario": "190043873", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "mayllak@hotmail.com" + }, + { + "nome": "Pedro Cesar Ribeiro Passos", + "curso": "ENGENHARIA MECATRÔNICA - CONTROLE E AUTOMAÇÃO/FTD", + "matricula": "180139312", + "usuario": "180139312", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "pedrocesarribeiro2013@gmail.com" + }, + { + "nome": "Rafael Mascarenhas Dal Moro", + "curso": "ENGENHARIA DE COMPUTAÇÃO/CIC", + "matricula": "170021041", + "usuario": "170021041", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "170021041@aluno.unb.br" + }, + { + "nome": "Rodrigo Mamedio Arrelaro", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190095164", + "usuario": "190095164", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "arrelaro1@hotmail.com" + }, + { + "nome": "Thiago de Oliveira Albuquerque", + "curso": "ENGENHARIA DE COMPUTAÇÃO/CIC", + "matricula": "140177442", + "usuario": "140177442", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "thiago.work.ti@outlook.com" + }, + { + "nome": "Thiago Elias dos Reis", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190126892", + "usuario": "190126892", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "thiagoeliasdosreis01@gmail.com" + }, + { + "nome": "Victor Hugo Rodrigues Fernandes", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "180132041", + "usuario": "180132041", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "aluno0sem.luz@gmail.com" + }, + { + "nome": "Vinicius Lima Passos", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "200028545", + "usuario": "200028545", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "viniciuslimapassos@gmail.com" + }, + { + "nome": "William Xavier dos Santos", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190075384", + "usuario": "190075384", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "wilxavier@me.com" + } + ], + "docente": { + "nome": "MARISTELA TERTO DE HOLANDA", + "departamento": "DEPTO CIÊNCIAS DA COMPUTAÇÃO", + "formacao": "DOUTORADO", + "usuario": "83807519491", + "email": "mholanda@unb.br", + "ocupacao": "docente" + } + } +] diff --git a/spec/fixtures/files/turmas_tipo_errado.json b/spec/fixtures/files/turmas_tipo_errado.json new file mode 100755 index 0000000000..e2193e848e --- /dev/null +++ b/spec/fixtures/files/turmas_tipo_errado.json @@ -0,0 +1,413 @@ +[ + { + "code": "CIC0097", + "classCode": "TA", + "semester": "2021.2", + "dicente": [ + { + "nome": "Ana Clara Jordao Perna", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "KIM TAEYEON", + "usuario": "190084006", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "acjpjvjp@gmail.com" + }, + { + "nome": "Andre Carvalho de Roure", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "200033522", + "usuario": "200033522", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "andreCarvalhoroure@gmail.com" + }, + { + "nome": "André Carvalho Marques", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "150005491", + "usuario": "150005491", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "andre.acm97@outlook.com" + }, + { + "nome": "Antonio Vinicius de Moura Rodrigues", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190084502", + "usuario": "190084502", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "antoniovmoura.r@gmail.com" + }, + { + "nome": "Arthur Barreiros de Oliveira Mota", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190102829", + "usuario": "190102829", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "arthurbarreirosmota@gmail.com" + }, + { + "nome": "ARTHUR RODRIGUES NEVES", + "curso": "ENGENHARIA DE COMPUTAÇÃO/CIC", + "matricula": "202014403", + "usuario": "202014403", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "arthurcontroleambiental@gmail.com" + }, + { + "nome": "Bianca Glycia Boueri", + "curso": "ENGENHARIA MECATRÔNICA - CONTROLE E AUTOMAÇÃO/FTD", + "matricula": "170161561", + "usuario": "170161561", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "biancaglyciaboueri@gmail.com" + }, + { + "nome": "Caio Otávio Peluti Alencar", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190085312", + "usuario": "190085312", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "peluticaio@gmail.com" + }, + { + "nome": "Camila Frealdo Fraga", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "170007561", + "usuario": "170007561", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "camilizx2021@gmail.com" + }, + { + "nome": "Claudio Roberto Oliveira Peres de Barros", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190097591", + "usuario": "190097591", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "dinhobarros15@gmail.com" + }, + { + "nome": "Daltro Oliveira Vinuto", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "160025966", + "usuario": "160025966", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "daltroov777@gmail.com" + }, + { + "nome": "Davi de Moura Amaral", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "200016750", + "usuario": "200016750", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "davimouraamaral@gmail.com" + }, + { + "nome": "Eduardo Xavier Dantas", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190086530", + "usuario": "190086530", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "eduardoxdantas@gmail.com" + }, + { + "nome": "Enzo Nunes Leal Sampaio", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190062789", + "usuario": "190062789", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "enzonleal2016@hotmail.com" + }, + { + "nome": "Enzo Yoshio Niho", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190027304", + "usuario": "190027304", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "enzoyn@hotmail.com" + }, + { + "nome": "Gabriel Faustino Lima da Rocha", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190013249", + "usuario": "190013249", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "gabrielfaustino99@gmail.com" + }, + { + "nome": "Gabriel Ligoski", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190087498", + "usuario": "190087498", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "gabriel.ligoski@gmail.com" + }, + { + "nome": "GABRIEL MENDES CIRIATICO GUIMARÃES", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "202033202", + "usuario": "202033202", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "gabrielciriatico@gmail.com" + }, + { + "nome": "Gustavo Rodrigues dos Santos", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190014121", + "usuario": "190014121", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "190014121@aluno.unb.br" + }, + { + "nome": "Gustavo Rodrigues Gualberto", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190108266", + "usuario": "190108266", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "gustavorgualberto@gmail.com" + }, + { + "nome": "Igor David Morais", + "curso": "ENGENHARIA MECATRÔNICA - CONTROLE E AUTOMAÇÃO/FTD", + "matricula": "180102141", + "usuario": "180102141", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "igordavid13@gmail.com" + }, + { + "nome": "Jefte Augusto Gomes Batista", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "180057570", + "usuario": "180057570", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "ndaffte@gmail.com" + }, + { + "nome": "Karolina de Souza Silva", + "curso": "ENGENHARIA DE COMPUTAÇÃO/CIC", + "matricula": "190046791", + "usuario": "190046791", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "karolinasouza@outlook.com" + }, + { + "nome": "Kléber Rodrigues da Costa Júnior", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "200053680", + "usuario": "200053680", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "kleberrjr7@gmail.com" + }, + { + "nome": "Luca Delpino Barbabella", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "180125559", + "usuario": "180125559", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "barbadluca@gmail.com" + }, + { + "nome": "Lucas de Almeida Abreu Faria", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "170016668", + "usuario": "170016668", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "lucasaafaria@gmail.com" + }, + { + "nome": "Lucas Gonçalves Ramalho", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190098091", + "usuario": "190098091", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "lucasramalho29@gmail.com" + }, + { + "nome": "Lucas Monteiro Miranda", + "curso": "ENGENHARIA MECATRÔNICA - CONTROLE E AUTOMAÇÃO/FTD", + "matricula": "170149684", + "usuario": "170149684", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "luquinha_miranda@hotmail.com" + }, + { + "nome": "Lucas Resende Silveira Reis", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "180144421", + "usuario": "180144421", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "180144421@aluno.unb.br" + }, + { + "nome": "Luis Fernando Freitas Lamellas", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190016841", + "usuario": "190016841", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "lflamellas@icloud.com" + }, + { + "nome": "Luiza de Araujo Nunes Gomes", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190112794", + "usuario": "190112794", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "luizangomes@outlook.com" + }, + { + "nome": "Marcelo Aiache Postiglione", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "180126652", + "usuario": "180126652", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "180126652@aluno.unb.br" + }, + { + "nome": "Marcelo Junqueira Ferreira", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "200023624", + "usuario": "200023624", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "marcelojunqueiraf@gmail.com" + }, + { + "nome": "MARIA EDUARDA CARVALHO SANTOS", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190092556", + "usuario": "190092556", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "auntduda@gmail.com" + }, + { + "nome": "Maria Eduarda Lacerda Dantas", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "200067184", + "usuario": "200067184", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "lacwerda@gmail.com" + }, + { + "nome": "Maylla Krislainy de Sousa Silva", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190043873", + "usuario": "190043873", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "mayllak@hotmail.com" + }, + { + "nome": "Pedro Cesar Ribeiro Passos", + "curso": "ENGENHARIA MECATRÔNICA - CONTROLE E AUTOMAÇÃO/FTD", + "matricula": "180139312", + "usuario": "180139312", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "pedrocesarribeiro2013@gmail.com" + }, + { + "nome": "Rafael Mascarenhas Dal Moro", + "curso": "ENGENHARIA DE COMPUTAÇÃO/CIC", + "matricula": "170021041", + "usuario": "170021041", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "170021041@aluno.unb.br" + }, + { + "nome": "Rodrigo Mamedio Arrelaro", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190095164", + "usuario": "190095164", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "arrelaro1@hotmail.com" + }, + { + "nome": "Thiago de Oliveira Albuquerque", + "curso": "ENGENHARIA DE COMPUTAÇÃO/CIC", + "matricula": "140177442", + "usuario": "140177442", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "thiago.work.ti@outlook.com" + }, + { + "nome": "Thiago Elias dos Reis", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190126892", + "usuario": "190126892", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "thiagoeliasdosreis01@gmail.com" + }, + { + "nome": "Victor Hugo Rodrigues Fernandes", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "180132041", + "usuario": "180132041", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "aluno0sem.luz@gmail.com" + }, + { + "nome": "Vinicius Lima Passos", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "200028545", + "usuario": "200028545", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "viniciuslimapassos@gmail.com" + }, + { + "nome": "William Xavier dos Santos", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190075384", + "usuario": "190075384", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "wilxavier@me.com" + } + ], + "docente": { + "nome": "MARISTELA TERTO DE HOLANDA", + "departamento": "DEPTO CIÊNCIAS DA COMPUTAÇÃO", + "formacao": "DOUTORADO", + "usuario": "83807519491", + "email": "mholanda@unb.br", + "ocupacao": "docente" + } + } +] From 387a6833455aa2df80c3b80c75b381fb629da27c Mon Sep 17 00:00:00 2001 From: Carol <232050975@aluno.unb.br> Date: Sat, 29 Nov 2025 11:53:08 -0300 Subject: [PATCH 034/100] =?UTF-8?q?Adi=C3=A7=C3=A3o=20de=20menu=20lateral?= =?UTF-8?q?=20que=20pode=20ser=20recolhido=20ao=20clicar=20no=20=C3=ADcone?= =?UTF-8?q?=20perto=20do=20t=C3=ADtulo=20da=20navbar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/assets/stylesheets/application.css | 36 ++++++++--- app/views/admins/management.html.erb | 27 +++++---- app/views/admins/new_import.html.erb | 83 +++++++++++++------------- app/views/admins/results.html.erb | 31 +++++----- app/views/layouts/application.html.erb | 45 ++++++++------ 5 files changed, 127 insertions(+), 95 deletions(-) diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index a9bb2b5672..e4f0296c82 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -12,7 +12,8 @@ body { .admin-container { display: flex; - height: 100vh; + flex: 1; + height: calc(100vh - 60px); width: 100vw; } @@ -23,15 +24,29 @@ body { display: flex; flex-direction: column; flex-shrink: 0; + overflow-y: auto; + transition: width 0.3s ease; + white-space: nowrap; + overflow-x: hidden; } -.sidebar-header { - padding: 20px; - font-size: 1.1rem; - font-weight: bold; - display: flex; - align-items: center; - gap: 10px; +.sidebar.collapsed { + width: 0; + border: none; +} + +.sidebar.collapsed .menu-text { + display: none; +} + +.sidebar.collapsed .sidebar-header { + justify-content: center; + padding: 0; + height: 60px; +} + +.sidebar.collapsed .sidebar-menu a { + display: none; } .sidebar-menu { @@ -43,7 +58,7 @@ body { padding: 15px 20px; text-decoration: none; color: #333; - font-size: 0.9rem; + font-size: 1.0rem; } .sidebar-menu a.active { @@ -57,6 +72,7 @@ body { background-color: #e0e0e0; display: flex; flex-direction: column; + overflow-y: auto; } .top-navbar { @@ -67,6 +83,8 @@ body { justify-content: space-between; padding: 0 30px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); + z-index: 10; + flex-shrink: 0; } .nav-search { diff --git a/app/views/admins/management.html.erb b/app/views/admins/management.html.erb index ebd7d80e50..0d0a53242e 100644 --- a/app/views/admins/management.html.erb +++ b/app/views/admins/management.html.erb @@ -1,23 +1,24 @@ +
+
+ +

Gerenciamento

+
+ + +
+
-
\ No newline at end of file diff --git a/app/views/admins/results.html.erb b/app/views/admins/results.html.erb index 09e5235997..e350d810d4 100644 --- a/app/views/admins/results.html.erb +++ b/app/views/admins/results.html.erb @@ -1,21 +1,24 @@ -
- +
+
+ +

Resultados

+
-
-
-

Resultados

-
+
+ + +
+ +
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 173428e2b5..0ac7b9cf19 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -9,32 +9,23 @@ <%= 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 %> +
<% if notice %> -
- <%= notice %> -
+
<%= notice %>
<% end %> - <% if alert %> -
- <%= alert %> -
+
<%= alert %>
<% end %>
@@ -44,14 +35,30 @@ alerts.forEach(function(alertBox) { setTimeout(function() { alertBox.classList.add('fade-out'); - setTimeout(function() { - alertBox.style.display = 'none'; - }, 1000); - }, 3000); // 3 segundos na tela + setTimeout(function() { alertBox.style.display = 'none'; }, 1000); + }, 3000); }); } - document.addEventListener("turbo:load", setupAutoDismiss); - document.addEventListener("DOMContentLoaded", setupAutoDismiss); + + function setupSidebar() { + const toggleBtn = document.getElementById('sidebar-toggle'); + const sidebar = document.getElementById('sidebar'); + + if (toggleBtn && sidebar) { + toggleBtn.onclick = function() { + sidebar.classList.toggle('collapsed'); + }; + } + } + + document.addEventListener("turbo:load", () => { + setupAutoDismiss(); + setupSidebar(); + }); + document.addEventListener("DOMContentLoaded", () => { + setupAutoDismiss(); + setupSidebar(); + }); - + \ No newline at end of file From 6a8549b31aa260b126a4c3b41f2b61fff0617e32 Mon Sep 17 00:00:00 2001 From: Carol <232050975@aluno.unb.br> Date: Mon, 1 Dec 2025 18:08:51 -0300 Subject: [PATCH 035/100] =?UTF-8?q?Implementa=C3=A7=C3=A3o=20da=20US=20#12?= =?UTF-8?q?=20testado=20no=20Cucumber?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/admins_controller.rb | 13 ++++- app/controllers/application_controller.rb | 6 +++ features/admin/gerenciar_departamento.feature | 47 +++++++----------- features/step_definitions/results_steps.rb | 49 ++++++++++++++----- 4 files changed, 73 insertions(+), 42 deletions(-) diff --git a/app/controllers/admins_controller.rb b/app/controllers/admins_controller.rb index 370681f766..8c24d647cd 100644 --- a/app/controllers/admins_controller.rb +++ b/app/controllers/admins_controller.rb @@ -33,7 +33,18 @@ def create_import end def results - @turmas = Turma.all + user = current_user + + admin_dept_id = user&.departamento_id || 1 + + prefixo_departamento = case admin_dept_id + when 1 then "CIC" + when 2 then "MAT" + when 3 then "EST" + else "CIC" + end + + @turmas = Turma.where("nome LIKE ?", "%(#{prefixo_departamento}%") end def export_csv # TEM QUE MUDAR ESSA LÓGICA PARA GERAR O FORMULÁRIO REAL diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 0d95db22b4..9ccb8a5a45 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,4 +1,10 @@ class ApplicationController < ActionController::Base # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has. + def current_user + nil # Ninguém está logado + end + + helper_method :current_user + allow_browser versions: :modern end diff --git a/features/admin/gerenciar_departamento.feature b/features/admin/gerenciar_departamento.feature index fa70a7b6aa..566f0bed36 100644 --- a/features/admin/gerenciar_departamento.feature +++ b/features/admin/gerenciar_departamento.feature @@ -1,34 +1,23 @@ # language: pt -Funcionalidade: Visualização de resultados de avaliação restrita por departamento +Funcionalidade: Restrição de visualização de turmas por departamento + Como um Administrador + Quero gerenciar somente as turmas do departamento ao qual eu pertenço + A fim de focar a avaliação no meu contexto e não acessar dados indevidos. - Como um administrador - Eu quero gerenciar (ver e avaliar) somente as turmas do departamento ao qual eu pertenço - A fim de avaliar o desempenho das turmas no semestre atual + Contexto: + Dado que existe um departamento "CIC" (ID: 1) e um departamento "MAT" (ID: 2) + E que existe uma turma "BANCOS DE DADOS (CIC0097 - TA)" do departamento "CIC" + E que existe uma turma "CÁLCULO 1 (MAT0025 - A)" do departamento "MAT" - Cenário: Admin do CIC vê apenas as turmas do CIC - Dado que eu estou logado como "admin@sistema.com" - E sou professor do depatamento "CIC" - E eu navego para a página "Gerenciamento" e clico em "Resultados" - E existe uma turma "Bancos de Dados (CIC0097)" que pertence ao departamento "CIC" - E existe uma turma "CÁLCULO 1 (MAT0025)" que pertence ao departamento "MAT" - Quando eu olho a lista de turmas na tela "Gerenciamento - Resultados" - Então eu devo ver o bloco da turma "Bancos de Dados (CIC0097)" - E eu não devo ver o bloco da turma "CÁLCULO 1 (MAT0025)" + Cenário: Admin do CIC vê apenas turmas do CIC + Dado que eu estou logado como "admin.cic@unb.br" com perfil "Admin" e departamento "CIC" (ID: 1) + E eu navego para a página "Resultados" + Então eu devo ver a turma "BANCOS DE DADOS (CIC0097 - TA)" + E eu não devo ver a turma "CÁLCULO 1 (MAT0025 - A)" - Cenário: Admin do MAT vê apenas as turmas do MAT - Dado que eu estou logado como "admin@sistema.com" - E sou professor do depatamento "MAT" - E eu navego para a página "Gerenciamento" e clico em "Resultados" - E existe uma turma "Bancos de Dados (CIC0097)" que pertence ao departamento "CIC" - E existe uma turma "CÁLCULO 1 (MAT0025)" que pertence ao departamento "MAT" - Quando eu olho a lista de turmas na tela "Gerenciamento - Resultados" - Então eu não devo ver o bloco da turma "Bancos de Dados (CIC0097)" - E eu devo ver o bloco da turma "CÁLCULO 1 (MAT0025)" - - Cenário: Admin do CIC tenta acessar resultados do MAT diretamente pela URL - Dado que eu estou logado como "admin@sistema.com" - E sou professor do depatamento "CIC" - E existe a turma "Cálculo 1 (MAT0025)" (de id: 42) que pertence ao departamento "MAT" - Quando eu tento acessar a URL "/gerenciamento/resultados/42" diretamente no meu navegador - Então eu devo ser redirecionado para a minha página inicial (ou "Dashboard") \ No newline at end of file + Cenário: Admin da MAT vê apenas turmas da MAT + Dado que eu estou logado como "admin.mat@unb.br" com perfil "Admin" e departamento "MAT" (ID: 2) + E eu navego para a página "Resultados" + Então eu não devo ver a turma "BANCOS DE DADOS (CIC0097 - TA)" + E eu devo ver a turma "CÁLCULO 1 (MAT0025 - A)" \ No newline at end of file diff --git a/features/step_definitions/results_steps.rb b/features/step_definitions/results_steps.rb index 4e38e2fdc3..5aeffdd337 100644 --- a/features/step_definitions/results_steps.rb +++ b/features/step_definitions/results_steps.rb @@ -1,10 +1,6 @@ -# features/step_definitions/results_steps.rb - Dado('que existe uma turma {string} com {int} alunos') do |nome_turma, qtd_alunos| - # Cria a turma turma = Turma.create!(nome: nome_turma, semestre: "2021.2", is_active: true) - # Cria os alunos (Vínculos) qtd_alunos.times do |i| u = Usuario.create!( nome: "Aluno #{i}", @@ -26,17 +22,14 @@ end Quando('eu clico no botão "Baixar CSV" da turma {string}') do |nome_turma| - # Acha o bloco da turma pelo texto do título turma_element = find('h4', text: nome_turma).find(:xpath, '..') - # Clica no link dentro desse bloco within(turma_element) do click_link "Baixar CSV" end end Então('o download do arquivo deve ser iniciado') do - # Verifica se o cabeçalho da resposta indica um anexo CSV expect(page.response_headers['Content-Type']).to include('text/csv') end @@ -47,17 +40,12 @@ end Então('a página não deve fazer download') do - # Se não fez download, o content-type continua sendo HTML expect(page.response_headers['Content-Type']).to include('text/html') end -# --- Lógica de Acesso Negado --- - Dado('o meu perfil é {string}') do |perfil| - # Cria um objeto falso (Mock) que responde a .profile user_mock = double("User", profile: perfil) - # Injeta esse objeto falso no controller APENAS para este cenário allow_any_instance_of(AdminsController).to receive(:current_user).and_return(user_mock) end @@ -68,4 +56,41 @@ Então('eu devo ser redirecionado para a {string}') do |pagina| path = pagina == "Dashboard" ? '/gerenciamento' : pagina expect(current_path).to eq(path) +end + +Dado('que existe um departamento {string} \(ID: {int}) e um departamento {string} \(ID: {int})') do |d1, id1, d2, id2| +end + +Dado('que existe uma turma {string} do departamento {string}') do |nome_turma, departamento| + Turma.create!(nome: nome_turma, semestre: "2021.2", is_active: true) +end + +Dado('que eu estou logado como {string} com perfil {string} e departamento {string} \(ID: {int})') do |email, perfil, _dept_nome, dept_id| + + user_mock = double("User", profile: perfil, departamento_id: dept_id, email: email) + + allow_any_instance_of(AdminsController).to receive(:current_user).and_return(user_mock) + + allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(user_mock) +end + +Então('eu devo ver a turma {string}') do |nome_turma| + expect(page).to have_content(nome_turma) +end + +Então('eu não devo ver a turma {string}') do |nome_turma| + expect(page).not_to have_content(nome_turma) +end + +Quando('eu navego para a página {string}') do |nome_pagina| + case nome_pagina + when "Resultados" + visit '/resultados' + when "Gerenciamento", "Dashboard" + visit '/gerenciamento' + when "Importar Dados" + visit '/importar_sigaa' + else + raise "Caminho para a página '#{nome_pagina}' não foi definido nos steps." + end end \ No newline at end of file From f2f5dea1038c84dadefd119b1f8000f3f57ede31 Mon Sep 17 00:00:00 2001 From: mariosantos-05 Date: Sat, 6 Dec 2025 19:05:05 -0300 Subject: [PATCH 036/100] Modal novo formulario --- app/assets/stylesheets/application.css | 69 ++++++++++++++++--- app/controllers/admins/forms_controller.rb | 39 +++++++++++ .../controllers/modal_controller.js | 17 +++++ app/views/admins/forms/_form.html.erb | 33 +++++++++ app/views/admins/forms/new.html.erb | 10 +++ app/views/admins/management.html.erb | 7 +- config/routes.rb | 4 +- 7 files changed, 167 insertions(+), 12 deletions(-) create mode 100644 app/controllers/admins/forms_controller.rb create mode 100644 app/javascript/controllers/modal_controller.js create mode 100644 app/views/admins/forms/_form.html.erb create mode 100644 app/views/admins/forms/new.html.erb diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index e4f0296c82..891e71d0f4 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -5,7 +5,7 @@ body { margin: 0; padding: 0; - font-family: 'Arial', sans-serif; + font-family: "Arial", sans-serif; height: 100vh; overflow: hidden; } @@ -58,7 +58,7 @@ body { padding: 15px 20px; text-decoration: none; color: #333; - font-size: 1.0rem; + font-size: 1rem; } .sidebar-menu a.active { @@ -73,6 +73,7 @@ body { display: flex; flex-direction: column; overflow-y: auto; + position: relative; } .top-navbar { @@ -82,7 +83,7 @@ body { align-items: center; justify-content: space-between; padding: 0 30px; - box-shadow: 0 2px 4px rgba(0,0,0,0.05); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); z-index: 10; flex-shrink: 0; } @@ -115,7 +116,7 @@ body { } .content-area { - flex: 1; + flex: 1; display: flex; justify-content: center; align-items: center; @@ -126,7 +127,7 @@ body { width: 450px; padding: 40px; border-radius: 8px; - box-shadow: 0 4px 10px rgba(0,0,0,0.1); + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); display: flex; flex-direction: column; gap: 15px; @@ -150,13 +151,17 @@ body { background-color: #2ecc71; color: white; } -.btn-primary-green:hover { background-color: #27ae60; } +.btn-primary-green:hover { + background-color: #27ae60; +} .btn-secondary-green { background-color: #8ceabb; color: white; } -.btn-secondary-green:hover { background-color: #7bd3a6; } +.btn-secondary-green:hover { + background-color: #7bd3a6; +} .flash-container { position: fixed; @@ -177,7 +182,7 @@ body { text-align: center; font-size: 0.85rem; font-weight: bold; - box-shadow: 0 2px 5px rgba(0,0,0,0.2); + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); opacity: 1; transition: opacity 1s ease-out, visibility 1s ease-out; } @@ -197,4 +202,50 @@ body { .fade-out { opacity: 0; visibility: hidden; -} \ No newline at end of file +} + +.modal-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + + background: rgba(0, 0, 0, 0.35); + + display: flex; + justify-content: center; + align-items: flex-start; + + padding-top: 40px; + + z-index: 20; +} + +.modal-window { + background: white; + padding: 25px; + border-radius: 12px; + width: 600px; + max-width: 90%; + box-shadow: 0 4px 25px rgba(0, 0, 0, 0.2); +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; +} + +.modal-title { + font-size: 1.4rem; +} + +.modal-close { + border: none; + background: none; + font-size: 28px; + cursor: pointer; + color: #555; +} diff --git a/app/controllers/admins/forms_controller.rb b/app/controllers/admins/forms_controller.rb new file mode 100644 index 0000000000..64ac732a89 --- /dev/null +++ b/app/controllers/admins/forms_controller.rb @@ -0,0 +1,39 @@ +require "ostruct" + +class Admins::FormsController < ApplicationController + def new + # Load real templates + @templates = Template.all + + # If none exist yet, create mock temporary objects (NOT saved in DB) + if @templates.empty? + @templates = [ + OpenStruct.new(id: 1, titulo: "Template de Avaliação - Geral"), + OpenStruct.new(id: 2, titulo: "Template de Professores"), + OpenStruct.new(id: 3, titulo: "Template de Autoavaliação") + ] + end + + @turmas = Turma.where(is_active: true) + end + + def create + template_id = params[:template_id] + turma_ids = params[:turma_ids] || [] + + if template_id.blank? || turma_ids.empty? + redirect_to new_admin_form_path, alert: "Selecione um template e pelo menos uma turma." + return + end + + turma_ids.each do |tid| + Form.create!( + template_id: template_id, + turma_id: tid, + is_active: true + ) + end + + redirect_to admin_management_path, notice: "Formulários criados com sucesso!" + end +end diff --git a/app/javascript/controllers/modal_controller.js b/app/javascript/controllers/modal_controller.js new file mode 100644 index 0000000000..416f854544 --- /dev/null +++ b/app/javascript/controllers/modal_controller.js @@ -0,0 +1,17 @@ +import { Controller } from "@hotwired/stimulus"; + +export default class extends Controller { + close() { + this.element.remove(); + } + + closeBackground(event) { + if (event.target === this.element) { + this.close(); + } + } + + stop(event) { + event.stopPropagation(); + } +} diff --git a/app/views/admins/forms/_form.html.erb b/app/views/admins/forms/_form.html.erb new file mode 100644 index 0000000000..9a1bd7acc2 --- /dev/null +++ b/app/views/admins/forms/_form.html.erb @@ -0,0 +1,33 @@ + + + \ No newline at end of file diff --git a/app/views/admins/forms/new.html.erb b/app/views/admins/forms/new.html.erb new file mode 100644 index 0000000000..efb8a92e6a --- /dev/null +++ b/app/views/admins/forms/new.html.erb @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/views/admins/management.html.erb b/app/views/admins/management.html.erb index 0d0a53242e..a91137920a 100644 --- a/app/views/admins/management.html.erb +++ b/app/views/admins/management.html.erb @@ -14,7 +14,7 @@ @@ -23,9 +23,12 @@
<%= link_to "Importar dados", import_sigaa_path, class: "btn-action btn-primary-green" %> - + <%= link_to "Criar Formulário", new_admins_form_path, data: { turbo_frame: "modal" }, class: "btn-action btn-secondary-green" %> <%= link_to "Resultados", admin_results_path, class: "btn-action btn-primary-green" %>
+
+ +
\ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 5fde70fb3b..8119865189 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -11,7 +11,9 @@ get 'resultados', to: 'admins#results', as: 'admin_results' get 'resultados/:turma_id/csv', to: 'admins#export_csv', as: 'export_results_csv' - + namespace :admins do + resources :forms, only: [:new, :create] + end # Render dynamic PWA files from app/views/pwa/* (remember to link manifest in application.html.erb) # get "manifest" => "rails/pwa#manifest", as: :pwa_manifest From 748d7369b9639b78b9ea79ac561bd55a80fcb8d1 Mon Sep 17 00:00:00 2001 From: mariosantos-05 Date: Sat, 6 Dec 2025 19:10:33 -0300 Subject: [PATCH 037/100] Fix box size --- app/assets/stylesheets/application.css | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 891e71d0f4..04ac960d0b 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -1,5 +1,5 @@ * { - box_sizing: border-box; + box-sizing: border-box; } body { @@ -174,7 +174,6 @@ body { } .flash-message { - width: auto; min-width: 200px; max-width: 280px; padding: 10px 15px; From 44d75a5224097bac6e7c35e6e6e5c1a2f6cd5113 Mon Sep 17 00:00:00 2001 From: mariosantos-05 Date: Sat, 6 Dec 2025 20:10:21 -0300 Subject: [PATCH 038/100] Css update --- app/assets/stylesheets/application.css | 114 ++++++++++++++++++++++--- app/views/admins/forms/_form.html.erb | 43 ++++++---- app/views/admins/management.html.erb | 2 +- 3 files changed, 130 insertions(+), 29 deletions(-) diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 04ac960d0b..bc39df47cd 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -214,31 +214,31 @@ body { display: flex; justify-content: center; - align-items: flex-start; - - padding-top: 40px; + align-items: center; z-index: 20; } .modal-window { background: white; - padding: 25px; - border-radius: 12px; - width: 600px; - max-width: 90%; - box-shadow: 0 4px 25px rgba(0, 0, 0, 0.2); + padding: 40px; + border-radius: 16px; + width: 750px; + max-width: 95%; + box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12); } -.modal-header { +.template-row { display: flex; - justify-content: space-between; align-items: center; - margin-bottom: 20px; + width: 100%; } -.modal-title { - font-size: 1.4rem; +.template-center { + margin: 0 auto; /* pushes it to the center */ + display: flex; + align-items: center; + gap: 8px; } .modal-close { @@ -248,3 +248,91 @@ body { cursor: pointer; color: #555; } + +.turmas-table { + width: 90%; + margin: 0 auto; + display: flex; + flex-direction: column; + gap: 10px; +} + +.turmas-table-header { + font-weight: bold; + font-size: 0.95rem; + display: grid; + grid-template-columns: 40px 1fr 120px; + color: #666; + width: 100%; + padding-bottom: 8px; + border-bottom: 1px solid #eee; +} + +.turmas-row { + display: grid; + grid-template-columns: 40px 1fr 120px; + align-items: center; + padding: 10px 0; + border-bottom: 1px solid #f1f1f1; +} + +.turmas-col { + display: flex; + justify-content: center; + align-items: center; +} + +.turma-checkbox { + width: 20px; + height: 20px; + accent-color: #2ecc71; + cursor: pointer; +} + +.submit-row { + display: flex; + justify-content: center; + margin-top: 20px; +} + +.submit-btn { + width: 125px; + padding: 12px 0; + background: #2ecc71; + color: white; + font-size: 1.1rem; + border-radius: 10px; + border: none; + cursor: pointer; + font-weight: bold; +} + +.submit-btn:hover { + background: #27ae60; +} + +.select-template { + padding: 8px 12px; + font-size: 15px; + border: 1px solid #ccc; + border-radius: 6px; + + background-color: white; + color: #333; + + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + + background-image: url('data:image/svg+xml;utf8,'); + background-repeat: no-repeat; + background-position: right 10px center; + background-size: 14px; + padding-right: 32px; +} + +.select-template:focus { + border-color: #888; + outline: none; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.15); +} diff --git a/app/views/admins/forms/_form.html.erb b/app/views/admins/forms/_form.html.erb index 9a1bd7acc2..02a5327632 100644 --- a/app/views/admins/forms/_form.html.erb +++ b/app/views/admins/forms/_form.html.erb @@ -1,32 +1,45 @@ + \ No newline at end of file From c519e42f732d3ae2b4637395e0ec748e983fc328 Mon Sep 17 00:00:00 2001 From: mariosantos-05 Date: Sun, 7 Dec 2025 00:08:39 -0300 Subject: [PATCH 042/100] Rotas para responder formulario --- app/controllers/avaliacoes_controller.rb | 12 ++++++++ app/views/avaliacoes/index.html.erb | 8 +++-- app/views/avaliacoes/responder.html.erb | 39 ++++++++++++++++++++++++ config/routes.rb | 18 +++-------- 4 files changed, 61 insertions(+), 16 deletions(-) create mode 100644 app/views/avaliacoes/responder.html.erb diff --git a/app/controllers/avaliacoes_controller.rb b/app/controllers/avaliacoes_controller.rb index 6e2fae4292..442a193eca 100644 --- a/app/controllers/avaliacoes_controller.rb +++ b/app/controllers/avaliacoes_controller.rb @@ -18,6 +18,18 @@ def index ] end + + def responder + @form_id = params[:id] + + # mock form info + @fake_form = OpenStruct.new( + id: @form_id, + titulo: "Pesquisa de Satisfação - Exemplo", + turma_nome: "Turma Mockada" + ) + end + private def require_user diff --git a/app/views/avaliacoes/index.html.erb b/app/views/avaliacoes/index.html.erb index 883b67fa45..534b3bcc6f 100644 --- a/app/views/avaliacoes/index.html.erb +++ b/app/views/avaliacoes/index.html.erb @@ -38,9 +38,11 @@

<% turma.forms.each do |form| %> - - Responder: <%= form.titulo %> - + <%= link_to responder_form_path(turma.id, form.id), + class: "btn-action btn-primary-green", + style: "margin-top: 8px; display: inline-block; padding: 8px 15px; font-size: 0.9rem;" do %> + Responder: <%= form.titulo %> + <% end %> <% end %>
diff --git a/app/views/avaliacoes/responder.html.erb b/app/views/avaliacoes/responder.html.erb new file mode 100644 index 0000000000..b92e34ad86 --- /dev/null +++ b/app/views/avaliacoes/responder.html.erb @@ -0,0 +1,39 @@ +
+
+ +

+ Responder Formulário +

+
+ + +
+ +
+ + +
+
+ +

<%= @fake_form.titulo %>

+

Turma: <%= @fake_form.turma_nome %>

+ +
+ +

+ Aqui futuramente será exibido o formulário real. +

+ + + +
+
+
\ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index b914debdd6..e14f983484 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,9 +1,4 @@ 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 'gerenciamento', to: 'admins#management', as: 'admin_management' get 'importar_sigaa', to: 'admins#new_import', as: 'import_sigaa' post 'importar_sigaa', to: 'admins#create_import' @@ -15,13 +10,10 @@ resources :forms, only: [:new, :create] end - get "avaliacoes", to: "avaliacoes#index", as: "avaliacoes" - - - # 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 + # NEW — route to render the form answering page + get "avaliacoes/:turma_id/forms/:form_id/responder", + to: "avaliacoes#responder", + as: "responder_form" - # Defines the root path route ("/") - # root "posts#index" + get "avaliacoes", to: "avaliacoes#index", as: "avaliacoes" end From 8e1b2bb92f7dedb579ffbbc59bd8e25a26840e5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Sun, 7 Dec 2025 17:23:51 -0300 Subject: [PATCH 043/100] Update routes.rb Crud --- config/routes.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/routes.rb b/config/routes.rb index 5fde70fb3b..ca3be03b01 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -11,7 +11,8 @@ get 'resultados', to: 'admins#results', as: 'admin_results' get 'resultados/:turma_id/csv', to: 'admins#export_csv', as: 'export_results_csv' - + resources :templates + resources :forms, only: [:new, :create] # Render dynamic PWA files from app/views/pwa/* (remember to link manifest in application.html.erb) # get "manifest" => "rails/pwa#manifest", as: :pwa_manifest From 6c1263c83f3747808e4c1fc81d80b8b6697b6b71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Sun, 7 Dec 2025 17:30:10 -0300 Subject: [PATCH 044/100] Update template.rb update --- app/models/template.rb | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/app/models/template.rb b/app/models/template.rb index 9db120bdbf..ffdfb18bb1 100644 --- a/app/models/template.rb +++ b/app/models/template.rb @@ -1,3 +1,23 @@ class Template < ApplicationRecord - belongs_to :criado_por + belongs_to :criado_por, class_name: 'Usuario' + has_many :forms, dependent: :restrict_with_error + + attr_accessor :questions + + # Validações cenários tristes do BDD + validates :titulo, presence: { message: "O campo Título é obrigatório" } + validates :titulo, format: { + with: /\A[a-zA-Z0-9\s\-\.]+\z/, + message: "Formato de título inválido" + } + + validate :must_have_questions + + private + + def must_have_questions + if questions.blank? || (questions.respond_to?(:empty?) && questions.empty?) + errors.add(:base, "O template deve conter pelo menos uma questão") + end + end end From baa93d7ea59dcc85559a58a59d42fa0f3b8f847b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Sun, 7 Dec 2025 17:32:51 -0300 Subject: [PATCH 045/100] Create templates_controller.rb MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit controlador responsável pelo crud de templates --- app/controllers/templates_controller.rb | 62 +++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 app/controllers/templates_controller.rb diff --git a/app/controllers/templates_controller.rb b/app/controllers/templates_controller.rb new file mode 100644 index 0000000000..40f7e25251 --- /dev/null +++ b/app/controllers/templates_controller.rb @@ -0,0 +1,62 @@ +class TemplatesController < ApplicationController + + def index + @templates = Template.all + # Cenário: Visualizar lista vazia + if @templates.empty? + flash.now[:notice] = "Nenhum template foi criado" + end + end + + def new + @template = Template.new + end + + def create + @template = Template.new(template_params) + # Atribui o usuário logado + @template.criado_por = current_user + + # Simula questões vindas do form + @template.questions = params[:template][:questions] + + if @template.save + redirect_to templates_path, notice: "Template criado com sucesso" + else + # Cenários tristes: Título vazio ou Sem questões + flash.now[:alert] = @template.errors.full_messages.join(", ") + render :new, status: :unprocessable_entity + end + end + + def edit + @template = Template.find(params[:id]) + end + + def update + @template = Template.find(params[:id]) + + # Simula questões + @template.questions = ["mock_question"] + + if @template.update(template_params) + redirect_to templates_path, notice: "Template atualizado com sucesso" + else + # Cenário Triste: Caracteres inválidos + flash.now[:alert] = @template.errors.full_messages.first + render :edit, status: :unprocessable_entity + end + end + + def destroy + @template = Template.find(params[:id]) + @template.destroy + redirect_to templates_path, notice: "Template removido com sucesso" + end + + private + + def template_params + params.require(:template).permit(:titulo, :target_audience) + end +end From 06fcb39a6b4b726b9c34df29f1ab510c110d879e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Sun, 7 Dec 2025 17:34:46 -0300 Subject: [PATCH 046/100] Create forms_controller.rb MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit lógica de criação de formulários para públicos específicos --- app/controllers/forms_controller.rb | 31 +++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 app/controllers/forms_controller.rb diff --git a/app/controllers/forms_controller.rb b/app/controllers/forms_controller.rb new file mode 100644 index 0000000000..eb878195f3 --- /dev/null +++ b/app/controllers/forms_controller.rb @@ -0,0 +1,31 @@ +class FormsController < ApplicationController + def new + @form = Form.new + end + + def create + # Cenário Triste: Tentativa de criar sem selecionar público + if params[:target_audience].blank? + redirect_to new_form_path, alert: "Selecione o público-alvo da avaliação" + return + end + + # Lógica para Docentes (sem turma) e Discentes (com turma) + if params[:target_audience] == "Discentes" && params[:turma_id].blank? + #BDD foca no público-alvo no cenário triste simples + end + + # Criação simulada + @form = Form.new( + template_id: params[:template_id], + turma_id: params[:turma_id], + is_active: true + ) + + if @form.save + redirect_to forms_path, notice: "Formulário enviado com sucesso" + else + render :new + end + end +end From af345f9f6b254d4f7b1a49464bbd9f01880d926b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Sun, 7 Dec 2025 17:43:05 -0300 Subject: [PATCH 047/100] Update template_spec.rb testes de unidade --- spec/models/template_spec.rb | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/spec/models/template_spec.rb b/spec/models/template_spec.rb index 068ef0ee3c..031b666ef6 100644 --- a/spec/models/template_spec.rb +++ b/spec/models/template_spec.rb @@ -1,5 +1,27 @@ require 'rails_helper' RSpec.describe Template, type: :model do - pending "add some examples to (or delete) #{__FILE__}" + # Testes de unidade + + context "Validações" do + it "é inválido sem título (Cenário Triste)" do + template = Template.new(titulo: nil) + template.valid? + expect(template.errors[:titulo]).to include("O campo Título é obrigatório") + end + + it "é inválido com caracteres especiais no título (Cenário Triste)" do + template = Template.new(titulo: "Titulo@Invalido") + template.valid? + expect(template.errors[:titulo]).to include("Formato de título inválido") + end + + it "é inválido sem questões (Cenário Triste)" do + template = Template.new(titulo: "Valido") + # Simulando ausência de questões + template.questions = [] + template.valid? + expect(template.errors[:base]).to include("O template deve conter pelo menos uma questão") + end + end end From 35ce28011dd1b9bbcb02e2d7d269467b3a46fda6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Sun, 7 Dec 2025 17:46:46 -0300 Subject: [PATCH 048/100] Create templates_spec.rb teste com mock --- spec/requests/templates_spec.rb | 125 ++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 spec/requests/templates_spec.rb diff --git a/spec/requests/templates_spec.rb b/spec/requests/templates_spec.rb new file mode 100644 index 0000000000..df0fc3dd25 --- /dev/null +++ b/spec/requests/templates_spec.rb @@ -0,0 +1,125 @@ +require 'rails_helper' + +RSpec.describe "Templates", type: :request do + let(:admin) { double("Usuario", id: 1, profile: 'Admin') } + + before do + allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(admin) + end + + describe "GET /templates (Visualização)" do + context "Cenário Feliz: Visualizar lista de templates existentes" do + it "exibe a lista de templates" do + templates = [double("Template", titulo: "Avaliação A"), double("Template", titulo: "Avaliação B")] + allow(Template).to receive(:all).and_return(templates) + allow(templates).to receive(:empty?).and_return(false) + + get templates_path + + expect(response).to have_http_status(200) + expect(response.body).to include("Avaliação A") + end + end + + context "Cenário Triste: Visualizar lista vazia" do + it "exibe mensagem que nenhum template foi criado" do + allow(Template).to receive(:all).and_return([]) + + get templates_path + + expect(flash[:notice]).to eq("Nenhum template foi criado") + end + end + end + + describe "POST /templates (Criação)" do + context "Cenário Feliz: Criação com dados válidos" do + it "cria o template e redireciona" do + template_params = { titulo: "Avaliação 2024", questions: ["q1", "q2"] } + template_mock = double("Template", save: true) + + allow(Template).to receive(:new).and_return(template_mock) + allow(template_mock).to receive(:criado_por=) + allow(template_mock).to receive(:questions=) + + post templates_path, params: { template: template_params } + + expect(flash[:notice]).to eq("Template criado com sucesso") + expect(response).to redirect_to(templates_path) + end + end + + context "Cenário Triste: Tentativa de criação com título vazio" do + it "não salva e exibe erro" do + template_mock = double("Template", save: false, errors: double(full_messages: ["O campo Título é obrigatório"])) + + allow(Template).to receive(:new).and_return(template_mock) + allow(template_mock).to receive(:criado_por=) + allow(template_mock).to receive(:questions=) + + post templates_path, params: { template: { titulo: "" } } + + expect(flash[:alert]).to include("O campo Título é obrigatório") + expect(response).to have_http_status(:unprocessable_entity) + end + end + + context "Cenário Triste: Tentativa de criação sem questões" do + it "não salva e exibe erro de questões" do + template_mock = double("Template", save: false, errors: double(full_messages: ["O template deve conter pelo menos uma questão"])) + + allow(Template).to receive(:new).and_return(template_mock) + allow(template_mock).to receive(:criado_por=) + allow(template_mock).to receive(:questions=) + + post templates_path, params: { template: { titulo: "Título Válido", questions: [] } } + + expect(flash[:alert]).to include("O template deve conter pelo menos uma questão") + end + end + end + + describe "PUT /templates/:id (Edição)" do + let(:template_mock) { double("Template", id: 1) } + + before do + allow(Template).to receive(:find).with("1").and_return(template_mock) + allow(template_mock).to receive(:questions=) + end + + context "Cenário Feliz: Edição de nome" do + it "atualiza o template" do + expect(template_mock).to receive(:update).with(hash_including("titulo" => "Avaliação A - Revisada")).and_return(true) + + put template_path(1), params: { template: { titulo: "Avaliação A - Revisada" } } + + expect(flash[:notice]).to eq("Template atualizado com sucesso") + expect(response).to redirect_to(templates_path) + end + end + + context "Cenário Triste: Edição com caracteres inválidos" do + it "não salva e exibe erro" do + expect(template_mock).to receive(:update).and_return(false) + allow(template_mock).to receive(:errors).and_return(double(full_messages: ["Formato de título inválido"])) + + put template_path(1), params: { template: { titulo: "Inválido$$$" } } + + expect(flash[:alert]).to eq("Formato de título inválido") + end + end + end + + describe "DELETE /templates/:id (Deleção)" do + it "Cenário Feliz: remove o template" do + template_mock = double("Template", id: 1) + allow(Template).to receive(:find).with("1").and_return(template_mock) + expect(template_mock).to receive(:destroy) + + delete template_path(1) + + expect(flash[:notice]).to eq("Template removido com sucesso") + expect(response).to redirect_to(templates_path) + end + end +end From 79f184bc4a62bea91ba7ca1acfb013b6e1ef6428 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Sun, 7 Dec 2025 17:49:48 -0300 Subject: [PATCH 049/100] Create forms_creation_spec.rb MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit verificação de criação de formulários --- spec/requests/forms_creation_spec.rb | 34 ++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 spec/requests/forms_creation_spec.rb diff --git a/spec/requests/forms_creation_spec.rb b/spec/requests/forms_creation_spec.rb new file mode 100644 index 0000000000..9c07ceabe5 --- /dev/null +++ b/spec/requests/forms_creation_spec.rb @@ -0,0 +1,34 @@ +require 'rails_helper' + +RSpec.describe "Form Creation", type: :request do + describe "POST /forms" do + context "Cenário Feliz: Enviar para Discentes com turma" do + it "envia formulário com sucesso" do + + allow_any_instance_of(Form).to receive(:save).and_return(true) + post forms_path, params: { target_audience: "Discentes", turma_id: "1", template_id: "1" } + expect(response).to redirect_to(forms_path) + + end + end + + context "Cenário Feliz: Enviar para Docentes" do + it "envia formulário ignorando turma se necessário" do + allow_any_instance_of(Form).to receive(:save).and_return(true) + + post forms_path, params: { target_audience: "Docentes", template_id: "1" } + + expect(response).to redirect_to(forms_path) + end + end + + context "Cenário Triste: Tentativa de criar sem selecionar público" do + it "exibe erro e não envia" do + post forms_path, params: { target_audience: "" } + + expect(response).to redirect_to(new_form_path) + expect(flash[:alert]).to eq("Selecione o público-alvo da avaliação") + end + end + end +end From 363cd040581c5e17fefdfdbe14dcb8a156d5cefa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Sun, 7 Dec 2025 18:02:39 -0300 Subject: [PATCH 050/100] Update template_spec.rb MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit testa validações --- spec/models/template_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/models/template_spec.rb b/spec/models/template_spec.rb index 031b666ef6..dff50fb01d 100644 --- a/spec/models/template_spec.rb +++ b/spec/models/template_spec.rb @@ -1,7 +1,6 @@ require 'rails_helper' RSpec.describe Template, type: :model do - # Testes de unidade context "Validações" do it "é inválido sem título (Cenário Triste)" do From e362c53ce9f2aaa4455b50ae1f800772a9f1e172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Sun, 7 Dec 2025 18:05:23 -0300 Subject: [PATCH 051/100] Update template.rb MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit validações de template --- app/models/template.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/models/template.rb b/app/models/template.rb index ffdfb18bb1..c250768421 100644 --- a/app/models/template.rb +++ b/app/models/template.rb @@ -4,7 +4,6 @@ class Template < ApplicationRecord attr_accessor :questions - # Validações cenários tristes do BDD validates :titulo, presence: { message: "O campo Título é obrigatório" } validates :titulo, format: { with: /\A[a-zA-Z0-9\s\-\.]+\z/, From 3f510572ef76a7cd0b0cf23e633a4a53a09ecf3d Mon Sep 17 00:00:00 2001 From: mariosantos-05 Date: Sun, 7 Dec 2025 19:27:51 -0300 Subject: [PATCH 052/100] =?UTF-8?q?cria=C3=A7=C3=A3o=20e=20visualiza=C3=A7?= =?UTF-8?q?=C3=A3o=20de=20forms=20conectada=20ao=20BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Gemfile | 3 + Gemfile.lock | 6 ++ ...rio = Usuario.find_by(matricula: \"111\")" | 102 ++++++++++++++++++ app/controllers/admins/forms_controller.rb | 77 ++++++------- app/controllers/avaliacoes_controller.rb | 43 ++++---- app/models/template.rb | 2 +- app/models/turma.rb | 1 + ...207213610_fix_foreign_key_for_templates.rb | 9 ++ db/schema.rb | 57 +++++++++- lib/tasks/development.rake | 26 +++++ spec/factories/form.rb | 7 ++ spec/factories/template.rb | 7 ++ spec/factories/turma.rb | 7 ++ spec/factories/usuarios.rb | 19 ++++ spec/factories/vinculo.rb | 7 ++ spec/models/form_spec.rb | 16 ++- spec/rails_helper.rb | 4 + 17 files changed, 324 insertions(+), 69 deletions(-) create mode 100644 "Usuario = Usuario.find_by(matricula: \"111\")" create mode 100644 db/migrate/20251207213610_fix_foreign_key_for_templates.rb create mode 100644 lib/tasks/development.rake create mode 100644 spec/factories/form.rb create mode 100644 spec/factories/template.rb create mode 100644 spec/factories/turma.rb create mode 100644 spec/factories/usuarios.rb create mode 100644 spec/factories/vinculo.rb diff --git a/Gemfile b/Gemfile index 8453384cc3..8afb0bc6d5 100644 --- a/Gemfile +++ b/Gemfile @@ -50,6 +50,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" + end group :development do diff --git a/Gemfile.lock b/Gemfile.lock index 448b2e49ec..25d780bdcd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -145,6 +145,11 @@ GEM erubi (1.13.1) et-orbi (1.4.0) tzinfo + factory_bot (6.5.6) + activesupport (>= 6.1.0) + factory_bot_rails (6.5.1) + factory_bot (~> 6.5) + railties (>= 6.1.0) ffi (1.17.2-aarch64-linux-gnu) ffi (1.17.2-aarch64-linux-musl) ffi (1.17.2-arm-linux-gnu) @@ -446,6 +451,7 @@ DEPENDENCIES cucumber-rails database_cleaner-active_record debug + factory_bot_rails importmap-rails jbuilder kamal diff --git "a/Usuario = Usuario.find_by(matricula: \"111\")" "b/Usuario = Usuario.find_by(matricula: \"111\")" new file mode 100644 index 0000000000..ec953869b9 --- /dev/null +++ "b/Usuario = Usuario.find_by(matricula: \"111\")" @@ -0,0 +1,102 @@ +=> +[#, + #, + #, + #, + #, + #, + #, + #, + #, + #, + "..."] diff --git a/app/controllers/admins/forms_controller.rb b/app/controllers/admins/forms_controller.rb index 3854461f48..78bc244542 100644 --- a/app/controllers/admins/forms_controller.rb +++ b/app/controllers/admins/forms_controller.rb @@ -1,60 +1,47 @@ -require "ostruct" - class Admins::FormsController < ApplicationController def new - # Load real templates + # Carrega todos os templates do banco (incluindo o template de teste se estiver vazio) @templates = Template.all - # If none exist yet, create mock temporary objects (NOT saved in DB) - if @templates.empty? - @templates = [ - OpenStruct.new(id: 1, titulo: "Template de Avaliação - Geral"), - OpenStruct.new(id: 2, titulo: "Template de Professores"), - OpenStruct.new(id: 3, titulo: "Template de Autoavaliação") - ] - end - + # Carrega turmas ativas @turmas = Turma.where(is_active: true) end def create - template_id = params[:template_id] - turma_ids = params[:turma_ids] || [] - - if template_id.blank? || turma_ids.empty? - flash.now[:alert] = "Selecione um template e pelo menos uma turma." - - respond_to do |format| - format.turbo_stream do - render turbo_stream: [ - turbo_stream.remove("modal"), - turbo_stream.replace( - "flash_messages", - partial: "shared/flash" - ) - ] + template_id = params[:template_id] + turma_ids = params[:turma_ids] || [] + + if template_id.blank? || turma_ids.empty? + flash.now[:alert] = "Selecione um template e pelo menos uma turma." + + respond_to do |format| + format.turbo_stream do + render turbo_stream: [ + turbo_stream.remove("modal"), + turbo_stream.replace( + "flash_messages", + partial: "shared/flash" + ) + ] + end + + format.html do + redirect_to admin_management_path, alert: "Selecione um template e pelo menos uma turma." + end end - format.html do - redirect_to admin_management_path, alert: "Selecione um template e pelo menos uma turma." - end + return end - return - end + # Cria formulários para cada turma selecionada + turma_ids.each do |tid| + Form.create!( + template_id: template_id, + turma_id: tid, + is_active: true + ) + end - # SUCCESS CASE - turma_ids.each do |tid| - Form.create!( - template_id: template_id, - turma_id: tid, - is_active: true - ) + redirect_to admin_management_path, notice: "Formulários criados com sucesso!" end - - redirect_to admin_management_path, notice: "Formulários criados com sucesso!" -end - - - end diff --git a/app/controllers/avaliacoes_controller.rb b/app/controllers/avaliacoes_controller.rb index 442a193eca..470cbd1837 100644 --- a/app/controllers/avaliacoes_controller.rb +++ b/app/controllers/avaliacoes_controller.rb @@ -1,32 +1,39 @@ require "ostruct" - class AvaliacoesController < ApplicationController before_action :require_user def index - # Mock classes - @turmas = [ - OpenStruct.new( - id: 1, - nome: "Cálculo I", - forms: [ - OpenStruct.new(id: 11, titulo: "Avaliação do Professor"), - OpenStruct.new(id: 12, titulo: "Avaliação da Disciplina") - ] - ), + # Pega turmas que o usuário participa + @turmas = current_user.vinculos.includes(turma: :forms).map(&:turma) - ] + # Carrega os forms ativos de cada turma + @turmas = @turmas.map do |turma| + OpenStruct.new( + id: turma.id, + nome: turma.nome, + forms: turma.forms.where(is_active: true).map do |form| + OpenStruct.new( + id: form.id, + titulo: form.template.titulo + ) + end + ) + end end - def responder - @form_id = params[:id] + @form = Form.includes(:template, :turma).find(params[:form_id]) + + # Apenas permite acessar se o usuário faz parte da turma + unless current_user.vinculos.exists?(turma_id: @form.turma_id) + redirect_to avaliacoes_path, alert: "Você não tem acesso a esse formulário." + return + end - # mock form info @fake_form = OpenStruct.new( - id: @form_id, - titulo: "Pesquisa de Satisfação - Exemplo", - turma_nome: "Turma Mockada" + id: @form.id, + titulo: @form.template.titulo, + turma_nome: @form.turma.nome ) end diff --git a/app/models/template.rb b/app/models/template.rb index 9db120bdbf..b4be2f712a 100644 --- a/app/models/template.rb +++ b/app/models/template.rb @@ -1,3 +1,3 @@ class Template < ApplicationRecord - belongs_to :criado_por + belongs_to :criado_por, class_name: "Usuario" end diff --git a/app/models/turma.rb b/app/models/turma.rb index 6b10422401..184e29d81b 100644 --- a/app/models/turma.rb +++ b/app/models/turma.rb @@ -1,4 +1,5 @@ class Turma < ApplicationRecord + has_many :forms has_many :vinculos, dependent: :destroy has_many :usuarios, through: :vinculos end diff --git a/db/migrate/20251207213610_fix_foreign_key_for_templates.rb b/db/migrate/20251207213610_fix_foreign_key_for_templates.rb new file mode 100644 index 0000000000..4608c2251b --- /dev/null +++ b/db/migrate/20251207213610_fix_foreign_key_for_templates.rb @@ -0,0 +1,9 @@ +class FixForeignKeyForTemplates < ActiveRecord::Migration[8.0] + def change + # Remove foreign key incorreta + remove_foreign_key :templates, column: :criado_por_id rescue nil + + # Adiciona foreign key correta + add_foreign_key :templates, :usuarios, column: :criado_por_id + end +end diff --git a/db/schema.rb b/db/schema.rb index bce5668f83..67b292306c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_11_28_134249) do +ActiveRecord::Schema[8.0].define(version: 2025_12_07_213610) do create_table "forms", force: :cascade do |t| t.integer "template_id", null: false t.integer "turma_id", null: false @@ -21,6 +21,42 @@ t.index ["turma_id"], name: "index_forms_on_turma_id" end + create_table "issues", force: :cascade do |t| + t.string "title" + t.text "description" + t.string "status" + t.integer "project_id", null: false + t.integer "user_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["project_id"], name: "index_issues_on_project_id" + t.index ["user_id"], name: "index_issues_on_user_id" + end + + create_table "members", force: :cascade do |t| + t.string "nome" + t.string "matricula" + t.string "curso" + t.string "usuario" + t.string "formacao" + t.string "ocupacao" + t.string "email" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "members_turmas", id: false, force: :cascade do |t| + t.integer "turma_id", null: false + t.integer "member_id", null: false + end + + create_table "projects", force: :cascade do |t| + t.string "title" + t.text "description" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "resposta", force: :cascade do |t| t.integer "form_id", null: false t.integer "usuario_id", null: false @@ -30,6 +66,13 @@ t.index ["usuario_id"], name: "index_resposta_on_usuario_id" end + create_table "subjects", force: :cascade do |t| + t.string "code" + t.string "name" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "templates", force: :cascade do |t| t.string "titulo" t.string "target_audience" @@ -47,6 +90,14 @@ t.datetime "updated_at", null: false end + create_table "users", force: :cascade do |t| + t.string "name" + t.string "email" + t.string "role" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "usuarios", force: :cascade do |t| t.string "nome" t.string "matricula" @@ -70,9 +121,11 @@ add_foreign_key "forms", "templates" add_foreign_key "forms", "turmas" + add_foreign_key "issues", "projects" + add_foreign_key "issues", "users" add_foreign_key "resposta", "forms" add_foreign_key "resposta", "usuarios" - add_foreign_key "templates", "criado_pors" + add_foreign_key "templates", "usuarios", column: "criado_por_id" add_foreign_key "vinculos", "turmas" add_foreign_key "vinculos", "usuarios" end diff --git a/lib/tasks/development.rake b/lib/tasks/development.rake new file mode 100644 index 0000000000..585951992c --- /dev/null +++ b/lib/tasks/development.rake @@ -0,0 +1,26 @@ +namespace :dev do + desc "Cria ou reseta o template de teste para formulários" + task criar_template: :environment do + unless Rails.env.development? + puts "Essa task só deve ser executada em development!" + next + end + + # Garante que existe pelo menos um usuário + usuario = Usuario.first || FactoryBot.create(:usuario) + + # Deleta o template de teste existente + template = Template.find_by(titulo: "Template de Avaliação Temporário") + template.destroy if template + + # Cria o template de teste + template = FactoryBot.create( + :template, + titulo: "Template de Avaliação Temporário", + criado_por: usuario + ) + + puts "Template de teste criado com sucesso!" + puts "ID: #{template.id}, Título: #{template.titulo}, Criado por: #{template.criado_por.nome}" + end +end diff --git a/spec/factories/form.rb b/spec/factories/form.rb new file mode 100644 index 0000000000..40ee8c0bd7 --- /dev/null +++ b/spec/factories/form.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory :form do + association :template + association :turma + is_active { true } + end +end diff --git a/spec/factories/template.rb b/spec/factories/template.rb new file mode 100644 index 0000000000..1e6d4bc296 --- /dev/null +++ b/spec/factories/template.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory :template do + sequence(:titulo) { |n| "Template #{n}" } + target_audience { "Alunos de Engenharia" } + association :criado_por, factory: :usuario, class: "Usuario" + end +end diff --git a/spec/factories/turma.rb b/spec/factories/turma.rb new file mode 100644 index 0000000000..bb8ec26322 --- /dev/null +++ b/spec/factories/turma.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory :turma do + sequence(:nome) { |n| "Turma #{n}" } + semestre { "2025.2" } + is_active { true } + end +end diff --git a/spec/factories/usuarios.rb b/spec/factories/usuarios.rb new file mode 100644 index 0000000000..e3f72dacbb --- /dev/null +++ b/spec/factories/usuarios.rb @@ -0,0 +1,19 @@ +FactoryBot.define do + factory :usuario do + sequence(:nome) { |n| "Usuario Teste #{n}" } + sequence(:matricula) { |n| "000#{n}" } + sequence(:email) { |n| "usuario#{n}@teste.com" } + status { true } + profile { "aluno" } + departamento_id { 1 } # ajuste conforme necessário + end + + factory :admin, class: "Usuario" do + nome { "Admin Teste" } + matricula { "0000" } + email { "admin@teste.com" } + status { true } + profile { "admin" } + departamento_id { 1 } + end +end diff --git a/spec/factories/vinculo.rb b/spec/factories/vinculo.rb new file mode 100644 index 0000000000..9054537bd9 --- /dev/null +++ b/spec/factories/vinculo.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory :vinculo do + association :usuario + association :turma + papel_turma { 0 } # ex: 0 = aluno, 1 = professor + end +end diff --git a/spec/models/form_spec.rb b/spec/models/form_spec.rb index 4607aad445..30266c93d7 100644 --- a/spec/models/form_spec.rb +++ b/spec/models/form_spec.rb @@ -1,5 +1,15 @@ -require 'rails_helper' +require "rails_helper" RSpec.describe Form, type: :model do - pending "add some examples to (or delete) #{__FILE__}" -end + it "cria form com template e turma" do + usuario = create(:admin) + template = create(:template, criado_por: usuario) + turma = create(:turma) + + form = create(:form, template: template, turma: turma, is_active: true) + + expect(form).to be_persisted + expect(form.template).to eq(template) + expect(form.turma).to eq(turma) + end +end \ No newline at end of file diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 79811b8f6b..361f66272d 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -69,4 +69,8 @@ config.filter_rails_from_backtrace! # arbitrary gems may also be filtered via: # config.filter_gems_from_backtrace("gem name") + + config.include FactoryBot::Syntax::Methods + + end From aeac7fd286c7b7ce043fa0bb5539bf9ddec67df1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Sun, 7 Dec 2025 19:56:05 -0300 Subject: [PATCH 053/100] Create templates.css MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit estilização --- app/assets/stylesheets/templates.css | 106 +++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 app/assets/stylesheets/templates.css diff --git a/app/assets/stylesheets/templates.css b/app/assets/stylesheets/templates.css new file mode 100644 index 0000000000..097561197b --- /dev/null +++ b/app/assets/stylesheets/templates.css @@ -0,0 +1,106 @@ +.templates-container { + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: flex-start; + align-content: flex-start; + padding: 25px 43px; + gap: 50px; + width: 100%; + height: 100%; + background: #DBDBDB; + overflow-y: auto; +} + +.template-card { + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 24px; + gap: 8px; + width: 278px; + height: 110px; + background: #FFFFFF; + border-radius: 8px; + position: relative; + box-shadow: 0 2px 4px rgba(0,0,0,0.05); + text-decoration: none; + color: inherit; +} + +.template-card.add-card { + align-items: center; + justify-content: center; + cursor: pointer; + border: 2px dashed #ccc; +} + +.template-title { + font-family: 'Roboto', sans-serif; + font-style: normal; + font-weight: 500; + font-size: 24px; + line-height: 24px; + color: #000000; + width: 80%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.template-subtitle { + font-family: 'Roboto', sans-serif; + font-style: normal; + font-weight: 400; + font-size: 16px; + line-height: 24px; + color: #000000; +} + +.card-actions { + position: absolute; + right: 20px; + top: 24px; + display: flex; + gap: 10px; +} + +.action-icon { + width: 16px; + height: 16px; + cursor: pointer; + color: #000; + border: none; + background: none; + padding: 0; +} + +.plus-icon { + font-size: 48px; + color: #000; + font-weight: 300; + line-height: 0; + padding-bottom: 8px; +} + +.search-bar-mockup { + width: 271px; + position: relative; +} + +.search-bar-mockup input { + width: 100%; + padding: 8px 35px 8px 15px; + border-radius: 47px; + border: 1px solid #8E8E8E; + outline: none; +} + +.empty-message { + width: 100%; + text-align: center; + font-family: 'Roboto', sans-serif; + font-size: 18px; + color: #666; + margin-top: 50px; +} From 0e085df1c50a55194c764039aa3a74a283f9f849 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Sun, 7 Dec 2025 19:58:47 -0300 Subject: [PATCH 054/100] Create index.html.erb estrutura visual integrando o header, sidebar e a grid --- app/views/templates/index.html.erb | 73 ++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 app/views/templates/index.html.erb diff --git a/app/views/templates/index.html.erb b/app/views/templates/index.html.erb new file mode 100644 index 0000000000..3b6cabc460 --- /dev/null +++ b/app/views/templates/index.html.erb @@ -0,0 +1,73 @@ +<% content_for :head do %> + <%= stylesheet_link_tag "templates", "data-turbo-track": "reload" %> + +<% end %> + +
+
+ +

Gerenciamento - Templates

+
+ +
+
+ + 🔍 +
+ +
U
+
+
+ +
+ + +
+
+ + <% if @templates.empty? %> +
+

Nenhum template foi criado

+
+ <% else %> + <% @templates.each do |template| %> +
+
<%= template.titulo %>
+
<%= template.target_audience || "Geral" %>
+ +
+ <%= link_to edit_template_path(template), class: "action-icon", title: "Editar" do %> + + + + + <% end %> + + <%= button_to template_path(template), method: :delete, class: "action-icon", form: {style: "display:inline-block;"}, onclick: "return confirm('Você tem certeza que deseja deletar este template?');", title: "Deletar" do %> + + + + + <% end %> +
+
+ <% end %> + <% end %> + + <%= link_to new_template_path, class: "template-card add-card" do %> + + + <% end %> + +
+
+
From 73b80ae0cf10d6b87b0b0b18815f41c0c6b7f60f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Sun, 7 Dec 2025 20:00:36 -0300 Subject: [PATCH 055/100] Create _form.html.erb MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit view de criação e deleção de template --- app/views/templates/_form.html.erb | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 app/views/templates/_form.html.erb diff --git a/app/views/templates/_form.html.erb b/app/views/templates/_form.html.erb new file mode 100644 index 0000000000..add8c4583b --- /dev/null +++ b/app/views/templates/_form.html.erb @@ -0,0 +1,29 @@ +<%= form_with(model: template, local: true, class: "action-card", style: "width: 100%; max-width: 600px; margin: 0 auto;") do |form| %> + + <% if template.errors.any? %> +
+ <%= template.errors.full_messages.join(", ") %> +
+ <% end %> + +
+ <%= form.label :titulo, "Nome do template/Título", style: "display: block; font-weight: bold; margin-bottom: 5px;" %> + <%= form.text_field :titulo, class: "search-input", style: "width: 100%;" %> +
+ +
+ +

(Simulação: Adicione questões abaixo)

+
+ Questão Múltipla Escolha +
+
+ Questão Discursiva +
+
+ +
+ <%= form.submit "Salvar Template", class: "btn-action btn-primary-green" %> + <%= link_to "Cancelar", templates_path, class: "btn-action btn-secondary-green", style: "background-color: #ccc; color: #333;" %> +
+<% end %> From 4f4fd4437d0be15c56e6dc15521737db04604289 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Sun, 7 Dec 2025 20:10:35 -0300 Subject: [PATCH 056/100] Update application.html.erb adicionando roboto, inter, poppins --- app/views/layouts/application.html.erb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 0ac7b9cf19..ed6e8fc0b9 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -13,6 +13,10 @@ + + + + <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %> <%= javascript_importmap_tags %> @@ -61,4 +65,4 @@ }); - \ No newline at end of file + From cf61d453b8e11c519187cdf5344ce62d54a489b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Sun, 7 Dec 2025 20:13:15 -0300 Subject: [PATCH 057/100] Create admin_templates.css --- app/assets/stylesheets/admin_templates.css | 225 +++++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 app/assets/stylesheets/admin_templates.css diff --git a/app/assets/stylesheets/admin_templates.css b/app/assets/stylesheets/admin_templates.css new file mode 100644 index 0000000000..b867c2e2d2 --- /dev/null +++ b/app/assets/stylesheets/admin_templates.css @@ -0,0 +1,225 @@ +body { + margin: 0; + font-family: 'Roboto', sans-serif; + background-color: #F5F5F5; +} + +.top-navbar { + height: 60px; + background: #FFFFFF; + box-shadow: 0px 11px 10.7px rgba(0, 0, 0, 0.05); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 24px; + position: relative; + z-index: 10; +} + +.admin-container { + display: flex; + height: calc(100vh - 60px); +} + +.sidebar { + width: 257px; + background: #FFFFFF; + border-right: 1px solid #D9D9D9; + display: flex; + flex-direction: column; +} + +.sidebar-item { + height: 46px; + display: flex; + align-items: center; + justify-content: center; + border-bottom: 1px solid #D9D9D9; + text-decoration: none; + font-size: 16px; + color: #000; +} + +.sidebar-item.active { + background: #6C2365; + color: #FFFFFF; +} + +.main-content { + flex: 1; + background: #DBDBDB; + padding: 25px 43px; + overflow-y: auto; + position: relative; +} + +.cards-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(278px, 1fr)); + gap: 24px; +} + +.template-card { + background: #FFFFFF; + border-radius: 8px; + padding: 24px; + height: 130px; + position: relative; + display: flex; + flex-direction: column; + box-shadow: 0 2px 4px rgba(0,0,0,0.05); +} + +.card-title { + font-weight: 500; + font-size: 24px; + color: #000; +} + +.card-subtitle { + font-weight: 400; + font-size: 16px; + color: #000; + margin-top: 4px; +} + +.card-icons { + position: absolute; + top: 24px; + right: 24px; + display: flex; + gap: 12px; +} + +.icon-btn { + background: none; + border: none; + cursor: pointer; + padding: 0; +} + +.add-card { + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + text-decoration: none; +} + +.plus-sign { + font-size: 48px; + color: #000; + line-height: 0; +} + +.modal-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.2); + display: flex; + justify-content: center; + align-items: flex-start; + padding-top: 85px; + z-index: 20; +} + +.modal-content { + width: 453px; + background: #FFFFFF; + box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.5); + padding: 20px; + display: flex; + flex-direction: column; + gap: 16px; + border-radius: 4px; +} + +.input-group { + display: flex; + align-items: center; + gap: 10px; + width: 100%; +} + +.input-label { + font-family: 'Inter'; + font-size: 12px; + color: #000; + width: 110px; +} + +.input-field-line { + border: none; + border-bottom: 1px solid #000000; + font-family: 'Roboto'; + font-size: 13px; + color: #000; + flex: 1; + padding: 5px 0; + outline: none; +} + +.input-field-line::placeholder { + color: #8E8E8E; +} + +.question-block { + background: #FFFFFF; + padding: 10px 0; + display: flex; + flex-direction: column; + gap: 14px; +} + +.question-title { + font-family: 'Roboto'; + font-weight: 500; + font-size: 16px; +} + +.custom-select { + border: 1px solid #E0E0E0; + border-radius: 8px; + padding: 4px 10px; + font-family: 'Inter'; + font-size: 12px; + color: #828282; + background: white; + width: 163px; +} + +.btn-create { + width: 125px; + height: 35px; + background: #22C55E; + border-radius: 6px; + border: none; + color: #F0FDF4; + font-family: 'Poppins'; + font-weight: 500; + font-size: 16px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + margin: 20px auto 0; + box-shadow: 0px 1px 2px rgba(105, 81, 255, 0.05); +} + +.btn-add-question { + background: #6C2365; + color: white; + width: 30px; + height: 30px; + border-radius: 50%; + border: none; + font-size: 20px; + display: flex; + align-items: center; + justify-content: center; + margin: 10px auto; + cursor: pointer; +} From b166105efd52cff2c3a00ab143523857ce8295d6 Mon Sep 17 00:00:00 2001 From: mariosantos-05 Date: Sun, 7 Dec 2025 20:14:55 -0300 Subject: [PATCH 058/100] Fix da view responder forms --- app/assets/stylesheets/application.css | 61 +++++++++++++++++++++ app/controllers/avaliacoes_controller.rb | 26 ++++++--- app/views/avaliacoes/responder.html.erb | 68 +++++++++++++++--------- config/routes.rb | 9 ++-- 4 files changed, 128 insertions(+), 36 deletions(-) diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index bc39df47cd..3b8eb57946 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -336,3 +336,64 @@ body { outline: none; box-shadow: 0 0 5px rgba(0, 0, 0, 0.15); } + +.main-container { + flex: 1; + display: flex; + justify-content: center; + align-items: center; + padding: 2rem; + width: 100%; + overflow-y: auto; +} +/* FORM ENVELOPE */ +.form-envelope { + background: white; + width: 100%; + max-width: 600px; + padding: 2rem; + border-radius: 12px; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); + position: relative; +} + +/* QUESTIONS */ +.question-card { + background: #e0e0e0; + padding: 1rem; + border-radius: 8px; + margin-bottom: 1rem; +} + +.question-text { + margin: 0 0 0.5rem 0; + font-weight: bold; +} + +.text-input { + width: 100%; + padding: 0.5rem; + border-radius: 4px; + border: 1px solid #ccc; +} + +.radio-option { + margin-bottom: 0.25rem; +} + +/* SUBMIT BUTTON */ +.submit-button { + position: absolute; + bottom: -25px; + right: -80px; + width: 50px; + height: 50px; + border-radius: 50%; + background: #6a1b57; + color: white; + border: none; + font-size: 1.5rem; + display: flex; + align-items: center; + justify-content: center; +} diff --git a/app/controllers/avaliacoes_controller.rb b/app/controllers/avaliacoes_controller.rb index 470cbd1837..2a66fd9f27 100644 --- a/app/controllers/avaliacoes_controller.rb +++ b/app/controllers/avaliacoes_controller.rb @@ -6,15 +6,18 @@ def index # Pega turmas que o usuário participa @turmas = current_user.vinculos.includes(turma: :forms).map(&:turma) - # Carrega os forms ativos de cada turma + # Carrega apenas os forms ativos de cada turma @turmas = @turmas.map do |turma| OpenStruct.new( id: turma.id, nome: turma.nome, + semestre: turma.semestre, forms: turma.forms.where(is_active: true).map do |form| OpenStruct.new( id: form.id, - titulo: form.template.titulo + titulo: form.template.titulo, + turma_nome: turma.nome, + semestre: turma.semestre ) end ) @@ -29,12 +32,21 @@ def responder redirect_to avaliacoes_path, alert: "Você não tem acesso a esse formulário." return end + end + + def enviar_resposta + form = Form.find(params[:form_id]) + + # Apenas usuários da turma podem enviar + unless current_user.vinculos.exists?(turma_id: form.turma_id) + redirect_to avaliacoes_path, alert: "Você não tem acesso a esse formulário." + return + end + + # Aqui você poderia salvar respostas, mas vamos deletar o form para teste + form.destroy - @fake_form = OpenStruct.new( - id: @form.id, - titulo: @form.template.titulo, - turma_nome: @form.turma.nome - ) + redirect_to avaliacoes_path, notice: "Formulário enviado com sucesso!" end private diff --git a/app/views/avaliacoes/responder.html.erb b/app/views/avaliacoes/responder.html.erb index b92e34ad86..7e53bcf67f 100644 --- a/app/views/avaliacoes/responder.html.erb +++ b/app/views/avaliacoes/responder.html.erb @@ -1,39 +1,55 @@
-

- Responder Formulário +

-
-
- - -
-
- -

<%= @fake_form.titulo %>

-

Turma: <%= @fake_form.turma_nome %>

- -
- -

- Aqui futuramente será exibido o formulário real. +

+
+ + <% # Mockup de perguntas %> + <% questions = [ + { type: "radio", text: "Como você avalia a disciplina?", options: ["Muito bom", "Bom", "Satisfatório", "Ruim", "Péssimo"] }, + { type: "text", text: "Sugestões para melhoria:" }, + { type: "text", text: "Comentário adicional:" }, + { type: "radio", text: "Como você avalia o professor?", options: ["Muito bom", "Bom", "Satisfatório", "Ruim", "Péssimo"] } + ] %> + + <% questions.each_with_index do |q, index| %> +
+

+ Pergunta <%= index + 1 %>: <%= q[:text] %>

- - + <% if q[:type] == "text" %> + + + <% elsif q[:type] == "radio" %> + <% q[:options].each do |opt| %> +
+ +
+ <% end %> + <% end %>
-
+ <% end %> + + <%= button_to enviar_resposta_avaliacao_path(@form.id), + method: :post, + form: { "data-turbo" => false }, + class: "submit-button" do %> + ➤ + <% end %> + +
\ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index e14f983484..c50229f0ef 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,7 @@ Rails.application.routes.draw do + # Admin routes get 'gerenciamento', to: 'admins#management', as: 'admin_management' - get 'importar_sigaa', to: 'admins#new_import', as: 'import_sigaa' + get 'importar_sigaa', to: 'admins#new_import', as: 'import_sigaa' post 'importar_sigaa', to: 'admins#create_import' get 'resultados', to: 'admins#results', as: 'admin_results' @@ -10,10 +11,12 @@ resources :forms, only: [:new, :create] end - # NEW — route to render the form answering page + get "avaliacoes", to: "avaliacoes#index", as: "avaliacoes" get "avaliacoes/:turma_id/forms/:form_id/responder", to: "avaliacoes#responder", as: "responder_form" - get "avaliacoes", to: "avaliacoes#index", as: "avaliacoes" + post "avaliacoes/:form_id/enviar_resposta", + to: "avaliacoes#enviar_resposta", + as: "enviar_resposta_avaliacao" end From 356c12107e28acf070eb586201aa890a918f0bf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Sun, 7 Dec 2025 20:19:29 -0300 Subject: [PATCH 059/100] Update index.html.erb atualizando --- app/views/templates/index.html.erb | 89 +++++++++++++++--------------- 1 file changed, 46 insertions(+), 43 deletions(-) diff --git a/app/views/templates/index.html.erb b/app/views/templates/index.html.erb index 3b6cabc460..994d6da6cb 100644 --- a/app/views/templates/index.html.erb +++ b/app/views/templates/index.html.erb @@ -1,59 +1,61 @@ <% content_for :head do %> - <%= stylesheet_link_tag "templates", "data-turbo-track": "reload" %> - + <%= stylesheet_link_tag "admin_templates", "data-turbo-track": "reload" %> <% end %> -
-
- -

Gerenciamento - Templates

-
- -
-
- - 🔍 -
- -
U
-
+
+ +
-
From 0f2c9471a68b87f3ccc8d17bd23a87043c71e570 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Sun, 7 Dec 2025 20:22:00 -0300 Subject: [PATCH 060/100] Update admin_templates.css atualizando --- app/assets/stylesheets/admin_templates.css | 85 +++++++++------------- 1 file changed, 35 insertions(+), 50 deletions(-) diff --git a/app/assets/stylesheets/admin_templates.css b/app/assets/stylesheets/admin_templates.css index b867c2e2d2..d977c0a5a9 100644 --- a/app/assets/stylesheets/admin_templates.css +++ b/app/assets/stylesheets/admin_templates.css @@ -16,13 +16,21 @@ body { z-index: 10; } +.nav-left, .nav-right { + display: flex; + align-items: center; + gap: 15px; +} + .admin-container { display: flex; height: calc(100vh - 60px); + width: 100%; } .sidebar { width: 257px; + min-width: 257px; background: #FFFFFF; border-right: 1px solid #D9D9D9; display: flex; @@ -38,6 +46,7 @@ body { text-decoration: none; font-size: 16px; color: #000; + font-family: 'Roboto', sans-serif; } .sidebar-item.active { @@ -57,6 +66,7 @@ body { display: grid; grid-template-columns: repeat(auto-fill, minmax(278px, 1fr)); gap: 24px; + width: 100%; } .template-card { @@ -68,19 +78,24 @@ body { display: flex; flex-direction: column; box-shadow: 0 2px 4px rgba(0,0,0,0.05); + text-decoration: none; + color: inherit; } .card-title { font-weight: 500; font-size: 24px; color: #000; + margin-bottom: 4px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .card-subtitle { font-weight: 400; font-size: 16px; color: #000; - margin-top: 4px; } .card-icons { @@ -96,6 +111,9 @@ body { border: none; cursor: pointer; padding: 0; + color: #000; + display: flex; + align-items: center; } .add-card { @@ -110,6 +128,16 @@ body { font-size: 48px; color: #000; line-height: 0; + font-weight: 300; +} + +.empty-message { + text-align: center; + margin-top: 50px; + font-size: 18px; + color: #666; + font-family: 'Roboto', sans-serif; + width: 100%; } .modal-overlay { @@ -118,10 +146,9 @@ body { left: 0; width: 100%; height: 100%; - background: rgba(0, 0, 0, 0.2); + background: rgba(0, 0, 0, 0.3); display: flex; justify-content: center; - align-items: flex-start; padding-top: 85px; z-index: 20; } @@ -145,7 +172,7 @@ body { } .input-label { - font-family: 'Inter'; + font-family: 'Inter', sans-serif; font-size: 12px; color: #000; width: 110px; @@ -154,43 +181,19 @@ body { .input-field-line { border: none; border-bottom: 1px solid #000000; - font-family: 'Roboto'; + font-family: 'Roboto', sans-serif; font-size: 13px; color: #000; flex: 1; padding: 5px 0; outline: none; + background: transparent; } .input-field-line::placeholder { color: #8E8E8E; } -.question-block { - background: #FFFFFF; - padding: 10px 0; - display: flex; - flex-direction: column; - gap: 14px; -} - -.question-title { - font-family: 'Roboto'; - font-weight: 500; - font-size: 16px; -} - -.custom-select { - border: 1px solid #E0E0E0; - border-radius: 8px; - padding: 4px 10px; - font-family: 'Inter'; - font-size: 12px; - color: #828282; - background: white; - width: 163px; -} - .btn-create { width: 125px; height: 35px; @@ -198,28 +201,10 @@ body { border-radius: 6px; border: none; color: #F0FDF4; - font-family: 'Poppins'; + font-family: 'Poppins', sans-serif; font-weight: 500; font-size: 16px; cursor: pointer; - display: flex; - align-items: center; - justify-content: center; margin: 20px auto 0; - box-shadow: 0px 1px 2px rgba(105, 81, 255, 0.05); -} - -.btn-add-question { - background: #6C2365; - color: white; - width: 30px; - height: 30px; - border-radius: 50%; - border: none; - font-size: 20px; - display: flex; - align-items: center; - justify-content: center; - margin: 10px auto; - cursor: pointer; + display: block; } From 8684b3740874817512b2fc35f466cbbe7647f3c3 Mon Sep 17 00:00:00 2001 From: mariosantos-05 Date: Sun, 7 Dec 2025 20:47:39 -0300 Subject: [PATCH 061/100] =?UTF-8?q?visualiza=C3=A7=C3=A3o=20individual=20d?= =?UTF-8?q?e=20formulario?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/avaliacoes_controller.rb | 27 ++++++++++++++----- app/models/{respostum.rb => resposta.rb} | 2 +- app/views/avaliacoes/index.html.erb | 14 +++++++--- ...1207233036_rename_resposta_to_respostas.rb | 4 +++ db/schema.rb | 2 +- 5 files changed, 38 insertions(+), 11 deletions(-) rename app/models/{respostum.rb => resposta.rb} (55%) create mode 100644 db/migrate/20251207233036_rename_resposta_to_respostas.rb diff --git a/app/controllers/avaliacoes_controller.rb b/app/controllers/avaliacoes_controller.rb index 2a66fd9f27..943009dafd 100644 --- a/app/controllers/avaliacoes_controller.rb +++ b/app/controllers/avaliacoes_controller.rb @@ -1,12 +1,11 @@ require "ostruct" + class AvaliacoesController < ApplicationController before_action :require_user def index - # Pega turmas que o usuário participa @turmas = current_user.vinculos.includes(turma: :forms).map(&:turma) - # Carrega apenas os forms ativos de cada turma @turmas = @turmas.map do |turma| OpenStruct.new( id: turma.id, @@ -27,25 +26,41 @@ def index def responder @form = Form.includes(:template, :turma).find(params[:form_id]) - # Apenas permite acessar se o usuário faz parte da turma + # Verifica se o usuário participa da turma unless current_user.vinculos.exists?(turma_id: @form.turma_id) redirect_to avaliacoes_path, alert: "Você não tem acesso a esse formulário." return end + + # 🚨 Impede que responda mais de 1 vez + if Resposta.exists?(form_id: @form.id, usuario_id: current_user.id) + redirect_to avaliacoes_path, alert: "Você já respondeu este formulário." + return + end end def enviar_resposta form = Form.find(params[:form_id]) - # Apenas usuários da turma podem enviar + # Verifica permissão unless current_user.vinculos.exists?(turma_id: form.turma_id) redirect_to avaliacoes_path, alert: "Você não tem acesso a esse formulário." return end - # Aqui você poderia salvar respostas, mas vamos deletar o form para teste - form.destroy + # 🚨 Impede que o aluno envie novamente (segurança) + if Resposta.exists?(form_id: form.id, usuario_id: current_user.id) + redirect_to avaliacoes_path, alert: "Você já respondeu este formulário." + return + end + + # 👉 Aqui criamos o registro dizendo que ESTE aluno respondeu + Resposta.create!( + form_id: form.id, + usuario_id: current_user.id + ) + # ❗ Nada de destruir o form — cada aluno responde o mesmo form redirect_to avaliacoes_path, notice: "Formulário enviado com sucesso!" end diff --git a/app/models/respostum.rb b/app/models/resposta.rb similarity index 55% rename from app/models/respostum.rb rename to app/models/resposta.rb index d80c57eb33..f9a08520fe 100644 --- a/app/models/respostum.rb +++ b/app/models/resposta.rb @@ -1,4 +1,4 @@ -class Respostum < ApplicationRecord +class Resposta < ApplicationRecord belongs_to :form belongs_to :usuario end diff --git a/app/views/avaliacoes/index.html.erb b/app/views/avaliacoes/index.html.erb index 534b3bcc6f..9ab84b76e1 100644 --- a/app/views/avaliacoes/index.html.erb +++ b/app/views/avaliacoes/index.html.erb @@ -38,12 +38,20 @@

<% turma.forms.each do |form| %> - <%= link_to responder_form_path(turma.id, form.id), - class: "btn-action btn-primary-green", - style: "margin-top: 8px; display: inline-block; padding: 8px 15px; font-size: 0.9rem;" do %> + <% unless Resposta.exists?(form_id: form.id, usuario_id: current_user.id) %> + <%= link_to responder_form_path(turma.id, form.id), + class: "btn-action btn-primary-green", + style: "margin-top: 8px; display: inline-block; padding: 8px 15px; font-size: 0.9rem;" do %> Responder: <%= form.titulo %> <% end %> + <% else %> + + Já respondido + <% end %> + <% end %> + <% end %> diff --git a/db/migrate/20251207233036_rename_resposta_to_respostas.rb b/db/migrate/20251207233036_rename_resposta_to_respostas.rb new file mode 100644 index 0000000000..73513d588e --- /dev/null +++ b/db/migrate/20251207233036_rename_resposta_to_respostas.rb @@ -0,0 +1,4 @@ +class RenameRespostaToRespostas < ActiveRecord::Migration[8.0] + def change + end +end diff --git a/db/schema.rb b/db/schema.rb index 67b292306c..4ae2f162d1 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_12_07_213610) do +ActiveRecord::Schema[8.0].define(version: 2025_12_07_233036) do create_table "forms", force: :cascade do |t| t.integer "template_id", null: false t.integer "turma_id", null: false From 4486c0a352e9dd1cf884446c72e1e0551f7644a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Sun, 7 Dec 2025 20:53:29 -0300 Subject: [PATCH 062/100] Update index.html.erb From 74c73fb0db4d930847820b77b73a3045cb744829 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Sun, 7 Dec 2025 20:54:07 -0300 Subject: [PATCH 063/100] Update index.html.erb From 9665d10ec1d47be95e078065786b744f04d328ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Sun, 7 Dec 2025 20:58:33 -0300 Subject: [PATCH 064/100] Update index.html.erb From c6ae370ac26f8264f19e32daa5dc8c8c1258869d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Sun, 7 Dec 2025 20:59:16 -0300 Subject: [PATCH 065/100] Create new.html.erb modal branco flutuante sobre a tela de fundo --- app/views/templates/new.html.erb | 92 ++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 app/views/templates/new.html.erb diff --git a/app/views/templates/new.html.erb b/app/views/templates/new.html.erb new file mode 100644 index 0000000000..44ef2ae890 --- /dev/null +++ b/app/views/templates/new.html.erb @@ -0,0 +1,92 @@ +<% content_for :head do %> + <%= stylesheet_link_tag "admin_templates", "data-turbo-track": "reload" %> +<% end %> + +
+ + +
+ +
+ + +
+
+
Template 1
+
Template 2
+
+ + + +
+
From 928b82d00495304d558aee07024d655eb0c74863 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Sun, 7 Dec 2025 21:00:59 -0300 Subject: [PATCH 066/100] Update _form.html.erb --- app/views/templates/_form.html.erb | 59 ++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 15 deletions(-) diff --git a/app/views/templates/_form.html.erb b/app/views/templates/_form.html.erb index add8c4583b..877eed9daa 100644 --- a/app/views/templates/_form.html.erb +++ b/app/views/templates/_form.html.erb @@ -1,29 +1,58 @@ -<%= form_with(model: template, local: true, class: "action-card", style: "width: 100%; max-width: 600px; margin: 0 auto;") do |form| %> +<%= form_with(model: template, local: true) do |form| %> <% if template.errors.any? %> -
+
<%= template.errors.full_messages.join(", ") %>
<% end %> -
- <%= form.label :titulo, "Nome do template/Título", style: "display: block; font-weight: bold; margin-bottom: 5px;" %> - <%= form.text_field :titulo, class: "search-input", style: "width: 100%;" %> +
+ + <%= form.text_field :titulo, class: "input-field-line", placeholder: "Ex: Avaliação 2024" %>
-
- -

(Simulação: Adicione questões abaixo)

-
- Questão Múltipla Escolha +
+
Questão 1
+ +
+ +
-
- Questão Discursiva + +
+ + +
+ +
+ +
+ +
-
- <%= form.submit "Salvar Template", class: "btn-action btn-primary-green" %> - <%= link_to "Cancelar", templates_path, class: "btn-action btn-secondary-green", style: "background-color: #ccc; color: #333;" %> +
+
Questão 2
+ +
+ + +
+ +
+ + +
+ + + + <%= form.submit (template.new_record? ? "Criar" : "Salvar"), class: "btn-create" %> + <% end %> From c430e8cf20c417620fea49ceb858da2c0b6cc8cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Sun, 7 Dec 2025 21:01:44 -0300 Subject: [PATCH 067/100] Update new.html.erb --- app/views/templates/new.html.erb | 94 +++----------------------------- 1 file changed, 7 insertions(+), 87 deletions(-) diff --git a/app/views/templates/new.html.erb b/app/views/templates/new.html.erb index 44ef2ae890..926ac4e411 100644 --- a/app/views/templates/new.html.erb +++ b/app/views/templates/new.html.erb @@ -2,91 +2,11 @@ <%= stylesheet_link_tag "admin_templates", "data-turbo-track": "reload" %> <% end %> -
- - -
- -
- - -
-
-
Template 1
-
Template 2
-
- -
-
+
+<% end %> From 65ed048e1c2a0af365972cd035632c02ec791254 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Sun, 7 Dec 2025 21:02:20 -0300 Subject: [PATCH 068/100] Create edit.html.erb --- app/views/templates/edit.html.erb | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 app/views/templates/edit.html.erb diff --git a/app/views/templates/edit.html.erb b/app/views/templates/edit.html.erb new file mode 100644 index 0000000000..2a3d320ae6 --- /dev/null +++ b/app/views/templates/edit.html.erb @@ -0,0 +1,12 @@ +<% content_for :head do %> + <%= stylesheet_link_tag "admin_templates", "data-turbo-track": "reload" %> +<% end %> + +<%= render "shared_layout_structure" do %> + +<% end %> From d1dbc8fd6cfee261e8943a6777ce2980bf6cf39a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Sun, 7 Dec 2025 21:02:44 -0300 Subject: [PATCH 069/100] Create _shared_layout_structure.html.erb --- .../_shared_layout_structure.html.erb | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 app/views/templates/_shared_layout_structure.html.erb diff --git a/app/views/templates/_shared_layout_structure.html.erb b/app/views/templates/_shared_layout_structure.html.erb new file mode 100644 index 0000000000..362d497584 --- /dev/null +++ b/app/views/templates/_shared_layout_structure.html.erb @@ -0,0 +1,27 @@ +
+ + +
+ +
+ + +
+
+
Template A
+
Template B
+
Template C
+
+ + <%= yield %> + +
+
From 1ddc236ccf4cf0032585c6729bd11e7524c76c0e Mon Sep 17 00:00:00 2001 From: mariosantos-05 Date: Sun, 7 Dec 2025 21:30:51 -0300 Subject: [PATCH 070/100] Comment fix --- app/controllers/avaliacoes_controller.rb | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/app/controllers/avaliacoes_controller.rb b/app/controllers/avaliacoes_controller.rb index 943009dafd..5a06981537 100644 --- a/app/controllers/avaliacoes_controller.rb +++ b/app/controllers/avaliacoes_controller.rb @@ -26,13 +26,11 @@ def index def responder @form = Form.includes(:template, :turma).find(params[:form_id]) - # Verifica se o usuário participa da turma unless current_user.vinculos.exists?(turma_id: @form.turma_id) redirect_to avaliacoes_path, alert: "Você não tem acesso a esse formulário." return end - # 🚨 Impede que responda mais de 1 vez if Resposta.exists?(form_id: @form.id, usuario_id: current_user.id) redirect_to avaliacoes_path, alert: "Você já respondeu este formulário." return @@ -42,25 +40,23 @@ def responder def enviar_resposta form = Form.find(params[:form_id]) - # Verifica permissão + unless current_user.vinculos.exists?(turma_id: form.turma_id) redirect_to avaliacoes_path, alert: "Você não tem acesso a esse formulário." return end - # 🚨 Impede que o aluno envie novamente (segurança) + if Resposta.exists?(form_id: form.id, usuario_id: current_user.id) redirect_to avaliacoes_path, alert: "Você já respondeu este formulário." return end - # 👉 Aqui criamos o registro dizendo que ESTE aluno respondeu Resposta.create!( form_id: form.id, usuario_id: current_user.id ) - # ❗ Nada de destruir o form — cada aluno responde o mesmo form redirect_to avaliacoes_path, notice: "Formulário enviado com sucesso!" end From 9af17b3ad662c7c4d5682aa262aabcbd42a2bd4a Mon Sep 17 00:00:00 2001 From: mariosantos-05 Date: Sun, 7 Dec 2025 21:59:47 -0300 Subject: [PATCH 071/100] Update link entre views --- app/views/admins/management.html.erb | 2 +- .../_shared_layout_structure.html.erb | 46 ++++--- app/views/templates/index.html.erb | 118 +++++++++--------- 3 files changed, 86 insertions(+), 80 deletions(-) diff --git a/app/views/admins/management.html.erb b/app/views/admins/management.html.erb index 93cebdd9f5..247ad020ff 100644 --- a/app/views/admins/management.html.erb +++ b/app/views/admins/management.html.erb @@ -22,7 +22,7 @@
<%= link_to "Importar dados", import_sigaa_path, class: "btn-action btn-primary-green" %> - + <%= link_to "Templates", templates_path, class: "btn-action btn-primary-green" %> <%= link_to "Criar Formulário", new_admins_form_path, data: { turbo_frame: "modal" }, class: "btn-action btn-primary-green" %> <%= link_to "Resultados", admin_results_path, class: "btn-action btn-primary-green" %>
diff --git a/app/views/templates/_shared_layout_structure.html.erb b/app/views/templates/_shared_layout_structure.html.erb index 362d497584..a93a4d2953 100644 --- a/app/views/templates/_shared_layout_structure.html.erb +++ b/app/views/templates/_shared_layout_structure.html.erb @@ -1,27 +1,33 @@
- - + +
- + -
-
-
Template A
-
Template B
-
Template C
-
+
+
+
+
Template A
+
+
+
Template B
+
+
+
Template C
+
+
- <%= yield %> + <%= yield %> -
-
+ +
\ No newline at end of file diff --git a/app/views/templates/index.html.erb b/app/views/templates/index.html.erb index 994d6da6cb..4a256ac9ba 100644 --- a/app/views/templates/index.html.erb +++ b/app/views/templates/index.html.erb @@ -1,76 +1,76 @@ <% content_for :head do %> - <%= stylesheet_link_tag "admin_templates", "data-turbo-track": "reload" %> +<%= stylesheet_link_tag "admin_templates", "data-turbo-track": "reload" %> <% end %>
- - + +
- + -
- - <% if flash[:notice] %> -
- <%= flash[:notice] %> -
- <% end %> +
- <% if @templates.empty? %> - -
- <%= link_to new_template_path, class: "template-card add-card" do %> - + + <% if flash[:notice] %> +
+ <%= flash[:notice] %> +
<% end %> -
-
- Nenhum template foi criado -
+ <% if @templates.empty? %> - <% else %> - -
- <% @templates.each do |template| %> -
-
<%= template.titulo %>
-
<%= template.target_audience || "semestre" %>
- -
- <%= link_to edit_template_path(template), class: "icon-btn", title: "Editar" do %> - - - - - <% end %> +
+ <%= link_to new_template_path, class: "template-card add-card" do %> + + + <% end %> +
- <%= button_to template_path(template), method: :delete, class: "icon-btn", form: {style: "display:inline-block;"}, onclick: "return confirm('Você tem certeza que deseja deletar este template?');", title: "Deletar" do %> - - - - - <% end %> +
+ Nenhum template foi criado +
+ + <% else %> + +
+ <% @templates.each do |template| %> +
+
<%= template.titulo %>
+
<%= template.target_audience || "semestre" %>
+ +
+ <%= link_to edit_template_path(template), class: "icon-btn", title: "Editar" do %> + + + + + <% end %> + + <%= button_to template_path(template), method: :delete, class: "icon-btn", form: {style: "display:inline-block;"}, onclick: "return confirm('Você tem certeza que deseja deletar este template?');", title: "Deletar" do %> + + + + + <% end %> +
-
- <% end %> + <% end %> - <%= link_to new_template_path, class: "template-card add-card" do %> - + - <% end %> -
+ <%= link_to new_template_path, class: "template-card add-card" do %> + + + <% end %> +
- <% end %> + <% end %> -
-
+ +
\ No newline at end of file From 5d644020282abf4a0223fbf9425880b5852c65fa Mon Sep 17 00:00:00 2001 From: mariosantos-05 Date: Mon, 8 Dec 2025 09:39:45 -0300 Subject: [PATCH 072/100] Parcial --- app/controllers/templates_controller.rb | 7 ++++- app/models/question.rb | 7 +++++ app/models/template.rb | 10 ++++++- app/views/avaliacoes/responder.html.erb | 29 ++++++++++++++----- db/migrate/20251208123405_create_questions.rb | 12 ++++++++ db/schema.rb | 13 ++++++++- spec/factories/questions.rb | 8 +++++ spec/models/question_spec.rb | 5 ++++ 8 files changed, 81 insertions(+), 10 deletions(-) create mode 100644 app/models/question.rb create mode 100644 db/migrate/20251208123405_create_questions.rb create mode 100644 spec/factories/questions.rb create mode 100644 spec/models/question_spec.rb diff --git a/app/controllers/templates_controller.rb b/app/controllers/templates_controller.rb index 40f7e25251..e29c428f2a 100644 --- a/app/controllers/templates_controller.rb +++ b/app/controllers/templates_controller.rb @@ -57,6 +57,11 @@ def destroy private def template_params - params.require(:template).permit(:titulo, :target_audience) + params.require(:template).permit( + :titulo, + :target_audience, + questions_attributes: [:id, :text, :question_type, :options, :_destroy] + ) end + end diff --git a/app/models/question.rb b/app/models/question.rb new file mode 100644 index 0000000000..85cc69153a --- /dev/null +++ b/app/models/question.rb @@ -0,0 +1,7 @@ +class Question < ApplicationRecord + belongs_to :template + serialize :options, Array + + validates :text, presence: true + validates :question_type, inclusion: { in: %w[text radio] } +end diff --git a/app/models/template.rb b/app/models/template.rb index a020a01aa1..46964bc044 100644 --- a/app/models/template.rb +++ b/app/models/template.rb @@ -1,6 +1,9 @@ class Template < ApplicationRecord belongs_to :criado_por, class_name: 'Usuario' has_many :forms, dependent: :restrict_with_error + has_many :questions, dependent: :destroy + + accepts_nested_attributes_for :questions, allow_destroy: true attr_accessor :questions @@ -19,4 +22,9 @@ def must_have_questions errors.add(:base, "O template deve conter pelo menos uma questão") end end -end \ No newline at end of file + + def questions_attributes_blank? + self.questions_attributes&.values&.all? { |q| q["text"].blank? } + end + +end diff --git a/app/views/avaliacoes/responder.html.erb b/app/views/avaliacoes/responder.html.erb index 7e53bcf67f..66baa0455e 100644 --- a/app/views/avaliacoes/responder.html.erb +++ b/app/views/avaliacoes/responder.html.erb @@ -14,13 +14,28 @@
- <% # Mockup de perguntas %> - <% questions = [ - { type: "radio", text: "Como você avalia a disciplina?", options: ["Muito bom", "Bom", "Satisfatório", "Ruim", "Péssimo"] }, - { type: "text", text: "Sugestões para melhoria:" }, - { type: "text", text: "Comentário adicional:" }, - { type: "radio", text: "Como você avalia o professor?", options: ["Muito bom", "Bom", "Satisfatório", "Ruim", "Péssimo"] } - ] %> + <% @form.template.questions.each_with_index do |q, index| %> +
+

+ Pergunta <%= index + 1 %>: <%= q.text %> +

+ + <% if q.question_type == "text" %> + + + <% elsif q.question_type == "radio" %> + <% q.options.each do |opt| %> +
+ +
+ <% end %> + <% end %> +
+ <% end %> + <% questions.each_with_index do |q, index| %>
diff --git a/db/migrate/20251208123405_create_questions.rb b/db/migrate/20251208123405_create_questions.rb new file mode 100644 index 0000000000..8009f7b971 --- /dev/null +++ b/db/migrate/20251208123405_create_questions.rb @@ -0,0 +1,12 @@ +class CreateQuestions < ActiveRecord::Migration[8.0] + def change + create_table :questions do |t| + t.references :template, null: false, foreign_key: true + t.string :text + t.string :question_type + t.text :options + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 4ae2f162d1..e3f72b0f9e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_12_07_233036) do +ActiveRecord::Schema[8.0].define(version: 2025_12_08_123405) do create_table "forms", force: :cascade do |t| t.integer "template_id", null: false t.integer "turma_id", null: false @@ -57,6 +57,16 @@ t.datetime "updated_at", null: false end + create_table "questions", force: :cascade do |t| + t.integer "template_id", null: false + t.string "text" + t.string "question_type" + t.text "options" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["template_id"], name: "index_questions_on_template_id" + end + create_table "resposta", force: :cascade do |t| t.integer "form_id", null: false t.integer "usuario_id", null: false @@ -123,6 +133,7 @@ add_foreign_key "forms", "turmas" add_foreign_key "issues", "projects" add_foreign_key "issues", "users" + add_foreign_key "questions", "templates" add_foreign_key "resposta", "forms" add_foreign_key "resposta", "usuarios" add_foreign_key "templates", "usuarios", column: "criado_por_id" diff --git a/spec/factories/questions.rb b/spec/factories/questions.rb new file mode 100644 index 0000000000..8febbd4146 --- /dev/null +++ b/spec/factories/questions.rb @@ -0,0 +1,8 @@ +FactoryBot.define do + factory :question do + template { nil } + text { "MyString" } + question_type { "MyString" } + options { "MyText" } + end +end diff --git a/spec/models/question_spec.rb b/spec/models/question_spec.rb new file mode 100644 index 0000000000..5e95bcf838 --- /dev/null +++ b/spec/models/question_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Question, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end From dbccf92fb5ae40efe947022c796550105358d25c Mon Sep 17 00:00:00 2001 From: mariosantos-05 Date: Mon, 8 Dec 2025 11:24:44 -0300 Subject: [PATCH 073/100] =?UTF-8?q?Feature=20visualiza=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/admins_controller.rb | 180 +++++++++++------- app/controllers/avaliacoes_controller.rb | 23 ++- app/controllers/templates_controller.rb | 21 +- app/models/question.rb | 15 +- app/models/resposta.rb | 5 + app/models/template.rb | 15 +- app/views/admins/results.html.erb | 14 +- app/views/admins/show_respostas.html.erb | 54 ++++++ app/views/avaliacoes/responder.html.erb | 30 +-- app/views/templates/_form.html.erb | 144 +++++++++----- config/routes.rb | 9 + .../20251208135814_add_answers_to_resposta.rb | 5 + db/schema.rb | 3 +- 13 files changed, 340 insertions(+), 178 deletions(-) create mode 100644 app/views/admins/show_respostas.html.erb create mode 100644 db/migrate/20251208135814_add_answers_to_resposta.rb diff --git a/app/controllers/admins_controller.rb b/app/controllers/admins_controller.rb index 8c24d647cd..b9fefaaafd 100644 --- a/app/controllers/admins_controller.rb +++ b/app/controllers/admins_controller.rb @@ -1,83 +1,125 @@ require 'csv' class AdminsController < ApplicationController - before_action :require_admin, except: [:management] + before_action :require_admin, except: [:management] - def management - end - - def new_import - end + def management + end - def create_import - uploaded_file = params[:file] - - if uploaded_file.nil? - redirect_to import_sigaa_path, alert: 'Nenhum arquivo selecionado.' - return - end - - begin - SigaaImportService.new(uploaded_file.path).call - redirect_to admin_management_path, notice: 'Dados importados com sucesso!' - - rescue JSON::ParserError - redirect_to import_sigaa_path, alert: 'Erro: O arquivo enviado não é um JSON válido.' - - rescue SigaaImportService::InvalidFileError => e - redirect_to import_sigaa_path, alert: "Erro de Validação: #{e.message}" - - rescue StandardError => e - redirect_to import_sigaa_path, alert: "Ocorreu um erro inesperado: #{e.message}" - end - end + def new_import + end - def results - user = current_user - - admin_dept_id = user&.departamento_id || 1 + def create_import + uploaded_file = params[:file] - prefixo_departamento = case admin_dept_id - when 1 then "CIC" - when 2 then "MAT" - when 3 then "EST" - else "CIC" - end + if uploaded_file.nil? + redirect_to import_sigaa_path, alert: 'Nenhum arquivo selecionado.' + return + end - @turmas = Turma.where("nome LIKE ?", "%(#{prefixo_departamento}%") + begin + SigaaImportService.new(uploaded_file.path).call + redirect_to admin_management_path, notice: 'Dados importados com sucesso!' + + rescue JSON::ParserError + redirect_to import_sigaa_path, alert: 'Erro: O arquivo enviado não é um JSON válido.' + + rescue SigaaImportService::InvalidFileError => e + redirect_to import_sigaa_path, alert: "Erro de Validação: #{e.message}" + + rescue StandardError => e + redirect_to import_sigaa_path, alert: "Ocorreu um erro inesperado: #{e.message}" + end + end + + # =========================== + # RESULTADOS + # =========================== + def results + user = current_user + admin_dept_id = user&.departamento_id || 1 + + prefixo_departamento = case admin_dept_id + when 1 then "CIC" + when 2 then "MAT" + when 3 then "EST" + else "CIC" + end + + @turmas = Turma.where("nome LIKE ?", "%(#{prefixo_departamento}%") + end + + # =========================== + # VER RESPOSTAS DA TURMA + # =========================== + def show_respostas + @turma = Turma.find(params[:turma_id]) + + @forms = Form.where(turma_id: @turma.id) + + @respostas = Resposta + .where(form_id: @forms.pluck(:id)) + .includes(:usuario, form: { template: :questions }) + end + + # =========================== + # EXPORTAR CSV FINAL + # =========================== + def export_csv + @turma = Turma.find(params[:turma_id]) + + # Recupera os forms e respostas dessa turma + forms = Form.where(turma_id: @turma.id) + respostas = Resposta + .where(form_id: forms.pluck(:id)) + .includes(:usuario, form: { template: :questions }) + + if respostas.empty? + redirect_to admin_results_path, alert: "Este formulário ainda não possui respostas" + return end - def export_csv # TEM QUE MUDAR ESSA LÓGICA PARA GERAR O FORMULÁRIO REAL - @turma = Turma.find(params[:turma_id]) - - if @turma.vinculos.count == 0 - redirect_to admin_results_path, alert: "Este formulário ainda não possui respostas" - return - end - - codigo_materia = @turma.nome.match(/\((.*?)\s-/)&.captures&.first || "TURMA_#{@turma.id}" - - filename = "resultados_#{codigo_materia}.csv" - - csv_data = CSV.generate(headers: true) do |csv| - csv << ["Matricula", "Nome", "Email", "Perfil"] - - @turma.vinculos.each do |vinculo| - u = vinculo.usuario - csv << [u.matricula, u.nome, u.email, u.profile] - end - end - - send_data csv_data, filename: filename, type: 'text/csv' + # Pega todas as perguntas do template + perguntas = respostas.first.form.template.questions + + # Código da matéria + codigo_materia = @turma.nome.match(/\((.*?)\s-/)&.captures&.first || "TURMA_#{@turma.id}" + filename = "resultados_#{codigo_materia}.csv" + + csv_data = CSV.generate(headers: true) do |csv| + # Cabeçalho (dinâmico) + csv << ( + ["Matrícula", "Nome", "Email", "Perfil"] + + perguntas.map(&:text) + ) + + # Respostas + respostas.each do |resp| + user = resp.usuario + respostas_user = (resp.answers || {}) + + csv << ( + [ + user.matricula, + user.nome, + user.email, + user.profile + ] + + perguntas.map { |q| respostas_user[q.id.to_s] || "—" } + ) + end end - private + send_data csv_data, filename: filename, type: "text/csv" + end + + private - def require_admin - user = try(:current_user) - - if user.present? && user.profile != 'Admin' - redirect_to admin_management_path, alert: "Acesso negado" - end + def require_admin + user = try(:current_user) + + if user.present? && user.profile != 'Admin' + redirect_to admin_management_path, alert: "Acesso negado" end -end \ No newline at end of file + end +end diff --git a/app/controllers/avaliacoes_controller.rb b/app/controllers/avaliacoes_controller.rb index 5a06981537..4562c9af78 100644 --- a/app/controllers/avaliacoes_controller.rb +++ b/app/controllers/avaliacoes_controller.rb @@ -39,26 +39,35 @@ def responder def enviar_resposta form = Form.find(params[:form_id]) - - + unless current_user.vinculos.exists?(turma_id: form.turma_id) redirect_to avaliacoes_path, alert: "Você não tem acesso a esse formulário." return end - - + if Resposta.exists?(form_id: form.id, usuario_id: current_user.id) redirect_to avaliacoes_path, alert: "Você já respondeu este formulário." return end - + + normalized_answers = answers_params.transform_keys(&:to_s) + Resposta.create!( form_id: form.id, - usuario_id: current_user.id + usuario_id: current_user.id, + answers: normalized_answers ) - + redirect_to avaliacoes_path, notice: "Formulário enviado com sucesso!" end + + + private + + def answers_params + params.require(:answers).permit!.to_h + end + private diff --git a/app/controllers/templates_controller.rb b/app/controllers/templates_controller.rb index e29c428f2a..fc87f70d0a 100644 --- a/app/controllers/templates_controller.rb +++ b/app/controllers/templates_controller.rb @@ -1,29 +1,21 @@ class TemplatesController < ApplicationController - def index @templates = Template.all - # Cenário: Visualizar lista vazia - if @templates.empty? - flash.now[:notice] = "Nenhum template foi criado" - end + flash.now[:notice] = "Nenhum template foi criado" if @templates.empty? end def new @template = Template.new + @template.questions.build end def create @template = Template.new(template_params) - # Atribui o usuário logado - @template.criado_por = current_user - - # Simula questões vindas do form - @template.questions = params[:template][:questions] + @template.criado_por = current_user if @template.save redirect_to templates_path, notice: "Template criado com sucesso" else - # Cenários tristes: Título vazio ou Sem questões flash.now[:alert] = @template.errors.full_messages.join(", ") render :new, status: :unprocessable_entity end @@ -35,15 +27,11 @@ def edit def update @template = Template.find(params[:id]) - - # Simula questões - @template.questions = ["mock_question"] if @template.update(template_params) redirect_to templates_path, notice: "Template atualizado com sucesso" else - # Cenário Triste: Caracteres inválidos - flash.now[:alert] = @template.errors.full_messages.first + flash.now[:alert] = @template.errors.full_messages.join(", ") render :edit, status: :unprocessable_entity end end @@ -63,5 +51,4 @@ def template_params questions_attributes: [:id, :text, :question_type, :options, :_destroy] ) end - end diff --git a/app/models/question.rb b/app/models/question.rb index 85cc69153a..478fb2eb2c 100644 --- a/app/models/question.rb +++ b/app/models/question.rb @@ -1,7 +1,20 @@ class Question < ApplicationRecord belongs_to :template - serialize :options, Array + + # options é ARRAY no banco (via JSON) + attribute :options, :json, default: [] validates :text, presence: true validates :question_type, inclusion: { in: %w[text radio] } + + + def options=(value) + if value.is_a?(String) + super(value.split(",").map(&:strip)) + elsif value.nil? + super([]) + else + super(value) + end + end end diff --git a/app/models/resposta.rb b/app/models/resposta.rb index f9a08520fe..959d0e8a88 100644 --- a/app/models/resposta.rb +++ b/app/models/resposta.rb @@ -1,4 +1,9 @@ class Resposta < ApplicationRecord belongs_to :form belongs_to :usuario + + # answers será um hash armazenado no campo JSON; garantimos um hash vazio por padrão + def answers_hash + (self.answers || {}).with_indifferent_access + end end diff --git a/app/models/template.rb b/app/models/template.rb index 46964bc044..9418804f54 100644 --- a/app/models/template.rb +++ b/app/models/template.rb @@ -5,26 +5,19 @@ class Template < ApplicationRecord accepts_nested_attributes_for :questions, allow_destroy: true - attr_accessor :questions - validates :titulo, presence: { message: "O campo Título é obrigatório" } validates :titulo, format: { - with: /\A[a-zA-Z0-9\s\-\.]+\z/, - message: "Formato de título inválido" + with: /\A[a-zA-Z0-9\s\-\.]+\z/, + message: "Formato de título inválido" } - + validate :must_have_questions private def must_have_questions - if questions.blank? || (questions.respond_to?(:empty?) && questions.empty?) + if questions.reject(&:marked_for_destruction?).empty? errors.add(:base, "O template deve conter pelo menos uma questão") end end - - def questions_attributes_blank? - self.questions_attributes&.values&.all? { |q| q["text"].blank? } - end - end diff --git a/app/views/admins/results.html.erb b/app/views/admins/results.html.erb index d614d72035..f28d77cb90 100644 --- a/app/views/admins/results.html.erb +++ b/app/views/admins/results.html.erb @@ -24,15 +24,27 @@
<% @turmas.each do |turma| %>
+

<%= turma.nome %>

Respostas: <%= turma.vinculos.count %>

- <%= link_to export_results_csv_path(turma.id), class: "btn-action btn-primary-green", style: "width: auto; display: inline-block; padding: 8px 15px; font-size: 0.9rem;" do %> + + <%= link_to export_results_csv_path(turma.id), + class: "btn-action btn-primary-green", + style: "width: auto; display: inline-block; padding: 8px 15px; font-size: 0.9rem;" do %> Baixar CSV <% end %> + + + <%= link_to admin_turma_respostas_path(turma.id), + class: "btn-action btn-primary-green", + style: "width: auto; display: inline-block; padding: 8px 15px; font-size: 0.9rem;" do %> + Ver Respostas + <% end %> +
<% end %>
diff --git a/app/views/admins/show_respostas.html.erb b/app/views/admins/show_respostas.html.erb new file mode 100644 index 0000000000..04aae1f44b --- /dev/null +++ b/app/views/admins/show_respostas.html.erb @@ -0,0 +1,54 @@ +
+
+ +

+ Respostas – <%= @turma.nome %> +

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

Nenhuma resposta foi enviada ainda.

+ <% else %> + + <% @respostas.each do |resp| %> +
+ +

+ <%= resp.usuario.nome %> +

+ +

+ Enviado em: <%= resp.created_at.strftime("%d/%m/%Y %H:%M") %> +

+ +
+ + <% resp.form.template.questions.each do |q| %> +

+ <%= q.text %> +

+ +
+ <%= (resp.answers || {})[q.id.to_s] || "—" %> +
+ + <% end %> + +
+ <% end %> + + <% end %> + +
+
\ No newline at end of file diff --git a/app/views/avaliacoes/responder.html.erb b/app/views/avaliacoes/responder.html.erb index 66baa0455e..2eb13981c7 100644 --- a/app/views/avaliacoes/responder.html.erb +++ b/app/views/avaliacoes/responder.html.erb @@ -13,6 +13,7 @@
+ <%= form_with url: enviar_resposta_avaliacao_path(@form.id), method: :post, local: true do %> <% @form.template.questions.each_with_index do |q, index| %>
@@ -36,35 +37,12 @@
<% end %> - - <% questions.each_with_index do |q, index| %> -
-

- Pergunta <%= index + 1 %>: <%= q[:text] %> -

- - <% if q[:type] == "text" %> - - - <% elsif q[:type] == "radio" %> - <% q[:options].each do |opt| %> -
- -
- <% end %> +
+ <%= button_tag type: "submit", class: "submit-button" do %> + ➤ <% end %>
- <% end %> - <%= button_to enviar_resposta_avaliacao_path(@form.id), - method: :post, - form: { "data-turbo" => false }, - class: "submit-button" do %> - ➤ <% end %> -
\ No newline at end of file diff --git a/app/views/templates/_form.html.erb b/app/views/templates/_form.html.erb index 877eed9daa..c359ef56fd 100644 --- a/app/views/templates/_form.html.erb +++ b/app/views/templates/_form.html.erb @@ -1,58 +1,112 @@ <%= form_with(model: template, local: true) do |form| %> - - <% if template.errors.any? %> -
- <%= template.errors.full_messages.join(", ") %> -
- <% end %> -
+<% if template.errors.any? %> +
+ <%= template.errors.full_messages.join(", ") %> +
+<% end %> + +
<%= form.text_field :titulo, class: "input-field-line", placeholder: "Ex: Avaliação 2024" %> -
- -
-
Questão 1
- -
- - -
+
-
- - -
+ +
-
- - -
- - -
- -
-
Questão 2
- -
- - + +
+
Questão 1
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
-
- - + +
+
Questão 2
+ +
+ + +
+ +
+ + +
-
- +
+ - <%= form.submit (template.new_record? ? "Criar" : "Salvar"), class: "btn-create" %> + + +<%= form.submit (template.new_record? ? "Criar" : "Salvar"), class: "btn-create" %> <% end %> + + + + + \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 9beb1b6014..a6ce2ea300 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -21,4 +21,13 @@ post "avaliacoes/:form_id/enviar_resposta", to: "avaliacoes#enviar_resposta", as: "enviar_resposta_avaliacao" + + + get "turmas/:id/respostas", to: "results#show_respostas", as: :turma_respostas + + get "admins/results/:turma_id/respostas", + to: "admins#show_respostas", + as: :admin_turma_respostas + + end diff --git a/db/migrate/20251208135814_add_answers_to_resposta.rb b/db/migrate/20251208135814_add_answers_to_resposta.rb new file mode 100644 index 0000000000..8eaef120e6 --- /dev/null +++ b/db/migrate/20251208135814_add_answers_to_resposta.rb @@ -0,0 +1,5 @@ +class AddAnswersToResposta < ActiveRecord::Migration[8.0] + def change + add_column :resposta, :answers, :json + end +end diff --git a/db/schema.rb b/db/schema.rb index e3f72b0f9e..2689e8ca3b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_12_08_123405) do +ActiveRecord::Schema[8.0].define(version: 2025_12_08_135814) do create_table "forms", force: :cascade do |t| t.integer "template_id", null: false t.integer "turma_id", null: false @@ -72,6 +72,7 @@ t.integer "usuario_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.json "answers" t.index ["form_id"], name: "index_resposta_on_form_id" t.index ["usuario_id"], name: "index_resposta_on_usuario_id" end From 72a6ac42572809f0c74180494cee326f054cec7c Mon Sep 17 00:00:00 2001 From: mariosantos-05 Date: Mon, 8 Dec 2025 11:28:54 -0300 Subject: [PATCH 074/100] Comment fix --- app/controllers/admins_controller.rb | 9 --------- app/views/templates/_form.html.erb | 5 ++--- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/app/controllers/admins_controller.rb b/app/controllers/admins_controller.rb index b9fefaaafd..7c0aa5e82e 100644 --- a/app/controllers/admins_controller.rb +++ b/app/controllers/admins_controller.rb @@ -32,9 +32,6 @@ def create_import end end - # =========================== - # RESULTADOS - # =========================== def results user = current_user admin_dept_id = user&.departamento_id || 1 @@ -49,9 +46,6 @@ def results @turmas = Turma.where("nome LIKE ?", "%(#{prefixo_departamento}%") end - # =========================== - # VER RESPOSTAS DA TURMA - # =========================== def show_respostas @turma = Turma.find(params[:turma_id]) @@ -62,9 +56,6 @@ def show_respostas .includes(:usuario, form: { template: :questions }) end - # =========================== - # EXPORTAR CSV FINAL - # =========================== def export_csv @turma = Turma.find(params[:turma_id]) diff --git a/app/views/templates/_form.html.erb b/app/views/templates/_form.html.erb index c359ef56fd..e5c98ea854 100644 --- a/app/views/templates/_form.html.erb +++ b/app/views/templates/_form.html.erb @@ -11,10 +11,10 @@ <%= form.text_field :titulo, class: "input-field-line", placeholder: "Ex: Avaliação 2024" %>
- +
- +
Questão 1
@@ -39,7 +39,6 @@
-
Questão 2
From 6c92991c742b376cd62783e625a88841022a5bce Mon Sep 17 00:00:00 2001 From: mariosantos-05 Date: Mon, 8 Dec 2025 12:30:39 -0300 Subject: [PATCH 075/100] =?UTF-8?q?Visualiza=C3=A7=C3=A3o=20de=20turmas=20?= =?UTF-8?q?para=20teste?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/application_controller.rb | 47 +++++++++++++---------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 43022b55a0..d99be3493d 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -2,30 +2,37 @@ class ApplicationController < ActionController::Base helper_method :current_user allow_browser versions: :modern - - #This whole section is to mock user authentication for development/testing purposes. def current_user - fake_role = ENV["FAKE_ROLE"] || "admin" #default to admin + fake_role = ENV["FAKE_ROLE"] || "admin" - case fake_role - when "admin" - Usuario.find_or_create_by(profile: "Admin") do |u| - u.nome = "Fake Admin" - u.email = "admin@test.com" - u.matricula = "000" - u.status = true - u.departamento_id = 1 + user = + case fake_role + when "admin" + Usuario.find_or_create_by(profile: "Admin") do |u| + u.nome = "Fake Admin" + u.email = "admin@test.com" + u.matricula = "000" + u.status = true + u.departamento_id = 1 + end + when "user" + Usuario.find_or_create_by(profile: "User") do |u| + u.nome = "Fake User" + u.email = "user@test.com" + u.matricula = "111" + u.status = true + u.departamento_id = 1 + end + else + return nil end - when "user" - Usuario.find_or_create_by(profile: "User") do |u| - u.nome = "Fake User" - u.email = "user@test.com" - u.matricula = "111" - u.status = true - u.departamento_id = 1 + + Turma.find_each do |turma| + Vinculo.find_or_create_by(usuario_id: user.id, turma_id: turma.id) do |v| + v.papel_turma = 0 end - else - nil end + + user end end From 0978fc323fba05666b186ff8575deb254e43810a Mon Sep 17 00:00:00 2001 From: Carol <232050975@aluno.unb.br> Date: Mon, 8 Dec 2025 14:33:09 -0300 Subject: [PATCH 076/100] =?UTF-8?q?Ajustes=20na=20gera=C3=A7=C3=A3o=20do?= =?UTF-8?q?=20arquivo=20csv=20e=20na=20sidebar=20do=20site=20em=20certas?= =?UTF-8?q?=20p=C3=A1ginas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/assets/stylesheets/admin_templates.css | 11 +++- app/controllers/admins_controller.rb | 40 ++++++------- app/controllers/avaliacoes_controller.rb | 18 ++++-- app/models/template.rb | 21 +------ app/views/admins/management.html.erb | 2 +- app/views/admins/results.html.erb | 2 +- app/views/avaliacoes/responder.html.erb | 69 +++++++++++++--------- config/routes.rb | 2 - db/schema.rb | 53 ----------------- 9 files changed, 82 insertions(+), 136 deletions(-) diff --git a/app/assets/stylesheets/admin_templates.css b/app/assets/stylesheets/admin_templates.css index d977c0a5a9..edea579aa5 100644 --- a/app/assets/stylesheets/admin_templates.css +++ b/app/assets/stylesheets/admin_templates.css @@ -30,11 +30,15 @@ body { .sidebar { width: 257px; - min-width: 257px; + min-width: 0; background: #FFFFFF; border-right: 1px solid #D9D9D9; display: flex; flex-direction: column; + + transition: width 0.3s ease; + white-space: nowrap; + overflow-x: hidden; } .sidebar-item { @@ -54,6 +58,11 @@ body { color: #FFFFFF; } +.sidebar.collapsed { + width: 0; + border: none; +} + .main-content { flex: 1; background: #DBDBDB; diff --git a/app/controllers/admins_controller.rb b/app/controllers/admins_controller.rb index 7c0aa5e82e..43eb01ab23 100644 --- a/app/controllers/admins_controller.rb +++ b/app/controllers/admins_controller.rb @@ -33,17 +33,18 @@ def create_import end def results - user = current_user - admin_dept_id = user&.departamento_id || 1 + user = current_user + + admin_dept_id = user&.departamento_id || 1 prefixo_departamento = case admin_dept_id - when 1 then "CIC" - when 2 then "MAT" - when 3 then "EST" - else "CIC" - end + when 1 then "CIC" + when 2 then "MAT" + when 3 then "EST" + else "CIC" + end - @turmas = Turma.where("nome LIKE ?", "%(#{prefixo_departamento}%") + @turmas = Turma.where("nome LIKE ?", "%(#{prefixo_departamento}%") end def show_respostas @@ -59,42 +60,35 @@ def show_respostas def export_csv @turma = Turma.find(params[:turma_id]) - # Recupera os forms e respostas dessa turma forms = Form.where(turma_id: @turma.id) + respostas = Resposta .where(form_id: forms.pluck(:id)) - .includes(:usuario, form: { template: :questions }) + .includes(form: { template: :questions }) if respostas.empty? - redirect_to admin_results_path, alert: "Este formulário ainda não possui respostas" + redirect_to admin_results_path, alert: "Este formulário ainda não possui respostas." return end - # Pega todas as perguntas do template - perguntas = respostas.first.form.template.questions + perguntas = forms.first.template.questions.order(:id) - # Código da matéria codigo_materia = @turma.nome.match(/\((.*?)\s-/)&.captures&.first || "TURMA_#{@turma.id}" - filename = "resultados_#{codigo_materia}.csv" + filename = "resultados_anonimos_#{codigo_materia}.csv" csv_data = CSV.generate(headers: true) do |csv| - # Cabeçalho (dinâmico) + csv << ( - ["Matrícula", "Nome", "Email", "Perfil"] + + ["Data do Envio"] + perguntas.map(&:text) ) - # Respostas respostas.each do |resp| - user = resp.usuario respostas_user = (resp.answers || {}) csv << ( [ - user.matricula, - user.nome, - user.email, - user.profile + resp.created_at.strftime("%d/%m/%Y %H:%M") ] + perguntas.map { |q| respostas_user[q.id.to_s] || "—" } ) diff --git a/app/controllers/avaliacoes_controller.rb b/app/controllers/avaliacoes_controller.rb index 4562c9af78..6c45b6684a 100644 --- a/app/controllers/avaliacoes_controller.rb +++ b/app/controllers/avaliacoes_controller.rb @@ -26,11 +26,13 @@ def index def responder @form = Form.includes(:template, :turma).find(params[:form_id]) + # Verifica se o usuário participa da turma unless current_user.vinculos.exists?(turma_id: @form.turma_id) redirect_to avaliacoes_path, alert: "Você não tem acesso a esse formulário." return end + # 🚨 Impede que responda mais de 1 vez if Resposta.exists?(form_id: @form.id, usuario_id: current_user.id) redirect_to avaliacoes_path, alert: "Você já respondeu este formulário." return @@ -39,25 +41,29 @@ def responder def enviar_resposta form = Form.find(params[:form_id]) - + + # Verifica permissão unless current_user.vinculos.exists?(turma_id: form.turma_id) redirect_to avaliacoes_path, alert: "Você não tem acesso a esse formulário." return end - + + # 🚨 Impede que o aluno envie novamente (segurança) if Resposta.exists?(form_id: form.id, usuario_id: current_user.id) redirect_to avaliacoes_path, alert: "Você já respondeu este formulário." return end - - normalized_answers = answers_params.transform_keys(&:to_s) - + + normalized_answers = params[:answers] || {} + + # 👉 Aqui criamos o registro dizendo que ESTE aluno respondeu Resposta.create!( form_id: form.id, usuario_id: current_user.id, answers: normalized_answers ) - + + # ❗ Nada de destruir o form — cada aluno responde o mesmo form redirect_to avaliacoes_path, notice: "Formulário enviado com sucesso!" end diff --git a/app/models/template.rb b/app/models/template.rb index 9418804f54..20545d4b4c 100644 --- a/app/models/template.rb +++ b/app/models/template.rb @@ -1,23 +1,4 @@ class Template < ApplicationRecord - belongs_to :criado_por, class_name: 'Usuario' - has_many :forms, dependent: :restrict_with_error + belongs_to :criado_por, class_name: "Usuario" has_many :questions, dependent: :destroy - - accepts_nested_attributes_for :questions, allow_destroy: true - - validates :titulo, presence: { message: "O campo Título é obrigatório" } - validates :titulo, format: { - with: /\A[a-zA-Z0-9\s\-\.]+\z/, - message: "Formato de título inválido" - } - - validate :must_have_questions - - private - - def must_have_questions - if questions.reject(&:marked_for_destruction?).empty? - errors.add(:base, "O template deve conter pelo menos uma questão") - end - end end diff --git a/app/views/admins/management.html.erb b/app/views/admins/management.html.erb index 247ad020ff..93cebdd9f5 100644 --- a/app/views/admins/management.html.erb +++ b/app/views/admins/management.html.erb @@ -22,7 +22,7 @@
<%= link_to "Importar dados", import_sigaa_path, class: "btn-action btn-primary-green" %> - <%= link_to "Templates", templates_path, class: "btn-action btn-primary-green" %> + <%= link_to "Criar Formulário", new_admins_form_path, data: { turbo_frame: "modal" }, class: "btn-action btn-primary-green" %> <%= link_to "Resultados", admin_results_path, class: "btn-action btn-primary-green" %>
diff --git a/app/views/admins/results.html.erb b/app/views/admins/results.html.erb index f28d77cb90..787bc1d586 100644 --- a/app/views/admins/results.html.erb +++ b/app/views/admins/results.html.erb @@ -28,7 +28,7 @@

<%= turma.nome %>

- Respostas: <%= turma.vinculos.count %> + Respostas: <%= Resposta.where(form_id: turma.forms.ids).count %>

diff --git a/app/views/avaliacoes/responder.html.erb b/app/views/avaliacoes/responder.html.erb index 2eb13981c7..ca809f279c 100644 --- a/app/views/avaliacoes/responder.html.erb +++ b/app/views/avaliacoes/responder.html.erb @@ -1,7 +1,7 @@
-
@@ -11,38 +11,49 @@
-
-
- <%= form_with url: enviar_resposta_avaliacao_path(@form.id), method: :post, local: true do %> - - <% @form.template.questions.each_with_index do |q, index| %> -
-

- Pergunta <%= index + 1 %>: <%= q.text %> -

- - <% if q.question_type == "text" %> - - - <% elsif q.question_type == "radio" %> - <% q.options.each do |opt| %> -
- -
+
+ +
+
+ <%= form_with url: enviar_resposta_avaliacao_path(@form.id), method: :post, local: true do %> + + <% @form.template.questions.each_with_index do |q, index| %> +
+

+ Pergunta <%= index + 1 %>: <%= q.text %> +

+ + <% if q.question_type == "text" %> + + + <% elsif q.question_type == "radio" %> + <% q.options.each do |opt| %> +
+ +
+ <% end %> + <% end %> +
<% end %> -
- <% end %> -
- <%= button_tag type: "submit", class: "submit-button" do %> - ➤ +
+ <%= button_tag type: "submit", class: "submit-button", style: "cursor: pointer;" do %> + ➤ + <% end %> +
+ <% end %>
- - <% end %>
\ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index a6ce2ea300..029288f234 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -15,8 +15,6 @@ get "avaliacoes/:turma_id/forms/:form_id/responder", to: "avaliacoes#responder", as: "responder_form" - resources :templates - resources :forms, only: [:new, :create] post "avaliacoes/:form_id/enviar_resposta", to: "avaliacoes#enviar_resposta", diff --git a/db/schema.rb b/db/schema.rb index 2689e8ca3b..b38cdf7cdf 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -21,42 +21,6 @@ t.index ["turma_id"], name: "index_forms_on_turma_id" end - create_table "issues", force: :cascade do |t| - t.string "title" - t.text "description" - t.string "status" - t.integer "project_id", null: false - t.integer "user_id", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.index ["project_id"], name: "index_issues_on_project_id" - t.index ["user_id"], name: "index_issues_on_user_id" - end - - create_table "members", force: :cascade do |t| - t.string "nome" - t.string "matricula" - t.string "curso" - t.string "usuario" - t.string "formacao" - t.string "ocupacao" - t.string "email" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - end - - create_table "members_turmas", id: false, force: :cascade do |t| - t.integer "turma_id", null: false - t.integer "member_id", null: false - end - - create_table "projects", force: :cascade do |t| - t.string "title" - t.text "description" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - end - create_table "questions", force: :cascade do |t| t.integer "template_id", null: false t.string "text" @@ -77,13 +41,6 @@ t.index ["usuario_id"], name: "index_resposta_on_usuario_id" end - create_table "subjects", force: :cascade do |t| - t.string "code" - t.string "name" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - end - create_table "templates", force: :cascade do |t| t.string "titulo" t.string "target_audience" @@ -101,14 +58,6 @@ t.datetime "updated_at", null: false end - create_table "users", force: :cascade do |t| - t.string "name" - t.string "email" - t.string "role" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - end - create_table "usuarios", force: :cascade do |t| t.string "nome" t.string "matricula" @@ -132,8 +81,6 @@ add_foreign_key "forms", "templates" add_foreign_key "forms", "turmas" - add_foreign_key "issues", "projects" - add_foreign_key "issues", "users" add_foreign_key "questions", "templates" add_foreign_key "resposta", "forms" add_foreign_key "resposta", "usuarios" From 4c532fa3e831dc0613703ce168507373355b4d28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Mon, 8 Dec 2025 16:29:50 -0300 Subject: [PATCH 077/100] Update template.rb sla --- app/models/template.rb | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/app/models/template.rb b/app/models/template.rb index 20545d4b4c..ab45069ce5 100644 --- a/app/models/template.rb +++ b/app/models/template.rb @@ -1,4 +1,37 @@ class Template < ApplicationRecord - belongs_to :criado_por, class_name: "Usuario" - has_many :questions, dependent: :destroy + + + belongs_to :criado_por, class_name: 'Usuario' + + has_many :forms, dependent: :restrict_with_error + + attr_accessor :questions + + # Validações cenários tristes do BDD + + + validates :titulo, presence: { message: "O campo Título é obrigatório" } + + validates :titulo, format: { + + with: /\A[a-zA-Z0-9\s\-\.]+\z/, + message: "Formato de título inválido" + + + } + + validate :must_have_questions + + private + + def must_have_questions + + if questions.blank? || (questions.respond_to?(:empty?) && questions.empty?) + + errors.add(:base, "O template deve conter pelo menos uma questão") + + end + + end + end From 80949f212445fb1f71d2303f7225c038cdad2b33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Mon, 8 Dec 2025 16:33:19 -0300 Subject: [PATCH 078/100] Update routes.rb --- config/routes.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/config/routes.rb b/config/routes.rb index 029288f234..d9c51ece1a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -8,6 +8,7 @@ get 'resultados/:turma_id/csv', to: 'admins#export_csv', as: 'export_results_csv' namespace :admins do + resources :templates resources :forms, only: [:new, :create] end From c0f45076a52d2a9c9642fd72ab4f2708620da4be Mon Sep 17 00:00:00 2001 From: Carol <232050975@aluno.unb.br> Date: Mon, 8 Dec 2025 17:33:33 -0300 Subject: [PATCH 079/100] =?UTF-8?q?Corre=C3=A7=C3=A3o=20na=20parte=20dos?= =?UTF-8?q?=20templates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{ => admins}/templates_controller.rb | 33 ++++++++++++------- app/views/admins/management.html.erb | 4 +-- .../{ => admins}/templates/_form.html.erb | 2 +- .../_shared_layout_structure.html.erb | 15 +++++---- .../{ => admins}/templates/edit.html.erb | 0 .../{ => admins}/templates/index.html.erb | 20 +++++------ app/views/{ => admins}/templates/new.html.erb | 4 --- spec/requests/templates_spec.rb | 2 +- 8 files changed, 44 insertions(+), 36 deletions(-) rename app/controllers/{ => admins}/templates_controller.rb (57%) rename app/views/{ => admins}/templates/_form.html.erb (98%) rename app/views/{ => admins}/templates/_shared_layout_structure.html.erb (58%) rename app/views/{ => admins}/templates/edit.html.erb (100%) rename app/views/{ => admins}/templates/index.html.erb (82%) rename app/views/{ => admins}/templates/new.html.erb (70%) diff --git a/app/controllers/templates_controller.rb b/app/controllers/admins/templates_controller.rb similarity index 57% rename from app/controllers/templates_controller.rb rename to app/controllers/admins/templates_controller.rb index fc87f70d0a..7111c75dbd 100644 --- a/app/controllers/templates_controller.rb +++ b/app/controllers/admins/templates_controller.rb @@ -1,21 +1,31 @@ -class TemplatesController < ApplicationController +class Admins::TemplatesController < ApplicationController + def index @templates = Template.all - flash.now[:notice] = "Nenhum template foi criado" if @templates.empty? + + # Cenário: Visualizar lista vazia + if @templates.empty? + flash.now[:notice] = "Nenhum template foi criado" + end end def new @template = Template.new - @template.questions.build end def create @template = Template.new(template_params) - @template.criado_por = current_user + + # Atribui o usuário logado + @template.criado_por = current_user + + # Simula questões vindas do form + @template.questions = params[:template][:questions] if @template.save redirect_to templates_path, notice: "Template criado com sucesso" else + # Cenários tristes: Título vazio ou Sem questões flash.now[:alert] = @template.errors.full_messages.join(", ") render :new, status: :unprocessable_entity end @@ -28,10 +38,15 @@ def edit def update @template = Template.find(params[:id]) + # Simula questões + @template.questions = ["mock_question"] + + if @template.update(template_params) redirect_to templates_path, notice: "Template atualizado com sucesso" else - flash.now[:alert] = @template.errors.full_messages.join(", ") + # Cenário Triste: Caracteres inválidos + flash.now[:alert] = @template.errors.full_messages.first render :edit, status: :unprocessable_entity end end @@ -45,10 +60,6 @@ def destroy private def template_params - params.require(:template).permit( - :titulo, - :target_audience, - questions_attributes: [:id, :text, :question_type, :options, :_destroy] - ) + params.require(:template).permit(:titulo, :target_audience) end -end +end \ No newline at end of file diff --git a/app/views/admins/management.html.erb b/app/views/admins/management.html.erb index 93cebdd9f5..5411baa5a0 100644 --- a/app/views/admins/management.html.erb +++ b/app/views/admins/management.html.erb @@ -13,7 +13,7 @@
@@ -22,7 +22,7 @@
<%= link_to "Importar dados", import_sigaa_path, class: "btn-action btn-primary-green" %> - + <%= link_to "Editar Templates", admins_templates_path, class: "btn-action btn-primary-green" %> <%= link_to "Criar Formulário", new_admins_form_path, data: { turbo_frame: "modal" }, class: "btn-action btn-primary-green" %> <%= link_to "Resultados", admin_results_path, class: "btn-action btn-primary-green" %>
diff --git a/app/views/templates/_form.html.erb b/app/views/admins/templates/_form.html.erb similarity index 98% rename from app/views/templates/_form.html.erb rename to app/views/admins/templates/_form.html.erb index e5c98ea854..9c446a75cf 100644 --- a/app/views/templates/_form.html.erb +++ b/app/views/admins/templates/_form.html.erb @@ -1,4 +1,4 @@ -<%= form_with(model: template, local: true) do |form| %> +<%= form_with(model: [:admins, template], local: true) do |form| %> <% if template.errors.any? %>
diff --git a/app/views/templates/_shared_layout_structure.html.erb b/app/views/admins/templates/_shared_layout_structure.html.erb similarity index 58% rename from app/views/templates/_shared_layout_structure.html.erb rename to app/views/admins/templates/_shared_layout_structure.html.erb index a93a4d2953..3fa08d948b 100644 --- a/app/views/templates/_shared_layout_structure.html.erb +++ b/app/views/admins/templates/_shared_layout_structure.html.erb @@ -1,17 +1,20 @@
-
-
\ No newline at end of file +
From 5d90b923573d78303011a14c1909df2e4330aa01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Mon, 8 Dec 2025 21:09:28 -0300 Subject: [PATCH 090/100] =?UTF-8?q?Testando=20deixar=20a=20op=C3=A7=C3=A3o?= =?UTF-8?q?=20em=20branco?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/admins/forms/_form.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/admins/forms/_form.html.erb b/app/views/admins/forms/_form.html.erb index 2ede9a6c10..784b65961c 100644 --- a/app/views/admins/forms/_form.html.erb +++ b/app/views/admins/forms/_form.html.erb @@ -14,7 +14,7 @@ <%= select_tag :template_id, options_from_collection_for_select(@templates, :id, :titulo), - prompt: "Selecione um template", + prompt: " ", class: "select-template" %>
From b3eb4a608d6ebff459b49da52f392d2b4756b038 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Mon, 8 Dec 2025 21:20:29 -0300 Subject: [PATCH 091/100] mudando respostum para resposta, provavel motivo de erro no rspec --- spec/models/{respostum_spec.rb => resposta_spec.rb} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename spec/models/{respostum_spec.rb => resposta_spec.rb} (66%) diff --git a/spec/models/respostum_spec.rb b/spec/models/resposta_spec.rb similarity index 66% rename from spec/models/respostum_spec.rb rename to spec/models/resposta_spec.rb index 050980cbac..dda31522ab 100644 --- a/spec/models/respostum_spec.rb +++ b/spec/models/resposta_spec.rb @@ -1,5 +1,5 @@ require 'rails_helper' -RSpec.describe Respostum, type: :model do +RSpec.describe Resposta, type: :model do pending "add some examples to (or delete) #{__FILE__}" end From 28ec9c5c9936e00b74a874984809aecf097a5474 Mon Sep 17 00:00:00 2001 From: Eduardo Date: Mon, 8 Dec 2025 21:53:09 -0300 Subject: [PATCH 092/100] =?UTF-8?q?Feat:=20Finaliza=20autentica=C3=A7?= =?UTF-8?q?=C3=A3o=20e=20valida=C3=A7=C3=A3o=20dos=20usu=C3=A1rios=20com?= =?UTF-8?q?=20testes=20passando=20(Issues=20#6,=20#10,=20#11)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/dashboard_controller.rb | 4 +++ app/controllers/first_access_controller.rb | 33 +++++++++++++++++++++ app/controllers/sessions_controller.rb | 2 +- app/views/dashboard/index.html.erb | 2 ++ app/views/first_access/edit.html.erb | 34 ++++++++++++++++++++++ config/routes.rb | 4 +-- spec/models/user_spec.rb | 5 ---- spec/requests/auth_spec.rb | 2 +- spec/requests/first_access_spec.rb | 34 ++++++++++++++++++++++ 9 files changed, 111 insertions(+), 9 deletions(-) create mode 100644 app/controllers/dashboard_controller.rb create mode 100644 app/controllers/first_access_controller.rb create mode 100644 app/views/dashboard/index.html.erb create mode 100644 app/views/first_access/edit.html.erb delete mode 100644 spec/models/user_spec.rb create mode 100644 spec/requests/first_access_spec.rb diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb new file mode 100644 index 0000000000..40d9972a37 --- /dev/null +++ b/app/controllers/dashboard_controller.rb @@ -0,0 +1,4 @@ +class DashboardController < ApplicationController + def index + end +end \ No newline at end of file diff --git a/app/controllers/first_access_controller.rb b/app/controllers/first_access_controller.rb new file mode 100644 index 0000000000..9612d3854c --- /dev/null +++ b/app/controllers/first_access_controller.rb @@ -0,0 +1,33 @@ +class FirstAccessController < ApplicationController + # GET /first_access/:id/edit + def edit + @user = User.find(params[:id]) + + # RN-DS-01 (Indireto): Se o usuário já estiver ativo, o link "expira" + if @user.status == true + redirect_to root_path, alert: "Sua conta já foi ativada. Faça login." + end + end + + # PATCH /first_access/:id + def update + @user = User.find(params[:id]) + + # Verifica se as senhas batem e se atendem aos requisitos (RN-DS-02, RN-DS-03) + # E muda o status para true (RN-DS-04) + if @user.update(user_params.merge(status: true)) + # Loga o usuário automaticamente após definir a senha + session[:user_id] = @user.id + redirect_to avaliacoes_path, notice: "Senha definida com sucesso! Bem-vindo." + else + # Se der erro (senha fraca ou não coincidir), volta para a tela de edição + render :edit, status: :unprocessable_entity + end + end + + private + + def user_params + params.require(:user).permit(:password, :password_confirmation) + end +end \ No newline at end of file diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 96f6b88974..018b06a3f3 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -36,6 +36,6 @@ def destroy def destination_by_profile(user) # RN-L-04 e RN-L-05 - user.profile == 'Admin' ? admin_dashboard_path : user_dashboard_path + user.profile == 'Admin' ? admin_management_path : avaliacoes_path end end \ No newline at end of file diff --git a/app/views/dashboard/index.html.erb b/app/views/dashboard/index.html.erb new file mode 100644 index 0000000000..1d75fc6686 --- /dev/null +++ b/app/views/dashboard/index.html.erb @@ -0,0 +1,2 @@ +

Bem-vindo ao Dashboard do Aluno!

+<%= button_to "Sair", logout_path, method: :delete, class: "bg-red-500 text-white p-2 rounded" %> \ No newline at end of file diff --git a/app/views/first_access/edit.html.erb b/app/views/first_access/edit.html.erb new file mode 100644 index 0000000000..ccda40a69d --- /dev/null +++ b/app/views/first_access/edit.html.erb @@ -0,0 +1,34 @@ +
+
+

Primeiro Acesso

+

Defina sua senha para ativar sua conta.

+ + <%= form_with(model: @user, url: first_access_path(@user), local: true, method: :patch) do |form| %> + + <% if @user.errors.any? %> +
+ Atenção! +
    + <% @user.errors.full_messages.each do |message| %> +
  • <%= message %>
  • + <% end %> +
+
+ <% end %> + +
+ <%= form.label :password, "Nova Senha", class: "block text-gray-700 text-sm font-bold mb-2" %> + <%= form.password_field :password, class: "shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline", placeholder: "Mínimo 8 caracteres, letras e números" %> +
+ +
+ <%= form.label :password_confirmation, "Confirme a Senha", class: "block text-gray-700 text-sm font-bold mb-2" %> + <%= form.password_field :password_confirmation, class: "shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline", placeholder: "Repita a senha" %> +
+ +
+ <%= form.submit "Ativar Conta", class: "bg-action-green hover:bg-green-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline w-full cursor-pointer" %> +
+ <% end %> +
+
\ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index e5ff6022fe..6c58f41bb7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -32,6 +32,6 @@ post '/admin/import', to: 'imports#create' # Dashboards (apenas para redirecionamento) - get '/dashboard', to: 'dashboard#index', as: :user_dashboard - get '/admin/dashboard', to: 'admin#index', as: :admin_dashboard + get '/dashboard_placeholder', to: proc { [200, {}, ['Avaliações (Em construção)']] }, as: :avaliacoes + #get '/admin_placeholder', to: proc { [200, {}, ['Gerenciamento Admin (Em construção)']] }, as: :admin_management end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb deleted file mode 100644 index 47a31bb435..0000000000 --- a/spec/models/user_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'rails_helper' - -RSpec.describe User, type: :model do - pending "add some examples to (or delete) #{__FILE__}" -end diff --git a/spec/requests/auth_spec.rb b/spec/requests/auth_spec.rb index dc06fe912e..d3c111d683 100644 --- a/spec/requests/auth_spec.rb +++ b/spec/requests/auth_spec.rb @@ -14,7 +14,7 @@ describe "POST /login" do it "loga com sucesso e redireciona" do post login_path, params: { login: '190075384', password: 'Pass123()' } - expect(response).to redirect_to(user_dashboard_path) # Verifica se foi pro dashboard + expect(response).to redirect_to(avaliacoes_path) # Verifica se foi pro dashboard end it "falha com senha errada" do diff --git a/spec/requests/first_access_spec.rb b/spec/requests/first_access_spec.rb new file mode 100644 index 0000000000..82a9783043 --- /dev/null +++ b/spec/requests/first_access_spec.rb @@ -0,0 +1,34 @@ +require 'rails_helper' + +RSpec.describe "Primeiro Acesso", type: :request do + # Usuário importado (status: false) + let!(:user_pendente) { User.create!(matricula: '12345', email: 'novo@unb.br', password: 'Temp123()', status: false, profile: 'Aluno') } + + # Usuário já ativo (status: true) + let!(:user_ativo) { User.create!(matricula: '67890', email: 'ativo@unb.br', password: 'Pass123()', status: true, profile: 'Aluno') } + + describe "GET /first_access/:id/edit" do + it "acessa a tela de definir senha se o usuário estiver pendente" do + get edit_first_access_path(user_pendente) + expect(response).to have_http_status(200) + end + + it "redireciona se o usuário já estiver ativo" do + get edit_first_access_path(user_ativo) + expect(response).to redirect_to(root_path) + end + end + + describe "PATCH /first_access/:id" do + it "ativa a conta com senha válida" do + patch first_access_path(user_pendente), params: { + user: { password: 'NewPass123!', password_confirmation: 'NewPass123!' } + } + + user_pendente.reload + expect(user_pendente.status).to be true # Verifica se virou Ativo + expect(session[:user_id]).to eq(user_pendente.id) # Verifica se logou + expect(response).to redirect_to(avaliacoes_path) # Ajuste a rota se necessário + end + end +end \ No newline at end of file From 54e16dd653bc3176f7ff37ae22ad246f64cab56b Mon Sep 17 00:00:00 2001 From: Eduardo Date: Mon, 8 Dec 2025 23:23:58 -0300 Subject: [PATCH 093/100] =?UTF-8?q?Adi=C3=A7=C3=A3o=20de=20Admin=20para=20?= =?UTF-8?q?Teste=20Completo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/application_controller.rb | 59 +++++++++++----------- app/controllers/first_access_controller.rb | 6 +-- app/controllers/sessions_controller.rb | 28 +++++++--- db/seeds.rb | 19 +++++++ 4 files changed, 72 insertions(+), 40 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index d99be3493d..0f46bf9e26 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,38 +1,39 @@ class ApplicationController < ActionController::Base - helper_method :current_user + # Configuração padrão do Rails 8 (mantenha) allow_browser versions: :modern + # Disponibiliza estes métodos para serem usados nas Views (ex: no navbar) + helper_method :current_user, :logged_in?, :admin? + + # Retorna o usuário logado buscando pela sessão def current_user - fake_role = ENV["FAKE_ROLE"] || "admin" + # Se já carregou, usa a variável de instância. + # Se não, busca no banco usando o ID salvo no cookie de sessão. + @current_user ||= Usuario.find_by(id: session[:usuario_id]) if session[:usuario_id] + end - user = - case fake_role - when "admin" - Usuario.find_or_create_by(profile: "Admin") do |u| - u.nome = "Fake Admin" - u.email = "admin@test.com" - u.matricula = "000" - u.status = true - u.departamento_id = 1 - end - when "user" - Usuario.find_or_create_by(profile: "User") do |u| - u.nome = "Fake User" - u.email = "user@test.com" - u.matricula = "111" - u.status = true - u.departamento_id = 1 - end - else - return nil - end + # Retorna true se existe alguém logado + def logged_in? + !!current_user + end - Turma.find_each do |turma| - Vinculo.find_or_create_by(usuario_id: user.id, turma_id: turma.id) do |v| - v.papel_turma = 0 + # Retorna true se o usuário logado é Admin + def admin? + logged_in? && current_user.profile == 'Admin' + end + + # Filtro de segurança para usar com 'before_action' nos controllers de Admin + def require_admin + unless admin? + flash[:alert] = "Acesso negado. Apenas administradores." + + if logged_in? + # Se é aluno tentando acessar área admin, manda pro dashboard de aluno + redirect_to avaliacoes_path + else + # Se não tá logado, manda pro login + redirect_to root_path end end - - user end -end +end \ No newline at end of file diff --git a/app/controllers/first_access_controller.rb b/app/controllers/first_access_controller.rb index 9612d3854c..99761200fa 100644 --- a/app/controllers/first_access_controller.rb +++ b/app/controllers/first_access_controller.rb @@ -1,7 +1,7 @@ class FirstAccessController < ApplicationController # GET /first_access/:id/edit def edit - @user = User.find(params[:id]) + @user = Usuario.find(params[:id]) # RN-DS-01 (Indireto): Se o usuário já estiver ativo, o link "expira" if @user.status == true @@ -11,13 +11,13 @@ def edit # PATCH /first_access/:id def update - @user = User.find(params[:id]) + @user = Usuario.find(params[:id]) # Verifica se as senhas batem e se atendem aos requisitos (RN-DS-02, RN-DS-03) # E muda o status para true (RN-DS-04) if @user.update(user_params.merge(status: true)) # Loga o usuário automaticamente após definir a senha - session[:user_id] = @user.id + session[:usuario_id] = @user.id redirect_to avaliacoes_path, notice: "Senha definida com sucesso! Bem-vindo." else # Se der erro (senha fraca ou não coincidir), volta para a tela de edição diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 018b06a3f3..4ce36ee723 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -9,26 +9,38 @@ def new def create # RN-L-01: Autenticar por email OU matrícula login = params[:login] - user = User.find_by(email: login) || User.find_by(matricula: login) + user = Usuario.find_by(email: login) || Usuario.find_by(matricula: login) # RN-L-06: Só permite login se status for true (Ativo) - if user && user.authenticate(params[:password]) - if user.status == true - session[:user_id] = user.id + if user + # 1. Sem senha definida (senha é nil) + if user.password_digest.nil? + redirect_to edit_first_access_path(user), notice: "Bem-vindo! Por favor, defina sua senha para continuar." + return + end + + # 2. Conta inativa (status é false) + if user.status == false + redirect_to edit_first_access_path(user), alert: "Sua conta ainda não foi ativada." + return + end + # 3. Tentativa de Login + if user.authenticate(params[:password]) + session[:usuario_id] = user.id redirect_to destination_by_profile(user) else - flash.now[:alert] = "Sua conta ainda está pendente. Verifique seu email." + flash.now[:alert] = "Senha incorreta" render :new end else - # RN-L-03: Mensagem genérica - flash.now[:alert] = "Email/Matrícula ou senha inválidos" + # Usuário não encontrado + flash.now[:alert] = "E-mail ou matrícula não encontrados" render :new end end def destroy - session[:user_id] = nil + session[:usuario_id] = nil redirect_to root_path, notice: "Logout realizado com sucesso" end diff --git a/db/seeds.rb b/db/seeds.rb index 4fbd6ed970..b617eaf05d 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -7,3 +7,22 @@ # ["Action", "Comedy", "Drama", "Horror"].each do |genre_name| # MovieGenre.find_or_create_by!(name: genre_name) # end + +# db/seeds.rb + +puts "Criando usuário Admin..." + +# Cria o Admin apenas se ele não existir +Usuario.find_or_create_by!(matricula: 'admin') do |u| + u.nome = "Administrador do Sistema" + u.email = "admin@camaar.unb.br" + u.password = "Admin123" # Precisa atender ao regex (Maiúscula, minúscula, número) + u.password_confirmation = "Admin123" + u.profile = "Admin" # Perfil exato que o controller verifica + u.status = true # status: true para pular a etapa de Primeiro Acesso + u.departamento_id = 1 # ID fictício, já que ainda não temos tabela de departamentos +end + +puts "Admin criado com sucesso!" +puts "Login: admin (ou admin@camaar.unb.br)" +puts "Senha: Admin123" \ No newline at end of file From ef462554d7f65933d3e989b3a21d74a96da5c5e2 Mon Sep 17 00:00:00 2001 From: Eduardo Date: Mon, 8 Dec 2025 23:35:47 -0300 Subject: [PATCH 094/100] =?UTF-8?q?Corre=C3=A7=C3=A3o=20Erro=20Primeiro=20?= =?UTF-8?q?Acesso?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/first_access_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/first_access_controller.rb b/app/controllers/first_access_controller.rb index 99761200fa..8fe96858cc 100644 --- a/app/controllers/first_access_controller.rb +++ b/app/controllers/first_access_controller.rb @@ -28,6 +28,6 @@ def update private def user_params - params.require(:user).permit(:password, :password_confirmation) + params.require(:usuario).permit(:password, :password_confirmation) end end \ No newline at end of file From dd761340369b95561874cff8dd9cff62131f7cc4 Mon Sep 17 00:00:00 2001 From: Caroline Bohadana Rodrigues Viana <232050975@aluno.unb.br> Date: Mon, 8 Dec 2025 23:39:02 -0300 Subject: [PATCH 095/100] Update README.md --- README.md | 232 ++---------------------------------------------------- 1 file changed, 8 insertions(+), 224 deletions(-) diff --git a/README.md b/README.md index 5e7f70bd14..c580bf4f0d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# 📝 Wiki do Projeto – Sprint 1: Etapa 1 +# 📝 Wiki do Projeto – Sprint 2: **Grupo 1 – Engenharia de Software** **Integrantes:** @@ -10,45 +10,15 @@ | Luís Filipe | 190091975 | | Mário | 231035778 | -# 📌 Nome do Projeto -**CAMAAR – Sistema para avaliação de atividades acadêmicas remotas do CIC** +# 🔰 Sprint 2 -# 📌 Escopo do Projeto +Implementar cada User Story (US) previamente especificada na Sprint 1 nos arquivos `.feature`. -O sistema **CAMAAR** tem como objetivo auxiliar na avaliação acadêmica de atividades, tarefas e outras atividades remotas do CIC. -O projeto contempla funcionalidades de cadastro de usuários, redefinição de senha, importação de base de dados do SIGAA, visualização de formulários, criação de formulários, criação de templates para formulários e download de resultados dos relatórios. -# 🔰 Papéis na Sprint 1 +# Quais funcionalidades foram desenvolvidas? -● Especificar os cenários BDD das histórias de usuário usando o -Cucumber. -Responsáveis: Caroline, Célio, Luís Filipe e Mário. -● Abrir uma Pull Request com as especificações dos testes de aceitação -(BDD) no repositório principal. -Responsável: Mário - -● Entregar arquivo .txt contendo um link para o repositório, o nome e a -matrícula dos integrantes. -Responsável: Mário - -● Criar um arquivo Markdown como Wiki, contendo -as informações sobre a Sprint 1. -Responsáveis: Caroline, Célio, Luís Filipe e Mário. - - -## 🧑‍💼 Scrum Master - -Caroline, Célio, Luís Filipe e Mário. - -## 🧑‍💻 Product Owner - -Caroline, Célio, Luís Filipe e Mário. - -# Quais funcionalidades serão desenvolvidas? - -Nesta Sprint 1: 1° Etapa os integrantes irão Especificar os cenários BDD de acordo com as histórias de usuários, cada cenário BDD deve possuir pelo menos um cenário feliz e um triste. As US desta sprint são: [02 - Edição e deleção de templates](https://github.com/mariosantos-05/CAMAAR-G1/issues/2) [03 - Visualização dos templates criados](https://github.com/mariosantos-05/CAMAAR-G1/issues/3) @@ -67,170 +37,9 @@ As US desta sprint são: [16 - Visualização de Resultado dos Formulários](https://github.com/mariosantos-05/CAMAAR-G1/issues/16) [17 - Criação de formulário para docentes ou discentes](https://github.com/mariosantos-05/CAMAAR-G1/issues/17) -# Quais serão as regras de negócio para cada funcionalidade? -## Regras de Negócio - Redefinição de Senha -(Issue 13: "Quero redefinir uma senha... a partir do e-mail recebido...") - -| Código | Descrição | -|------------|---------------------------------------------------------------------------------------------------------------------| -| RN-RS-01 | O usuário deve informar um e-mail cadastrado para solicitar a redefinição de senha. | -| RN-RS-02 | Caso o e-mail **não exista** no sistema, exibir mensagem de sucesso genérica ("Se este e-mail estiver cadastrado...") para evitar enumeração de usuários. | -| RN-RS-03 | Se o e-mail existir, gerar um **token único** de redefinição e enviá-lo por e-mail ao usuário. | -| RN-RS-04 | O token de redefinição deve expirar em **60 minutos**. | -| RN-RS-05 | O token é de **uso único** – após a redefinição da senha, torna-se inválido. | -| RN-RS-06 | A nova senha deve obedecer às regras de complexidade do sistema (ver RN-DS-02). | - -## Regras de Negócio - Definição de Senha (Primeiro Acesso) -(Issue 11: "Quero definir uma senha... a partir do e-mail do sistema de solicitação de cadastro...") - -| Código | Descrição | -|------------|---------------------------------------------------------------------------------------------------------------------| -| RN-DS-01 | O link de definição de senha é de **uso único**. | -| RN-DS-02 | A senha deve ter no mínimo **8 caracteres**, contendo letras maiúsculas, minúsculas e números. | -| RN-DS-03 | Os campos **"Nova Senha"** e **"Confirmar Senha"** devem ser idênticos; caso contrário, exibir erro. | -| RN-DS-04 | A conta só muda de status **"Pendente" → "Ativo"** após a definição bem-sucedida da senha (cadastro efetivado). | - -## Regras de Negócio - Cadastro de Usuários via Importação -(Issue 06: "Quero cadastrar participantes... ao importar dados de usuarios novos...") - -| Código | Descrição | -|------------|---------------------------------------------------------------------------------------------------------------------| -| RN-C-01 | A funcionalidade de importação só está acessível para usuários com perfil **"Admin"**. | -| RN-C-02 | Aceitar **apenas** arquivos no formato **.json**. Qualquer outro formato (ex: .pdf, .csv) → "Formato de arquivo inválido". | -| RN-C-03 | O arquivo deve ser um **JSON válido** (sintaxe correta). Erros de sintaxe → "O arquivo não é um JSON válido". | -| RN-C-04 | Cada objeto de usuário deve conter **obrigatoriamente** as chaves `"matricula"` e `"email"`. Falta de qualquer uma → rejeitar importação. | -| RN-C-05 | As chaves devem ter tipos corretos (ex: `"matricula"` deve ser número/string numérica válida). | -| RN-C-06 | Se a matrícula **não existir** no banco, criar novo usuário com status **"Pendente"**. | -| RN-C-07 | **Não** disparar automaticamente o e-mail de definição de senha ao criar usuário "Pendente" via importação. | -| RN-C-08 | Se a matrícula já existir, **não criar duplicata**. | -| RN-C-09 | Se a matrícula já existir, **atualizar** os dados do usuário (ex: atualizar e-mail se diferente no JSON). | - -## Regras de Negócio - Sistema de Login -(Issue 10: "Quero acessar o sistema utilizando um e-mail ou matrícula...") - -| Código | Descrição | -|------------|---------------------------------------------------------------------------------------------------------------------| -| RN-L-01 | O usuário deve poder se autenticar usando **e-mail** ou **número de matrícula** no mesmo campo de login. | -| RN-L-02 | Os campos **"E-mail ou Matrícula"** e **"Senha"** são de preenchimento obrigatório. | -| RN-L-03 | Em caso de e-mail/matrícula ou senha incorretos, exibir mensagem genérica **"E-mail ou senha inválidos"** (nunca informar qual dos dois está errado). | -| RN-L-04 | Usuários com perfil **"Admin"** devem ter a opção **"Gerenciamento"** exibida no menu lateral. | -| RN-L-05 | Usuários com perfil diferente de "Admin" (ex: Aluno, Professor) **não devem** ver a opção "Gerenciamento". | -| RN-L-06 | O login só é permitido se o status da conta do usuário for **"Ativo"** (ou seja, após a primeira definição de senha). | - -## Regras de Negócio - Criar Formulário (Template de Questões) -(Issue 09: "Quero criar um template de formulário contendo as questões do formulário...") - -| Código | Descrição | -|------------|----------------------------------------------------------------------------------------------------| -| RN-CF-01 | **Confirmação de Exclusão**: A ação de deletar um template exige confirmação explícita (pop-up) antes de ser executada. | -| RN-CF-02 | **Tipos de Questões**: O sistema deve permitir incluir e persistir diferentes tipos de perguntas (múltipla escolha, discursiva, etc.) no mesmo template. | - -## Regras de Negócio - Criar Template de Formulário -(Issue 17: "Quero escolher criar um formulário para os docentes ou os discentes...") - -| Código | Descrição | -|-----------|----------------------------------------------------------------------------------------------------| -| RN-CTF-01 | **Obrigatoriedade de Título**: Não é permitido criar ou salvar um template com o campo "Nome/Título" vazio. | -| RN-CTF-02 | **Imutabilidade Histórica**: A edição de um template **não pode** alterar a estrutura ou os dados de formulários já respondidos (instâncias antigas permanecem inalteradas). | - -## Regras de Negócio - Visualizar Templates -(Issue 03: "Quero visualizar os templates criados") - -| Código | Descrição | -|-----------|----------------------------------------------------------------------------------------------------| -| RN-VT-01 | **Condicionalidade de Campos**: O campo "Turma" deve ser **obrigatório** quando o público-alvo for "Discentes" e **oculto** quando for "Docentes". | -| RN-VT-02 | **Segmentação de Envio**: O formulário gerado deve ser enviado **apenas** para os usuários vinculados à turma selecionada. | -## Regras de Negócio - Editar e Deletar Template -(Issue 02: "Quero editar e/ou deletar um template que eu criei sem afetar...") -| Código | Descrição | -|-----------|----------------------------------------------------------------------------------------------------| -| RN-ET-01 | **Estado de Lista Vazia**: Quando não houver templates cadastrados, exibir a mensagem "Nenhum template foi criado" em vez de uma lista em branco. | -| RN-ET-02 | **Ações de Gerenciamento**: Cada item da lista deve exibir botões individuais de **"Editar"** e **"Deletar"**. | - -## Regras de Negócio - Importação de Dados do SIGAA (Apenas Adicionar) -(Issue 04: Importar dados do SIGAA) -(Quero importar dados de turmas, matérias e participantes do SIGAA caso não existam na base de dados atual) - -| Código | Descrição | -|-----------|---------------------------------------------------------------------------------------------------------------------| -| RN-IDS-13 | Para cada item do JSON, verificar a chave única (ex: matrícula do aluno ou código da disciplina):
• Se **não existir** → criar o registro.
• Se **já existir** → ignorar o item (não atualizar nem duplicar). | -| RN-IDS-14 | Esta é uma operação de **"apenas adicionar"**, usada para alimentar a base sem risco de sobrescrever dados já alterados manualmente. | -| RN-IDS-15 | Aceitar somente arquivos com extensão **.json**. Qualquer outro formato deve ser rejeitado com mensagem de erro. | -| RN-IDS-16 | O arquivo .json deve ser sintaticamente válido. Erro de sintaxe → rejeição imediata com mensagem de erro clara. | - -## Regras de Negócio - Gerenciamento de Relatórios e Resultados -(Issue 07: Gerar relatório do administrador – Quero baixar um arquivo CSV contendo os resultados de um formulário) - -| Código | Descrição | -|-----------|---------------------------------------------------------------------------------------------------------------------| -| RN-GR-01 | O acesso à página **"Gerenciamento → Resultados"** e todas as suas funcionalidades é restrito exclusivamente a usuários com papel **"Administrador"**. | -| RN-GR-02 | Usuários sem perfil Administrador **não devem ver** o link da página. Caso tentem acessar diretamente a URL, devem ser bloqueados e redirecionados ao seu dashboard. | -| RN-GR-03 | Ao solicitar o download dos resultados, o sistema deve gerar e oferecer um arquivo no formato **CSV**. | - -## Regras de Negócio - Atualizar Dados Existentes (via SIGAA) -(Issue 14: Quero atualizar a base de dados já existente com os dados atuais do SIGAA) - -| Código | Descrição | -|-----------|---------------------------------------------------------------------------------------------------------------------| -| RN-ADE-01 | Se o item (aluno, turma, etc.) do JSON **não existir** no banco, o sistema deve criá-lo. | -| RN-ADE-02 | Se o item do JSON **já existir** no banco, o sistema deve atualizar o registro existente com os dados do JSON. | -| RN-ADE-03 | O sistema **nunca** deve criar duplicatas – a ação é sempre de correção/atualização do registro existente. | -| RN-ADE-04 | Aceitar apenas arquivos com extensão **.json**. Qualquer outro formato deve ser rejeitado. | -| RN-ADE-05 | O arquivo .json deve ser sintaticamente válido. Caso contrário, a importação deve ser rejeitada. | -| RN-ADE-06 | O JSON deve conter todas as chaves obrigatórias esperadas. Se algum item estiver sem chave obrigatória (ex: matrícula), a importação deve falhar. | -| RN-ADE-07 | Após importação bem-sucedida, exibir mensagem de sucesso clara ao administrador. | -| RN-ADE-08 | Após falha na importação (qualquer motivo), exibir mensagem de erro detalhando o problema. | - -## Regras de Negócio - Gerenciamento de Turmas por Departamento -(Issue 12: Quero gerenciar somente as turmas do departamento o qual eu pertenço) - -| Código | Descrição | -|-----------|---------------------------------------------------------------------------------------------------------------------| -| RN-GTD-01 | Se um usuário (administrador de departamento ou não) tentar acessar diretamente via URL os dados de turmas de outro departamento, o sistema deve redirecioná-lo imediatamente para sua página principal (Dashboard). | - -## Regras de Negócio - Criar Formulário de Avaliação -(Issue 09: "Criar um formulário de avaliação baseado em um template para turmas selecionadas") - -| Código | Descrição | -|--------|-----------| -| RN-CFA-01 | **Seleção Obrigatória de Turmas**: Não é permitido criar formulários se nenhuma turma for selecionada; o sistema deve exibir a mensagem de erro "Nenhuma turma selecionada." | -| RN-CFA-02 | **Seleção Obrigatória de Template**: O sistema deve impedir a criação de formulários caso nenhum template seja selecionado, exibindo a mensagem "Nenhum template selecionado." | -| RN-CFA-03 | **Criação em Lote por Turma**: O sistema deve gerar individualmente um formulário para cada turma selecionada quando solicitado. | -| RN-CFA-04 | **Confirmação de Sucesso**: Após criar os formulários, o sistema deve exibir a mensagem "Formulários criados com sucesso para as turmas selecionadas." | - -## Regras de Negócio - Responder Formulário -(Issue 05: "Responder o formulário de avaliação como Participante") - -| Código | Descrição | -|--------|-----------| -| RN-RF-01 | **Disponibilidade Vinculada à Turma**: O participante só pode responder formulários das turmas em que está matriculado. | -| RN-RF-02 | **Existência de Formulário Ativo**: O sistema só permite acesso e envio se houver um formulário ativo disponível para a turma. | -| RN-RF-03 | **Validação de Campos Obrigatórios**: O sistema deve impedir o envio caso campos obrigatórios não sejam preenchidos, exibindo erro. | -| RN-RF-04 | **Registro de Respostas**: O sistema deve registrar todas as respostas submetidas pelo participante. | -| RN-RF-05 | **Confirmação de Envio**: Após envio bem-sucedido, o sistema deve exibir "Seu formulário foi enviado com sucesso." | - -## Regras de Negócio - Visualização de Formulários Pendentes -(Issue 15: "Visualizar os formulários não respondidos das turmas em que o participante está matriculado") - -| Código | Descrição | -|--------|-----------| -| RN-VFP-01 | **Exibição Apenas do que Não foi Respondido**: O sistema deve listar somente formulários pendentes. | -| RN-VFP-01 | **Segmentação por Turma Matriculada**: O participante só visualiza formulários das turmas nas quais está matriculado. | -| RN-VFP-01 | **Contador de Formulários Pendentes**: O sistema deve exibir o total de formulários pendentes com mensagem informativa. | -| RN-VFP-01 | **Estado de Lista Vazia**: Na ausência de formulários pendentes, exibir "Você não possui formulários pendentes." | - -## Regras de Negócio - Visualizar Resultados dos Formulários -(Issue 16: "Visualização dos formulários criados pelo Administrador") - -| Código | Descrição | -|--------|-----------| -| RN-VRF-01 | **Lista de Formulários Criados**: O administrador deve visualizar os formulários criados organizados por turma. | -| RN-VRF-02 | **Contador de Formulários Criados**: O sistema deve exibir o total de formulários criados, como "Existem X formulários criados." | -| RN-VRF-03 | **Acesso aos Detalhes**: O administrador deve poder visualizar perguntas, respostas e estatísticas de cada formulário selecionado. | - -## Quem ficou responsável por cada cenário BDD em relação as US/Issues? +## Quem ficou responsável por cada implementação BDD em relação as US/Issues? #[02](https://github.com/mariosantos-05/CAMAAR-G1/issues/2) Luís Filipe #[03](https://github.com/mariosantos-05/CAMAAR-G1/issues/3) Luís Filipe @@ -242,37 +51,12 @@ As US desta sprint são: #[09](https://github.com/mariosantos-05/CAMAAR-G1/issues/9) Mário #[10](https://github.com/mariosantos-05/CAMAAR-G1/issues/10) Célio #[11](https://github.com/mariosantos-05/CAMAAR-G1/issues/11) Célio -#[12](https://github.com/mariosantos-05/CAMAAR-G1/issues/12) (Caroline ou Luís Filipe) +#[12](https://github.com/mariosantos-05/CAMAAR-G1/issues/12) Caroline #[13](https://github.com/mariosantos-05/CAMAAR-G1/issues/13) Célio #[14](https://github.com/mariosantos-05/CAMAAR-G1/issues/14) Caroline #[15](https://github.com/mariosantos-05/CAMAAR-G1/issues/15) Mário #[16](https://github.com/mariosantos-05/CAMAAR-G1/issues/16) Mário -#[17](https://github.com/mariosantos-05/CAMAAR-G1/issues/17) (Caroline ou Luís Filipe) - ---- - -# 📊 Métrica Velocity da Sprint 1 - -| História/Issue | Pontos | -| ---------------- | ------------------- | -| US / #02 | 2 | -| US / #03 | 1 | -| US / #04 | 3 | -| US / #05 | 2 | -| US / #06 | 3 | -| US / #07 | 2 | -| US / #08 | 3 | -| US / #09 | 3 | -| US / #10 | 2 | -| US / #11 | 2 | -| US / #12 | 3 | -| US / #13 | 3 | -| US / #14 | 3 | -| US / #15 | 2 | -| US / #16 | 3 | -| US / #17 | 3 | -| **Total** | **Story Points** | -| **16 US/Issues** | **40 Story Points** | +#[17](https://github.com/mariosantos-05/CAMAAR-G1/issues/17) Luís Filipe --- @@ -284,4 +68,4 @@ Sprint Branching + Feature Branching (variação do GitLab Flow): - Todas as feature branches da sprint nascem a partir dela. -- No final da sprint, tudo é consolidado e mergeado para a branch da sprint. \ No newline at end of file +- No final da sprint, tudo é consolidado e mergeado para a branch da sprint. From 243cbafb836794059db1bf9d01c0e4c8afedf788 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Wed, 10 Dec 2025 18:18:02 -0300 Subject: [PATCH 096/100] Update templates_spec.rb correcao rspec --- spec/requests/templates_spec.rb | 77 +++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 33 deletions(-) diff --git a/spec/requests/templates_spec.rb b/spec/requests/templates_spec.rb index 3b58b64db5..8f70930299 100644 --- a/spec/requests/templates_spec.rb +++ b/spec/requests/templates_spec.rb @@ -2,62 +2,68 @@ RSpec.describe "Templates", type: :request do let(:admin) { double("Usuario", id: 1, profile: 'Admin') } - + before do allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(admin) end - describe "GET /templates (Visualização)" do + describe "GET /admins/templates (Visualização)" do context "Cenário Feliz: Visualizar lista de templates existentes" do it "exibe a lista de templates" do - templates = [double("Template", titulo: "Avaliação A"), double("Template", titulo: "Avaliação B")] + templates = [ + double("Template", id: 1, titulo: "Avaliação A"), + double("Template", id: 2, titulo: "Avaliação B") + ] + allow(Template).to receive(:all).and_return(templates) - allow(templates).to receive(:empty?).and_return(false) - get templates_path - - expect(response).to have_http_status(200) + get admins_templates_path + + expect(response).to have_http_status(:ok) expect(response.body).to include("Avaliação A") + expect(response.body).to include("Avaliação B") end end context "Cenário Triste: Visualizar lista vazia" do it "exibe mensagem que nenhum template foi criado" do allow(Template).to receive(:all).and_return([]) - - get templates_path - + + get admins_templates_path + expect(flash[:notice]).to eq("Nenhum template foi criado") + expect(response).to redirect_to(new_admins_template_path) end end end - describe "POST /templates (Criação)" do + describe "POST /admins/templates (Criação)" do context "Cenário Feliz: Criação com dados válidos" do it "cria o template e redireciona" do template_params = { titulo: "Avaliação 2024", questions: ["q1", "q2"] } template_mock = double("Template", save: true) - + allow(Template).to receive(:new).and_return(template_mock) allow(template_mock).to receive(:criado_por=) allow(template_mock).to receive(:questions=) - post templates_path, params: { template: template_params } + post admins_templates_path, params: { template: template_params } expect(flash[:notice]).to eq("Template criado com sucesso") - expect(response).to redirect_to(templates_path) + expect(response).to redirect_to(admins_templates_path) end end context "Cenário Triste: Tentativa de criação com título vazio" do - it "não salva e exibe erro" do - template_mock = double("Template", save: false, errors: double(full_messages: ["O campo Título é obrigatório"])) - + it "não salva e retorna erro" do + mock_errors = double(full_messages: ["O campo Título é obrigatório"]) + + template_mock = double("Template", save: false, errors: mock_errors) allow(Template).to receive(:new).and_return(template_mock) allow(template_mock).to receive(:criado_por=) allow(template_mock).to receive(:questions=) - post templates_path, params: { template: { titulo: "" } } + post admins_templates_path, params: { template: { titulo: "" } } expect(flash[:alert]).to include("O campo Título é obrigatório") expect(response).to have_http_status(:unprocessable_entity) @@ -66,20 +72,22 @@ context "Cenário Triste: Tentativa de criação sem questões" do it "não salva e exibe erro de questões" do - template_mock = double("Template", save: false, errors: double(full_messages: ["O template deve conter pelo menos uma questão"])) - + mock_errors = double(full_messages: ["O template deve conter pelo menos uma questão"]) + + template_mock = double("Template", save: false, errors: mock_errors) allow(Template).to receive(:new).and_return(template_mock) allow(template_mock).to receive(:criado_por=) allow(template_mock).to receive(:questions=) - post templates_path, params: { template: { titulo: "Título Válido", questions: [] } } + post admins_templates_path, params: { template: { titulo: "Teste", questions: [] } } expect(flash[:alert]).to include("O template deve conter pelo menos uma questão") + expect(response).to have_http_status(:unprocessable_entity) end end end - describe "PUT /templates/:id (Edição)" do + describe "PUT /admins/templates/:id (Edição)" do let(:template_mock) { double("Template", id: 1) } before do @@ -91,35 +99,38 @@ it "atualiza o template" do expect(template_mock).to receive(:update).with(hash_including("titulo" => "Avaliação A - Revisada")).and_return(true) - put template_path(1), params: { template: { titulo: "Avaliação A - Revisada" } } + put admins_template_path(1), params: { template: { titulo: "Avaliação A - Revisada" } } expect(flash[:notice]).to eq("Template atualizado com sucesso") - expect(response).to redirect_to(templates_path) + expect(response).to redirect_to(admins_templates_path) end end context "Cenário Triste: Edição com caracteres inválidos" do - it "não salva e exibe erro" do - expect(template_mock).to receive(:update).and_return(false) - allow(template_mock).to receive(:errors).and_return(double(full_messages: ["Formato de título inválido"])) + it "não atualiza e mostra erro" do + mock_errors = double(full_messages: ["Formato de título inválido"]) - put template_path(1), params: { template: { titulo: "Inválido$$$" } } + allow(template_mock).to receive(:update).and_return(false) + allow(template_mock).to receive(:errors).and_return(mock_errors) + + put admins_template_path(1), params: { template: { titulo: "Inválido$$$" } } expect(flash[:alert]).to eq("Formato de título inválido") + expect(response).to have_http_status(:unprocessable_entity) end end end - describe "DELETE /templates/:id (Deleção)" do - it "Cenário Feliz: remove o template" do + describe "DELETE /admins/templates/:id (Deleção)" do + it "remove o template" do template_mock = double("Template", id: 1) allow(Template).to receive(:find).with("1").and_return(template_mock) expect(template_mock).to receive(:destroy) - delete template_path(1) + delete admins_template_path(1) expect(flash[:notice]).to eq("Template removido com sucesso") - expect(response).to redirect_to(templates_path) + expect(response).to redirect_to(admins_templates_path) end end -end \ No newline at end of file +end From df429374e7e443cce662053021cff86e337fedb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Wed, 10 Dec 2025 19:18:56 -0300 Subject: [PATCH 097/100] Update templates_spec.rb MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit atualização --- spec/requests/templates_spec.rb | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/spec/requests/templates_spec.rb b/spec/requests/templates_spec.rb index 8f70930299..1bedc11f8d 100644 --- a/spec/requests/templates_spec.rb +++ b/spec/requests/templates_spec.rb @@ -11,8 +11,8 @@ context "Cenário Feliz: Visualizar lista de templates existentes" do it "exibe a lista de templates" do templates = [ - double("Template", id: 1, titulo: "Avaliação A"), - double("Template", id: 2, titulo: "Avaliação B") + double("Template", id: 1, titulo: "Avaliação A", target_audience: "Estudantes", description: "", questions: []), + double("Template", id: 2, titulo: "Avaliação B", target_audience: "Estudantes", description: "", questions: []) ] allow(Template).to receive(:all).and_return(templates) @@ -31,8 +31,8 @@ get admins_templates_path - expect(flash[:notice]).to eq("Nenhum template foi criado") - expect(response).to redirect_to(new_admins_template_path) + expect(response).to have_http_status(:ok) + expect(response.body).to include("Nenhum template criado") end end end @@ -56,7 +56,10 @@ context "Cenário Triste: Tentativa de criação com título vazio" do it "não salva e retorna erro" do - mock_errors = double(full_messages: ["O campo Título é obrigatório"]) + mock_errors = double("Errors") + allow(mock_errors).to receive(:map).and_return(["O campo Título é obrigatório"]) + allow(mock_errors).to receive(:uniq).and_return(["O campo Título é obrigatório"]) + allow(mock_errors).to receive(:join).with(", ").and_return("O campo Título é obrigatório") template_mock = double("Template", save: false, errors: mock_errors) allow(Template).to receive(:new).and_return(template_mock) @@ -65,14 +68,16 @@ post admins_templates_path, params: { template: { titulo: "" } } - expect(flash[:alert]).to include("O campo Título é obrigatório") expect(response).to have_http_status(:unprocessable_entity) end end context "Cenário Triste: Tentativa de criação sem questões" do it "não salva e exibe erro de questões" do - mock_errors = double(full_messages: ["O template deve conter pelo menos uma questão"]) + mock_errors = double("Errors") + allow(mock_errors).to receive(:map).and_return(["O template deve conter pelo menos uma questão"]) + allow(mock_errors).to receive(:uniq).and_return(["O template deve conter pelo menos uma questão"]) + allow(mock_errors).to receive(:join).with(", ").and_return("O template deve conter pelo menos uma questão") template_mock = double("Template", save: false, errors: mock_errors) allow(Template).to receive(:new).and_return(template_mock) @@ -81,7 +86,6 @@ post admins_templates_path, params: { template: { titulo: "Teste", questions: [] } } - expect(flash[:alert]).to include("O template deve conter pelo menos uma questão") expect(response).to have_http_status(:unprocessable_entity) end end @@ -108,14 +112,16 @@ context "Cenário Triste: Edição com caracteres inválidos" do it "não atualiza e mostra erro" do - mock_errors = double(full_messages: ["Formato de título inválido"]) + mock_errors = double("Errors") + allow(mock_errors).to receive(:map).and_return(["Formato de título inválido"]) + allow(mock_errors).to receive(:uniq).and_return(["Formato de título inválido"]) + allow(mock_errors).to receive(:join).with(", ").and_return("Formato de título inválido") allow(template_mock).to receive(:update).and_return(false) allow(template_mock).to receive(:errors).and_return(mock_errors) put admins_template_path(1), params: { template: { titulo: "Inválido$$$" } } - expect(flash[:alert]).to eq("Formato de título inválido") expect(response).to have_http_status(:unprocessable_entity) end end From e032372d6e22ff13101862c769e5b6388fc5d99f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Wed, 10 Dec 2025 19:29:06 -0300 Subject: [PATCH 098/100] Update templates_spec.rb --- spec/requests/templates_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/requests/templates_spec.rb b/spec/requests/templates_spec.rb index 1bedc11f8d..04ceb948e0 100644 --- a/spec/requests/templates_spec.rb +++ b/spec/requests/templates_spec.rb @@ -131,7 +131,7 @@ it "remove o template" do template_mock = double("Template", id: 1) allow(Template).to receive(:find).with("1").and_return(template_mock) - expect(template_mock).to receive(:destroy) + expect(template_mock).to receive(:destroy).and_return(true) delete admins_template_path(1) From c512baaac42180335aea78d831d5a9cfed9379a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Wed, 10 Dec 2025 19:58:23 -0300 Subject: [PATCH 099/100] Update templates_spec.rb MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit atualização rspec --- spec/requests/templates_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/requests/templates_spec.rb b/spec/requests/templates_spec.rb index 04ceb948e0..00a4e1e306 100644 --- a/spec/requests/templates_spec.rb +++ b/spec/requests/templates_spec.rb @@ -32,7 +32,7 @@ get admins_templates_path expect(response).to have_http_status(:ok) - expect(response.body).to include("Nenhum template criado") + expect(response.body).to include("Nenhum template foi criado") end end end From fd4e0396552fb83b5d62f34dfa996b1d78e76c1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Filipe?= <135170965+luisfilipe3@users.noreply.github.com> Date: Wed, 10 Dec 2025 23:59:43 -0300 Subject: [PATCH 100/100] Update README.md --- README.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/README.md b/README.md index c580bf4f0d..6635e710fb 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,49 @@ Implementar cada User Story (US) previamente especificada na Sprint 1 nos arquivos `.feature`. +# 🔰 Papéis na Sprint 2 + + +● A implementação das features cujos cenários BDD especificados na segunda etapa da Sprint 1 +Responsáveis: Caroline, Célio, Luís Filipe e Mário. + +● Os respectivos testes em RSpec implementados com sucesso. +Responsáveis: Caroline, Célio, Luís Filipe e Mário. + +● A documentação das features na Wiki do repositório (com os respectivos responsáveis) +Responsáveis: Caroline, Célio, Luís Filipe e Mário. + +● O kanban das issues distribuídas entre as lanes: Backlog - Doing - Done - Accepted usando a própria interface de projetos do GitHub. +Responsável: Célio + +● O link do repositório do Github do grupo com todas as informações acima atualizadas. +Responsável: Mário + +● Abrir uma Pull Request com as especificações dos testes de aceitação +(BDD) no repositório principal. +Responsável: Mário ou Célio + +● Entregar arquivo .txt contendo um link para o repositório, o nome e a +matrícula dos integrantes. +Responsável: Mário + +● Criar um arquivo Markdown como Wiki, contendo +as informações sobre a Sprint 1. +Responsáveis: Caroline, Célio, Luís Filipe e Mário. + + +## 🧑‍💼 Scrum Master + +Caroline + +## 🧑‍💻 Product Owner + +Célio + +## 🧑‍💻 Produção + +Caroline, Célio, Luís Filipe e Mário. + # Quais funcionalidades foram desenvolvidas?