diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..6ad66bb7e2 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +# See https://git-scm.com/docs/gitattributes for more about git attribute files. + +# Mark the database schema as having been generated. +project/db/schema.rb linguist-generated + +# Mark any vendored files as having been vendored. +project/vendor/* linguist-vendored +project/config/credentials/*.yml.enc diff=rails_credentials +project/config/credentials.yml.enc diff=rails_credentials diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..262523cdd0 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: +- package-ecosystem: bundler + directory: "/project" + schedule: + interval: weekly + open-pull-requests-limit: 10 +- package-ecosystem: github-actions + directory: "/project" + schedule: + interval: weekly + open-pull-requests-limit: 10 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000..07761490f5 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,134 @@ +name: CI + +defaults: + run: + working-directory: project + +on: + pull_request: + push: + branches: [ main ] + +jobs: + scan_ruby: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Scan for common Rails security vulnerabilities using static analysis + run: bin/brakeman --no-pager + + - name: Scan for known security vulnerabilities in gems used + run: bin/bundler-audit + + scan_js: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Scan for security vulnerabilities in JavaScript dependencies + run: bin/importmap audit + + lint: + runs-on: ubuntu-latest + env: + RUBOCOP_CACHE_ROOT: tmp/rubocop + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Prepare RuboCop cache + uses: actions/cache@v4 + env: + DEPENDENCIES_HASH: ${{ hashFiles('.ruby-version', '**/.rubocop.yml', '**/.rubocop_todo.yml', 'Gemfile.lock') }} + with: + path: ${{ env.RUBOCOP_CACHE_ROOT }} + key: rubocop-${{ runner.os }}-${{ env.DEPENDENCIES_HASH }}-${{ github.ref_name == github.event.repository.default_branch && github.run_id || 'default' }} + restore-keys: | + rubocop-${{ runner.os }}-${{ env.DEPENDENCIES_HASH }}- + + - name: Lint code for consistent style + run: bin/rubocop -f github + + test: + runs-on: ubuntu-latest + + # services: + # redis: + # image: valkey/valkey:8 + # ports: + # - 6379:6379 + # options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 + steps: + - name: Install packages + run: sudo apt-get update && sudo apt-get install --no-install-recommends -y libvips + + - name: Checkout code + uses: actions/checkout@v6 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Run tests + env: + RAILS_ENV: test + # RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }} + # REDIS_URL: redis://localhost:6379/0 + run: bin/rails db:test:prepare test + + system-test: + runs-on: ubuntu-latest + + # services: + # redis: + # image: valkey/valkey:8 + # ports: + # - 6379:6379 + # options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 + steps: + - name: Install packages + run: sudo apt-get update && sudo apt-get install --no-install-recommends -y libvips + + - name: Checkout code + uses: actions/checkout@v6 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Run System Tests + env: + RAILS_ENV: test + # RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }} + # REDIS_URL: redis://localhost:6379/0 + run: bin/rails db:test:prepare test:system + + - name: Keep screenshots from failed system tests + uses: actions/upload-artifact@v4 + if: failure() + with: + name: screenshots + path: ${{ github.workspace }}/tmp/screenshots + if-no-files-found: ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..d4c518525d --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +# See https://help.github.com/articles/ignoring-files for more about ignoring files. +# +# Temporary files generated by your text editor or operating system +# belong in git's global ignore instead: +# `$XDG_CONFIG_HOME/git/ignore` or `~/.config/git/ignore` + +# Ignore bundler config. +project/.bundle + +# Ignore all environment files. +project/.env* + +# Ignore all logfiles and tempfiles. +project/log/* +project/tmp/* +!project/log/.keep +!project/tmp/.keep + +# Ignore pidfiles, but keep the directory. +project/tmp/pids/* +!project/tmp/pids/ +!project/tmp/pids/.keep + +# Ignore storage (uploaded files in development and any SQLite databases). +project/storage/* +!project/storage/.keep +project/tmp/storage/* +!project/tmp/storage/ +!project/tmp/storage/.keep + +project/public/assets + +# Ignore key files for decrypting credentials and more. +project/config/*.key + +AGENTS.md +PROJECT_CONTEXT.md + +CLAUDE.md +.claude/ \ No newline at end of file diff --git a/project/.dockerignore b/project/.dockerignore new file mode 100644 index 0000000000..325bfc036d --- /dev/null +++ b/project/.dockerignore @@ -0,0 +1,51 @@ +# See https://docs.docker.com/engine/reference/builder/#dockerignore-file for more about ignoring files. + +# Ignore git directory. +/.git/ +/.gitignore + +# Ignore bundler config. +/.bundle + +# Ignore all environment files. +/.env* + +# Ignore all default key files. +/config/master.key +/config/credentials/*.key + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/.keep + +# Ignore storage (uploaded files in development and any SQLite databases). +/storage/* +!/storage/.keep +/tmp/storage/* +!/tmp/storage/.keep + +# Ignore assets. +/node_modules/ +/app/assets/builds/* +!/app/assets/builds/.keep +/public/assets + +# Ignore CI service files. +/.github + +# Ignore Kamal files. +/config/deploy*.yml +/.kamal + +# Ignore development files +/.devcontainer + +# Ignore Docker-related files +/.dockerignore +/Dockerfile* diff --git a/project/.kamal/hooks/docker-setup.sample b/project/.kamal/hooks/docker-setup.sample new file mode 100755 index 0000000000..2fb07d7d7a --- /dev/null +++ b/project/.kamal/hooks/docker-setup.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Docker set up on $KAMAL_HOSTS..." diff --git a/project/.kamal/hooks/post-app-boot.sample b/project/.kamal/hooks/post-app-boot.sample new file mode 100755 index 0000000000..70f9c4bc95 --- /dev/null +++ b/project/.kamal/hooks/post-app-boot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Booted app version $KAMAL_VERSION on $KAMAL_HOSTS..." diff --git a/project/.kamal/hooks/post-deploy.sample b/project/.kamal/hooks/post-deploy.sample new file mode 100755 index 0000000000..fd364c2a77 --- /dev/null +++ b/project/.kamal/hooks/post-deploy.sample @@ -0,0 +1,14 @@ +#!/bin/sh + +# A sample post-deploy hook +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_ROLES (if set) +# KAMAL_DESTINATION (if set) +# KAMAL_RUNTIME + +echo "$KAMAL_PERFORMER deployed $KAMAL_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds" diff --git a/project/.kamal/hooks/post-proxy-reboot.sample b/project/.kamal/hooks/post-proxy-reboot.sample new file mode 100755 index 0000000000..1435a677f2 --- /dev/null +++ b/project/.kamal/hooks/post-proxy-reboot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Rebooted kamal-proxy on $KAMAL_HOSTS" diff --git a/project/.kamal/hooks/pre-app-boot.sample b/project/.kamal/hooks/pre-app-boot.sample new file mode 100755 index 0000000000..45f7355045 --- /dev/null +++ b/project/.kamal/hooks/pre-app-boot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Booting app version $KAMAL_VERSION on $KAMAL_HOSTS..." diff --git a/project/.kamal/hooks/pre-build.sample b/project/.kamal/hooks/pre-build.sample new file mode 100755 index 0000000000..c5a55678b2 --- /dev/null +++ b/project/.kamal/hooks/pre-build.sample @@ -0,0 +1,51 @@ +#!/bin/sh + +# A sample pre-build hook +# +# Checks: +# 1. We have a clean checkout +# 2. A remote is configured +# 3. The branch has been pushed to the remote +# 4. The version we are deploying matches the remote +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_ROLES (if set) +# KAMAL_DESTINATION (if set) + +if [ -n "$(git status --porcelain)" ]; then + echo "Git checkout is not clean, aborting..." >&2 + git status --porcelain >&2 + exit 1 +fi + +first_remote=$(git remote) + +if [ -z "$first_remote" ]; then + echo "No git remote set, aborting..." >&2 + exit 1 +fi + +current_branch=$(git branch --show-current) + +if [ -z "$current_branch" ]; then + echo "Not on a git branch, aborting..." >&2 + exit 1 +fi + +remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1) + +if [ -z "$remote_head" ]; then + echo "Branch not pushed to remote, aborting..." >&2 + exit 1 +fi + +if [ "$KAMAL_VERSION" != "$remote_head" ]; then + echo "Version ($KAMAL_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2 + exit 1 +fi + +exit 0 diff --git a/project/.kamal/hooks/pre-connect.sample b/project/.kamal/hooks/pre-connect.sample new file mode 100755 index 0000000000..77744bdca8 --- /dev/null +++ b/project/.kamal/hooks/pre-connect.sample @@ -0,0 +1,47 @@ +#!/usr/bin/env ruby + +# A sample pre-connect check +# +# Warms DNS before connecting to hosts in parallel +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_ROLES (if set) +# KAMAL_DESTINATION (if set) +# KAMAL_RUNTIME + +hosts = ENV["KAMAL_HOSTS"].split(",") +results = nil +max = 3 + +elapsed = Benchmark.realtime do + results = hosts.map do |host| + Thread.new do + tries = 1 + + begin + Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME) + rescue SocketError + if tries < max + puts "Retrying DNS warmup: #{host}" + tries += 1 + sleep rand + retry + else + puts "DNS warmup failed: #{host}" + host + end + end + + tries + end + end.map(&:value) +end + +retries = results.sum - hosts.size +nopes = results.count { |r| r == max } + +puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ] diff --git a/project/.kamal/hooks/pre-deploy.sample b/project/.kamal/hooks/pre-deploy.sample new file mode 100755 index 0000000000..05b3055b72 --- /dev/null +++ b/project/.kamal/hooks/pre-deploy.sample @@ -0,0 +1,122 @@ +#!/usr/bin/env ruby + +# A sample pre-deploy hook +# +# Checks the Github status of the build, waiting for a pending build to complete for up to 720 seconds. +# +# Fails unless the combined status is "success" +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_COMMAND +# KAMAL_SUBCOMMAND +# KAMAL_ROLES (if set) +# KAMAL_DESTINATION (if set) + +# Only check the build status for production deployments +if ENV["KAMAL_COMMAND"] == "rollback" || ENV["KAMAL_DESTINATION"] != "production" + exit 0 +end + +require "bundler/inline" + +# true = install gems so this is fast on repeat invocations +gemfile(true, quiet: true) do + source "https://rubygems.org" + + gem "octokit" + gem "faraday-retry" +end + +MAX_ATTEMPTS = 72 +ATTEMPTS_GAP = 10 + +def exit_with_error(message) + $stderr.puts message + exit 1 +end + +class GithubStatusChecks + attr_reader :remote_url, :git_sha, :github_client, :combined_status + + def initialize + @remote_url = github_repo_from_remote_url + @git_sha = `git rev-parse HEAD`.strip + @github_client = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"]) + refresh! + end + + def refresh! + @combined_status = github_client.combined_status(remote_url, git_sha) + end + + def state + combined_status[:state] + end + + def first_status_url + first_status = combined_status[:statuses].find { |status| status[:state] == state } + first_status && first_status[:target_url] + end + + def complete_count + combined_status[:statuses].count { |status| status[:state] != "pending"} + end + + def total_count + combined_status[:statuses].count + end + + def current_status + if total_count > 0 + "Completed #{complete_count}/#{total_count} checks, see #{first_status_url} ..." + else + "Build not started..." + end + end + + private + def github_repo_from_remote_url + url = `git config --get remote.origin.url`.strip.delete_suffix(".git") + if url.start_with?("https://github.com/") + url.delete_prefix("https://github.com/") + elsif url.start_with?("git@github.com:") + url.delete_prefix("git@github.com:") + else + url + end + end +end + + +$stdout.sync = true + +begin + puts "Checking build status..." + + attempts = 0 + checks = GithubStatusChecks.new + + loop do + case checks.state + when "success" + puts "Checks passed, see #{checks.first_status_url}" + exit 0 + when "failure" + exit_with_error "Checks failed, see #{checks.first_status_url}" + when "pending" + attempts += 1 + end + + exit_with_error "Checks are still pending, gave up after #{MAX_ATTEMPTS * ATTEMPTS_GAP} seconds" if attempts == MAX_ATTEMPTS + + puts checks.current_status + sleep(ATTEMPTS_GAP) + checks.refresh! + end +rescue Octokit::NotFound + exit_with_error "Build status could not be found" +end diff --git a/project/.kamal/hooks/pre-proxy-reboot.sample b/project/.kamal/hooks/pre-proxy-reboot.sample new file mode 100755 index 0000000000..061f8059e6 --- /dev/null +++ b/project/.kamal/hooks/pre-proxy-reboot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Rebooting kamal-proxy on $KAMAL_HOSTS..." diff --git a/project/.kamal/secrets b/project/.kamal/secrets new file mode 100644 index 0000000000..b3089d6f5a --- /dev/null +++ b/project/.kamal/secrets @@ -0,0 +1,20 @@ +# Secrets defined here are available for reference under registry/password, env/secret, builder/secrets, +# and accessories/*/env/secret in config/deploy.yml. All secrets should be pulled from either +# password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git. + +# Example of extracting secrets from 1password (or another compatible pw manager) +# SECRETS=$(kamal secrets fetch --adapter 1password --account your-account --from Vault/Item KAMAL_REGISTRY_PASSWORD RAILS_MASTER_KEY) +# KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD ${SECRETS}) +# RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY ${SECRETS}) + +# Example of extracting secrets from Rails credentials +# KAMAL_REGISTRY_PASSWORD=$(rails credentials:fetch kamal.registry_password) + +# Use a GITHUB_TOKEN if private repositories are needed for the image +# GITHUB_TOKEN=$(gh config get -h github.com oauth_token) + +# Grab the registry password from ENV +# KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD + +# Improve security by using a password manager. Never check config/master.key into git! +RAILS_MASTER_KEY=$(cat config/master.key) diff --git a/project/.rspec b/project/.rspec new file mode 100644 index 0000000000..5be63fcb08 --- /dev/null +++ b/project/.rspec @@ -0,0 +1,2 @@ +--require spec_helper +--format documentation diff --git a/project/.rubocop.yml b/project/.rubocop.yml new file mode 100644 index 0000000000..1391b9893d --- /dev/null +++ b/project/.rubocop.yml @@ -0,0 +1,12 @@ +# 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 + +AllCops: + Exclude: + - "lib/tasks/cucumber.rake" diff --git a/project/.ruby-version b/project/.ruby-version new file mode 100644 index 0000000000..61a52c9149 --- /dev/null +++ b/project/.ruby-version @@ -0,0 +1 @@ +ruby-3.4.9 diff --git a/project/Dockerfile b/project/Dockerfile new file mode 100644 index 0000000000..633d3305bc --- /dev/null +++ b/project/Dockerfile @@ -0,0 +1,77 @@ +# syntax=docker/dockerfile:1 +# check=error=true + +# This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand: +# docker build -t project . +# docker run -d -p 80:80 -e RAILS_MASTER_KEY= --name project project + +# For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html + +# Make sure RUBY_VERSION matches the Ruby version in .ruby-version +ARG RUBY_VERSION=3.4.9 +FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base + +# Rails app lives here +WORKDIR /rails + +# Install base packages +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 && \ + ln -s /usr/lib/$(uname -m)-linux-gnu/libjemalloc.so.2 /usr/local/lib/libjemalloc.so && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Set production environment variables and enable jemalloc for reduced memory usage and latency. +ENV RAILS_ENV="production" \ + BUNDLE_DEPLOYMENT="1" \ + BUNDLE_PATH="/usr/local/bundle" \ + BUNDLE_WITHOUT="development" \ + LD_PRELOAD="/usr/local/lib/libjemalloc.so" + +# Throw-away build stage to reduce size of final image +FROM base AS build + +# Install packages needed to build gems +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y build-essential git libvips libyaml-dev pkg-config && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Install application gems +COPY vendor/* ./vendor/ +COPY Gemfile Gemfile.lock ./ + +RUN bundle install && \ + rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \ + # -j 1 disable parallel compilation to avoid a QEMU bug: https://github.com/rails/bootsnap/issues/495 + bundle exec bootsnap precompile -j 1 --gemfile + +# Copy application code +COPY . . + +# Precompile bootsnap code for faster boot times. +# -j 1 disable parallel compilation to avoid a QEMU bug: https://github.com/rails/bootsnap/issues/495 +RUN bundle exec bootsnap precompile -j 1 app/ lib/ + +# Precompiling assets for production without requiring secret RAILS_MASTER_KEY +RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile + + + + +# Final stage for app image +FROM base + +# Run and own only the runtime files as a non-root user for security +RUN groupadd --system --gid 1000 rails && \ + useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash +USER 1000:1000 + +# Copy built artifacts: gems, application +COPY --chown=rails:rails --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}" +COPY --chown=rails:rails --from=build /rails /rails + +# Entrypoint prepares the database. +ENTRYPOINT ["/rails/bin/docker-entrypoint"] + +# Start server via Thruster by default, this can be overwritten at runtime +EXPOSE 80 +CMD ["./bin/thrust", "./bin/rails", "server"] diff --git a/project/Gemfile b/project/Gemfile new file mode 100644 index 0000000000..98005f64d2 --- /dev/null +++ b/project/Gemfile @@ -0,0 +1,75 @@ +source "https://rubygems.org" + +# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" +gem "rails", "~> 8.1.3" +# The modern asset pipeline for Rails [https://github.com/rails/propshaft] +gem "propshaft" +# Use sqlite3 as the database for Active Record +gem "sqlite3", ">= 2.1" +# Use the Puma web server [https://github.com/puma/puma] +gem "puma", ">= 5.0" +# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails] +gem "importmap-rails" +# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev] +gem "turbo-rails" +# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev] +gem "stimulus-rails" +# Build JSON APIs with ease [https://github.com/rails/jbuilder] +gem "jbuilder" + +# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] +gem "bcrypt", "~> 3.1.7" + +# Windows does not include zoneinfo files, so bundle the tzinfo-data gem +gem "tzinfo-data", platforms: %i[ windows jruby ] +# Ruby 4 no longer ships fiddle as a default gem; Rails console uses it through reline on Windows. +gem "fiddle" + +# Use the database-backed adapters for Rails.cache, Active Job, and Action Cable +gem "solid_cache" +gem "solid_queue" +gem "solid_cable" + +# Reduces boot times through caching; required in config/boot.rb +gem "bootsnap", require: false + +# Deploy this application anywhere as a Docker container [https://kamal-deploy.org] +gem "kamal", require: false + +# Add HTTP asset caching/compression and X-Sendfile acceleration to Puma [https://github.com/basecamp/thruster/] +gem "thruster", require: false + +# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] +gem "image_processing", "~> 1.2" + +group :development, :test do + # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem + gem "debug", platforms: %i[ mri windows ], require: "debug/prelude" + + # Audits gems for known security defects (use config/bundler-audit.yml to ignore issues) + gem "bundler-audit", require: false + + # Static analysis for security vulnerabilities [https://brakemanscanner.org/] + gem "brakeman", require: false + + # Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/] + gem "rubocop-rails-omakase", require: false + + gem "factory_bot_rails" + gem "rspec-rails", "~> 8.0" +end + +group :development do + # Use console on exceptions pages [https://github.com/rails/web-console] + gem "web-console" +end + +group :test do + # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing] + gem "capybara" + gem "selenium-webdriver" + gem "cucumber-rails", require: false + gem "database_cleaner" +end + +gem "csv", "~> 3.3" diff --git a/project/Gemfile.lock b/project/Gemfile.lock new file mode 100644 index 0000000000..ac72d3b254 --- /dev/null +++ b/project/Gemfile.lock @@ -0,0 +1,671 @@ +GEM + remote: https://rubygems.org/ + specs: + action_text-trix (2.1.19) + railties + actioncable (8.1.3) + actionpack (= 8.1.3) + activesupport (= 8.1.3) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + zeitwerk (~> 2.6) + actionmailbox (8.1.3) + actionpack (= 8.1.3) + activejob (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) + mail (>= 2.8.0) + actionmailer (8.1.3) + actionpack (= 8.1.3) + actionview (= 8.1.3) + activejob (= 8.1.3) + activesupport (= 8.1.3) + mail (>= 2.8.0) + rails-dom-testing (~> 2.2) + actionpack (8.1.3) + actionview (= 8.1.3) + activesupport (= 8.1.3) + nokogiri (>= 1.8.5) + rack (>= 2.2.4) + rack-session (>= 1.0.1) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) + actiontext (8.1.3) + action_text-trix (~> 2.1.15) + actionpack (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (8.1.3) + activesupport (= 8.1.3) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (8.1.3) + activesupport (= 8.1.3) + globalid (>= 0.3.6) + activemodel (8.1.3) + activesupport (= 8.1.3) + activerecord (8.1.3) + activemodel (= 8.1.3) + activesupport (= 8.1.3) + timeout (>= 0.4.0) + activestorage (8.1.3) + actionpack (= 8.1.3) + activejob (= 8.1.3) + activerecord (= 8.1.3) + activesupport (= 8.1.3) + marcel (~> 1.0) + activesupport (8.1.3) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + json + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) + ast (2.4.3) + base64 (0.3.0) + bcrypt (3.1.22) + bcrypt_pbkdf (1.1.2) + bigdecimal (4.1.2) + bindex (0.8.1) + bootsnap (1.24.5) + msgpack (~> 1.2) + brakeman (8.0.4) + racc + builder (3.3.0) + bundler-audit (0.9.3) + bundler (>= 1.2.0) + thor (~> 1.0) + capybara (3.40.0) + addressable + matrix + mini_mime (>= 0.1.3) + nokogiri (~> 1.11) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + regexp_parser (>= 1.5, < 3.0) + xpath (~> 3.2) + concurrent-ruby (1.3.6) + connection_pool (3.0.2) + crass (1.0.6) + csv (3.3.5) + cucumber (10.2.0) + base64 (~> 0.2) + builder (~> 3.2) + cucumber-ci-environment (> 9, < 12) + cucumber-core (> 15, < 17) + cucumber-cucumber-expressions (> 17, < 20) + cucumber-html-formatter (> 21, < 23) + diff-lcs (~> 1.5) + logger (~> 1.6) + mini_mime (~> 1.1) + multi_test (~> 1.1) + sys-uname (~> 1.3) + cucumber-ci-environment (11.0.0) + cucumber-core (16.2.0) + cucumber-gherkin (> 36, < 40) + cucumber-messages (> 31, < 33) + cucumber-tag-expressions (> 6, < 9) + cucumber-cucumber-expressions (19.0.1) + bigdecimal + cucumber-gherkin (39.1.0) + cucumber-messages (>= 31, < 33) + cucumber-html-formatter (22.3.0) + cucumber-messages (> 23, < 33) + cucumber-messages (32.3.1) + cucumber-rails (4.0.1) + capybara (>= 3.25, < 4) + cucumber (>= 7, < 11) + railties (>= 6.1, < 9) + cucumber-tag-expressions (8.1.0) + database_cleaner (2.1.0) + database_cleaner-active_record (>= 2, < 3) + database_cleaner-active_record (2.2.2) + activerecord (>= 5.a) + database_cleaner-core (~> 2.0) + database_cleaner-core (2.0.1) + date (3.5.1) + debug (1.11.1) + irb (~> 1.10) + reline (>= 0.3.8) + diff-lcs (1.6.2) + dotenv (3.2.0) + drb (2.2.3) + ed25519 (1.4.0) + erb (6.0.4) + erubi (1.13.1) + et-orbi (1.4.0) + tzinfo + factory_bot (6.6.0) + activesupport (>= 6.1.0) + factory_bot_rails (6.5.1) + factory_bot (~> 6.5) + railties (>= 6.1.0) + ffi (1.17.4-aarch64-linux-gnu) + ffi (1.17.4-aarch64-linux-musl) + ffi (1.17.4-arm-linux-gnu) + ffi (1.17.4-arm-linux-musl) + ffi (1.17.4-x64-mingw-ucrt) + ffi (1.17.4-x86_64-linux-gnu) + ffi (1.17.4-x86_64-linux-musl) + fiddle (1.1.8) + fugit (1.12.1) + et-orbi (~> 1.4) + raabro (~> 1.4) + globalid (1.3.0) + activesupport (>= 6.1) + i18n (1.14.8) + concurrent-ruby (~> 1.0) + image_processing (1.14.0) + mini_magick (>= 4.9.5, < 6) + ruby-vips (>= 2.0.17, < 3) + importmap-rails (2.2.3) + actionpack (>= 6.0.0) + activesupport (>= 6.0.0) + railties (>= 6.0.0) + io-console (0.8.2) + irb (1.18.0) + pp (>= 0.6.0) + prism (>= 1.3.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + jbuilder (2.15.0) + actionview (>= 7.0.0) + activesupport (>= 7.0.0) + json (2.19.5) + kamal (2.11.0) + activesupport (>= 7.0) + base64 (~> 0.2) + bcrypt_pbkdf (~> 1.0) + concurrent-ruby (~> 1.2) + dotenv (~> 3.1) + ed25519 (~> 1.4) + net-ssh (~> 7.3) + sshkit (>= 1.23.0, < 2.0) + thor (~> 1.3) + zeitwerk (>= 2.6.18, < 3.0) + language_server-protocol (3.17.0.5) + lint_roller (1.1.0) + logger (1.7.0) + loofah (2.25.1) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mail (2.9.0) + logger + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + marcel (1.2.1) + matrix (0.4.3) + memoist3 (1.0.0) + mini_magick (5.3.1) + logger + mini_mime (1.1.5) + minitest (6.0.6) + drb (~> 2.0) + prism (~> 1.5) + msgpack (1.8.0) + multi_test (1.1.0) + net-imap (0.6.4) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-scp (4.1.0) + net-ssh (>= 2.6.5, < 8.0.0) + net-sftp (4.0.0) + net-ssh (>= 5.0.0, < 8.0.0) + net-smtp (0.5.1) + net-protocol + net-ssh (7.3.2) + nio4r (2.7.5) + nokogiri (1.19.3-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.3-aarch64-linux-musl) + racc (~> 1.4) + nokogiri (1.19.3-arm-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.3-arm-linux-musl) + racc (~> 1.4) + nokogiri (1.19.3-x64-mingw-ucrt) + racc (~> 1.4) + nokogiri (1.19.3-x86_64-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.3-x86_64-linux-musl) + racc (~> 1.4) + ostruct (0.6.3) + parallel (2.1.0) + parser (3.3.11.1) + ast (~> 2.4.1) + racc + pp (0.6.3) + prettyprint + prettyprint (0.2.0) + prism (1.9.0) + propshaft (1.3.2) + actionpack (>= 7.0.0) + activesupport (>= 7.0.0) + rack + psych (5.3.1) + date + stringio + public_suffix (7.0.5) + puma (8.0.1) + nio4r (~> 2.0) + raabro (1.4.0) + racc (1.8.1) + rack (3.2.6) + rack-session (2.1.2) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rack-test (2.2.0) + rack (>= 1.3) + rackup (2.3.1) + rack (>= 3) + rails (8.1.3) + actioncable (= 8.1.3) + actionmailbox (= 8.1.3) + actionmailer (= 8.1.3) + actionpack (= 8.1.3) + actiontext (= 8.1.3) + actionview (= 8.1.3) + activejob (= 8.1.3) + activemodel (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) + bundler (>= 1.15.0) + railties (= 8.1.3) + rails-dom-testing (2.3.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.7.0) + loofah (~> 2.25) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + railties (8.1.3) + actionpack (= 8.1.3) + activesupport (= 8.1.3) + irb (~> 1.13) + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + tsort (>= 0.2) + zeitwerk (~> 2.6) + rainbow (3.1.1) + rake (13.4.2) + rdoc (7.2.0) + erb + psych (>= 4.0.0) + tsort + regexp_parser (2.12.0) + reline (0.6.3) + io-console (~> 0.5) + rexml (3.4.4) + rspec-core (3.13.6) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.8) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-rails (8.0.4) + actionpack (>= 7.2) + activesupport (>= 7.2) + railties (>= 7.2) + rspec-core (>= 3.13.0, < 5.0.0) + rspec-expectations (>= 3.13.0, < 5.0.0) + rspec-mocks (>= 3.13.0, < 5.0.0) + rspec-support (>= 3.13.0, < 5.0.0) + rspec-support (3.13.7) + rubocop (1.86.2) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (>= 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.49.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.49.1) + parser (>= 3.3.7.2) + prism (~> 1.7) + rubocop-performance (1.26.1) + lint_roller (~> 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.47.1, < 2.0) + rubocop-rails (2.35.2) + activesupport (>= 4.2.0) + lint_roller (~> 1.1) + rack (>= 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.44.0, < 2.0) + rubocop-rails-omakase (1.1.0) + rubocop (>= 1.72) + rubocop-performance (>= 1.24) + rubocop-rails (>= 2.30) + ruby-progressbar (1.13.0) + ruby-vips (2.3.0) + ffi (~> 1.12) + logger + rubyzip (3.3.0) + securerandom (0.4.1) + selenium-webdriver (4.44.0) + base64 (~> 0.2) + logger (~> 1.4) + rexml (~> 3.2, >= 3.2.5) + rubyzip (>= 1.2.2, < 4.0) + websocket (~> 1.0) + solid_cable (4.0.0) + actioncable (>= 7.2) + activejob (>= 7.2) + activerecord (>= 7.2) + railties (>= 7.2) + solid_cache (1.0.10) + activejob (>= 7.2) + activerecord (>= 7.2) + railties (>= 7.2) + solid_queue (1.4.0) + activejob (>= 7.1) + activerecord (>= 7.1) + concurrent-ruby (>= 1.3.1) + fugit (~> 1.11) + railties (>= 7.1) + thor (>= 1.3.1) + sqlite3 (2.9.4-aarch64-linux-gnu) + sqlite3 (2.9.4-aarch64-linux-musl) + sqlite3 (2.9.4-arm-linux-gnu) + sqlite3 (2.9.4-arm-linux-musl) + sqlite3 (2.9.4-x64-mingw-ucrt) + sqlite3 (2.9.4-x86_64-linux-gnu) + sqlite3 (2.9.4-x86_64-linux-musl) + sshkit (1.25.0) + base64 + logger + net-scp (>= 1.1.2) + net-sftp (>= 2.1.2) + net-ssh (>= 2.8.0) + ostruct + stimulus-rails (1.3.4) + railties (>= 6.0.0) + stringio (3.2.0) + sys-uname (1.5.1) + ffi (~> 1.1) + memoist3 (~> 1.0.0) + sys-uname (1.5.1-universal-mingw32) + ffi (~> 1.1) + memoist3 (~> 1.0.0) + win32ole + thor (1.5.0) + thruster (0.1.21) + thruster (0.1.21-aarch64-linux) + thruster (0.1.21-x86_64-linux) + timeout (0.6.1) + tsort (0.2.0) + turbo-rails (2.0.23) + actionpack (>= 7.1.0) + railties (>= 7.1.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + tzinfo-data (1.2026.2) + tzinfo (>= 1.0.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.2.0) + uri (1.1.1) + useragent (0.16.11) + web-console (4.3.0) + actionview (>= 8.0.0) + bindex (>= 0.4.0) + railties (>= 8.0.0) + websocket (1.2.11) + websocket-driver (0.8.0) + base64 + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + win32ole (1.9.3) + xpath (3.2.0) + nokogiri (~> 1.8) + zeitwerk (2.8.2) + +PLATFORMS + aarch64-linux + aarch64-linux-gnu + aarch64-linux-musl + arm-linux-gnu + arm-linux-musl + x64-mingw-ucrt + x86_64-linux + x86_64-linux-gnu + x86_64-linux-musl + +DEPENDENCIES + bcrypt (~> 3.1.7) + bootsnap + brakeman + bundler-audit + capybara + csv (~> 3.3) + cucumber-rails + database_cleaner + debug + factory_bot_rails + fiddle + image_processing (~> 1.2) + importmap-rails + jbuilder + kamal + propshaft + puma (>= 5.0) + rails (~> 8.1.3) + rspec-rails (~> 8.0) + rubocop-rails-omakase + selenium-webdriver + solid_cable + solid_cache + solid_queue + sqlite3 (>= 2.1) + stimulus-rails + thruster + turbo-rails + tzinfo-data + web-console + +CHECKSUMS + action_text-trix (2.1.19) sha256=7012f59421009cf284aa651294896414d653a61a2417c9b8714c8476d2f74009 + actioncable (8.1.3) sha256=e5bc7f75e44e6a22de29c4f43176927c3a9ce4824464b74ed18d8226e75a80f0 + actionmailbox (8.1.3) sha256=df7da474eaa0e70df4ed5a6fef66eb3b3b0f2dbf7f14518deee8d77f1b4aae59 + actionmailer (8.1.3) sha256=831f724891bb70d0aaa4d76581a6321124b6a752cb655c9346aae5479318448d + actionpack (8.1.3) sha256=af998cae4d47c5d581a2cc363b5c77eb718b7c4b45748d81b1887b25621c29a3 + actiontext (8.1.3) sha256=d291019c00e1ea9e6463011fa214f6081a56d7b9a1d224e7d3f6384c1dafc7d2 + actionview (8.1.3) sha256=1347c88c7f3edb38100c5ce0e9fb5e62d7755f3edc1b61cce2eb0b2c6ea2fd5d + activejob (8.1.3) sha256=a149b1766aa8204c3c3da7309e4becd40fcd5529c348cffbf6c9b16b565fe8d3 + activemodel (8.1.3) sha256=90c05cbe4cef3649b8f79f13016191ea94c4525ce4a5c0fb7ef909c4b91c8219 + activerecord (8.1.3) sha256=8003be7b2466ba0a2a670e603eeb0a61dd66058fccecfc49901e775260ac70ab + activestorage (8.1.3) sha256=0564ce9309143951a67615e1bb4e090ee54b8befed417133cae614479b46384d + activesupport (8.1.3) sha256=21a5e0dfbd4c3ddd9e1317ec6a4d782fa226e7867dc70b0743acda81a1dca20e + addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af + ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383 + base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b + bcrypt (3.1.22) sha256=1f0072e88c2d705d94aff7f2c5cb02eb3f1ec4b8368671e19112527489f29032 + bcrypt_pbkdf (1.1.2) sha256=c2414c23ce66869b3eb9f643d6a3374d8322dfb5078125c82792304c10b94cf6 + bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd + bindex (0.8.1) sha256=7b1ecc9dc539ed8bccfc8cb4d2732046227b09d6f37582ff12e50a5047ceb17e + bootsnap (1.24.5) sha256=36b677448524d279b470469aabd5dff4a980e3fa4931a0df68da4a500eb1b6c4 + brakeman (8.0.4) sha256=7bf921fa9638544835df9aa7b3e720a9a72c0267f34f92135955edd80d4dcf6f + builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f + bundler-audit (0.9.3) sha256=81c8766c71e47d0d28a0f98c7eed028539f21a6ea3cd8f685eb6f42333c9b4e9 + capybara (3.40.0) sha256=42dba720578ea1ca65fd7a41d163dd368502c191804558f6e0f71b391054aeef + concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab + connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a + crass (1.0.6) sha256=dc516022a56e7b3b156099abc81b6d2b08ea1ed12676ac7a5657617f012bd45d + csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f + cucumber (10.2.0) sha256=fdedbd31ecf40858b60f04853f2aa15c44f5c30bbac29c6a227fa1e7005a8158 + cucumber-ci-environment (11.0.0) sha256=0df79a9e1d0b015b3d9def680f989200d96fef206f4d19ccf86a338c4f71d1e2 + cucumber-core (16.2.0) sha256=592b58a95cf42feef8e5a349f68e363784ba3b6568ffbcf6776e38e136cf970b + cucumber-cucumber-expressions (19.0.1) sha256=648ec09045190d818fb797af46e1648148599fd67a086a34a7f0e647d9e36c8c + cucumber-gherkin (39.1.0) sha256=aed12a0c955d8563d80a012633c1a72075525f4d64d4cc983001df2181b379ed + cucumber-html-formatter (22.3.0) sha256=f9768ed05588dbd73a5f3824c2cc648bd86b00206e6972d743af8051281d0729 + cucumber-messages (32.3.1) sha256=ddc88e4c1cf7afb96c06005b92a4a6f221a2fa435a8b4ca04677d215fd82771c + cucumber-rails (4.0.1) sha256=bd3513ec47dc06188cc05703648cbc3560fb115f3f5cfb8b616065b4d6e8024d + cucumber-tag-expressions (8.1.0) sha256=9bd8c4b6654f8e5bf2a9c99329b6f32136a75e50cd39d4cfb3927d0fa9f52e21 + database_cleaner (2.1.0) sha256=1dcba26e3b1576da692fc6bac10136a4744da5bcc293d248aae19640c65d89cd + database_cleaner-active_record (2.2.2) sha256=88296b9f3088c31f7c0d4fcec10f68e4b71c96698043916de59b04debec10388 + database_cleaner-core (2.0.1) sha256=8646574c32162e59ed7b5258a97a208d3c44551b854e510994f24683865d846c + date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0 + debug (1.11.1) sha256=2e0b0ac6119f2207a6f8ac7d4a73ca8eb4e440f64da0a3136c30343146e952b6 + diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962 + dotenv (3.2.0) sha256=e375b83121ea7ca4ce20f214740076129ab8514cd81378161f11c03853fe619d + drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373 + ed25519 (1.4.0) sha256=16e97f5198689a154247169f3453ef4cfd3f7a47481fde0ae33206cdfdcac506 + erb (6.0.4) sha256=38e3803694be357fe2bfe312487c74beaf9fb4e5beb3e22498952fe1645b95d9 + erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9 + et-orbi (1.4.0) sha256=6c7e3c90779821f9e3b324c5e96fda9767f72995d6ae435b96678a4f3e2de8bc + factory_bot (6.6.0) sha256=1fc1b3b5620ec980a6a27aec1b6ec8c250ca82962e970e8a40f93e8d388d4b89 + factory_bot_rails (6.5.1) sha256=d3cc4851eae4dea8a665ec4a4516895045e710554d2b5ac9e68b94d351bc6d68 + ffi (1.17.4-aarch64-linux-gnu) sha256=b208f06f91ffd8f5e1193da3cae3d2ccfc27fc36fba577baf698d26d91c080df + ffi (1.17.4-aarch64-linux-musl) sha256=9286b7a615f2676245283aef0a0a3b475ae3aae2bb5448baace630bb77b91f39 + ffi (1.17.4-arm-linux-gnu) sha256=d6dbddf7cb77bf955411af5f187a65b8cd378cb003c15c05697f5feee1cb1564 + ffi (1.17.4-arm-linux-musl) sha256=9d4838ded0465bef6e2426935f6bcc93134b6616785a84ffd2a3d82bc3cf6f95 + ffi (1.17.4-x64-mingw-ucrt) sha256=f6ff9618cfccc494138bddade27aa06c74c6c7bc367a1ea1103d80c2fcb9ed35 + ffi (1.17.4-x86_64-linux-gnu) sha256=9d3db14c2eae074b382fa9c083fe95aec6e0a1451da249eab096c34002bc752d + ffi (1.17.4-x86_64-linux-musl) sha256=3fdf9888483de005f8ef8d1cf2d3b20d86626af206cbf780f6a6a12439a9c49e + fiddle (1.1.8) sha256=7fa8ee3627271497f3add5503acdbc3f40b32f610fc1cf49634f083ef3f32eee + fugit (1.12.1) sha256=5898f478ede9b415f0804e42b8f3fd53f814bd85eebffceebdbc34e1107aaf68 + globalid (1.3.0) sha256=05c639ad6eb4594522a0b07983022f04aa7254626ab69445a0e493aa3786ff11 + i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5 + image_processing (1.14.0) sha256=754cc169c9c262980889bec6bfd325ed1dafad34f85242b5a07b60af004742fb + importmap-rails (2.2.3) sha256=7101be2a4dc97cf1558fb8f573a718404c5f6bcfe94f304bf1f39e444feeb16a + io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc + irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3 + jbuilder (2.15.0) sha256=fe36cd45b47dd88cb2cbebc3adc4348f041825f580d503578594e8255817f889 + json (2.19.5) sha256=218a18553e4801d579ca7e0f5bc72bafd776d7397238a1fb4e74db5b0a812c59 + kamal (2.11.0) sha256=1408864425e0dec7e0a14d712a3b13f614e9f3a425b7661d3f9d287a51d7dd75 + language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc + lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87 + logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 + loofah (2.25.1) sha256=d436c73dbd0c1147b16c4a41db097942d217303e1f7728704b37e4df9f6d2e04 + mail (2.9.0) sha256=6fa6673ecd71c60c2d996260f9ee3dd387d4673b8169b502134659ece6d34941 + marcel (1.2.1) sha256=1678e9360e32f9eafa917c80029e2f6d10b2715c66a4b87b6d0da9b9cd1f859f + matrix (0.4.3) sha256=a0d5ab7ddcc1973ff690ab361b67f359acbb16958d1dc072b8b956a286564c5b + memoist3 (1.0.0) sha256=686e42402cf150a362050c23143dc57b0ef88f8c344943ff8b7845792b50d56f + mini_magick (5.3.1) sha256=29395dfd76badcabb6403ee5aff6f681e867074f8f28ce08d78661e9e4a351c4 + mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef + minitest (6.0.6) sha256=153ea36d1d987a62942382b61075745042a2b3123b1cd48f4c3675af9cc7d6f1 + msgpack (1.8.0) sha256=e64ce0212000d016809f5048b48eb3a65ffb169db22238fb4b72472fecb2d732 + multi_test (1.1.0) sha256=e9e550cdd863fb72becfe344aefdcd4cbd26ebf307847f4a6c039a4082324d10 + net-imap (0.6.4) sha256=9a5598c67a3022c284d98430ef1d4948e7dbdb62596f61081ea8ca933270a02b + net-pop (0.1.2) sha256=848b4e982013c15b2f0382792268763b748cce91c9e91e36b0f27ed26420dff3 + net-protocol (0.2.2) sha256=aa73e0cba6a125369de9837b8d8ef82a61849360eba0521900e2c3713aa162a8 + net-scp (4.1.0) sha256=a99b0b92a1e5d360b0de4ffbf2dc0c91531502d3d4f56c28b0139a7c093d1a5d + net-sftp (4.0.0) sha256=65bb91c859c2f93b09826757af11b69af931a3a9155050f50d1b06d384526364 + net-smtp (0.5.1) sha256=ed96a0af63c524fceb4b29b0d352195c30d82dd916a42f03c62a3a70e5b70736 + net-ssh (7.3.2) sha256=65029e213c380e20e5fd92ece663934ab0a0fe888e0cd7cc6a5b664074362dd4 + nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1 + nokogiri (1.19.3-aarch64-linux-gnu) sha256=46b89e5d7b9e844c2ee360794240c6ea2a4e6fa0c5892a4ed487db621224b639 + nokogiri (1.19.3-aarch64-linux-musl) sha256=8392dfdcd21be7a94dbbe9ccc138dea01b97b24cb2dc02a114ca98bfb1d9a0b7 + nokogiri (1.19.3-arm-linux-gnu) sha256=3919d5ffc334ad778a4a9eb88fda7dcb8b1fb58c8a52ac640c6dcd2f038e774f + nokogiri (1.19.3-arm-linux-musl) sha256=9ce1cb6346bb9c67b1550eb537aa183ead91e4b6eadb2f36ade02d8dd2a79fb6 + nokogiri (1.19.3-x64-mingw-ucrt) sha256=8bb7132cad356c879a1286eaabcb5e68326cb2490317984280fbc62f456d506a + nokogiri (1.19.3-x86_64-linux-gnu) sha256=2f5078620fe12e83669b5b17311b32532a8153d02eee7ad06948b926d6080976 + nokogiri (1.19.3-x86_64-linux-musl) sha256=248c906d2166eca5efb56d52fdee5f9a1f51d69a72e2b64fdac647b4ce39ea3f + ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912 + parallel (2.1.0) sha256=b35258865c2e31134c5ecb708beaaf6772adf9d5efae28e93e99260877b09356 + parser (3.3.11.1) sha256=d17ace7aabe3e72c3cc94043714be27cc6f852f104d81aa284c2281aecc65d54 + pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6 + prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193 + prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85 + propshaft (1.3.2) sha256=1d56a3e56a92c21bfc29caf07406b5386b00d4c47ddf357cf989a5a234b1389e + psych (5.3.1) sha256=eb7a57cef10c9d70173ff74e739d843ac3b2c019a003de48447b2963d81b1974 + public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623 + puma (8.0.1) sha256=7b94e50c07655718c1fb8ae41a11fc06c7d61293208b3aa608ff71a46d3ad37c + raabro (1.4.0) sha256=d4fa9ff5172391edb92b242eed8be802d1934b1464061ae5e70d80962c5da882 + racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f + rack (3.2.6) sha256=5ed78e1f73b2e25679bec7d45ee2d4483cc4146eb1be0264fc4d94cb5ef212c2 + rack-session (2.1.2) sha256=595434f8c0c3473ae7d7ac56ecda6cc6dfd9d37c0b2b5255330aa1576967ffe8 + rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463 + rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868 + rails (8.1.3) sha256=6d017ba5348c98fc909753a8169b21d44de14d2a0b92d140d1a966834c3c9cd3 + rails-dom-testing (2.3.0) sha256=8acc7953a7b911ca44588bf08737bc16719f431a1cc3091a292bca7317925c1d + rails-html-sanitizer (1.7.0) sha256=28b145cceaf9cc214a9874feaa183c3acba036c9592b19886e0e45efc62b1e89 + railties (8.1.3) sha256=913eb0e0cb520aac687ffd74916bd726d48fa21f47833c6292576ef6a286de22 + rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a + rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701 + rdoc (7.2.0) sha256=8650f76cd4009c3b54955eb5d7e3a075c60a57276766ebf36f9085e8c9f23192 + regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb + reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835 + rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142 + rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d + rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836 + rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47 + rspec-rails (8.0.4) sha256=06235692fc0892683d3d34977e081db867434b3a24ae0dd0c6f3516bad4e22df + rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c + rubocop (1.86.2) sha256=bb2e97f635eda42c448f2588f4a6ff78f221b8bdfdf65b1e9b07fbd57521b45d + rubocop-ast (1.49.1) sha256=4412f3ee70f6fe4546cc489548e0f6fcf76cafcfa80fa03af67098ffed755035 + rubocop-performance (1.26.1) sha256=cd19b936ff196df85829d264b522fd4f98b6c89ad271fa52744a8c11b8f71834 + rubocop-rails (2.35.2) sha256=088865be9675922a5c8f13c00055a71ab768ea5eed211437cffd2a8b46b64ac2 + rubocop-rails-omakase (1.1.0) sha256=2af73ac8ee5852de2919abbd2618af9c15c19b512c4cfc1f9a5d3b6ef009109d + ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33 + ruby-vips (2.3.0) sha256=e685ec02c13969912debbd98019e50492e12989282da5f37d05f5471442f5374 + rubyzip (3.3.0) sha256=a372fc67892a4f8c0bc8ec906b720353d8e48807a64b2e63adf99b1e3583a034 + securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1 + selenium-webdriver (4.44.0) sha256=6f1df072529af369589c46f0e01132952aabb250cfd683c274d74dc1eb5d8477 + solid_cable (4.0.0) sha256=8379680ef6bf36e195eb876a6306ea290f87d5fa10bc4a757bc2a918f83229b5 + solid_cache (1.0.10) sha256=bc05a2fb3ac78a6f43cbb5946679cf9db67dd30d22939ededc385cb93e120d41 + solid_queue (1.4.0) sha256=e6a18d196f0b27cb6e3c77c5b31258b05fb634f8ed64fb1866ed164047216c2a + sqlite3 (2.9.4-aarch64-linux-gnu) sha256=ecabed721e6eaad54601d2685f09029d90025efc8d931040dc89cb3f8a2080ec + sqlite3 (2.9.4-aarch64-linux-musl) sha256=ffb4255947fb54c8c3eeca97460c9702b40de91ce390455ef7367ca6a3929a31 + sqlite3 (2.9.4-arm-linux-gnu) sha256=9ee2008b9fbec984c3c165b0d7eedd2bd2a415100b761bfa3a4c6fbec9208bf6 + sqlite3 (2.9.4-arm-linux-musl) sha256=8dc1fe4da6977992cd62decf4a93ccf6cc2e124a5e6a340160d52092f70e837a + sqlite3 (2.9.4-x64-mingw-ucrt) sha256=40997c549b19e2fdfcc5e271f6bdd4d502179742c0bfd678da23d0d09b929848 + sqlite3 (2.9.4-x86_64-linux-gnu) sha256=537a3eda71b1df1336d0055cbebe55a7317c34870c192c7b6b9d8d0be6871847 + sqlite3 (2.9.4-x86_64-linux-musl) sha256=3fc5e865b4be9a85d998203ef8d0c0fdcb92f20acf34a254346ff8a19088efec + sshkit (1.25.0) sha256=c8c6543cdb60f91f1d277306d585dd11b6a064cb44eab0972827e4311ff96744 + stimulus-rails (1.3.4) sha256=765676ffa1f33af64ce026d26b48e8ffb2e0b94e0f50e9119e11d6107d67cb06 + stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1 + sys-uname (1.5.1) sha256=784d7e6491b0393c25cbbe5ac38324ac7be9fda083a6094832648af669386d7b + sys-uname (1.5.1-universal-mingw32) sha256=aceb618e3276da5eae0ce368e9f6fae8c1f3e9ef23a0595cb88db7b6ecd45f62 + thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73 + thruster (0.1.21) sha256=dc67928f36e5894844579a95e45637a5091db7a7ea05468ee8c2c6eb0a3f77cf + thruster (0.1.21-aarch64-linux) sha256=f5aff78fb7a6431ed3d6ab4bde03a89c461e9a73981dbc97d6990d85c3db235c + thruster (0.1.21-x86_64-linux) sha256=6e2fbcf826540a72d3710ae4db072c2333287ac2ee57e7e52f35bc10900d74a7 + timeout (0.6.1) sha256=78f57368a7e7bbadec56971f78a3f5ecbcfb59b7fcbb0a3ed6ddc08a5094accb + tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f + turbo-rails (2.0.23) sha256=ee0d90733aafff056cf51ff11e803d65e43cae258cc55f6492020ec1f9f9315f + tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b + tzinfo-data (1.2026.2) sha256=7db0d3d3d53b8d7601fc183fccc8c6d056a3004e14eb59ea995bf6aec4ae10bc + unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42 + unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f + uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6 + useragent (0.16.11) sha256=700e6413ad4bb954bb63547fa098dddf7b0ebe75b40cc6f93b8d54255b173844 + web-console (4.3.0) sha256=e13b71301cdfc2093f155b5aa3a622db80b4672d1f2f713119cc7ec7ac6a6da4 + websocket (1.2.11) sha256=b7e7a74e2410b5e85c25858b26b3322f29161e300935f70a0e0d3c35e0462737 + websocket-driver (0.8.0) sha256=ed0dba4b943c22f17f9a734817e808bc84cdce6a7e22045f5315aa57676d4962 + websocket-extensions (0.1.5) sha256=1c6ba63092cda343eb53fc657110c71c754c56484aad42578495227d717a8241 + win32ole (1.9.3) sha256=01f43dc5dc13806e6e58204f538b4a28f3d85968ea89074abc9a3cd118e94d96 + xpath (3.2.0) sha256=6dfda79d91bb3b949b947ecc5919f042ef2f399b904013eb3ef6d20dd3a4082e + zeitwerk (2.8.2) sha256=7212a61311083c604184b1ea2574b9aa05cd14f855a0841c06985cabe9181d12 + +BUNDLED WITH + 4.0.9 diff --git a/project/README.md b/project/README.md new file mode 100644 index 0000000000..7db80e4ca1 --- /dev/null +++ b/project/README.md @@ -0,0 +1,24 @@ +# README + +This README would normally document whatever steps are necessary to get the +application up and running. + +Things you may want to cover: + +* Ruby version + +* System dependencies + +* Configuration + +* Database creation + +* Database initialization + +* How to run the test suite + +* Services (job queues, cache servers, search engines, etc.) + +* Deployment instructions + +* ... diff --git a/project/Rakefile b/project/Rakefile new file mode 100644 index 0000000000..9a5ea7383a --- /dev/null +++ b/project/Rakefile @@ -0,0 +1,6 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require_relative "config/application" + +Rails.application.load_tasks diff --git a/project/app/assets/images/.keep b/project/app/assets/images/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/project/app/assets/stylesheets/application.css b/project/app/assets/stylesheets/application.css new file mode 100644 index 0000000000..fe93333c0f --- /dev/null +++ b/project/app/assets/stylesheets/application.css @@ -0,0 +1,10 @@ +/* + * This is a manifest file that'll be compiled into application.css. + * + * With Propshaft, assets are served efficiently without preprocessing steps. You can still include + * application-wide styles in this file, but keep in mind that CSS precedence will follow the standard + * cascading order, meaning styles declared later in the document or manifest will override earlier ones, + * depending on specificity. + * + * Consider organizing styles into separate files for maintainability. + */ diff --git a/project/app/controllers/application_controller.rb b/project/app/controllers/application_controller.rb new file mode 100644 index 0000000000..4c87d12aef --- /dev/null +++ b/project/app/controllers/application_controller.rb @@ -0,0 +1,37 @@ +class ApplicationController < ActionController::Base + # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has. + allow_browser versions: :modern + + # Changes to the importmap will invalidate the etag for HTML responses + stale_when_importmap_changes + + helper_method :current_admin, :current_user, :user_signed_in? + + private + + def current_user + @current_user ||= User.find_by(id: session[:user_id]) if session[:user_id].present? + end + + def current_admin + current_user&.admin? ? current_user : nil + end + + def user_signed_in? + current_user.present? + end + + def require_login + redirect_to login_path, alert: "Faca login para continuar" unless user_signed_in? + end + + def require_admin! + redirect_to login_path, alert: "Acesso restrito a administradores" unless current_admin + end + + def require_admin + return if user_signed_in? && current_user.admin? + + redirect_to root_path, alert: "Acesso restrito a administradores" + end +end diff --git a/project/app/controllers/concerns/.keep b/project/app/controllers/concerns/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/project/app/controllers/dashboard_controller.rb b/project/app/controllers/dashboard_controller.rb new file mode 100644 index 0000000000..2000f0424c --- /dev/null +++ b/project/app/controllers/dashboard_controller.rb @@ -0,0 +1,6 @@ +class DashboardController < ApplicationController + before_action :require_login + + def show + end +end diff --git a/project/app/controllers/formularios_controller.rb b/project/app/controllers/formularios_controller.rb new file mode 100644 index 0000000000..5010e2ff8c --- /dev/null +++ b/project/app/controllers/formularios_controller.rb @@ -0,0 +1,95 @@ +class FormulariosController < ApplicationController + before_action :set_formulario, only: %i[ show edit update destroy exportar_csv ] + + # GET /formularios or /formularios.json + def index + # Issue 110: Visualizar os formulários criados (Listagem geral) + @formularios = Formulario.all + + # Issue 109: Visualizar os formulários não respondidos das turmas + if current_user + # 1. Mapeia quais formulários o usuário atual já respondeu + formularios_respondidos_ids = Submissao.where(user_id: current_user.id).select(:formulario_id) + + # 2. Busca os formulários que pertencem às turmas do usuário E que não estão na lista de respondidos + @formularios_pendentes = Formulario + .where(course_class_id: current_user.course_classes.select(:id)) + .where.not(id: formularios_respondidos_ids) + else + @formularios_pendentes = [] + end + end + + # GET /formularios/1 or /formularios/1.json + def show + end + + # GET /formularios/new + def new + @formulario = Formulario.new + end + + # GET /formularios/1/edit + def edit + end + + # POST /formularios or /formularios.json + def create + @formulario = Formulario.new(formulario_params) + + respond_to do |format| + if @formulario.save + format.html { redirect_to @formulario, notice: "Formulario was successfully created." } + format.json { render :show, status: :created, location: @formulario } + else + format.html { render :new, status: :unprocessable_content } + format.json { render json: @formulario.errors, status: :unprocessable_content } + end + end + end + + # PATCH/PUT /formularios/1 or /formularios/1.json + def update + respond_to do |format| + if @formulario.update(formulario_params) + format.html { redirect_to @formulario, notice: "Formulario was successfully updated.", status: :see_other } + format.json { render :show, status: :ok, location: @formulario } + else + format.html { render :edit, status: :unprocessable_content } + format.json { render json: @formulario.errors, status: :unprocessable_content } + end + end + end + + # DELETE /formularios/1 or /formularios/1.json + def destroy + @formulario.destroy! + + respond_to do |format| + format.html { redirect_to formularios_path, notice: "Formulario was successfully destroyed.", status: :see_other } + format.json { head :no_content } + end + end + + # GET /formularios/1/exportar_csv + def exportar_csv + require 'csv' + + csv_data = CSV.generate(headers: true) do |csv| + csv << ["ID da Pergunta", "Enunciado", "Scores Atribuídos"] + end + + send_data csv_data, + filename: "respostas_formulario_#{@formulario.id}.csv", + type: "text/csv" + end + + private + def set_formulario + @formulario = Formulario.find(params.expect(:id)) + end + + def formulario_params + params.expect(formulario: [ :title, :target_role, :status, :template_id, :course_class_id, :admin_id ]) + end +end diff --git a/project/app/controllers/imports/class_members_controller.rb b/project/app/controllers/imports/class_members_controller.rb new file mode 100644 index 0000000000..290edab245 --- /dev/null +++ b/project/app/controllers/imports/class_members_controller.rb @@ -0,0 +1,10 @@ +module Imports + class ClassMembersController < ApplicationController + before_action :require_admin + + def create + result = Sigaa::ClassMembersImporter.new(imported_by: current_user).call + redirect_to root_path, notice: "Importacao concluida: #{result.created_users} usuarios criados" + end + end +end diff --git a/project/app/controllers/imports/sigaa_controller.rb b/project/app/controllers/imports/sigaa_controller.rb new file mode 100644 index 0000000000..2ddbe240be --- /dev/null +++ b/project/app/controllers/imports/sigaa_controller.rb @@ -0,0 +1,27 @@ +module Imports + class SigaaController < ApplicationController + before_action :require_admin + + def new + @semesters = Sigaa::DataSynchronizer.available_semesters + @semester = params[:semester].presence || @semesters.first + end + + def create + result = Sigaa::DataSynchronizer.new(semester: sync_params[:semester], imported_by: current_user).call + + if result.already_up_to_date? + redirect_to imports_sigaa_path, notice: "A base de dados ja esta atualizada com o SIGAA para este periodo." + else + redirect_to imports_sigaa_path, + notice: "Sincronizacao concluida. #{result.created_count} novos registros adicionados e #{result.updated_count} registros atualizados." + end + end + + private + + def sync_params + params.require(:sigaa_sync).permit(:semester) + end + end +end diff --git a/project/app/controllers/password_resets_controller.rb b/project/app/controllers/password_resets_controller.rb new file mode 100644 index 0000000000..cf76ccda2f --- /dev/null +++ b/project/app/controllers/password_resets_controller.rb @@ -0,0 +1,51 @@ +class PasswordResetsController < ApplicationController + GENERIC_MESSAGE = "Se o email estiver cadastrado, voce recebera instrucoes para redefinir sua senha" + + def new + end + + def create + user = User.find_by(email: reset_request_params[:email].to_s.strip.downcase) + UserMailer.password_reset(user, user.generate_password_reset_token!).deliver_now if user + + redirect_to new_password_reset_path, notice: GENERIC_MESSAGE + end + + def edit + load_user + render_invalid_token unless @user + end + + def update + load_user + return render_invalid_token unless @user + + if password_params[:password] != password_params[:password_confirmation] + flash.now[:alert] = "Confirmacao de senha nao confere" + render :edit, status: :unprocessable_entity + else + @user.apply_new_password!(password_params[:password]) + redirect_to login_path, notice: "Senha redefinida com sucesso" + end + end + + private + + def load_user + @token = params[:token] + @user = User.find_by_reset_token(@token) + end + + def reset_request_params + params.require(:password_reset).permit(:email) + end + + def password_params + params.require(:user).permit(:password, :password_confirmation) + end + + def render_invalid_token + flash.now[:alert] = "Link de redefinicao de senha invalido ou expirado" + render :invalid, status: :not_found + end +end diff --git a/project/app/controllers/password_setups_controller.rb b/project/app/controllers/password_setups_controller.rb new file mode 100644 index 0000000000..10916d7281 --- /dev/null +++ b/project/app/controllers/password_setups_controller.rb @@ -0,0 +1,35 @@ +class PasswordSetupsController < ApplicationController + before_action :load_user + + def edit + render_invalid_token unless @user + end + + def update + return render_invalid_token unless @user + + if password_params[:password] != password_params[:password_confirmation] + flash.now[:alert] = "Confirmacao de senha nao confere" + render :edit, status: :unprocessable_entity + else + @user.apply_new_password!(password_params[:password]) + redirect_to login_path, notice: "Senha definida com sucesso" + end + end + + private + + def load_user + @token = params[:token] + @user = User.find_by_setup_token(@token) + end + + def password_params + params.require(:user).permit(:password, :password_confirmation) + end + + def render_invalid_token + flash.now[:alert] = "Link de definicao de senha invalido ou expirado" + render :invalid, status: :not_found + end +end diff --git a/project/app/controllers/questaos_controller.rb b/project/app/controllers/questaos_controller.rb new file mode 100644 index 0000000000..9cfd2366f0 --- /dev/null +++ b/project/app/controllers/questaos_controller.rb @@ -0,0 +1,70 @@ +class QuestaosController < ApplicationController + before_action :set_questao, only: %i[ show edit update destroy ] + + # GET /questaos or /questaos.json + def index + @questaos = Questao.all + end + + # GET /questaos/1 or /questaos/1.json + def show + end + + # GET /questaos/new + def new + @questao = Questao.new + end + + # GET /questaos/1/edit + def edit + end + + # POST /questaos or /questaos.json + def create + @questao = Questao.new(questao_params) + + respond_to do |format| + if @questao.save + format.html { redirect_to @questao, notice: "Questao was successfully created." } + format.json { render :show, status: :created, location: @questao } + else + format.html { render :new, status: :unprocessable_content } + format.json { render json: @questao.errors, status: :unprocessable_content } + end + end + end + + # PATCH/PUT /questaos/1 or /questaos/1.json + def update + respond_to do |format| + if @questao.update(questao_params) + format.html { redirect_to @questao, notice: "Questao was successfully updated.", status: :see_other } + format.json { render :show, status: :ok, location: @questao } + else + format.html { render :edit, status: :unprocessable_content } + format.json { render json: @questao.errors, status: :unprocessable_content } + end + end + end + + # DELETE /questaos/1 or /questaos/1.json + def destroy + @questao.destroy! + + respond_to do |format| + format.html { redirect_to questaos_path, notice: "Questao was successfully destroyed.", status: :see_other } + format.json { head :no_content } + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_questao + @questao = Questao.find(params.expect(:id)) + end + + # Only allow a list of trusted parameters through. + def questao_params + params.expect(questao: [ :enunciado, :tipo, :template_id, :formulario_id ]) + end +end diff --git a/project/app/controllers/resposta_controller.rb b/project/app/controllers/resposta_controller.rb new file mode 100644 index 0000000000..f7be379ec1 --- /dev/null +++ b/project/app/controllers/resposta_controller.rb @@ -0,0 +1,83 @@ +class RespostaController < ApplicationController + before_action :set_respostum, only: %i[ show edit update destroy ] + + # GET /resposta or /resposta.json + def index + @resposta = Respostum.all + end + + # GET /resposta/1 or /resposta/1.json + def show + end + + # GET /resposta/new + def new + @respostum = Respostum.new + end + + # GET /resposta/1/edit + def edit + end + + # POST /resposta or /resposta.json + def create + # Caminho Triste: Valida se o campo de nota foi deixado em branco pelo robô/usuário + if params.dig(:respostum, :valor_numerico).blank? + redirect_to formulario_path(params[:respostum][:formulario_id]), alert: "Por favor, preencha todas as questões obrigatórias" + return + end + + # Caminho Feliz: Cria a submissão vinculando o usuário e o formulário (idêntico ao banco) + @submissao = Submissao.find_or_create_by!( + user_id: current_user.id, + formulario_id: params[:respostum][:formulario_id] + ) + + # Cria a resposta vinculando à questão e à submissão recém-criada + @respostum = Respostum.new( + valor_numerico: params[:respostum][:valor_numerico], + questao_id: params[:respostum][:questao_id], + submissao_id: @submissao.id + ) + + if @respostum.save + redirect_to formularios_path, notice: "Avaliação submetida com sucesso" + else + redirect_to formulario_path(params[:respostum][:formulario_id]), alert: "Por favor, preencha todas as questões obrigatórias" + end + end + + # PATCH/PUT /resposta/1 or /resposta/1.json + def update + respond_to do |format| + if @respostum.update(respostum_params) + format.html { redirect_to @respostum, notice: "Respostum was successfully updated.", status: :see_other } + format.json { render :show, status: :ok, location: @respostum } + else + format.html { render :edit, status: :unprocessable_content } + format.json { render json: @respostum.errors, status: :unprocessable_content } + end + end + end + + # DELETE /resposta/1 or /resposta/1.json + def destroy + @respostum.destroy! + + respond_to do |format| + format.html { redirect_to resposta_path, notice: "Respostum was successfully destroyed.", status: :see_other } + format.json { head :no_content } + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_respostum + @respostum = Respostum.find(params.expect(:id)) + end + + # Only allow a list of trusted parameters through. + def respostum_params + params.expect(respostum: [ :valor_texto, :valor_numerico, :submissao_id, :questao_id ]) + end +end diff --git a/project/app/controllers/sessions_controller.rb b/project/app/controllers/sessions_controller.rb new file mode 100644 index 0000000000..f83f8ed224 --- /dev/null +++ b/project/app/controllers/sessions_controller.rb @@ -0,0 +1,30 @@ +class SessionsController < ApplicationController + def new + end + + def create + user = User.find_for_login(session_params[:identifier]) + + if user&.pending_password_setup? + flash.now[:alert] = "Senha inicial precisa ser definida antes do acesso" + render :new, status: :unprocessable_entity + elsif user&.authenticate(session_params[:password]) + session[:user_id] = user.id + redirect_to root_path, notice: "Login realizado com sucesso" + else + flash.now[:alert] = "Email, matricula ou senha invalidos" + render :new, status: :unprocessable_entity + end + end + + def destroy + reset_session + redirect_to login_path, notice: "Logout realizado com sucesso" + end + + private + + def session_params + params.require(:session).permit(:identifier, :password) + end +end diff --git a/project/app/controllers/submissaos_controller.rb b/project/app/controllers/submissaos_controller.rb new file mode 100644 index 0000000000..f8b8e050b5 --- /dev/null +++ b/project/app/controllers/submissaos_controller.rb @@ -0,0 +1,60 @@ +class SubmissaosController < ApplicationController + before_action :set_submissao, only: %i[ show edit update destroy ] + + def index + @submissaos = Submissao.all + end + + def show + end + + def new + @submissao = Submissao.new + end + + def edit + end + + def create + @submissao = Submissao.new(submissao_params) + respond_to do |format| + if @submissao.save + format.html { redirect_to @submissao, notice: "Submissao was successfully created." } + format.json { render :show, status: :created, location: @submissao } + else + format.html { render :new, status: :unprocessable_content } + format.json { render json: @submissao.errors, status: :unprocessable_content } + end + end + end + + def update + respond_to do |format| + if @submissao.update(submissao_params) + format.html { redirect_to @submissao, notice: "Submissao was successfully updated.", status: :see_other } + format.json { render :show, status: :ok, location: @submissao } + else + format.html { render :edit, status: :unprocessable_content } + format.json { render json: @submissao.errors, status: :unprocessable_content } + end + end + end + + def destroy + @submissao.destroy! + respond_to do |format| + format.html { redirect_to submissaos_path, notice: "Submissao was successfully destroyed.", status: :see_other } + format.json { head :no_content } + end + end + + private + + def set_submissao + @submissao = Submissao.find(params.expect(:id)) + end + + def submissao_params + params.expect(submissao: [ :formulario_id, :user_id ]) + end +end diff --git a/project/app/controllers/templates_controller.rb b/project/app/controllers/templates_controller.rb new file mode 100644 index 0000000000..a61bd94ed0 --- /dev/null +++ b/project/app/controllers/templates_controller.rb @@ -0,0 +1,133 @@ +class TemplatesController < ApplicationController + before_action :require_admin! + before_action :set_template, only: %i[ show edit update destroy ] + before_action :authorize_template_access!, only: %i[ show edit update destroy ] + + # GET /templates or /templates.json + def index + @templates = current_admin.templates + end + + # GET /templates/1 or /templates/1.json + def show + end + + # GET /templates/new + def new + @template = Template.new + @questoes_attrs = [] + end + + # GET /templates/1/edit + def edit + @questoes_attrs = @template.questaos.map do |questao| + { id: questao.id, enunciado: questao.enunciado, tipo: questao.tipo } + end + end + + # POST /templates or /templates.json + def create + @template = Template.new(template_params) + @questoes_attrs = extract_questoes_attrs + + if params[:add_questao] + @questoes_attrs << blank_questao_attrs + return render :new, status: :ok + end + + if @template.valid? && questoes_attrs_valid? + Template.transaction do + @template.save! + @questoes_attrs.each do |questao| + @template.questaos.create!(enunciado: questao[:enunciado], tipo: questao[:tipo]) + end + end + redirect_to templates_path, notice: "Template criado com sucesso." + else + render :new, status: :unprocessable_content + end + end + + # PATCH/PUT /templates/1 or /templates/1.json + def update + @template.assign_attributes(template_params) + @questoes_attrs = extract_questoes_attrs + + if params[:add_questao] + @questoes_attrs << blank_questao_attrs + return render :edit, status: :ok + end + + novas_questoes = @questoes_attrs.reject { |questao| questao[:id].present? } + + if @template.valid? && novas_questoes_validas?(novas_questoes) + Template.transaction do + @template.save! + @questoes_attrs.each do |questao| + if questao[:id].present? + @template.questaos.find(questao[:id]).update!(enunciado: questao[:enunciado], tipo: questao[:tipo]) + else + @template.questaos.create!(enunciado: questao[:enunciado], tipo: questao[:tipo]) + end + end + end + redirect_to templates_path, notice: "Template atualizado com sucesso." + else + render :edit, status: :unprocessable_content + end + end + + # DELETE /templates/1 or /templates/1.json + def destroy + @template.destroy! + redirect_to templates_path, notice: "Template deletado com sucesso.", status: :see_other + end + + private + def set_template + @template = Template.find(params.expect(:id)) + end + + def authorize_template_access! + redirect_to templates_path, alert: "Você não tem acesso a esse template" unless @template.admin_id == current_admin.id + end + + def template_params + attrs = params.expect(template: [ :title, :target_role ]) + attrs[:admin_id] = current_admin.id + attrs + end + + def extract_questoes_attrs + raw = params.dig(:template, :questaos) + return [] unless raw + + raw.values.map do |questao| + attrs = { enunciado: questao[:enunciado].to_s, tipo: questao[:tipo].presence || "rating" } + attrs[:id] = questao[:id] if questao[:id].present? + attrs + end + end + + def blank_questao_attrs + { enunciado: "", tipo: "rating" } + end + + def questoes_attrs_valid? + if @questoes_attrs.any? { |questao| questao[:enunciado].blank? } + @template.errors.add(:base, "Questão não possui enunciado") + false + else + true + end + end + + def novas_questoes_validas?(novas_questoes) + if novas_questoes.any? { |questao| questao[:enunciado].blank? } + @template.errors.add(:base, "A nova questão não possui enunciado") + false + else + true + end + end +end diff --git a/project/app/helpers/admins_helper.rb b/project/app/helpers/admins_helper.rb new file mode 100644 index 0000000000..d4f7b3486b --- /dev/null +++ b/project/app/helpers/admins_helper.rb @@ -0,0 +1,2 @@ +module AdminsHelper +end diff --git a/project/app/helpers/alunos_helper.rb b/project/app/helpers/alunos_helper.rb new file mode 100644 index 0000000000..cdbe19f2dd --- /dev/null +++ b/project/app/helpers/alunos_helper.rb @@ -0,0 +1,2 @@ +module AlunosHelper +end diff --git a/project/app/helpers/application_helper.rb b/project/app/helpers/application_helper.rb new file mode 100644 index 0000000000..de6be7945c --- /dev/null +++ b/project/app/helpers/application_helper.rb @@ -0,0 +1,2 @@ +module ApplicationHelper +end diff --git a/project/app/helpers/departamentos_helper.rb b/project/app/helpers/departamentos_helper.rb new file mode 100644 index 0000000000..df3b94bfda --- /dev/null +++ b/project/app/helpers/departamentos_helper.rb @@ -0,0 +1,2 @@ +module DepartamentosHelper +end diff --git a/project/app/helpers/formularios_helper.rb b/project/app/helpers/formularios_helper.rb new file mode 100644 index 0000000000..3b96bb2209 --- /dev/null +++ b/project/app/helpers/formularios_helper.rb @@ -0,0 +1,2 @@ +module FormulariosHelper +end diff --git a/project/app/helpers/professors_helper.rb b/project/app/helpers/professors_helper.rb new file mode 100644 index 0000000000..15367d63c7 --- /dev/null +++ b/project/app/helpers/professors_helper.rb @@ -0,0 +1,2 @@ +module ProfessorsHelper +end diff --git a/project/app/helpers/questaos_helper.rb b/project/app/helpers/questaos_helper.rb new file mode 100644 index 0000000000..c313f9d4b0 --- /dev/null +++ b/project/app/helpers/questaos_helper.rb @@ -0,0 +1,2 @@ +module QuestaosHelper +end diff --git a/project/app/helpers/resposta_helper.rb b/project/app/helpers/resposta_helper.rb new file mode 100644 index 0000000000..6cc35b5534 --- /dev/null +++ b/project/app/helpers/resposta_helper.rb @@ -0,0 +1,2 @@ +module RespostaHelper +end diff --git a/project/app/helpers/submissaos_helper.rb b/project/app/helpers/submissaos_helper.rb new file mode 100644 index 0000000000..645fdbb6b5 --- /dev/null +++ b/project/app/helpers/submissaos_helper.rb @@ -0,0 +1,2 @@ +module SubmissaosHelper +end diff --git a/project/app/helpers/templates_helper.rb b/project/app/helpers/templates_helper.rb new file mode 100644 index 0000000000..888093b832 --- /dev/null +++ b/project/app/helpers/templates_helper.rb @@ -0,0 +1,2 @@ +module TemplatesHelper +end diff --git a/project/app/helpers/turmas_helper.rb b/project/app/helpers/turmas_helper.rb new file mode 100644 index 0000000000..617e1373e9 --- /dev/null +++ b/project/app/helpers/turmas_helper.rb @@ -0,0 +1,2 @@ +module TurmasHelper +end diff --git a/project/app/javascript/application.js b/project/app/javascript/application.js new file mode 100644 index 0000000000..0d7b49404c --- /dev/null +++ b/project/app/javascript/application.js @@ -0,0 +1,3 @@ +// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails +import "@hotwired/turbo-rails" +import "controllers" diff --git a/project/app/javascript/controllers/application.js b/project/app/javascript/controllers/application.js new file mode 100644 index 0000000000..1213e85c7a --- /dev/null +++ b/project/app/javascript/controllers/application.js @@ -0,0 +1,9 @@ +import { Application } from "@hotwired/stimulus" + +const application = Application.start() + +// Configure Stimulus development experience +application.debug = false +window.Stimulus = application + +export { application } diff --git a/project/app/javascript/controllers/hello_controller.js b/project/app/javascript/controllers/hello_controller.js new file mode 100644 index 0000000000..5975c0789d --- /dev/null +++ b/project/app/javascript/controllers/hello_controller.js @@ -0,0 +1,7 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + connect() { + this.element.textContent = "Hello World!" + } +} diff --git a/project/app/javascript/controllers/index.js b/project/app/javascript/controllers/index.js new file mode 100644 index 0000000000..1156bf8362 --- /dev/null +++ b/project/app/javascript/controllers/index.js @@ -0,0 +1,4 @@ +// Import and register all your controllers from the importmap via controllers/**/*_controller +import { application } from "controllers/application" +import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading" +eagerLoadControllersFrom("controllers", application) diff --git a/project/app/jobs/application_job.rb b/project/app/jobs/application_job.rb new file mode 100644 index 0000000000..d394c3d106 --- /dev/null +++ b/project/app/jobs/application_job.rb @@ -0,0 +1,7 @@ +class ApplicationJob < ActiveJob::Base + # Automatically retry jobs that encountered a deadlock + # retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + # discard_on ActiveJob::DeserializationError +end diff --git a/project/app/mailers/application_mailer.rb b/project/app/mailers/application_mailer.rb new file mode 100644 index 0000000000..3c34c8148f --- /dev/null +++ b/project/app/mailers/application_mailer.rb @@ -0,0 +1,4 @@ +class ApplicationMailer < ActionMailer::Base + default from: "from@example.com" + layout "mailer" +end diff --git a/project/app/mailers/user_mailer.rb b/project/app/mailers/user_mailer.rb new file mode 100644 index 0000000000..8ce249be84 --- /dev/null +++ b/project/app/mailers/user_mailer.rb @@ -0,0 +1,15 @@ +class UserMailer < ApplicationMailer + default from: "noreply@camaar.local" + + def password_setup(user, token) + @user = user + @url = edit_password_setup_url(token) + mail(to: user.email, subject: "Defina sua senha no CAMAAR") + end + + def password_reset(user, token) + @user = user + @url = edit_password_reset_url(token) + mail(to: user.email, subject: "Redefinicao de senha do CAMAAR") + end +end diff --git a/project/app/models/application_record.rb b/project/app/models/application_record.rb new file mode 100644 index 0000000000..b63caeb8a5 --- /dev/null +++ b/project/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + primary_abstract_class +end diff --git a/project/app/models/class_membership.rb b/project/app/models/class_membership.rb new file mode 100644 index 0000000000..0b94fdd2a0 --- /dev/null +++ b/project/app/models/class_membership.rb @@ -0,0 +1,9 @@ +class ClassMembership < ApplicationRecord + belongs_to :user + belongs_to :course_class + + enum :role, { discente: 0, docente: 1 } + + validates :role, presence: true + validates :user_id, uniqueness: { scope: [ :course_class_id, :role ] } +end diff --git a/project/app/models/concerns/.keep b/project/app/models/concerns/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/project/app/models/course_class.rb b/project/app/models/course_class.rb new file mode 100644 index 0000000000..b88367db50 --- /dev/null +++ b/project/app/models/course_class.rb @@ -0,0 +1,9 @@ +class CourseClass < ApplicationRecord + belongs_to :department + has_many :class_memberships, dependent: :destroy + has_many :users, through: :class_memberships + has_many :formularios, foreign_key: :course_class_id, dependent: :destroy + + validates :code, :class_code, :semester, presence: true + validates :code, uniqueness: { scope: [ :class_code, :semester ] } +end diff --git a/project/app/models/department.rb b/project/app/models/department.rb new file mode 100644 index 0000000000..a1025cc54e --- /dev/null +++ b/project/app/models/department.rb @@ -0,0 +1,7 @@ +class Department < ApplicationRecord + has_many :course_classes, dependent: :restrict_with_exception + has_many :users, dependent: :nullify + + validates :name, :code, presence: true + validates :code, uniqueness: true +end diff --git a/project/app/models/formulario.rb b/project/app/models/formulario.rb new file mode 100644 index 0000000000..b280b18956 --- /dev/null +++ b/project/app/models/formulario.rb @@ -0,0 +1,12 @@ +class Formulario < ApplicationRecord + belongs_to :template, optional: true + belongs_to :course_class, optional: true + belongs_to :admin, class_name: "User" + has_many :questaos, dependent: :destroy + + validates :title, presence: { message: "O nome do formulário é obrigatório" } + validates :template_id, presence: { message: "É necessário escolher um template base" }, on: :create + validates :course_class_id, presence: { message: "É necessário escolher ao menos uma turma" }, on: :create + + after_initialize { self.status ||= "draft" } +end diff --git a/project/app/models/import_inconsistency.rb b/project/app/models/import_inconsistency.rb new file mode 100644 index 0000000000..5da38ddd65 --- /dev/null +++ b/project/app/models/import_inconsistency.rb @@ -0,0 +1,3 @@ +class ImportInconsistency < ApplicationRecord + validates :source, :message, presence: true +end diff --git a/project/app/models/questao.rb b/project/app/models/questao.rb new file mode 100644 index 0000000000..2622bb55f4 --- /dev/null +++ b/project/app/models/questao.rb @@ -0,0 +1,8 @@ +class Questao < ApplicationRecord + # A chave estrangeira é nula para template se pertencer a formulário, e vice-versa + belongs_to :template, optional: true + belongs_to :formulario, optional: true + has_many :respostas + + validates :enunciado, presence: true +end \ No newline at end of file diff --git a/project/app/models/respostum.rb b/project/app/models/respostum.rb new file mode 100644 index 0000000000..8f4da0fd44 --- /dev/null +++ b/project/app/models/respostum.rb @@ -0,0 +1,4 @@ +class Respostum < ApplicationRecord + belongs_to :submissao + belongs_to :questao +end diff --git a/project/app/models/submissao.rb b/project/app/models/submissao.rb new file mode 100644 index 0000000000..c532cf6595 --- /dev/null +++ b/project/app/models/submissao.rb @@ -0,0 +1,4 @@ +class Submissao < ApplicationRecord + belongs_to :formulario + belongs_to :user +end diff --git a/project/app/models/template.rb b/project/app/models/template.rb new file mode 100644 index 0000000000..1a0a6c2a8e --- /dev/null +++ b/project/app/models/template.rb @@ -0,0 +1,7 @@ +class Template < ApplicationRecord + belongs_to :admin, class_name: "User" + has_many :questaos, dependent: :destroy + has_many :formularios, dependent: :nullify + + validates :title, presence: { message: "O nome do template é obrigatório" } +end diff --git a/project/app/models/user.rb b/project/app/models/user.rb new file mode 100644 index 0000000000..5f1c6d0e7e --- /dev/null +++ b/project/app/models/user.rb @@ -0,0 +1,78 @@ +class User < ApplicationRecord + PASSWORD_SETUP_TTL = 7.days + PASSWORD_RESET_TTL = 2.hours + + belongs_to :department, optional: true + has_many :class_memberships, dependent: :destroy + has_many :course_classes, through: :class_memberships + has_many :templates, foreign_key: :admin_id, dependent: :destroy + has_many :formularios, foreign_key: :admin_id, dependent: :destroy + + has_secure_password validations: false + + enum :role, { admin: 0, participant: 1 } + + normalizes :email, with: ->(email) { email.strip.downcase } + normalizes :registration, with: ->(registration) { registration.strip } + + validates :name, :email, :registration, :role, presence: true + validates :email, uniqueness: true + validates :registration, uniqueness: true + + def self.find_for_login(identifier) + normalized = identifier.to_s.strip + find_by(email: normalized.downcase) || find_by(registration: normalized) + end + + def self.find_by_setup_token(token) + find_valid_token(:password_setup_token_digest, :password_setup_sent_at, token, PASSWORD_SETUP_TTL) + end + + def self.find_by_reset_token(token) + find_valid_token(:password_reset_token_digest, :password_reset_sent_at, token, PASSWORD_RESET_TTL) + end + + def password_defined? + password_digest.present? + end + + def pending_password_setup? + !password_defined? + end + + def generate_password_setup_token! + token = SecureRandom.urlsafe_base64(32) + update!(password_setup_token_digest: self.class.token_digest(token), password_setup_sent_at: Time.current) + token + end + + def generate_password_reset_token! + token = SecureRandom.urlsafe_base64(32) + update!(password_reset_token_digest: self.class.token_digest(token), password_reset_sent_at: Time.current) + token + end + + def apply_new_password!(new_password) + update!( + password: new_password, + password_setup_token_digest: nil, + password_setup_sent_at: nil, + password_reset_token_digest: nil, + password_reset_sent_at: nil + ) + end + + def self.token_digest(token) + Digest::SHA256.hexdigest(token.to_s) + end + + def self.find_valid_token(digest_column, sent_at_column, token, ttl) + user = find_by(digest_column => token_digest(token)) + return unless user + return if user.public_send(sent_at_column).blank? + return if user.public_send(sent_at_column) < ttl.ago + + user + end + private_class_method :find_valid_token +end diff --git a/project/app/services/sigaa/class_members_importer.rb b/project/app/services/sigaa/class_members_importer.rb new file mode 100644 index 0000000000..23420eabb9 --- /dev/null +++ b/project/app/services/sigaa/class_members_importer.rb @@ -0,0 +1,143 @@ +module Sigaa + class ClassMembersImporter + Result = Data.define(:created_count, :updated_count, :inconsistencies) + + DEFAULT_PATH = Rails.root.join("..", "class_members.json") + + def initialize(path: DEFAULT_PATH, semester: nil, imported_by: nil) + @path = path + @semester = semester + @imported_by = imported_by + end + + def call + created_count = 0 + updated_count = 0 + inconsistencies = 0 + + parsed_data.each do |class_payload| + course_class = find_or_create_course_class(class_payload) + + members_for(class_payload).each do |member_payload, membership_role| + if invalid_member?(member_payload) + register_inconsistency(member_payload, "Participante sem email ou matricula") + inconsistencies += 1 + next + end + + user_status = sync_user(member_payload, course_class.department) + created_count += 1 if user_status == :created + updated_count += 1 if user_status == :updated + + membership_status = ensure_membership(user_for(member_payload), course_class, membership_role) + created_count += 1 if membership_status == :created + end + end + + Result.new(created_count:, updated_count:, inconsistencies:) + end + + private + + attr_reader :path, :semester, :imported_by + + def parsed_data + data = JSON.parse(File.read(path)) + return data if semester.blank? + + data.select { |entry| entry["semester"] == semester } + end + + def find_or_create_course_class(class_payload) + code = class_payload.fetch("code") + department = department_for(code) + + CourseClass.find_or_create_by!( + code:, + class_code: class_payload.fetch("classCode"), + semester: class_payload.fetch("semester") + ) do |course_class| + course_class.department = department + course_class.name = class_payload["name"] + end + end + + def department_for(code) + department_code = imported_by&.department&.code || code.to_s[/\A[A-Za-z]+/]&.upcase || "GERAL" + Department.find_or_create_by!(code: department_code) { |department| department.name = department_code } + end + + def members_for(class_payload) + class_payload.flat_map do |key, value| + next [] unless %w[dicente docente].include?(key) + + role = key == "docente" ? :docente : :discente + members = value.is_a?(Array) ? value : (value.is_a?(Hash) ? [ value ] : []) + members.map { |member_payload| [ member_payload, role ] } + end + end + + def invalid_member?(member_payload) + member_payload["email"].blank? || member_registration(member_payload).blank? + end + + def member_registration(member_payload) + member_payload["matricula"].presence || member_payload["usuario"].presence + end + + def register_inconsistency(member_payload, message) + ImportInconsistency.create!(source: "class_members.json", message:, payload: member_payload) + end + + def sync_user(member_payload, department) + registration = member_registration(member_payload).to_s.strip + email = normalized_email(member_payload) + name = member_payload["nome"] + + user = User.find_by(email:) || User.find_by(registration:) + + if user.nil? + user = User.create!( + name:, + email:, + registration:, + role: :participant, + department: + ) + send_password_setup(user) if user.pending_password_setup? + return :created + end + + updates = {} + updates[:name] = name if user.name != name + updates[:email] = email if user.email != email + updates[:registration] = registration if user.registration != registration + updates[:department] = department if user.department_id != department.id + + if updates.any? + user.update!(updates) + :updated + else + :unchanged + end + end + + def user_for(member_payload) + registration = member_registration(member_payload).to_s.strip + User.find_by(email: normalized_email(member_payload)) || User.find_by!(registration:) + end + + def normalized_email(member_payload) + member_payload["email"].to_s.strip.downcase + end + + def send_password_setup(user) + UserMailer.password_setup(user, user.generate_password_setup_token!).deliver_now + end + + def ensure_membership(user, course_class, role) + membership = ClassMembership.find_or_create_by!(user:, course_class:, role:) + membership.previously_new_record? ? :created : :unchanged + end + end +end diff --git a/project/app/services/sigaa/classes_importer.rb b/project/app/services/sigaa/classes_importer.rb new file mode 100644 index 0000000000..c0706c9511 --- /dev/null +++ b/project/app/services/sigaa/classes_importer.rb @@ -0,0 +1,67 @@ +module Sigaa + class ClassesImporter + Result = Data.define(:created_count, :updated_count) + + DEFAULT_PATH = Rails.root.join("..", "classes.json") + + def initialize(path: DEFAULT_PATH, semester:, imported_by: nil) + @path = path + @semester = semester + @imported_by = imported_by + end + + def call + created_count = 0 + updated_count = 0 + + entries_for_semester.each do |entry| + attrs = extract_class_attrs(entry) + course_class = CourseClass.find_by( + code: attrs[:code], + class_code: attrs[:class_code], + semester: attrs[:semester] + ) + + if course_class.nil? + CourseClass.create!(attrs.merge(department: department_for(attrs[:code]))) + created_count += 1 + elsif course_class_attributes_changed?(course_class, attrs) + course_class.update!(attrs.slice(:name, :time)) + updated_count += 1 + end + end + + Result.new(created_count:, updated_count:) + end + + private + + attr_reader :path, :semester, :imported_by + + def entries_for_semester + JSON.parse(File.read(path)).select do |entry| + entry.dig("class", "semester") == semester + end + end + + def extract_class_attrs(entry) + class_info = entry.fetch("class") + { + code: entry.fetch("code"), + name: entry.fetch("name"), + class_code: class_info.fetch("classCode"), + semester: class_info.fetch("semester"), + time: class_info["time"] + } + end + + def department_for(code) + department_code = imported_by&.department&.code || code.to_s[/\A[A-Za-z]+/]&.upcase || "GERAL" + Department.find_or_create_by!(code: department_code) { |department| department.name = department_code } + end + + def course_class_attributes_changed?(course_class, attrs) + course_class.name != attrs[:name] || course_class.time != attrs[:time] + end + end +end diff --git a/project/app/services/sigaa/data_synchronizer.rb b/project/app/services/sigaa/data_synchronizer.rb new file mode 100644 index 0000000000..1644dc2aa8 --- /dev/null +++ b/project/app/services/sigaa/data_synchronizer.rb @@ -0,0 +1,38 @@ +module Sigaa + class DataSynchronizer + Result = Data.define(:created_count, :updated_count, :inconsistencies, :already_up_to_date?) + + def initialize(semester:, imported_by:, classes_path: ClassesImporter::DEFAULT_PATH, members_path: ClassMembersImporter::DEFAULT_PATH) + @semester = semester + @imported_by = imported_by + @classes_path = classes_path + @members_path = members_path + end + + def call + classes_result = ClassesImporter.new(path: classes_path, semester:, imported_by:).call + members_result = ClassMembersImporter.new(path: members_path, semester:, imported_by:).call + + created_count = classes_result.created_count + members_result.created_count + updated_count = classes_result.updated_count + members_result.updated_count + + Result.new( + created_count:, + updated_count:, + inconsistencies: members_result.inconsistencies, + already_up_to_date?: created_count.zero? && updated_count.zero? + ) + end + + def self.available_semesters(classes_path: ClassesImporter::DEFAULT_PATH, members_path: ClassMembersImporter::DEFAULT_PATH) + from_classes = JSON.parse(File.read(classes_path)).filter_map { |entry| entry.dig("class", "semester") } + from_members = JSON.parse(File.read(members_path)).filter_map { |entry| entry["semester"] } + + (from_classes + from_members).uniq.sort.reverse + end + + private + + attr_reader :semester, :imported_by, :classes_path, :members_path + end +end diff --git a/project/app/views/dashboard/show.html.erb b/project/app/views/dashboard/show.html.erb new file mode 100644 index 0000000000..167ad71ba0 --- /dev/null +++ b/project/app/views/dashboard/show.html.erb @@ -0,0 +1,20 @@ +

