diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..325bfc036d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,51 @@ +# See https://docs.docker.com/engine/reference/builder/#dockerignore-file for more about ignoring files. + +# Ignore git directory. +/.git/ +/.gitignore + +# Ignore bundler config. +/.bundle + +# Ignore all environment files. +/.env* + +# Ignore all default key files. +/config/master.key +/config/credentials/*.key + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/.keep + +# Ignore storage (uploaded files in development and any SQLite databases). +/storage/* +!/storage/.keep +/tmp/storage/* +!/tmp/storage/.keep + +# Ignore assets. +/node_modules/ +/app/assets/builds/* +!/app/assets/builds/.keep +/public/assets + +# Ignore CI service files. +/.github + +# Ignore Kamal files. +/config/deploy*.yml +/.kamal + +# Ignore development files +/.devcontainer + +# Ignore Docker-related files +/.dockerignore +/Dockerfile* diff --git a/.gitgnore b/.gitgnore new file mode 100644 index 0000000000..d3a75ab722 --- /dev/null +++ b/.gitgnore @@ -0,0 +1,75 @@ +# Created by https://www.toptal.com/developers/gitignore/api/rails +# Edit at https://www.toptal.com/developers/gitignore?templates=rails + +### Rails ### +*.rbc +capybara-*.html +.rspec +/db/*.sqlite3 +/db/*.sqlite3-journal +/db/*.sqlite3-[0-9]* +/public/system +/coverage/ +/spec/tmp +*.orig +rerun.txt +pickle-email-*.html + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# TODO Comment out this rule if you are OK with secrets being uploaded to the repo +config/initializers/secret_token.rb +config/master.key + +# Only include if you have production secrets in this file, which is no longer a Rails default +# config/secrets.yml + +# dotenv, dotenv-rails +# TODO Comment out these rules if environment variables can be committed +.env +.env*.local + +## Environment normalization: +/.bundle +/vendor/bundle + +# these should all be checked in to normalize the environment: +# Gemfile.lock, .ruby-version, .ruby-gemset + +# unless supporting rvm < 1.11.0 or doing something fancy, ignore this: +.rvmrc + +# if using bower-rails ignore default bower_components path bower.json files +/vendor/assets/bower_components +*.bowerrc +bower.json + +# Ignore pow environment settings +.powenv + +# Ignore Byebug command history file. +.byebug_history + +# Ignore node_modules +node_modules/ + +# Ignore precompiled javascript packs +/public/packs +/public/packs-test +/public/assets + +# Ignore yarn files +/yarn-error.log +yarn-debug.log* +.yarn-integrity + +# Ignore uploaded files in development +/storage/* +!/storage/.keep +/public/uploads + +# End of https://www.toptal.com/developers/gitignore/api/rails \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..83610cfa4c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: +- package-ecosystem: bundler + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 10 +- package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 10 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000..0514d9c999 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,124 @@ +name: CI + +on: + pull_request: + push: + branches: [ master ] + +jobs: + scan_ruby: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - 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@v5 + + - 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@v5 + + - 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: Checkout code + uses: actions/checkout@v5 + + - 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: Checkout code + uses: actions/checkout@v5 + + - 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..161df85dc1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,63 @@ +# See https://help.github.com/articles/ignoring-files for more about ignoring files. +# +# If you find yourself ignoring temporary files generated by your text editor +# or operating system, you probably want to add a global ignore instead: +# git config --global core.excludesfile '~/.gitignore_global' + +# Ignore bundler config. +/.bundle + +# Ignore all environment files (except templates). +/.env* +!/.env*.erb + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep +!/tmp/pids/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/ +!/tmp/pids/.keep + +# Ignore storage (uploaded files in development and any SQLite databases). +/storage/* +!/storage/.keep +/tmp/storage/* +!/tmp/storage/ +!/tmp/storage/.keep + +# Ignore master key for decrypting credentials and more. +/config/master.key + +# Ignore assets. +/app/assets/builds/* +!/app/assets/builds/.keep +/public/assets + +# Ignore CI service files. +/.github + +# Ignore development database files +*.sqlite3 +*.sqlite3-journal +*.sqlite3-shm +*.sqlite3-wal + +# Ignore bootsnap cache +/tmp/cache/ + +# Ignore node_modules +/node_modules + +# Ignore yarn files +/yarn-error.log +yarn-debug.log* +.yarn-integrity + +# Ignore uploaded files in development +/storage/development.sqlite3 +/storage/test.sqlite3 diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000000..f9d86d4a54 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,8 @@ +# Omakase Ruby styling for Rails +inherit_gem: { rubocop-rails-omakase: rubocop.yml } + +# Overwrite or add rules to create your own house style +# +# # Use `[a, [b, c]]` not `[ a, [ b, c ] ]` +# Layout/SpaceInsideArrayLiteralBrackets: +# Enabled: false diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000000..408069ae67 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +ruby-3.4.1 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..f63ab2f47b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,76 @@ +# syntax=docker/dockerfile:1 +# check=error=true + +# This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand: +# docker build -t camaar . +# docker run -d -p 80:80 -e RAILS_MASTER_KEY= --name camaar camaar + +# 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.1 +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 libyaml-dev pkg-config && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Install application gems +COPY Gemfile Gemfile.lock vendor ./ + +RUN bundle install && \ + rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \ + # -j 1 disable parallel compilation to avoid a QEMU bug: https://github.com/rails/bootsnap/issues/495 + bundle exec bootsnap precompile -j 1 --gemfile + +# Copy application code +COPY . . + +# Precompile bootsnap code for faster boot times. +# -j 1 disable parallel compilation to avoid a QEMU bug: https://github.com/rails/bootsnap/issues/495 +RUN bundle exec bootsnap precompile -j 1 app/ lib/ + +# Precompiling assets for production without requiring secret RAILS_MASTER_KEY +RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile + + + + +# Final stage for app image +FROM base + +# Run and own only the runtime files as a non-root user for security +RUN groupadd --system --gid 1000 rails && \ + useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash +USER 1000:1000 + +# Copy built artifacts: gems, application +COPY --chown=rails:rails --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}" +COPY --chown=rails:rails --from=build /rails /rails + +# Entrypoint prepares the database. +ENTRYPOINT ["/rails/bin/docker-entrypoint"] + +# Start server via Thruster by default, this can be overwritten at runtime +EXPOSE 80 +CMD ["./bin/thrust", "./bin/rails", "server"] diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000000..f0798f4279 --- /dev/null +++ b/Gemfile @@ -0,0 +1,71 @@ +source "https://rubygems.org" + +# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" +gem "rails", "~> 8.1.1" +# The modern asset pipeline for Rails [https://github.com/rails/propshaft] +gem "propshaft" +# Use sqlite3 as the database for Active Record +gem "sqlite3", ">= 2.1" +# Use the Puma web server [https://github.com/puma/puma] +gem "puma", ">= 5.0" +# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails] +gem "importmap-rails" +# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev] +gem "turbo-rails" +# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev] +gem "stimulus-rails" +# Build JSON APIs with ease [https://github.com/rails/jbuilder] +gem "jbuilder" + +# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] +# gem "bcrypt", "~> 3.1.7" + +# Windows does not include zoneinfo files, so bundle the tzinfo-data gem +gem "tzinfo-data", platforms: %i[ windows jruby ] + +# Use the database-backed adapters for Rails.cache, Active Job, and Action Cable +gem "solid_cache" +gem "solid_queue" +gem "solid_cable" + +# Reduces boot times through caching; required in config/boot.rb +gem "bootsnap", require: false + +# Deploy this application anywhere as a Docker container [https://kamal-deploy.org] +gem "kamal", require: false + +# Add HTTP asset caching/compression and X-Sendfile acceleration to Puma [https://github.com/basecamp/thruster/] +gem "thruster", require: false + +# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] +gem "image_processing", "~> 1.2" + +group :development, :test do + # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem + gem "debug", platforms: %i[ mri windows ], require: "debug/prelude" + + # Audits gems for known security defects (use config/bundler-audit.yml to ignore issues) + gem "bundler-audit", require: false + + # Static analysis for security vulnerabilities [https://brakemanscanner.org/] + gem "brakeman", require: false + + # Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/] + gem "rubocop-rails-omakase", require: false +end + +group :development do + # Use console on exceptions pages [https://github.com/rails/web-console] + gem "web-console" +end + +group :test do + # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing] + gem "capybara" + gem "selenium-webdriver" +end + +group :test do + gem "cucumber-rails", "~> 4.0", require: false + gem "database_cleaner-active_record", "~> 2.2" +end diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000000..d87f091176 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,449 @@ +GEM + remote: https://rubygems.org/ + specs: + action_text-trix (2.1.15) + railties + actioncable (8.1.1) + actionpack (= 8.1.1) + activesupport (= 8.1.1) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + zeitwerk (~> 2.6) + actionmailbox (8.1.1) + actionpack (= 8.1.1) + activejob (= 8.1.1) + activerecord (= 8.1.1) + activestorage (= 8.1.1) + activesupport (= 8.1.1) + mail (>= 2.8.0) + actionmailer (8.1.1) + actionpack (= 8.1.1) + actionview (= 8.1.1) + activejob (= 8.1.1) + activesupport (= 8.1.1) + mail (>= 2.8.0) + rails-dom-testing (~> 2.2) + actionpack (8.1.1) + actionview (= 8.1.1) + activesupport (= 8.1.1) + 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.1) + action_text-trix (~> 2.1.15) + actionpack (= 8.1.1) + activerecord (= 8.1.1) + activestorage (= 8.1.1) + activesupport (= 8.1.1) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (8.1.1) + activesupport (= 8.1.1) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (8.1.1) + activesupport (= 8.1.1) + globalid (>= 0.3.6) + activemodel (8.1.1) + activesupport (= 8.1.1) + activerecord (8.1.1) + activemodel (= 8.1.1) + activesupport (= 8.1.1) + timeout (>= 0.4.0) + activestorage (8.1.1) + actionpack (= 8.1.1) + activejob (= 8.1.1) + activerecord (= 8.1.1) + activesupport (= 8.1.1) + marcel (~> 1.0) + activesupport (8.1.1) + 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.8.7) + public_suffix (>= 2.0.2, < 7.0) + ast (2.4.3) + base64 (0.3.0) + bcrypt_pbkdf (1.1.1) + bigdecimal (3.3.1) + bindex (0.8.1) + bootsnap (1.19.0) + msgpack (~> 1.2) + brakeman (7.1.1) + racc + builder (3.3.0) + bundler-audit (0.9.2) + bundler (>= 1.2.0, < 3) + 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.5) + connection_pool (2.5.4) + crass (1.0.6) + cucumber (10.1.1) + base64 (~> 0.2) + builder (~> 3.2) + cucumber-ci-environment (> 9, < 11) + cucumber-core (> 15, < 17) + cucumber-cucumber-expressions (> 17, < 19) + cucumber-html-formatter (> 20.3, < 22) + diff-lcs (~> 1.5) + logger (~> 1.6) + mini_mime (~> 1.1) + multi_test (~> 1.1) + sys-uname (~> 1.3) + cucumber-ci-environment (10.0.1) + cucumber-core (15.3.0) + cucumber-gherkin (> 27, < 35) + cucumber-messages (> 26, < 30) + cucumber-tag-expressions (> 5, < 9) + cucumber-cucumber-expressions (18.0.1) + bigdecimal + cucumber-gherkin (34.0.0) + cucumber-messages (> 25, < 29) + cucumber-html-formatter (21.15.1) + cucumber-messages (> 19, < 28) + cucumber-messages (27.2.0) + cucumber-rails (4.0.0) + capybara (>= 3.25, < 4) + cucumber (>= 7, < 11) + railties (>= 6.1, < 9) + cucumber-tag-expressions (8.0.0) + database_cleaner-active_record (2.2.2) + activerecord (>= 5.a) + database_cleaner-core (~> 2.0) + database_cleaner-core (2.0.1) + date (3.5.0) + debug (1.11.0) + irb (~> 1.10) + reline (>= 0.3.8) + diff-lcs (1.6.2) + dotenv (3.1.8) + drb (2.2.3) + ed25519 (1.4.0) + erb (6.0.0) + erubi (1.13.1) + et-orbi (1.4.0) + tzinfo + ffi (1.17.2-aarch64-linux-gnu) + ffi (1.17.2-aarch64-linux-musl) + ffi (1.17.2-arm-linux-gnu) + ffi (1.17.2-arm-linux-musl) + ffi (1.17.2-x86_64-linux-gnu) + ffi (1.17.2-x86_64-linux-musl) + fugit (1.12.1) + et-orbi (~> 1.4) + raabro (~> 1.4) + globalid (1.3.0) + activesupport (>= 6.1) + i18n (1.14.7) + 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.2) + actionpack (>= 6.0.0) + activesupport (>= 6.0.0) + railties (>= 6.0.0) + io-console (0.8.1) + irb (1.15.3) + pp (>= 0.6.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + jbuilder (2.14.1) + actionview (>= 7.0.0) + activesupport (>= 7.0.0) + json (2.16.0) + kamal (2.8.2) + activesupport (>= 7.0) + base64 (~> 0.2) + bcrypt_pbkdf (~> 1.0) + concurrent-ruby (~> 1.2) + dotenv (~> 3.1) + ed25519 (~> 1.4) + net-ssh (~> 7.3) + sshkit (>= 1.23.0, < 2.0) + thor (~> 1.3) + zeitwerk (>= 2.6.18, < 3.0) + language_server-protocol (3.17.0.5) + lint_roller (1.1.0) + logger (1.7.0) + loofah (2.24.1) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mail (2.9.0) + logger + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + marcel (1.1.0) + matrix (0.4.3) + memoist3 (1.0.0) + mini_magick (5.3.1) + logger + mini_mime (1.1.5) + minitest (5.26.1) + msgpack (1.8.0) + multi_test (1.1.0) + net-imap (0.5.12) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-scp (4.1.0) + net-ssh (>= 2.6.5, < 8.0.0) + net-sftp (4.0.0) + net-ssh (>= 5.0.0, < 8.0.0) + net-smtp (0.5.1) + net-protocol + net-ssh (7.3.0) + nio4r (2.7.5) + nokogiri (1.18.10-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.10-aarch64-linux-musl) + racc (~> 1.4) + nokogiri (1.18.10-arm-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.10-arm-linux-musl) + racc (~> 1.4) + nokogiri (1.18.10-x86_64-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.10-x86_64-linux-musl) + racc (~> 1.4) + ostruct (0.6.3) + parallel (1.27.0) + parser (3.3.10.0) + ast (~> 2.4.1) + racc + pp (0.6.3) + prettyprint + prettyprint (0.2.0) + prism (1.6.0) + propshaft (1.3.1) + actionpack (>= 7.0.0) + activesupport (>= 7.0.0) + rack + psych (5.2.6) + date + stringio + public_suffix (6.0.2) + puma (7.1.0) + nio4r (~> 2.0) + raabro (1.4.0) + racc (1.8.1) + rack (3.2.4) + rack-session (2.1.1) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rack-test (2.2.0) + rack (>= 1.3) + rackup (2.2.1) + rack (>= 3) + rails (8.1.1) + actioncable (= 8.1.1) + actionmailbox (= 8.1.1) + actionmailer (= 8.1.1) + actionpack (= 8.1.1) + actiontext (= 8.1.1) + actionview (= 8.1.1) + activejob (= 8.1.1) + activemodel (= 8.1.1) + activerecord (= 8.1.1) + activestorage (= 8.1.1) + activesupport (= 8.1.1) + bundler (>= 1.15.0) + railties (= 8.1.1) + rails-dom-testing (2.3.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.6.2) + loofah (~> 2.21) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + railties (8.1.1) + actionpack (= 8.1.1) + activesupport (= 8.1.1) + irb (~> 1.13) + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + tsort (>= 0.2) + zeitwerk (~> 2.6) + rainbow (3.1.1) + rake (13.3.1) + rdoc (6.15.1) + erb + psych (>= 4.0.0) + tsort + regexp_parser (2.11.3) + reline (0.6.3) + io-console (~> 0.5) + rexml (3.4.4) + rubocop (1.81.7) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.47.1, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.48.0) + parser (>= 3.3.7.2) + prism (~> 1.4) + rubocop-performance (1.26.1) + lint_roller (~> 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.47.1, < 2.0) + rubocop-rails (2.34.0) + 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.2.5) + ffi (~> 1.12) + logger + rubyzip (3.2.2) + securerandom (0.4.1) + selenium-webdriver (4.38.0) + base64 (~> 0.2) + logger (~> 1.4) + rexml (~> 3.2, >= 3.2.5) + rubyzip (>= 1.2.2, < 4.0) + websocket (~> 1.0) + solid_cable (3.0.12) + actioncable (>= 7.2) + activejob (>= 7.2) + activerecord (>= 7.2) + railties (>= 7.2) + solid_cache (1.0.10) + activejob (>= 7.2) + activerecord (>= 7.2) + railties (>= 7.2) + solid_queue (1.2.4) + activejob (>= 7.1) + activerecord (>= 7.1) + concurrent-ruby (>= 1.3.1) + fugit (~> 1.11) + railties (>= 7.1) + thor (>= 1.3.1) + sqlite3 (2.8.0-aarch64-linux-gnu) + sqlite3 (2.8.0-aarch64-linux-musl) + sqlite3 (2.8.0-arm-linux-gnu) + sqlite3 (2.8.0-arm-linux-musl) + sqlite3 (2.8.0-x86_64-linux-gnu) + sqlite3 (2.8.0-x86_64-linux-musl) + sshkit (1.24.0) + base64 + logger + net-scp (>= 1.1.2) + net-sftp (>= 2.1.2) + net-ssh (>= 2.8.0) + ostruct + stimulus-rails (1.3.4) + railties (>= 6.0.0) + stringio (3.1.8) + sys-uname (1.4.1) + ffi (~> 1.1) + memoist3 (~> 1.0.0) + thor (1.4.0) + thruster (0.1.16) + thruster (0.1.16-aarch64-linux) + thruster (0.1.16-x86_64-linux) + timeout (0.4.4) + tsort (0.2.0) + turbo-rails (2.0.20) + actionpack (>= 7.1.0) + railties (>= 7.1.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.1.0) + uri (1.1.1) + useragent (0.16.11) + web-console (4.2.1) + actionview (>= 6.0.0) + activemodel (>= 6.0.0) + bindex (>= 0.4.0) + railties (>= 6.0.0) + websocket (1.2.11) + websocket-driver (0.8.0) + base64 + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + xpath (3.2.0) + nokogiri (~> 1.8) + zeitwerk (2.7.3) + +PLATFORMS + aarch64-linux + aarch64-linux-gnu + aarch64-linux-musl + arm-linux-gnu + arm-linux-musl + x86_64-linux-gnu + x86_64-linux-musl + +DEPENDENCIES + bootsnap + brakeman + bundler-audit + capybara + cucumber-rails (~> 4.0) + database_cleaner-active_record (~> 2.2) + debug + image_processing (~> 1.2) + importmap-rails + jbuilder + kamal + propshaft + puma (>= 5.0) + rails (~> 8.1.1) + rubocop-rails-omakase + selenium-webdriver + solid_cable + solid_cache + solid_queue + sqlite3 (>= 2.1) + stimulus-rails + thruster + turbo-rails + tzinfo-data + web-console + +BUNDLED WITH + 2.6.2 diff --git a/README.md b/README.md index 9d7fe1bf53..902ee88ad1 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,64 @@ -# CAMAAR -Sistema para avaliação de atividades acadêmicas remotas do CIC +# CAMAAR - Sistema de Avaliação de Turmas + +Sistema de avaliação e gerenciamento de formulários para turmas acadêmicas. + +## Sprint 1 - Resumo Executivo + +### Papéis +- **Scrum Master**: Kelven Dias +- **Product Owner**: Equipe de Engenharia de Software + +### Funcionalidades Desenvolvidas + +| ID | Funcionalidade | Responsável | Pontos | Regras de Negócio | +|---|---|---|---|---| +| #99 | Responder formulário | Equipe | 5 | Participantes respondem questionários; todas as perguntas obrigatórias devem ser preenchidas; formulário bloqueado após submissão | +| #100 | Cadastrar usuários via SIGAA | Equipe | 3 | Importação de usuários do SIGAA; validação de duplicatas | +| #102 | Criar template de formulário | Equipe | 8 | Administradores criam templates com 3 tipos de questões (numérica 1-5, múltipla escolha, texto); mínimo 1 questão; múltipla escolha requer ≥2 opções | +| #103 | Criar formulário de avaliação | Equipe | 5 | Formulários baseados em templates; vinculados a turmas específicas | +| #105 | Configuração de senha | Equipe | 3 | Primeiro acesso requer definição de senha; validação de força da senha | +| #109 | Visualizar formulários pendentes | Equipe | 3 | Participantes veem formulários disponíveis para responder | +| #110 | Visualizar resultados | Equipe | 8 | Administradores acessam resultados consolidados; exportação de relatórios; gráficos e estatísticas | +| #111 | Visualizar templates | Equipe | 2 | Listagem de templates cadastrados | +| #112 | Editar e excluir templates | Equipe | 5 | Gerenciamento completo de templates; validação de templates em uso | +| #113 | Importar dados do SIGAA | Equipe | 8 | Importação de turmas, matérias e participantes via JSON; evita duplicação | + +**Total de Pontos (Velocity)**: 50 pontos + +### Política de Branching +- **Branch principal**: `master` +- **Branch de sprint**: `sprint-1` +- **Features**: Uma branch por funcionalidade (`sprint-1-bdd-`) +- **Convenção**: Pull Requests obrigatórios; CI deve passar antes do merge +- **Commits**: Mensagens descritivas no padrão `feat:`, `fix:`, `test:`, `chore:` + +### Tecnologias +- **Ruby**: 3.4.1 +- **Rails**: 8.1.1 +- **BDD**: Cucumber + Capybara +- **CI/CD**: GitHub Actions + +--- + +## Setup do Projeto + +### Requisitos +* Ruby 3.4.1 +* Rails 8.1.1 +* SQLite3 + +### Instalação +```bash +bundle install +rails db:setup +``` + +### Executar testes BDD +```bash +bundle exec cucumber +``` + +### Rodar aplicação +```bash +bin/dev +``` diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000000..9a5ea7383a --- /dev/null +++ b/Rakefile @@ -0,0 +1,6 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require_relative "config/application" + +Rails.application.load_tasks diff --git a/app/assets/images/.keep b/app/assets/images/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css new file mode 100644 index 0000000000..fe93333c0f --- /dev/null +++ b/app/assets/stylesheets/application.css @@ -0,0 +1,10 @@ +/* + * This is a manifest file that'll be compiled into application.css. + * + * With Propshaft, assets are served efficiently without preprocessing steps. You can still include + * application-wide styles in this file, but keep in mind that CSS precedence will follow the standard + * cascading order, meaning styles declared later in the document or manifest will override earlier ones, + * depending on specificity. + * + * Consider organizing styles into separate files for maintainability. + */ diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb new file mode 100644 index 0000000000..c3537563da --- /dev/null +++ b/app/controllers/application_controller.rb @@ -0,0 +1,7 @@ +class ApplicationController < ActionController::Base + # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has. + allow_browser versions: :modern + + # Changes to the importmap will invalidate the etag for HTML responses + stale_when_importmap_changes +end diff --git a/app/controllers/concerns/.keep b/app/controllers/concerns/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb new file mode 100644 index 0000000000..de6be7945c --- /dev/null +++ b/app/helpers/application_helper.rb @@ -0,0 +1,2 @@ +module ApplicationHelper +end diff --git a/app/javascript/application.js b/app/javascript/application.js new file mode 100644 index 0000000000..beff742ec3 --- /dev/null +++ b/app/javascript/application.js @@ -0,0 +1 @@ +// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb new file mode 100644 index 0000000000..d394c3d106 --- /dev/null +++ b/app/jobs/application_job.rb @@ -0,0 +1,7 @@ +class ApplicationJob < ActiveJob::Base + # Automatically retry jobs that encountered a deadlock + # retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + # discard_on ActiveJob::DeserializationError +end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb new file mode 100644 index 0000000000..3c34c8148f --- /dev/null +++ b/app/mailers/application_mailer.rb @@ -0,0 +1,4 @@ +class ApplicationMailer < ActionMailer::Base + default from: "from@example.com" + layout "mailer" +end diff --git a/app/models/application_record.rb b/app/models/application_record.rb new file mode 100644 index 0000000000..b63caeb8a5 --- /dev/null +++ b/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + primary_abstract_class +end diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb new file mode 100644 index 0000000000..9e51e3817f --- /dev/null +++ b/app/views/layouts/application.html.erb @@ -0,0 +1,29 @@ + + + + <%= content_for(:title) || "Camaar" %> + + + + + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + + <%= yield :head %> + + <%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %> + <%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %> + + + + + + <%# Includes all stylesheet files in app/assets/stylesheets %> + <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %> + <%= javascript_importmap_tags %> + + + + <%= yield %> + + diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb new file mode 100644 index 0000000000..3aac9002ed --- /dev/null +++ b/app/views/layouts/mailer.html.erb @@ -0,0 +1,13 @@ + + + + + + + + + <%= yield %> + + diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb new file mode 100644 index 0000000000..37f0bddbd7 --- /dev/null +++ b/app/views/layouts/mailer.text.erb @@ -0,0 +1 @@ +<%= yield %> diff --git a/app/views/pwa/manifest.json.erb b/app/views/pwa/manifest.json.erb new file mode 100644 index 0000000000..fca522bbe4 --- /dev/null +++ b/app/views/pwa/manifest.json.erb @@ -0,0 +1,22 @@ +{ + "name": "Camaar", + "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": "Camaar.", + "theme_color": "red", + "background_color": "red" +} diff --git a/app/views/pwa/service-worker.js b/app/views/pwa/service-worker.js new file mode 100644 index 0000000000..b3a13fb7bb --- /dev/null +++ b/app/views/pwa/service-worker.js @@ -0,0 +1,26 @@ +// Add a service worker for processing Web Push notifications: +// +// self.addEventListener("push", async (event) => { +// const { title, options } = await event.data.json() +// event.waitUntil(self.registration.showNotification(title, options)) +// }) +// +// self.addEventListener("notificationclick", function(event) { +// event.notification.close() +// event.waitUntil( +// clients.matchAll({ type: "window" }).then((clientList) => { +// for (let i = 0; i < clientList.length; i++) { +// let client = clientList[i] +// let clientPath = (new URL(client.url)).pathname +// +// if (clientPath == event.notification.data.path && "focus" in client) { +// return client.focus() +// } +// } +// +// if (clients.openWindow) { +// return clients.openWindow(event.notification.data.path) +// } +// }) +// ) +// }) diff --git a/bin/brakeman b/bin/brakeman new file mode 100755 index 0000000000..ace1c9ba08 --- /dev/null +++ b/bin/brakeman @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby +require "rubygems" +require "bundler/setup" + +ARGV.unshift("--ensure-latest") + +load Gem.bin_path("brakeman", "brakeman") diff --git a/bin/bundler-audit b/bin/bundler-audit new file mode 100755 index 0000000000..e2ef22690c --- /dev/null +++ b/bin/bundler-audit @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby +require_relative "../config/boot" +require "bundler/audit/cli" + +ARGV.concat %w[ --config config/bundler-audit.yml ] if ARGV.empty? || ARGV.include?("check") +Bundler::Audit::CLI.start diff --git a/bin/ci b/bin/ci new file mode 100755 index 0000000000..4137ad5bb0 --- /dev/null +++ b/bin/ci @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby +require_relative "../config/boot" +require "active_support/continuous_integration" + +CI = ActiveSupport::ContinuousIntegration +require_relative "../config/ci.rb" diff --git a/bin/cucumber b/bin/cucumber new file mode 100755 index 0000000000..eb5e962e86 --- /dev/null +++ b/bin/cucumber @@ -0,0 +1,11 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +vendored_cucumber_bin = Dir["#{File.dirname(__FILE__)}/../vendor/{gems,plugins}/cucumber*/bin/cucumber"].first +if vendored_cucumber_bin + load File.expand_path(vendored_cucumber_bin) +else + require 'rubygems' unless ENV['NO_RUBYGEMS'] + require 'cucumber' + load Cucumber::BINARY +end diff --git a/bin/dev b/bin/dev new file mode 100755 index 0000000000..5f91c20545 --- /dev/null +++ b/bin/dev @@ -0,0 +1,2 @@ +#!/usr/bin/env ruby +exec "./bin/rails", "server", *ARGV diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint new file mode 100755 index 0000000000..ed31659f40 --- /dev/null +++ b/bin/docker-entrypoint @@ -0,0 +1,8 @@ +#!/bin/bash -e + +# If running the rails server then create or migrate existing database +if [ "${@: -2:1}" == "./bin/rails" ] && [ "${@: -1:1}" == "server" ]; then + ./bin/rails db:prepare +fi + +exec "${@}" diff --git a/bin/importmap b/bin/importmap new file mode 100755 index 0000000000..36502ab16c --- /dev/null +++ b/bin/importmap @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby + +require_relative "../config/application" +require "importmap/commands" diff --git a/bin/rails b/bin/rails new file mode 100755 index 0000000000..efc0377492 --- /dev/null +++ b/bin/rails @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +APP_PATH = File.expand_path("../config/application", __dir__) +require_relative "../config/boot" +require "rails/commands" diff --git a/bin/rake b/bin/rake new file mode 100755 index 0000000000..4fbf10b960 --- /dev/null +++ b/bin/rake @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +require_relative "../config/boot" +require "rake" +Rake.application.run diff --git a/bin/rubocop b/bin/rubocop new file mode 100755 index 0000000000..5a20504716 --- /dev/null +++ b/bin/rubocop @@ -0,0 +1,8 @@ +#!/usr/bin/env ruby +require "rubygems" +require "bundler/setup" + +# Explicit RuboCop config increases performance slightly while avoiding config confusion. +ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__)) + +load Gem.bin_path("rubocop", "rubocop") diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000000..81be011e87 --- /dev/null +++ b/bin/setup @@ -0,0 +1,35 @@ +#!/usr/bin/env ruby +require "fileutils" + +APP_ROOT = File.expand_path("..", __dir__) + +def system!(*args) + system(*args, exception: true) +end + +FileUtils.chdir APP_ROOT do + # This script is a way to set up or update your development environment automatically. + # This script is idempotent, so that you can run it at any time and get an expectable outcome. + # Add necessary setup steps to this file. + + puts "== Installing dependencies ==" + system("bundle check") || system!("bundle install") + + # puts "\n== Copying sample files ==" + # unless File.exist?("config/database.yml") + # FileUtils.cp "config/database.yml.sample", "config/database.yml" + # end + + puts "\n== Preparing database ==" + system! "bin/rails db:prepare" + system! "bin/rails db:reset" if ARGV.include?("--reset") + + puts "\n== Removing old logs and tempfiles ==" + system! "bin/rails log:clear tmp:clear" + + unless ARGV.include?("--skip-server") + puts "\n== Starting development server ==" + STDOUT.flush # flush the output before exec(2) so that it displays + exec "bin/dev" + end +end diff --git a/bin/thrust b/bin/thrust new file mode 100755 index 0000000000..36bde2d832 --- /dev/null +++ b/bin/thrust @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("thruster", "thrust") diff --git a/config.ru b/config.ru new file mode 100644 index 0000000000..4a3c09a688 --- /dev/null +++ b/config.ru @@ -0,0 +1,6 @@ +# This file is used by Rack-based servers to start the application. + +require_relative "config/environment" + +run Rails.application +Rails.application.load_server diff --git a/config/application.rb b/config/application.rb new file mode 100644 index 0000000000..b4848d838b --- /dev/null +++ b/config/application.rb @@ -0,0 +1,27 @@ +require_relative "boot" + +require "rails/all" + +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(*Rails.groups) + +module Camaar + class Application < Rails::Application + # Initialize configuration defaults for originally generated Rails version. + config.load_defaults 8.1 + + # Please, add to the `ignore` list any other `lib` subdirectories that do + # not contain `.rb` files, or that should not be reloaded or eager loaded. + # Common ones are `templates`, `generators`, or `middleware`, for example. + config.autoload_lib(ignore: %w[assets tasks]) + + # Configuration for the application, engines, and railties goes here. + # + # These settings can be overridden in specific environments using the files + # in config/environments, which are processed later. + # + # config.time_zone = "Central Time (US & Canada)" + # config.eager_load_paths << Rails.root.join("extras") + end +end diff --git a/config/boot.rb b/config/boot.rb new file mode 100644 index 0000000000..988a5ddc46 --- /dev/null +++ b/config/boot.rb @@ -0,0 +1,4 @@ +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +require "bundler/setup" # Set up gems listed in the Gemfile. +require "bootsnap/setup" # Speed up boot time by caching expensive operations. diff --git a/config/bundler-audit.yml b/config/bundler-audit.yml new file mode 100644 index 0000000000..e74b3af949 --- /dev/null +++ b/config/bundler-audit.yml @@ -0,0 +1,5 @@ +# Audit all gems listed in the Gemfile for known security problems by running bin/bundler-audit. +# CVEs that are not relevant to the application can be enumerated on the ignore list below. + +ignore: + - CVE-THAT-DOES-NOT-APPLY diff --git a/config/cable.yml b/config/cable.yml new file mode 100644 index 0000000000..21ab7228f9 --- /dev/null +++ b/config/cable.yml @@ -0,0 +1,10 @@ +development: + adapter: async + +test: + adapter: test + +production: + adapter: redis + url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> + channel_prefix: camaar_production diff --git a/config/ci.rb b/config/ci.rb new file mode 100644 index 0000000000..e56a92e418 --- /dev/null +++ b/config/ci.rb @@ -0,0 +1,23 @@ +# 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: System", "bin/rails test:system" + step "Tests: Seeds", "env RAILS_ENV=test bin/rails db:seed:replant" + + # Optional: set a green GitHub commit status to unblock PR merge. + # Requires the `gh` CLI and `gh extension install basecamp/gh-signoff`. + # if success? + # step "Signoff: All systems go. Ready for merge and deploy.", "gh signoff" + # else + # failure "Signoff: CI failed. Do not merge or deploy.", "Fix the issues and try again." + # end +end diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc new file mode 100644 index 0000000000..d213710c2b --- /dev/null +++ b/config/credentials.yml.enc @@ -0,0 +1 @@ +3ZvB+9p9Dd6LxCqwnh/avw5JbLDQvUyey05wwNVb9Y4bSYtFM75zSqH4+59W+B5uoqYT8X+RaLULbOOqPQ+c1K4UEZR4mj16rOv4kACejkTfg/XfccVlRCKUfjPZwQmbHBVGTU7MsKeKsU2KT2qMTPaZ7UD/4kiDrkptFonxnx+1Gbsc/MYIcW0NmVI12CkCy4FKokCSxeeNy35pV2j7qMfavvwBaBq1oPSYlO8jjztIdg0u05s5OJlNWQjHodSyg4rwao4Ia5h5oweCJlL9j/vK9hmVPSh1xnfEbizic0aaNXKRvcZylkwdHiTa+Gmcl0KhBC8n36SYwK5Yq3lbXaJNPm2TvdMlemXE/zeXSbs1dyyMrS/YbmLiTD0FwMAIdl4RLghKrFRiWPOigp4GZrhjK9c3XeDuCn8bLCAdxWocRvGHYZMnSOt32uHq1X0lkwBmrtZuIkdOh6KoPlM17yYCBfCZUXRIZwdA4vU0md9cOscEHaXYLTEl--Ejtd3cyO8x/opFxr--io0jss1RikHSbSZ1xv5jdg== \ No newline at end of file diff --git a/config/cucumber.yml b/config/cucumber.yml new file mode 100644 index 0000000000..47a4663ae2 --- /dev/null +++ b/config/cucumber.yml @@ -0,0 +1,8 @@ +<% +rerun = File.file?('rerun.txt') ? IO.read('rerun.txt') : "" +rerun = rerun.strip.gsub /\s/, ' ' +rerun_opts = rerun.empty? ? "--format #{ENV['CUCUMBER_FORMAT'] || 'progress'} features" : "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} #{rerun}" +std_opts = "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} --strict --tags 'not @wip'" +%> +default: <%= std_opts %> features +rerun: <%= rerun_opts %> --format rerun --out rerun.txt --strict --tags 'not @wip' diff --git a/config/database.yml b/config/database.yml new file mode 100644 index 0000000000..693252b7c3 --- /dev/null +++ b/config/database.yml @@ -0,0 +1,41 @@ +# SQLite. Versions 3.8.0 and up are supported. +# gem install sqlite3 +# +# Ensure the SQLite 3 gem is defined in your Gemfile +# gem "sqlite3" +# +default: &default + adapter: sqlite3 + max_connections: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + timeout: 5000 + +development: + <<: *default + database: storage/development.sqlite3 + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + <<: *default + database: storage/test.sqlite3 + + +# Store production database in the storage/ directory, which by default +# is mounted as a persistent Docker volume in config/deploy.yml. +production: + primary: + <<: *default + database: storage/production.sqlite3 + cache: + <<: *default + database: storage/production_cache.sqlite3 + migrations_paths: db/cache_migrate + queue: + <<: *default + database: storage/production_queue.sqlite3 + migrations_paths: db/queue_migrate + cable: + <<: *default + database: storage/production_cable.sqlite3 + migrations_paths: db/cable_migrate diff --git a/config/environment.rb b/config/environment.rb new file mode 100644 index 0000000000..cac5315775 --- /dev/null +++ b/config/environment.rb @@ -0,0 +1,5 @@ +# Load the Rails application. +require_relative "application" + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/config/environments/development.rb b/config/environments/development.rb new file mode 100644 index 0000000000..4a6ea19c9c --- /dev/null +++ b/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/config/environments/production.rb b/config/environments/production.rb new file mode 100644 index 0000000000..cb0241df64 --- /dev/null +++ b/config/environments/production.rb @@ -0,0 +1,89 @@ +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 = :mem_cache_store + + # Replace the default in-process and non-durable queuing backend for Active Job. + # config.active_job.queue_adapter = :resque + + # Ignore bad email addresses and do not raise email delivery errors. + # Set this to true and configure the email server for immediate delivery to raise delivery errors. + # config.action_mailer.raise_delivery_errors = false + + # Set host to be used by links generated in mailer templates. + config.action_mailer.default_url_options = { host: "example.com" } + + # Specify outgoing SMTP server. Remember to add smtp/* credentials via bin/rails credentials:edit. + # config.action_mailer.smtp_settings = { + # user_name: Rails.application.credentials.dig(:smtp, :user_name), + # password: Rails.application.credentials.dig(:smtp, :password), + # address: "smtp.example.com", + # port: 587, + # authentication: :plain + # } + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true + + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false + + # Only use :id for inspections in production. + config.active_record.attributes_for_inspect = [ :id ] + + # Enable DNS rebinding protection and other `Host` header attacks. + # config.hosts = [ + # "example.com", # Allow requests from example.com + # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` + # ] + # + # Skip DNS rebinding protection for the default health check endpoint. + # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } +end diff --git a/config/environments/test.rb b/config/environments/test.rb new file mode 100644 index 0000000000..29d195b837 --- /dev/null +++ b/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/config/importmap.rb b/config/importmap.rb new file mode 100644 index 0000000000..0086a327b8 --- /dev/null +++ b/config/importmap.rb @@ -0,0 +1,3 @@ +# Pin npm packages by running ./bin/importmap + +pin "application" diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb new file mode 100644 index 0000000000..487324424f --- /dev/null +++ b/config/initializers/assets.rb @@ -0,0 +1,7 @@ +# Be sure to restart your server when you modify this file. + +# Version of your assets, change this if you want to expire all your assets. +Rails.application.config.assets.version = "1.0" + +# Add additional assets to the asset load path. +# Rails.application.config.assets.paths << Emoji.images_path diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb new file mode 100644 index 0000000000..d51d713979 --- /dev/null +++ b/config/initializers/content_security_policy.rb @@ -0,0 +1,29 @@ +# Be sure to restart your server when you modify this file. + +# Define an application-wide content security policy. +# See the Securing Rails Applications Guide for more information: +# https://guides.rubyonrails.org/security.html#content-security-policy-header + +# Rails.application.configure do +# config.content_security_policy do |policy| +# policy.default_src :self, :https +# policy.font_src :self, :https, :data +# policy.img_src :self, :https, :data +# policy.object_src :none +# policy.script_src :self, :https +# policy.style_src :self, :https +# # Specify URI for violation reports +# # policy.report_uri "/csp-violation-report-endpoint" +# end +# +# # Generate session nonces for permitted importmap, inline scripts, and inline styles. +# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } +# config.content_security_policy_nonce_directives = %w(script-src style-src) +# +# # Automatically add `nonce` to `javascript_tag`, `javascript_include_tag`, and `stylesheet_link_tag` +# # if the corresponding directives are specified in `content_security_policy_nonce_directives`. +# # config.content_security_policy_nonce_auto = true +# +# # Report violations without enforcing the policy. +# # config.content_security_policy_report_only = true +# end diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb new file mode 100644 index 0000000000..c0b717f7ec --- /dev/null +++ b/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. +# Use this to limit dissemination of sensitive information. +# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. +Rails.application.config.filter_parameters += [ + :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc +] diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb new file mode 100644 index 0000000000..3860f659ea --- /dev/null +++ b/config/initializers/inflections.rb @@ -0,0 +1,16 @@ +# Be sure to restart your server when you modify this file. + +# Add new inflection rules using the following format. Inflections +# are locale specific, and you may define rules for as many different +# locales as you wish. All of these examples are active by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.plural /^(ox)$/i, "\\1en" +# inflect.singular /^(ox)en/i, "\\1" +# inflect.irregular "person", "people" +# inflect.uncountable %w( fish sheep ) +# end + +# These inflection rules are supported but not enabled by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.acronym "RESTful" +# end diff --git a/config/locales/en.yml b/config/locales/en.yml new file mode 100644 index 0000000000..6c349ae5e3 --- /dev/null +++ b/config/locales/en.yml @@ -0,0 +1,31 @@ +# Files in the config/locales directory are used for internationalization and +# are automatically loaded by Rails. If you want to use locales other than +# English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t "hello" +# +# In views, this is aliased to just `t`: +# +# <%= t("hello") %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# To learn more about the API, please read the Rails Internationalization guide +# at https://guides.rubyonrails.org/i18n.html. +# +# Be aware that YAML interprets the following case-insensitive strings as +# booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings +# must be quoted to be interpreted as strings. For example: +# +# en: +# "yes": yup +# enabled: "ON" + +en: + hello: "Hello world" diff --git a/config/master.key b/config/master.key new file mode 100644 index 0000000000..3a2cf052d5 --- /dev/null +++ b/config/master.key @@ -0,0 +1 @@ +6eb1b7c2bf7b3c4eb1fbe8786d13ba47 \ No newline at end of file diff --git a/config/puma.rb b/config/puma.rb new file mode 100644 index 0000000000..38c4b86596 --- /dev/null +++ b/config/puma.rb @@ -0,0 +1,42 @@ +# This configuration file will be evaluated by Puma. The top-level methods that +# are invoked here are part of Puma's configuration DSL. For more information +# about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. +# +# Puma starts a configurable number of processes (workers) and each process +# serves each request in a thread from an internal thread pool. +# +# You can control the number of workers using ENV["WEB_CONCURRENCY"]. You +# should only set this value when you want to run 2 or more workers. The +# default is already 1. You can set it to `auto` to automatically start a worker +# for each available processor. +# +# The ideal number of threads per worker depends both on how much time the +# application spends waiting for IO operations and on how much you wish to +# prioritize throughput over latency. +# +# As a rule of thumb, increasing the number of threads will increase how much +# traffic a given process can handle (throughput), but due to CRuby's +# Global VM Lock (GVL) it has diminishing returns and will degrade the +# response time (latency) of the application. +# +# The default is set to 3 threads as it's deemed a decent compromise between +# throughput and latency for the average Rails application. +# +# Any libraries that use a connection pool or another resource pool should +# be configured to provide at least as many connections as the number of +# threads. This includes Active Record's `pool` parameter in `database.yml`. +threads_count = ENV.fetch("RAILS_MAX_THREADS", 3) +threads threads_count, threads_count + +# Specifies the `port` that Puma will listen on to receive requests; default is 3000. +port ENV.fetch("PORT", 3000) + +# Allow puma to be restarted by `bin/rails restart` command. +plugin :tmp_restart + +# Run the Solid Queue supervisor inside of Puma for single-server deployments. +plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"] + +# Specify the PID file. Defaults to tmp/pids/server.pid in development. +# In other environments, only set the PID file if requested. +pidfile ENV["PIDFILE"] if ENV["PIDFILE"] diff --git a/config/routes.rb b/config/routes.rb new file mode 100644 index 0000000000..48254e88ed --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,14 @@ +Rails.application.routes.draw do + # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html + + # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. + # Can be used by load balancers and uptime monitors to verify that the app is live. + get "up" => "rails/health#show", as: :rails_health_check + + # Render dynamic PWA files from app/views/pwa/* (remember to link manifest in application.html.erb) + # get "manifest" => "rails/pwa#manifest", as: :pwa_manifest + # get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker + + # Defines the root path route ("/") + # root "posts#index" +end diff --git a/config/storage.yml b/config/storage.yml new file mode 100644 index 0000000000..927dc537c8 --- /dev/null +++ b/config/storage.yml @@ -0,0 +1,27 @@ +test: + service: Disk + root: <%= Rails.root.join("tmp/storage") %> + +local: + service: Disk + root: <%= Rails.root.join("storage") %> + +# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) +# amazon: +# service: S3 +# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> +# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> +# region: us-east-1 +# bucket: your_own_bucket-<%= Rails.env %> + +# Remember not to checkin your GCS keyfile to a repository +# google: +# service: GCS +# project: your_project +# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> +# bucket: your_own_bucket-<%= Rails.env %> + +# mirror: +# service: Mirror +# primary: local +# mirrors: [ amazon, google, microsoft ] diff --git a/db/schema.rb b/db/schema.rb new file mode 100644 index 0000000000..03e73681a8 --- /dev/null +++ b/db/schema.rb @@ -0,0 +1,14 @@ +# 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: 0) do +end diff --git a/db/seeds.rb b/db/seeds.rb new file mode 100644 index 0000000000..4fbd6ed970 --- /dev/null +++ b/db/seeds.rb @@ -0,0 +1,9 @@ +# This file should ensure the existence of records required to run the application in every environment (production, +# development, test). The code here should be idempotent so that it can be executed at any point in every environment. +# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). +# +# Example: +# +# ["Action", "Comedy", "Drama", "Horror"].each do |genre_name| +# MovieGenre.find_or_create_by!(name: genre_name) +# end diff --git a/docs/CAMAAR_ER_diagram.png b/docs/CAMAAR_ER_diagram.png new file mode 100644 index 0000000000..4dde608ab8 Binary files /dev/null and b/docs/CAMAAR_ER_diagram.png differ diff --git a/docs/ER-CAMAAR.pdf b/docs/ER-CAMAAR.pdf new file mode 100644 index 0000000000..4e7463653a Binary files /dev/null and b/docs/ER-CAMAAR.pdf differ diff --git a/docs/ER-CAMAAR.tex b/docs/ER-CAMAAR.tex new file mode 100644 index 0000000000..ca6e05c043 --- /dev/null +++ b/docs/ER-CAMAAR.tex @@ -0,0 +1,555 @@ +\documentclass[a4paper,12pt]{article} + +\usepackage[utf8]{inputenc} +\usepackage[brazil]{babel} +\usepackage{graphicx} +\usepackage{geometry} +\usepackage{caption} +\usepackage{booktabs} +\usepackage{longtable} + +\geometry{margin=1in} + +\title{Modelo Relacional UML da Aplicação CAMAAR\\ +\large Sistema de Avaliação de Turmas} + +\author{Kelven Dias\\ +Universidade de Brasília - UnB\\ +Departamento de Ciência da Computação} + +\date{Novembro de 2025} + +\begin{document} + +\maketitle + +\begin{abstract} +Este documento apresenta o modelo relacional UML da aplicação CAMAAR (Sistema de Avaliação de Turmas), desenvolvida em Ruby on Rails para gerenciamento de avaliações acadêmicas. O sistema permite a gestão completa de turmas, docentes, discentes, templates de formulários, avaliações e respostas, integrando-se com dados do SIGAA. +\end{abstract} + +\section{Introdução} + +A aplicação \textbf{CAMAAR} é um sistema desenvolvido em \textit{Ruby on Rails 8.1.1}, utilizando Ruby 3.4.1, projetado para gerenciar avaliações de turmas acadêmicas na Universidade de Brasília. O sistema oferece funcionalidades para criação de templates de formulários, aplicação de avaliações, coleta e análise de respostas, além de integração com dados do SIGAA (Sistema Integrado de Gestão de Atividades Acadêmicas). + +Este relatório apresenta o modelo relacional UML completo da aplicação, descrevendo suas entidades principais, atributos, relacionamentos e regras de negócio implementadas. + +\section{Entidades Principais} + +\subsection{Usuário} + +Entidade base para todos os usuários do sistema, utilizando herança de tabela única (Single Table Inheritance - STI) para Docente e Dicente. + +\begin{itemize} + \item \textbf{usuario}: Identificador único do usuário (chave primária) - string + \item \textbf{nome}: Nome completo do usuário + \item \textbf{email}: Email institucional (único) + \item \textbf{formacao}: Nível de formação (graduando, mestrado, doutorado) + \item \textbf{ocupacao}: Tipo de ocupação (docente/dicente) + \item \textbf{password\_digest}: Senha criptografada (bcrypt) + \item \textbf{created\_at}: Data de criação do registro + \item \textbf{updated\_at}: Data da última atualização +\end{itemize} + +\textbf{Relacionamentos}: Classe pai de Docente e Dicente (relacionamento ISA). + +\textbf{Regras de Negócio}: +\begin{itemize} + \item Email deve ser único no sistema + \item Senha deve ter no mínimo 8 caracteres + \item Ocupação determina o perfil de acesso (administrador/participante) +\end{itemize} + +\subsection{Docente} + +Especialização de Usuário representando professores. + +\begin{itemize} + \item \textbf{usuario}: Chave primária e estrangeira (FK para Usuario.usuario) + \item \textbf{departamento}: Departamento de lotação (ex: "DEPTO CIÊNCIAS DA COMPUTAÇÃO") + \item \textbf{titulacao}: Nível de titulação (especialização, mestrado, doutorado) +\end{itemize} + +\textbf{Relacionamentos}: +\begin{itemize} + \item Um docente pode lecionar várias turmas (1:N com Turma) + \item Um docente pode criar várias avaliações (1:N com Avaliacao) + \item Um docente pode criar vários templates (1:N com Template) +\end{itemize} + +\subsection{Dicente} + +Especialização de Usuário representando alunos. + +\begin{itemize} + \item \textbf{usuario}: Chave primária e estrangeira (FK para Usuario.usuario) + \item \textbf{matricula}: Matrícula institucional (única) + \item \textbf{curso}: Curso de graduação (ex: "CIÊNCIA DA COMPUTAÇÃO/CIC") +\end{itemize} + +\textbf{Relacionamentos}: +\begin{itemize} + \item Um dicente pode estar matriculado em várias turmas (1:N com Matricula) + \item Um dicente pode responder várias avaliações (1:N com Resposta) +\end{itemize} + +\subsection{Materia} + +Representa disciplinas oferecidas pela instituição. + +\begin{itemize} + \item \textbf{code}: Código da matéria (chave primária) - ex: "CIC0097" + \item \textbf{name}: Nome da matéria (ex: "BANCOS DE DADOS") + \item \textbf{created\_at}: Data de criação do registro + \item \textbf{updated\_at}: Data da última atualização +\end{itemize} + +\textbf{Relacionamentos}: Uma matéria pode ter várias turmas (1:N com Turma). + +\textbf{Dados Exemplo}: +\begin{itemize} + \item CIC0097 - BANCOS DE DADOS + \item CIC0105 - ENGENHARIA DE SOFTWARE + \item CIC0202 - PROGRAMAÇÃO CONCORRENTE +\end{itemize} + +\subsection{Turma} + +Representa uma oferta específica de uma matéria em um semestre. + +\begin{itemize} + \item \textbf{id}: Identificador único (chave primária) - int + \item \textbf{course\_code}: Código da matéria (FK para Materia.code) + \item \textbf{class\_code}: Código da turma (ex: "TA") + \item \textbf{semester}: Semestre letivo (formato: "YYYY.S", ex: "2021.2") + \item \textbf{time}: Horário de aula (ex: "35T45") + \item \textbf{docente\_usuario}: Docente responsável (FK para Docente.usuario) + \item \textbf{created\_at}: Data de criação do registro + \item \textbf{updated\_at}: Data da última atualização +\end{itemize} + +\textbf{Relacionamentos}: +\begin{itemize} + \item Uma turma pertence a uma matéria (N:1 com Materia) + \item Uma turma tem um docente responsável (N:1 com Docente) + \item Uma turma pode ter várias matrículas (1:N com Matricula) + \item Uma turma pode ter várias avaliações (1:N com Avaliacao) +\end{itemize} + +\subsection{Matricula} + +Relacionamento entre Dicente e Turma, representando a matrícula de um aluno. + +\begin{itemize} + \item \textbf{id}: Identificador único (chave primária) - int + \item \textbf{dicente\_usuario}: Identificador do aluno (FK para Dicente.usuario) + \item \textbf{turma\_id}: Identificador da turma (FK para Turma.id) + \item \textbf{status}: Status da matrícula (ativo/trancado/concluído) + \item \textbf{enrollment\_date}: Data de matrícula + \item \textbf{final\_grade}: Nota final do aluno + \item \textbf{created\_at}: Data de criação do registro + \item \textbf{updated\_at}: Data da última atualização +\end{itemize} + +\textbf{Relacionamentos}: +\begin{itemize} + \item Cada matrícula vincula um dicente a uma turma + \item Um dicente pode ter múltiplas matrículas em turmas diferentes + \item Uma turma pode ter múltiplas matrículas +\end{itemize} + +\textbf{Regras de Negócio}: +\begin{itemize} + \item Um aluno não pode se matricular duas vezes na mesma turma + \item Status padrão ao criar matrícula: "ativo" + \item Nota final aceita valores de 0.0 a 10.0 +\end{itemize} + +\subsection{Template} + +Modelos reutilizáveis de formulários de avaliação criados por docentes. + +\begin{itemize} + \item \textbf{id}: Identificador único (chave primária) - int + \item \textbf{docente\_usuario}: Criador do template (FK para Docente.usuario) + \item \textbf{name}: Nome do template + \item \textbf{description}: Descrição detalhada do template + \item \textbf{created\_at}: Data de criação do registro + \item \textbf{updated\_at}: Data da última atualização +\end{itemize} + +\textbf{Relacionamentos}: +\begin{itemize} + \item Um template pertence a um docente (N:1 com Docente) + \item Um template pode ser usado em várias avaliações (1:N com Avaliacao) +\end{itemize} + +\textbf{Regras de Negócio}: +\begin{itemize} + \item Templates podem conter questões dos tipos: numérica (1-5), múltipla escolha, e texto livre + \item Questões de múltipla escolha devem ter no mínimo 2 opções + \item Templates devem ter ao menos uma questão + \item Nome do template deve ser único por docente e semestre +\end{itemize} + +\subsection{Avaliacao} + +Representa uma avaliação específica aplicada em uma turma. + +\begin{itemize} + \item \textbf{id}: Identificador único (chave primária) - int + \item \textbf{turma\_id}: Turma avaliada (FK para Turma.id) + \item \textbf{docente\_usuario}: Docente responsável (FK para Docente.usuario) + \item \textbf{template\_id}: Template utilizado (FK para Template.id, opcional) + \item \textbf{title}: Título da avaliação + \item \textbf{description}: Descrição detalhada (text) + \item \textbf{due\_date}: Data limite para respostas + \item \textbf{max\_score}: Pontuação máxima (float) + \item \textbf{status}: Status da avaliação (rascunho/publicada/fechada) + \item \textbf{created\_at}: Data de criação do registro + \item \textbf{updated\_at}: Data da última atualização +\end{itemize} + +\textbf{Relacionamentos}: +\begin{itemize} + \item Uma avaliação pertence a uma turma (N:1 com Turma) + \item Uma avaliação pertence a um docente (N:1 com Docente) + \item Uma avaliação pode ser baseada em um template (N:0..1 com Template) + \item Uma avaliação pode ter várias questões (1:N com Questao) + \item Uma avaliação pode ter várias respostas (1:N com Resposta) +\end{itemize} + +\textbf{Regras de Negócio}: +\begin{itemize} + \item Avaliações no estado "rascunho" não são visíveis para alunos + \item Avaliações "publicadas" são visíveis e podem ser respondidas + \item Avaliações "fechadas" não aceitam mais respostas + \item Data limite deve ser posterior à data de criação +\end{itemize} + +\subsection{Questao} + +Representa perguntas individuais dentro de uma avaliação. + +\begin{itemize} + \item \textbf{id}: Identificador único (chave primária) - int + \item \textbf{evaluation\_id}: Avaliação associada (FK para Avaliacao.id) + \item \textbf{order}: Ordem da questão na avaliação (int) + \item \textbf{question\_text}: Texto da pergunta (text) + \item \textbf{question\_type}: Tipo de questão (dissertativa/multipla\_escolha/numerica) + \item \textbf{points}: Pontuação da questão (float) + \item \textbf{created\_at}: Data de criação do registro + \item \textbf{updated\_at}: Data da última atualização +\end{itemize} + +\textbf{Relacionamentos}: +\begin{itemize} + \item Cada questão pertence a uma avaliação (N:1 com Avaliacao) + \item Questões são ordenadas dentro de cada avaliação +\end{itemize} + +\textbf{Tipos de Questão}: +\begin{itemize} + \item \textbf{numerica}: Escala de 1 a 5 + \item \textbf{multipla\_escolha}: Opções pré-definidas (mínimo 2) + \item \textbf{dissertativa}: Texto livre +\end{itemize} + +\subsection{Resposta} + +Representa as respostas dos dicentes às avaliações. + +\begin{itemize} + \item \textbf{id}: Identificador único (chave primária) - int + \item \textbf{evaluation\_id}: Avaliação respondida (FK para Avaliacao.id) + \item \textbf{dicente\_usuario}: Aluno que respondeu (FK para Dicente.usuario) + \item \textbf{submission\_date}: Data de submissão + \item \textbf{score}: Pontuação obtida (float) + \item \textbf{feedback}: Feedback do docente (text) + \item \textbf{status}: Status da resposta (pendente/corrigida/atrasada) + \item \textbf{created\_at}: Data de criação do registro + \item \textbf{updated\_at}: Data da última atualização +\end{itemize} + +\textbf{Relacionamentos}: +\begin{itemize} + \item Cada resposta pertence a uma avaliação (N:1 com Avaliacao) + \item Cada resposta é de um dicente (N:1 com Dicente) +\end{itemize} + +\textbf{Regras de Negócio}: +\begin{itemize} + \item Um aluno pode responder cada avaliação apenas uma vez + \item Respostas após a data limite são marcadas como "atrasada" + \item Todas as questões obrigatórias devem ser respondidas + \item Formulário é bloqueado após submissão bem-sucedida +\end{itemize} + +\section{Relacionamentos e Cardinalidades} + +\subsection{Herança (ISA)} +\begin{itemize} + \item Usuario $\rightarrow$ Docente (1:1, disjunta, total) + \item Usuario $\rightarrow$ Dicente (1:1, disjunta, total) +\end{itemize} + +\subsection{Relacionamentos 1:N} +\begin{itemize} + \item Materia $\rightarrow$ Turma (1:N) + \item Docente $\rightarrow$ Turma (1:N) + \item Docente $\rightarrow$ Template (1:N) + \item Docente $\rightarrow$ Avaliacao (1:N) + \item Turma $\rightarrow$ Matricula (1:N) + \item Turma $\rightarrow$ Avaliacao (1:N) + \item Template $\rightarrow$ Avaliacao (1:N, opcional) + \item Avaliacao $\rightarrow$ Questao (1:N) + \item Avaliacao $\rightarrow$ Resposta (1:N) + \item Dicente $\rightarrow$ Matricula (1:N) + \item Dicente $\rightarrow$ Resposta (1:N) +\end{itemize} + +\section{Modelo Relacional UML} + +O modelo relacional UML completo da aplicação \textbf{CAMAAR} é apresentado na Figura \ref{fig:uml}. O diagrama ilustra todas as entidades, atributos, chaves primárias e estrangeiras, além dos relacionamentos e suas cardinalidades. + +\begin{figure}[h!] + \centering + \includegraphics[width=1.0\textwidth]{CAMAAR_ER_diagram.png} + \caption{Modelo Relacional UML da aplicação CAMAAR - Sistema de Avaliação de Turmas.} + \label{fig:uml} +\end{figure} + +\section{Considerações de Implementação} + +\subsection{Tecnologias Utilizadas} +\begin{itemize} + \item \textbf{Ruby on Rails 8.1.1}: Framework web MVC + \item \textbf{Ruby 3.4.1}: Linguagem de programação + \item \textbf{SQLite3}: Banco de dados (desenvolvimento/teste) + \item \textbf{Cucumber + Capybara}: Framework BDD para testes + \item \textbf{GitHub Actions}: CI/CD +\end{itemize} + +\subsection{Padrões Adotados} + +\textbf{Single Table Inheritance (STI)}: Utilizado para as entidades Usuario, Docente e Dicente, permitindo polimorfismo e reutilização de código. + +\textbf{Soft Deletes}: Registros não são deletados fisicamente, apenas marcados como inativos através do campo \texttt{status}. + +\textbf{Timestamps}: Todos os modelos incluem \texttt{created\_at} e \texttt{updated\_at} para auditoria. + +\textbf{Validações}: Implementadas no nível de aplicação (ActiveRecord) e banco de dados (constraints). + +\subsection{Segurança} +\begin{itemize} + \item Senhas armazenadas com bcrypt (has\_secure\_password) + \item Autenticação baseada em sessões + \item Controle de acesso por perfil (Admin/Participante) + \item Validação de permissões em todas as operações sensíveis +\end{itemize} + +\subsection{Integração com SIGAA} + +O sistema importa dados do SIGAA através de arquivos JSON: +\begin{itemize} + \item \texttt{class\_members.json}: Dados de turmas, docentes e dicentes + \item \texttt{classes.json}: Informações de disciplinas e ofertas +\end{itemize} + +A importação evita duplicação através de validações de unicidade em campos-chave (usuario, matricula, email). + +\section{Casos de Uso Principais} + +\subsection{UC01: Responder Formulário (\#99)} +\textbf{Ator}: Dicente\\ +\textbf{Fluxo}: +\begin{enumerate} + \item Dicente visualiza formulários disponíveis para sua turma + \item Sistema valida permissões de acesso + \item Dicente preenche questões obrigatórias + \item Sistema valida completude das respostas + \item Sistema registra resposta e bloqueia formulário +\end{enumerate} + +\subsection{UC02: Cadastrar Usuários do Sistema (\#100)} +\textbf{Ator}: Administrador\\ +\textbf{Fluxo}: +\begin{enumerate} + \item Administrador acessa módulo de cadastro de usuários + \item Sistema solicita dados: nome, email, tipo (docente/dicente) + \item Administrador preenche informações do usuário + \item Sistema valida unicidade de email e matrícula + \item Sistema cria usuário e envia email de ativação +\end{enumerate} + +\subsection{UC03: Sistema de Login (\#104)} +\textbf{Ator}: Usuário (Docente/Dicente)\\ +\textbf{Fluxo}: +\begin{enumerate} + \item Usuário acessa página de login + \item Sistema solicita credenciais (email/usuário e senha) + \item Usuário fornece credenciais + \item Sistema valida autenticação + \item Sistema redireciona para dashboard apropriado ao perfil +\end{enumerate} + +\subsection{UC04: Sistema de Definição de Senha (\#105)} +\textbf{Ator}: Usuário (primeiro acesso)\\ +\textbf{Fluxo}: +\begin{enumerate} + \item Usuário acessa sistema pela primeira vez + \item Sistema detecta ausência de senha definida + \item Sistema solicita criação de senha forte + \item Usuário define senha (mínimo 8 caracteres) + \item Sistema valida força da senha e confirma cadastro +\end{enumerate} + +\subsection{UC05: Criar Template de Formulário (\#102)} +\textbf{Ator}: Docente\\ +\textbf{Fluxo}: +\begin{enumerate} + \item Docente acessa página de gerenciamento de templates + \item Docente define nome e descrição + \item Docente adiciona questões (numéricas, múltipla escolha, texto) + \item Sistema valida regras de negócio + \item Template é salvo e disponibilizado para criação de avaliações +\end{enumerate} + +\subsection{UC06: Criar Formulário de Avaliação (\#103)} +\textbf{Ator}: Docente\\ +\textbf{Fluxo}: +\begin{enumerate} + \item Docente seleciona turma e template (opcional) + \item Docente define título, descrição e prazo + \item Docente configura questões e pontuação + \item Sistema valida dados e cria avaliação em estado "rascunho" + \item Docente publica avaliação tornando-a visível para alunos +\end{enumerate} + +\subsection{UC07: Visualizar Formulários Pendentes (\#109)} +\textbf{Ator}: Dicente\\ +\textbf{Fluxo}: +\begin{enumerate} + \item Dicente acessa dashboard de formulários + \item Sistema lista avaliações disponíveis para suas turmas + \item Sistema destaca prazos próximos e formulários atrasados + \item Dicente pode filtrar por turma ou status + \item Dicente seleciona formulário para responder +\end{enumerate} + +\subsection{UC08: Visualizar Resultados dos Formulários (\#110)} +\textbf{Ator}: Docente\\ +\textbf{Fluxo}: +\begin{enumerate} + \item Docente acessa página de resultados + \item Sistema lista avaliações com respostas + \item Docente seleciona avaliação específica + \item Sistema exibe estatísticas consolidadas e gráficos + \item Docente pode exportar relatório +\end{enumerate} + +\subsection{UC09: Visualizar Templates Criados (\#111)} +\textbf{Ator}: Docente\\ +\textbf{Fluxo}: +\begin{enumerate} + \item Docente acessa biblioteca de templates + \item Sistema lista templates criados pelo docente + \item Docente pode buscar por nome ou filtrar por semestre + \item Sistema exibe preview do template selecionado + \item Docente pode editar, duplicar ou excluir template +\end{enumerate} + +\subsection{UC10: Edição e Deleção de Templates (\#112)} +\textbf{Ator}: Docente\\ +\textbf{Fluxo}: +\begin{enumerate} + \item Docente seleciona template para editar/excluir + \item Sistema verifica se template está em uso + \item Para edição: docente modifica questões e salva alterações + \item Para exclusão: sistema alerta sobre avaliações vinculadas + \item Sistema confirma operação e atualiza registro +\end{enumerate} + +\subsection{UC11: Importar Dados do SIGAA (\#98, \#108)} +\textbf{Ator}: Administrador\\ +\textbf{Fluxo}: +\begin{enumerate} + \item Administrador acessa página de importação + \item Sistema solicita arquivos JSON + \item Administrador seleciona arquivos + \item Sistema valida formato e integridade + \item Sistema importa dados evitando duplicatas + \item Sistema exibe relatório de importação +\end{enumerate} + +\subsection{UC12: Gerar Relatório do Administrador (\#101)} +\textbf{Ator}: Administrador\\ +\textbf{Fluxo}: +\begin{enumerate} + \item Administrador acessa módulo de relatórios + \item Sistema oferece opções de filtros (período, turma, docente) + \item Administrador seleciona parâmetros desejados + \item Sistema processa dados e gera visualizações + \item Administrador pode exportar em PDF, Excel ou CSV +\end{enumerate} + +\section{Métricas do Projeto} + +\subsection{Sprint 1 - Velocity} +\begin{longtable}{|l|p{6cm}|c|} +\hline +\textbf{ID} & \textbf{Funcionalidade} & \textbf{Pontos} \\ +\hline +\endfirsthead +\hline +\textbf{ID} & \textbf{Funcionalidade} & \textbf{Pontos} \\ +\hline +\endhead +\#98 & Importar dados do SIGAA (base) & 5 \\ +\#99 & Responder formulário & 5 \\ +\#100 & Cadastrar usuários do sistema & 3 \\ +\#101 & Gerar relatório do administrador & 5 \\ +\#102 & Criar template de formulário & 8 \\ +\#103 & Criar formulário de avaliação & 5 \\ +\#104 & Sistema de Login & 5 \\ +\#105 & Sistema de definição de senha & 3 \\ +\#108 & Atualizar base de dados com dados do SIGAA & 5 \\ +\#109 & Visualização de formulários para responder & 3 \\ +\#110 & Visualização de resultados dos formulários & 8 \\ +\#111 & Visualização dos templates criados & 2 \\ +\#112 & Edição e deleção de templates & 5 \\ +\hline +\multicolumn{2}{|r|}{\textbf{Total (Velocity):}} & \textbf{61} \\ +\hline +\end{longtable} + +\subsection{Funcionalidades Bônus (Não Incluídas no Velocity)} +\begin{itemize} + \item \textbf{\#106} - Sistema de gerenciamento por departamento (8 pontos) + \item \textbf{\#107} - Redefinição de senha (3 pontos) + \item \textbf{\#113} - Criação de formulário para docentes ou dicentes (5 pontos) +\end{itemize} + +\section{Considerações Finais} + +O modelo apresentado reflete uma arquitetura robusta e escalável para o sistema \textbf{CAMAAR}. A estrutura permite: + +\begin{itemize} + \item \textbf{Flexibilidade}: Templates reutilizáveis e tipos variados de questões + \item \textbf{Escalabilidade}: Suporte a múltiplas turmas, matérias e semestres + \item \textbf{Auditoria}: Timestamps e status em todas as entidades críticas + \item \textbf{Integridade}: Relacionamentos bem definidos com constraints + \item \textbf{Extensibilidade}: Estrutura preparada para novas funcionalidades +\end{itemize} + +O sistema está em conformidade com as práticas recomendadas de desenvolvimento Rails e segue princípios SOLID, facilitando manutenção e evolução contínua. + +\section*{Referências} + +\begin{itemize} + \item Ruby on Rails Guides: \texttt{https://guides.rubyonrails.org/} + \item Cucumber Documentation: \texttt{https://cucumber.io/docs/} + \item GitHub Repository: \texttt{https://github.com/EngSwCIC/CAMAAR} + \item Figma Mockups: \texttt{https://www.figma.com/design/5GVzfaJSBbcXmGvuvAi7WF/} +\end{itemize} + +\end{document} diff --git a/features/100_cadastrar_usuarios.feature b/features/100_cadastrar_usuarios.feature new file mode 100644 index 0000000000..c5a55f33bb --- /dev/null +++ b/features/100_cadastrar_usuarios.feature @@ -0,0 +1,31 @@ +# language: en +Feature: Register system users (#100) + As an Administrator + I want to register participants of SIGAA classes by importing new users into the system + So that they can access the CAMAAR system + + Note: + Although this is called "registration", at this stage the user is created in a pending state + and a request to set a password is sent. The registration is only completed after the password is set. + + Background: + Given I am authenticated as an administrator + + Scenario: Import new participants and send password setup request + Given there are participants in SIGAA who do not have a user in the system yet + When I import the participants from SIGAA + Then the system creates pending activation records for each imported participant + And the system sends a password setup request email to each new participant + And the participants cannot access the system until they set their passwords + + Scenario: Do not create duplicate users on import + Given there is already a user registered for a SIGAA participant + When I import the participants from SIGAA again + Then the system does not create a new user for that participant + And the system does not send the password setup request again for that participant + + Scenario: Partial import failure + Given some SIGAA records are incomplete or invalid + When I import the participants from SIGAA + Then the system logs the import errors for review + And the system continues the import for the other valid participants diff --git a/features/102_create_a_form_template.feature b/features/102_create_a_form_template.feature new file mode 100644 index 0000000000..fda58be20e --- /dev/null +++ b/features/102_create_a_form_template.feature @@ -0,0 +1,76 @@ +# language: pt +# features/criar_template_formulario.feature + +Funcionalidade: Criar template de formulário (#102) + Como Administrador + Quero criar um template de formulário contendo as questões do formulário + A fim de gerar formulários de avaliações para avaliar o desempenho das turmas + + Contexto: + Dado que estou autenticado como administrador + E estou na página "gerenciamento/templates" do sistema + + @happy + Cenário: Criar template com questões dos três tipos (numérica 1-5, múltipla escolha e texto) + Quando eu abro o modal "Novo template" + E preencho o nome do template com "Template Avaliação 2025.1" + E adiciono a questão 1 do tipo "numérica (1-5)" com o texto "Satisfação geral" + E adiciono a questão 2 do tipo "múltipla escolha" com o texto "Infraestrutura" e as opções: + | Muito bom | + | Bom | + | Regular | + | Ruim | + E adiciono a questão 3 do tipo "texto" com o texto "Sugestões" + E clico em "Criar" + Então devo ver a mensagem "Template criado com sucesso" + E devo ver o card do template "Template Avaliação 2025.1" na listagem + + @happy + Cenário: Manter a ordem das questões após salvar + Dado que preenchi o modal com três questões em ordem + Quando eu clico em "Criar" + Então ao abrir o template salvo devo ver as questões na mesma ordem + + @sad + Cenário: Tentar criar template sem nome + Quando eu abro o modal "Novo template" + E adiciono a questão 1 do tipo "texto" com o texto "Comentário" + E clico em "Criar" + Então devo ver a mensagem "Informe o nome do template" + E o template não é criado + + @sad + Cenário: Múltipla escolha sem opções + Quando eu abro o modal "Novo template" + E preencho o nome do template com "Template Sem Opções" + E adiciono a questão 1 do tipo "múltipla escolha" com o texto "Escolha uma opção" e nenhuma opção + E clico em "Criar" + Então devo ver a mensagem "Adicione pelo menos duas opções" + E o template não é criado + + @sad + Cenário: Template sem nenhuma questão + Quando eu abro o modal "Novo template" + E preencho o nome do template com "Template Vazio" + E clico em "Criar" + Então devo ver a mensagem "Adicione pelo menos uma questão" + E o template não é criado + + @sad + Cenário: Nome duplicado no mesmo semestre + Dado que já existe um template chamado "Template Avaliação 2025.1" no semestre "2025.1" + Quando eu abro o modal "Novo template" + E preencho o nome do template com "Template Avaliação 2025.1" + E adiciono a questão 1 do tipo "texto" com o texto "Q1" + E clico em "Criar" + Então devo ver a mensagem "Já existe um template com esse nome no semestre selecionado" + E o template não é criado + + @sad + Cenário: Questão numérica fora da escala 1-5 (regra de negócio) + Quando eu abro o modal "Novo template" + E preencho o nome do template com "Template Numérico Inválido" + E tento configurar a questão 1 do tipo "numérica (1-5)" com escala fora de 1 a 5 + E clico em "Criar" + Então devo ver a mensagem "A escala numérica deve ser de 1 a 5" + E o template não é criado diff --git a/features/103_create_evaluation_form.feature b/features/103_create_evaluation_form.feature new file mode 100644 index 0000000000..0dccf35efa --- /dev/null +++ b/features/103_create_evaluation_form.feature @@ -0,0 +1,27 @@ +# language: en +Feature: Create evaluation form from template (#103) + As an Administrator + I want to create an evaluation form based on a template for the classes I choose + So that I can evaluate class performance in the current semester + + Background: + Given I am authenticated as an administrator + And there is at least one evaluation template available + And there are classes available for the current semester + + Scenario: Create forms for selected classes using a template + When I select a template and choose one or more classes + And I confirm the creation of forms + Then the system creates one evaluation form per selected class for the current semester + And I see a success message indicating how many forms were created + + Scenario: Prevent duplicate forms for the same class and semester + Given there is already an evaluation form for a class in the current semester + When I try to create forms again using the same template for the same class + Then the system does not create a new form for that class and semester + And I see a message indicating the form already exists + + Scenario: Validation error when no template is selected + When I try to create forms without selecting a template + Then I see a validation error indicating a template is required + And no forms are created diff --git a/features/105_password_setup.feature b/features/105_password_setup.feature new file mode 100644 index 0000000000..e25a56e878 --- /dev/null +++ b/features/105_password_setup.feature @@ -0,0 +1,32 @@ +# language: en +Feature: Password setup system (#105) + As a User + I want to set a password for my account using the link I receive by email from the registration request + So that I can access the system + + Background: + Given there is a pending user who received a password setup email with a valid token + + Scenario: Set password successfully from email link + When I open the password setup link + And I set a new valid password and confirmation + Then my account becomes active + And I can sign in with my new credentials + + Scenario: Invalid or expired token + Given my password setup token is invalid or expired + When I open the password setup link + Then I see an error indicating the link is invalid or expired + And I am offered a way to request a new password setup email + + Scenario: Password confirmation mismatch + When I open the password setup link + And I enter a password and a non-matching confirmation + Then I see a validation error about the confirmation not matching + And my account remains pending until I set a valid password + + Scenario: Token can be used only once + When I successfully set a password using the email link + And I try to use the same link again + Then I see an error that the token has already been used + And I remain signed in with my new credentials diff --git a/features/109_view_pending_forms.feature b/features/109_view_pending_forms.feature new file mode 100644 index 0000000000..68950e32d9 --- /dev/null +++ b/features/109_view_pending_forms.feature @@ -0,0 +1,25 @@ +# language: en +Feature: View forms to answer (#109) + As a Class Participant + I want to view the unanswered evaluation forms for the classes I am enrolled in + So that I can choose which one to answer + + Background: + Given I am signed in as a participant + And I am enrolled in one or more classes for the current semester + + Scenario: List unanswered forms for my classes + Given there are evaluation forms assigned to my classes that I have not answered yet + When I open the page to view my pending forms + Then I see a list of the unanswered forms for my classes + And each item shows the class, semester, and form title + + Scenario: Do not show forms I already answered + Given I have already answered an evaluation form for one of my classes + When I open the page to view my pending forms + Then that form is not listed among my pending forms + + Scenario: Empty state when there are no pending forms + Given there are no unanswered forms for my classes + When I open the page to view my pending forms + Then I see a message indicating there are no pending forms diff --git a/features/110_view_form_results.feature b/features/110_view_form_results.feature new file mode 100644 index 0000000000..cd68eda10b --- /dev/null +++ b/features/110_view_form_results.feature @@ -0,0 +1,69 @@ +# language: pt +# features/visualizacao_resultados_formularios.feature + +Funcionalidade: Visualização de resultados dos formulários (#110) + Como Administrador + Quero visualizar os formulários criados + A fim de poder gerar um relatório a partir das respostas + + Contexto: + Dado que estou logado como um administrador válido + + @happy + Cenário: Exibir a lista de formulários disponíveis + Dado que existem formulários para o semestre atual: + | título | + | Avaliação Docente | + | Avaliação da Infraestrutura | + Quando eu acesso a página "resultados" + Então devo visualizar os cards dos formulários "Avaliação Docente" e "Avaliação da Infraestrutura" + E cada card exibe semestre, professor e total de respostas + + @happy + Cenário: Acessar o detalhamento de um formulário com respostas + Dado que existe o formulário "Avaliação Docente" com respostas enviadas + Quando eu abro o card "Avaliação Docente" + Então vejo o resumo consolidado com gráficos e estatísticas + E devo ver a mensagem "Total de respostas: xx" + E o botão "Exportar resultados" deve estar habilitado + + @happy + Cenário: Buscar por professor ou título + Dado que existem formulários com professor "Maria Silva" + Quando eu pesquiso por "Maria" + Então apenas os formulários relacionados a "Maria Silva" são exibidos + + @sad + Cenário: Formulário sem respostas + Dado que existe o formulário "Avaliação da Infraestrutura" sem nenhuma resposta + Quando eu abro o card "Avaliação da Infraestrutura" + Então vejo a mensagem "Ainda não há respostas disponíveis" + E o botão "Exportar resultados" deve estar desabilitado + + @sad + Cenário: Nenhum formulário cadastrado + Dado que não existem formulários cadastrados + Quando eu acesso a página "resultados" + Então vejo a mensagem "Nenhum formulário disponível no momento" + E não há cards exibidos + + @sad + Cenário: Tentar acessar um formulário inexistente + Quando eu acesso diretamente o endereço "resultados/FormularioInexistente" + Então vejo a mensagem "O formulário solicitado não foi encontrado" + E permaneço na página de resultados + + @sad + Cenário: Busca sem correspondências + Dado que existem formulários cadastrados + Quando eu pesquiso por "termo-inexistente" + Então vejo a mensagem "Nenhum resultado para a busca" + E a lista de cards fica vazia + + @sad + Cenário: Falha ao exportar resultados + Dado que existe o formulário "Avaliação Docente" com respostas enviadas + E o serviço de exportação está indisponível + Quando tento exportar os resultados + Então vejo a mensagem "Não foi possível gerar o arquivo. Tente novamente mais tarde." + E nenhum arquivo é baixado diff --git a/features/111_view_templates.feature b/features/111_view_templates.feature new file mode 100644 index 0000000000..2d4d30c4d4 --- /dev/null +++ b/features/111_view_templates.feature @@ -0,0 +1,25 @@ +# language: pt +Funcionalidade: Visualização dos templates criados (#111) + Como Administrador + Quero visualizar os templates criados + A fim de poder editar e/ou deletar um template que eu criei + + Contexto: + Dado que estou autenticado como administrador + E estou na página "gerenciamento/templates" do sistema + + Cenário: Exibir a lista de templates criados pelo administrador atual + Dado que existem templates cadastrados para o administrador atual: + | nome | + | Template Avaliação 1 | + | Template Avaliação 2 | + Quando eu visualizo a listagem de templates + Então devo ver o card do template "Template Avaliação 1" na listagem + E devo ver o card do template "Template Avaliação 2" na listagem + E não devo ver templates criados por outros administradores + + Cenário: Administrador sem templates cadastrados + Dado que não existem templates cadastrados para o administrador atual + Quando eu visualizo a listagem de templates + Então devo ver a mensagem "Nenhum template cadastrado ainda" + E a listagem de templates fica vazia diff --git a/features/112_edit_delete_templates.feature b/features/112_edit_delete_templates.feature new file mode 100644 index 0000000000..2ce5a04df8 --- /dev/null +++ b/features/112_edit_delete_templates.feature @@ -0,0 +1,24 @@ +# language: pt +Funcionalidade: Edição e deleção de templates (#112) + Como Administrador + Quero editar e/ou deletar um template que eu criei sem afetar os formulários já criados + A fim de organizar os templates existentes + + Contexto: + Dado que estou autenticado como administrador + E estou na página "gerenciamento/templates" do sistema + + Cenário: Editar um template sem alterar os formulários já criados + Dado que existe o template "Avaliação Docente 2025.1" associado a 2 formulários já criados + Quando eu acesso a edição do template "Avaliação Docente 2025.1" + E altero o nome do template para "Avaliação Docente 2025.1 - Revisado" + E salvo as alterações do template + Então devo ver a mensagem "Template atualizado com sucesso" + E devo ver o card do template "Avaliação Docente 2025.1 - Revisado" na listagem + + Cenário: Deletar um template sem remover os formulários já criados + Dado que existe o template "Avaliação Docente 2025.1" associado a 3 formulários já criados + Quando eu excluo o template "Avaliação Docente 2025.1" + E confirmo a exclusão do template + Então devo ver a mensagem "Template removido com sucesso" + E o template "Avaliação Docente 2025.1" não aparece mais na listagem de templates diff --git a/features/113_import_sigaa_data.feature b/features/113_import_sigaa_data.feature new file mode 100644 index 0000000000..0942b35999 --- /dev/null +++ b/features/113_import_sigaa_data.feature @@ -0,0 +1,38 @@ +# language: pt +Funcionalidade: Importação de dados do SIGAA (#113) + Como Administrador + Quero importar dados de turmas, matérias e participantes do SIGAA (caso não existam na base de dados atual) + A fim de alimentar a base de dados do sistema + + Contexto: + Dado que estou autenticado como administrador + E estou na página "importacao/sigaa" do sistema + E existem arquivos JSON de turmas, matérias e participantes disponíveis no repositório + + Cenário: Importar novos dados do SIGAA com sucesso + Dado que existem turmas, matérias e participantes nos arquivos JSON que ainda não existem na base de dados + Quando eu seleciono os arquivos JSON do SIGAA para importação + E confirmo a importação dos dados + Então as turmas do SIGAA que não existiam são cadastradas na base de dados + E as matérias do SIGAA que não existiam são cadastradas na base de dados + E os participantes do SIGAA que não existiam são cadastrados na base de dados + E devo ver a mensagem "Importação concluída com sucesso" + + Cenário: Evitar duplicação de dados já existentes na base + Dado que algumas turmas, matérias e participantes dos arquivos JSON já existem na base de dados + Quando eu realizo a importação dos dados do SIGAA + Então os registros já existentes não são duplicados na base de dados + E apenas os registros inexistentes são criados + E devo ver a mensagem "Importação concluída: X novos registros criados, Y registros ignorados por já existirem" + + Cenário: Erro de validação quando o arquivo JSON está inválido + Dado que o arquivo JSON de turmas está em formato inválido + Quando eu tento importar os dados do SIGAA + Então devo ver a mensagem "Não foi possível processar o arquivo JSON de turmas" + E nenhum dado é importado a partir desse arquivo inválido + + Cenário: Erro quando nenhum arquivo é selecionado + Dado que não selecionei nenhum arquivo JSON para importação + Quando eu tento iniciar a importação dos dados do SIGAA + Então devo ver a mensagem "Selecione ao menos um arquivo JSON para importação" + E nenhum dado é importado diff --git a/features/99_answer_a_form.feature b/features/99_answer_a_form.feature new file mode 100644 index 0000000000..c64578851f --- /dev/null +++ b/features/99_answer_a_form.feature @@ -0,0 +1,47 @@ +# language: pt +# features/responder_formulario.feature + +Funcionalidade: Responder formulário (#99) + Como Participante de uma turma + Quero responder o questionário sobre a turma em que estou matriculado + A fim de submeter minha avaliação da turma + + Contexto: + Dado que estou autenticado como um participante válido + E existe o formulário "Avaliação - Programação I - 2025.1" disponível para minha turma + + @happy + Cenário: Responder todas as perguntas obrigatórias e enviar o formulário com sucesso + Dado que estou na página "formularios/Avaliação - Programação I - 2025.1" + Quando preencho todas as perguntas obrigatórias com respostas válidas + E clico no botão "Enviar" + Então devo ver a mensagem "Avaliação enviada com sucesso!" + E o sistema registra minhas respostas no banco de dados + E o formulário é bloqueado para nova submissão + + @happy + Cenário: Visualizar perguntas e tipos de resposta + Quando eu acesso a página "formularios/Avaliação - Programação I - 2025.1" + Então devo ver perguntas com alternativas de múltipla escolha + E devo ver perguntas com campos de texto + E devo ver o botão "Enviar" desabilitado até preencher todos os campos obrigatórios + + @sad + Cenário: Tentar enviar o formulário sem responder todas as perguntas obrigatórias + Quando deixo perguntas obrigatórias em branco + E clico em "Enviar" + Então devo ver a mensagem "Responda todas as perguntas obrigatórias antes de enviar" + E o formulário não é submetido + + @sad + Cenário: Tentar acessar um formulário que não pertence à turma do participante + Quando eu acesso diretamente o endereço "formularios/Avaliação - Programação II - 2025.1" + Então devo ver a mensagem "Você não tem permissão para responder este formulário" + E sou redirecionado para a página inicial + + @sad + Cenário: Falha de conexão ao enviar o formulário + Dado que o servidor está indisponível + Quando eu clico em "Enviar" + Então devo ver a mensagem "Erro ao enviar o formulário. Tente novamente mais tarde." + E as respostas não são perdidas localmente (mantêm-se visíveis na tela) diff --git a/features/base_dados.feature b/features/base_dados.feature new file mode 100644 index 0000000000..87986647da --- /dev/null +++ b/features/base_dados.feature @@ -0,0 +1,54 @@ +Feature: Database Update + As an Administrator + I want to update the existing database with current SIGAA data + In order to correct the system database + + Background: + Given that I am logged in as an administrator + + Scenario: Successfully update database with SIGAA data + Given that I am on the Gerenciamento page + When I click on "Importar Database" + And the system connects to SIGAA successfully + Then I should see "Database update started" + And the system should synchronize data from SIGAA + Then I should see "Database updated successfully" + And the database should contain the latest SIGAA data + + Scenario: Verify updated data in database + Given that the database was updated with SIGAA data + When I access the database management page + Then I should see updated student records + And I should see updated class records + And I should see updated enrollment records + And the data should match the current SIGAA data + + Scenario: Update database with connection error to SIGAA + Given that I am on the Gerenciamento page + When I click on "Importar Dados" + And the system cannot connect to SIGAA + Then I should see "Error connecting to SIGAA" + And the database should remain unchanged + And I should see an option to retry the connection + + Scenario: Update database when already up to date + Given that the database was recently updated + And the database is already synchronized with SIGAA + When I click on "Importar Dados" + Then I should see "Database is already up to date" + And no data should be modified + + Scenario: Update database without administrator permissions + Given that I am logged in as a regular user + When I try to access the database update page + Then I should see "Access denied" + And I should be redirected to the home page + + Scenario: Partial update when some SIGAA data is unavailable + Given that I am on the database update page + When I click on "Update Database" + And some SIGAA data is temporarily unavailable + Then I should see "Partial update completed" + And the available data should be updated + And I should see a warning about unavailable data + diff --git a/features/relatorio_admin.feature b/features/relatorio_admin.feature new file mode 100644 index 0000000000..7176a71ffe --- /dev/null +++ b/features/relatorio_admin.feature @@ -0,0 +1,22 @@ +Feature: Export Form Results to CSV + As an Administrator + I want to download a CSV file containing form results + In order to evaluate class performance + + Background: + Given that I am logged in as an administrator + + Scenario: Successfully download CSV file with form results + Given that I am on the form results page + And there are form results available + When I click on "Download CSV" + Then a CSV file should be downloaded + And the file should be named "form_results.csv" + And the CSV file should contain form result data + + Scenario: Download CSV when no form results are available + Given that I am on the form results page + And there are no form results available + When I click on "Download CSV" + Then I should see "No form results available for export" + And no file should be downloaded \ No newline at end of file diff --git a/features/sistema_login.feature b/features/sistema_login.feature new file mode 100644 index 0000000000..ebc51058ea --- /dev/null +++ b/features/sistema_login.feature @@ -0,0 +1,25 @@ +Feature: Login System + As a System User + I want to access the system using an email or registration number and a registered password + In order to answer forms or manage the system + + Scenario: Login with valid credentials + Given that I am on the login page + When I fill in the "email" field with "usuario@example.com" + And I fill in the "password" field with "senha123" + And I click on "Entrar" + Then I should be on the evaluations page + + Scenario: Login with invalid email + Given that I am on the login page + When I fill in the "email" field with "invalido@example.com" + And I fill in the "password" field with "senha123" + And I click on "Entrar" + Then I should see "Invalid email or password" + + Scenario: Login with invalid password + Given that I am on the login page + When I fill in the "email" field with "usuario@example.com" + And I fill in the "password" field with "senhainvalida123" + And I click on "Entrar" + Then I should see "Invalid email or password" \ No newline at end of file diff --git a/features/step_definitions/.keep b/features/step_definitions/.keep new file mode 100644 index 0000000000..d958d9b0fc --- /dev/null +++ b/features/step_definitions/.keep @@ -0,0 +1 @@ +git checkout -b sprint1bdd diff --git a/features/step_definitions/102_create_a_form_template.rb b/features/step_definitions/102_create_a_form_template.rb new file mode 100644 index 0000000000..fb2f896031 --- /dev/null +++ b/features/step_definitions/102_create_a_form_template.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +Dado('que estou autenticado como administrador') do + @admin = FactoryBot.create(:user, :admin) + login_as(@admin) +end + +Dado('estou na página {string} do sistema') do |path| + visit("/#{path}") +end + +Quando('eu abro o modal {string}') do |_titulo| + find("[data-testid='botao-novo-template']").click + expect(page).to have_selector("[data-testid='modal-template']", visible: true) +end + +Quando('preencho o nome do template com {string}') do |nome| + find("[data-testid='input-nome-template']").set(nome) +end + +Quando('adiciono a questão {int} do tipo {string} com o texto {string}') do |ordem, tipo, texto| + within("[data-testid='modal-template']") do + find("[data-testid='botao-adicionar-questao']").click + bloco = all("[data-testid='bloco-questao']").last + bloco.find("[data-testid='select-tipo-questao']").select(tipo) + bloco.find("[data-testid='input-texto-questao']").set(texto) + bloco.find("[data-testid='input-ordem']").set(ordem) if bloco.has_selector?("[data-testid='input-ordem']") + end +end + +Quando('adiciono a questão {int} do tipo {string} com o texto {string} e as opções:') do |ordem, tipo, texto, table| + step %(adiciono a questão #{ordem} do tipo "#{tipo}" com o texto "#{texto}") + bloco = all("[data-testid='bloco-questao']").last + table.raw.flatten.each do |opcao| + bloco.find("[data-testid='input-opcao']").set(opcao) + bloco.find("[data-testid='botao-adicionar-opcao']").click + end +end + +Quando('adiciono a questão {int} do tipo {string} com o texto {string} e nenhuma opção') do |ordem, tipo, texto| + step %(adiciono a questão #{ordem} do tipo "#{tipo}" com o texto "#{texto}") + # intencionalmente não adiciona opções +end + +Quando('tento configurar a questão {int} do tipo {string} com escala fora de 1 a 5') do |_ordem, _tipo| + # se a UI permitir range, tenta setar valores inválidos + if page.has_selector?("[data-testid='input-escala-min']") && page.has_selector?("[data-testid='input-escala-max']") + find("[data-testid='input-escala-min']").set('0') + find("[data-testid='input-escala-max']").set('6') + else + # se a escala for fixa 1-5 e não editável, apenas marca o tipo e segue + # (a validação ficará no backend; mantemos o step por consistência) + end +end + +Quando('clico em {string}') do |rotulo| + testid = case rotulo.downcase + when 'criar' then 'botao-criar-template' + else "botao-#{rotulo.downcase.tr(' ', '-')}" + end + find("[data-testid='#{testid}']").click +end + +Dado('já existe um template chamado {string} no semestre {string}') do |nome, semestre| + FactoryBot.create(:template, nome:, semestre:) +end + +Dado('que preenchi o modal com três questões em ordem') do + step %(eu abro o modal "Novo template") + step %(preencho o nome do template com "Template Ordenado") + step %(adiciono a questão 1 do tipo "numérica (1-5)" com o texto "Q1") + step %(adiciono a questão 2 do tipo "múltipla escolha" com o texto "Q2" e as opções:), Cucumber::MultilineArgument::DataTable.from([ [ 'A' ], [ 'B' ] ]) + step %(adiciono a questão 3 do tipo "texto" com o texto "Q3") +end + +Então('devo ver a mensagem {string}') do |mensagem| + expect(page).to have_content(mensagem) +end + +Então('devo ver o card do template {string} na listagem') do |nome| + expect(page).to have_selector("[data-testid='template-card']", text: nome) +end + +Então('ao abrir o template salvo devo ver as questões na mesma ordem') do + find("[data-testid='template-card']", text: 'Template Ordenado').click + textos = all("[data-testid='bloco-questao'] [data-testid='input-texto-questao']").map(&:value) + expect(textos).to eq([ 'Q1', 'Q2', 'Q3' ]) +end + +Então('o template não é criado') do + # continua com modal aberto ou não aparece novo card + expect(page).not_to have_selector("[data-testid='template-card']", text: /Template/i) +end diff --git a/features/step_definitions/110_view_form_results.rb b/features/step_definitions/110_view_form_results.rb new file mode 100644 index 0000000000..e50cc9adf6 --- /dev/null +++ b/features/step_definitions/110_view_form_results.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +Dado('que estou logado como um administrador válido') do + @admin = FactoryBot.create(:user, :admin) + login_as(@admin) +end + +Dado('que não existem formulários cadastrados') do + Formulario.delete_all +end + +Dado('que existem formulários para o semestre atual:') do |table| + table.hashes.each do |row| + FactoryBot.create(:formulario, titulo: row['título'], semestre: '2025.1') + end +end + +Dado('que existe o formulário {string} com {int} respostas enviadas') do |titulo, total| + formulario = FactoryBot.create(:formulario, titulo: titulo, semestre: '2025.1') + FactoryBot.create_list(:resposta, total, formulario: formulario) +end + +Dado('que existem formulários com professor {string}') do |nome| + FactoryBot.create(:formulario, titulo: 'Avaliação Docente', professor: nome, semestre: '2025.1') + FactoryBot.create(:formulario, titulo: 'Avaliação da Infraestrutura', professor: 'Outro', semestre: '2025.1') +end + +Dado('que existem formulários de semestres diferentes') do + FactoryBot.create(:formulario, titulo: 'Avaliação Docente', semestre: '2025.1') + FactoryBot.create(:formulario, titulo: 'Avaliação Docente', semestre: '2024.2') +end + +Dado('que existe o formulário {string} sem nenhuma resposta') do |titulo| + FactoryBot.create(:formulario, titulo: titulo, semestre: '2025.1') +end + +Dado('o serviço de exportação está indisponível') do + allow(ExportadorResultadosService).to receive(:call).and_raise(StandardError, 'falha') +end + +# -------- Ações -------- + +Quando('eu acesso a página {string}') do |path| + visit("/#{path}") +end + +Quando('eu acesso diretamente o endereço {string}') do |path| + visit("/#{path}") +end + +Quando('eu abro o card {string}') do |titulo| + find("[data-testid='form-card']", text: titulo).click +end + +Quando('eu pesquiso por {string}') do |termo| + find("[data-testid='campo-busca']").set(termo) + find("[data-testid='botao-buscar']").click +end + +Quando('seleciono o filtro de semestre {string}') do |semestre| + find("[data-testid='filtro-semestre']").select(semestre) +end + +Quando('tento exportar os resultados') do + find("[data-testid='botao-exportar']").click +end + +# -------- Verificações -------- + +Então('devo visualizar os cards dos formulários {string} e {string}') do |t1, t2| + expect(page).to have_selector("[data-testid='form-card']", text: t1) + expect(page).to have_selector("[data-testid='form-card']", text: t2) +end + +Então('cada card exibe semestre, professor e total de respostas') do + all("[data-testid='form-card']").each do |card| + expect(card).to have_selector("[data-testid='info-semestre']") + expect(card).to have_selector("[data-testid='info-professor']") + expect(card).to have_selector("[data-testid='info-total-respostas']") + end +end + +Então('vejo o resumo consolidado com gráficos e estatísticas') do + expect(page).to have_selector("[data-testid='resumo-formulario']") + expect(page).to have_selector("[data-testid='grafico-resultados']") +end + +Então('devo ver a mensagem {string}') do |texto| + expect(page).to have_content(texto) +end + +Então('o botão {string} deve estar desabilitado') do |rotulo| + # mapeia "Exportar resultados" -> botao-exportar-resultados + testid = "botao-#{rotulo.downcase.tr(' ', '-')}" + expect(page).to have_selector("[data-testid='#{testid}'][disabled]") +end + +Então('permaneço na página de resultados') do + expect(URI.parse(current_url).path).to eq('/resultados') +end + +Então('vejo apenas os formulários do semestre {string}') do |semestre| + # todos exibidos com aquele semestre + expect(page).to have_selector("[data-testid='info-semestre']", text: semestre) + # e não aparece outro semestre +end + +Então('nenhum arquivo é baixado') do + # Depende de como é feito; validar ausência de toast de sucesso: + expect(page).not_to have_content('Relatório exportado com sucesso') +end + +Então('o botão "Exportar resultados" deve estar habilitado') do + expect(page).to have_selector("[data-testid='botao-exportar']", visible: true) + expect(find("[data-testid='botao-exportar']")).not_to be_disabled +end diff --git a/features/step_definitions/111_view_templates.rb b/features/step_definitions/111_view_templates.rb new file mode 100644 index 0000000000..dc801b45fa --- /dev/null +++ b/features/step_definitions/111_view_templates.rb @@ -0,0 +1 @@ +# Step definitions placeholder diff --git a/features/step_definitions/112_edit_delete_templates.rb b/features/step_definitions/112_edit_delete_templates.rb new file mode 100644 index 0000000000..dc801b45fa --- /dev/null +++ b/features/step_definitions/112_edit_delete_templates.rb @@ -0,0 +1 @@ +# Step definitions placeholder diff --git a/features/step_definitions/113_import_sigaa_data.rb b/features/step_definitions/113_import_sigaa_data.rb new file mode 100644 index 0000000000..81dec26fd6 --- /dev/null +++ b/features/step_definitions/113_import_sigaa_data.rb @@ -0,0 +1,127 @@ +# frozen_string_literal: true + +# Step definitions para a feature de importação de dados do SIGAA (#113). +# IMPORTANTE: +# - Ajuste os nomes dos models (Turma, Materia, Participante) conforme o projeto. +# - Ajuste caminhos dos arquivos JSON e os identificadores de campos/botões da view. + +Dado('que estou autenticado como administrador') do + # Se esse step já existir em outro arquivo, remova esta definição para evitar duplicação. + @admin = FactoryBot.create(:user, :admin) + login_as(@admin, scope: :user) +end + +Dado('estou na página "importacao/sigaa" do sistema') do + visit('/importacao/sigaa') +end + +Dado('existem arquivos JSON de turmas, matérias e participantes disponíveis no repositório') do + @turmas_json_path = Rails.root.join('spec/fixtures/sigaa/turmas.json') + @materias_json_path = Rails.root.join('spec/fixtures/sigaa/materias.json') + @participantes_json_path = Rails.root.join('spec/fixtures/sigaa/participantes.json') + + expect(File.exist?(@turmas_json_path)).to be(true) + expect(File.exist?(@materias_json_path)).to be(true) + expect(File.exist?(@participantes_json_path)).to be(true) +end + +Dado('que existem turmas, matérias e participantes nos arquivos JSON que ainda não existem na base de dados') do + Turma.delete_all + Materia.delete_all + Participante.delete_all + + @turmas_antes = Turma.count + @materias_antes = Materia.count + @participantes_antes = Participante.count +end + +Quando('eu seleciono os arquivos JSON do SIGAA para importação') do + attach_file('turmas_file', @turmas_json_path) + attach_file('materias_file', @materias_json_path) + attach_file('participantes_file', @participantes_json_path) +end + +Quando('eu confirmo a importação dos dados') do + click_button 'Importar dados do SIGAA' +end + +Então('as turmas do SIGAA que não existiam são cadastradas na base de dados') do + expect(Turma.count).to be > @turmas_antes +end + +Então('as matérias do SIGAA que não existiam são cadastradas na base de dados') do + expect(Materia.count).to be > @materias_antes +end + +Então('os participantes do SIGAA que não existiam são cadastrados na base de dados') do + expect(Participante.count).to be > @participantes_antes +end + +Então('devo ver a mensagem {string}') do |mensagem| + expect(page).to have_content(mensagem) +end + +Dado('que algumas turmas, matérias e participantes dos arquivos JSON já existem na base de dados') do + Turma.delete_all + Materia.delete_all + Participante.delete_all + + @turma_existente = FactoryBot.create(:turma, codigo: 'TURMA_EXISTENTE') + @materia_existente = FactoryBot.create(:materia, codigo: 'MAT_EXISTENTE') + @participante_existente = FactoryBot.create(:participante, matricula: 'PART_EXISTENTE') + + @turmas_antes = Turma.count + @materias_antes = Materia.count + @participantes_antes = Participante.count +end + +Quando('eu realizo a importação dos dados do SIGAA') do + step 'eu seleciono os arquivos JSON do SIGAA para importação' + step 'eu confirmo a importação dos dados' +end + +Então('os registros já existentes não são duplicados na base de dados') do + expect(Turma.where(id: @turma_existente.id).count).to eq(1) + expect(Materia.where(id: @materia_existente.id).count).to eq(1) + expect(Participante.where(id: @participante_existente.id).count).to eq(1) +end + +Então('apenas os registros inexistentes são criados') do + expect(Turma.count).to be >= @turmas_antes + expect(Materia.count).to be >= @materias_antes + expect(Participante.count).to be >= @participantes_antes +end + +Dado('que o arquivo JSON de turmas está em formato inválido') do + @turmas_json_path = Rails.root.join('spec/fixtures/sigaa/turmas_invalido.json') + File.write(@turmas_json_path, 'isso_nao_e_um_json_valido') unless File.exist?(@turmas_json_path) + @turmas_antes = Turma.count +end + +Quando('eu tento importar os dados do SIGAA') do + attach_file('turmas_file', @turmas_json_path) + attach_file('materias_file', @materias_json_path) if defined?(@materias_json_path) + attach_file('participantes_file', @participantes_json_path) if defined?(@participantes_json_path) + + click_button 'Importar dados do SIGAA' +end + +Então('nenhum dado é importado a partir desse arquivo inválido') do + expect(Turma.count).to eq(@turmas_antes) +end + +Dado('que não selecionei nenhum arquivo JSON para importação') do + @turmas_antes = Turma.count + @materias_antes = Materia.count + @participantes_antes = Participante.count +end + +Quando('eu tento iniciar a importação dos dados do SIGAA') do + click_button 'Importar dados do SIGAA' +end + +Então('nenhum dado é importado') do + expect(Turma.count).to eq(@turmas_antes) + expect(Materia.count).to eq(@materias_antes) + expect(Participante.count).to eq(@participantes_antes) +end diff --git a/features/step_definitions/99_answer_a_form.rb b/features/step_definitions/99_answer_a_form.rb new file mode 100644 index 0000000000..f1545b243b --- /dev/null +++ b/features/step_definitions/99_answer_a_form.rb @@ -0,0 +1,137 @@ +# frozen_string_literal: true + +# ------------------------------------------------------------ +# Contexto / Given +# ------------------------------------------------------------ + +Dado('que estou autenticado como um participante válido') do + @aluno = FactoryBot.create(:user, :participant) + login_as(@aluno) +end + +Dado('existe o formulário {string} disponível para minha turma') do |titulo| + # Ajuste os factories/modelos conforme seu domínio. + @turma = FactoryBot.create(:turma) + @form = FactoryBot.create(:formulario, titulo:, turma: @turma, semestre: '2025.1', publicado: true) + + # Relacione o aluno à turma + FactoryBot.create(:matricula, user: @aluno, turma: @turma) + + # Exemplo de perguntas obrigatórias (múltipla escolha e texto) + @q1 = FactoryBot.create(:pergunta, formulario: @form, tipo: :radio, obrigatoria: true, titulo: 'Satisfação geral') + @q2 = FactoryBot.create(:pergunta, formulario: @form, tipo: :texto, obrigatoria: true, titulo: 'Comentário') + + # Alternativas para a pergunta de radio + %w[ Muito\ bom Bom Satisfatório Ruim Péssimo ].each do |op| + FactoryBot.create(:alternativa, pergunta: @q1, texto: op) + end +end + +Dado('que o servidor está indisponível') do + # Simula falha na camada de envio/submissão + allow(FormSubmissions::Create).to receive(:call).and_raise(StandardError, 'indisponível') +rescue NameError + # Caso ainda não exista o serviço, "stub" um endpoint genérico usado no submit + allow_any_instance_of(ApplicationController).to receive(:perform_submission).and_raise(StandardError, 'indisponível') +end + +# ------------------------------------------------------------ +# Ações / When +# ------------------------------------------------------------ + +Quando('estou na página {string}') do |path| + visit("/#{path}") +end + +Quando('eu acesso a página {string}') do |path| + visit("/#{path}") +end + +Quando('preencho todas as perguntas obrigatórias com respostas válidas') do + # Múltipla escolha: pegar a primeira opção disponível + all("[data-testid='pergunta-radio']").each do |bloco| + bloco.first("[data-testid='alternativa-opcao']").click + end + + # Texto: preencher com alguma mensagem + all("[data-testid='pergunta-texto'] [data-testid='campo-texto-pergunta']").each do |campo| + campo.set('Resposta de teste') + end +end + +Quando('deixo perguntas obrigatórias em branco') do + # Não faz nada de propósito — garante que existam obrigatórias visíveis + expect(page).to have_selector("[data-testid='pergunta-radio']", minimum: 1) +end + +Quando('clico no botão {string}') do |rotulo| + testid = case rotulo + when /enviar/i then 'botao-enviar' + else "botao-#{rotulo.downcase.tr(' ', '-')}" + end + find("[data-testid='#{testid}']").click +end + +Quando('eu clico em {string}') do |rotulo| + step %(clico no botão "#{rotulo}") +end + +# ------------------------------------------------------------ +# Verificações / Then +# ------------------------------------------------------------ + +Então('devo ver a mensagem {string}') do |texto| + # mensagens podem aparecer em toasts/alerts + expect(page).to have_content(texto) +end + +Então('o sistema registra minhas respostas no banco de dados') do + # Ajuste o modelo conforme seu domínio (ex.: Submissao, RespostaFormulario, etc.) + # Exemplo genérico: + expect(Resposta.where(usuario: @aluno, formulario: @form).count).to be > 0 +rescue NameError + # Mantém o step “passável” caso o modelo ainda não exista na etapa 2 + pending 'Modelo de persistência de respostas ainda não definido (ajuste este step quando implementar).' +end + +Então('o formulário é bloqueado para nova submissão') do + # Botão enviar desabilitado ou mensagem de “já respondido” + if page.has_selector?("[data-testid='botao-enviar']") + expect(find("[data-testid='botao-enviar']")[:disabled]).to be_truthy + else + expect(page).to have_content('Você já respondeu este formulário').or have_content('Formulário encerrado') + end +end + +Então('devo ver perguntas com alternativas de múltipla escolha') do + expect(page).to have_selector("[data-testid='pergunta-radio']", minimum: 1) + expect(page).to have_selector("[data-testid='alternativa-opcao']", minimum: 1) +end + +Então('devo ver perguntas com campos de texto') do + expect(page).to have_selector("[data-testid='pergunta-texto']", minimum: 1) + expect(page).to have_selector("[data-testid='campo-texto-pergunta']", minimum: 1) +end + +Então('devo ver o botão {string} desabilitado até preencher todos os campos obrigatórios') do |rotulo| + testid = rotulo =~ /enviar/i ? 'botao-enviar' : "botao-#{rotulo.downcase.tr(' ', '-')}" + expect(find("[data-testid='#{testid}']", visible: true)[:disabled]).to be_truthy +end + +Então('o formulário não é submetido') do + # Continua na mesma página e não mostra sucesso + expect(page).not_to have_content('Avaliação enviada com sucesso!') +end + +Então('sou redirecionado para a página inicial') do + expect(URI.parse(current_url).path).to eq('/') +end + +Então('as respostas não são perdidas localmente \(mantêm-se visíveis na tela\)') do + # Campos ainda preenchidos após erro de envio + all("[data-testid='pergunta-texto'] [data-testid='campo-texto-pergunta']").each do |campo| + expect(campo.value).to be_present + end + # Pelo menos uma alternativa de rádio continua marcada + expect(page).to have_selector("[data-testid='alternativa-opcao']:checked", minimum: 1, wait: 1) +end diff --git a/features/support/env.rb b/features/support/env.rb new file mode 100644 index 0000000000..3b97d14087 --- /dev/null +++ b/features/support/env.rb @@ -0,0 +1,53 @@ +# IMPORTANT: This file is generated by cucumber-rails - edit at your own peril. +# It is recommended to regenerate this file in the future when you upgrade to a +# newer version of cucumber-rails. Consider adding your own code to a new file +# instead of editing this one. Cucumber will automatically load all features/**/*.rb +# files. + + +require 'cucumber/rails' + +# By default, any exception happening in your Rails application will bubble up +# to Cucumber so that your scenario will fail. This is a different from how +# your application behaves in the production environment, where an error page will +# be rendered instead. +# +# Sometimes we want to override this default behaviour and allow Rails to rescue +# exceptions and display an error page (just like when the app is running in production). +# Typical scenarios where you want to do this is when you test your error pages. +# There are two ways to allow Rails to rescue exceptions: +# +# 1) Tag your scenario (or feature) with @allow-rescue +# +# 2) Set the value below to true. Beware that doing this globally is not +# recommended as it will mask a lot of errors for you! +# +ActionController::Base.allow_rescue = false + +# Remove/comment out the lines below if your app doesn't have a database. +# For some databases (like MongoDB and CouchDB) you may need to use :truncation instead. +begin + DatabaseCleaner.strategy = :transaction +rescue NameError + raise "You need to add database_cleaner to your Gemfile (in the :test group) if you wish to use it." +end + +# You may also want to configure DatabaseCleaner to use different strategies for certain features and scenarios. +# See the DatabaseCleaner documentation for details. Example: +# +# Before('@no-txn,@selenium,@culerity,@celerity,@javascript') do +# # { except: [:widgets] } may not do what you expect here +# # as Cucumber::Rails::Database.javascript_strategy overrides +# # this setting. +# DatabaseCleaner.strategy = :truncation +# end +# +# Before('not @no-txn', 'not @selenium', 'not @culerity', 'not @celerity', 'not @javascript') do +# DatabaseCleaner.strategy = :transaction +# end +# + +# Possible values are :truncation and :transaction +# The :transaction strategy is faster, but might give you threading problems. +# See https://github.com/cucumber/cucumber-rails/blob/master/features/choose_javascript_database_strategy.feature +Cucumber::Rails::Database.javascript_strategy = :truncation diff --git a/lib/tasks/.keep b/lib/tasks/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/tasks/cucumber.rake b/lib/tasks/cucumber.rake new file mode 100644 index 0000000000..b790c4d483 --- /dev/null +++ b/lib/tasks/cucumber.rake @@ -0,0 +1,68 @@ +# 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/log/.keep b/log/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/public/400.html b/public/400.html new file mode 100644 index 0000000000..640de03397 --- /dev/null +++ b/public/400.html @@ -0,0 +1,135 @@ + + + + + + + The server cannot process the request due to a client error (400 Bad Request) + + + + + + + + + + + + + +
+
+ +
+
+

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

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

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

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

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

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

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

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

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