CAMAAR

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

Usuario autenticado: <%= current_user.name %>

diff --git a/project/app/views/formularios/_form.html.erb b/project/app/views/formularios/_form.html.erb new file mode 100644 index 0000000000..707ad1d3b3 --- /dev/null +++ b/project/app/views/formularios/_form.html.erb @@ -0,0 +1,31 @@ +<%= form_with(model: formulario) do |form| %> + <% if formulario.errors.any? %> +
+

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

+ +
+ <% end %> + +
+ <%= form.label :title, "Nome do formulário:" %> + <%= form.text_field :title %> +
+ +
+ <%= form.label :template_id, "Template:" %> + <%= form.collection_select :template_id, @templates, :id, :title, include_blank: "Selecione um template" %> +
+ +
+ <%= form.label :course_class_id, "Turma:" %> + <%= form.collection_select :course_class_id, @course_classes, :id, :name, include_blank: "Selecione uma turma" %> +
+ +
+ <%= form.submit "Enviar" %> +
+<% end %> diff --git a/project/app/views/formularios/_formulario.html.erb b/project/app/views/formularios/_formulario.html.erb new file mode 100644 index 0000000000..aa0adb521c --- /dev/null +++ b/project/app/views/formularios/_formulario.html.erb @@ -0,0 +1,31 @@ +
+
+ Title: + <%= formulario.title %> +
+ +
+ Target role: + <%= formulario.target_role %> +
+ +
+ Status: + <%= formulario.status %> +
+ +
+ Template: + <%= formulario.template_id %> +
+ +
+ Turma: + <%= formulario.course_class_id %> +
+ +
+ Admin: + <%= formulario.admin_id %> +
+
diff --git a/project/app/views/formularios/_formulario.json.jbuilder b/project/app/views/formularios/_formulario.json.jbuilder new file mode 100644 index 0000000000..7572ef155d --- /dev/null +++ b/project/app/views/formularios/_formulario.json.jbuilder @@ -0,0 +1,2 @@ +json.extract! formulario, :id, :title, :target_role, :status, :template_id, :turma_id, :admin_id, :created_at, :updated_at +json.url formulario_url(formulario, format: :json) diff --git a/project/app/views/formularios/edit.html.erb b/project/app/views/formularios/edit.html.erb new file mode 100644 index 0000000000..2fb833aa29 --- /dev/null +++ b/project/app/views/formularios/edit.html.erb @@ -0,0 +1,12 @@ +<% content_for :title, "Editing formulario" %> + +

Editing formulario

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

<%= notice %>

+

<%= alert %>

+ +

Formulários

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

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

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

Avaliações Pendentes

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

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

+ <% else %> +
+ <% @formularios_pendentes.each do |formulario| %> +
+ <%= formulario.title %> +
+ <%= link_to "Responder Avaliação", formulario %> +
+
+ <% end %> +
+ <% end %> +<% end %> diff --git a/project/app/views/formularios/index.json.jbuilder b/project/app/views/formularios/index.json.jbuilder new file mode 100644 index 0000000000..70770973a8 --- /dev/null +++ b/project/app/views/formularios/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @formularios, partial: "formularios/formulario", as: :formulario diff --git a/project/app/views/formularios/new.html.erb b/project/app/views/formularios/new.html.erb new file mode 100644 index 0000000000..5f3d969fa3 --- /dev/null +++ b/project/app/views/formularios/new.html.erb @@ -0,0 +1,11 @@ +<% content_for :title, "Novo formulário" %> + +

Novo formulário

+ +<%= render "form", formulario: @formulario %> + +
+ +
+ <%= link_to "Voltar para formulários", formularios_path %> +
diff --git a/project/app/views/formularios/show.html.erb b/project/app/views/formularios/show.html.erb new file mode 100644 index 0000000000..74a78c837e --- /dev/null +++ b/project/app/views/formularios/show.html.erb @@ -0,0 +1,42 @@ +

<%= notice %>

+

<%= alert %>

+ +

<%= @formulario.title %>

+

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

+ +
+ +<% if current_user.admin? %> + <%# -- VISÃO DO ADMINISTRADOR (Issue 4) -- %> +

Resultados

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

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

+ <% end %> + +

+ <%= link_to "Voltar para listagem", formularios_path %> + +<% else %> + <%# -- VISÃO DO PARTICIPANTE RESPONDENDO (Issue 3) -- %> +

Responda o Questionário

+ + <%= form_with scope: :respostum, url: resposta_path, method: :post do |f| %> + <%# Atributos ocultos necessários para sincronizar com as colunas do banco %> + <%= f.hidden_field :formulario_id, value: @formulario.id %> + <%= f.hidden_field :questao_id, value: @formulario.questaos.first&.id %> + +
+ <%= f.label :valor_numerico, "Score" %>
+ <%= f.number_field :valor_numerico %> +
+ +
+ <%= f.submit "Enviar Resposta" %> + <% end %> + +
+ <%= link_to "Voltar", formularios_path %> +<% end %> \ No newline at end of file diff --git a/project/app/views/formularios/show.json.jbuilder b/project/app/views/formularios/show.json.jbuilder new file mode 100644 index 0000000000..476f64d624 --- /dev/null +++ b/project/app/views/formularios/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! "formularios/formulario", formulario: @formulario diff --git a/project/app/views/imports/sigaa/new.html.erb b/project/app/views/imports/sigaa/new.html.erb new file mode 100644 index 0000000000..0a23da92a4 --- /dev/null +++ b/project/app/views/imports/sigaa/new.html.erb @@ -0,0 +1,19 @@ +

Importacao de Dados do SIGAA

+ +<%= render "shared/flash" %> + +<%= form_with scope: :sigaa_sync, url: imports_sigaa_path, method: :post do |form| %> +
+ <%= form.label :semester, "Periodo letivo" %> + <%= form.select :semester, options_for_select(@semesters, @semester) %> +
+ +
+ <%= form.label :scope, "Dados para sincronizar" %> +

Turmas, Materias e Participantes

+
+ + <%= form.submit "Sincronizar turmas, materias e participantes" %> +<% end %> + +

<%= link_to "Voltar ao painel", root_path %>

diff --git a/project/app/views/layouts/application.html.erb b/project/app/views/layouts/application.html.erb new file mode 100644 index 0000000000..6a1d584026 --- /dev/null +++ b/project/app/views/layouts/application.html.erb @@ -0,0 +1,29 @@ + + + + <%= content_for(:title) || "Project" %> + + + + + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + + <%= yield :head %> + + <%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %> + <%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %> + + + + + + <%# Includes all stylesheet files in app/assets/stylesheets %> + <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %> + <%= javascript_importmap_tags %> + + + + <%= yield %> + + diff --git a/project/app/views/layouts/mailer.html.erb b/project/app/views/layouts/mailer.html.erb new file mode 100644 index 0000000000..3aac9002ed --- /dev/null +++ b/project/app/views/layouts/mailer.html.erb @@ -0,0 +1,13 @@ + + + + + + + + + <%= yield %> + + diff --git a/project/app/views/layouts/mailer.text.erb b/project/app/views/layouts/mailer.text.erb new file mode 100644 index 0000000000..37f0bddbd7 --- /dev/null +++ b/project/app/views/layouts/mailer.text.erb @@ -0,0 +1 @@ +<%= yield %> diff --git a/project/app/views/password_resets/edit.html.erb b/project/app/views/password_resets/edit.html.erb new file mode 100644 index 0000000000..1f5eb36ee3 --- /dev/null +++ b/project/app/views/password_resets/edit.html.erb @@ -0,0 +1,17 @@ +

Redefinir senha

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

Link invalido

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

Esqueci minha senha

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

Definir senha

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

Link invalido

+ +<%= render "shared/flash" %> diff --git a/project/app/views/pwa/manifest.json.erb b/project/app/views/pwa/manifest.json.erb new file mode 100644 index 0000000000..5cd24347d3 --- /dev/null +++ b/project/app/views/pwa/manifest.json.erb @@ -0,0 +1,22 @@ +{ + "name": "Project", + "icons": [ + { + "src": "/icon.png", + "type": "image/png", + "sizes": "512x512" + }, + { + "src": "/icon.png", + "type": "image/png", + "sizes": "512x512", + "purpose": "maskable" + } + ], + "start_url": "/", + "display": "standalone", + "scope": "/", + "description": "Project.", + "theme_color": "red", + "background_color": "red" +} diff --git a/project/app/views/pwa/service-worker.js b/project/app/views/pwa/service-worker.js new file mode 100644 index 0000000000..b3a13fb7bb --- /dev/null +++ b/project/app/views/pwa/service-worker.js @@ -0,0 +1,26 @@ +// Add a service worker for processing Web Push notifications: +// +// self.addEventListener("push", async (event) => { +// const { title, options } = await event.data.json() +// event.waitUntil(self.registration.showNotification(title, options)) +// }) +// +// self.addEventListener("notificationclick", function(event) { +// event.notification.close() +// event.waitUntil( +// clients.matchAll({ type: "window" }).then((clientList) => { +// for (let i = 0; i < clientList.length; i++) { +// let client = clientList[i] +// let clientPath = (new URL(client.url)).pathname +// +// if (clientPath == event.notification.data.path && "focus" in client) { +// return client.focus() +// } +// } +// +// if (clients.openWindow) { +// return clients.openWindow(event.notification.data.path) +// } +// }) +// ) +// }) diff --git a/project/app/views/questaos/_form.html.erb b/project/app/views/questaos/_form.html.erb new file mode 100644 index 0000000000..d5c685e11e --- /dev/null +++ b/project/app/views/questaos/_form.html.erb @@ -0,0 +1,37 @@ +<%= form_with(model: questao) do |form| %> + <% if questao.errors.any? %> +
+

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

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

Editing questao

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

<%= notice %>

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

Questaos

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

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

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

New questao

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

<%= notice %>

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

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

+ + +
+ <% end %> + +
+ <%= form.label :valor_texto, "Comentário (Opcional)", style: "display: block" %> + <%= form.text_area :valor_texto %> +
+ +
+ <%= form.label :Score, "Nota da Avaliação", style: "display: block" %> + <%= form.number_field :Score %> +
+ +
+ <%= form.label :submissao_id, style: "display: block" %> + <%= form.text_field :submissao_id %> +
+ +
+ <%= form.label :questao_id, style: "display: block" %> + <%= form.text_field :questao_id %> +
+ +
+ <%= form.submit "Enviar Resposta" %> +
+<% end %> diff --git a/project/app/views/resposta/_respostum.html.erb b/project/app/views/resposta/_respostum.html.erb new file mode 100644 index 0000000000..cea81e4be5 --- /dev/null +++ b/project/app/views/resposta/_respostum.html.erb @@ -0,0 +1,22 @@ +
+
+ Valor texto: + <%= respostum.valor_texto %> +
+ +
+ Valor numerico: + <%= respostum.valor_numerico %> +
+ +
+ Submissao: + <%= respostum.submissao_id %> +
+ +
+ Questao: + <%= respostum.questao_id %> +
+ +
diff --git a/project/app/views/resposta/_respostum.json.jbuilder b/project/app/views/resposta/_respostum.json.jbuilder new file mode 100644 index 0000000000..8d02d3b179 --- /dev/null +++ b/project/app/views/resposta/_respostum.json.jbuilder @@ -0,0 +1,2 @@ +json.extract! respostum, :id, :valor_texto, :valor_numerico, :submissao_id, :questao_id, :created_at, :updated_at +json.url respostum_url(respostum, format: :json) diff --git a/project/app/views/resposta/edit.html.erb b/project/app/views/resposta/edit.html.erb new file mode 100644 index 0000000000..e84eb42170 --- /dev/null +++ b/project/app/views/resposta/edit.html.erb @@ -0,0 +1,12 @@ +<% content_for :title, "Editing respostum" %> + +

Editing respostum

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

<%= notice %>

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

Resposta

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

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

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

Responder Avaliação

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

<%= notice %>

+ +<%= render @respostum %> + +
+ <%= link_to "Edit this respostum", edit_respostum_path(@respostum) %> | + <%= link_to "Back to resposta", resposta_path %> + + <%= button_to "Destroy this respostum", @respostum, method: :delete %> +
diff --git a/project/app/views/resposta/show.json.jbuilder b/project/app/views/resposta/show.json.jbuilder new file mode 100644 index 0000000000..f1575b75b6 --- /dev/null +++ b/project/app/views/resposta/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! "resposta/respostum", respostum: @respostum diff --git a/project/app/views/sessions/new.html.erb b/project/app/views/sessions/new.html.erb new file mode 100644 index 0000000000..9d877e010f --- /dev/null +++ b/project/app/views/sessions/new.html.erb @@ -0,0 +1,19 @@ +

Login

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

<%= message %>

+<% end %> diff --git a/project/app/views/submissaos/_form.html.erb b/project/app/views/submissaos/_form.html.erb new file mode 100644 index 0000000000..46a15dc54a --- /dev/null +++ b/project/app/views/submissaos/_form.html.erb @@ -0,0 +1,27 @@ +<%= form_with(model: submissao) do |form| %> + <% if submissao.errors.any? %> +
+

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

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

Editing submissao

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

<%= notice %>

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

Submissaos

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

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

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

New submissao

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

<%= notice %>

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

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

+ + +
+ <% end %> + +
+ <%= form.label :title, "Nome do template:" %> + <%= form.text_field :title %> +
+ +
+ <%= form.label :target_role, "Público-alvo:" %> + <%= form.select :target_role, [ [ "Discentes", "discente" ], [ "Docentes", "docente" ] ], include_blank: "Selecione" %> +
+ +

Questões