+
+
+ + + + diff --git a/public/icon.png b/public/icon.png new file mode 100644 index 0000000000..c4c9dbfbbd Binary files /dev/null and b/public/icon.png differ diff --git a/public/icon.svg b/public/icon.svg new file mode 100644 index 0000000000..04b34bf83f --- /dev/null +++ b/public/icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000000..c19f78ab68 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1 @@ +# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file diff --git a/script/.keep b/script/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/storage/.keep b/storage/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb new file mode 100644 index 0000000000..cee29fd214 --- /dev/null +++ b/test/application_system_test_case.rb @@ -0,0 +1,5 @@ +require "test_helper" + +class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + driven_by :selenium, using: :headless_chrome, screen_size: [ 1400, 1400 ] +end diff --git a/test/controllers/.keep b/test/controllers/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/fixtures/files/.keep b/test/fixtures/files/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/helpers/.keep b/test/helpers/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/integration/.keep b/test/integration/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/mailers/.keep b/test/mailers/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/models/.keep b/test/models/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/system/.keep b/test/system/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 0000000000..0c22470ec1 --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,15 @@ +ENV["RAILS_ENV"] ||= "test" +require_relative "../config/environment" +require "rails/test_help" + +module ActiveSupport + class TestCase + # Run tests in parallel with specified workers + parallelize(workers: :number_of_processors) + + # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. + fixtures :all + + # Add more helper methods to be used by all tests here... + end +end diff --git a/tmp/.keep b/tmp/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tmp/pids/.keep b/tmp/pids/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tmp/storage/.keep b/tmp/storage/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/vendor/.keep b/vendor/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/vendor/javascript/.keep b/vendor/javascript/.keep new file mode 100644 index 0000000000..e69de29bb2