+ +
+ <% questoes_attrs.each_with_index do |questao, index| %> +
+ <% if questao[:id] %> + <%= hidden_field_tag "template[questaos][#{index}][id]", questao[:id] %> + <% end %> +
+ <%= label_tag "template_questaos_#{index}_enunciado", "Enunciado da questão:" %> + <%= text_area_tag "template[questaos][#{index}][enunciado]", questao[:enunciado], id: "template_questaos_#{index}_enunciado" %> +
+
+ <%= label_tag "template_questaos_#{index}_tipo", "Tipo:" %> + <%= select_tag "template[questaos][#{index}][tipo]", + options_for_select([ [ "Nota (1-5)", "rating" ], [ "Texto", "text" ], [ "Sim/Não", "boolean" ] ], questao[:tipo]), + id: "template_questaos_#{index}_tipo" %> +
+
+ <% end %> +
+ + <%= submit_tag "+", name: "add_questao", data: { turbo: false } %> + +
+ <%= form.submit(template.persisted? ? "Confirmar edição" : "Criar") %> +
+<% end %> diff --git a/project/app/views/templates/_template.json.jbuilder b/project/app/views/templates/_template.json.jbuilder new file mode 100644 index 0000000000..aad7a56810 --- /dev/null +++ b/project/app/views/templates/_template.json.jbuilder @@ -0,0 +1,2 @@ +json.extract! template, :id, :title, :target_role, :admin_id, :created_at, :updated_at +json.url template_url(template, format: :json) diff --git a/project/app/views/templates/edit.html.erb b/project/app/views/templates/edit.html.erb new file mode 100644 index 0000000000..2ae4c24299 --- /dev/null +++ b/project/app/views/templates/edit.html.erb @@ -0,0 +1,11 @@ +<% content_for :title, "Editar template" %> + +

Editar template

+ +<%= render "form", template: @template, questoes_attrs: @questoes_attrs %> + +
+ +
+ <%= link_to "Voltar para templates", templates_path %> +
diff --git a/project/app/views/templates/index.html.erb b/project/app/views/templates/index.html.erb new file mode 100644 index 0000000000..88c379f5b2 --- /dev/null +++ b/project/app/views/templates/index.html.erb @@ -0,0 +1,23 @@ +

<%= notice %>

+

<%= alert %>

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

Meus Templates

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

<%= template.title %>

+

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

+

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

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

Novo template

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

<%= notice %>

+

<%= alert %>

+ +<% content_for :title, @template.title %> + +

<%= @template.title %>

+ +

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

+ +

Questões

+ + + +
+ <%= link_to "Editar", edit_template_path(@template) %> +
+ +
+

Tem certeza que deseja deletar este template?

+ <%= button_to "Confirmar", template_path(@template), method: :delete %> +
+ +
+ <%= link_to "Voltar para templates", templates_path %> +
diff --git a/project/app/views/templates/show.json.jbuilder b/project/app/views/templates/show.json.jbuilder new file mode 100644 index 0000000000..33110ec93b --- /dev/null +++ b/project/app/views/templates/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! "templates/template", template: @template diff --git a/project/app/views/user_mailer/password_reset.html.erb b/project/app/views/user_mailer/password_reset.html.erb new file mode 100644 index 0000000000..e72841469d --- /dev/null +++ b/project/app/views/user_mailer/password_reset.html.erb @@ -0,0 +1,5 @@ +

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

+ +

Use o link abaixo para redefinir sua senha no CAMAAR.

+ +

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

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

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

+ +

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

+ +

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

diff --git a/project/app/views/user_mailer/password_setup.text.erb b/project/app/views/user_mailer/password_setup.text.erb new file mode 100644 index 0000000000..5db9bce1f9 --- /dev/null +++ b/project/app/views/user_mailer/password_setup.text.erb @@ -0,0 +1,5 @@ +Ola, <%= @user.name %>. + +Use o link abaixo para definir sua primeira senha no CAMAAR. + +<%= @url %> diff --git a/project/bin/brakeman b/project/bin/brakeman new file mode 100755 index 0000000000..ace1c9ba08 --- /dev/null +++ b/project/bin/brakeman @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby +require "rubygems" +require "bundler/setup" + +ARGV.unshift("--ensure-latest") + +load Gem.bin_path("brakeman", "brakeman") diff --git a/project/bin/bundler-audit b/project/bin/bundler-audit new file mode 100755 index 0000000000..e2ef22690c --- /dev/null +++ b/project/bin/bundler-audit @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby +require_relative "../config/boot" +require "bundler/audit/cli" + +ARGV.concat %w[ --config config/bundler-audit.yml ] if ARGV.empty? || ARGV.include?("check") +Bundler::Audit::CLI.start diff --git a/project/bin/ci b/project/bin/ci new file mode 100755 index 0000000000..4137ad5bb0 --- /dev/null +++ b/project/bin/ci @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby +require_relative "../config/boot" +require "active_support/continuous_integration" + +CI = ActiveSupport::ContinuousIntegration +require_relative "../config/ci.rb" diff --git a/project/bin/cucumber b/project/bin/cucumber new file mode 100755 index 0000000000..eb5e962e86 --- /dev/null +++ b/project/bin/cucumber @@ -0,0 +1,11 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +vendored_cucumber_bin = Dir["#{File.dirname(__FILE__)}/../vendor/{gems,plugins}/cucumber*/bin/cucumber"].first +if vendored_cucumber_bin + load File.expand_path(vendored_cucumber_bin) +else + require 'rubygems' unless ENV['NO_RUBYGEMS'] + require 'cucumber' + load Cucumber::BINARY +end diff --git a/project/bin/dev b/project/bin/dev new file mode 100755 index 0000000000..5f91c20545 --- /dev/null +++ b/project/bin/dev @@ -0,0 +1,2 @@ +#!/usr/bin/env ruby +exec "./bin/rails", "server", *ARGV diff --git a/project/bin/docker-entrypoint b/project/bin/docker-entrypoint new file mode 100755 index 0000000000..ed31659f40 --- /dev/null +++ b/project/bin/docker-entrypoint @@ -0,0 +1,8 @@ +#!/bin/bash -e + +# If running the rails server then create or migrate existing database +if [ "${@: -2:1}" == "./bin/rails" ] && [ "${@: -1:1}" == "server" ]; then + ./bin/rails db:prepare +fi + +exec "${@}" diff --git a/project/bin/importmap b/project/bin/importmap new file mode 100755 index 0000000000..36502ab16c --- /dev/null +++ b/project/bin/importmap @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby + +require_relative "../config/application" +require "importmap/commands" diff --git a/project/bin/jobs b/project/bin/jobs new file mode 100755 index 0000000000..dcf59f309a --- /dev/null +++ b/project/bin/jobs @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby + +require_relative "../config/environment" +require "solid_queue/cli" + +SolidQueue::Cli.start(ARGV) diff --git a/project/bin/kamal b/project/bin/kamal new file mode 100755 index 0000000000..d9ba276702 --- /dev/null +++ b/project/bin/kamal @@ -0,0 +1,16 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'kamal' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("kamal", "kamal") diff --git a/project/bin/rails b/project/bin/rails new file mode 100755 index 0000000000..efc0377492 --- /dev/null +++ b/project/bin/rails @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +APP_PATH = File.expand_path("../config/application", __dir__) +require_relative "../config/boot" +require "rails/commands" diff --git a/project/bin/rake b/project/bin/rake new file mode 100755 index 0000000000..4fbf10b960 --- /dev/null +++ b/project/bin/rake @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +require_relative "../config/boot" +require "rake" +Rake.application.run diff --git a/project/bin/rubocop b/project/bin/rubocop new file mode 100755 index 0000000000..5a20504716 --- /dev/null +++ b/project/bin/rubocop @@ -0,0 +1,8 @@ +#!/usr/bin/env ruby +require "rubygems" +require "bundler/setup" + +# Explicit RuboCop config increases performance slightly while avoiding config confusion. +ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__)) + +load Gem.bin_path("rubocop", "rubocop") diff --git a/project/bin/setup b/project/bin/setup new file mode 100755 index 0000000000..81be011e87 --- /dev/null +++ b/project/bin/setup @@ -0,0 +1,35 @@ +#!/usr/bin/env ruby +require "fileutils" + +APP_ROOT = File.expand_path("..", __dir__) + +def system!(*args) + system(*args, exception: true) +end + +FileUtils.chdir APP_ROOT do + # This script is a way to set up or update your development environment automatically. + # This script is idempotent, so that you can run it at any time and get an expectable outcome. + # Add necessary setup steps to this file. + + puts "== Installing dependencies ==" + system("bundle check") || system!("bundle install") + + # puts "\n== Copying sample files ==" + # unless File.exist?("config/database.yml") + # FileUtils.cp "config/database.yml.sample", "config/database.yml" + # end + + puts "\n== Preparing database ==" + system! "bin/rails db:prepare" + system! "bin/rails db:reset" if ARGV.include?("--reset") + + puts "\n== Removing old logs and tempfiles ==" + system! "bin/rails log:clear tmp:clear" + + unless ARGV.include?("--skip-server") + puts "\n== Starting development server ==" + STDOUT.flush # flush the output before exec(2) so that it displays + exec "bin/dev" + end +end diff --git a/project/bin/thrust b/project/bin/thrust new file mode 100755 index 0000000000..36bde2d832 --- /dev/null +++ b/project/bin/thrust @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("thruster", "thrust") diff --git a/project/config.ru b/project/config.ru new file mode 100644 index 0000000000..4a3c09a688 --- /dev/null +++ b/project/config.ru @@ -0,0 +1,6 @@ +# This file is used by Rack-based servers to start the application. + +require_relative "config/environment" + +run Rails.application +Rails.application.load_server diff --git a/project/config/application.rb b/project/config/application.rb new file mode 100644 index 0000000000..a69c974960 --- /dev/null +++ b/project/config/application.rb @@ -0,0 +1,27 @@ +require_relative "boot" + +require "rails/all" + +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(*Rails.groups) + +module Project + class Application < Rails::Application + # Initialize configuration defaults for originally generated Rails version. + config.load_defaults 8.1 + + # Please, add to the `ignore` list any other `lib` subdirectories that do + # not contain `.rb` files, or that should not be reloaded or eager loaded. + # Common ones are `templates`, `generators`, or `middleware`, for example. + config.autoload_lib(ignore: %w[assets tasks]) + + # Configuration for the application, engines, and railties goes here. + # + # These settings can be overridden in specific environments using the files + # in config/environments, which are processed later. + # + # config.time_zone = "Central Time (US & Canada)" + # config.eager_load_paths << Rails.root.join("extras") + end +end diff --git a/project/config/boot.rb b/project/config/boot.rb new file mode 100644 index 0000000000..988a5ddc46 --- /dev/null +++ b/project/config/boot.rb @@ -0,0 +1,4 @@ +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +require "bundler/setup" # Set up gems listed in the Gemfile. +require "bootsnap/setup" # Speed up boot time by caching expensive operations. diff --git a/project/config/bundler-audit.yml b/project/config/bundler-audit.yml new file mode 100644 index 0000000000..e74b3af949 --- /dev/null +++ b/project/config/bundler-audit.yml @@ -0,0 +1,5 @@ +# Audit all gems listed in the Gemfile for known security problems by running bin/bundler-audit. +# CVEs that are not relevant to the application can be enumerated on the ignore list below. + +ignore: + - CVE-THAT-DOES-NOT-APPLY diff --git a/project/config/cable.yml b/project/config/cable.yml new file mode 100644 index 0000000000..b9adc5aa3a --- /dev/null +++ b/project/config/cable.yml @@ -0,0 +1,17 @@ +# Async adapter only works within the same process, so for manually triggering cable updates from a console, +# and seeing results in the browser, you must do so from the web console (running inside the dev process), +# not a terminal started via bin/rails console! Add "console" to any action or any ERB template view +# to make the web console appear. +development: + adapter: async + +test: + adapter: test + +production: + adapter: solid_cable + connects_to: + database: + writing: cable + polling_interval: 0.1.seconds + message_retention: 1.day diff --git a/project/config/cache.yml b/project/config/cache.yml new file mode 100644 index 0000000000..19d490843b --- /dev/null +++ b/project/config/cache.yml @@ -0,0 +1,16 @@ +default: &default + store_options: + # Cap age of oldest cache entry to fulfill retention policies + # max_age: <%= 60.days.to_i %> + max_size: <%= 256.megabytes %> + namespace: <%= Rails.env %> + +development: + <<: *default + +test: + <<: *default + +production: + database: cache + <<: *default diff --git a/project/config/ci.rb b/project/config/ci.rb new file mode 100644 index 0000000000..1712cc1127 --- /dev/null +++ b/project/config/ci.rb @@ -0,0 +1,24 @@ +# Run using bin/ci + +CI.run do + step "Setup", "bin/setup --skip-server" + + step "Style: Ruby", "bin/rubocop" + + step "Security: Gem audit", "bin/bundler-audit" + step "Security: Importmap vulnerability audit", "bin/importmap audit" + step "Security: Brakeman code analysis", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error" + step "Tests: Rails", "bin/rails test" + step "Tests: Seeds", "env RAILS_ENV=test bin/rails db:seed:replant" + + # Optional: Run system tests + # step "Tests: System", "bin/rails test:system" + + # Optional: set a green GitHub commit status to unblock PR merge. + # Requires the `gh` CLI and `gh extension install basecamp/gh-signoff`. + # if success? + # step "Signoff: All systems go. Ready for merge and deploy.", "gh signoff" + # else + # failure "Signoff: CI failed. Do not merge or deploy.", "Fix the issues and try again." + # end +end diff --git a/project/config/credentials.yml.enc b/project/config/credentials.yml.enc new file mode 100644 index 0000000000..6fa056b6fe --- /dev/null +++ b/project/config/credentials.yml.enc @@ -0,0 +1 @@ +zAhTSDhDMGxkP/oq8ep7nzeYW8usCy5ZAxebSamtvX+ePfQSIR7IsVz+7jOMqX2UgLTnGu3zEiPjQ+DESz7Q5J8l71GqGwKXG3WxBmaUAmrY6D3QnMONjJZFKbe5i51SRFyKchJ0h3h1FboH4hYg1D1fsJluy1uFLRlA91YZ70uKFXm9DMIfFysymte1KNx762s/S//88EuH0ov5vkeiuAn/FPS4VmcDuKaKQ63tysC36Vqlyv/N4ZwQ2DD6xbzQZgEFfO5VBDmXeZYc0HqwcfOksHC5FbwsPDzRokwwYOKlbrRV85LOzjNQcJQzaXhcc1r20FzksWEbH7q2Q3vQT90yD/Hx83dmoj5jbgbGJpzxjiwykpvU8upKMjVhrxMV4wrOiAQMtP/2uyLORHXZ7ptQ2XwPsUkIPuzMpCaLxGMpWYujblmkUq/FVNGfZHjIZ2icWU4hKve5LqC2TkJV5NY0qPrgQh1cIPSskSOunisOOu9N+NOkrVm6--EAc8LSksNFtj+8n3--qryypbJabJv0tzjhOgJVkQ== \ No newline at end of file diff --git a/project/config/cucumber.yml b/project/config/cucumber.yml new file mode 100644 index 0000000000..47a4663ae2 --- /dev/null +++ b/project/config/cucumber.yml @@ -0,0 +1,8 @@ +<% +rerun = File.file?('rerun.txt') ? IO.read('rerun.txt') : "" +rerun = rerun.strip.gsub /\s/, ' ' +rerun_opts = rerun.empty? ? "--format #{ENV['CUCUMBER_FORMAT'] || 'progress'} features" : "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} #{rerun}" +std_opts = "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} --strict --tags 'not @wip'" +%> +default: <%= std_opts %> features +rerun: <%= rerun_opts %> --format rerun --out rerun.txt --strict --tags 'not @wip' diff --git a/project/config/database.yml b/project/config/database.yml new file mode 100644 index 0000000000..302d638c96 --- /dev/null +++ b/project/config/database.yml @@ -0,0 +1,40 @@ +# SQLite. Versions 3.8.0 and up are supported. +# gem install sqlite3 +# +# Ensure the SQLite 3 gem is defined in your Gemfile +# gem "sqlite3" +# +default: &default + adapter: sqlite3 + max_connections: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + timeout: 5000 + +development: + <<: *default + database: storage/development.sqlite3 + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + <<: *default + database: storage/test.sqlite3 + +# Store production database in the storage/ directory, which by default +# is mounted as a persistent Docker volume in config/deploy.yml. +production: + primary: + <<: *default + database: storage/production.sqlite3 + cache: + <<: *default + database: storage/production_cache.sqlite3 + migrations_paths: db/cache_migrate + queue: + <<: *default + database: storage/production_queue.sqlite3 + migrations_paths: db/queue_migrate + cable: + <<: *default + database: storage/production_cable.sqlite3 + migrations_paths: db/cable_migrate diff --git a/project/config/deploy.yml b/project/config/deploy.yml new file mode 100644 index 0000000000..167f14168a --- /dev/null +++ b/project/config/deploy.yml @@ -0,0 +1,119 @@ +# Name of your application. Used to uniquely configure containers. +service: project + +# Name of the container image (use your-user/app-name on external registries). +image: project + +# Deploy to these servers. +servers: + web: + - 192.168.0.1 + # job: + # hosts: + # - 192.168.0.1 + # cmd: bin/jobs + +# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server. +# If used with Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption. +# +# Using an SSL proxy like this requires turning on config.assume_ssl and config.force_ssl in production.rb! +# +# Don't use this when deploying to multiple web servers (then you have to terminate SSL at your load balancer). +# +# proxy: +# ssl: true +# host: app.example.com + +# Where you keep your container images. +registry: + # Alternatives: hub.docker.com / registry.digitalocean.com / ghcr.io / ... + server: localhost:5555 + + # Needed for authenticated registries. + # username: your-user + + # Always use an access token rather than real password when possible. + # password: + # - KAMAL_REGISTRY_PASSWORD + +# Inject ENV variables into containers (secrets come from .kamal/secrets). +env: + secret: + - RAILS_MASTER_KEY + clear: + # Run the Solid Queue Supervisor inside the web server's Puma process to do jobs. + # When you start using multiple servers, you should split out job processing to a dedicated machine. + SOLID_QUEUE_IN_PUMA: true + + # Set number of processes dedicated to Solid Queue (default: 1) + # JOB_CONCURRENCY: 3 + + # Set number of cores available to the application on each server (default: 1). + # WEB_CONCURRENCY: 2 + + # Match this to any external database server to configure Active Record correctly + # Use project-db for a db accessory server on same machine via local kamal docker network. + # DB_HOST: 192.168.0.2 + + # Log everything from Rails + # RAILS_LOG_LEVEL: debug + +# Aliases are triggered with "bin/kamal ". You can overwrite arguments on invocation: +# "bin/kamal logs -r job" will tail logs from the first server in the job section. +aliases: + console: app exec --interactive --reuse "bin/rails console" + shell: app exec --interactive --reuse "bash" + logs: app logs -f + dbc: app exec --interactive --reuse "bin/rails dbconsole --include-password" + +# Use a persistent storage volume for sqlite database files and local Active Storage files. +# Recommended to change this to a mounted volume path that is backed up off server. +volumes: + - "project_storage:/rails/storage" + +# Bridge fingerprinted assets, like JS and CSS, between versions to avoid +# hitting 404 on in-flight requests. Combines all files from new and old +# version inside the asset_path. +asset_path: /rails/public/assets + +# Configure the image builder. +builder: + arch: amd64 + + # # Build image via remote server (useful for faster amd64 builds on arm64 computers) + # remote: ssh://docker@docker-builder-server + # + # # Pass arguments and secrets to the Docker build process + # args: + # RUBY_VERSION: ruby-3.4.9 + # secrets: + # - GITHUB_TOKEN + # - RAILS_MASTER_KEY + +# Use a different ssh user than root +# ssh: +# user: app + +# Use accessory services (secrets come from .kamal/secrets). +# accessories: +# db: +# image: mysql:8.0 +# host: 192.168.0.2 +# # Change to 3306 to expose port to the world instead of just local network. +# port: "127.0.0.1:3306:3306" +# env: +# clear: +# MYSQL_ROOT_HOST: '%' +# secret: +# - MYSQL_ROOT_PASSWORD +# files: +# - config/mysql/production.cnf:/etc/mysql/my.cnf +# - db/production.sql:/docker-entrypoint-initdb.d/setup.sql +# directories: +# - data:/var/lib/mysql +# redis: +# image: valkey/valkey:8 +# host: 192.168.0.2 +# port: 6379 +# directories: +# - data:/data diff --git a/project/config/environment.rb b/project/config/environment.rb new file mode 100644 index 0000000000..cac5315775 --- /dev/null +++ b/project/config/environment.rb @@ -0,0 +1,5 @@ +# Load the Rails application. +require_relative "application" + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/project/config/environments/development.rb b/project/config/environments/development.rb new file mode 100644 index 0000000000..4a6ea19c9c --- /dev/null +++ b/project/config/environments/development.rb @@ -0,0 +1,82 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Configure 'rails notes' to inspect Cucumber files + config.annotations.register_directories("features") + config.annotations.register_extensions("feature") { |tag| /#\s*(#{tag}):?\s*(.*)$/ } + + # Settings specified here will take precedence over those in config/application.rb. + + # Make code changes take effect immediately without server restart. + config.enable_reloading = true + + # Do not eager load code on boot. + config.eager_load = false + + # Show full error reports. + config.consider_all_requests_local = true + + # Enable server timing. + config.server_timing = true + + # Enable/disable Action Controller caching. By default Action Controller caching is disabled. + # Run rails dev:cache to toggle Action Controller caching. + if Rails.root.join("tmp/caching-dev.txt").exist? + config.action_controller.perform_caching = true + config.action_controller.enable_fragment_cache_logging = true + config.public_file_server.headers = { "cache-control" => "public, max-age=#{2.days.to_i}" } + else + config.action_controller.perform_caching = false + end + + # Change to :null_store to avoid any caching. + config.cache_store = :memory_store + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + + # Don't care if the mailer can't send. + config.action_mailer.raise_delivery_errors = false + + # Make template changes take effect immediately. + config.action_mailer.perform_caching = false + + # Set localhost to be used by links generated in mailer templates. + config.action_mailer.default_url_options = { host: "localhost", port: 3000 } + + # Print deprecation notices to the Rails logger. + config.active_support.deprecation = :log + + # Raise an error on page load if there are pending migrations. + config.active_record.migration_error = :page_load + + # Highlight code that triggered database queries in logs. + config.active_record.verbose_query_logs = true + + # Append comments with runtime information tags to SQL queries in logs. + config.active_record.query_log_tags_enabled = true + + # Highlight code that enqueued background job in logs. + config.active_job.verbose_enqueue_logs = true + + # Highlight code that triggered redirect in logs. + config.action_dispatch.verbose_redirect_logs = true + + # Suppress logger output for asset requests. + config.assets.quiet = true + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + config.action_view.annotate_rendered_view_with_filenames = true + + # Uncomment if you wish to allow Action Cable access from any origin. + # config.action_cable.disable_request_forgery_protection = true + + # Raise error when a before_action's only/except options reference missing actions. + config.action_controller.raise_on_missing_callback_actions = true + + # Apply autocorrection by RuboCop to files generated by `bin/rails generate`. + # config.generators.apply_rubocop_autocorrect_after_generate! +end diff --git a/project/config/environments/production.rb b/project/config/environments/production.rb new file mode 100644 index 0000000000..f5763e04e5 --- /dev/null +++ b/project/config/environments/production.rb @@ -0,0 +1,90 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Code is not reloaded between requests. + config.enable_reloading = false + + # Eager load code on boot for better performance and memory savings (ignored by Rake tasks). + config.eager_load = true + + # Full error reports are disabled. + config.consider_all_requests_local = false + + # Turn on fragment caching in view templates. + config.action_controller.perform_caching = true + + # Cache assets for far-future expiry since they are all digest stamped. + config.public_file_server.headers = { "cache-control" => "public, max-age=#{1.year.to_i}" } + + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.asset_host = "http://assets.example.com" + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + + # Assume all access to the app is happening through a SSL-terminating reverse proxy. + # config.assume_ssl = true + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + # config.force_ssl = true + + # Skip http-to-https redirect for the default health check endpoint. + # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } } + + # Log to STDOUT with the current request id as a default log tag. + config.log_tags = [ :request_id ] + config.logger = ActiveSupport::TaggedLogging.logger(STDOUT) + + # Change to "debug" to log everything (including potentially personally-identifiable information!). + config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") + + # Prevent health checks from clogging up the logs. + config.silence_healthcheck_path = "/up" + + # Don't log any deprecations. + config.active_support.report_deprecations = false + + # Replace the default in-process memory cache store with a durable alternative. + config.cache_store = :solid_cache_store + + # Replace the default in-process and non-durable queuing backend for Active Job. + config.active_job.queue_adapter = :solid_queue + config.solid_queue.connects_to = { database: { writing: :queue } } + + # Ignore bad email addresses and do not raise email delivery errors. + # Set this to true and configure the email server for immediate delivery to raise delivery errors. + # config.action_mailer.raise_delivery_errors = false + + # Set host to be used by links generated in mailer templates. + config.action_mailer.default_url_options = { host: "example.com" } + + # Specify outgoing SMTP server. Remember to add smtp/* credentials via bin/rails credentials:edit. + # config.action_mailer.smtp_settings = { + # user_name: Rails.application.credentials.dig(:smtp, :user_name), + # password: Rails.application.credentials.dig(:smtp, :password), + # address: "smtp.example.com", + # port: 587, + # authentication: :plain + # } + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true + + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false + + # Only use :id for inspections in production. + config.active_record.attributes_for_inspect = [ :id ] + + # Enable DNS rebinding protection and other `Host` header attacks. + # config.hosts = [ + # "example.com", # Allow requests from example.com + # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` + # ] + # + # Skip DNS rebinding protection for the default health check endpoint. + # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } +end diff --git a/project/config/environments/test.rb b/project/config/environments/test.rb new file mode 100644 index 0000000000..29d195b837 --- /dev/null +++ b/project/config/environments/test.rb @@ -0,0 +1,57 @@ +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! + +Rails.application.configure do + # Configure 'rails notes' to inspect Cucumber files + config.annotations.register_directories("features") + config.annotations.register_extensions("feature") { |tag| /#\s*(#{tag}):?\s*(.*)$/ } + + # Settings specified here will take precedence over those in config/application.rb. + + # While tests run files are not watched, reloading is not necessary. + config.enable_reloading = false + + # Eager loading loads your entire application. When running a single test locally, + # this is usually not necessary, and can slow down your test suite. However, it's + # recommended that you enable it in continuous integration systems to ensure eager + # loading is working properly before deploying your code. + config.eager_load = ENV["CI"].present? + + # Configure public file server for tests with cache-control for performance. + config.public_file_server.headers = { "cache-control" => "public, max-age=3600" } + + # Show full error reports. + config.consider_all_requests_local = true + config.cache_store = :null_store + + # Render exception templates for rescuable exceptions and raise for other exceptions. + config.action_dispatch.show_exceptions = :rescuable + + # Disable request forgery protection in test environment. + config.action_controller.allow_forgery_protection = false + + # Store uploaded files on the local file system in a temporary directory. + config.active_storage.service = :test + + # Tell Action Mailer not to deliver emails to the real world. + # The :test delivery method accumulates sent emails in the + # ActionMailer::Base.deliveries array. + config.action_mailer.delivery_method = :test + + # Set host to be used by links generated in mailer templates. + config.action_mailer.default_url_options = { host: "example.com" } + + # Print deprecation notices to the stderr. + config.active_support.deprecation = :stderr + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true + + # Raise error when a before_action's only/except options reference missing actions. + config.action_controller.raise_on_missing_callback_actions = true +end diff --git a/project/config/importmap.rb b/project/config/importmap.rb new file mode 100644 index 0000000000..909dfc542d --- /dev/null +++ b/project/config/importmap.rb @@ -0,0 +1,7 @@ +# Pin npm packages by running ./bin/importmap + +pin "application" +pin "@hotwired/turbo-rails", to: "turbo.min.js" +pin "@hotwired/stimulus", to: "stimulus.min.js" +pin "@hotwired/stimulus-loading", to: "stimulus-loading.js" +pin_all_from "app/javascript/controllers", under: "controllers" diff --git a/project/config/initializers/assets.rb b/project/config/initializers/assets.rb new file mode 100644 index 0000000000..487324424f --- /dev/null +++ b/project/config/initializers/assets.rb @@ -0,0 +1,7 @@ +# Be sure to restart your server when you modify this file. + +# Version of your assets, change this if you want to expire all your assets. +Rails.application.config.assets.version = "1.0" + +# Add additional assets to the asset load path. +# Rails.application.config.assets.paths << Emoji.images_path diff --git a/project/config/initializers/content_security_policy.rb b/project/config/initializers/content_security_policy.rb new file mode 100644 index 0000000000..d51d713979 --- /dev/null +++ b/project/config/initializers/content_security_policy.rb @@ -0,0 +1,29 @@ +# Be sure to restart your server when you modify this file. + +# Define an application-wide content security policy. +# See the Securing Rails Applications Guide for more information: +# https://guides.rubyonrails.org/security.html#content-security-policy-header + +# Rails.application.configure do +# config.content_security_policy do |policy| +# policy.default_src :self, :https +# policy.font_src :self, :https, :data +# policy.img_src :self, :https, :data +# policy.object_src :none +# policy.script_src :self, :https +# policy.style_src :self, :https +# # Specify URI for violation reports +# # policy.report_uri "/csp-violation-report-endpoint" +# end +# +# # Generate session nonces for permitted importmap, inline scripts, and inline styles. +# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } +# config.content_security_policy_nonce_directives = %w(script-src style-src) +# +# # Automatically add `nonce` to `javascript_tag`, `javascript_include_tag`, and `stylesheet_link_tag` +# # if the corresponding directives are specified in `content_security_policy_nonce_directives`. +# # config.content_security_policy_nonce_auto = true +# +# # Report violations without enforcing the policy. +# # config.content_security_policy_report_only = true +# end diff --git a/project/config/initializers/filter_parameter_logging.rb b/project/config/initializers/filter_parameter_logging.rb new file mode 100644 index 0000000000..c0b717f7ec --- /dev/null +++ b/project/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. +# Use this to limit dissemination of sensitive information. +# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. +Rails.application.config.filter_parameters += [ + :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc +] diff --git a/project/config/initializers/inflections.rb b/project/config/initializers/inflections.rb new file mode 100644 index 0000000000..3860f659ea --- /dev/null +++ b/project/config/initializers/inflections.rb @@ -0,0 +1,16 @@ +# Be sure to restart your server when you modify this file. + +# Add new inflection rules using the following format. Inflections +# are locale specific, and you may define rules for as many different +# locales as you wish. All of these examples are active by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.plural /^(ox)$/i, "\\1en" +# inflect.singular /^(ox)en/i, "\\1" +# inflect.irregular "person", "people" +# inflect.uncountable %w( fish sheep ) +# end + +# These inflection rules are supported but not enabled by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.acronym "RESTful" +# end diff --git a/project/config/locales/en.yml b/project/config/locales/en.yml new file mode 100644 index 0000000000..6c349ae5e3 --- /dev/null +++ b/project/config/locales/en.yml @@ -0,0 +1,31 @@ +# Files in the config/locales directory are used for internationalization and +# are automatically loaded by Rails. If you want to use locales other than +# English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t "hello" +# +# In views, this is aliased to just `t`: +# +# <%= t("hello") %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# To learn more about the API, please read the Rails Internationalization guide +# at https://guides.rubyonrails.org/i18n.html. +# +# Be aware that YAML interprets the following case-insensitive strings as +# booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings +# must be quoted to be interpreted as strings. For example: +# +# en: +# "yes": yup +# enabled: "ON" + +en: + hello: "Hello world" diff --git a/project/config/puma.rb b/project/config/puma.rb new file mode 100644 index 0000000000..38c4b86596 --- /dev/null +++ b/project/config/puma.rb @@ -0,0 +1,42 @@ +# This configuration file will be evaluated by Puma. The top-level methods that +# are invoked here are part of Puma's configuration DSL. For more information +# about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. +# +# Puma starts a configurable number of processes (workers) and each process +# serves each request in a thread from an internal thread pool. +# +# You can control the number of workers using ENV["WEB_CONCURRENCY"]. You +# should only set this value when you want to run 2 or more workers. The +# default is already 1. You can set it to `auto` to automatically start a worker +# for each available processor. +# +# The ideal number of threads per worker depends both on how much time the +# application spends waiting for IO operations and on how much you wish to +# prioritize throughput over latency. +# +# As a rule of thumb, increasing the number of threads will increase how much +# traffic a given process can handle (throughput), but due to CRuby's +# Global VM Lock (GVL) it has diminishing returns and will degrade the +# response time (latency) of the application. +# +# The default is set to 3 threads as it's deemed a decent compromise between +# throughput and latency for the average Rails application. +# +# Any libraries that use a connection pool or another resource pool should +# be configured to provide at least as many connections as the number of +# threads. This includes Active Record's `pool` parameter in `database.yml`. +threads_count = ENV.fetch("RAILS_MAX_THREADS", 3) +threads threads_count, threads_count + +# Specifies the `port` that Puma will listen on to receive requests; default is 3000. +port ENV.fetch("PORT", 3000) + +# Allow puma to be restarted by `bin/rails restart` command. +plugin :tmp_restart + +# Run the Solid Queue supervisor inside of Puma for single-server deployments. +plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"] + +# Specify the PID file. Defaults to tmp/pids/server.pid in development. +# In other environments, only set the PID file if requested. +pidfile ENV["PIDFILE"] if ENV["PIDFILE"] diff --git a/project/config/queue.yml b/project/config/queue.yml new file mode 100644 index 0000000000..9eace59c41 --- /dev/null +++ b/project/config/queue.yml @@ -0,0 +1,18 @@ +default: &default + dispatchers: + - polling_interval: 1 + batch_size: 500 + workers: + - queues: "*" + threads: 3 + processes: <%= ENV.fetch("JOB_CONCURRENCY", 1) %> + polling_interval: 0.1 + +development: + <<: *default + +test: + <<: *default + +production: + <<: *default diff --git a/project/config/recurring.yml b/project/config/recurring.yml new file mode 100644 index 0000000000..b4207f9b07 --- /dev/null +++ b/project/config/recurring.yml @@ -0,0 +1,15 @@ +# examples: +# periodic_cleanup: +# class: CleanSoftDeletedRecordsJob +# queue: background +# args: [ 1000, { batch_size: 500 } ] +# schedule: every hour +# periodic_cleanup_with_command: +# command: "SoftDeletedRecord.due.delete_all" +# priority: 2 +# schedule: at 5am every day + +production: + clear_solid_queue_finished_jobs: + command: "SolidQueue::Job.clear_finished_in_batches(sleep_between_batches: 0.3)" + schedule: every hour at minute 12 diff --git a/project/config/routes.rb b/project/config/routes.rb new file mode 100644 index 0000000000..19c729728c --- /dev/null +++ b/project/config/routes.rb @@ -0,0 +1,34 @@ +Rails.application.routes.draw do + resources :resposta + resources :submissaos + resources :questaos + + resources :formularios do + member do + get :exportar_csv + end + end + + resources :templates + + get "up" => "rails/health#show", as: :rails_health_check + + root "dashboard#show" + + get "login", to: "sessions#new", as: :login + post "login", to: "sessions#create" + delete "logout", to: "sessions#destroy" + + get "senha/definir/:token", to: "password_setups#edit", as: :edit_password_setup + patch "senha/definir/:token", to: "password_setups#update", as: :password_setup + + get "senha/esqueci", to: "password_resets#new", as: :new_password_reset + post "senha/esqueci", to: "password_resets#create", as: :password_resets + get "senha/redefinir/:token", to: "password_resets#edit", as: :edit_password_reset + patch "senha/redefinir/:token", to: "password_resets#update", as: :password_reset + + namespace :imports do + get "sigaa", to: "sigaa#new" + post "sigaa", to: "sigaa#create" + end +end diff --git a/project/config/storage.yml b/project/config/storage.yml new file mode 100644 index 0000000000..927dc537c8 --- /dev/null +++ b/project/config/storage.yml @@ -0,0 +1,27 @@ +test: + service: Disk + root: <%= Rails.root.join("tmp/storage") %> + +local: + service: Disk + root: <%= Rails.root.join("storage") %> + +# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) +# amazon: +# service: S3 +# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> +# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> +# region: us-east-1 +# bucket: your_own_bucket-<%= Rails.env %> + +# Remember not to checkin your GCS keyfile to a repository +# google: +# service: GCS +# project: your_project +# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> +# bucket: your_own_bucket-<%= Rails.env %> + +# mirror: +# service: Mirror +# primary: local +# mirrors: [ amazon, google, microsoft ] diff --git a/project/db/cable_schema.rb b/project/db/cable_schema.rb new file mode 100644 index 0000000000..23666604a5 --- /dev/null +++ b/project/db/cable_schema.rb @@ -0,0 +1,11 @@ +ActiveRecord::Schema[7.1].define(version: 1) do + create_table "solid_cable_messages", force: :cascade do |t| + t.binary "channel", limit: 1024, null: false + t.binary "payload", limit: 536870912, null: false + t.datetime "created_at", null: false + t.integer "channel_hash", limit: 8, null: false + t.index ["channel"], name: "index_solid_cable_messages_on_channel" + t.index ["channel_hash"], name: "index_solid_cable_messages_on_channel_hash" + t.index ["created_at"], name: "index_solid_cable_messages_on_created_at" + end +end diff --git a/project/db/cache_schema.rb b/project/db/cache_schema.rb new file mode 100644 index 0000000000..81a410d188 --- /dev/null +++ b/project/db/cache_schema.rb @@ -0,0 +1,12 @@ +ActiveRecord::Schema[7.2].define(version: 1) do + create_table "solid_cache_entries", force: :cascade do |t| + t.binary "key", limit: 1024, null: false + t.binary "value", limit: 536870912, null: false + t.datetime "created_at", null: false + t.integer "key_hash", limit: 8, null: false + t.integer "byte_size", limit: 4, null: false + t.index ["byte_size"], name: "index_solid_cache_entries_on_byte_size" + t.index ["key_hash", "byte_size"], name: "index_solid_cache_entries_on_key_hash_and_byte_size" + t.index ["key_hash"], name: "index_solid_cache_entries_on_key_hash", unique: true + end +end diff --git a/project/db/migrate/20260611134150_create_departamentos.rb b/project/db/migrate/20260611134150_create_departamentos.rb new file mode 100644 index 0000000000..486d3458cf --- /dev/null +++ b/project/db/migrate/20260611134150_create_departamentos.rb @@ -0,0 +1,10 @@ +class CreateDepartamentos < ActiveRecord::Migration[8.1] + def change + create_table :departamentos do |t| + t.string :name + t.string :code + + t.timestamps + end + end +end diff --git a/project/db/migrate/20260611134210_create_alunos.rb b/project/db/migrate/20260611134210_create_alunos.rb new file mode 100644 index 0000000000..3975496e21 --- /dev/null +++ b/project/db/migrate/20260611134210_create_alunos.rb @@ -0,0 +1,13 @@ +class CreateAlunos < ActiveRecord::Migration[8.1] + def change + create_table :alunos do |t| + t.string :matricula + t.string :name + t.string :email + t.string :password_digest + t.string :course + + t.timestamps + end + end +end diff --git a/project/db/migrate/20260611134235_create_admins.rb b/project/db/migrate/20260611134235_create_admins.rb new file mode 100644 index 0000000000..08f8aa503f --- /dev/null +++ b/project/db/migrate/20260611134235_create_admins.rb @@ -0,0 +1,13 @@ +class CreateAdmins < ActiveRecord::Migration[8.1] + def change + create_table :admins do |t| + t.string :username + t.string :name + t.string :email + t.string :password_digest + t.references :departamento, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/project/db/migrate/20260611134249_create_professors.rb b/project/db/migrate/20260611134249_create_professors.rb new file mode 100644 index 0000000000..1fe1fec171 --- /dev/null +++ b/project/db/migrate/20260611134249_create_professors.rb @@ -0,0 +1,14 @@ +class CreateProfessors < ActiveRecord::Migration[8.1] + def change + create_table :professors do |t| + t.string :matricula + t.string :name + t.string :email + t.string :password_digest + t.string :formation + t.references :departamento, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/project/db/migrate/20260611134304_create_turmas.rb b/project/db/migrate/20260611134304_create_turmas.rb new file mode 100644 index 0000000000..9c584091c2 --- /dev/null +++ b/project/db/migrate/20260611134304_create_turmas.rb @@ -0,0 +1,15 @@ +class CreateTurmas < ActiveRecord::Migration[8.1] + def change + create_table :turmas do |t| + t.string :subject_code + t.string :subject_name + t.string :class_code + t.string :semester + t.string :time + t.references :departamento, null: false, foreign_key: true + t.references :professor, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/project/db/migrate/20260611134355_create_turma_alunos.rb b/project/db/migrate/20260611134355_create_turma_alunos.rb new file mode 100644 index 0000000000..f5a2c39e31 --- /dev/null +++ b/project/db/migrate/20260611134355_create_turma_alunos.rb @@ -0,0 +1,10 @@ +class CreateTurmaAlunos < ActiveRecord::Migration[8.1] + def change + create_table :turma_alunos do |t| + t.references :aluno, null: false, foreign_key: true + t.references :turma, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/project/db/migrate/20260611134403_create_templates.rb b/project/db/migrate/20260611134403_create_templates.rb new file mode 100644 index 0000000000..e56ae85339 --- /dev/null +++ b/project/db/migrate/20260611134403_create_templates.rb @@ -0,0 +1,11 @@ +class CreateTemplates < ActiveRecord::Migration[8.1] + def change + create_table :templates do |t| + t.string :title + t.string :target_role + t.references :admin, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/project/db/migrate/20260611134426_create_formularios.rb b/project/db/migrate/20260611134426_create_formularios.rb new file mode 100644 index 0000000000..8a2684cf87 --- /dev/null +++ b/project/db/migrate/20260611134426_create_formularios.rb @@ -0,0 +1,14 @@ +class CreateFormularios < ActiveRecord::Migration[8.1] + def change + create_table :formularios do |t| + t.string :title + t.string :target_role + t.string :status + t.references :template, null: false, foreign_key: true + t.references :turma, null: false, foreign_key: true + t.references :admin, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/project/db/migrate/20260611134433_create_questaos.rb b/project/db/migrate/20260611134433_create_questaos.rb new file mode 100644 index 0000000000..5bd2781a26 --- /dev/null +++ b/project/db/migrate/20260611134433_create_questaos.rb @@ -0,0 +1,12 @@ +class CreateQuestaos < ActiveRecord::Migration[8.1] + def change + create_table :questaos do |t| + t.text :enunciado + t.string :tipo + t.references :template, null: true, foreign_key: true + t.references :formulario, null: true, foreign_key: true + + t.timestamps + end + end +end diff --git a/project/db/migrate/20260611134446_create_submissaos.rb b/project/db/migrate/20260611134446_create_submissaos.rb new file mode 100644 index 0000000000..d59930feb7 --- /dev/null +++ b/project/db/migrate/20260611134446_create_submissaos.rb @@ -0,0 +1,10 @@ +class CreateSubmissaos < ActiveRecord::Migration[8.1] + def change + create_table :submissaos do |t| + t.references :formulario, null: false, foreign_key: true + t.references :participant, polymorphic: true, null: false + + t.timestamps + end + end +end diff --git a/project/db/migrate/20260611134500_create_resposta.rb b/project/db/migrate/20260611134500_create_resposta.rb new file mode 100644 index 0000000000..c40ef181da --- /dev/null +++ b/project/db/migrate/20260611134500_create_resposta.rb @@ -0,0 +1,12 @@ +class CreateResposta < ActiveRecord::Migration[8.1] + def change + create_table :resposta do |t| + t.text :valor_texto + t.integer :valor_numerico + t.references :submissao, null: false, foreign_key: true + t.references :questao, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/project/db/migrate/20260614234457_allow_null_template_id_on_formularios.rb b/project/db/migrate/20260614234457_allow_null_template_id_on_formularios.rb new file mode 100644 index 0000000000..f3ede1c71e --- /dev/null +++ b/project/db/migrate/20260614234457_allow_null_template_id_on_formularios.rb @@ -0,0 +1,5 @@ +class AllowNullTemplateIdOnFormularios < ActiveRecord::Migration[8.1] + def change + change_column_null :formularios, :template_id, true + end +end diff --git a/project/db/migrate/20260615000100_create_departments.rb b/project/db/migrate/20260615000100_create_departments.rb new file mode 100644 index 0000000000..98ffe30b3a --- /dev/null +++ b/project/db/migrate/20260615000100_create_departments.rb @@ -0,0 +1,12 @@ +class CreateDepartments < ActiveRecord::Migration[8.1] + def change + create_table :departments do |t| + t.string :name, null: false + t.string :code, null: false + + t.timestamps + end + + add_index :departments, :code, unique: true + end +end diff --git a/project/db/migrate/20260615000200_create_course_classes.rb b/project/db/migrate/20260615000200_create_course_classes.rb new file mode 100644 index 0000000000..a5eebe34d7 --- /dev/null +++ b/project/db/migrate/20260615000200_create_course_classes.rb @@ -0,0 +1,15 @@ +class CreateCourseClasses < ActiveRecord::Migration[8.1] + def change + create_table :course_classes do |t| + t.references :department, null: false, foreign_key: true + t.string :code, null: false + t.string :name + t.string :class_code, null: false + t.string :semester, null: false + + t.timestamps + end + + add_index :course_classes, [ :code, :class_code, :semester ], unique: true + end +end diff --git a/project/db/migrate/20260615000300_create_users.rb b/project/db/migrate/20260615000300_create_users.rb new file mode 100644 index 0000000000..2f77e553db --- /dev/null +++ b/project/db/migrate/20260615000300_create_users.rb @@ -0,0 +1,23 @@ +class CreateUsers < ActiveRecord::Migration[8.1] + def change + create_table :users do |t| + t.references :department, foreign_key: true + t.string :name, null: false + t.string :email, null: false + t.string :registration, null: false + t.integer :role, null: false, default: 1 + t.string :password_digest + t.string :password_setup_token_digest + t.datetime :password_setup_sent_at + t.string :password_reset_token_digest + t.datetime :password_reset_sent_at + + t.timestamps + end + + add_index :users, :email, unique: true + add_index :users, :registration, unique: true + add_index :users, :password_setup_token_digest, unique: true + add_index :users, :password_reset_token_digest, unique: true + end +end diff --git a/project/db/migrate/20260615000400_create_class_memberships.rb b/project/db/migrate/20260615000400_create_class_memberships.rb new file mode 100644 index 0000000000..aadc8a7cd2 --- /dev/null +++ b/project/db/migrate/20260615000400_create_class_memberships.rb @@ -0,0 +1,13 @@ +class CreateClassMemberships < ActiveRecord::Migration[8.1] + def change + create_table :class_memberships do |t| + t.references :user, null: false, foreign_key: true + t.references :course_class, null: false, foreign_key: true + t.integer :role, null: false, default: 0 + + t.timestamps + end + + add_index :class_memberships, [ :user_id, :course_class_id, :role ], unique: true + end +end diff --git a/project/db/migrate/20260615000500_create_import_inconsistencies.rb b/project/db/migrate/20260615000500_create_import_inconsistencies.rb new file mode 100644 index 0000000000..821c18b1ad --- /dev/null +++ b/project/db/migrate/20260615000500_create_import_inconsistencies.rb @@ -0,0 +1,11 @@ +class CreateImportInconsistencies < ActiveRecord::Migration[8.1] + def change + create_table :import_inconsistencies do |t| + t.string :source, null: false + t.string :message, null: false + t.json :payload + + t.timestamps + end + end +end diff --git a/project/db/migrate/20260616000001_change_admin_id_fk_to_users.rb b/project/db/migrate/20260616000001_change_admin_id_fk_to_users.rb new file mode 100644 index 0000000000..464c411bda --- /dev/null +++ b/project/db/migrate/20260616000001_change_admin_id_fk_to_users.rb @@ -0,0 +1,9 @@ +class ChangeAdminIdFkToUsers < ActiveRecord::Migration[8.1] + def change + remove_foreign_key :templates, :admins + remove_foreign_key :formularios, :admins + + add_foreign_key :templates, :users, column: :admin_id + add_foreign_key :formularios, :users, column: :admin_id + end +end diff --git a/project/db/migrate/20260616000002_consolidate_to_user_model.rb b/project/db/migrate/20260616000002_consolidate_to_user_model.rb new file mode 100644 index 0000000000..5fb6c44b5d --- /dev/null +++ b/project/db/migrate/20260616000002_consolidate_to_user_model.rb @@ -0,0 +1,28 @@ +class ConsolidateToUserModel < ActiveRecord::Migration[8.1] + def up + # --- course_classes: horário para exibição --- + add_column :course_classes, :time, :string + + # --- formularios: turma_id → course_class_id --- + remove_foreign_key :formularios, column: :turma_id + rename_column :formularios, :turma_id, :course_class_id + add_foreign_key :formularios, :course_classes, column: :course_class_id + + # --- submissaos: substituir polimórfico por user_id --- + remove_column :submissaos, :participant_id + remove_column :submissaos, :participant_type + add_reference :submissaos, :user, null: false, foreign_key: true + + # --- remover tabelas legadas (ordem respeita FKs) --- + drop_table :turma_alunos + drop_table :turmas + drop_table :professors + drop_table :admins + drop_table :alunos + drop_table :departamentos + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/project/db/queue_schema.rb b/project/db/queue_schema.rb new file mode 100644 index 0000000000..85194b6a88 --- /dev/null +++ b/project/db/queue_schema.rb @@ -0,0 +1,129 @@ +ActiveRecord::Schema[7.1].define(version: 1) do + create_table "solid_queue_blocked_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.string "queue_name", null: false + t.integer "priority", default: 0, null: false + t.string "concurrency_key", null: false + t.datetime "expires_at", null: false + t.datetime "created_at", null: false + t.index [ "concurrency_key", "priority", "job_id" ], name: "index_solid_queue_blocked_executions_for_release" + t.index [ "expires_at", "concurrency_key" ], name: "index_solid_queue_blocked_executions_for_maintenance" + t.index [ "job_id" ], name: "index_solid_queue_blocked_executions_on_job_id", unique: true + end + + create_table "solid_queue_claimed_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.bigint "process_id" + t.datetime "created_at", null: false + t.index [ "job_id" ], name: "index_solid_queue_claimed_executions_on_job_id", unique: true + t.index [ "process_id", "job_id" ], name: "index_solid_queue_claimed_executions_on_process_id_and_job_id" + end + + create_table "solid_queue_failed_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.text "error" + t.datetime "created_at", null: false + t.index [ "job_id" ], name: "index_solid_queue_failed_executions_on_job_id", unique: true + end + + create_table "solid_queue_jobs", force: :cascade do |t| + t.string "queue_name", null: false + t.string "class_name", null: false + t.text "arguments" + t.integer "priority", default: 0, null: false + t.string "active_job_id" + t.datetime "scheduled_at" + t.datetime "finished_at" + t.string "concurrency_key" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index [ "active_job_id" ], name: "index_solid_queue_jobs_on_active_job_id" + t.index [ "class_name" ], name: "index_solid_queue_jobs_on_class_name" + t.index [ "finished_at" ], name: "index_solid_queue_jobs_on_finished_at" + t.index [ "queue_name", "finished_at" ], name: "index_solid_queue_jobs_for_filtering" + t.index [ "scheduled_at", "finished_at" ], name: "index_solid_queue_jobs_for_alerting" + end + + create_table "solid_queue_pauses", force: :cascade do |t| + t.string "queue_name", null: false + t.datetime "created_at", null: false + t.index [ "queue_name" ], name: "index_solid_queue_pauses_on_queue_name", unique: true + end + + create_table "solid_queue_processes", force: :cascade do |t| + t.string "kind", null: false + t.datetime "last_heartbeat_at", null: false + t.bigint "supervisor_id" + t.integer "pid", null: false + t.string "hostname" + t.text "metadata" + t.datetime "created_at", null: false + t.string "name", null: false + t.index [ "last_heartbeat_at" ], name: "index_solid_queue_processes_on_last_heartbeat_at" + t.index [ "name", "supervisor_id" ], name: "index_solid_queue_processes_on_name_and_supervisor_id", unique: true + t.index [ "supervisor_id" ], name: "index_solid_queue_processes_on_supervisor_id" + end + + create_table "solid_queue_ready_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.string "queue_name", null: false + t.integer "priority", default: 0, null: false + t.datetime "created_at", null: false + t.index [ "job_id" ], name: "index_solid_queue_ready_executions_on_job_id", unique: true + t.index [ "priority", "job_id" ], name: "index_solid_queue_poll_all" + t.index [ "queue_name", "priority", "job_id" ], name: "index_solid_queue_poll_by_queue" + end + + create_table "solid_queue_recurring_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.string "task_key", null: false + t.datetime "run_at", null: false + t.datetime "created_at", null: false + t.index [ "job_id" ], name: "index_solid_queue_recurring_executions_on_job_id", unique: true + t.index [ "task_key", "run_at" ], name: "index_solid_queue_recurring_executions_on_task_key_and_run_at", unique: true + end + + create_table "solid_queue_recurring_tasks", force: :cascade do |t| + t.string "key", null: false + t.string "schedule", null: false + t.string "command", limit: 2048 + t.string "class_name" + t.text "arguments" + t.string "queue_name" + t.integer "priority", default: 0 + t.boolean "static", default: true, null: false + t.text "description" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index [ "key" ], name: "index_solid_queue_recurring_tasks_on_key", unique: true + t.index [ "static" ], name: "index_solid_queue_recurring_tasks_on_static" + end + + create_table "solid_queue_scheduled_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.string "queue_name", null: false + t.integer "priority", default: 0, null: false + t.datetime "scheduled_at", null: false + t.datetime "created_at", null: false + t.index [ "job_id" ], name: "index_solid_queue_scheduled_executions_on_job_id", unique: true + t.index [ "scheduled_at", "priority", "job_id" ], name: "index_solid_queue_dispatch_all" + end + + create_table "solid_queue_semaphores", force: :cascade do |t| + t.string "key", null: false + t.integer "value", default: 1, null: false + t.datetime "expires_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index [ "expires_at" ], name: "index_solid_queue_semaphores_on_expires_at" + t.index [ "key", "value" ], name: "index_solid_queue_semaphores_on_key_and_value" + t.index [ "key" ], name: "index_solid_queue_semaphores_on_key", unique: true + end + + add_foreign_key "solid_queue_blocked_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_claimed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_failed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_ready_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_recurring_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_scheduled_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade +end diff --git a/project/db/schema.rb b/project/db/schema.rb new file mode 100644 index 0000000000..d4f420563d --- /dev/null +++ b/project/db/schema.rb @@ -0,0 +1,142 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema[8.1].define(version: 2026_06_16_000002) do + create_table "class_memberships", force: :cascade do |t| + t.integer "course_class_id", null: false + t.datetime "created_at", null: false + t.integer "role", default: 0, null: false + t.datetime "updated_at", null: false + t.integer "user_id", null: false + t.index ["course_class_id"], name: "index_class_memberships_on_course_class_id" + t.index ["user_id", "course_class_id", "role"], name: "idx_on_user_id_course_class_id_role_c0460d53c1", unique: true + t.index ["user_id"], name: "index_class_memberships_on_user_id" + end + + create_table "course_classes", force: :cascade do |t| + t.string "class_code", null: false + t.string "code", null: false + t.datetime "created_at", null: false + t.integer "department_id", null: false + t.string "name" + t.string "semester", null: false + t.string "time" + t.datetime "updated_at", null: false + t.index ["code", "class_code", "semester"], name: "index_course_classes_on_code_and_class_code_and_semester", unique: true + t.index ["department_id"], name: "index_course_classes_on_department_id" + end + + create_table "departments", force: :cascade do |t| + t.string "code", null: false + t.datetime "created_at", null: false + t.string "name", null: false + t.datetime "updated_at", null: false + t.index ["code"], name: "index_departments_on_code", unique: true + end + + create_table "formularios", force: :cascade do |t| + t.integer "admin_id", null: false + t.integer "course_class_id", null: false + t.datetime "created_at", null: false + t.string "status" + t.string "target_role" + t.integer "template_id" + t.string "title" + t.datetime "updated_at", null: false + t.index ["admin_id"], name: "index_formularios_on_admin_id" + t.index ["course_class_id"], name: "index_formularios_on_course_class_id" + t.index ["template_id"], name: "index_formularios_on_template_id" + end + + create_table "import_inconsistencies", force: :cascade do |t| + t.datetime "created_at", null: false + t.string "message", null: false + t.json "payload" + t.string "source", null: false + t.datetime "updated_at", null: false + end + + create_table "questaos", force: :cascade do |t| + t.datetime "created_at", null: false + t.text "enunciado" + t.integer "formulario_id" + t.integer "template_id" + t.string "tipo" + t.datetime "updated_at", null: false + t.index ["formulario_id"], name: "index_questaos_on_formulario_id" + t.index ["template_id"], name: "index_questaos_on_template_id" + end + + create_table "resposta", force: :cascade do |t| + t.datetime "created_at", null: false + t.integer "questao_id", null: false + t.integer "submissao_id", null: false + t.datetime "updated_at", null: false + t.integer "valor_numerico" + t.text "valor_texto" + t.index ["questao_id"], name: "index_resposta_on_questao_id" + t.index ["submissao_id"], name: "index_resposta_on_submissao_id" + end + + create_table "submissaos", force: :cascade do |t| + t.datetime "created_at", null: false + t.integer "formulario_id", null: false + t.datetime "updated_at", null: false + t.integer "user_id", null: false + t.index ["formulario_id"], name: "index_submissaos_on_formulario_id" + t.index ["user_id"], name: "index_submissaos_on_user_id" + end + + create_table "templates", force: :cascade do |t| + t.integer "admin_id", null: false + t.datetime "created_at", null: false + t.string "target_role" + t.string "title" + t.datetime "updated_at", null: false + t.index ["admin_id"], name: "index_templates_on_admin_id" + end + + create_table "users", force: :cascade do |t| + t.datetime "created_at", null: false + t.integer "department_id" + t.string "email", null: false + t.string "name", null: false + t.string "password_digest" + t.datetime "password_reset_sent_at" + t.string "password_reset_token_digest" + t.datetime "password_setup_sent_at" + t.string "password_setup_token_digest" + t.string "registration", null: false + t.integer "role", default: 1, null: false + t.datetime "updated_at", null: false + t.index ["department_id"], name: "index_users_on_department_id" + t.index ["email"], name: "index_users_on_email", unique: true + t.index ["password_reset_token_digest"], name: "index_users_on_password_reset_token_digest", unique: true + t.index ["password_setup_token_digest"], name: "index_users_on_password_setup_token_digest", unique: true + t.index ["registration"], name: "index_users_on_registration", unique: true + end + + add_foreign_key "class_memberships", "course_classes" + add_foreign_key "class_memberships", "users" + add_foreign_key "course_classes", "departments" + add_foreign_key "formularios", "course_classes" + add_foreign_key "formularios", "templates" + add_foreign_key "formularios", "users", column: "admin_id" + add_foreign_key "questaos", "formularios" + add_foreign_key "questaos", "templates" + add_foreign_key "resposta", "questaos" + add_foreign_key "resposta", "submissaos" + add_foreign_key "submissaos", "formularios" + add_foreign_key "submissaos", "users" + add_foreign_key "templates", "users", column: "admin_id" + add_foreign_key "users", "departments" +end diff --git a/project/db/seeds.rb b/project/db/seeds.rb new file mode 100644 index 0000000000..fcda9a8f2c --- /dev/null +++ b/project/db/seeds.rb @@ -0,0 +1,104 @@ +# Seed de desenvolvimento — pode ser executado múltiplas vezes (idempotente). + +# --- Departamento (sistema novo) --- +department_cic = Department.find_or_create_by!(code: "CIC") { |d| d.name = "Ciência da Computação" } +department_ene = Department.find_or_create_by!(code: "ENE") { |d| d.name = "Engenharia Elétrica" } + +# --- Usuário administrador --- +admin = User.find_or_create_by!(email: "admin.cic@unb.br") do |u| + u.name = "Admin CAMAAR" + u.registration = "000000001" + u.role = :admin + u.password = "Senha@123" + u.department = department_cic +end + +# --- Usuários participantes --- +participantes = [ + { name: "Carla Mendes", email: "carla.mendes@aluno.unb.br", registration: "190000001", role: :participant }, + { name: "Diego Alves", email: "diego.alves@aluno.unb.br", registration: "190000002", role: :participant }, + { name: "Ana Souza", email: "ana.souza@unb.br", registration: "PROF0001", role: :participant }, + { name: "Bruno Lima", email: "bruno.lima@unb.br", registration: "PROF0002", role: :participant } +].map do |dados| + User.find_or_create_by!(email: dados[:email]) do |u| + u.name = dados[:name] + u.registration = dados[:registration] + u.role = dados[:role] + u.password = "Senha@123" + u.department = department_cic + end +end + +carla, diego, ana_prof, bruno_prof = participantes + +# --- Turmas (CourseClass) --- +turma_ed = CourseClass.find_or_create_by!(code: "CIC0097", class_code: "T01", semester: "2026.1") do |t| + t.name = "Estrutura de Dados" + t.time = "Seg/Qua 08:00-10:00" + t.department = department_cic +end + +turma_circ = CourseClass.find_or_create_by!(code: "ENE0011", class_code: "T01", semester: "2026.1") do |t| + t.name = "Circuitos Elétricos" + t.time = "Ter/Qui 10:00-12:00" + t.department = department_ene +end + +# --- Vínculos de membros (ClassMembership) --- +ClassMembership.find_or_create_by!(user: ana_prof, course_class: turma_ed, role: :docente) +ClassMembership.find_or_create_by!(user: bruno_prof, course_class: turma_circ, role: :docente) +ClassMembership.find_or_create_by!(user: carla, course_class: turma_ed, role: :discente) +ClassMembership.find_or_create_by!(user: diego, course_class: turma_ed, role: :discente) +ClassMembership.find_or_create_by!(user: carla, course_class: turma_circ, role: :discente) + +# --- Templates --- +template_discente = Template.find_or_create_by!(title: "Avaliação de Disciplina - Discentes") do |t| + t.target_role = "discente" + t.admin_id = admin.id +end + +if template_discente.questaos.empty? + template_discente.questaos.create!(enunciado: "O professor demonstrou domínio do conteúdo?", tipo: "rating") + template_discente.questaos.create!(enunciado: "O material disponibilizado foi adequado?", tipo: "rating") + template_discente.questaos.create!(enunciado: "Comentários e sugestões para a disciplina:", tipo: "text") +end + +template_docente = Template.find_or_create_by!(title: "Autoavaliação - Docentes") do |t| + t.target_role = "docente" + t.admin_id = admin.id +end + +if template_docente.questaos.empty? + template_docente.questaos.create!(enunciado: "Você conseguiu cumprir o plano de ensino proposto?", tipo: "boolean") + template_docente.questaos.create!(enunciado: "Quais dificuldades você encontrou no semestre?", tipo: "text") +end + +# --- Formulário de exemplo --- +formulario = Formulario.find_or_create_by!(title: "Avaliação Estrutura de Dados - 2026.1") do |f| + f.target_role = template_discente.target_role + f.admin_id = admin.id + f.template_id = template_discente.id + f.course_class_id = turma_ed.id +end + +if formulario.questaos.empty? + template_discente.questaos.each { |q| formulario.questaos.create!(enunciado: q.enunciado, tipo: q.tipo) } +end + +# --- Submissão de exemplo --- +submissao = Submissao.find_or_create_by!(formulario: formulario, user: carla) + +if Respostum.where(submissao_id: submissao.id).empty? + formulario.questaos.each do |q| + Respostum.create!(submissao_id: submissao.id, questao_id: q.id, + valor_numerico: (q.tipo == "rating" ? 5 : nil), + valor_texto: (q.tipo != "rating" ? "Resposta de exemplo" : nil)) + end +end + +puts "Seed concluída:" +puts " Admin: #{admin.email} / Senha@123" +puts " Participantes: #{User.participant.count} (#{User.participant.pluck(:email).join(', ')})" +puts " Turmas: #{CourseClass.count}" +puts " Templates: #{Template.count} | Formulários: #{Formulario.count}" +puts " Submissões: #{Submissao.count} | Respostas: #{Respostum.count}" diff --git a/project/features/admin_baixar_resultados_csv.feature b/project/features/admin_baixar_resultados_csv.feature new file mode 100644 index 0000000000..5755748657 --- /dev/null +++ b/project/features/admin_baixar_resultados_csv.feature @@ -0,0 +1,22 @@ +# language: pt +Funcionalidade: Exportação de resultados de formulário em CSV + + Como um Administrador + Quero baixar um arquivo csv contendo os resultados de um formulário + A fim de avaliar o desempenho das turmas + + Cenário: Download de resultados em CSV com sucesso + Dado que estou logado no sistema como "Administrador" + E que estou na página de visualização de resultados da turma "Estrutura de Dados" + Quando o formulário selecionado já possui respostas submetidas + E eu clico no botão "Exportar Resultados (CSV)" + Então o sistema deve gerar um arquivo ".csv" com todas as respostas estruturadas + E o download do arquivo deve iniciar automaticamente no meu navegador + + Cenário: Tentativa de download de CSV de um formulário sem respostas + Dado que estou logado no sistema como "Administrador" + E que estou na página de visualização de resultados de um formulário recém-criado + Quando o formulário selecionado ainda não possui nenhuma resposta de participante + E eu clico no botão "Exportar Resultados (CSV)" + Então o sistema não deve gerar o arquivo + E deve exibir um alerta impeditivo "Não é possível exportar o relatório pois nenhuma resposta foi registrada. Aguarde os participantes responderem." \ No newline at end of file diff --git a/project/features/admin_criar_formulario.feature b/project/features/admin_criar_formulario.feature new file mode 100644 index 0000000000..d8a653e471 --- /dev/null +++ b/project/features/admin_criar_formulario.feature @@ -0,0 +1,28 @@ +# language: pt +Funcionalidade: Admin criar formulário a partir de um template para as turmas que escolher + + Eu como Administrador + Quero criar um formulário baseado em um template para as turmas que eu escolher + A fim de avaliar o desempenho das turmas no semestre atual + + Cenário: Administrador cria formulário com sucesso + Dado que sou administrador + E existe pelo menos um template criado por mim + E existe pelo menos uma turma cadastrada + Quando eu preencher o nome do formulário + E selecionar o template e a turma desejada + E clicar no botão 'Enviar' + Então devo ver uma mensagem dizendo 'Formulário gerado com sucesso!' + E as questões do template devem ser clonadas para o novo formulário + + Cenário: Administrador tenta criar formulário sem selecionar um template + Dado que sou um administrador + E estou criando um formulário + Quando eu tentar criar um formulário sem selecionar nenhum template + Então o sistema deve exibir uma mensagem dizendo 'É necessário escolher um template base' + + Cenário: Administrador tenta criar formulário sem selecionar nenhuma turma + Dado que sou um administrador + E estou criando um formulário + Quando eu tentar criar um formulário sem selecionar nenhuma turma + Então o sistema deve exibir uma mensagem dizendo 'É necessário escolher ao menos uma turma' \ No newline at end of file diff --git a/project/features/admin_criar_formulario_avaliacao.feature b/project/features/admin_criar_formulario_avaliacao.feature new file mode 100644 index 0000000000..cd6b1ca50e --- /dev/null +++ b/project/features/admin_criar_formulario_avaliacao.feature @@ -0,0 +1,35 @@ +# language: pt +Funcionalidade: Criação de Formulário de Avaliação de Turma + + Como um Administrador + Quero escolher criar um formulário para os docentes ou os discentes de uma turma + A fim de avaliar o desempenho de uma matéria + + Cenário: Criação de formulário com sucesso para discentes + Dado que eu acesse a tela de "Criação de Formulários" + Quando eu selecionar a matéria "Cálculo 1" e a turma "1" + E definir o público-alvo como "Discentes" + E preencher as perguntas do formulário de avaliação + E clicar em "Publicar Formulário" + Então o sistema deve salvar o formulário com sucesso + E exibir a mensagem "Formulário de avaliação para discentes publicado com sucesso." + E o formulário deve ficar disponível para os alunos da turma "1" responderem + + Cenário: Tentativa de publicação de formulário sem selecionar o público-alvo + Dado que eu acesse a tela de "Criação de Formulários" + Quando eu selecionar a matéria "Cálculo 1" e a turma "1" + E deixar o campo de público-alvo em branco + E preencher as perguntas do formulário de avaliação + E clicar em "Publicar Formulário" + Então o sistema não deve permitir a publicação + E deve exibir um alerta impeditivo "Por favor, selecione se o formulário é para Docentes ou Discentes." + + Cenário: Tentativa de criação de formulário sem preencher as perguntas + Dado que eu acesse a tela de "Criação de Formulários" + Quando eu selecionar a matéria Cálculo 2" e a turma "3" + E definir o público-alvo como "Docentes" + E não adicionar nenhuma pergunta ao formulário + E clicar em "Publicar Formulário" + Então o sistema deve bloquear a ação + E exibir a mensagem de erro "O formulário não pode ser publicado sem perguntas cadastradas." + diff --git a/project/features/admin_criar_template.feature b/project/features/admin_criar_template.feature new file mode 100644 index 0000000000..48e8971eb4 --- /dev/null +++ b/project/features/admin_criar_template.feature @@ -0,0 +1,30 @@ +# language: pt +Funcionalidade: Admin cria template para formulários + + Eu como Administrador + Quero criar um template de formulário contendo as questões do formulário + A fim de gerar formulários de avaliações para avaliar o desempenho das turmas + + Cenário: Admin cria template com uma questão com sucesso + Dado que estou na página de criar template + Quando eu preencho o campo 'Nome do template:' + E clico no botão '+' + E seleciono o 'Público-alvo:' + E preencho o campo 'Enunciado da questão:' + E clico no botão 'Criar' + Então o novo template deve aparecer na tela de meus templates + + Cenário: Admin tenta criar template sem preencher o nome + Dado que estou na página de criar template + Quando eu deixo o campo 'Nome do template:' vazio + E adiciono uma questão válida + E clico no botão 'Criar' + Então deve aparecer uma mensagem 'O nome do template é obrigatório' + + Cenário: Admin tenta adicionar questão sem preencher o enunciado + Dado que estou na página de criar template + Quando eu preencho o campo 'Nome do template:' + E clico no botão '+' + E não preencho o campo 'Enunciado da questão:' + E clico no botão 'Criar' + Então deve aparecer uma mensagem 'Questão não possui enunciado' \ No newline at end of file diff --git a/project/features/admin_editar_deletar_templates.feature b/project/features/admin_editar_deletar_templates.feature new file mode 100644 index 0000000000..f7d0dd384d --- /dev/null +++ b/project/features/admin_editar_deletar_templates.feature @@ -0,0 +1,26 @@ +# language: pt +Funcionalidade: Admin editar e deletar templates criados + + Como Administrador + Quero editar e/ou deletar um template que eu criei sem afetar os formulários já criados + A fim de organizar os templates existentes + + Cenário: Edição de um template criado mantendo isolamento + Dado que eu editei informações de um template existente + Quando eu pressionar o botão "Confirmar edição" na tela de edição + Então eu devo visualizar as alterações no template na tela de templates + E os formulários criados usando a versão antiga não devem sofrer alterações nas suas questões + + Cenário: Deleção de um template criado + Dado que eu escolhi um template na minha lista + Quando eu pressionar o botão de "Deletar" + E confirmar a deleção pressionando o botão "Confirmar" + Então esse template não deve aparecer mais na tela de templates + E os formulários gerados por ele devem permanecer intactos no sistema + + Cenário: Tentar adicionar questão com campo vazio durante edição + Dado que estou na página de edição do template + Quando eu clico no botão '+' + E não preencho o campo 'Enunciado da questão:' + E clico no botão 'Confirmar edição' + Então deve aparecer uma mensagem 'A nova questão não possui enunciado' \ No newline at end of file diff --git a/project/features/admin_gerenciar_turmas.feature b/project/features/admin_gerenciar_turmas.feature new file mode 100644 index 0000000000..1ae1336648 --- /dev/null +++ b/project/features/admin_gerenciar_turmas.feature @@ -0,0 +1,26 @@ +# language: pt +Funcionalidade: Gerenciamento de Turmas por Departamento + + Como um Administrador + Quero gerenciar somente as turmas do departamento o qual eu pertenço + A fim de avaliar o desempenho das turmas no semestre atual + + Cenário: Listagem de turmas filtrada com sucesso pelo departamento do Administrador + Dado que eu acesse a tela de "Gerenciamento de Turmas" + Quando o sistema carregar a página para o semestre atual + Então eu devo visualizar apenas as turmas pertencentes ao "Departamento de Ciências da Computação" + E não devo visualizar turmas de outros departamentos (como "Matemática") + E o sistema deve liberar as opções de avaliação de desempenho para as turmas listadas + + Cenário: Tentativa de acesso direto a uma turma de outro departamento via URL/ID + Dado que eu tente acessar diretamente o painel da turma "Cálculo 1", que pertence ao "Departamento de Matemática" + Quando o sistema validar minhas permissões de Administrador + Então o sistema deve bloquear o meu acesso + E deve redirecionar-me para a página inicial de gerenciamento + E exibir a mensagem de erro: "Acesso negado. Você só pode gerenciar turmas do seu próprio departamento." + + Cenário: Administrador acessa o gerenciamento mas seu departamento não possui turmas no semestre atual + Dado que o "Departamento de Ciências da Computação" não tenha nenhuma turma ofertada ou ativa no semestre atual + Quando eu acessar a tela de "Gerenciamento de Turmas" + Então o sistema deve carregar a página sem registros de turmas + E deve exibir a mensagem informativa: "Nenhuma turma encontrada para o seu departamento no semestre atual." \ No newline at end of file diff --git a/project/features/admin_importar_dados_sigaa.feature b/project/features/admin_importar_dados_sigaa.feature new file mode 100644 index 0000000000..0c8cbb47ed --- /dev/null +++ b/project/features/admin_importar_dados_sigaa.feature @@ -0,0 +1,23 @@ +# language: pt +Funcionalidade: Importação de Dados do SIGAA + + Como um Administrador + Quero importar dados de turmas, matérias e participantes do SIGAA + A fim de alimentar a base de dados do sistema + + Cenário: Sincronização de dados realizada com sucesso (inserção e atualização) + Dado que eu acesse a tela de "Importação de Dados do SIGAA" + Quando eu selecionar o período letivo desejado + E solicitar a sincronização de "Turmas, Matérias e Participantes" + Então o sistema deve consultar o SIGAA (arquivos no repositório original) + E deve inserir os novos registros identificados + E deve atualizar os dados dos registros que sofreram alterações no SIGAA + E exibir a mensagem: "Sincronização concluída. X novos registros adicionados e Y registros atualizados." + + Cenário: Tentativa de importação quando todos os dados já estão atualizados + Dado que todos os dados do SIGAA para o período atual já tenham sido importados previamente + E eu acesse a tela de "Importação de Dados do SIGAA" + Quando eu solicitar a sincronização dos dados + Então o sistema deve verificar a base de dados + E constatar que não há novos registros para inserir + E deve finalizar o processo exibindo a mensagem: "A base de dados já está atualizada com o SIGAA para este período." \ No newline at end of file diff --git a/project/features/admin_ver_formularios_criados.feature b/project/features/admin_ver_formularios_criados.feature new file mode 100644 index 0000000000..1750b56703 --- /dev/null +++ b/project/features/admin_ver_formularios_criados.feature @@ -0,0 +1,20 @@ +# language: pt +Funcionalidade: Visualização dos formulários criados + + Como um Administrador + Quero visualizar os formulários criados + A fim de poder gerar um relatório a partir das respostas + + Cenário: Visualização de lista de formulários com sucesso + Dado que estou logado no sistema como "Administrador" + Quando eu clico na aba de "Meus Formulários" no menu lateral + E existem formulários previamente criados por mim no sistema + Então eu devo ver uma lista com todos os meus formulários publicados + E a lista deve exibir o título, a turma e o status de cada formulário + + Cenário: Visualização de lista quando nenhum formulário foi criado + Dado que estou logado no sistema como "Administrador" + Quando eu clico na aba de "Meus Formulários" no menu lateral + E eu ainda não criei nenhum formulário no sistema + Então a lista de formulários deve aparecer vazia + E o sistema deve exibir a mensagem "Você ainda não possui formulários criados para as suas turmas." \ No newline at end of file diff --git a/project/features/admin_ver_templates.feature b/project/features/admin_ver_templates.feature new file mode 100644 index 0000000000..4dc52fd3dd --- /dev/null +++ b/project/features/admin_ver_templates.feature @@ -0,0 +1,17 @@ +# language: pt +Funcionalidade: Administrador gerenciar templates criados + + Como Administrador + Quero visualizar os templates criados + A fim de poder editar e/ou deletar um template que eu criei + + Cenário: Visualização dos templates criados + Dado que o administrador está na interface de templates + Quando o sistema carrega os templates + Então ele deverá ver somente os templates criados por ele + E deverá ter a opção de deletar ou editar esses templates + + Cenário: Tentativa de visualizar templates criados por outro administrador + Dado que o administrador está na interface de templates + Quando ele tenta acessar diretamente um template criado por outro administrador + Então ele deverá ver uma mensagem de erro de permissão "Você não tem acesso a esse template" \ No newline at end of file diff --git a/project/features/cadastro_usuarios_importados.feature b/project/features/cadastro_usuarios_importados.feature new file mode 100644 index 0000000000..766967aba9 --- /dev/null +++ b/project/features/cadastro_usuarios_importados.feature @@ -0,0 +1,31 @@ +# language: pt +@auth @issue-100 +Funcionalidade: Cadastrar Usuarios do Sistema + Como administrador + Quero cadastrar usuarios a partir dos dados importados do SIGAA + Para que novos participantes recebam o link de definicao de senha e possam acessar o CAMAAR + + Contexto: + Dado que existe um administrador autenticado do departamento "CIC" + E que o arquivo "class_members.json" contem participantes da turma "CIC0097" no semestre "2021.2" + + Cenario: Administrador importa participantes novos e dispara definicao de senha + Quando o administrador importa os participantes do SIGAA + Entao o sistema deve cadastrar o usuario "Ana Clara Jordao Perna" com email "acjpjvjp@gmail.com" e matricula "190084006" + E deve associar o usuario a turma "CIC0097" como participante discente + E deve marcar o usuario como pendente de definicao de senha + E deve enviar email com link de definicao de senha para "acjpjvjp@gmail.com" + + Cenario: Importacao nao duplica usuario ja cadastrado + Dado que ja existe um usuario cadastrado com email "acjpjvjp@gmail.com" e matricula "190084006" + Quando o administrador importa novamente os participantes do SIGAA + Entao o sistema nao deve criar outro usuario com a matricula "190084006" + E deve manter uma unica conta para o email "acjpjvjp@gmail.com" + E deve atualizar os vinculos de turma quando necessario + + Cenario: Registro importado sem dados obrigatorios nao cria conta + Dado que o arquivo de importacao contem um participante sem email ou matricula + Quando o administrador importa os participantes do SIGAA + Entao o sistema nao deve criar conta para o participante invalido + E deve registrar a inconsistencia encontrada na importacao + E deve continuar processando os demais participantes validos diff --git a/project/features/definicao_de_senha.feature b/project/features/definicao_de_senha.feature new file mode 100644 index 0000000000..6a853fc0c0 --- /dev/null +++ b/project/features/definicao_de_senha.feature @@ -0,0 +1,32 @@ +# language: pt +@auth @issue-105 +Funcionalidade: Sistema de Definicao de Senha + Como usuario recem-cadastrado + Quero definir minha primeira senha por um link recebido por email + Para ativar meu acesso ao CAMAAR + + Contexto: + Dado que existe um participante importado do SIGAA com email "acjpjvjp@gmail.com", matricula "190084006" e sem senha definida + E que o sistema enviou um link valido de definicao de senha para "acjpjvjp@gmail.com" + + Cenario: Usuario define a primeira senha com link valido + Quando o usuario acessa o link valido de definicao de senha + E informa a nova senha "Senha@123" + E confirma a nova senha "Senha@123" + E envia o formulario de definicao de senha + Entao o sistema deve salvar a senha do usuario + E deve ativar o acesso do usuario ao CAMAAR + E deve permitir login com email ou matricula e a nova senha + + Cenario: Usuario nao define senha com link invalido + Quando o usuario acessa um link invalido de definicao de senha + Entao o sistema nao deve permitir a definicao de senha + E deve exibir a mensagem "Link de definicao de senha invalido ou expirado" + + Cenario: Usuario nao define senha quando a confirmacao e diferente + Quando o usuario acessa o link valido de definicao de senha + E informa a nova senha "Senha@123" + E confirma a nova senha "OutraSenha@123" + E envia o formulario de definicao de senha + Entao o sistema nao deve salvar a senha do usuario + E deve exibir a mensagem "Confirmacao de senha nao confere" diff --git a/project/features/login.feature b/project/features/login.feature new file mode 100644 index 0000000000..cde880b5a1 --- /dev/null +++ b/project/features/login.feature @@ -0,0 +1,44 @@ +# language: pt +@auth @issue-104 +Funcionalidade: Sistema de Login + Como usuario cadastrado no CAMAAR + Quero acessar o sistema com email ou matricula e senha + Para visualizar as funcionalidades disponiveis ao meu perfil + + Contexto: + Dado que existe um administrador cadastrado com email "admin.cic@unb.br", matricula "000000001" e senha definida "Senha@123" + E que existe um participante cadastrado com email "acjpjvjp@gmail.com", matricula "190084006" e senha definida "Senha@123" + E que existe um participante cadastrado com email "andreCarvalhoroure@gmail.com", matricula "200033522" e sem senha definida + + Cenario: Administrador acessa com email e ve o menu administrativo + Quando o usuario acessa a pagina de login + E informa o identificador "admin.cic@unb.br" + E informa a senha "Senha@123" + E envia o formulario de login + Entao o sistema deve autenticar o administrador + E deve exibir o menu lateral com opcoes de gerenciamento + + Cenario: Participante acessa com matricula e ve o menu de participante + Quando o usuario acessa a pagina de login + E informa o identificador "190084006" + E informa a senha "Senha@123" + E envia o formulario de login + Entao o sistema deve autenticar o participante + E deve exibir o menu lateral com formularios pendentes + E nao deve exibir opcoes administrativas + + Cenario: Usuario nao acessa com senha incorreta + Quando o usuario acessa a pagina de login + E informa o identificador "acjpjvjp@gmail.com" + E informa a senha "SenhaErrada" + E envia o formulario de login + Entao o sistema nao deve autenticar o usuario + E deve exibir a mensagem "Email, matricula ou senha invalidos" + + Cenario: Usuario ainda sem senha definida nao consegue acessar + Quando o usuario acessa a pagina de login + E informa o identificador "200033522" + E informa a senha "Senha@123" + E envia o formulario de login + Entao o sistema nao deve autenticar o usuario + E deve informar que a senha inicial precisa ser definida antes do acesso diff --git a/project/features/participante_responder_questionario.feature b/project/features/participante_responder_questionario.feature new file mode 100644 index 0000000000..b60f426cba --- /dev/null +++ b/project/features/participante_responder_questionario.feature @@ -0,0 +1,22 @@ +# language: pt + +Funcionalidade: Responder questionário sobre a turma + Como um Participante de uma turma + Quero responder o questionário sobre a turma em que estou matriculado + A fim de submeter minha avaliação da turma + + Cenário: Envio de questionário com sucesso (Caminho Feliz) + Dado que estou autenticado no sistema como participante + E que estou na página do formulário não respondido da minha turma + Quando eu preencho todas as avaliações corretamente + E clico no botão de submeter avaliação + Então o sistema deve salvar os "dados_resposta" vinculados ao meu usuário + E eu devo ser redirecionado com uma mensagem "Avaliação submetida com sucesso" + + Cenário: Tentativa de envio com dados em branco (Caminho Triste) + Dado que estou autenticado no sistema como participante + E que estou na página do formulário não respondido da minha turma + Quando eu deixo questões obrigatórias em branco + E clico no botão de submeter avaliação + Então o sistema não deve processar o envio + E eu devo ver a mensagem de erro "Por favor, preencha todas as questões obrigatórias" \ No newline at end of file diff --git a/project/features/participante_ver_formularios_nao_respondidos.feature b/project/features/participante_ver_formularios_nao_respondidos.feature new file mode 100644 index 0000000000..3a20ec31c4 --- /dev/null +++ b/project/features/participante_ver_formularios_nao_respondidos.feature @@ -0,0 +1,20 @@ +# language: pt +Funcionalidade: Visualização de formulários pendentes do participante + + Como um Participante de uma turma + Quero visualizar os formulários não respondidos das turmas em que estou matriculado + A fim de poder escolher qual irei responder + + Cenário: Participante possui formulários pendentes para responder + Dado que estou logado no sistema como "Participante" + Quando eu acesso a página inicial do meu painel (Dashboard) + E existem formulários ativos das minhas turmas que eu ainda não respondi + Então o sistema deve listar os formulários pendentes na seção "Avaliações Pendentes" + E cada item da lista deve ter um botão "Responder Avaliação" + + Cenário: Participante já respondeu todos os formulários + Dado que estou logado no sistema como "Participante" + Quando eu acesso a página inicial do meu painel (Dashboard) + E eu já respondi a todos os formulários ativos das minhas turmas + Então a seção "Avaliações Pendentes" não deve listar nenhum formulário + E o sistema deve exibir a mensagem "Você não possui nenhuma avaliação pendente no momento." \ No newline at end of file diff --git a/project/features/redefinicao_de_senha.feature b/project/features/redefinicao_de_senha.feature new file mode 100644 index 0000000000..b2e79e159e --- /dev/null +++ b/project/features/redefinicao_de_senha.feature @@ -0,0 +1,38 @@ +# language: pt +@auth @issue-107 +Funcionalidade: Redefinicao de Senha + Como usuario cadastrado + Quero solicitar a redefinicao da minha senha por email + Para recuperar meu acesso ao CAMAAR + + Contexto: + Dado que existe um participante cadastrado com email "acjpjvjp@gmail.com", matricula "190084006" e senha definida "Senha@123" + + Cenario: Usuario redefine a senha com token valido + Quando o usuario acessa a pagina de esqueci minha senha + E informa o email "acjpjvjp@gmail.com" + E solicita a redefinicao de senha + Entao o sistema deve enviar um email com link de redefinicao para "acjpjvjp@gmail.com" + Quando o usuario acessa o link valido de redefinicao de senha + E informa a nova senha "NovaSenha@123" + E confirma a nova senha "NovaSenha@123" + E envia o formulario de redefinicao de senha + Entao o sistema deve atualizar a senha do usuario + E deve permitir login com a nova senha + + Cenario: Sistema nao revela se email inexistente esta cadastrado + Quando o usuario acessa a pagina de esqueci minha senha + E informa o email "naoexiste@unb.br" + E solicita a redefinicao de senha + Entao o sistema deve exibir uma mensagem generica de instrucao + E nao deve revelar se o email esta cadastrado + E nao deve criar token de redefinicao para usuario inexistente + + Cenario: Usuario nao redefine senha com token expirado + Dado que existe um link expirado de redefinicao de senha para "acjpjvjp@gmail.com" + Quando o usuario acessa o link expirado de redefinicao de senha + E informa a nova senha "NovaSenha@123" + E confirma a nova senha "NovaSenha@123" + E envia o formulario de redefinicao de senha + Entao o sistema nao deve atualizar a senha do usuario + E deve exibir a mensagem "Link de redefinicao de senha invalido ou expirado" diff --git a/project/features/step_definitions/.keep b/project/features/step_definitions/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/project/features/step_definitions/auth_steps.rb b/project/features/step_definitions/auth_steps.rb new file mode 100644 index 0000000000..c09d2a7f4c --- /dev/null +++ b/project/features/step_definitions/auth_steps.rb @@ -0,0 +1,319 @@ +Before("@auth") do + ActionMailer::Base.deliveries.clear +end + +After("@auth") do + File.delete(@temporary_import_path) if @temporary_import_path.present? && File.exist?(@temporary_import_path) +end + +Dado("que existe um administrador autenticado do departamento {string}") do |department_code| + department = Department.find_or_create_by!(code: department_code) { |record| record.name = department_code } + @current_admin = User.create!( + name: "Administrador #{department_code}", + email: "admin.#{department_code.downcase}@unb.br", + registration: "000000001", + role: :admin, + department:, + password: "Senha@123" + ) +end + +Dado("que o arquivo {string} contem participantes da turma {string} no semestre {string}") do |file_name, class_code, semester| + @import_path = Rails.root.join("..", file_name) + expect(JSON.parse(File.read(@import_path))).to include(a_hash_including("code" => class_code, "semester" => semester)) +end + +Quando("o administrador importa os participantes do SIGAA") do + @import_result = Sigaa::ClassMembersImporter.new(path: @import_path, imported_by: @current_admin).call +end + +Quando("o administrador importa novamente os participantes do SIGAA") do + step "o administrador importa os participantes do SIGAA" +end + +Entao("o sistema deve cadastrar o usuario {string} com email {string} e matricula {string}") do |name, email, registration| + @last_user = User.find_by!(email:, registration:) + expect(@last_user.name).to eq(name) +end + +Entao("deve associar o usuario a turma {string} como participante discente") do |class_code| + expect(@last_user.course_classes.where(code: class_code)).to exist + expect(@last_user.class_memberships.discente).to exist +end + +Entao("deve marcar o usuario como pendente de definicao de senha") do + expect(@last_user).to be_pending_password_setup +end + +Entao("deve enviar email com link de definicao de senha para {string}") do |email| + message = ActionMailer::Base.deliveries.find { |delivery| delivery.to.include?(email) } + expect(message).to be_present + expect(message.body.encoded).to include("senha/definir") +end + +Dado("que ja existe um usuario cadastrado com email {string} e matricula {string}") do |email, registration| + User.create!( + name: "Usuario Existente", + email:, + registration:, + role: :participant, + department: Department.find_or_create_by!(code: "CIC") { |record| record.name = "CIC" }, + password: "Senha@123" + ) +end + +Entao("o sistema nao deve criar outro usuario com a matricula {string}") do |registration| + expect(User.where(registration:).count).to eq(1) +end + +Entao("deve manter uma unica conta para o email {string}") do |email| + expect(User.where(email:).count).to eq(1) +end + +Entao("deve atualizar os vinculos de turma quando necessario") do + expect(User.find_by!(email: "acjpjvjp@gmail.com").course_classes).not_to be_empty +end + +Dado("que o arquivo de importacao contem um participante sem email ou matricula") do + @temporary_import_path = Rails.root.join("tmp", "cucumber_class_members.json") + @import_path = @temporary_import_path + payload = [ + { + "code" => "CIC0097", + "classCode" => "TA", + "semester" => "2021.2", + "dicente" => [ + { "nome" => "Participante Invalido", "matricula" => "", "email" => "" }, + { "nome" => "Ana Clara Jordao Perna", "matricula" => "190084006", "email" => "acjpjvjp@gmail.com" } + ] + } + ] + File.write(@temporary_import_path, JSON.pretty_generate(payload)) +end + +Entao("o sistema nao deve criar conta para o participante invalido") do + expect(User.where(name: "Participante Invalido")).not_to exist +end + +Entao("deve registrar a inconsistencia encontrada na importacao") do + expect(ImportInconsistency.where(message: "Participante sem email ou matricula")).to exist +end + +Entao("deve continuar processando os demais participantes validos") do + expect(User.where(registration: "190084006")).to exist +end + +Dado("que existe um administrador cadastrado com email {string}, matricula {string} e senha definida {string}") do |email, registration, password| + User.create!( + name: "Administrador CIC", + email:, + registration:, + role: :admin, + department: Department.find_or_create_by!(code: "CIC") { |record| record.name = "CIC" }, + password: + ) +end + +Dado("que existe um participante cadastrado com email {string}, matricula {string} e senha definida {string}") do |email, registration, password| + @user = User.create!( + name: "Participante", + email:, + registration:, + role: :participant, + department: Department.find_or_create_by!(code: "CIC") { |record| record.name = "CIC" }, + password: + ) +end + +Dado("que existe um participante cadastrado com email {string}, matricula {string} e sem senha definida") do |email, registration| + User.create!( + name: "Participante sem senha", + email:, + registration:, + role: :participant, + department: Department.find_or_create_by!(code: "CIC") { |record| record.name = "CIC" } + ) +end + +Quando("o usuario acessa a pagina de login") do + visit login_path +end + +Quando("informa o identificador {string}") do |identifier| + fill_in "Email ou matricula", with: identifier +end + +Quando("informa a senha {string}") do |password| + fill_in "Senha", with: password +end + +Quando("envia o formulario de login") do + click_button "Entrar" +end + +Entao("o sistema deve autenticar o administrador") do + expect(page).to have_content("Login realizado com sucesso") + expect(page).to have_content("Administrador CIC") +end + +Entao("deve exibir o menu lateral com opcoes de gerenciamento") do + expect(page).to have_content("Gerenciar templates") + expect(page).to have_content("Gerenciar formularios") +end + +Entao("o sistema deve autenticar o participante") do + expect(page).to have_content("Login realizado com sucesso") +end + +Entao("deve exibir o menu lateral com formularios pendentes") do + expect(page).to have_content("Formularios pendentes") +end + +Entao("nao deve exibir opcoes administrativas") do + expect(page).not_to have_content("Gerenciar templates") +end + +Entao("o sistema nao deve autenticar o usuario") do + expect(page).not_to have_content("Login realizado com sucesso") +end + +Entao("deve exibir a mensagem {string}") do |message| + expect(page).to have_content(message) +end + +Entao("deve informar que a senha inicial precisa ser definida antes do acesso") do + expect(page).to have_content("Senha inicial precisa ser definida antes do acesso") +end + +Dado("que existe um participante importado do SIGAA com email {string}, matricula {string} e sem senha definida") do |email, registration| + @user = User.create!( + name: "Ana Clara Jordao Perna", + email:, + registration:, + role: :participant, + department: Department.find_or_create_by!(code: "CIC") { |record| record.name = "CIC" } + ) +end + +Dado("que o sistema enviou um link valido de definicao de senha para {string}") do |email| + user = User.find_by!(email:) + @setup_token = user.generate_password_setup_token! + UserMailer.password_setup(user, @setup_token).deliver_now +end + +Quando("o usuario acessa o link valido de definicao de senha") do + @old_password_digest = @user.reload.password_digest + visit edit_password_setup_path(@setup_token) +end + +Quando("informa a nova senha {string}") do |password| + @new_password = password + fill_in "Nova senha", with: password if page.has_field?("Nova senha") +end + +Quando("confirma a nova senha {string}") do |password| + @new_password_confirmation = password + fill_in "Confirmacao de senha", with: password if page.has_field?("Confirmacao de senha") +end + +Quando("envia o formulario de definicao de senha") do + click_button "Definir senha" +end + +Entao("o sistema deve salvar a senha do usuario") do + expect(@user.reload.password_digest).to be_present + expect(@user.password_digest).not_to eq(@old_password_digest) +end + +Entao("deve ativar o acesso do usuario ao CAMAAR") do + expect(@user.reload).not_to be_pending_password_setup +end + +Entao("deve permitir login com email ou matricula e a nova senha") do + visit login_path + fill_in "Email ou matricula", with: @user.email + fill_in "Senha", with: "Senha@123" + click_button "Entrar" + expect(page).to have_content("Login realizado com sucesso") +end + +Quando("o usuario acessa um link invalido de definicao de senha") do + visit edit_password_setup_path("token-invalido") +end + +Entao("o sistema nao deve permitir a definicao de senha") do + expect(page).to have_content("Link de definicao de senha invalido ou expirado") +end + +Entao("o sistema nao deve salvar a senha do usuario") do + expect(@user.reload.password_digest).to eq(@old_password_digest) +end + +Quando("o usuario acessa a pagina de esqueci minha senha") do + visit new_password_reset_path +end + +Quando("informa o email {string}") do |email| + fill_in "Email", with: email +end + +Quando("solicita a redefinicao de senha") do + click_button "Solicitar redefinicao de senha" +end + +Entao("o sistema deve enviar um email com link de redefinicao para {string}") do |email| + @reset_email = ActionMailer::Base.deliveries.find { |delivery| delivery.to.include?(email) } + expect(@reset_email).to be_present + expect(@reset_email.body.encoded).to include("senha/redefinir") +end + +Quando("o usuario acessa o link valido de redefinicao de senha") do + @old_password_digest = @user.reload.password_digest + @reset_token = @reset_email.body.encoded.match(%r{senha/redefinir/([^"\s<]+)})[1] + visit edit_password_reset_path(@reset_token) +end + +Quando("envia o formulario de redefinicao de senha") do + click_button "Redefinir senha" if page.has_button?("Redefinir senha") +end + +Entao("o sistema deve atualizar a senha do usuario") do + expect(@user.reload.password_digest).to be_present + expect(@user.password_digest).not_to eq(@old_password_digest) +end + +Entao("deve permitir login com a nova senha") do + visit login_path + fill_in "Email ou matricula", with: @user.email + fill_in "Senha", with: "NovaSenha@123" + click_button "Entrar" + expect(page).to have_content("Login realizado com sucesso") +end + +Entao("o sistema deve exibir uma mensagem generica de instrucao") do + expect(page).to have_content(PasswordResetsController::GENERIC_MESSAGE) +end + +Entao("nao deve revelar se o email esta cadastrado") do + expect(page).not_to have_content("nao cadastrado") + expect(page).not_to have_content("inexistente") +end + +Entao("nao deve criar token de redefinicao para usuario inexistente") do + expect(User.where.not(password_reset_token_digest: nil)).not_to exist +end + +Dado("que existe um link expirado de redefinicao de senha para {string}") do |email| + @user = User.find_by!(email:) + @old_password_digest = @user.password_digest + @reset_token = @user.generate_password_reset_token! + @user.update!(password_reset_sent_at: 3.hours.ago) +end + +Quando("o usuario acessa o link expirado de redefinicao de senha") do + visit edit_password_reset_path(@reset_token) +end + +Entao("o sistema nao deve atualizar a senha do usuario") do + expect(@user.reload.password_digest).to eq(@old_password_digest) +end diff --git a/project/features/step_definitions/formularios_steps.rb b/project/features/step_definitions/formularios_steps.rb new file mode 100644 index 0000000000..9a2b18a986 --- /dev/null +++ b/project/features/step_definitions/formularios_steps.rb @@ -0,0 +1,188 @@ +# frozen_string_literal: true + + +# CONTEXTO GERAL E AUTENTICAÇÃO + + +Dado('que estou logado no sistema como {string}') do |perfil| + trait = perfil == 'Administrador' ? :admin : nil + @user = trait ? create(:user, trait) : create(:user) + + visit '/login' + fill_in 'Email ou matricula', with: @user.email + fill_in 'Senha', with: 'Senha@123' + click_button 'Entrar' +end + +Dado('que estou autenticado no sistema como participante') do + step 'que estou logado no sistema como "Participante"' +end + + +# ISSUE 110: VISUALIZAÇÃO DE FORMULÁRIOS (ADMINISTRADOR) + + +Quando('eu clico na aba de {string} no menu lateral') do |aba| + nome_do_link = aba == "Meus Formulários" ? "Gerenciar formularios" : aba + click_link nome_do_link +end + +E('existem formulários previamente criados por mim no sistema') do + @formularios = create_list(:formulario, 3, admin: @user) + visit current_path +end + +E('eu ainda não criei nenhum formulário no sistema') do + expect(Formulario.where(admin: @user).count).to eq(0) +end + +Então('eu devo ver uma lista com todos os meus formulários publicados') do + @formularios.each do |formulario| + expect(page).to have_content(formulario.title) + end +end + +E('a lista deve exibir o título, a turma e o status de cada formulário') do + @formularios.each do |formulario| + expect(page).to have_content(formulario.title) + expect(page).to have_content(formulario.course_class.name) + expect(page).to have_content(formulario.status || 'Ativo') + end +end + +Então('a lista de formulários deve aparecer vazia') do + expect(page).not_to have_css('.formulario-item') +end + + +# ISSUE 109: FORMULÁRIOS PENDENTES (PARTICIPANTE) + + +Quando('eu acesso a página inicial do meu painel \(Dashboard)') do + visit formularios_path +end + +E('existem formulários ativos das minhas turmas que eu ainda não respondi') do + @turma = create(:course_class) + @user.course_classes << @turma + @formulario_pendente = create(:formulario, course_class: @turma) + visit current_path +end + +E('eu já respondi a todos os formulários ativos das minhas turmas') do + @turma = create(:course_class) + @user.course_classes << @turma + formulario = create(:formulario, course_class: @turma) + + create(:submissao, formulario: formulario, user: @user) + visit current_path +end + +Então('o sistema deve listar os formulários pendentes na seção {string}') do |secao| + expect(page).to have_content(secao) + expect(page).to have_content(@formulario_pendente.title) +end + +Então('a seção {string} não deve listar nenhum formulário') do |secao| + expect(page).to have_content(secao) +end + +E('cada item da lista deve ter um botão {string}') do |nome_botao| + expect(page).to have_link(nome_botao) +end + + +# ISSUE 99: RESPONDER QUESTIONÁRIO + + +E('que estou na página do formulário não respondido da minha turma') do + @turma = create(:course_class) + @user.course_classes << @turma + @formulario = create(:formulario, :com_questao, course_class: @turma) + + visit formulario_path(@formulario) +end + +Quando('eu preencho todas as avaliações corretamente') do + fill_in 'Score', with: '10' +end + +Quando('eu deixo questões obrigatórias em branco') do + fill_in 'Score', with: '' +end + +E('clico no botão de submeter avaliação') do + click_button 'Enviar Resposta' +end + +Então('o sistema deve salvar os {string} vinculados ao meu usuário') do |_| + expect(Submissao.where(user: @user, formulario: @formulario).count).to eq(1) + expect(Respostum.count).to eq(1) +end + +Então('o sistema não deve processar o envio') do + expect(Submissao.count).to eq(0) +end + +E('eu devo ser redirecionado com uma mensagem {string}') do |mensagem| + expect(page).to have_content(mensagem) +end + +E('eu devo ver a mensagem de erro {string}') do |mensagem| + expect(Submissao.count).to eq(0) + expect(page).to have_content(mensagem) +end + +E('o sistema deve exibir a mensagem {string}') do |mensagem| + expect(page).to have_content(mensagem) +end + + +# ISSUE 100: EXPORTAÇÃO DE CSV (ADMIN) + + +E('que estou na página de visualização de resultados da turma {string}') do |nome_turma| + @turma = create(:course_class, name: nome_turma) + @formulario = create(:formulario, admin: @user, course_class: @turma) + visit formulario_path(@formulario) +end + +E('que estou na página de visualização de resultados de um formulário recém-criado') do + @formulario = create(:formulario, admin: @user) + visit formulario_path(@formulario) +end + +Quando('o formulário selecionado já possui respostas submetidas') do + aluno = create(:user) + create(:submissao, formulario: @formulario, user: aluno) + visit formulario_path(@formulario) # <--- Força o recarregamento explícito da página +end + +Quando('o formulário selecionado ainda não possui nenhuma resposta de participante') do + expect(Submissao.where(formulario_id: @formulario.id).count).to eq(0) +end + +E('eu clico no botão {string}') do |texto_botao| + if page.has_link?(texto_botao) + click_link texto_botao + elsif page.has_button?(texto_botao) + click_button texto_botao + end +end + +Então('o sistema deve gerar um arquivo {string} com todas as respostas estruturadas') do |_| + expect(page.response_headers['Content-Type']).to include('text/csv') +end + +E('o download do arquivo deve iniciar automaticamente no meu navegador') do + expect(page.response_headers['Content-Disposition']).to include('attachment') + expect(page.response_headers['Content-Disposition']).to include(".csv") +end + +Então('o sistema não deve gerar o arquivo') do + expect(page.response_headers['Content-Type']).not_to include('text/csv') +end + +E('deve exibir um alerta impeditivo {string}') do |mensagem| + expect(page).to have_content(mensagem) +end \ No newline at end of file diff --git a/project/features/step_definitions/sigaa_import_steps.rb b/project/features/step_definitions/sigaa_import_steps.rb new file mode 100644 index 0000000000..b3d36b3544 --- /dev/null +++ b/project/features/step_definitions/sigaa_import_steps.rb @@ -0,0 +1,63 @@ +Dado("que eu acesse a tela de {string}") do |screen_title| + visit_sigaa_import_screen(screen_title) +end + +E("eu acesse a tela de {string}") do |screen_title| + visit_sigaa_import_screen(screen_title) +end + +def visit_sigaa_import_screen(screen_title) + fazer_login_como(@current_admin) + visit imports_sigaa_path + expect(page).to have_content(screen_title) +end + +Quando("eu selecionar o periodo letivo desejado") do + @selected_semester = Sigaa::DataSynchronizer.available_semesters.first + select @selected_semester, from: "Periodo letivo" +end + +Quando("solicitar a sincronizacao de {string}") do |_scope| + click_button "Sincronizar turmas, materias e participantes" +end + +Entao("o sistema deve consultar o SIGAA \\(arquivos no repositorio original)") do + expect(CourseClass.where(semester: @selected_semester)).to exist + expect(User.where(role: :participant)).to exist +end + +Entao("deve inserir os novos registros identificados") do + expect(CourseClass.where(semester: @selected_semester).count).to be >= 1 + expect(User.where(role: :participant).count).to be >= 1 +end + +Entao("deve atualizar os dados dos registros que sofreram alteracoes no SIGAA") do + expect(page).to have_content(/Sincronizacao concluida\. \d+ novos registros adicionados e \d+ registros atualizados\./) +end + +Entao("exibir a mensagem de sincronizacao concluida com novos registros") do + expect(page).to have_content(/Sincronizacao concluida\. \d+ novos registros adicionados e \d+ registros atualizados\./) + expect(page).to have_content(/[1-9]\d* novos registros adicionados/) +end + +Dado("que todos os dados do SIGAA para o periodo atual ja tenham sido importados previamente") do + @selected_semester = Sigaa::DataSynchronizer.available_semesters.first + Sigaa::DataSynchronizer.new(semester: @selected_semester, imported_by: @current_admin).call +end + +Quando("eu solicitar a sincronizacao dos dados") do + select @selected_semester, from: "Periodo letivo" + click_button "Sincronizar turmas, materias e participantes" +end + +Entao("o sistema deve verificar a base de dados") do + expect(CourseClass.where(semester: @selected_semester)).to exist +end + +Entao("constatar que nao ha novos registros para inserir") do + expect(CourseClass.where(semester: @selected_semester).count).to be >= 1 +end + +Entao("deve finalizar o processo exibindo a mensagem: {string}") do |message| + expect(page).to have_content(message) +end diff --git a/project/features/step_definitions/templates_edicao_steps.rb b/project/features/step_definitions/templates_edicao_steps.rb new file mode 100644 index 0000000000..d2a07f3d26 --- /dev/null +++ b/project/features/step_definitions/templates_edicao_steps.rb @@ -0,0 +1,87 @@ +# Step definitions para features/admin_editar_deletar_templates.feature (#112) + +Dado("que eu editei informações de um template existente") do + @admin = criar_admin + @template = criar_template_com_questao(@admin) + + # Formulário gerado a partir da versão ATUAL do template, com questões já clonadas. + turma = criar_turma + @formulario = @admin.formularios.create!(title: "Formulário Antigo", template_id: @template.id, course_class_id: turma.id, target_role: @template.target_role) + @template.questaos.each do |questao| + @formulario.questaos.create!(enunciado: questao.enunciado, tipo: questao.tipo) + end + @enunciado_original_formulario = @formulario.questaos.first.enunciado + + @novo_titulo = "Template Editado #{SecureRandom.hex(4)}" + + fazer_login_como(@admin) + visit edit_template_path(@template) + fill_in "template[title]", with: @novo_titulo +end + +Quando("eu pressionar o botão {string} na tela de edição") do |texto_botao| + click_button texto_botao +end + +Então("eu devo visualizar as alterações no template na tela de templates") do + expect(page).to have_text(@novo_titulo) +end + +Então("os formulários criados usando a versão antiga não devem sofrer alterações nas suas questões") do + @formulario.reload + expect(@formulario.questaos.first.enunciado).to eq(@enunciado_original_formulario) + expect(@formulario.questaos.first.template_id).to be_nil +end + +Dado("que eu escolhi um template na minha lista") do + @admin = criar_admin + @template = criar_template_com_questao(@admin) + + turma = criar_turma + @formulario = @admin.formularios.create!(title: "Formulário Gerado", template_id: @template.id, course_class_id: turma.id, target_role: @template.target_role) + @template.questaos.each do |questao| + @formulario.questaos.create!(enunciado: questao.enunciado, tipo: questao.tipo) + end + + @template_title = @template.title + fazer_login_como(@admin) + visit templates_path +end + +Quando("eu pressionar o botão de {string}") do |texto| + click_link texto +end + +Então("confirmar a deleção pressionando o botão {string}") do |texto| + click_button texto +end + +Então("esse template não deve aparecer mais na tela de templates") do + expect(page).not_to have_text(@template_title) +end + +Então("os formulários gerados por ele devem permanecer intactos no sistema") do + @formulario.reload + expect(@formulario.title).to eq("Formulário Gerado") + expect(@formulario.template_id).to be_nil + expect(@formulario.questaos.count).to eq(1) +end + +Dado("que estou na página de edição do template") do + @admin = criar_admin + @template = criar_template_com_questao(@admin) + fazer_login_como(@admin) + visit edit_template_path(@template) +end + +Quando("eu clico no botão '+'") do + click_button "+" +end + +Quando("clico no botão 'Confirmar edição'") do + click_button "Confirmar edição" +end + +Então("deve aparecer uma mensagem 'A nova questão não possui enunciado'") do + expect(page).to have_text("A nova questão não possui enunciado") +end diff --git a/project/features/step_definitions/templates_steps.rb b/project/features/step_definitions/templates_steps.rb new file mode 100644 index 0000000000..165bbd3f6f --- /dev/null +++ b/project/features/step_definitions/templates_steps.rb @@ -0,0 +1,54 @@ +# Step definitions para features/admin_criar_template.feature (#102) +# Alguns passos abaixo são reutilizados por features/admin_editar_deletar_templates.feature (#112). + +Dado("que estou na página de criar template") do + @admin = criar_admin + fazer_login_como(@admin) + visit new_template_path +end + +Quando("eu preencho o campo 'Nome do template:'") do + @template_title = "Template Avaliação #{SecureRandom.hex(4)}" + fill_in "template[title]", with: @template_title +end + +Quando("eu deixo o campo 'Nome do template:' vazio") do + # O campo é deliberadamente deixado em branco. +end + +Quando("clico no botão '+'") do + click_button "+" +end + +Quando("seleciono o 'Público-alvo:'") do + select "Discentes", from: "template[target_role]" +end + +Quando("preencho o campo 'Enunciado da questão:'") do + fill_in "template[questaos][0][enunciado]", with: "Qual é a capital do Brasil?" +end + +Quando("não preencho o campo 'Enunciado da questão:'") do + # O campo é deliberadamente deixado em branco. +end + +Quando("adiciono uma questão válida") do + click_button "+" + fill_in "template[questaos][0][enunciado]", with: "Qual é a maior montanha do mundo?" +end + +Quando("clico no botão 'Criar'") do + click_button "Criar" +end + +Então("o novo template deve aparecer na tela de meus templates") do + expect(page).to have_text(@template_title) +end + +Então("deve aparecer uma mensagem 'O nome do template é obrigatório'") do + expect(page).to have_text("O nome do template é obrigatório") +end + +Então("deve aparecer uma mensagem 'Questão não possui enunciado'") do + expect(page).to have_text("Questão não possui enunciado") +end diff --git a/project/features/step_definitions/templates_visualizacao_steps.rb b/project/features/step_definitions/templates_visualizacao_steps.rb new file mode 100644 index 0000000000..79dc40ce72 --- /dev/null +++ b/project/features/step_definitions/templates_visualizacao_steps.rb @@ -0,0 +1,34 @@ +# Step definitions para features/admin_ver_templates.feature (#111) + +Dado("que o administrador está na interface de templates") do + @admin = criar_admin + @template = criar_template_com_questao(@admin, titulo: "Template do Admin #{SecureRandom.hex(4)}") + + @outro_admin = criar_admin + @template_outro_admin = criar_template_com_questao(@outro_admin, titulo: "Template de Outro Admin #{SecureRandom.hex(4)}") + + fazer_login_como(@admin) + visit templates_path +end + +Quando("o sistema carrega os templates") do + # A página já foi carregada no passo "Dado". +end + +Então("ele deverá ver somente os templates criados por ele") do + expect(page).to have_text(@template.title) + expect(page).not_to have_text(@template_outro_admin.title) +end + +Então("deverá ter a opção de deletar ou editar esses templates") do + expect(page).to have_link("Editar") + expect(page).to have_link("Deletar") +end + +Quando("ele tenta acessar diretamente um template criado por outro administrador") do + visit edit_template_path(@template_outro_admin) +end + +Então("ele deverá ver uma mensagem de erro de permissão {string}") do |mensagem| + expect(page).to have_text(mensagem) +end diff --git a/project/features/support/env.rb b/project/features/support/env.rb new file mode 100644 index 0000000000..c64903a620 --- /dev/null +++ b/project/features/support/env.rb @@ -0,0 +1,57 @@ +# 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' +require 'rspec/expectations' + +# By default, any exception happening in your Rails application will bubble up +# to Cucumber so that your scenario will fail. This is a different from how +# 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 + +require 'factory_bot_rails' +World(FactoryBot::Syntax::Methods) \ No newline at end of file diff --git a/project/features/support/test_helpers.rb b/project/features/support/test_helpers.rb new file mode 100644 index 0000000000..2de7787736 --- /dev/null +++ b/project/features/support/test_helpers.rb @@ -0,0 +1,40 @@ +module TestHelpers + def criar_admin + sufixo = SecureRandom.hex(4) + User.create!( + name: "Admin Teste #{sufixo}", + email: "admin#{sufixo}@teste.com", + registration: "ADM#{sufixo}", + role: :admin, + password: "Senha@123" + ) + end + + def fazer_login_como(user, password: "Senha@123") + visit login_path + fill_in "Email ou matricula", with: user.email + fill_in "Senha", with: password + click_button "Entrar" + end + + def criar_template_com_questao(admin, titulo: "Template #{SecureRandom.hex(4)}", target_role: "discente", enunciado: "Qual é a resposta correta?") + template = admin.templates.create!(title: titulo, target_role: target_role) + template.questaos.create!(enunciado: enunciado, tipo: "text") + template + end + + def criar_turma + department = Department.find_or_create_by!(code: "DEPT001") { |d| d.name = "Departamento de Teste" } + sufixo = SecureRandom.hex(4) + CourseClass.create!( + code: "CIC#{sufixo}", + class_code: "T#{sufixo}", + name: "Disciplina Teste #{sufixo}", + semester: "2026.1", + time: "10:00", + department: department + ) + end +end + +World(TestHelpers) diff --git a/project/lib/tasks/.keep b/project/lib/tasks/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/project/lib/tasks/cucumber.rake b/project/lib/tasks/cucumber.rake new file mode 100644 index 0000000000..0caa4d2553 --- /dev/null +++ b/project/lib/tasks/cucumber.rake @@ -0,0 +1,69 @@ +# IMPORTANT: This file is generated by cucumber-rails - edit at your own peril. +# It is recommended to regenerate this file in the future when you upgrade to a +# newer version of cucumber-rails. Consider adding your own code to a new file +# instead of editing this one. Cucumber will automatically load all features/**/*.rb +# files. + + +unless ARGV.any? {|a| a =~ /^gems/} # Don't load anything when running the gems:* tasks + +vendored_cucumber_bin = Dir["#{Rails.root}/vendor/{gems,plugins}/cucumber*/bin/cucumber"].first +$LOAD_PATH.unshift(File.dirname(vendored_cucumber_bin) + '/../lib') unless vendored_cucumber_bin.nil? + +begin + require 'cucumber/rake/task' + + namespace :cucumber do + Cucumber::Rake::Task.new({ok: 'test:prepare'}, 'Run features that should pass') do |t| + t.binary = vendored_cucumber_bin # If nil, the gem's binary is used. + t.fork = true # You may get faster startup if you set this to false + t.profile = 'default' + end + + Cucumber::Rake::Task.new({wip: 'test:prepare'}, 'Run features that are being worked on') do |t| + t.binary = vendored_cucumber_bin + t.fork = true # You may get faster startup if you set this to false + t.profile = 'wip' + end + + Cucumber::Rake::Task.new({rerun: 'test:prepare'}, 'Record failing features and run only them if any exist') do |t| + t.binary = vendored_cucumber_bin + t.fork = true # You may get faster startup if you set this to false + t.profile = 'rerun' + end + + desc 'Run all features' + task all: [:ok, :wip] + + task :statsetup do + require 'rails/code_statistics' + ::STATS_DIRECTORIES << %w(Cucumber\ features features) if File.exist?('features') + ::CodeStatistics::TEST_TYPES << "Cucumber features" if File.exist?('features') + end + + end + + desc 'Alias for cucumber:ok' + task cucumber: 'cucumber:ok' + + task default: :cucumber + + task features: :cucumber do + STDERR.puts "*** The 'features' task is deprecated. See rake -T cucumber ***" + end + + # In case we don't have the generic Rails test:prepare hook, append a no-op task that we can depend upon. + task 'test:prepare' do + end + + task stats: 'cucumber:statsetup' + + +rescue LoadError + desc 'cucumber rake task not available (cucumber not installed)' + task :cucumber do + abort 'Cucumber rake task is not available. Be sure to install cucumber as a gem or plugin' + end +end + +end diff --git a/project/log/.keep b/project/log/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/project/public/400.html b/project/public/400.html new file mode 100644 index 0000000000..640de03397 --- /dev/null +++ b/project/public/400.html @@ -0,0 +1,135 @@ + + + + + + + The server cannot process the request due to a client error (400 Bad Request) + + + + + + + + + + + + + +
+
+ +
+
+

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

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

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

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

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

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

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

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

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

+
+
+ + + + diff --git a/project/public/icon.png b/project/public/icon.png new file mode 100644 index 0000000000..c4c9dbfbbd Binary files /dev/null and b/project/public/icon.png differ diff --git a/project/public/icon.svg b/project/public/icon.svg new file mode 100644 index 0000000000..04b34bf83f --- /dev/null +++ b/project/public/icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/project/public/robots.txt b/project/public/robots.txt new file mode 100644 index 0000000000..c19f78ab68 --- /dev/null +++ b/project/public/robots.txt @@ -0,0 +1 @@ +# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file diff --git a/project/script/.keep b/project/script/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/project/spec/factories/course_classes.rb b/project/spec/factories/course_classes.rb new file mode 100644 index 0000000000..b30bcb8aff --- /dev/null +++ b/project/spec/factories/course_classes.rb @@ -0,0 +1,10 @@ +FactoryBot.define do + factory :course_class do + association :department + sequence(:code) { |n| "CIC#{n.to_s.rjust(4, '0')}" } + sequence(:class_code) { |n| "T#{n}" } + name { "Disciplina Teste" } + semester { "2026.1" } + time { "10:00" } + end +end diff --git a/project/spec/factories/departments.rb b/project/spec/factories/departments.rb new file mode 100644 index 0000000000..f65cc6885c --- /dev/null +++ b/project/spec/factories/departments.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :department do + sequence(:code) { |n| "DEPT#{n}" } + sequence(:name) { |n| "Departamento #{n}" } + end +end diff --git a/project/spec/factories/formularios.rb b/project/spec/factories/formularios.rb new file mode 100644 index 0000000000..a8015a9544 --- /dev/null +++ b/project/spec/factories/formularios.rb @@ -0,0 +1,15 @@ +FactoryBot.define do + factory :formulario do + association :admin, factory: %i[user admin] + template + association :course_class + sequence(:title) { |n| "Formulário de Teste #{n}" } + target_role { "discente" } + + trait :com_questao do + after(:create) do |formulario| + create(:questao, :de_formulario, formulario: formulario) + end + end + end +end diff --git a/project/spec/factories/questaos.rb b/project/spec/factories/questaos.rb new file mode 100644 index 0000000000..a23cffa03c --- /dev/null +++ b/project/spec/factories/questaos.rb @@ -0,0 +1,16 @@ +FactoryBot.define do + factory :questao do + template + sequence(:enunciado) { |n| "Pergunta de teste #{n}?" } + tipo { "text" } + + trait :rating do + tipo { "rating" } + end + + trait :de_formulario do + template { nil } + formulario + end + end +end diff --git a/project/spec/factories/respostas.rb b/project/spec/factories/respostas.rb new file mode 100644 index 0000000000..ea45109848 --- /dev/null +++ b/project/spec/factories/respostas.rb @@ -0,0 +1,13 @@ +FactoryBot.define do + factory :respostum do + submissao + questao + valor_texto { "Resposta de teste" } + valor_numerico { nil } + + trait :numerica do + valor_texto { nil } + valor_numerico { 5 } + end + end +end diff --git a/project/spec/factories/submissaos.rb b/project/spec/factories/submissaos.rb new file mode 100644 index 0000000000..4ee953fde5 --- /dev/null +++ b/project/spec/factories/submissaos.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :submissao do + formulario + association :user + end +end diff --git a/project/spec/factories/templates.rb b/project/spec/factories/templates.rb new file mode 100644 index 0000000000..1cc98c61d7 --- /dev/null +++ b/project/spec/factories/templates.rb @@ -0,0 +1,17 @@ +FactoryBot.define do + factory :template do + association :admin, factory: %i[user admin] + sequence(:title) { |n| "Template #{n}" } + target_role { "discente" } + + trait :docente do + target_role { "docente" } + end + + trait :com_questao do + after(:create) do |template| + create(:questao, template: template) + end + end + end +end diff --git a/project/spec/factories/users.rb b/project/spec/factories/users.rb new file mode 100644 index 0000000000..01a52101b6 --- /dev/null +++ b/project/spec/factories/users.rb @@ -0,0 +1,13 @@ +FactoryBot.define do + factory :user do + sequence(:name) { |n| "Usuario #{n}" } + sequence(:email) { |n| "user#{n}@example.com" } + sequence(:registration) { |n| "U#{1000 + n}" } + role { :participant } + password { "Senha@123" } + + trait :admin do + role { :admin } + end + end +end diff --git a/project/spec/mailers/user_mailer_spec.rb b/project/spec/mailers/user_mailer_spec.rb new file mode 100644 index 0000000000..808e1d9ff7 --- /dev/null +++ b/project/spec/mailers/user_mailer_spec.rb @@ -0,0 +1,21 @@ +require "rails_helper" + +RSpec.describe UserMailer do + it "envia link de definicao de senha" do + user = create(:user, email: "acjpjvjp@gmail.com", registration: "190084006", password: nil) + email = described_class.password_setup(user, "token-123") + + expect(email.to).to include("acjpjvjp@gmail.com") + expect(email.subject).to eq("Defina sua senha no CAMAAR") + expect(email.body.encoded).to include("senha/definir/token-123") + end + + it "envia link de redefinicao de senha" do + user = create(:user, email: "acjpjvjp@gmail.com", registration: "190084006", password: "Senha@123") + email = described_class.password_reset(user, "token-123") + + expect(email.to).to include("acjpjvjp@gmail.com") + expect(email.subject).to eq("Redefinicao de senha do CAMAAR") + expect(email.body.encoded).to include("senha/redefinir/token-123") + end +end diff --git a/project/spec/models/formulario_spec.rb b/project/spec/models/formulario_spec.rb new file mode 100644 index 0000000000..96ed594bbb --- /dev/null +++ b/project/spec/models/formulario_spec.rb @@ -0,0 +1,67 @@ +require "rails_helper" + +RSpec.describe Formulario, type: :model do + describe "validações" do + it "é válido e persistido com os atributos da factory" do + expect(create(:formulario)).to be_persisted + end + + it "exige um título" do + formulario = build(:formulario, title: "") + + expect(formulario).not_to be_valid + expect(formulario.errors[:title]).to include("O nome do formulário é obrigatório") + end + + it "exige um template ao ser criado (#103)" do + formulario = build(:formulario, template: nil) + + expect(formulario).not_to be_valid + expect(formulario.errors[:template_id]).to include("É necessário escolher um template base") + end + + it "exige uma turma ao ser criado (#103)" do + formulario = build(:formulario, course_class: nil) + + expect(formulario).not_to be_valid + expect(formulario.errors[:course_class_id]).to include("É necessário escolher ao menos uma turma") + end + + it "não exige template nem turma em atualizações (#112)" do + formulario = create(:formulario) + + formulario.template_id = nil + formulario.course_class_id = nil + + expect(formulario).to be_valid + end + end + + describe "valores padrão" do + it "define status como 'draft' quando não informado" do + expect(Formulario.new.status).to eq("draft") + end + + it "não sobrescreve o status quando já informado" do + formulario = Formulario.new(status: "concluido") + + expect(formulario.status).to eq("concluido") + end + end + + describe "associações" do + it "pertence a um admin, um template e uma turma" do + formulario = create(:formulario) + + expect(formulario.admin).to be_present + expect(formulario.template).to be_present + expect(formulario.course_class).to be_present + end + + it "destrói suas próprias questões (clonadas do template) ao ser destruído" do + formulario = create(:formulario, :com_questao) + + expect { formulario.destroy }.to change(Questao, :count).by(-1) + end + end +end diff --git a/project/spec/models/questao_spec.rb b/project/spec/models/questao_spec.rb new file mode 100644 index 0000000000..766d1490cb --- /dev/null +++ b/project/spec/models/questao_spec.rb @@ -0,0 +1,39 @@ +require "rails_helper" + +RSpec.describe Questao, type: :model do + describe "validações" do + it "é válida com os atributos da factory" do + expect(build(:questao)).to be_valid + end + + it "exige um enunciado" do + questao = build(:questao, enunciado: nil) + + expect(questao).not_to be_valid + expect(questao.errors[:enunciado]).to include("can't be blank") + end + end + + describe "associações" do + it "pode pertencer a um template (questão de um template, #102)" do + template = create(:template) + questao = create(:questao, template: template) + + expect(questao.template).to eq(template) + expect(questao.formulario_id).to be_nil + end + + it "pode pertencer a um formulário sem template (questão clonada, #103)" do + questao = create(:questao, :de_formulario) + + expect(questao.template_id).to be_nil + expect(questao.formulario).to be_present + end + + it "é válida sem template e sem formulário associados" do + questao = build(:questao, template: nil, formulario: nil) + + expect(questao).to be_valid + end + end +end diff --git a/project/spec/models/template_spec.rb b/project/spec/models/template_spec.rb new file mode 100644 index 0000000000..4ab7f734ab --- /dev/null +++ b/project/spec/models/template_spec.rb @@ -0,0 +1,45 @@ +require "rails_helper" + +RSpec.describe Template, type: :model do + describe "validações" do + it "é válido com os atributos da factory" do + expect(build(:template)).to be_valid + end + + it "exige um título" do + template = build(:template, title: "") + + expect(template).not_to be_valid + expect(template.errors[:title]).to include("O nome do template é obrigatório") + end + end + + describe "associações" do + it "pertence a um admin" do + template = create(:template) + + expect(template.admin).to be_present + end + + it "destrói suas questões ao ser destruído (#102)" do + template = create(:template, :com_questao) + + expect { template.destroy }.to change(Questao, :count).by(-1) + end + + it "desvincula, mas não destrói, formulários gerados ao ser destruído (#112)" do + template = create(:template) + formulario = create(:formulario, template: template) + + expect { template.destroy }.not_to change(Formulario, :count) + expect(formulario.reload.template_id).to be_nil + end + + it "não afeta as questões já clonadas para um formulário ao ser destruído (#112)" do + template = create(:template) + create(:formulario, :com_questao, template: template) + + expect { template.destroy }.not_to change(Questao, :count) + end + end +end diff --git a/project/spec/rails_helper.rb b/project/spec/rails_helper.rb new file mode 100644 index 0000000000..acb1445723 --- /dev/null +++ b/project/spec/rails_helper.rb @@ -0,0 +1,26 @@ +ENV["RAILS_ENV"] ||= "test" +require_relative "../config/environment" +abort("The Rails environment is running in production mode!") if Rails.env.production? + +require "rspec/rails" + +Rails.root.glob("spec/support/**/*.rb").sort_by(&:to_s).each { |file| require file } + +begin + ActiveRecord::Migration.maintain_test_schema! +rescue ActiveRecord::PendingMigrationError => e + abort e.to_s.strip +end + +RSpec.configure do |config| + config.fixture_paths = [ Rails.root.join("spec/fixtures") ] + config.use_transactional_fixtures = true + config.infer_spec_type_from_file_location! + config.filter_rails_from_backtrace! + config.include ActiveSupport::Testing::TimeHelpers + config.include FactoryBot::Syntax::Methods + + config.before do + ActionMailer::Base.deliveries.clear + end +end diff --git a/project/spec/requests/class_members_importer_spec.rb b/project/spec/requests/class_members_importer_spec.rb new file mode 100644 index 0000000000..3f2dcf0619 --- /dev/null +++ b/project/spec/requests/class_members_importer_spec.rb @@ -0,0 +1,100 @@ +require "rails_helper" + +RSpec.describe Sigaa::ClassMembersImporter do + let!(:admin) { create(:user, :admin) } + let(:path) { Rails.root.join("tmp", "spec_class_members.json") } + + after do + File.delete(path) if File.exist?(path) + end + + it "cadastra participantes novos, associa turma e envia link de definicao de senha" do + write_import_file([ + { + "code" => "CIC0097", + "classCode" => "TA", + "semester" => "2021.2", + "dicente" => [ + { + "nome" => "Ana Clara Jordao Perna", + "matricula" => "190084006", + "email" => "acjpjvjp@gmail.com" + } + ] + } + ]) + + result = described_class.new(path:, imported_by: admin).call + + user = User.find_by!(registration: "190084006") + expect(result.created_count).to eq(2) + expect(user.name).to eq("Ana Clara Jordao Perna") + expect(user.email).to eq("acjpjvjp@gmail.com") + expect(user).to be_pending_password_setup + expect(user.password_setup_token_digest).to be_present + expect(user.class_memberships.first).to be_discente + expect(user.course_classes.first.code).to eq("CIC0097") + expect(ActionMailer::Base.deliveries.last.to).to include("acjpjvjp@gmail.com") + expect(ActionMailer::Base.deliveries.last.body.encoded).to include("senha/definir") + end + + it "nao duplica usuario ja cadastrado e mantem uma unica conta por matricula e email" do + create(:user, name: "Ana", email: "acjpjvjp@gmail.com", registration: "190084006", password: "Senha@123") + + write_import_file([ + { + "code" => "CIC0097", + "classCode" => "TA", + "semester" => "2021.2", + "dicente" => [ + { + "nome" => "Ana Clara Jordao Perna", + "matricula" => "190084006", + "email" => "acjpjvjp@gmail.com" + } + ] + } + ]) + + expect do + described_class.new(path:, imported_by: admin).call + end.not_to change(User, :count) + + expect(User.where(registration: "190084006").count).to eq(1) + expect(User.where(email: "acjpjvjp@gmail.com").count).to eq(1) + expect(User.find_by!(registration: "190084006").course_classes.count).to eq(1) + end + + it "registra inconsistencia e continua processando os participantes validos" do + write_import_file([ + { + "code" => "CIC0097", + "classCode" => "TA", + "semester" => "2021.2", + "dicente" => [ + { + "nome" => "Participante Invalido", + "matricula" => "", + "email" => "" + }, + { + "nome" => "Ana Clara Jordao Perna", + "matricula" => "190084006", + "email" => "acjpjvjp@gmail.com" + } + ] + } + ]) + + result = described_class.new(path:, imported_by: admin).call + + expect(result.inconsistencies).to eq(1) + expect(ImportInconsistency.last.message).to eq("Participante sem email ou matricula") + expect(User.exists?(name: "Participante Invalido")).to be(false) + expect(User.exists?(registration: "190084006")).to be(true) + end + + def write_import_file(payload) + File.write(path, JSON.pretty_generate(payload)) + end +end diff --git a/project/spec/requests/formularios_spec.rb b/project/spec/requests/formularios_spec.rb new file mode 100644 index 0000000000..60deedd7ba --- /dev/null +++ b/project/spec/requests/formularios_spec.rb @@ -0,0 +1,72 @@ +require 'rails_helper' + +RSpec.describe "Formularios", type: :request do + describe "GET /formularios/:id/exportar_csv" do + it "retorna um arquivo CSV de sucesso com as respostas da avaliação" do + admin = create(:user, :admin) + formulario = create(:formulario, admin: admin) + + login_as(admin) + get exportar_csv_formulario_path(formulario, format: :csv) + + expect(response).to have_http_status(:success) + expect(response.media_type).to eq('text/csv') + expect(response.body).to include("ID da Pergunta", "Enunciado") + end + end + + describe "POST /formularios" do + it "cria um formulário a partir de um template e clona as questões do template" do + admin = create(:user, :admin) + template = create(:template, admin: admin) + create(:questao, template: template, enunciado: "O professor foi claro?") + course_class = create(:course_class) + + login_as(admin) + expect { + post formularios_path, params: { + formulario: { title: "Avaliação do Semestre", template_id: template.id, course_class_id: course_class.id } + } + }.to change(Formulario, :count).by(1).and change(Questao, :count).by(1) + + expect(response).to redirect_to(formularios_path) + follow_redirect! + expect(response.body).to include("Formulário gerado com sucesso!") + + formulario = Formulario.last + expect(formulario.admin_id).to eq(admin.id) + expect(formulario.questaos.first.enunciado).to eq("O professor foi claro?") + expect(formulario.questaos.first.template_id).to be_nil + end + + it "não cria o formulário quando nenhum template é selecionado" do + admin = create(:user, :admin) + course_class = create(:course_class) + + login_as(admin) + expect { + post formularios_path, params: { + formulario: { title: "Sem template", course_class_id: course_class.id } + } + }.not_to change(Formulario, :count) + + expect(response).to have_http_status(:unprocessable_content) + expect(response.body).to include("É necessário escolher um template base") + end + + it "não cria o formulário quando nenhuma turma é selecionada" do + admin = create(:user, :admin) + template = create(:template, :com_questao, admin: admin) + + login_as(admin) + expect { + post formularios_path, params: { + formulario: { title: "Sem turma", template_id: template.id } + } + }.not_to change(Formulario, :count) + + expect(response).to have_http_status(:unprocessable_content) + expect(response.body).to include("É necessário escolher ao menos uma turma") + end + end +end diff --git a/project/spec/requests/password_resets_spec.rb b/project/spec/requests/password_resets_spec.rb new file mode 100644 index 0000000000..db768336cb --- /dev/null +++ b/project/spec/requests/password_resets_spec.rb @@ -0,0 +1,52 @@ +require "rails_helper" + +RSpec.describe "Redefinicao de senha" do + let!(:user) do + create(:user, name: "Ana Clara Jordao Perna", email: "acjpjvjp@gmail.com", registration: "190084006", password: "Senha@123") + end + + it "redefine a senha com token valido e permite login com a nova senha" do + post password_resets_path, params: { password_reset: { email: "acjpjvjp@gmail.com" } } + + expect(response).to redirect_to(new_password_reset_path) + expect(ActionMailer::Base.deliveries.last.to).to include("acjpjvjp@gmail.com") + + token = extract_token_from(ActionMailer::Base.deliveries.last.body.encoded, "senha/redefinir") + patch password_reset_path(token), + params: { user: { password: "NovaSenha@123", password_confirmation: "NovaSenha@123" } } + + expect(response).to redirect_to(login_path) + expect(user.reload.authenticate("NovaSenha@123")).to eq(user) + + post login_path, params: { session: { identifier: "acjpjvjp@gmail.com", password: "NovaSenha@123" } } + expect(response).to redirect_to(root_path) + end + + it "nao revela se email inexistente esta cadastrado" do + expect do + post password_resets_path, params: { password_reset: { email: "naoexiste@unb.br" } } + end.not_to change { User.where.not(password_reset_token_digest: nil).count } + + expect(response).to redirect_to(new_password_reset_path) + follow_redirect! + expect(response.body).to include(PasswordResetsController::GENERIC_MESSAGE) + expect(ActionMailer::Base.deliveries).to be_empty + end + + it "nao redefine senha com token expirado" do + token = user.generate_password_reset_token! + user.update!(password_reset_sent_at: 3.hours.ago) + + patch password_reset_path(token), + params: { user: { password: "NovaSenha@123", password_confirmation: "NovaSenha@123" } } + + expect(response).to have_http_status(:not_found) + expect(response.body).to include("Link de redefinicao de senha invalido ou expirado") + expect(user.reload.authenticate("Senha@123")).to eq(user) + expect(user.authenticate("NovaSenha@123")).to be(false) + end + + def extract_token_from(body, segment) + body.match(%r{#{segment}/([^"\s<]+)})[1] + end +end diff --git a/project/spec/requests/password_setups_spec.rb b/project/spec/requests/password_setups_spec.rb new file mode 100644 index 0000000000..0a6c5ef19a --- /dev/null +++ b/project/spec/requests/password_setups_spec.rb @@ -0,0 +1,40 @@ +require "rails_helper" + +RSpec.describe "Definicao de senha" do + let(:user) do + create(:user, name: "Ana Clara Jordao Perna", email: "acjpjvjp@gmail.com", registration: "190084006", password: nil) + end + + it "define a primeira senha com link valido e permite login" do + token = user.generate_password_setup_token! + + patch password_setup_path(token), + params: { user: { password: "Senha@123", password_confirmation: "Senha@123" } } + + expect(response).to redirect_to(login_path) + user.reload + expect(user).not_to be_pending_password_setup + expect(user.authenticate("Senha@123")).to eq(user) + + post login_path, params: { session: { identifier: "190084006", password: "Senha@123" } } + expect(response).to redirect_to(root_path) + end + + it "nao permite definicao com link invalido" do + get edit_password_setup_path("token-invalido") + + expect(response).to have_http_status(:not_found) + expect(response.body).to include("Link de definicao de senha invalido ou expirado") + end + + it "nao salva senha quando a confirmacao e diferente" do + token = user.generate_password_setup_token! + + patch password_setup_path(token), + params: { user: { password: "Senha@123", password_confirmation: "OutraSenha@123" } } + + expect(response).to have_http_status(:unprocessable_content) + expect(response.body).to include("Confirmacao de senha nao confere") + expect(user.reload).to be_pending_password_setup + end +end diff --git a/project/spec/requests/respostas_spec.rb b/project/spec/requests/respostas_spec.rb new file mode 100644 index 0000000000..7b395cfdf0 --- /dev/null +++ b/project/spec/requests/respostas_spec.rb @@ -0,0 +1,31 @@ +require 'rails_helper' + +RSpec.describe "Respostas", type: :request do + describe "POST /resposta" do + it "cria uma nova resposta mapeando a nota corretamente para o atributo do banco" do + submissao = create(:submissao) + questao = create(:questao, :de_formulario, formulario: submissao.formulario) + + # Parâmetros usando o nome exato do Schema para sincronização perfeita + parametros = { + respostum: { + valor_numerico: 10, + valor_texto: "Ótima aula", + submissao_id: submissao.id, + questao_id: questao.id + } + } + + # Valida se o registro foi criado no banco + expect { + post "/resposta", params: parametros + }.to change(Respostum, :count).by(1) + + expect(response).to be_redirect + + # Garante a sincronização + nova_resposta = Respostum.last + expect(nova_resposta.valor_numerico).to eq(10) + end + end +end \ No newline at end of file diff --git a/project/spec/requests/sessions_spec.rb b/project/spec/requests/sessions_spec.rb new file mode 100644 index 0000000000..da5d8bbe3b --- /dev/null +++ b/project/spec/requests/sessions_spec.rb @@ -0,0 +1,41 @@ +require "rails_helper" + +RSpec.describe "Login" do + before do + create(:user, :admin, name: "Admin CIC", email: "admin.cic@unb.br", registration: "000000001") + create(:user, name: "Ana Clara Jordao Perna", email: "acjpjvjp@gmail.com", registration: "190084006", password: "Senha@123") + create(:user, name: "Andre Carvalho de Roure", email: "andreCarvalhoroure@gmail.com", registration: "200033522", password: nil) + end + + it "autentica administrador com email e exibe menu administrativo" do + post login_path, params: { session: { identifier: "admin.cic@unb.br", password: "Senha@123" } } + + expect(response).to redirect_to(root_path) + follow_redirect! + expect(response.body).to include("Gerenciar templates") + expect(response.body).to include("Gerenciar formularios") + end + + it "autentica participante com matricula e exibe menu de participante" do + post login_path, params: { session: { identifier: "190084006", password: "Senha@123" } } + + expect(response).to redirect_to(root_path) + follow_redirect! + expect(response.body).to include("Formularios pendentes") + expect(response.body).not_to include("Gerenciar templates") + end + + it "nao autentica usuario com senha incorreta" do + post login_path, params: { session: { identifier: "acjpjvjp@gmail.com", password: "SenhaErrada" } } + + expect(response).to have_http_status(:unprocessable_content) + expect(response.body).to include("Email, matricula ou senha invalidos") + end + + it "nao autentica usuario ainda sem senha definida" do + post login_path, params: { session: { identifier: "200033522", password: "Senha@123" } } + + expect(response).to have_http_status(:unprocessable_content) + expect(response.body).to include("Senha inicial precisa ser definida antes do acesso") + end +end diff --git a/project/spec/requests/sigaa_import_spec.rb b/project/spec/requests/sigaa_import_spec.rb new file mode 100644 index 0000000000..24cebda10a --- /dev/null +++ b/project/spec/requests/sigaa_import_spec.rb @@ -0,0 +1,47 @@ +require "rails_helper" + +RSpec.describe "Importacao de Dados do SIGAA", type: :request do + let!(:admin) { create(:user, :admin, department: create(:department, code: "CIC")) } + + it "exibe a tela de importacao para administradores autenticados" do + login_as(admin) + get imports_sigaa_path + + expect(response).to have_http_status(:success) + expect(response.body).to include("Importacao de Dados do SIGAA") + expect(response.body).to include("2021.2") + end + + it "sincroniza os dados do SIGAA e exibe mensagem de conclusao" do + login_as(admin) + + expect do + post imports_sigaa_path, params: { sigaa_sync: { semester: "2021.2" } } + end.to change(CourseClass, :count).by_at_least(1) + .and change(User.participant, :count).by_at_least(1) + + expect(response).to redirect_to(imports_sigaa_path) + follow_redirect! + expect(response.body).to match(/Sincronizacao concluida\. \d+ novos registros adicionados e \d+ registros atualizados\./) + end + + it "informa quando a base ja esta atualizada para o periodo" do + login_as(admin) + Sigaa::DataSynchronizer.new(semester: "2021.2", imported_by: admin).call + + post imports_sigaa_path, params: { sigaa_sync: { semester: "2021.2" } } + + expect(response).to redirect_to(imports_sigaa_path) + follow_redirect! + expect(response.body).to include("A base de dados ja esta atualizada com o SIGAA para este periodo.") + end + + it "restringe acesso a administradores" do + participant = create(:user) + login_as(participant) + + get imports_sigaa_path + + expect(response).to redirect_to(root_path) + end +end diff --git a/project/spec/requests/templates_spec.rb b/project/spec/requests/templates_spec.rb new file mode 100644 index 0000000000..17293e3a54 --- /dev/null +++ b/project/spec/requests/templates_spec.rb @@ -0,0 +1,185 @@ +require "rails_helper" + +RSpec.describe "Templates", type: :request do + describe "POST /templates" do + it "cria um template com uma questão e redireciona para a lista de templates" do + admin = create(:user, :admin) + login_as(admin) + + expect { + post templates_path, params: { + template: { + title: "Avaliação de Disciplina", + target_role: "discente", + questaos: { "0" => { enunciado: "O conteúdo foi claro?", tipo: "rating" } } + } + } + }.to change(Template, :count).by(1).and change(Questao, :count).by(1) + + expect(response).to redirect_to(templates_path) + + template = Template.last + expect(template.title).to eq("Avaliação de Disciplina") + expect(template.admin_id).to eq(admin.id) + expect(template.questaos.first.enunciado).to eq("O conteúdo foi claro?") + end + + it "não cria o template e exibe erro quando o nome está em branco" do + admin = create(:user, :admin) + login_as(admin) + + expect { + post templates_path, params: { + template: { + title: "", + target_role: "discente", + questaos: { "0" => { enunciado: "Pergunta válida", tipo: "rating" } } + } + } + }.not_to change(Template, :count) + + expect(response).to have_http_status(:unprocessable_content) + expect(response.body).to include("O nome do template é obrigatório") + end + + it "não cria o template e exibe erro quando a questão não possui enunciado" do + admin = create(:user, :admin) + login_as(admin) + + expect { + post templates_path, params: { + template: { + title: "Avaliação sem enunciado", + target_role: "discente", + questaos: { "0" => { enunciado: "", tipo: "rating" } } + } + } + }.not_to change(Template, :count) + + expect(response).to have_http_status(:unprocessable_content) + expect(response.body).to include("Questão não possui enunciado") + end + + it "adiciona um novo campo de questão sem persistir nada ao clicar em '+'" do + admin = create(:user, :admin) + login_as(admin) + template_count_before = Template.count + questao_count_before = Questao.count + + post templates_path, params: { + add_questao: "+", + template: { title: "Rascunho", target_role: "discente" } + } + + expect(response).to have_http_status(:ok) + expect(Template.count).to eq(template_count_before) + expect(Questao.count).to eq(questao_count_before) + expect(response.body).to include("template[questaos][0][enunciado]") + end + end + + describe "GET /templates" do + it "lista apenas os templates do administrador autenticado" do + admin = create(:user, :admin) + outro_admin = create(:user, :admin) + + meu_template = create(:template, :com_questao, admin: admin, title: "Meu Template") + create(:template, :com_questao, admin: outro_admin, title: "Template de Outro Admin") + + login_as(admin) + get templates_path + + expect(response).to have_http_status(:success) + expect(response.body).to include(meu_template.title) + expect(response.body).not_to include("Template de Outro Admin") + end + end + + describe "GET /templates/:id/edit" do + it "bloqueia o acesso a um template de outro administrador" do + admin = create(:user, :admin) + outro_admin = create(:user, :admin) + template_outro_admin = create(:template, :com_questao, admin: outro_admin) + + login_as(admin) + get edit_template_path(template_outro_admin) + + expect(response).to redirect_to(templates_path) + follow_redirect! + expect(response.body).to include("Você não tem acesso a esse template") + end + end + + describe "PATCH /templates/:id" do + it "atualiza o template sem afetar as questões dos formulários já gerados a partir dele" do + admin = create(:user, :admin) + template = create(:template, admin: admin) + create(:questao, template: template, enunciado: "Pergunta original") + course_class = create(:course_class) + + formulario = create(:formulario, admin: admin, template: template, course_class: course_class, title: "Formulário Gerado", target_role: template.target_role) + template.questaos.each { |questao| formulario.questaos.create!(enunciado: questao.enunciado, tipo: questao.tipo) } + + questao_existente = template.questaos.first + + login_as(admin) + patch template_path(template), params: { + template: { + title: "Título atualizado", + target_role: template.target_role, + questaos: { "0" => { id: questao_existente.id, enunciado: questao_existente.enunciado, tipo: questao_existente.tipo } } + } + } + + expect(response).to redirect_to(templates_path) + expect(template.reload.title).to eq("Título atualizado") + + formulario.reload + expect(formulario.questaos.first.enunciado).to eq("Pergunta original") + expect(formulario.questaos.first.template_id).to be_nil + end + + it "exige enunciado para novas questões adicionadas durante a edição" do + admin = create(:user, :admin) + template = create(:template, :com_questao, admin: admin) + questao_existente = template.questaos.first + + login_as(admin) + patch template_path(template), params: { + template: { + title: template.title, + target_role: template.target_role, + questaos: { + "0" => { id: questao_existente.id, enunciado: questao_existente.enunciado, tipo: questao_existente.tipo }, + "1" => { enunciado: "", tipo: "rating" } + } + } + } + + expect(response).to have_http_status(:unprocessable_content) + expect(response.body).to include("A nova questão não possui enunciado") + end + end + + describe "DELETE /templates/:id" do + it "remove o template, desvincula formulários gerados e mantém suas questões clonadas" do + admin = create(:user, :admin) + template = create(:template, :com_questao, admin: admin) + course_class = create(:course_class) + + formulario = create(:formulario, admin: admin, template: template, course_class: course_class, title: "Formulário Gerado", target_role: template.target_role) + template.questaos.each { |questao| formulario.questaos.create!(enunciado: questao.enunciado, tipo: questao.tipo) } + + login_as(admin) + expect { + delete template_path(template) + }.to change(Template, :count).by(-1) + + expect(response).to redirect_to(templates_path) + + formulario.reload + expect(formulario.template_id).to be_nil + expect(formulario.questaos.count).to eq(1) + end + end +end diff --git a/project/spec/services/sigaa/classes_importer_spec.rb b/project/spec/services/sigaa/classes_importer_spec.rb new file mode 100644 index 0000000000..a75077f132 --- /dev/null +++ b/project/spec/services/sigaa/classes_importer_spec.rb @@ -0,0 +1,74 @@ +require "rails_helper" + +RSpec.describe Sigaa::ClassesImporter do + let!(:admin) { create(:user, :admin) } + let(:path) { Rails.root.join("tmp", "spec_classes.json") } + let(:semester) { "2021.2" } + + after do + File.delete(path) if File.exist?(path) + end + + it "cadastra turmas novas do periodo informado" do + write_classes_file([ + { + "code" => "CIC0097", + "name" => "BANCOS DE DADOS", + "class" => { "classCode" => "TA", "semester" => "2021.2", "time" => "35T45" } + } + ]) + + result = described_class.new(path:, semester:, imported_by: admin).call + + course_class = CourseClass.find_by!(code: "CIC0097", class_code: "TA", semester: "2021.2") + expect(result.created_count).to eq(1) + expect(result.updated_count).to eq(0) + expect(course_class.name).to eq("BANCOS DE DADOS") + expect(course_class.time).to eq("35T45") + end + + it "atualiza turmas existentes quando os dados mudam no SIGAA" do + department = create(:department, code: "CIC") + CourseClass.create!( + code: "CIC0097", + class_code: "TA", + semester: "2021.2", + name: "Nome antigo", + time: "10:00", + department: + ) + + write_classes_file([ + { + "code" => "CIC0097", + "name" => "BANCOS DE DADOS", + "class" => { "classCode" => "TA", "semester" => "2021.2", "time" => "35T45" } + } + ]) + + result = described_class.new(path:, semester:, imported_by: admin).call + + expect(result.created_count).to eq(0) + expect(result.updated_count).to eq(1) + expect(CourseClass.find_by!(code: "CIC0097").name).to eq("BANCOS DE DADOS") + end + + it "ignora turmas de outros periodos" do + write_classes_file([ + { + "code" => "CIC0097", + "name" => "BANCOS DE DADOS", + "class" => { "classCode" => "TA", "semester" => "2026.1", "time" => "35T45" } + } + ]) + + result = described_class.new(path:, semester:, imported_by: admin).call + + expect(result.created_count).to eq(0) + expect(result.updated_count).to eq(0) + end + + def write_classes_file(payload) + File.write(path, JSON.pretty_generate(payload)) + end +end diff --git a/project/spec/services/sigaa/data_synchronizer_spec.rb b/project/spec/services/sigaa/data_synchronizer_spec.rb new file mode 100644 index 0000000000..bcae2e29c6 --- /dev/null +++ b/project/spec/services/sigaa/data_synchronizer_spec.rb @@ -0,0 +1,83 @@ +require "rails_helper" + +RSpec.describe Sigaa::DataSynchronizer do + let!(:admin) { create(:user, :admin) } + let(:classes_path) { Rails.root.join("tmp", "spec_sync_classes.json") } + let(:members_path) { Rails.root.join("tmp", "spec_sync_members.json") } + let(:semester) { "2021.2" } + + after do + File.delete(classes_path) if File.exist?(classes_path) + File.delete(members_path) if File.exist?(members_path) + end + + it "sincroniza turmas e participantes do periodo informado" do + write_classes_file([ + { + "code" => "CIC0097", + "name" => "BANCOS DE DADOS", + "class" => { "classCode" => "TA", "semester" => "2021.2", "time" => "35T45" } + } + ]) + write_members_file([ + { + "code" => "CIC0097", + "classCode" => "TA", + "semester" => "2021.2", + "dicente" => [ + { "nome" => "Ana Clara Jordao Perna", "matricula" => "190084006", "email" => "acjpjvjp@gmail.com" } + ], + "docente" => { + "nome" => "Maristela Terto de Holanda", + "usuario" => "83807519491", + "email" => "mholanda@unb.br" + } + } + ]) + + result = described_class.new(semester:, imported_by: admin, classes_path:, members_path:).call + + expect(result.already_up_to_date?).to be(false) + expect(result.created_count).to be >= 3 + expect(CourseClass.find_by!(code: "CIC0097", semester: "2021.2")).to be_present + expect(User.find_by!(registration: "190084006")).to be_present + expect(User.find_by!(registration: "83807519491")).to be_present + expect(ClassMembership.docente.count).to eq(1) + end + + it "informa que a base ja esta atualizada quando nao ha mudancas" do + write_classes_file([ + { + "code" => "CIC0097", + "name" => "BANCOS DE DADOS", + "class" => { "classCode" => "TA", "semester" => "2021.2", "time" => "35T45" } + } + ]) + write_members_file([ + { + "code" => "CIC0097", + "classCode" => "TA", + "semester" => "2021.2", + "dicente" => [ + { "nome" => "Ana Clara Jordao Perna", "matricula" => "190084006", "email" => "acjpjvjp@gmail.com" } + ] + } + ]) + + synchronizer = described_class.new(semester:, imported_by: admin, classes_path:, members_path:) + synchronizer.call + result = synchronizer.call + + expect(result.already_up_to_date?).to be(true) + expect(result.created_count).to eq(0) + expect(result.updated_count).to eq(0) + end + + def write_classes_file(payload) + File.write(classes_path, JSON.pretty_generate(payload)) + end + + def write_members_file(payload) + File.write(members_path, JSON.pretty_generate(payload)) + end +end diff --git a/project/spec/spec_helper.rb b/project/spec/spec_helper.rb new file mode 100644 index 0000000000..06e09a3093 --- /dev/null +++ b/project/spec/spec_helper.rb @@ -0,0 +1,11 @@ +RSpec.configure do |config| + config.expect_with :rspec do |expectations| + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + config.mock_with :rspec do |mocks| + mocks.verify_partial_doubles = true + end + + config.shared_context_metadata_behavior = :apply_to_host_groups +end diff --git a/project/spec/support/auth_helpers.rb b/project/spec/support/auth_helpers.rb new file mode 100644 index 0000000000..3cdb08ea4b --- /dev/null +++ b/project/spec/support/auth_helpers.rb @@ -0,0 +1,9 @@ +module AuthHelpers + def login_as(user, password: "Senha@123") + post login_path, params: { session: { identifier: user.email, password: } } + end +end + +RSpec.configure do |config| + config.include AuthHelpers +end diff --git a/project/storage/.keep b/project/storage/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/project/test/controllers/.keep b/project/test/controllers/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/project/test/controllers/admins_controller_test.rb b/project/test/controllers/admins_controller_test.rb new file mode 100644 index 0000000000..8580113c65 --- /dev/null +++ b/project/test/controllers/admins_controller_test.rb @@ -0,0 +1,48 @@ +require "test_helper" + +class AdminsControllerTest < ActionDispatch::IntegrationTest + setup do + @admin = admins(:one) + end + + test "should get index" do + get admins_url + assert_response :success + end + + test "should get new" do + get new_admin_url + assert_response :success + end + + test "should create admin" do + assert_difference("Admin.count") do + post admins_url, params: { admin: { departamento_id: @admin.departamento_id, email: @admin.email, name: @admin.name, password_digest: @admin.password_digest, username: @admin.username } } + end + + assert_redirected_to admin_url(Admin.last) + end + + test "should show admin" do + get admin_url(@admin) + assert_response :success + end + + test "should get edit" do + get edit_admin_url(@admin) + assert_response :success + end + + test "should update admin" do + patch admin_url(@admin), params: { admin: { departamento_id: @admin.departamento_id, email: @admin.email, name: @admin.name, password_digest: @admin.password_digest, username: @admin.username } } + assert_redirected_to admin_url(@admin) + end + + test "should destroy admin" do + assert_difference("Admin.count", -1) do + delete admin_url(@admin) + end + + assert_redirected_to admins_url + end +end diff --git a/project/test/controllers/alunos_controller_test.rb b/project/test/controllers/alunos_controller_test.rb new file mode 100644 index 0000000000..b99d6f547a --- /dev/null +++ b/project/test/controllers/alunos_controller_test.rb @@ -0,0 +1,48 @@ +require "test_helper" + +class AlunosControllerTest < ActionDispatch::IntegrationTest + setup do + @aluno = alunos(:one) + end + + test "should get index" do + get alunos_url + assert_response :success + end + + test "should get new" do + get new_aluno_url + assert_response :success + end + + test "should create aluno" do + assert_difference("Aluno.count") do + post alunos_url, params: { aluno: { course: @aluno.course, email: @aluno.email, matricula: @aluno.matricula, name: @aluno.name, password_digest: @aluno.password_digest } } + end + + assert_redirected_to aluno_url(Aluno.last) + end + + test "should show aluno" do + get aluno_url(@aluno) + assert_response :success + end + + test "should get edit" do + get edit_aluno_url(@aluno) + assert_response :success + end + + test "should update aluno" do + patch aluno_url(@aluno), params: { aluno: { course: @aluno.course, email: @aluno.email, matricula: @aluno.matricula, name: @aluno.name, password_digest: @aluno.password_digest } } + assert_redirected_to aluno_url(@aluno) + end + + test "should destroy aluno" do + assert_difference("Aluno.count", -1) do + delete aluno_url(@aluno) + end + + assert_redirected_to alunos_url + end +end diff --git a/project/test/controllers/departamentos_controller_test.rb b/project/test/controllers/departamentos_controller_test.rb new file mode 100644 index 0000000000..bcec0c556e --- /dev/null +++ b/project/test/controllers/departamentos_controller_test.rb @@ -0,0 +1,48 @@ +require "test_helper" + +class DepartamentosControllerTest < ActionDispatch::IntegrationTest + setup do + @departamento = departamentos(:one) + end + + test "should get index" do + get departamentos_url + assert_response :success + end + + test "should get new" do + get new_departamento_url + assert_response :success + end + + test "should create departamento" do + assert_difference("Departamento.count") do + post departamentos_url, params: { departamento: { code: @departamento.code, name: @departamento.name } } + end + + assert_redirected_to departamento_url(Departamento.last) + end + + test "should show departamento" do + get departamento_url(@departamento) + assert_response :success + end + + test "should get edit" do + get edit_departamento_url(@departamento) + assert_response :success + end + + test "should update departamento" do + patch departamento_url(@departamento), params: { departamento: { code: @departamento.code, name: @departamento.name } } + assert_redirected_to departamento_url(@departamento) + end + + test "should destroy departamento" do + assert_difference("Departamento.count", -1) do + delete departamento_url(@departamento) + end + + assert_redirected_to departamentos_url + end +end diff --git a/project/test/controllers/formularios_controller_test.rb b/project/test/controllers/formularios_controller_test.rb new file mode 100644 index 0000000000..26ad815e85 --- /dev/null +++ b/project/test/controllers/formularios_controller_test.rb @@ -0,0 +1,48 @@ +require "test_helper" + +class FormulariosControllerTest < ActionDispatch::IntegrationTest + setup do + @formulario = formularios(:one) + end + + test "should get index" do + get formularios_url + assert_response :success + end + + test "should get new" do + get new_formulario_url + assert_response :success + end + + test "should create formulario" do + assert_difference("Formulario.count") do + post formularios_url, params: { formulario: { admin_id: @formulario.admin_id, status: @formulario.status, target_role: @formulario.target_role, template_id: @formulario.template_id, title: @formulario.title, turma_id: @formulario.turma_id } } + end + + assert_redirected_to formularios_url + end + + test "should show formulario" do + get formulario_url(@formulario) + assert_response :success + end + + test "should get edit" do + get edit_formulario_url(@formulario) + assert_response :success + end + + test "should update formulario" do + patch formulario_url(@formulario), params: { formulario: { admin_id: @formulario.admin_id, status: @formulario.status, target_role: @formulario.target_role, template_id: @formulario.template_id, title: @formulario.title, turma_id: @formulario.turma_id } } + assert_redirected_to formulario_url(@formulario) + end + + test "should destroy formulario" do + assert_difference("Formulario.count", -1) do + delete formulario_url(@formulario) + end + + assert_redirected_to formularios_url + end +end diff --git a/project/test/controllers/professors_controller_test.rb b/project/test/controllers/professors_controller_test.rb new file mode 100644 index 0000000000..20f8d030ac --- /dev/null +++ b/project/test/controllers/professors_controller_test.rb @@ -0,0 +1,48 @@ +require "test_helper" + +class ProfessorsControllerTest < ActionDispatch::IntegrationTest + setup do + @professor = professors(:one) + end + + test "should get index" do + get professors_url + assert_response :success + end + + test "should get new" do + get new_professor_url + assert_response :success + end + + test "should create professor" do + assert_difference("Professor.count") do + post professors_url, params: { professor: { departamento_id: @professor.departamento_id, email: @professor.email, formation: @professor.formation, matricula: @professor.matricula, name: @professor.name, password_digest: @professor.password_digest } } + end + + assert_redirected_to professor_url(Professor.last) + end + + test "should show professor" do + get professor_url(@professor) + assert_response :success + end + + test "should get edit" do + get edit_professor_url(@professor) + assert_response :success + end + + test "should update professor" do + patch professor_url(@professor), params: { professor: { departamento_id: @professor.departamento_id, email: @professor.email, formation: @professor.formation, matricula: @professor.matricula, name: @professor.name, password_digest: @professor.password_digest } } + assert_redirected_to professor_url(@professor) + end + + test "should destroy professor" do + assert_difference("Professor.count", -1) do + delete professor_url(@professor) + end + + assert_redirected_to professors_url + end +end diff --git a/project/test/controllers/questaos_controller_test.rb b/project/test/controllers/questaos_controller_test.rb new file mode 100644 index 0000000000..e7048a010b --- /dev/null +++ b/project/test/controllers/questaos_controller_test.rb @@ -0,0 +1,48 @@ +require "test_helper" + +class QuestaosControllerTest < ActionDispatch::IntegrationTest + setup do + @questao = questaos(:one) + end + + test "should get index" do + get questaos_url + assert_response :success + end + + test "should get new" do + get new_questao_url + assert_response :success + end + + test "should create questao" do + assert_difference("Questao.count") do + post questaos_url, params: { questao: { enunciado: @questao.enunciado, formulario_id: @questao.formulario_id, template_id: @questao.template_id, tipo: @questao.tipo } } + end + + assert_redirected_to questao_url(Questao.last) + end + + test "should show questao" do + get questao_url(@questao) + assert_response :success + end + + test "should get edit" do + get edit_questao_url(@questao) + assert_response :success + end + + test "should update questao" do + patch questao_url(@questao), params: { questao: { enunciado: @questao.enunciado, formulario_id: @questao.formulario_id, template_id: @questao.template_id, tipo: @questao.tipo } } + assert_redirected_to questao_url(@questao) + end + + test "should destroy questao" do + assert_difference("Questao.count", -1) do + delete questao_url(@questao) + end + + assert_redirected_to questaos_url + end +end diff --git a/project/test/controllers/resposta_controller_test.rb b/project/test/controllers/resposta_controller_test.rb new file mode 100644 index 0000000000..44d839dbfe --- /dev/null +++ b/project/test/controllers/resposta_controller_test.rb @@ -0,0 +1,48 @@ +require "test_helper" + +class RespostaControllerTest < ActionDispatch::IntegrationTest + setup do + @respostum = resposta(:one) + end + + test "should get index" do + get resposta_url + assert_response :success + end + + test "should get new" do + get new_respostum_url + assert_response :success + end + + test "should create respostum" do + assert_difference("Respostum.count") do + post resposta_url, params: { respostum: { questao_id: @respostum.questao_id, submissao_id: @respostum.submissao_id, valor_numerico: @respostum.valor_numerico, valor_texto: @respostum.valor_texto } } + end + + assert_redirected_to respostum_url(Respostum.last) + end + + test "should show respostum" do + get respostum_url(@respostum) + assert_response :success + end + + test "should get edit" do + get edit_respostum_url(@respostum) + assert_response :success + end + + test "should update respostum" do + patch respostum_url(@respostum), params: { respostum: { questao_id: @respostum.questao_id, submissao_id: @respostum.submissao_id, valor_numerico: @respostum.valor_numerico, valor_texto: @respostum.valor_texto } } + assert_redirected_to respostum_url(@respostum) + end + + test "should destroy respostum" do + assert_difference("Respostum.count", -1) do + delete respostum_url(@respostum) + end + + assert_redirected_to resposta_url + end +end diff --git a/project/test/controllers/submissaos_controller_test.rb b/project/test/controllers/submissaos_controller_test.rb new file mode 100644 index 0000000000..9fcf24d8d3 --- /dev/null +++ b/project/test/controllers/submissaos_controller_test.rb @@ -0,0 +1,48 @@ +require "test_helper" + +class SubmissaosControllerTest < ActionDispatch::IntegrationTest + setup do + @submissao = submissaos(:one) + end + + test "should get index" do + get submissaos_url + assert_response :success + end + + test "should get new" do + get new_submissao_url + assert_response :success + end + + test "should create submissao" do + assert_difference("Submissao.count") do + post submissaos_url, params: { submissao: { formulario_id: @submissao.formulario_id, participant_id: @submissao.participant_id, participant_type: @submissao.participant_type } } + end + + assert_redirected_to submissao_url(Submissao.last) + end + + test "should show submissao" do + get submissao_url(@submissao) + assert_response :success + end + + test "should get edit" do + get edit_submissao_url(@submissao) + assert_response :success + end + + test "should update submissao" do + patch submissao_url(@submissao), params: { submissao: { formulario_id: @submissao.formulario_id, participant_id: @submissao.participant_id, participant_type: @submissao.participant_type } } + assert_redirected_to submissao_url(@submissao) + end + + test "should destroy submissao" do + assert_difference("Submissao.count", -1) do + delete submissao_url(@submissao) + end + + assert_redirected_to submissaos_url + end +end diff --git a/project/test/controllers/templates_controller_test.rb b/project/test/controllers/templates_controller_test.rb new file mode 100644 index 0000000000..4568eadea4 --- /dev/null +++ b/project/test/controllers/templates_controller_test.rb @@ -0,0 +1,48 @@ +require "test_helper" + +class TemplatesControllerTest < ActionDispatch::IntegrationTest + setup do + @template = templates(:one) + end + + test "should get index" do + get templates_url + assert_response :success + end + + test "should get new" do + get new_template_url + assert_response :success + end + + test "should create template" do + assert_difference("Template.count") do + post templates_url, params: { template: { admin_id: @template.admin_id, target_role: @template.target_role, title: @template.title } } + end + + assert_redirected_to templates_url + end + + test "should show template" do + get template_url(@template) + assert_response :success + end + + test "should get edit" do + get edit_template_url(@template) + assert_response :success + end + + test "should update template" do + patch template_url(@template), params: { template: { admin_id: @template.admin_id, target_role: @template.target_role, title: @template.title } } + assert_redirected_to templates_url + end + + test "should destroy template" do + assert_difference("Template.count", -1) do + delete template_url(@template) + end + + assert_redirected_to templates_url + end +end diff --git a/project/test/controllers/turmas_controller_test.rb b/project/test/controllers/turmas_controller_test.rb new file mode 100644 index 0000000000..0602a4a51d --- /dev/null +++ b/project/test/controllers/turmas_controller_test.rb @@ -0,0 +1,48 @@ +require "test_helper" + +class TurmasControllerTest < ActionDispatch::IntegrationTest + setup do + @turma = turmas(:one) + end + + test "should get index" do + get turmas_url + assert_response :success + end + + test "should get new" do + get new_turma_url + assert_response :success + end + + test "should create turma" do + assert_difference("Turma.count") do + post turmas_url, params: { turma: { class_code: @turma.class_code, departamento_id: @turma.departamento_id, professor_id: @turma.professor_id, semester: @turma.semester, subject_code: @turma.subject_code, subject_name: @turma.subject_name, time: @turma.time } } + end + + assert_redirected_to turma_url(Turma.last) + end + + test "should show turma" do + get turma_url(@turma) + assert_response :success + end + + test "should get edit" do + get edit_turma_url(@turma) + assert_response :success + end + + test "should update turma" do + patch turma_url(@turma), params: { turma: { class_code: @turma.class_code, departamento_id: @turma.departamento_id, professor_id: @turma.professor_id, semester: @turma.semester, subject_code: @turma.subject_code, subject_name: @turma.subject_name, time: @turma.time } } + assert_redirected_to turma_url(@turma) + end + + test "should destroy turma" do + assert_difference("Turma.count", -1) do + delete turma_url(@turma) + end + + assert_redirected_to turmas_url + end +end diff --git a/project/test/fixtures/admins.yml b/project/test/fixtures/admins.yml new file mode 100644 index 0000000000..ead3a962d6 --- /dev/null +++ b/project/test/fixtures/admins.yml @@ -0,0 +1,15 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + username: MyString + name: MyString + email: MyString + password_digest: MyString + departamento: one + +two: + username: MyString + name: MyString + email: MyString + password_digest: MyString + departamento: two diff --git a/project/test/fixtures/alunos.yml b/project/test/fixtures/alunos.yml new file mode 100644 index 0000000000..436a123e1b --- /dev/null +++ b/project/test/fixtures/alunos.yml @@ -0,0 +1,15 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + matricula: MyString + name: MyString + email: MyString + password_digest: MyString + course: MyString + +two: + matricula: MyString + name: MyString + email: MyString + password_digest: MyString + course: MyString diff --git a/project/test/fixtures/departamentos.yml b/project/test/fixtures/departamentos.yml new file mode 100644 index 0000000000..ee775db7f8 --- /dev/null +++ b/project/test/fixtures/departamentos.yml @@ -0,0 +1,9 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + name: MyString + code: MyString + +two: + name: MyString + code: MyString diff --git a/project/test/fixtures/files/.keep b/project/test/fixtures/files/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/project/test/fixtures/formularios.yml b/project/test/fixtures/formularios.yml new file mode 100644 index 0000000000..d923f00d76 --- /dev/null +++ b/project/test/fixtures/formularios.yml @@ -0,0 +1,17 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + title: MyString + target_role: MyString + status: MyString + template: one + turma: one + admin: one + +two: + title: MyString + target_role: MyString + status: MyString + template: two + turma: two + admin: two diff --git a/project/test/fixtures/professors.yml b/project/test/fixtures/professors.yml new file mode 100644 index 0000000000..9b9c9fb5d2 --- /dev/null +++ b/project/test/fixtures/professors.yml @@ -0,0 +1,17 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + matricula: MyString + name: MyString + email: MyString + password_digest: MyString + formation: MyString + departamento: one + +two: + matricula: MyString + name: MyString + email: MyString + password_digest: MyString + formation: MyString + departamento: two diff --git a/project/test/fixtures/questaos.yml b/project/test/fixtures/questaos.yml new file mode 100644 index 0000000000..81fe4d9782 --- /dev/null +++ b/project/test/fixtures/questaos.yml @@ -0,0 +1,13 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + enunciado: MyText + tipo: MyString + template: one + formulario: one + +two: + enunciado: MyText + tipo: MyString + template: two + formulario: two diff --git a/project/test/fixtures/resposta.yml b/project/test/fixtures/resposta.yml new file mode 100644 index 0000000000..355094eb02 --- /dev/null +++ b/project/test/fixtures/resposta.yml @@ -0,0 +1,13 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + valor_texto: MyText + valor_numerico: 1 + submissao: one + questao: one + +two: + valor_texto: MyText + valor_numerico: 1 + submissao: two + questao: two diff --git a/project/test/fixtures/submissaos.yml b/project/test/fixtures/submissaos.yml new file mode 100644 index 0000000000..ac34ae408e --- /dev/null +++ b/project/test/fixtures/submissaos.yml @@ -0,0 +1,11 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + formulario: one + participant: one + participant_type: Participant + +two: + formulario: two + participant: two + participant_type: Participant diff --git a/project/test/fixtures/templates.yml b/project/test/fixtures/templates.yml new file mode 100644 index 0000000000..eccf8371b2 --- /dev/null +++ b/project/test/fixtures/templates.yml @@ -0,0 +1,11 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + title: MyString + target_role: MyString + admin: one + +two: + title: MyString + target_role: MyString + admin: two diff --git a/project/test/fixtures/turma_alunos.yml b/project/test/fixtures/turma_alunos.yml new file mode 100644 index 0000000000..bec55c7985 --- /dev/null +++ b/project/test/fixtures/turma_alunos.yml @@ -0,0 +1,9 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + aluno: one + turma: one + +two: + aluno: two + turma: two diff --git a/project/test/fixtures/turmas.yml b/project/test/fixtures/turmas.yml new file mode 100644 index 0000000000..6298f5b41c --- /dev/null +++ b/project/test/fixtures/turmas.yml @@ -0,0 +1,19 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + subject_code: MyString + subject_name: MyString + class_code: MyString + semester: MyString + time: MyString + departamento: one + professor: one + +two: + subject_code: MyString + subject_name: MyString + class_code: MyString + semester: MyString + time: MyString + departamento: two + professor: two diff --git a/project/test/helpers/.keep b/project/test/helpers/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/project/test/integration/.keep b/project/test/integration/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/project/test/mailers/.keep b/project/test/mailers/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/project/test/models/.keep b/project/test/models/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/project/test/models/admin_test.rb b/project/test/models/admin_test.rb new file mode 100644 index 0000000000..33dd1944a1 --- /dev/null +++ b/project/test/models/admin_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class AdminTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/project/test/models/aluno_test.rb b/project/test/models/aluno_test.rb new file mode 100644 index 0000000000..4e070b6ffc --- /dev/null +++ b/project/test/models/aluno_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class AlunoTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/project/test/models/departamento_test.rb b/project/test/models/departamento_test.rb new file mode 100644 index 0000000000..15a03e49ef --- /dev/null +++ b/project/test/models/departamento_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class DepartamentoTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/project/test/models/formulario_test.rb b/project/test/models/formulario_test.rb new file mode 100644 index 0000000000..b3d3e2cc7f --- /dev/null +++ b/project/test/models/formulario_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class FormularioTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/project/test/models/professor_test.rb b/project/test/models/professor_test.rb new file mode 100644 index 0000000000..42dc05ee40 --- /dev/null +++ b/project/test/models/professor_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class ProfessorTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/project/test/models/questao_test.rb b/project/test/models/questao_test.rb new file mode 100644 index 0000000000..63b5513595 --- /dev/null +++ b/project/test/models/questao_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class QuestaoTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/project/test/models/respostum_test.rb b/project/test/models/respostum_test.rb new file mode 100644 index 0000000000..03ba117c95 --- /dev/null +++ b/project/test/models/respostum_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class RespostumTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/project/test/models/submissao_test.rb b/project/test/models/submissao_test.rb new file mode 100644 index 0000000000..1c54e16944 --- /dev/null +++ b/project/test/models/submissao_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class SubmissaoTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/project/test/models/template_test.rb b/project/test/models/template_test.rb new file mode 100644 index 0000000000..5bc4995c31 --- /dev/null +++ b/project/test/models/template_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class TemplateTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/project/test/models/turma_aluno_test.rb b/project/test/models/turma_aluno_test.rb new file mode 100644 index 0000000000..e2ebd6d30f --- /dev/null +++ b/project/test/models/turma_aluno_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class TurmaAlunoTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/project/test/models/turma_test.rb b/project/test/models/turma_test.rb new file mode 100644 index 0000000000..21806827d6 --- /dev/null +++ b/project/test/models/turma_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class TurmaTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/project/test/test_helper.rb b/project/test/test_helper.rb new file mode 100644 index 0000000000..0c22470ec1 --- /dev/null +++ b/project/test/test_helper.rb @@ -0,0 +1,15 @@ +ENV["RAILS_ENV"] ||= "test" +require_relative "../config/environment" +require "rails/test_help" + +module ActiveSupport + class TestCase + # Run tests in parallel with specified workers + parallelize(workers: :number_of_processors) + + # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. + fixtures :all + + # Add more helper methods to be used by all tests here... + end +end diff --git a/project/vendor/.keep b/project/vendor/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/project/vendor/javascript/.keep b/project/vendor/javascript/.keep new file mode 100644 index 0000000000..e69de29bb2