diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..fbc400407d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,51 @@ +# See https://docs.docker.com/engine/reference/builder/#dockerignore-file for more about ignoring files. + +# Ignore git directory. +/.git/ +/.gitignore + +# Ignore bundler config. +/.bundle + +# Ignore all environment files. +/.env* + +# Ignore all default key files. +/config/master.key +/config/credentials/*.key + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/.keep + +# Ignore storage (uploaded files in development and any SQLite databases). +/storage/* +!/storage/.keep +/tmp/storage/* +!/tmp/storage/.keep + +# Ignore assets. +/node_modules/ +/app/assets/builds/* +!/app/assets/builds/.keep +/public/assets + +# Ignore CI service files. +/.github + +# Ignore Kamal files. +/config/deploy*.yml +/.kamal + +# Ignore development files +/.devcontainer + +# Ignore Docker-related files +/.dockerignore +/Dockerfile* diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..0ac91dd2d8 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +# See https://git-scm.com/docs/gitattributes for more about git attribute files. + +# Mark the database schema as having been generated. +db/schema.rb linguist-generated + +# Mark any vendored files as having been vendored. +vendor/* linguist-vendored +config/credentials/*.yml.enc diff=rails_credentials +config/credentials.yml.enc diff=rails_credentials diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..c815c2cd7d --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: +- package-ecosystem: bundler + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 +- package-ecosystem: github-actions + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000..653b137e79 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,90 @@ +name: CI + +on: + pull_request: + push: + branches: [ main ] + +jobs: + scan_ruby: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + - name: Scan for common Rails security vulnerabilities using static analysis + run: bin/brakeman --no-pager + + scan_js: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + - name: Scan for security vulnerabilities in JavaScript dependencies + run: bin/importmap audit + + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + - name: Lint code for consistent style + run: bin/rubocop -f github + + test: + runs-on: ubuntu-latest + + # services: + # redis: + # image: redis + # ports: + # - 6379:6379 + # options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 + steps: + - name: Install packages + run: sudo apt-get update && sudo apt-get install --no-install-recommends -y build-essential git libyaml-dev pkg-config google-chrome-stable + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + - name: Run tests + env: + RAILS_ENV: test + # REDIS_URL: redis://localhost:6379/0 + run: bin/rails db:test:prepare test test:system + + - name: Keep screenshots from failed system tests + uses: actions/upload-artifact@v4 + if: failure() + with: + name: screenshots + path: ${{ github.workspace }}/tmp/screenshots + if-no-files-found: ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..189a50bf5d --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# See https://help.github.com/articles/ignoring-files for more about ignoring files. +# +# Temporary files generated by your text editor or operating system +# belong in git's global ignore instead: +# `$XDG_CONFIG_HOME/git/ignore` or `~/.config/git/ignore` + +# Ignore bundler config. +/.bundle + +# Ignore all environment files. +/.env* + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/ +!/tmp/pids/.keep + +# Ignore storage (uploaded files in development and any SQLite databases). +/storage/* +!/storage/.keep +/tmp/storage/* +!/tmp/storage/ +!/tmp/storage/.keep + +/public/assets + +# Ignore master key for decrypting credentials and more. +/config/master.key diff --git a/.kamal/hooks/docker-setup.sample b/.kamal/hooks/docker-setup.sample new file mode 100644 index 0000000000..7b8a4ea378 --- /dev/null +++ b/.kamal/hooks/docker-setup.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Docker set up on $KAMAL_HOSTS..." diff --git a/.kamal/hooks/post-app-boot.sample b/.kamal/hooks/post-app-boot.sample new file mode 100644 index 0000000000..84353aaaaa --- /dev/null +++ b/.kamal/hooks/post-app-boot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Booted app version $KAMAL_VERSION on $KAMAL_HOSTS..." diff --git a/.kamal/hooks/post-deploy.sample b/.kamal/hooks/post-deploy.sample new file mode 100644 index 0000000000..5fe35d16cd --- /dev/null +++ b/.kamal/hooks/post-deploy.sample @@ -0,0 +1,14 @@ +#!/bin/sh + +# A sample post-deploy hook +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_ROLES (if set) +# KAMAL_DESTINATION (if set) +# KAMAL_RUNTIME + +echo "$KAMAL_PERFORMER deployed $KAMAL_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds" diff --git a/.kamal/hooks/post-proxy-reboot.sample b/.kamal/hooks/post-proxy-reboot.sample new file mode 100644 index 0000000000..ba2e14929b --- /dev/null +++ b/.kamal/hooks/post-proxy-reboot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Rebooted kamal-proxy on $KAMAL_HOSTS" diff --git a/.kamal/hooks/pre-app-boot.sample b/.kamal/hooks/pre-app-boot.sample new file mode 100644 index 0000000000..e5201d609a --- /dev/null +++ b/.kamal/hooks/pre-app-boot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Booting app version $KAMAL_VERSION on $KAMAL_HOSTS..." diff --git a/.kamal/hooks/pre-build.sample b/.kamal/hooks/pre-build.sample new file mode 100644 index 0000000000..bb3d9dc687 --- /dev/null +++ b/.kamal/hooks/pre-build.sample @@ -0,0 +1,51 @@ +#!/bin/sh + +# A sample pre-build hook +# +# Checks: +# 1. We have a clean checkout +# 2. A remote is configured +# 3. The branch has been pushed to the remote +# 4. The version we are deploying matches the remote +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_ROLES (if set) +# KAMAL_DESTINATION (if set) + +if [ -n "$(git status --porcelain)" ]; then + echo "Git checkout is not clean, aborting..." >&2 + git status --porcelain >&2 + exit 1 +fi + +first_remote=$(git remote) + +if [ -z "$first_remote" ]; then + echo "No git remote set, aborting..." >&2 + exit 1 +fi + +current_branch=$(git branch --show-current) + +if [ -z "$current_branch" ]; then + echo "Not on a git branch, aborting..." >&2 + exit 1 +fi + +remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1) + +if [ -z "$remote_head" ]; then + echo "Branch not pushed to remote, aborting..." >&2 + exit 1 +fi + +if [ "$KAMAL_VERSION" != "$remote_head" ]; then + echo "Version ($KAMAL_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2 + exit 1 +fi + +exit 0 diff --git a/.kamal/hooks/pre-connect.sample b/.kamal/hooks/pre-connect.sample new file mode 100644 index 0000000000..e1bcab1e8c --- /dev/null +++ b/.kamal/hooks/pre-connect.sample @@ -0,0 +1,47 @@ +#!/usr/bin/env ruby + +# A sample pre-connect check +# +# Warms DNS before connecting to hosts in parallel +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_ROLES (if set) +# KAMAL_DESTINATION (if set) +# KAMAL_RUNTIME + +hosts = ENV["KAMAL_HOSTS"].split(",") +results = nil +max = 3 + +elapsed = Benchmark.realtime do + results = hosts.map do |host| + Thread.new do + tries = 1 + + begin + Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME) + rescue SocketError + if tries < max + puts "Retrying DNS warmup: #{host}" + tries += 1 + sleep rand + retry + else + puts "DNS warmup failed: #{host}" + host + end + end + + tries + end + end.map(&:value) +end + +retries = results.sum - hosts.size +nopes = results.count { |r| r == max } + +puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ] diff --git a/.kamal/hooks/pre-deploy.sample b/.kamal/hooks/pre-deploy.sample new file mode 100644 index 0000000000..62c87a88bb --- /dev/null +++ b/.kamal/hooks/pre-deploy.sample @@ -0,0 +1,122 @@ +#!/usr/bin/env ruby + +# A sample pre-deploy hook +# +# Checks the Github status of the build, waiting for a pending build to complete for up to 720 seconds. +# +# Fails unless the combined status is "success" +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_COMMAND +# KAMAL_SUBCOMMAND +# KAMAL_ROLES (if set) +# KAMAL_DESTINATION (if set) + +# Only check the build status for production deployments +if ENV["KAMAL_COMMAND"] == "rollback" || ENV["KAMAL_DESTINATION"] != "production" + exit 0 +end + +require "bundler/inline" + +# true = install gems so this is fast on repeat invocations +gemfile(true, quiet: true) do + source "https://rubygems.org" + + gem "octokit" + gem "faraday-retry" +end + +MAX_ATTEMPTS = 72 +ATTEMPTS_GAP = 10 + +def exit_with_error(message) + $stderr.puts message + exit 1 +end + +class GithubStatusChecks + attr_reader :remote_url, :git_sha, :github_client, :combined_status + + def initialize + @remote_url = github_repo_from_remote_url + @git_sha = `git rev-parse HEAD`.strip + @github_client = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"]) + refresh! + end + + def refresh! + @combined_status = github_client.combined_status(remote_url, git_sha) + end + + def state + combined_status[:state] + end + + def first_status_url + first_status = combined_status[:statuses].find { |status| status[:state] == state } + first_status && first_status[:target_url] + end + + def complete_count + combined_status[:statuses].count { |status| status[:state] != "pending"} + end + + def total_count + combined_status[:statuses].count + end + + def current_status + if total_count > 0 + "Completed #{complete_count}/#{total_count} checks, see #{first_status_url} ..." + else + "Build not started..." + end + end + + private + def github_repo_from_remote_url + url = `git config --get remote.origin.url`.strip.delete_suffix(".git") + if url.start_with?("https://github.com/") + url.delete_prefix("https://github.com/") + elsif url.start_with?("git@github.com:") + url.delete_prefix("git@github.com:") + else + url + end + end +end + + +$stdout.sync = true + +begin + puts "Checking build status..." + + attempts = 0 + checks = GithubStatusChecks.new + + loop do + case checks.state + when "success" + puts "Checks passed, see #{checks.first_status_url}" + exit 0 + when "failure" + exit_with_error "Checks failed, see #{checks.first_status_url}" + when "pending" + attempts += 1 + end + + exit_with_error "Checks are still pending, gave up after #{MAX_ATTEMPTS * ATTEMPTS_GAP} seconds" if attempts == MAX_ATTEMPTS + + puts checks.current_status + sleep(ATTEMPTS_GAP) + checks.refresh! + end +rescue Octokit::NotFound + exit_with_error "Build status could not be found" +end diff --git a/.kamal/hooks/pre-proxy-reboot.sample b/.kamal/hooks/pre-proxy-reboot.sample new file mode 100644 index 0000000000..807fb707ef --- /dev/null +++ b/.kamal/hooks/pre-proxy-reboot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Rebooting kamal-proxy on $KAMAL_HOSTS..." diff --git a/.kamal/secrets b/.kamal/secrets new file mode 100644 index 0000000000..54ffde8407 --- /dev/null +++ b/.kamal/secrets @@ -0,0 +1,17 @@ +# Secrets defined here are available for reference under registry/password, env/secret, builder/secrets, +# and accessories/*/env/secret in config/deploy.yml. All secrets should be pulled from either +# password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git. + +# Example of extracting secrets from 1password (or another compatible pw manager) +# SECRETS=$(kamal secrets fetch --adapter 1password --account your-account --from Vault/Item KAMAL_REGISTRY_PASSWORD RAILS_MASTER_KEY) +# KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD ${SECRETS}) +# RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY ${SECRETS}) + +# Use a GITHUB_TOKEN if private repositories are needed for the image +# GITHUB_TOKEN=$(gh config get -h github.com oauth_token) + +# Grab the registry password from ENV +KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD + +# Improve security by using a password manager. Never check config/master.key into git! +RAILS_MASTER_KEY=$(cat config/master.key) diff --git a/.rspec b/.rspec new file mode 100644 index 0000000000..2673547e8d --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--require spec_helper diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000000..eda589f6bc --- /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..919911cc0c --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +ruby-3.4.7 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..9e47ef92bb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,72 @@ +# syntax=docker/dockerfile:1 +# check=error=true + +# This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand: +# docker build -t projeto_camaar . +# docker run -d -p 80:80 -e RAILS_MASTER_KEY= --name projeto_camaar projeto_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.7 +FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base + +# Rails app lives here +WORKDIR /rails + +# Install base packages +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Set production environment +ENV RAILS_ENV="production" \ + BUNDLE_DEPLOYMENT="1" \ + BUNDLE_PATH="/usr/local/bundle" \ + BUNDLE_WITHOUT="development" + +# Throw-away build stage to reduce size of final image +FROM base AS build + +# Install packages needed to build gems +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y build-essential git libyaml-dev pkg-config && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Install application gems +COPY Gemfile Gemfile.lock ./ +RUN bundle install && \ + rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \ + bundle exec bootsnap precompile --gemfile + +# Copy application code +COPY . . + +# Precompile bootsnap code for faster boot times +RUN bundle exec bootsnap precompile app/ lib/ + +# Precompiling assets for production without requiring secret RAILS_MASTER_KEY +RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile + + + + +# Final stage for app image +FROM base + +# Copy built artifacts: gems, application +COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}" +COPY --from=build /rails /rails + +# Run and own only the runtime files as a non-root user for security +RUN groupadd --system --gid 1000 rails && \ + useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \ + chown -R rails:rails db log storage tmp +USER 1000:1000 + +# Entrypoint prepares the database. +ENTRYPOINT ["/rails/bin/docker-entrypoint"] + +# Start server via Thruster by default, this can be overwritten at runtime +EXPOSE 80 +CMD ["./bin/thrust", "./bin/rails", "server"] diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000000..e43dea12c5 --- /dev/null +++ b/Gemfile @@ -0,0 +1,62 @@ +source "https://rubygems.org" + +gem "rails", "~> 8.0.4" +gem "propshaft" +gem "sqlite3", ">= 2.1" +gem "puma", ">= 5.0" +gem "importmap-rails" +gem "turbo-rails" +gem "stimulus-rails" +gem "jbuilder" + +# ============================================ +# Authentication +# ============================================ +gem "devise", "~> 4.9" + +gem "bcrypt", "~> 3.1.7" + +gem "tzinfo-data", platforms: %i[ windows jruby ] + +gem "solid_cache" +gem "solid_queue" +gem "solid_cable" + +gem "bootsnap", require: false +gem "kamal", require: false +gem "thruster", require: false + +group :development, :test do + gem "debug", platforms: %i[ mri windows ], require: "debug/prelude" + gem "brakeman", require: false + gem "rubocop-rails-omakase", require: false + + gem 'rubycritic' + gem 'flog', '~> 4.8.0' + gem 'flay', '~> 2.13.0' + + # ============================================ + # RSpec - Test-Driven Development (TDD) + # ============================================ + gem "rspec-rails", "~> 6.0.0" + gem "factory_bot_rails", "~> 6.2" + gem "faker", "~> 3.2" + gem "shoulda-matchers", "~> 5.1" + gem "csv" +end + +group :development do + gem "web-console" +end + +group :test do + gem "capybara" + gem "selenium-webdriver" + + gem 'cucumber-rails', require: false + gem 'database_cleaner' + + gem "simplecov", "~> 0.22.0" + gem "simplecov-console" +end +gem 'rdoc', require: false diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000000..5a55fd9f08 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,586 @@ +GEM + remote: https://rubygems.org/ + specs: + actioncable (8.0.4) + actionpack (= 8.0.4) + activesupport (= 8.0.4) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + zeitwerk (~> 2.6) + actionmailbox (8.0.4) + actionpack (= 8.0.4) + activejob (= 8.0.4) + activerecord (= 8.0.4) + activestorage (= 8.0.4) + activesupport (= 8.0.4) + mail (>= 2.8.0) + actionmailer (8.0.4) + actionpack (= 8.0.4) + actionview (= 8.0.4) + activejob (= 8.0.4) + activesupport (= 8.0.4) + mail (>= 2.8.0) + rails-dom-testing (~> 2.2) + actionpack (8.0.4) + actionview (= 8.0.4) + activesupport (= 8.0.4) + nokogiri (>= 1.8.5) + rack (>= 2.2.4) + rack-session (>= 1.0.1) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) + actiontext (8.0.4) + actionpack (= 8.0.4) + activerecord (= 8.0.4) + activestorage (= 8.0.4) + activesupport (= 8.0.4) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (8.0.4) + activesupport (= 8.0.4) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (8.0.4) + activesupport (= 8.0.4) + globalid (>= 0.3.6) + activemodel (8.0.4) + activesupport (= 8.0.4) + activerecord (8.0.4) + activemodel (= 8.0.4) + activesupport (= 8.0.4) + timeout (>= 0.4.0) + activestorage (8.0.4) + actionpack (= 8.0.4) + activejob (= 8.0.4) + activerecord (= 8.0.4) + activesupport (= 8.0.4) + marcel (~> 1.0) + activesupport (8.0.4) + base64 + benchmark (>= 0.3) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + ansi (1.5.0) + ast (2.4.3) + axiom-types (0.1.1) + descendants_tracker (~> 0.0.4) + ice_nine (~> 0.11.0) + thread_safe (~> 0.3, >= 0.3.1) + base64 (0.3.0) + bcrypt (3.1.20) + bcrypt_pbkdf (1.1.1) + benchmark (0.5.0) + bigdecimal (3.3.1) + bindex (0.8.1) + bootsnap (1.18.6) + msgpack (~> 1.2) + brakeman (7.1.1) + racc + builder (3.3.0) + capybara (3.40.0) + addressable + matrix + mini_mime (>= 0.1.3) + nokogiri (~> 1.11) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + regexp_parser (>= 1.5, < 3.0) + xpath (~> 3.2) + childprocess (5.1.0) + logger (~> 1.5) + coercible (1.0.0) + descendants_tracker (~> 0.0.1) + concurrent-ruby (1.3.5) + connection_pool (2.5.4) + crass (1.0.6) + csv (3.3.5) + cucumber (10.1.1) + base64 (~> 0.2) + builder (~> 3.2) + 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 (2.1.0) + database_cleaner-active_record (>= 2, < 3) + database_cleaner-active_record (2.2.2) + activerecord (>= 5.a) + database_cleaner-core (~> 2.0) + database_cleaner-core (2.0.1) + date (3.5.0) + debug (1.11.0) + irb (~> 1.10) + reline (>= 0.3.8) + descendants_tracker (0.0.4) + thread_safe (~> 0.3, >= 0.3.1) + devise (4.9.4) + bcrypt (~> 3.0) + orm_adapter (~> 0.1) + railties (>= 4.1.0) + responders + warden (~> 1.2.3) + diff-lcs (1.6.2) + docile (1.4.1) + dotenv (3.1.8) + drb (2.2.3) + dry-configurable (1.3.0) + dry-core (~> 1.1) + zeitwerk (~> 2.6) + dry-core (1.1.0) + concurrent-ruby (~> 1.0) + logger + zeitwerk (~> 2.6) + dry-inflector (1.2.0) + dry-initializer (3.2.0) + dry-logic (1.6.0) + bigdecimal + concurrent-ruby (~> 1.0) + dry-core (~> 1.1) + zeitwerk (~> 2.6) + dry-schema (1.14.1) + concurrent-ruby (~> 1.0) + dry-configurable (~> 1.0, >= 1.0.1) + dry-core (~> 1.1) + dry-initializer (~> 3.2) + dry-logic (~> 1.5) + dry-types (~> 1.8) + zeitwerk (~> 2.6) + dry-types (1.8.3) + bigdecimal (~> 3.0) + concurrent-ruby (~> 1.0) + dry-core (~> 1.0) + dry-inflector (~> 1.0) + dry-logic (~> 1.4) + zeitwerk (~> 2.6) + ed25519 (1.4.0) + erb (6.0.0) + erubi (1.13.1) + et-orbi (1.4.0) + tzinfo + factory_bot (6.5.6) + activesupport (>= 6.1.0) + factory_bot_rails (6.5.1) + factory_bot (~> 6.5) + railties (>= 6.1.0) + faker (3.5.3) + i18n (>= 1.8.11, < 2) + 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) + flay (2.13.3) + erubi (~> 1.10) + path_expander (~> 1.0) + ruby_parser (~> 3.0) + sexp_processor (~> 4.0) + flog (4.8.0) + path_expander (~> 1.0) + ruby_parser (~> 3.1, > 3.1.0) + sexp_processor (~> 4.8) + 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) + ice_nine (0.11.2) + 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) + launchy (3.1.1) + addressable (~> 2.8) + childprocess (~> 5.0) + logger (~> 1.6) + 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_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) + orm_adapter (0.5.0) + ostruct (0.6.3) + parallel (1.27.0) + parser (3.3.10.0) + ast (~> 2.4.1) + racc + path_expander (1.1.3) + pp (0.6.3) + prettyprint + prettyprint (0.2.0) + prism (1.6.0) + propshaft (1.3.1) + actionpack (>= 7.0.0) + activesupport (>= 7.0.0) + rack + psych (5.2.6) + date + stringio + public_suffix (6.0.2) + puma (7.1.0) + nio4r (~> 2.0) + raabro (1.4.0) + racc (1.8.1) + rack (3.2.4) + rack-session (2.1.1) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rack-test (2.2.0) + rack (>= 1.3) + rackup (2.2.1) + rack (>= 3) + rails (8.0.4) + actioncable (= 8.0.4) + actionmailbox (= 8.0.4) + actionmailer (= 8.0.4) + actionpack (= 8.0.4) + actiontext (= 8.0.4) + actionview (= 8.0.4) + activejob (= 8.0.4) + activemodel (= 8.0.4) + activerecord (= 8.0.4) + activestorage (= 8.0.4) + activesupport (= 8.0.4) + bundler (>= 1.15.0) + railties (= 8.0.4) + rails-dom-testing (2.3.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.6.2) + loofah (~> 2.21) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + railties (8.0.4) + actionpack (= 8.0.4) + activesupport (= 8.0.4) + irb (~> 1.13) + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + tsort (>= 0.2) + zeitwerk (~> 2.6) + rainbow (3.1.1) + rake (13.3.1) + rdoc (6.15.1) + erb + psych (>= 4.0.0) + tsort + reek (6.5.0) + dry-schema (~> 1.13) + logger (~> 1.6) + parser (~> 3.3.0) + rainbow (>= 2.0, < 4.0) + rexml (~> 3.1) + regexp_parser (2.11.3) + reline (0.6.3) + io-console (~> 0.5) + responders (3.2.0) + actionpack (>= 7.0) + railties (>= 7.0) + rexml (3.4.4) + rspec-core (3.13.6) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.7) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-rails (6.0.4) + actionpack (>= 6.1) + activesupport (>= 6.1) + railties (>= 6.1) + rspec-core (~> 3.12) + rspec-expectations (~> 3.12) + rspec-mocks (~> 3.12) + rspec-support (~> 3.12) + rspec-support (3.13.6) + rubocop (1.81.7) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.47.1, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.48.0) + parser (>= 3.3.7.2) + prism (~> 1.4) + rubocop-performance (1.26.1) + lint_roller (~> 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.47.1, < 2.0) + rubocop-rails (2.33.4) + activesupport (>= 4.2.0) + lint_roller (~> 1.1) + rack (>= 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.44.0, < 2.0) + rubocop-rails-omakase (1.1.0) + rubocop (>= 1.72) + rubocop-performance (>= 1.24) + rubocop-rails (>= 2.30) + ruby-progressbar (1.13.0) + ruby_parser (3.21.1) + racc (~> 1.5) + sexp_processor (~> 4.16) + rubycritic (4.11.0) + flay (~> 2.13) + flog (~> 4.7) + launchy (>= 2.5.2) + parser (>= 3.3.0.5) + rainbow (~> 3.1.1) + reek (~> 6.5.0, < 7.0) + rexml + ruby_parser (~> 3.21) + simplecov (>= 0.22.0) + tty-which (~> 0.5.0) + virtus (~> 2.0) + rubyzip (3.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) + sexp_processor (4.17.4) + shoulda-matchers (5.3.0) + activesupport (>= 5.2.0) + simplecov (0.22.0) + docile (~> 1.1) + simplecov-html (~> 0.11) + simplecov_json_formatter (~> 0.1) + simplecov-console (0.9.4) + ansi + simplecov + terminal-table + simplecov-html (0.13.2) + simplecov_json_formatter (0.1.4) + 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) + terminal-table (4.0.0) + unicode-display_width (>= 1.1.1, < 4) + thor (1.4.0) + thread_safe (0.3.6) + 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) + tty-which (0.5.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) + virtus (2.0.0) + axiom-types (~> 0.1) + coercible (~> 1.0) + descendants_tracker (~> 0.0, >= 0.0.3) + warden (1.2.9) + rack (>= 2.0.9) + 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 + x86_64-linux-gnu + x86_64-linux-musl + +DEPENDENCIES + bcrypt (~> 3.1.7) + bootsnap + brakeman + capybara + csv + cucumber-rails + database_cleaner + debug + devise (~> 4.9) + factory_bot_rails (~> 6.2) + faker (~> 3.2) + flay (~> 2.13.0) + flog (~> 4.8.0) + importmap-rails + jbuilder + kamal + propshaft + puma (>= 5.0) + rails (~> 8.0.4) + rdoc + rspec-rails (~> 6.0.0) + rubocop-rails-omakase + rubycritic + selenium-webdriver + shoulda-matchers (~> 5.1) + simplecov (~> 0.22.0) + simplecov-console + solid_cable + solid_cache + solid_queue + sqlite3 (>= 2.1) + stimulus-rails + thruster + turbo-rails + tzinfo-data + web-console + +BUNDLED WITH + 2.6.9 diff --git a/Grupo 10 - Sprint 3 ESW.md b/Grupo 10 - Sprint 3 ESW.md new file mode 100644 index 0000000000..aac0d1aee1 --- /dev/null +++ b/Grupo 10 - Sprint 3 ESW.md @@ -0,0 +1,1328 @@ +# Grupo 10 - Sprint 3 ESW + +### Grupo 10 - Engenharia de Software: + +Bernardo Gomes Rodrigues - 231034190 + +Isaac Silva - 231025216 + +Filipe Abadia Marcelino - 190087161 + +Maria Carolina Burgum Abreu Jorge - 231013547 + +Link de Visualização no Notion: https://www.notion.so/Grupo-10-Sprint-3-ESW-2cb7274feb388008842fc0bd122e5fd0?source=copy_link + +--- + +## 1. Avaliando o ABC score com o rubycritic + +Precisamos avaliar o ABC score e garantir que ele seja menor que 20 para todos os métodos. Ao rodar o rubycritic inicialmente, recebemos o seguinte retorno: + +```ruby +bgrod@Bernardo:~/sprint3/CAMAAR-Grupo-10$ bundle exec rubycritic +running flay smells +.......................................... +running flog smells +........................................................................................................ +running reek smells +........................................................................................................ +running complexity +........................................................................................................ +running attributes +........................................................................................................ +running churn +........................................................................................................ +running simple_cov +.....................Warning: coverage data provided by Coverage [30] exceeds number of lines in /home/bgrod/sprint3/CAMAAR-Grupo-10/app/models/form_template_field.rb [29] +................................................................................... +New critique at file:////home/bgrod/sprint3/CAMAAR-Grupo-10/tmp/rubycritic/overview.html +Score: 74.51 +``` + +Ou seja, foi uma análise bem sucedida. Ao executar um comando do flog para mostrar todos os métodos com ABC score maior que 40, obtemos a saída: + +```ruby +# Comando do Flog + +bundle exec flog -a app/ | awk ' +BEGIN { + print "\n═══════════════════════════════════════════════════════════════════════════════" + print " ANÁLISE ABC SCORE - TODOS OS MÉTODOS" + print "═══════════════════════════════════════════════════════════════════════════════\n" + high=0; medium=0; good=0; +} +NR<=2 { next } +/^[[:space:]]+[0-9]+\.[0-9]+:/ { + score = $1+0; + sub(/^[[:space:]]+[0-9]+\.[0-9]+:[[:space:]]*/, ""); + + if (score > 20) { + printf "🔴 ALTO %5.1f │ %s\n", score, $0; + high++; + } else if (score > 15) { + printf "🟡 MÉDIO %5.1f │ %s\n", score, $0; + medium++; + } else if (score > 0) { + printf "🟢 BOM %5.1f │ %s\n", score, $0; + good++; + } +} +END { + print "\n═══════════════════════════════════════════════════════════════════════════════" + printf "📊 RESUMO: 🔴 %d alto (>20) │ 🟡 %d médio (>15) │ 🟢 %d bom (≤15)\n", high, medium, good; + print "═══════════════════════════════════════════════════════════════════════════════\n" +}' +``` + +```ruby +# Saída + +═══════════════════════════════════════════════════════════════════════════════ + ANÁLISE ABC SCORE - TODOS OS MÉTODOS +═══════════════════════════════════════════════════════════════════════════════ + +🔴 ALTO 43.7 │ Admin::ImportsController#import_klasses app/controllers/admin/imports_controller.rb:13-44 +🔴 ALTO 23.6 │ Admin::FormsController#create app/controllers/admin/forms_controller.rb:25-45 +🔴 ALTO 20.6 │ ImportService#find_or_create_user app/services/import_service.rb:74-101 +🟡 MÉDIO 16.5 │ Student::FormsController#answer app/controllers/student/forms_controller.rb:20-37 +🟢 BOM 14.9 │ Admin::FormTemplatesController#create app/controllers/admin/form_templates_controller.rb:21-32 +🟢 BOM 14.8 │ Admin::FormTemplatesHelper#link_to_add_fields app/helpers/admin/form_templates_helper.rb:4-14 +🟢 BOM 14.4 │ Student::FormsController#submit_answer app/controllers/student/forms_controller.rb:39-52 +🟢 BOM 13.5 │ FormResponse#build_answers_for_fields app/models/form_response.rb:23-29 +🟢 BOM 13.3 │ HomeController#index app/controllers/home_controller.rb:6-13 +🟢 BOM 13.0 │ User#pending_forms app/models/user.rb:29-35 +🟢 BOM 9.8 │ ImportService#import_single_klass app/services/import_service.rb:35-44 +🟢 BOM 9.6 │ Form#none +🟢 BOM 9.3 │ ImportService#find_or_create_klass app/services/import_service.rb:46-54 +🟢 BOM 9.0 │ FormTemplateField#none +🟢 BOM 8.9 │ Student::FormsController#check_form_accessible app/controllers/student/forms_controller.rb:64-68 +🟢 BOM 8.6 │ Admin::FormsController#update app/controllers/admin/forms_controller.rb:52-60 +🟢 BOM 8.4 │ Klass#none +🟢 BOM 8.3 │ User#none +🟢 BOM 7.6 │ Form#pending_responses app/models/form.rb:18-20 +🟢 BOM 7.6 │ FormTemplateField#ensure_position_is_integer app/models/form_template_field.rb:26-28 +🟢 BOM 7.5 │ Student::FormsController#index app/controllers/student/forms_controller.rb:10-13 +🟢 BOM 7.4 │ ImportService#import_single_student app/services/import_service.rb:64-72 +🟢 BOM 7.0 │ FormResponse#none +🟢 BOM 7.0 │ FormTemplate#none +🟢 BOM 7.0 │ User#completed_forms app/models/user.rb:38-41 +🟢 BOM 6.1 │ ImportService#parse_json_file app/services/import_service.rb:27-33 +🟢 BOM 5.9 │ Student::FormsController#show app/controllers/student/forms_controller.rb:15-18 +🟢 BOM 5.9 │ Admin::FormsController#publish app/controllers/admin/forms_controller.rb:67-73 +🟢 BOM 5.9 │ Admin::FormsController#close app/controllers/admin/forms_controller.rb:75-81 +🟢 BOM 5.8 │ Admin::FormTemplatesController#update app/controllers/admin/form_templates_controller.rb:38-44 +🟢 BOM 5.8 │ Admin::UsersController#update app/controllers/admin/users_controller.rb:19-25 +🟢 BOM 5.8 │ ImportService#import_klasses app/services/import_service.rb:13-23 +🟢 BOM 5.2 │ Admin::DashboardController#index app/controllers/admin/dashboard_controller.rb:8-12 +🟢 BOM 5.0 │ Admin::FormsController#none +🟢 BOM 5.0 │ Student::FormsController#none +🟢 BOM 5.0 │ FormAnswer#none +🟢 BOM 4.8 │ Admin::FormsController#show app/controllers/admin/forms_controller.rb:14-17 +🟢 BOM 4.8 │ Klass#students app/models/klass.rb:18-20 +🟢 BOM 4.8 │ Klass#teachers app/models/klass.rb:22-24 +🟢 BOM 4.7 │ Admin::ImportsController#check_admin app/controllers/admin/imports_controller.rb:48-50 +🟢 BOM 4.7 │ Admin::DashboardController#check_admin app/controllers/admin/dashboard_controller.rb:16-18 +🟢 BOM 4.7 │ Admin::FormTemplatesController#edit app/controllers/admin/form_templates_controller.rb:34-36 +🟢 BOM 4.7 │ Admin::FormTemplatesController#check_admin app/controllers/admin/form_templates_controller.rb:64-66 +🟢 BOM 4.7 │ Admin::FormsController#check_admin app/controllers/admin/forms_controller.rb:101-103 +🟢 BOM 4.7 │ Admin::UsersController#check_admin app/controllers/admin/users_controller.rb:42-44 +🟢 BOM 4.7 │ Student::FormsController#check_student app/controllers/student/forms_controller.rb:60-62 +🟢 BOM 4.7 │ Student::FormsController#update_answers app/controllers/student/forms_controller.rb:70-73 +🟢 BOM 4.2 │ Admin::FormsController#new app/controllers/admin/forms_controller.rb:19-23 +🟢 BOM 4.0 │ Admin::FormTemplatesController#none +🟢 BOM 4.0 │ Admin::UsersController#none +🟢 BOM 4.0 │ ClassMember#none +🟢 BOM 3.8 │ Admin::ImportsController#index app/controllers/admin/imports_controller.rb:8-11 +🟢 BOM 3.7 │ Admin::FormTemplatesController#index app/controllers/admin/form_templates_controller.rb:9-11 +🟢 BOM 3.7 │ Admin::FormTemplatesController#set_form_template app/controllers/admin/form_templates_controller.rb:53-55 +🟢 BOM 3.7 │ Admin::FormsController#set_form app/controllers/admin/forms_controller.rb:89-91 +🟢 BOM 3.7 │ Admin::FormsController#set_form_response app/controllers/admin/forms_controller.rb:93-95 +🟢 BOM 3.7 │ Admin::UsersController#set_user app/controllers/admin/users_controller.rb:34-36 +🟢 BOM 3.7 │ Student::FormsController#set_form app/controllers/student/forms_controller.rb:56-58 +🟢 BOM 3.7 │ ImportService#import_students app/services/import_service.rb:56-62 +🟢 BOM 3.6 │ Admin::FormTemplatesController#form_template_params app/controllers/admin/form_templates_controller.rb:57-62 +🟢 BOM 3.6 │ Admin::FormsController#form_params app/controllers/admin/forms_controller.rb:97-99 +🟢 BOM 3.6 │ Admin::UsersController#user_params app/controllers/admin/users_controller.rb:38-40 +🟢 BOM 3.4 │ Form#completed_responses app/models/form.rb:23-25 +🟢 BOM 3.4 │ Admin::FormTemplatesController#new app/controllers/admin/form_templates_controller.rb:16-19 +🟢 BOM 3.2 │ Admin::FormTemplatesController#destroy app/controllers/admin/form_templates_controller.rb:46-49 +🟢 BOM 3.2 │ Admin::FormsController#destroy app/controllers/admin/forms_controller.rb:62-65 +🟢 BOM 3.2 │ Admin::UsersController#destroy app/controllers/admin/users_controller.rb:27-30 +🟢 BOM 3.0 │ Admin::DashboardController#none +🟢 BOM 3.0 │ Admin::ImportsController#none +🟢 BOM 3.0 │ ImportService#initialize app/services/import_service.rb:6-10 +🟢 BOM 2.8 │ Admin::FormsController#edit app/controllers/admin/forms_controller.rb:47-50 +🟢 BOM 2.4 │ Admin::FormsController#index app/controllers/admin/forms_controller.rb:10-12 +🟢 BOM 2.2 │ FormResponse#completed? app/models/form_response.rb:14-16 +🟢 BOM 2.2 │ FormResponse#pending? app/models/form_response.rb:18-20 +🟢 BOM 2.2 │ FormResponse#submit! app/models/form_response.rb:32-34 +🟢 BOM 2.2 │ FormTemplateField#requires_options? app/models/form_template_field.rb:22-24 +🟢 BOM 2.2 │ User#admin? app/models/user.rb:20-22 +🟢 BOM 2.2 │ User#user? app/models/user.rb:24-26 +🟢 BOM 2.0 │ ApplicationMailer#none +🟢 BOM 2.0 │ ImportService#none +🟢 BOM 1.4 │ Admin::UsersController#index app/controllers/admin/users_controller.rb:9-11 +🟢 BOM 1.0 │ ApplicationController#none +🟢 BOM 1.0 │ HomeController#none +🟢 BOM 1.0 │ ApplicationRecord#none + +═══════════════════════════════════════════════════════════════════════════════ +📊 RESUMO: 🔴 3 alto (>20) │ 🟡 1 médio (>15) │ 🟢 80 bom (≤15) +═══════════════════════════════════════════════════════════════════════════════ +``` + +Como é possível observar, tem 3 métodos que precisam ser refatorados. Aqui está o antes e depois de cada método. + +**Admin::ImportsController#import_klasses** + +```ruby +# Antes - imports_controller + +# app/controllers/admin/imports_controller.rb + +module Admin + class ImportsController < ApplicationController + before_action :authenticate_user! + before_action :check_admin + + def index + @total_klasses = Klass.count + @total_users = User.where(role: :user).count + end + + def import_klasses + if params[:file].blank? + redirect_to admin_imports_path, alert: 'Por favor, selecione um arquivo' + return + end + + file = params[:file] + + # Validar tipo de arquivo + unless file.content_type == 'application/json' || file.original_filename.end_with?('.json') + redirect_to admin_imports_path, alert: 'Por favor, envie um arquivo JSON válido' + return + end + + # Executar importação + service = ImportService.new(file.path) + result = service.import_klasses + + if result[:success] + message = "✅ #{result[:imported]} turma(s) importada(s) com sucesso!" + + if result[:errors].present? + message += "\n\n⚠️ Aviso: #{result[:errors].count} erro(s) durante importação:" + result[:errors].each { |error| message += "\n• #{error}" } + redirect_to admin_imports_path, alert: message + else + redirect_to admin_imports_path, notice: message + end + else + redirect_to admin_imports_path, alert: "❌ Erro na importação: #{result[:error]}" + end + end + + private + + def check_admin + redirect_to root_path, alert: 'Acesso negado!' unless current_user.admin? + end + end +end +``` + +```ruby +# Depois - imports_controller + +# app/controllers/admin/imports_controller.rb + +module Admin + class ImportsController < ApplicationController + before_action :authenticate_user! + before_action :check_admin + + def index + @total_klasses = Klass.count + @total_users = User.where(role: :user).count + end + + def import_klasses + return redirect_with_error('Por favor, selecione um arquivo') if params[:file].blank? + return redirect_with_error('Por favor, envie um arquivo JSON válido') unless valid_json_file? + + result = ImportService.new(params[:file].path).import_klasses + handle_import_result(result) + end + + private + + def valid_json_file? + file = params[:file] + file.content_type == 'application/json' || file.original_filename.end_with?('.json') + end + + def redirect_with_error(message) + redirect_to admin_imports_path, alert: message + end + + def handle_import_result(result) + if result[:success] + handle_success(result) + else + redirect_with_error("❌ Erro na importação: #{result[:error]}") + end + end + + def handle_success(result) + message = "✅ #{result[:imported]} turma(s) importada(s) com sucesso!" + + if result[:errors].present? + redirect_to admin_imports_path, alert: build_error_message(message, result[:errors]) + else + redirect_to admin_imports_path, notice: message + end + end + + def build_error_message(base_message, errors) + message = "#{base_message}\n\n⚠️ Aviso: #{errors.count} erro(s) durante importação:" + errors.each { |error| message += "\n• #{error}" } + message + end + + def check_admin + redirect_to root_path, alert: 'Acesso negado!' unless current_user.admin? + end + end +end + +``` + +**Admin::FormsController#create** + +```ruby +# Antes - forms_controller + +# app/controllers/admin/forms_controller.rb + +module Admin + class FormsController < ApplicationController + before_action :authenticate_user! + before_action :check_admin + before_action :set_form, only: [:show, :edit, :update, :destroy, :publish, :close, :view_response] + before_action :set_form_response, only: [:view_response] + + def index + @forms = Form.all.order(created_at: :desc) + end + + def show + @pending_count = @form.pending_responses.count + @completed_count = @form.completed_responses.count + end + + def new + @form = Form.new + @form_templates = FormTemplate.all + @klasses = Klass.all + end + + def create + template = FormTemplate.find(form_params[:form_template_id]) + klass = Klass.find(form_params[:klass_id]) + + @form = Form.new( + form_template: template, + klass: klass, + title: form_params[:title], + description: form_params[:description], + due_date: form_params[:due_date], + status: :draft + ) + + if @form.save + redirect_to admin_form_path(@form), notice: 'Formulário criado com sucesso!' + else + @form_templates = FormTemplate.all + @klasses = Klass.all + render :new, status: :unprocessable_entity + end + end + + def edit + @form_templates = FormTemplate.all + @klasses = Klass.all + end + + def update + if @form.update(form_params) + redirect_to admin_form_path(@form), notice: 'Formulário atualizado com sucesso!' + else + @form_templates = FormTemplate.all + @klasses = Klass.all + render :edit, status: :unprocessable_entity + end + end + + def destroy + @form.destroy + redirect_to admin_forms_url, notice: 'Formulário deletado com sucesso!' + end + + def publish + if @form.update(status: :published) + redirect_to admin_form_path(@form), notice: 'Formulário publicado com sucesso!' + else + redirect_to admin_form_path(@form), alert: 'Erro ao publicar formulário' + end + end + + def close + if @form.update(status: :closed) + redirect_to admin_form_path(@form), notice: 'Formulário fechado com sucesso!' + else + redirect_to admin_form_path(@form), alert: 'Erro ao fechar formulário' + end + end + + def view_response + # @form_response já é setado pelo before_action + end + + private + + def set_form + @form = Form.find(params[:id]) + end + + def set_form_response + @form_response = FormResponse.find(params[:response_id]) + end + + def form_params + params.require(:form).permit(:form_template_id, :klass_id, :title, :description, :due_date, :status) + end + + def check_admin + redirect_to root_path, alert: 'Acesso negado!' unless current_user.admin? + end + end +end + +``` + +```ruby +# Depois - forms_controller + +# app/controllers/admin/forms_controller.rb + +module Admin + class FormsController < ApplicationController + before_action :authenticate_user! + before_action :check_admin + before_action :set_form, only: [:show, :edit, :update, :destroy, :publish, :close, :view_response] + before_action :set_form_response, only: [:view_response] + + def index + @forms = Form.all.order(created_at: :desc) + end + + def show + @pending_count = @form.pending_responses.count + @completed_count = @form.completed_responses.count + end + + def new + @form = Form.new + load_form_dependencies + end + + def create + @form = build_form_from_params + + if @form.save + redirect_to admin_form_path(@form), notice: 'Formulário criado com sucesso!' + else + load_form_dependencies + render :new, status: :unprocessable_entity + end + end + + def edit + load_form_dependencies + end + + def update + if @form.update(form_params) + redirect_to admin_form_path(@form), notice: 'Formulário atualizado com sucesso!' + else + load_form_dependencies + render :edit, status: :unprocessable_entity + end + end + + def destroy + @form.destroy + redirect_to admin_forms_url, notice: 'Formulário deletado com sucesso!' + end + + def publish + if @form.update(status: :published) + redirect_to admin_form_path(@form), notice: 'Formulário publicado com sucesso!' + else + redirect_to admin_form_path(@form), alert: 'Erro ao publicar formulário' + end + end + + def close + if @form.update(status: :closed) + redirect_to admin_form_path(@form), notice: 'Formulário fechado com sucesso!' + else + redirect_to admin_form_path(@form), alert: 'Erro ao fechar formulário' + end + end + + def view_response + # @form_response já é setado pelo before_action + end + + private + + def build_form_from_params + Form.new( + form_template: FormTemplate.find(form_params[:form_template_id]), + klass: Klass.find(form_params[:klass_id]), + title: form_params[:title], + description: form_params[:description], + due_date: form_params[:due_date], + status: :draft + ) + end + + def load_form_dependencies + @form_templates = FormTemplate.all + @klasses = Klass.all + end + + def set_form + @form = Form.find(params[:id]) + end + + def set_form_response + @form_response = FormResponse.find(params[:response_id]) + end + + def form_params + params.require(:form).permit(:form_template_id, :klass_id, :title, :description, :due_date, :status) + end + + def check_admin + redirect_to root_path, alert: 'Acesso negado!' unless current_user.admin? + end + end +end + +``` + +**ImportService#find_or_create_user** + +```ruby +# Antes - import_service.rb + +# app/services/import_service.rb + +class ImportService + attr_reader :file_path, :imported_count, :errors + + def initialize(file_path) + @file_path = file_path + @imported_count = 0 + @errors = [] + end + + # Importa turmas com seus estudantes + def import_klasses + data = parse_json_file + + data.each do |klass_data| + import_single_klass(klass_data) + end + + { success: true, imported: @imported_count, errors: @errors } + rescue StandardError => e + { success: false, error: e.message, imported: @imported_count, errors: @errors } + end + + private + + def parse_json_file + JSON.parse(File.read(@file_path)) + rescue JSON::ParserError => e + raise "Erro ao ler arquivo JSON: #{e.message}" + rescue Errno::ENOENT + raise "Arquivo não encontrado: #{@file_path}" + end + + def import_single_klass(klass_data) + klass = find_or_create_klass(klass_data) + + # Importar estudantes + import_students(klass, klass_data['dicente']) + + @imported_count += 1 + rescue StandardError => e + @errors << "Erro ao importar turma #{klass_data['code']}: #{e.message}" + end + + def find_or_create_klass(klass_data) + klass_info = klass_data['class'] + + Klass.find_or_create_by(code: klass_data['code']) do |klass| + klass.name = klass_data['name'] + klass.semester = klass_info['semester'] + klass.description = "Turma #{klass_info['classCode']} - #{klass_info['time']}" + end + end + + def import_students(klass, students_data) + return unless students_data.present? + + students_data.each do |student_data| + import_single_student(klass, student_data, 'dicente') + end + end + + def import_single_student(klass, student_data, role) + user = find_or_create_user(student_data) + + ClassMember.find_or_create_by(user: user, klass: klass) do |cm| + cm.role = role + end + rescue StandardError => e + @errors << "Erro ao importar estudante #{student_data['nome']}: #{e.message}" + end + + def find_or_create_user(user_data) + user = User.find_by(email: user_data['email']) + + if user.present? + return user + end + + # Usa matrícula como senha (Opção A) + password = user_data['matricula'] + + user = User.new( + email: user_data['email'], + name: user_data['nome'], + matricula: user_data['matricula'], + curso: user_data['curso'], + formacao: user_data['formacao'], + ocupacao: user_data['ocupacao'], + password: password, + password_confirmation: password, + role: :user + ) + + if user.save + user + else + raise "Erro ao salvar usuário #{user_data['email']}: #{user.errors.full_messages.join(', ')}" + end + end +end +``` + +```ruby +# Depois = import_service.rb + +# app/services/import_service.rb + +class ImportService + attr_reader :file_path, :imported_count, :errors + + def initialize(file_path) + @file_path = file_path + @imported_count = 0 + @errors = [] + end + + def import_klasses + data = parse_json_file + + data.each do |klass_data| + import_single_klass(klass_data) + end + + { success: true, imported: @imported_count, errors: @errors } + rescue StandardError => e + { success: false, error: e.message, imported: @imported_count, errors: @errors } + end + + private + + def parse_json_file + JSON.parse(File.read(@file_path)) + rescue JSON::ParserError => e + raise "Erro ao ler arquivo JSON: #{e.message}" + rescue Errno::ENOENT + raise "Arquivo não encontrado: #{@file_path}" + end + + def import_single_klass(klass_data) + klass = find_or_create_klass(klass_data) + import_students(klass, klass_data['dicente']) + @imported_count += 1 + rescue StandardError => e + @errors << "Erro ao importar turma #{klass_data['code']}: #{e.message}" + end + + def find_or_create_klass(klass_data) + klass_info = klass_data['class'] + + Klass.find_or_create_by(code: klass_data['code']) do |klass| + klass.name = klass_data['name'] + klass.semester = klass_info['semester'] + klass.description = "Turma #{klass_info['classCode']} - #{klass_info['time']}" + end + end + + def import_students(klass, students_data) + return unless students_data.present? + + students_data.each do |student_data| + import_single_student(klass, student_data, 'dicente') + end + end + + def import_single_student(klass, student_data, role) + user = find_or_create_user(student_data) + + ClassMember.find_or_create_by(user: user, klass: klass) do |cm| + cm.role = role + end + rescue StandardError => e + @errors << "Erro ao importar estudante #{student_data['nome']}: #{e.message}" + end + + def find_or_create_user(user_data) + User.find_by(email: user_data['email']) || create_new_user(user_data) + end + + def create_new_user(user_data) + user = build_user(user_data) + + if user.save + user + else + raise "Erro ao salvar usuário #{user_data['email']}: #{user.errors.full_messages.join(', ')}" + end + end + + def build_user(user_data) + password = user_data['matricula'] + + User.new( + email: user_data['email'], + name: user_data['nome'], + matricula: user_data['matricula'], + curso: user_data['curso'], + formacao: user_data['formacao'], + ocupacao: user_data['ocupacao'], + password: password, + password_confirmation: password, + role: :user + ) + end +end + +``` + +Após realizar as modificações acima e rodar os comandos no flog e rubycritic, obtemos a seguinte saída, que indica que as complexidades foram reduzidas: + +```ruby +═══════════════════════════════════════════════════════════════════════════════ + ANÁLISE ABC SCORE - TODOS OS MÉTODOS +═══════════════════════════════════════════════════════════════════════════════ + +🟡 MÉDIO 17.2 │ Admin::FormsController#build_form_from_params app/controllers/admin/forms_controller.rb:75-84 +🟡 MÉDIO 16.5 │ Student::FormsController#answer app/controllers/student/forms_controller.rb:20-37 +🟢 BOM 15.0 │ Admin::ImportsController#import_klasses app/controllers/admin/imports_controller.rb:13-19 +🟢 BOM 14.8 │ Admin::FormTemplatesHelper#link_to_add_fields app/helpers/admin/form_templates_helper.rb:4-14 +🟢 BOM 14.4 │ Student::FormsController#submit_answer app/controllers/student/forms_controller.rb:39-52 +🟢 BOM 13.5 │ FormResponse#build_answers_for_fields app/models/form_response.rb:23-29 +🟢 BOM 13.3 │ HomeController#index app/controllers/home_controller.rb:6-13 +🟢 BOM 13.0 │ User#pending_forms app/models/user.rb:29-35 +🟢 BOM 10.9 │ Admin::ImportsController#handle_success app/controllers/admin/imports_controller.rb:40-48 +🟢 BOM 9.8 │ ImportService#import_single_klass app/services/import_service.rb:34-40 +🟢 BOM 9.6 │ Form#none +🟢 BOM 9.4 │ Admin::FormTemplatesController#create app/controllers/admin/form_templates_controller.rb:21-29 +🟢 BOM 9.3 │ ImportService#find_or_create_klass app/services/import_service.rb:42-50 +🟢 BOM 9.3 │ ImportService#build_user app/services/import_service.rb:84-98 +🟢 BOM 9.0 │ ImportService#create_new_user app/services/import_service.rb:74-82 +🟢 BOM 9.0 │ FormTemplateField#none +🟢 BOM 8.9 │ Student::FormsController#check_form_accessible app/controllers/student/forms_controller.rb:64-68 +🟢 BOM 8.4 │ Klass#none +🟢 BOM 8.3 │ User#none +🟢 BOM 7.6 │ Form#pending_responses app/models/form.rb:18-20 +🟢 BOM 7.6 │ FormTemplateField#ensure_position_is_integer app/models/form_template_field.rb:26-28 +🟢 BOM 7.5 │ Student::FormsController#index app/controllers/student/forms_controller.rb:10-13 +🟢 BOM 7.4 │ ImportService#import_single_student app/services/import_service.rb:60-68 +🟢 BOM 7.1 │ Admin::ImportsController#valid_json_file? app/controllers/admin/imports_controller.rb:23-26 +🟢 BOM 7.1 │ Admin::FormsController#update app/controllers/admin/forms_controller.rb:39-46 +🟢 BOM 7.0 │ FormResponse#none +🟢 BOM 7.0 │ FormTemplate#none +🟢 BOM 7.0 │ User#completed_forms app/models/user.rb:38-41 +🟢 BOM 6.9 │ Admin::FormsController#create app/controllers/admin/forms_controller.rb:24-33 +🟢 BOM 6.1 │ ImportService#parse_json_file app/services/import_service.rb:26-32 +🟢 BOM 5.9 │ Student::FormsController#show app/controllers/student/forms_controller.rb:15-18 +🟢 BOM 5.9 │ Admin::FormsController#publish app/controllers/admin/forms_controller.rb:53-59 +🟢 BOM 5.9 │ Admin::FormsController#close app/controllers/admin/forms_controller.rb:61-67 +🟢 BOM 5.8 │ Admin::FormTemplatesController#update app/controllers/admin/form_templates_controller.rb:35-41 +🟢 BOM 5.8 │ Admin::UsersController#update app/controllers/admin/users_controller.rb:19-25 +🟢 BOM 5.8 │ ImportService#import_klasses app/services/import_service.rb:12-22 +🟢 BOM 5.2 │ Admin::DashboardController#index app/controllers/admin/dashboard_controller.rb:8-12 +🟢 BOM 5.0 │ Admin::FormsController#none +🟢 BOM 5.0 │ Student::FormsController#none +🟢 BOM 5.0 │ FormAnswer#none +🟢 BOM 4.8 │ Admin::FormsController#show app/controllers/admin/forms_controller.rb:14-17 +🟢 BOM 4.8 │ Klass#students app/models/klass.rb:18-20 +🟢 BOM 4.8 │ Klass#teachers app/models/klass.rb:22-24 +🟢 BOM 4.7 │ Admin::DashboardController#check_admin app/controllers/admin/dashboard_controller.rb:16-18 +🟢 BOM 4.7 │ Admin::FormTemplatesController#edit app/controllers/admin/form_templates_controller.rb:31-33 +🟢 BOM 4.7 │ Admin::FormTemplatesController#check_admin app/controllers/admin/form_templates_controller.rb:61-63 +🟢 BOM 4.7 │ Admin::FormsController#check_admin app/controllers/admin/forms_controller.rb:103-105 +🟢 BOM 4.7 │ Admin::ImportsController#check_admin app/controllers/admin/imports_controller.rb:56-58 +🟢 BOM 4.7 │ Admin::UsersController#check_admin app/controllers/admin/users_controller.rb:42-44 +🟢 BOM 4.7 │ Student::FormsController#check_student app/controllers/student/forms_controller.rb:60-62 +🟢 BOM 4.7 │ Student::FormsController#update_answers app/controllers/student/forms_controller.rb:70-73 +🟢 BOM 4.6 │ Admin::ImportsController#handle_import_result app/controllers/admin/imports_controller.rb:32-38 +🟢 BOM 4.0 │ Admin::FormTemplatesController#none +🟢 BOM 4.0 │ Admin::UsersController#none +🟢 BOM 4.0 │ ClassMember#none +🟢 BOM 3.9 │ Admin::ImportsController#build_error_message app/controllers/admin/imports_controller.rb:50-54 +🟢 BOM 3.8 │ Admin::ImportsController#index app/controllers/admin/imports_controller.rb:8-11 +🟢 BOM 3.7 │ Admin::FormTemplatesController#index app/controllers/admin/form_templates_controller.rb:9-11 +🟢 BOM 3.7 │ Admin::FormTemplatesController#set_form_template app/controllers/admin/form_templates_controller.rb:50-52 +🟢 BOM 3.7 │ Admin::FormsController#set_form app/controllers/admin/forms_controller.rb:91-93 +🟢 BOM 3.7 │ Admin::FormsController#set_form_response app/controllers/admin/forms_controller.rb:95-97 +🟢 BOM 3.7 │ Admin::UsersController#set_user app/controllers/admin/users_controller.rb:34-36 +🟢 BOM 3.7 │ Student::FormsController#set_form app/controllers/student/forms_controller.rb:56-58 +🟢 BOM 3.7 │ ImportService#import_students app/services/import_service.rb:52-58 +🟢 BOM 3.6 │ ImportService#find_or_create_user app/services/import_service.rb:70-72 +🟢 BOM 3.6 │ Admin::FormTemplatesController#form_template_params app/controllers/admin/form_templates_controller.rb:54-59 +🟢 BOM 3.6 │ Admin::FormsController#form_params app/controllers/admin/forms_controller.rb:99-101 +🟢 BOM 3.6 │ Admin::UsersController#user_params app/controllers/admin/users_controller.rb:38-40 +🟢 BOM 3.4 │ Form#completed_responses app/models/form.rb:23-25 +🟢 BOM 3.4 │ Admin::FormTemplatesController#new app/controllers/admin/form_templates_controller.rb:16-19 +🟢 BOM 3.2 │ Admin::FormTemplatesController#destroy app/controllers/admin/form_templates_controller.rb:43-46 +🟢 BOM 3.2 │ Admin::FormsController#destroy app/controllers/admin/forms_controller.rb:48-51 +🟢 BOM 3.2 │ Admin::UsersController#destroy app/controllers/admin/users_controller.rb:27-30 +🟢 BOM 3.0 │ Admin::DashboardController#none +🟢 BOM 3.0 │ Admin::ImportsController#none +🟢 BOM 3.0 │ ImportService#initialize app/services/import_service.rb:6-10 +🟢 BOM 2.8 │ Admin::FormsController#load_form_dependencies app/controllers/admin/forms_controller.rb:86-89 +🟢 BOM 2.4 │ Admin::FormsController#index app/controllers/admin/forms_controller.rb:10-12 +🟢 BOM 2.2 │ Admin::FormsController#new app/controllers/admin/forms_controller.rb:19-22 +🟢 BOM 2.2 │ Admin::ImportsController#redirect_with_error app/controllers/admin/imports_controller.rb:28-30 +🟢 BOM 2.2 │ FormResponse#completed? app/models/form_response.rb:14-16 +🟢 BOM 2.2 │ FormResponse#pending? app/models/form_response.rb:18-20 +🟢 BOM 2.2 │ FormResponse#submit! app/models/form_response.rb:32-34 +🟢 BOM 2.2 │ FormTemplateField#requires_options? app/models/form_template_field.rb:22-24 +🟢 BOM 2.2 │ User#admin? app/models/user.rb:20-22 +🟢 BOM 2.2 │ User#user? app/models/user.rb:24-26 +🟢 BOM 2.0 │ ApplicationMailer#none +🟢 BOM 2.0 │ ImportService#none +🟢 BOM 1.4 │ Admin::UsersController#index app/controllers/admin/users_controller.rb:9-11 +🟢 BOM 1.0 │ Admin::FormsController#edit app/controllers/admin/forms_controller.rb:35-37 +🟢 BOM 1.0 │ ApplicationController#none +🟢 BOM 1.0 │ HomeController#none +🟢 BOM 1.0 │ ApplicationRecord#none + +═══════════════════════════════════════════════════════════════════════════════ +📊 RESUMO: 🔴 0 alto (>20) │ 🟡 2 médio (>15) │ 🟢 91 bom (≤15) +═══════════════════════════════════════════════════════════════════════════════ +``` + +Caso deseje, basta abrir o arquivo overview.html na pasta do rubycritic (dentro de tmp) e analisar os resultados. + +--- + +## 2. Garantindo cobertura de testes > 90%: + +Após realizar a instalação e configuração do simplecov e executar o comando `bundle exec rspec` é possível obter a cobertura total dos testes do nosso código: + +```ruby +bgrod@Bernardo:~/sprint3/CAMAAR-Grupo-10$ bundle exec rspec + +Randomized with seed 41365 +..............**...................................................................................................................................................*................***............................................*.......**.............................................................* + +Pending: (Failures listed here are expected and do not affect your suite's status) + + 1) users/show.html.erb add some examples to (or delete) /home/bgrod/sprint3/CAMAAR-Grupo-10/spec/views/admin/users/show.html.erb_spec.rb + # Not yet implemented + # ./spec/views/admin/users/show.html.erb_spec.rb:4 + + 2) Admin::DashboardHelper add some examples to (or delete) /home/bgrod/sprint3/CAMAAR-Grupo-10/spec/helpers/admin/dashboard_helper_spec.rb + # Not yet implemented + # ./spec/helpers/admin/dashboard_helper_spec.rb:14 + + 3) users/index.html.erb add some examples to (or delete) /home/bgrod/sprint3/CAMAAR-Grupo-10/spec/views/admin/users/index.html.erb_spec.rb + # Not yet implemented + # ./spec/views/admin/users/index.html.erb_spec.rb:4 + + 4) users/edit.html.erb add some examples to (or delete) /home/bgrod/sprint3/CAMAAR-Grupo-10/spec/views/admin/users/edit.html.erb_spec.rb + # Not yet implemented + # ./spec/views/admin/users/edit.html.erb_spec.rb:4 + + 5) users/update.html.erb add some examples to (or delete) /home/bgrod/sprint3/CAMAAR-Grupo-10/spec/views/admin/users/update.html.erb_spec.rb + # Not yet implemented + # ./spec/views/admin/users/update.html.erb_spec.rb:4 + + 6) dashboard/index.html.erb add some examples to (or delete) /home/bgrod/sprint3/CAMAAR-Grupo-10/spec/views/admin/dashboard/index.html.erb_spec.rb + # Not yet implemented + # ./spec/views/admin/dashboard/index.html.erb_spec.rb:4 + + 7) Admin::UsersHelper add some examples to (or delete) /home/bgrod/sprint3/CAMAAR-Grupo-10/spec/helpers/admin/users_helper_spec.rb + # Not yet implemented + # ./spec/helpers/admin/users_helper_spec.rb:14 + + 8) users/destroy.html.erb add some examples to (or delete) /home/bgrod/sprint3/CAMAAR-Grupo-10/spec/views/admin/users/destroy.html.erb_spec.rb + # Not yet implemented + # ./spec/views/admin/users/destroy.html.erb_spec.rb:4 + + 9) HomeHelper add some examples to (or delete) /home/bgrod/sprint3/CAMAAR-Grupo-10/spec/helpers/home_helper_spec.rb + # Not yet implemented + # ./spec/helpers/home_helper_spec.rb:14 + + 10) home/index.html.erb add some examples to (or delete) /home/bgrod/sprint3/CAMAAR-Grupo-10/spec/views/home/index.html.erb_spec.rb + # Not yet implemented + # ./spec/views/home/index.html.erb_spec.rb:4 + +Top 10 slowest examples (2.52 seconds, 51.7% of total time): + Admin::Forms POST /admin/forms (create) com parâmetros inválidos não cria um formulário sem template + 0.30314 seconds ./spec/requests/admin/forms_spec.rb:163 + Student::Forms GET /student/forms/:id (show) retorna 404 para formulário inexistente + 0.29641 seconds ./spec/requests/student/forms_spec.rb:87 + Admin::Forms GET /admin/forms/:id/view_response retorna 404 para resposta inexistente + 0.27824 seconds ./spec/requests/admin/forms_spec.rb:360 + Admin::Users GET /admin/users/:id (show) retorna 404 para usuário inexistente + 0.26641 seconds ./spec/requests/admin/users_spec.rb:66 + Admin::Forms GET /admin/forms/:id (show) retorna 404 para formulário inexistente + 0.26154 seconds ./spec/requests/admin/forms_spec.rb:94 + Admin::FormTemplates DELETE /admin/form_templates/:id (destroy) redireciona com 404 para template inexistente + 0.25699 seconds ./spec/requests/admin/form_templates_spec.rb:367 + Admin::FormTemplates GET /admin/form_templates/:id/edit redireciona com 404 para template inexistente + 0.25622 seconds ./spec/requests/admin/form_templates_spec.rb:237 + Admin::FormTemplates GET /admin/form_templates/:id (show) retorna 404 para template inexistente + 0.25605 seconds ./spec/requests/admin/form_templates_spec.rb:95 + FormAnswer#create não cria answer sem resposta + 0.24659 seconds ./spec/models/form_answer_spec.rb:50 + Admin::Dashboards GET admin dashboard returns http success + 0.09424 seconds ./spec/requests/admin/dashboard_spec.rb:13 + +Top 10 slowest example groups: + Admin::Dashboards + 0.09441 seconds average (0.09441 seconds / 1 example) ./spec/requests/admin/dashboard_spec.rb:3 + Admin::Forms + 0.03612 seconds average (1.26 seconds / 35 examples) ./spec/requests/admin/forms_spec.rb:5 + Student::Forms + 0.03049 seconds average (0.57935 seconds / 19 examples) ./spec/requests/student/forms_spec.rb:3 + Admin::FormTemplates + 0.02982 seconds average (0.98415 seconds / 33 examples) ./spec/requests/admin/form_templates_spec.rb:5 + FormAnswer + 0.02464 seconds average (0.34501 seconds / 14 examples) ./spec/models/form_answer_spec.rb:5 + Admin::Users + 0.02451 seconds average (0.44119 seconds / 18 examples) ./spec/requests/admin/users_spec.rb:3 + Homes + 0.01233 seconds average (0.19728 seconds / 16 examples) ./spec/requests/home_spec.rb:3 + Admin::Imports + 0.00922 seconds average (0.23047 seconds / 25 examples) ./spec/requests/admin/imports_spec.rb:3 + FormResponse + 0.00854 seconds average (0.17934 seconds / 21 examples) ./spec/models/form_response_spec.rb:5 + Form + 0.00727 seconds average (0.16732 seconds / 23 examples) ./spec/models/form_spec.rb:5 + +Finished in 4.87 seconds (files took 2.14 seconds to load) +299 examples, 0 failures, 10 pending + +Randomized with seed 41365 + +Coverage report generated for RSpec to /home/bgrod/sprint3/CAMAAR-Grupo-10/coverage. +Line Coverage: 98.58% (347 / 352) + +COVERAGE: 98.58% -- 347/352 lines in 22 files + ++----------+-------------------------------------------+-------+--------+---------------+ +| coverage | file | lines | missed | missing | ++----------+-------------------------------------------+-------+--------+---------------+ +| 92.45% | app/controllers/admin/forms_controller.rb | 53 | 4 | 30-31, 57, 65 | +| 97.87% | app/services/import_service.rb | 47 | 1 | 39 | ++----------+-------------------------------------------+-------+--------+---------------+ +20 file(s) with 100% coverage not shown +``` + +Como é possível observar, todos os testes estão passando e a cobertura total é de 98.58%, com todos os controllers, models e services atingindo cobertura maior que 90%. +OBS: Alguns testes não estão implementados como visto no resultado (indicados com *), mas eles não influenciam ou indicam mal funcionamento da plataforma. + +--- + +## 3. Happy Paths e Sad Paths no Cucumber + +Após verificar a instalação e funcionamento do Cucumber em nosso sistema, desenvolvemos os arquivos de step.rb para realizar a execução dos happy path e sad path descritos nas features: + +```ruby +├── step_definitions +│   ├── admin_response_steps.rb +│   ├── authentication_steps.rb +│   ├── form_field_steps.rb +│   ├── form_steps.rb +│   ├── import_steps.rb +│   ├── navigation_steps.rb +│   ├── student_form_steps.rb +│   ├── template_steps.rb +│   └── user_steps.rb +``` + +E após a implementação desses arquivos e a execução do comando `bundle exec cucumber` obtemos a saída, indicando sucesso na implementação: + +```ruby +bgrod@Bernardo:~/sprint3/CAMAAR-Grupo-10$ bundle exec cucumber +Using the default profile... +# language: pt +Funcionalidade: Criar e Gerenciar Templates de Formulários + Como administrador + Eu quero criar templates de formulários reutilizáveis + Para usar como base na criação de múltiplos formulários + + Contexto: # features/criar_gerenciar_templates.feature:7 + Dado que sou um usuário admin autenticado # features/step_definitions/template_steps.rb:5 + + # HAPPY PATH + Cenário: Listar todos os templates criados # features/criar_gerenciar_templates.feature:11 + Dado que existem 3 templates criados pelo admin # features/step_definitions/template_steps.rb:40 + Quando acesso a página de templates # features/step_definitions/template_steps.rb:65 + Então devo ver todos os 3 templates listados # features/step_definitions/template_steps.rb:129 + + Cenário: Deletar template com sucesso # features/criar_gerenciar_templates.feature:16 + Dado que existe um template chamado "Pesquisa Antiga" # features/step_definitions/template_steps.rb:25 + Quando acesso a página de templates # features/step_definitions/template_steps.rb:65 + E clico no botão "Deletar" # features/step_definitions/authentication_steps.rb:23 + Então o template não deve estar mais listado # features/step_definitions/template_steps.rb:124 + + # SAD PATH + Cenário: Falha ao criar template sem nome # features/criar_gerenciar_templates.feature:23 + Quando acesso a página de criação de template # features/step_definitions/template_steps.rb:69 + E deixo o nome vazio # features/step_definitions/template_steps.rb:92 + E preencho a descrição com "Uma descrição" # features/step_definitions/template_steps.rb:88 + E clico no botão "Criar Template" # features/step_definitions/authentication_steps.rb:23 + Então o template não deve ser criado # features/step_definitions/template_steps.rb:135 + + Cenário: Falha ao criar template sem descrição # features/criar_gerenciar_templates.feature:30 + Quando acesso a página de criação de template # features/step_definitions/template_steps.rb:69 + E preencho o nome com "Template Incompleto" # features/step_definitions/template_steps.rb:83 + E deixo a descrição vazia # features/step_definitions/template_steps.rb:96 + E clico no botão "Criar Template" # features/step_definitions/authentication_steps.rb:23 + Então o template não deve ser criado # features/step_definitions/template_steps.rb:135 + + Cenário: Usuário não-admin não consegue criar template # features/criar_gerenciar_templates.feature:37 + Dado que sou um usuário dicente autenticado # features/step_definitions/template_steps.rb:15 + Quando tento acessar a página de criação de template (/admin/form_templates/new) # features/step_definitions/template_steps.rb:77 + Então devo ser redirecionado para a página inicial # features/step_definitions/template_steps.rb:140 + +# language: pt +Funcionalidade: Criar e Publicar Formulários + Como administrador + Eu quero criar formulários baseados em templates + E publicar para que alunos de uma turma possam responder + + Contexto: # features/criar_publicar_formularios.feature:7 + Dado que sou um usuário admin autenticado # features/step_definitions/template_steps.rb:5 + E existe um template de formulário chamado "Pesquisa de Satisfação" # features/step_definitions/form_steps.rb:5 + E existe uma turma "CC001" com 3 alunos registrados # features/step_definitions/form_steps.rb:20 + + Cenário: Criar formulário com sucesso # features/criar_publicar_formularios.feature:13 + Quando acesso a página de formulários (/admin/forms) # features/step_definitions/form_steps.rb:95 + E clico no link "Novo Formulário" # features/step_definitions/authentication_steps.rb:27 + E seleciono o template "Pesquisa de Satisfação" # features/step_definitions/form_steps.rb:117 + E seleciono a turma "CC001" # features/step_definitions/form_steps.rb:121 + E preencho o título com "Pesquisa - Algoritmos - 2025.1" # features/step_definitions/form_steps.rb:125 + E preencho a descrição com "Sua opinião é importante para melhorar o curso" # features/step_definitions/template_steps.rb:88 + E clico no botão "Criar Formulário" # features/step_definitions/authentication_steps.rb:23 + Então o formulário deve estar com status "Rascunho" # features/step_definitions/form_steps.rb:146 + E o formulário deve estar listado na página de formulários # features/step_definitions/form_steps.rb:150 + + Cenário: Publicar formulário com sucesso # features/criar_publicar_formularios.feature:24 + Dado que existe um formulário em status "Rascunho" # features/step_definitions/form_steps.rb:31 + Quando acesso a página de formulários # features/step_definitions/form_steps.rb:99 + E clico no botão "Publicar" # features/step_definitions/authentication_steps.rb:23 + Então o formulário deve ter status "Publicado" # features/step_definitions/form_steps.rb:155 + + Cenário: Fechar formulário com sucesso # features/criar_publicar_formularios.feature:30 + Dado que existe um formulário em status "Publicado" # features/step_definitions/form_steps.rb:31 + Quando acesso o formulário na página show # features/step_definitions/form_steps.rb:107 + E clico no botão "Fechar" # features/step_definitions/authentication_steps.rb:23 + Então o formulário deve ter status "Fechado" # features/step_definitions/form_steps.rb:155 + + Cenário: Editar formulário em rascunho # features/criar_publicar_formularios.feature:36 + Dado que existe um formulário em status "Rascunho" com título "Pesquisa v1" # features/step_definitions/form_steps.rb:52 + Quando acesso o formulário na página show # features/step_definitions/form_steps.rb:107 + E clico no link "Editar" # features/step_definitions/authentication_steps.rb:27 + E altero o título para "Pesquisa v2" # features/step_definitions/form_steps.rb:139 + E clico no botão "Atualizar Formulário" # features/step_definitions/authentication_steps.rb:23 + Então o formulário deve ter o novo título "Pesquisa v2" # features/step_definitions/form_steps.rb:169 + + Cenário: Listar todos os formulários # features/criar_publicar_formularios.feature:44 + Dado que existem 3 formulários criados # features/step_definitions/form_steps.rb:74 + Quando acesso a página de formulários # features/step_definitions/form_steps.rb:99 + Então devo ver todos os 3 formulários listados # features/step_definitions/form_steps.rb:159 + + Cenário: Falha ao criar formulário sem título # features/criar_publicar_formularios.feature:50 + Quando acesso a página de criação de formulário # features/step_definitions/form_steps.rb:103 + E seleciono o template "Pesquisa de Satisfação" # features/step_definitions/form_steps.rb:117 + E seleciono a turma "CC001" # features/step_definitions/form_steps.rb:121 + E deixo o título vazio # features/step_definitions/form_steps.rb:135 + E preencho a descrição com "Uma descrição" # features/step_definitions/template_steps.rb:88 + E clico no botão "Criar Formulário" # features/step_definitions/authentication_steps.rb:23 + Então o formulário não deve ser criado # features/step_definitions/form_steps.rb:165 + + Cenário: Falha ao editar formulário já publicado # features/criar_publicar_formularios.feature:59 + Dado que existe um formulário em status "Publicado" # features/step_definitions/form_steps.rb:31 + Quando acesso o formulário na página show # features/step_definitions/form_steps.rb:107 + Então não devo ver o botão "Editar" # features/step_definitions/form_steps.rb:173 + + Cenário: Usuário não-admin não consegue criar formulário # features/criar_publicar_formularios.feature:64 + Dado que sou um usuário dicente autenticado # features/step_definitions/template_steps.rb:15 + Quando tento acessar a página de criação de formulário (/admin/forms/new) # features/step_definitions/form_steps.rb:111 + Então devo ser redirecionado para a página inicial # features/step_definitions/template_steps.rb:140 + +# language: pt +Funcionalidade: Importar Turmas e Alunos via JSON + Como administrador + Eu quero importar turmas e alunos de um arquivo JSON + Para registrar os dados dos estudantes no sistema de forma automatizada + + Contexto: # features/importar_turmas_alunos.feature:7 + Dado que sou um usuário admin autenticado # features/step_definitions/template_steps.rb:5 + + Cenário: Acessar página de importação # features/importar_turmas_alunos.feature:11 + Quando acesso a página de importação (/admin/imports) # features/step_definitions/import_steps.rb:33 + Então devo ver o formulário de upload de arquivo # features/step_definitions/import_steps.rb:47 + E devo ver as instruções de importação # features/step_definitions/import_steps.rb:52 + + Cenário: Visualizar estatísticas da importação # features/importar_turmas_alunos.feature:16 + Dado que existem 3 turmas importadas # features/step_definitions/import_steps.rb:5 + E existem 10 alunos registrados # features/step_definitions/import_steps.rb:18 + Quando acesso a página de importação # features/step_definitions/import_steps.rb:37 + Então devo ver "3" turmas no total # features/step_definitions/import_steps.rb:57 + E devo ver "10" alunos no total # features/step_definitions/import_steps.rb:63 + + Cenário: Usuário não-admin não consegue acessar importação # features/importar_turmas_alunos.feature:24 + Dado que sou um usuário dicente autenticado # features/step_definitions/template_steps.rb:15 + Quando tento acessar a página de importação (/admin/imports) # features/step_definitions/import_steps.rb:41 + Então devo ser redirecionado para a página inicial # features/step_definitions/template_steps.rb:140 + +# language: pt +Funcionalidade: Login no Sistema CAMAAR + Como usuário do sistema (admin ou dicente) + Eu quero fazer login com minhas credenciais + Para acessar o sistema e suas funcionalidades + + Contexto: # features/login_sistema.feature:7 + Dado que o banco de dados está limpo # features/step_definitions/user_steps.rb:2 + E um usuário admin existe com: # features/step_definitions/user_steps.rb:8 + | email | admin@example.com | + | senha | senha123 | + | nome | Admin | + E um usuário dicente existe com: # features/step_definitions/user_steps.rb:8 + | email | dicente@example.com | + | nome | João Silva | + | matricula | 202201234 | + + Cenário: Admin faz login com sucesso # features/login_sistema.feature:19 + Quando acesso a página de login # features/step_definitions/authentication_steps.rb:3 + E preencho o email com "admin@example.com" # features/step_definitions/authentication_steps.rb:7 + E preencho a senha com "senha123" # features/step_definitions/authentication_steps.rb:11 + E clico no botão "Log in" # features/step_definitions/authentication_steps.rb:23 + Então devo estar autenticado como "admin@example.com" # features/step_definitions/authentication_steps.rb:32 + E devo ser redirecionado para o dashboard admin # features/step_definitions/navigation_steps.rb:1 + E devo ver a mensagem "Bem-vindo, Admin" # features/step_definitions/authentication_steps.rb:40 + + Cenário: Dicente faz login com sucesso # features/login_sistema.feature:28 + Quando acesso a página de login # features/step_definitions/authentication_steps.rb:3 + E preencho o email com "dicente@example.com" # features/step_definitions/authentication_steps.rb:7 + E preencho a senha com "202201234" # features/step_definitions/authentication_steps.rb:11 + E clico no botão "Log in" # features/step_definitions/authentication_steps.rb:23 + Então devo estar autenticado como "dicente@example.com" # features/step_definitions/authentication_steps.rb:32 + E devo ser redirecionado para o dashboard estudante # features/step_definitions/navigation_steps.rb:7 + E devo ver meus formulários pendentes # features/step_definitions/navigation_steps.rb:17 + + Cenário: Login falha com email inexistente # features/login_sistema.feature:38 + Quando acesso a página de login # features/step_definitions/authentication_steps.rb:3 + E preencho o email com "inexistente@example.com" # features/step_definitions/authentication_steps.rb:7 + E preencho a senha com "qualquersenha" # features/step_definitions/authentication_steps.rb:11 +/home/bgrod/.local/share/mise/installs/ruby/3.2.2/lib/ruby/gems/3.2.0/gems/devise-4.9.4/lib/devise/failure_app.rb:80: warning: Status code :unprocessable_entity is deprecated and will be removed in a future version of Rack. Please use :unprocessable_content instead. + E clico no botão "Log in" # features/step_definitions/authentication_steps.rb:23 + Então devo ver a mensagem de erro "E-mail ou senha inválidos" # features/step_definitions/authentication_steps.rb:52 + E não devo estar autenticado # features/step_definitions/authentication_steps.rb:36 + E devo permanecer na página de login # features/step_definitions/navigation_steps.rb:13 + + Cenário: Login falha com senha incorreta # features/login_sistema.feature:47 + Quando acesso a página de login # features/step_definitions/authentication_steps.rb:3 + E preencho o email com "dicente@example.com" # features/step_definitions/authentication_steps.rb:7 + E preencho a senha com "senhaerrada" # features/step_definitions/authentication_steps.rb:11 +/home/bgrod/.local/share/mise/installs/ruby/3.2.2/lib/ruby/gems/3.2.0/gems/devise-4.9.4/lib/devise/failure_app.rb:80: warning: Status code :unprocessable_entity is deprecated and will be removed in a future version of Rack. Please use :unprocessable_content instead. + E clico no botão "Log in" # features/step_definitions/authentication_steps.rb:23 + Então devo ver a mensagem de erro "E-mail ou senha inválidos" # features/step_definitions/authentication_steps.rb:52 + E não devo estar autenticado # features/step_definitions/authentication_steps.rb:36 + E devo permanecer na página de login # features/step_definitions/navigation_steps.rb:13 + + Cenário: Login falha com campos vazios # features/login_sistema.feature:56 + Quando acesso a página de login # features/step_definitions/authentication_steps.rb:3 + E deixo o email vazio # features/step_definitions/authentication_steps.rb:15 + E deixo a senha vazia # features/step_definitions/authentication_steps.rb:19 +/home/bgrod/.local/share/mise/installs/ruby/3.2.2/lib/ruby/gems/3.2.0/gems/devise-4.9.4/lib/devise/failure_app.rb:80: warning: Status code :unprocessable_entity is deprecated and will be removed in a future version of Rack. Please use :unprocessable_content instead. + E clico no botão "Log in" # features/step_definitions/authentication_steps.rb:23 + Então devo ver uma mensagem de validação # features/step_definitions/authentication_steps.rb:59 + E não devo estar autenticado # features/step_definitions/authentication_steps.rb:36 + +# language: pt +Funcionalidade: Responder Formulários como Aluno + Como aluno (dicente) + Eu quero responder formulários publicados da minha turma + Para participar das pesquisas e avaliações + + Contexto: # features/responder_formularios_aluno.feature:7 + Dado que sou um usuário dicente autenticado # features/step_definitions/template_steps.rb:15 + E estou inscrito na turma "CC001" # features/step_definitions/student_form_steps.rb:5 + + Cenário: Visualizar formulários pendentes # features/responder_formularios_aluno.feature:12 + Dado que existe um formulário publicado "Pesquisa de Satisfação" para minha turma # features/step_definitions/student_form_steps.rb:11 + Quando acesso meu dashboard de formulários # features/step_definitions/student_form_steps.rb:153 + Então devo ver a seção "Formulários Pendentes" # features/step_definitions/student_form_steps.rb:169 + E devo ver o formulário "Pesquisa de Satisfação" na lista # features/step_definitions/student_form_steps.rb:173 + + Cenário: Acessar página de responder formulário # features/responder_formularios_aluno.feature:18 + Dado que existe um formulário publicado "Pesquisa de Satisfação" para minha turma # features/step_definitions/student_form_steps.rb:11 + Quando acesso meu dashboard de formulários # features/step_definitions/student_form_steps.rb:153 + E clico no link "Responder Formulário" # features/step_definitions/authentication_steps.rb:27 + Então devo ver a página de resposta do formulário # features/step_definitions/student_form_steps.rb:177 + + Cenário: Formulários pendentes e respondidos aparecem separados # features/responder_formularios_aluno.feature:24 + Dado que já respondi 2 formulários # features/step_definitions/student_form_steps.rb:40 + E existem 3 formulários publicados na minha turma # features/step_definitions/student_form_steps.rb:64 + Quando acesso meu dashboard de formulários # features/step_definitions/student_form_steps.rb:153 + Então devo ver a seção "Formulários Pendentes" # features/step_definitions/student_form_steps.rb:169 + E devo ver a seção "Formulários Respondidos" # features/step_definitions/student_form_steps.rb:169 + E a seção de respondidos deve ter 2 formulários # features/step_definitions/student_form_steps.rb:186 + + Cenário: Visualizar formulário já respondido # features/responder_formularios_aluno.feature:32 + Dado que existe um formulário publicado que já respondi # features/step_definitions/student_form_steps.rb:83 + Quando acesso meu dashboard de formulários # features/step_definitions/student_form_steps.rb:153 + E clico em "Ver" na seção de respondidos # features/step_definitions/student_form_steps.rb:157 + Então devo ver "Formulário respondido" # features/step_definitions/student_form_steps.rb:194 + E devo ver minhas respostas anteriores # features/step_definitions/student_form_steps.rb:198 + + Cenário: Aluno não consegue acessar formulário de outra turma # features/responder_formularios_aluno.feature:40 + Dado que existe um formulário publicado para a turma "CC002" # features/step_definitions/student_form_steps.rb:114 + E não estou inscrito na turma "CC002" # features/step_definitions/student_form_steps.rb:129 + Quando tento acessar esse formulário diretamente # features/step_definitions/student_form_steps.rb:163 + Então devo ser redirecionado para a página inicial # features/step_definitions/template_steps.rb:140 + + Cenário: Formulário em rascunho não aparece para aluno # features/responder_formularios_aluno.feature:46 + Dado que existe um formulário em status "Rascunho" da minha turma # features/step_definitions/student_form_steps.rb:133 + Quando acesso meu dashboard de formulários # features/step_definitions/student_form_steps.rb:153 + Então o formulário não deve aparecer na lista de pendentes # features/step_definitions/student_form_steps.rb:203 + + Cenário: Formulário fechado não aparece para aluno # features/responder_formularios_aluno.feature:51 + Dado que existe um formulário em status "Fechado" da minha turma # features/step_definitions/student_form_steps.rb:133 + Quando acesso meu dashboard de formulários # features/step_definitions/student_form_steps.rb:153 + Então o formulário não deve aparecer na lista de pendentes # features/step_definitions/student_form_steps.rb:203 + +# language: pt +Funcionalidade: Visualizar Respostas de Formulários (Admin) + Como administrador + Eu quero visualizar todas as respostas dos alunos + Para analisar os resultados dos formulários + + Contexto: # features/visualizar_respostas_admin.feature:7 + Dado que sou um usuário admin autenticado # features/step_definitions/template_steps.rb:5 + + Cenário: Visualizar lista de respostas de um formulário # features/visualizar_respostas_admin.feature:11 + Dado que existe um formulário publicado "Pesquisa de Satisfação" # features/step_definitions/admin_response_steps.rb:5 + E 3 alunos responderam o formulário # features/step_definitions/admin_response_steps.rb:36 + Quando acesso a página do formulário # features/step_definitions/admin_response_steps.rb:175 + Então devo ver a seção "Respostas" # features/step_definitions/student_form_steps.rb:169 + E devo ver 3 respostas na tabela # features/step_definitions/admin_response_steps.rb:195 + E cada resposta deve ter o nome do aluno # features/step_definitions/admin_response_steps.rb:199 + E cada resposta deve mostrar o status "Respondido" # features/step_definitions/admin_response_steps.rb:205 + + Cenário: Visualizar detalhes de uma resposta específica # features/visualizar_respostas_admin.feature:20 + Dado que existe um formulário publicado com respostas # features/step_definitions/admin_response_steps.rb:66 + E um aluno "João Silva" respondeu o formulário # features/step_definitions/admin_response_steps.rb:70 + Quando acesso a página do formulário # features/step_definitions/admin_response_steps.rb:175 + E clico em "Ver Respostas" na linha do aluno # features/step_definitions/admin_response_steps.rb:179 + Então devo ver o nome "João Silva" # features/step_definitions/admin_response_steps.rb:211 + E devo ver a data de submissão # features/step_definitions/admin_response_steps.rb:215 + E devo ver todas as perguntas e respostas dele # features/step_definitions/admin_response_steps.rb:219 + + Cenário: Visualizar formulário sem respostas # features/visualizar_respostas_admin.feature:29 + Dado que existe um formulário publicado sem respostas # features/step_definitions/admin_response_steps.rb:98 + Quando acesso a página do formulário # features/step_definitions/admin_response_steps.rb:175 + Então devo ver a mensagem "Nenhum aluno respondeu ainda" # features/step_definitions/authentication_steps.rb:40 + + Cenário: Visualizar formulário fechado com respostas # features/visualizar_respostas_admin.feature:34 + Dado que existe um formulário em status "Fechado" # features/step_definitions/form_steps.rb:31 + E 2 alunos responderam antes de fechar # features/step_definitions/admin_response_steps.rb:103 + Quando acesso a página do formulário # features/step_definitions/admin_response_steps.rb:175 + Então devo ver o badge "Fechado" # features/step_definitions/admin_response_steps.rb:224 + E devo conseguir ver as 2 respostas coletadas # features/step_definitions/admin_response_steps.rb:228 + + Cenário: Admin pode acessar formulários de outros admins # features/visualizar_respostas_admin.feature:41 + Dado que existe um formulário criado por outro admin # features/step_definitions/admin_response_steps.rb:148 + Quando acesso esse formulário # features/step_definitions/admin_response_steps.rb:185 + Então devo conseguir visualizar o formulário # features/step_definitions/admin_response_steps.rb:232 + E devo conseguir ver as respostas # features/step_definitions/admin_response_steps.rb:236 + + Cenário: Usuário não-admin não consegue visualizar respostas # features/visualizar_respostas_admin.feature:48 + Dado que sou um usuário dicente autenticado # features/step_definitions/template_steps.rb:15 + E existe um formulário publicado # features/step_definitions/admin_response_steps.rb:169 + Quando tento acessar a página admin do formulário # features/step_definitions/admin_response_steps.rb:189 + Então devo ser redirecionado para a página inicial # features/step_definitions/template_steps.rb:140 + +34 scenarios (34 passed) +230 steps (230 passed) +0m1.401s +``` + +--- + +## 4. Documentação com RDoc + +Após a instalação do RDoc no Gemfile e a documentação dos métodos nos arquivos de model, controller e service, o comando `rdoc app/models app/controllers app/services` foi executado, obtendo o seguinte retorno: + +```ruby +bgrod@Bernardo:~/sprint3/CAMAAR-Grupo-10$ rdoc app/models app/controllers app/services +Parsing sources... +100% [18/18] app/services/import_service.rb + +Generating Darkfish format into /home/bgrod/sprint3/CAMAAR-Grupo-10/doc... + +You can visit the home page at: file:///home/bgrod/sprint3/CAMAAR-Grupo-10/doc/index.html + + Files: 18 + + Classes: 18 (1 undocumented) + Modules: 2 (0 undocumented) + Constants: 1 (0 undocumented) + Attributes: 3 (3 undocumented) + Methods: 44 (0 undocumented) + + Total: 68 (4 undocumented) + 94.12% documented + + Elapsed: 0.3s +``` + +Isso indica que tudo ocorreu corretamente. Caso queira, basta abrir o arquivo index.html dentro da pasta doc e analisar a documentação do projeto. \ No newline at end of file diff --git a/Procfile.dev b/Procfile.dev new file mode 100644 index 0000000000..e69de29bb2 diff --git a/README.md b/README.md index 9d7fe1bf53..cf4c8feea6 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,608 @@ -# CAMAAR -Sistema para avaliação de atividades acadêmicas remotas do CIC +# Wiki CAMAAR - Plataforma de Avaliação de Cursos + +OBS: md da sprint 3 está no projeto! O nome do arquivo é "Grupo 10 - Sprint 3 ESW.md" + +--- + +## Informações do Projeto + +| Item | Descrição | +|------|-----------| +| **Nome do Projeto** | CAMAAR - Plataforma de Avaliação de Cursos | +| **Disciplina** | Engenharia de Software | +| **Período** | 2025.2 | +| **Integrantes do Grupo** | Bernardo Gomes Rodrigues - 231034190
Isaac Silva - 231025216
Filipe Abadia Marcelino - 190087161
Maria Carolina Burgum Abreu Jorge - 231013547 | + +--- + +## Papéis Scrum + +| Papel | Responsável | Matrícula | +|-------|-------------|-----------| +| **Scrum Master** | Bernardo Gomes Rodrigues | 231034190 | +| **Product Owner** | Maria Carolina Burgum Abreu Jorge | 231013547 | + +--- + +## Escopo do Projeto + +### Descrição Geral + +O CAMAAR é uma **plataforma web para avaliação de cursos e disciplinas** que integra dados do SIGAA (Sistema Integrado de Gestão de Atividades Acadêmicas) através de importação de arquivos JSON. O sistema permite que administradores criem formulários de avaliação reutilizáveis via templates, que são respondidos por usuários (alunos e professores). Os resultados são visualizados e gerenciados diretamente na plataforma. + +### Objetivos Principais + +- Facilitar a importação e sincronização de dados de turmas, disciplinas e participantes do SIGAA +- Permitir a criação e gestão de templates de formulários reutilizáveis +- Automatizar o processo de coleta de feedback sobre disciplinas e infraestrutura +- Viabilizar a análise de resultados de avaliações através de relatórios interativos +- Gerenciar usuários com autenticação simples e segura + +### Stakeholders + +- **Coordenadores de Curso (Administradores)**: Importam dados, criam templates e formulários, visualizam relatórios +- **Usuários (Alunos e Professores)**: Fazem login e respondem formulários de avaliação quando solicitado +- **Sistema SIGAA**: Fornecedor de dados de turmas, disciplinas e participantes via arquivos JSON + +--- + +## Funcionalidades e Regras de Negócio + +### 1. Autenticação de Usuários (3 pontos) + +**Funcionalidades:** +- Login com email ou matrícula + senha +- Acesso diferenciado por perfil de usuário (usuário ou administrador) + +**Regras de Negócio:** +- Credenciais válidas são necessárias para acessar o sistema +- Cada perfil tem acesso a funcionalidades específicas +- Apenas usuários ativos podem fazer login +- Administrador tem acesso a todas as funcionalidades de gestão +- Usuário comum tem acesso apenas a responder formulários + +**Responsável:** Bernardo Gomes Rodrigues + +--- + +### 2. Definir Senha no Cadastro (2 pontos) + +**Funcionalidades:** +- Usuários novos utilizam senha padrão no primeiro acesso +- Sistema oferece interface de login com email/matrícula + +**Regras de Negócio:** +- Senha padrão é a matrícula do usuário +- A senha não pode ser alterada após o cadastro +- A mesma senha (matrícula) é sempre utilizada para login +- Usuário acessa com email ou matrícula + senha + +**Responsável:** Bernardo Gomes Rodrigues + +--- + +### 3. Importação de Dados do SIGAA (5 pontos) + +**Funcionalidades:** +- Importar turmas, disciplinas e participantes do SIGAA a partir de arquivos JSON +- Carregamento automatizado de dados de estrutura acadêmica +- Criação automática de usuários baseada nos dados importados + +**Regras de Negócio:** +- Importação deve ser realizada apenas por administradores +- Dados são parseados do formato JSON específico do SIGAA +- Novos usuários são criados automaticamente com senha padrão (matrícula) +- Duplicatas são ignoradas (validação por ID/matrícula única) +- Alunos e professores são diferenciados no banco de dados durante a importação +- Turmas são associadas aos seus respectivos professores + +**Responsável:** Bernardo Gomes Rodrigues + +--- + +### 4. Atualizar Base de Dados com Dados do SIGAA (3 pontos) + +**Funcionalidades:** +- Sincronizar informações de turmas, disciplinas e participantes com novos dados do SIGAA +- Manter base de dados atualizada com mudanças acadêmicas + +**Regras de Negócio:** +- Atualização deve ser realizada apenas por administradores +- Novos registros são adicionados à base de dados +- Registros existentes são atualizados com informações recentes +- Mudanças de alocação de alunos em turmas são sincronizadas + +**Responsável:** Bernardo Gomes Rodrigues + +--- + +### 5. Gestão de Usuários e Participantes (2 pontos) + +**Funcionalidades:** +- Cadastro automático de participantes através da importação SIGAA +- Visualização de usuários cadastrados +- Desativação de usuários inativos + +**Regras de Negócio:** +- Usuários são criados automaticamente durante importação +- Senha inicial é a matrícula do usuário (não alterável) +- Usuário inativo não pode fazer login +- Email é obrigatório e único por usuário +- Alunos e professores são classificados conforme dados do SIGAA + +**Responsável:** Bernardo Gomes Rodrigues + +--- + +### 6. Criação de Templates de Formulários (4 pontos) + +**Funcionalidades:** +- Criar templates com múltiplos tipos de questões +- Visualizar templates criados +- Editar templates existentes +- Deletar templates + +**Regras de Negócio:** +- Template deve ter no mínimo 1 questão +- Nome do template deve ser único +- Apenas o criador (administrador) do template pode editá-lo e deletá-lo +- Tipos de questões suportados: Múltipla escolha, Texto aberto, Verdadeiro/Falso +- Mudanças em template apenas afetam novos formulários criados + +**Responsável:** Maria Carolina Burgum Abreu Jorge + +--- + +### 7. Criação de Formulários de Avaliação (6 pontos) + +**Funcionalidades:** +- Criar formulário baseado em template pré-existente +- Selecionar turmas para aplicar o formulário +- Visualizar formulários criados +- Gerenciar status dos formulários + +**Regras de Negócio:** +- Formulário herda todas as questões do template no momento da criação +- Deve selecionar no mínimo 1 turma para aplicar o formulário +- Mesmo formulário não pode ser respondido 2 vezes pelo mesmo participante +- Apenas administradores podem criar formulários +- Formulários podem estar em rascunho ou ativos +- Formulários estarão disponíveis para todos os usuários das turmas selecionadas + +**Responsável:** Isaac Silva + +--- + +### 8. Visualização de Formulários Disponíveis (3 pontos) + +**Funcionalidades:** +- Usuários visualizam formulários não respondidos das suas turmas +- Filtrar formulários por turma e disciplina +- Visualizar informações do formulário antes de responder + +**Regras de Negócio:** +- Usuário visualiza apenas formulários de turmas onde está matriculado ou leciona +- Formulários já respondidos não aparecem na lista +- Informações do formulário incluem: nome, disciplina, turma, questões +- Sistema valida se usuário tem permissão de responder cada formulário + +**Responsável:** Isaac Silva + +--- + +### 9. Resposta de Formulários (5 pontos) + +**Funcionalidades:** +- Responder questionários de avaliação +- Salvar respostas como rascunho +- Submeter respostas finais +- Editar respostas enquanto não submetidas +- Validação de campos obrigatórios + +**Regras de Negócio:** +- Participante visualiza apenas formulários de turmas onde está matriculado ou leciona +- Todos os campos obrigatórios devem ser preenchidos antes de enviar +- Participante pode editar respostas enquanto formulário não foi submetido +- Uma vez submetido, formulário não pode ser alterado +- Resposta não pode ser submetida duas vezes pelo mesmo participante +- Sistema valida tipo de resposta conforme tipo de questão +- Respostas são armazenadas com timestamp de submissão + +**Responsável:** Isaac Silva + +--- + +### 10. Visualização de Resultados dos Formulários (5 pontos) + +**Funcionalidades:** +- Administradores visualizam respostas de todos os formulários criados +- Filtrar resultados por formulário +- Visualizar respostas detalhadas de participantes +- Acompanhamento em tempo real do número de respondentes +- Análise das respostas direto na plataforma + +**Regras de Negócio:** +- Apenas administrador pode acessar resultados +- Respostas são exibidas de forma organizada por formulário +- Informações exibidas incluem: participante, turma, data de resposta, respostas individuais +- Dados são apresentados através de interface web interativa +- Relatórios podem ser visualizados conforme respostas vão chegando +- Nomes de participantes podem ser anonimizados (apenas matrícula) conforme necessário + +**Responsável:** Isaac Silva + +--- + +## Atribuição de Histórias de Usuário + +| # | História de Usuário | Funcionalidade | Pontos | Responsável | +|---|---------------------|-----------------|--------|-------------| +| 1 | Importar dados do SIGAA | Importação de Dados | 5 | Bernardo Gomes | +| 2 | Responder questionário sobre turma | Resposta de Formulários | 5 | Isaac Silva | +| 3 | Cadastrar participantes ao importar | Gestão de Usuários | 2 | Bernardo Gomes | +| 4 | Visualizar resultados no site | Visualização de Resultados | 5 | Isaac Silva | +| 5 | Criar template de formulário | Gerenciamento de Templates | 4 | Maria Carolina | +| 6 | Criar formulário baseado em template | Criação de Formulários | 6 | Maria Carolina | +| 7 | Acessar sistema com credenciais | Autenticação | 3 | Bernardo Gomes | +| 8 | Definir senha no cadastro | Definição de Senha | 2 | Bernardo Gomes | +| 9 | Atualizar base de dados existente | Atualização de Dados | 3 | Bernardo Gomes | +| 10 | Visualizar formulários não respondidos | Visualização de Formulários | 3 | Isaac Silva | +| 11 | Visualizar formulários criados | Criação de Formulários | 2 | Isaac Silva | +| 12 | Visualizar templates criados | Gerenciamento de Templates | 2 | Maria Carolina | +| 13 | Editar e deletar template | Gerenciamento de Templates | 2 | Maria Carolina | + +**Total de Pontos: 40 pontos** + +--- + +## Histórias de Usuário Detalhadas com Pontuação + +### HU-01: Importar dados do SIGAA (5 pontos) + +``` +Como Administrador +Quero importar dados de turmas, disciplinas e participantes do SIGAA através de arquivos JSON +A fim de carregar a base de dados inicial do sistema com informações acadêmicas + +Critérios de Aceitação: +- Sistema importa com sucesso turmas, disciplinas e participantes do SIGAA +- Dados duplicados são ignorados (validação por matrícula/ID único) +- Novos usuários são criados automaticamente com senha padrão (matrícula) +- Alunos e professores são diferenciados no banco de dados conforme dados importados +- Turmas são associadas aos professores responsáveis +- Mensagem de sucesso ou erro é exibida após importação +``` + +**Pontos:** 5 | **Responsável:** Bernardo Gomes Rodrigues + +--- + +### HU-02: Responder questionário sobre turma (5 pontos) + +``` +Como Usuário (Aluno ou Professor) +Quero responder o questionário sobre a turma em que estou matriculado/leciono +A fim de submeter minha avaliação da turma e disciplina + +Critérios de Aceitação: +- Visualiza questionário da turma com todas as questões +- Preenche questões obrigatórias antes de submeter +- Submete respostas finais e recebe confirmação +- Não pode responder 2 vezes o mesmo formulário +- Pode editar respostas enquanto não submeter +- Uma vez submetido, formulário se torna imutável +``` + +**Pontos:** 5 | **Responsável:** Isaac Silva + +--- + +### HU-03: Cadastrar participantes ao importar (2 pontos) + +``` +Como Administrador +Quero cadastrar participantes de turmas do SIGAA ao importar dados +A fim de que eles possam acessar o sistema CAMAAR + +Critérios de Aceitação: +- Novos usuários são criados automaticamente +- Senha inicial é a matrícula do usuário +- Usuário pode fazer login com email/matrícula e essa senha +- Usuários já existentes são associados às novas turmas +``` + +**Pontos:** 2 | **Responsável:** Bernardo Gomes Rodrigues + +--- + +### HU-04: Visualizar resultados no site (5 pontos) + +``` +Como Administrador +Quero visualizar os resultados de um formulário direto na plataforma +A fim de avaliar o desempenho das turmas e analisar feedback + +Critérios de Aceitação: +- Interface exibe todas as respostas de um formulário +- Informações incluem: participante, turma, data, respostas individuais +- Dados são apresentados de forma organizada e legível +- Pode filtrar por formulário específico +- Visualização funciona em tempo real conforme respostas chegam +``` + +**Pontos:** 5 | **Responsável:** Isaac Silva + +--- + +### HU-05: Criar template de formulário (4 pontos) + +``` +Como Administrador +Quero criar um template de formulário contendo questões reutilizáveis +A fim de gerar formulários de avaliações de forma eficiente e consistente + +Critérios de Aceitação: +- Template com nome único +- Mínimo 1 questão obrigatória +- Suporta múltiplos tipos: múltipla escolha, texto aberto, verdadeiro/falso +- Apenas criador pode editar e deletar +- Mudanças em template só afetam novos formulários +``` + +**Pontos:** 4 | **Responsável:** Maria Carolina Burgum Abreu Jorge + +--- + +### HU-06: Criar formulário baseado em template (6 pontos) + +``` +Como Administrador +Quero criar um formulário baseado em um template para turmas específicas +A fim de coletar avaliações das disciplinas de forma organizada + +Critérios de Aceitação: +- Seleciona template existente +- Escolhe múltiplas turmas (mínimo 1) +- Formulário herda questões do template +- Formulário fica disponível para todos os usuários das turmas selecionadas +``` + +**Pontos:** 6 | **Responsável:** Maria Carolina Burgum Abreu Jorge + +--- + +### HU-07: Acessar sistema com credenciais (3 pontos) + +``` +Como Usuário do sistema +Quero acessar o sistema utilizando email/matrícula e senha +A fim de responder formulários ou gerenciar o sistema conforme meu perfil + +Critérios de Aceitação: +- Login com email funciona +- Login com matrícula funciona +- Validação de credenciais funciona +- Redirecionamento para painel/dashboard após sucesso +- Mensagens de erro claras para credenciais inválidas +``` + +**Pontos:** 3 | **Responsável:** Bernardo Gomes Rodrigues + +--- + +### HU-08: Definir senha no cadastro (2 pontos) + +``` +Como Usuário +Quero ter acesso ao sistema com uma senha definida +A fim de acessar o sistema com minhas credenciais de login + +Critérios de Aceitação: +- Senha inicial é a matrícula do usuário +- Senha não pode ser alterada após o cadastro +- Usuário faz login com email/matrícula e a senha (matrícula) +- Mesma senha é sempre utilizada para futuros logins +``` + +**Pontos:** 2 | **Responsável:** Bernardo Gomes Rodrigues + +--- + +### HU-09: Atualizar base de dados existente (3 pontos) + +``` +Como Administrador +Quero atualizar a base de dados com dados atuais do SIGAA +A fim de sincronizar mudanças acadêmicas (novas turmas, alunos, etc) + +Critérios de Aceitação: +- Sincroniza dados existentes +- Adiciona novos dados +- Atualiza mudanças de turmas de alunos/professores +- Mensagem clara de conclusão ou erros +``` + +**Pontos:** 3 | **Responsável:** Bernardo Gomes Rodrigues + +--- + +### HU-10: Visualizar formulários não respondidos (3 pontos) + +``` +Como Usuário (Aluno ou Professor) +Quero visualizar os formulários não respondidos das turmas em que estou matriculado/leciono +A fim de poder escolher qual irei responder + +Critérios de Aceitação: +- Lista apenas formulários não respondidos +- Mostra turma, disciplina, informações relevantes +- Formulários respondidos não aparecem +- Sistema valida se usuário tem permissão de responder cada formulário +``` + +**Pontos:** 3 | **Responsável:** Isaac Silva + +--- + +### HU-11: Visualizar formulários criados (2 pontos) + +``` +Como Administrador +Quero visualizar os formulários que criei +A fim de gerenciar e acompanhar o status das avaliações + +Critérios de Aceitação: +- Lista todos os formulários criados +- Mostra nome, template, turmas, status, data +- Quantidade de respostas exibida +- Opção de visualizar detalhes e resultados +``` + +**Pontos:** 2 | **Responsável:** Isaac Silva + +--- + +### HU-12: Visualizar templates criados (2 pontos) + +``` +Como Administrador +Quero visualizar os templates que criei +A fim de poder editar, deletar ou usar como base para novos formulários + +Critérios de Aceitação: +- Lista todos os templates +- Mostra nome, quantidade de questões, data de criação +- Acesso aos detalhes do template +- Opções de editar e deletar +``` + +**Pontos:** 2 | **Responsável:** Maria Carolina Burgum Abreu Jorge + +--- + +### HU-13: Editar e deletar templates (2 pontos) + +``` +Como Administrador +Quero editar e/ou deletar um template que criei +A fim de organizar e manter meus templates atualizados + +Critérios de Aceitação: +- Pode editar questões de um template +- Confirmação de exclusão antes de deletar +- Apenas criador pode editar e excluir +- Mudanças em template não afetam formulários já criados +``` + +**Pontos:** 2 | **Responsável:** Maria Carolina Burgum Abreu Jorge + +--- + +## Política de Branching + +### Estratégia Git Flow + +``` +- main: Produção (releases e hotfixes) +- develop: Desenvolvimento principal +- feature/HU-XX-descricao: Novas funcionalidades +- bugfix/HU-XX-descricao: Correções de bugs +- hotfix/issue-descricao: Correções urgentes em produção +``` + +### Fluxo de Trabalho + +1. **Iniciar Nova Funcionalidade:** + - Criação de branch para a funcionalidade + - Garantir que a nova branch está atualizada com as alterações da main + +2. **Trabalhar na Funcionalidade:** + - Commits descritivos em português + - Formato: `HU-01: Descrição da alteração` + - Exemplo: `HU-01: Implementar validação de duplicatas` + +3. **Pull Request para Develop:** + - Descrever as alterações + - Referenciar a HU + - Mínimo 1 aprovação antes de merge + - Rodar testes antes de merge + +### Convenção de Commits + +``` +(): + + +``` + +**Tipos:** +- `feat`: Nova funcionalidade (HU) +- `fix`: Correção de bug +- `docs`: Documentação +- `style`: Formatação +- `refactor`: Refatoração +- `test`: Testes +- `chore`: Tarefas de build/dependências + +**Exemplo:** +``` +feat(autenticacao): Implementar login com email e matrícula +``` +--- + +## Estrutura de Dados JSON + +### Arquivo classes.json (exemplo) + +```json +[ + { + "code": "CIC0097", + "name": "BANCOS DE DADOS", + "class": { + "classCode": "TA", + "semester": "2025.2", + "time": "35T45" + } + }, + { + "code": "CIC0105", + "name": "ENGENHARIA DE SOFTWARE", + "class": { + "classCode": "TA", + "semester": "2025.2", + "time": "35M12" + } + } +] +``` + +### Arquivo class_members.json (exemplo) + +```json +[ + { + "code": "CIC0097", + "classCode": "TA", + "semester": "2025.2", + "dicente": [ + { + "nome": "Ana Clara Jordao Perna", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190084006", + "usuario": "190084006", + "email": "acjpjvjp@gmail.com" + } + ], + "docente": { + "nome": "MARISTELA TERTO DE HOLANDA", + "matricula": "123456789", + "usuario": "123456789", + "email": "maristela@unb.br" + } + } +] +``` + +--- + diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000000..cc4cf93277 --- /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/application.js b/app/assets/application.js new file mode 100644 index 0000000000..e4eb668013 --- /dev/null +++ b/app/assets/application.js @@ -0,0 +1,5 @@ +// app/assets/application.js + +//= require rails-ujs +//= require turbo +//= require_tree . diff --git a/app/assets/controllers/form_fields_controller.js b/app/assets/controllers/form_fields_controller.js new file mode 100644 index 0000000000..b1e328d11f --- /dev/null +++ b/app/assets/controllers/form_fields_controller.js @@ -0,0 +1,23 @@ +// app/javascript/controllers/form_fields_controller.js + +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["container"] + + add_field(event) { + event.preventDefault() + + const link = event.currentTarget + const association = link.dataset.association + const content = link.dataset.fields + + const new_id = new Date().getTime() + const regexp = new RegExp("new_" + association, "g") + const new_fields = content.replace(regexp, new_id) + + const temp = document.createElement('div') + temp.innerHTML = new_fields + this.containerTarget.appendChild(temp.firstElementChild) + } +} diff --git a/app/assets/images/.keep b/app/assets/images/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/assets/javascripts/form_fields.js b/app/assets/javascripts/form_fields.js new file mode 100644 index 0000000000..00c2e21e5c --- /dev/null +++ b/app/assets/javascripts/form_fields.js @@ -0,0 +1,18 @@ +// app/assets/javascripts/form_fields.js + +document.addEventListener('DOMContentLoaded', function() { + window.add_fields = function(link, association, content) { + var new_id = new Date().getTime(); + var regexp = new RegExp("new_" + association, "g"); + var new_fields = content.replace(regexp, new_id); + + var container = link.closest('form').querySelector("#fields-container"); + if (container) { + var temp = document.createElement('div'); + temp.innerHTML = new_fields; + container.appendChild(temp.firstElementChild); + } + + return false; + }; +}); diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css new file mode 100644 index 0000000000..44658d98b5 --- /dev/null +++ b/app/assets/stylesheets/application.css @@ -0,0 +1,225 @@ +/* --------------------------------------------------- + GLOBAL DESIGN SYSTEM +--------------------------------------------------- */ + +:root { + /* Marine blue palette */ + --blue-900: #072b44; + --blue-800: #0a3d62; + --blue-700: #0f527f; + --blue-600: #1d6fa1; + --blue-300: #aed7f3; + --blue-100: #e8f5ff; + + /* Light green palette */ + --green-500: #78e08f; + --green-600: #62c977; + --green-100: #e8fbe9; + + /* Neutrals */ + --gray-900: #1c1f21; + --gray-700: #4b5258; + --gray-500: #7d858c; + --gray-200: #e5e9ef; + --gray-100: #f7f9fb; + + --radius-lg: 22px; + --radius-md: 14px; + --shadow-1: 0 4px 14px rgba(0, 0, 0, 0.08); + --shadow-2: 0 10px 30px rgba(0, 0, 0, 0.12); +} + +/* --------------------------------------------------- + PAGE WRAPPER +--------------------------------------------------- */ + +.page-wrapper { + background: var(--gray-100); + min-height: 100vh; +} + +/* --------------------------------------------------- + HERO +--------------------------------------------------- */ + +.hero { + padding: 80px 0; + background: linear-gradient(135deg, var(--blue-900), var(--blue-600)); + color: white; +} + +.hero-title { + font-size: 2.8rem; + font-weight: 700; +} + +.hero-subtitle { + opacity: 0.85; + font-size: 1.3rem; +} + +.hero-back-btn { + margin-top: 22px; + display: inline-block; + padding: 10px 22px; + border-radius: var(--radius-md); + background: var(--green-500); + color: var(--blue-900) !important; + font-weight: 600; + text-decoration: none; + transition: 0.2s; +} + +.hero-back-btn:hover { + background: var(--green-600); +} + +/* --------------------------------------------------- + CONTENT +--------------------------------------------------- */ + +.content-section { + margin-top: -35px; + padding-bottom: 60px; +} + +.shadow-layer { + box-shadow: var(--shadow-1); +} + +/* Cards */ +.info-card, +.responses-card, +.status-card { + border-radius: var(--radius-lg); + background: white; + padding: 32px; + margin-bottom: 40px; +} + +.section-title { + font-weight: 700; + color: var(--blue-800); + margin-bottom: 24px; + letter-spacing: -0.5px; +} + +/* --------------------------------------------------- + INFO GRID +--------------------------------------------------- */ + +.info-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 24px; +} + +.info-label { + font-size: 0.9rem; + color: var(--gray-700); + font-weight: 600; + margin-bottom: 4px; +} + +.info-value { + font-size: 1.15rem; + color: var(--gray-900); +} + +/* --------------------------------------------------- + STATUS CARDS +--------------------------------------------------- */ + +.status-card { + display: flex; + align-items: center; + gap: 18px; +} + +.status-icon { + width: 48px; + height: 48px; + border-radius: 12px; + display: flex; + justify-content: center; + align-items: center; + font-size: 1.4rem; + font-weight: bold; +} + +.completed .status-icon { + background: var(--green-100); + color: var(--green-600); +} + +.pending .status-icon { + background: #fff3cd; + color: #d39e00; +} + +.status-title { + font-size: 1.2rem; + font-weight: 700; +} + +.status-date { + font-size: 0.95rem; + color: var(--gray-700); +} + +/* Primary action */ +.primary-action-btn { + margin-left: auto; + padding: 10px 26px; + border-radius: var(--radius-md); + background: var(--green-500); + color: var(--blue-900); + font-weight: 600; + text-decoration: none; + transition: 0.2s; +} + +.primary-action-btn:hover { + background: var(--green-600); +} + +/* --------------------------------------------------- + TABLE +--------------------------------------------------- */ + +.modern-table { + width: 100%; + border-collapse: separate; + border-spacing: 0 10px; +} + +.modern-table thead tr { + background: var(--blue-900); + color: white; +} + +.modern-table th { + padding: 14px 16px; + font-weight: 600; + letter-spacing: 0.5px; +} + +.modern-table tbody tr { + background: white; + border-radius: var(--radius-md); + box-shadow: var(--shadow-1); +} + +.modern-table td { + padding: 16px; + color: var(--gray-900); +} + +/* Rounded table rows */ +.modern-table tbody tr td:first-child { + border-radius: var(--radius-md) 0 0 var(--radius-md); +} + +.modern-table tbody tr td:last-child { + border-radius: 0 var(--radius-md) var(--radius-md) 0; +} diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb new file mode 100644 index 0000000000..3ab8ba69bf --- /dev/null +++ b/app/controllers/admin/dashboard_controller.rb @@ -0,0 +1,55 @@ +# app/controllers/admin/dashboard_controller.rb + +module Admin + ## + # Controller do dashboard administrativo. + # Exibe estatísticas e resumo geral do sistema para administradores. + # + class DashboardController < ApplicationController + before_action :authenticate_user! + before_action :check_admin + + ## + # Exibe o dashboard com estatísticas de usuários do sistema. + # + # ==== Argumentos + # * Nenhum + # + # ==== Retorno + # * Nenhum (renderiza view) + # + # ==== Efeitos Colaterais + # * Carrega @users com todos os usuários do banco (User.all) + # * Calcula @total_users e @admin_count via queries COUNT no banco + # * Renderiza view admin/dashboard/index.html.erb + # + # ==== Exemplo + # GET /admin # => renderiza dashboard com estatísticas + # + def index + @users = User.all + @total_users = User.count + @admin_count = User.where(role: :admin).count + end + + private + + ## + # Verifica se o usuário atual possui perfil de administrador. + # Redireciona para página inicial se não for admin. + # + # ==== Argumentos + # * Nenhum (usa current_user do Devise) + # + # ==== Retorno + # * Nenhum (void ou redireciona) + # + # ==== Efeitos Colaterais + # * Redireciona para root_path com mensagem de alerta se não for admin + # * Interrompe a execução da action se redirecionar + # + def check_admin + redirect_to root_path, alert: 'Acesso negado!' unless current_user.admin? + end + end +end diff --git a/app/controllers/admin/form_templates_controller.rb b/app/controllers/admin/form_templates_controller.rb new file mode 100644 index 0000000000..90d871bae0 --- /dev/null +++ b/app/controllers/admin/form_templates_controller.rb @@ -0,0 +1,227 @@ +# app/controllers/admin/form_templates_controller.rb + +module Admin + ## + # Controller para gerenciamento de templates de formulários. + # Permite criar, editar, visualizar e deletar templates reutilizáveis. + # + class FormTemplatesController < ApplicationController + before_action :authenticate_user! + before_action :check_admin + before_action :set_form_template, only: [:show, :edit, :update, :destroy] + + ## + # Lista todos os templates de formulário do usuário atual. + # + # ==== Argumentos + # * Nenhum (usa current_user) + # + # ==== Retorno + # * Nenhum (renderiza view) + # + # ==== Efeitos Colaterais + # * Carrega @form_templates via query no banco (ordered by created_at DESC) + # * Renderiza view admin/form_templates/index.html.erb + # + # ==== Exemplo + # GET /admin/form_templates # => lista templates mais recentes primeiro + # + def index + @form_templates = current_user.form_templates.order(created_at: :desc) + end + + ## + # Exibe detalhes de um template específico. + # + # ==== Argumentos + # * Nenhum (usa @form_template definido pelo before_action) + # + # ==== Retorno + # * Nenhum (renderiza view) + # + # ==== Efeitos Colaterais + # * Renderiza view admin/form_templates/show.html.erb + # + # ==== Exemplo + # GET /admin/form_templates/1 # => exibe template e seus campos + # + def show + end + + ## + # Exibe formulário para criação de novo template. + # + # ==== Argumentos + # * Nenhum + # + # ==== Retorno + # * Nenhum (renderiza view) + # + # ==== Efeitos Colaterais + # * Cria objeto FormTemplate em memória (@form_template) + # * Adiciona 1 campo vazio via build para exibir no formulário + # * Renderiza view admin/form_templates/new.html.erb + # + # ==== Exemplo + # GET /admin/form_templates/new # => formulário com 1 campo vazio + # + def new + @form_template = FormTemplate.new + @form_template.form_template_fields.build # Apenas 1 campo vazio + end + + ## + # Cria um novo template de formulário com seus campos. + # + # ==== Argumentos + # * Nenhum (usa form_template_params dos parâmetros do request) + # + # ==== Retorno + # * Nenhum (redireciona ou renderiza view) + # + # ==== Efeitos Colaterais + # * Cria registro de FormTemplate no banco associado ao current_user + # * Cria registros de FormTemplateField via accepts_nested_attributes + # * Redireciona para show do template criado se salvou com sucesso + # * Renderiza view new com status 422 se validações falharem + # + # ==== Exemplo + # POST /admin/form_templates + # # com dados válidos => redirect e "Template criado com sucesso!" + # # com erros => renderiza :new com mensagens de erro + # + def create + @form_template = current_user.form_templates.build(form_template_params) + + if @form_template.save + redirect_to admin_form_template_path(@form_template), notice: 'Template criado com sucesso!' + else + render :new, status: :unprocessable_entity + end + end + + ## + # Exibe formulário de edição de template existente. + # + # ==== Argumentos + # * Nenhum (usa @form_template definido pelo before_action) + # + # ==== Retorno + # * Nenhum (renderiza view) + # + # ==== Efeitos Colaterais + # * Adiciona 1 campo vazio se o template não tiver nenhum campo + # * Renderiza view admin/form_templates/edit.html.erb + # + # ==== Exemplo + # GET /admin/form_templates/1/edit # => formulário de edição + # + def edit + @form_template.form_template_fields.build if @form_template.form_template_fields.empty? + end + + ## + # Atualiza um template de formulário existente. + # + # ==== Argumentos + # * Nenhum (usa @form_template e form_template_params) + # + # ==== Retorno + # * Nenhum (redireciona ou renderiza view) + # + # ==== Efeitos Colaterais + # * Atualiza registro de FormTemplate no banco + # * Atualiza/cria/deleta FormTemplateFields via nested_attributes + # * Redireciona para show do template se atualizar com sucesso + # * Renderiza view edit com status 422 se validações falharem + # + # ==== Exemplo + # PATCH /admin/form_templates/1 + # # com dados válidos => redirect e "Template atualizado com sucesso!" + # + def update + if @form_template.update(form_template_params) + redirect_to admin_form_template_path(@form_template), notice: 'Template atualizado com sucesso!' + else + render :edit, status: :unprocessable_entity + end + end + + ## + # Remove um template de formulário do sistema. + # + # ==== Argumentos + # * Nenhum (usa @form_template definido pelo before_action) + # + # ==== Retorno + # * Nenhum (redireciona) + # + # ==== Efeitos Colaterais + # * Deleta registro de FormTemplate do banco + # * Remove FormTemplateFields associados devido a dependent: :destroy + # * Redireciona para lista de templates com mensagem de sucesso + # + # ==== Exemplo + # DELETE /admin/form_templates/1 # => deleta e redireciona + # + def destroy + @form_template.destroy + redirect_to admin_form_templates_url, notice: 'Template deletado com sucesso!' + end + + private + + ## + # Busca e define o template a partir do ID nos parâmetros da URL. + # + # ==== Argumentos + # * Nenhum (usa params[:id]) + # + # ==== Retorno + # * Nenhum (define @form_template) + # + # ==== Efeitos Colaterais + # * Executa query no banco (FormTemplate.find) + # * Levanta ActiveRecord::RecordNotFound se ID não existir + # + def set_form_template + @form_template = FormTemplate.find(params[:id]) + end + + ## + # Define os parâmetros permitidos para criação/atualização de template. + # Inclui nested attributes para campos do formulário. + # + # ==== Argumentos + # * Nenhum (usa params do request) + # + # ==== Retorno + # * +ActionController::Parameters+ - parâmetros filtrados e permitidos + # + # ==== Efeitos Colaterais + # * Nenhum (apenas filtragem de parâmetros) + # + def form_template_params + params.require(:form_template).permit( + :name, :description, + form_template_fields_attributes: [:id, :field_type, :label, :required, :options, :position, :_destroy] + ) + end + + ## + # Verifica se o usuário atual possui perfil de administrador. + # + # ==== Argumentos + # * Nenhum (usa current_user do Devise) + # + # ==== Retorno + # * Nenhum (void ou redireciona) + # + # ==== Efeitos Colaterais + # * Redireciona para root_path com alerta se não for admin + # + def check_admin + redirect_to root_path, alert: 'Acesso negado!' unless current_user.admin? + end + end +end diff --git a/app/controllers/admin/forms_controller.rb b/app/controllers/admin/forms_controller.rb new file mode 100644 index 0000000000..bfb4351a17 --- /dev/null +++ b/app/controllers/admin/forms_controller.rb @@ -0,0 +1,357 @@ +# app/controllers/admin/forms_controller.rb + +module Admin + ## + # Controller para gerenciamento de formulários pelos administradores. + # Permite criar, editar, publicar, fechar e visualizar respostas de formulários. + # + class FormsController < ApplicationController + before_action :authenticate_user! + before_action :check_admin + before_action :set_form, only: [:show, :edit, :update, :destroy, :publish, :close, :view_response] + before_action :set_form_response, only: [:view_response] + + ## + # Lista todos os formulários do sistema. + # + # ==== Argumentos + # * Nenhum + # + # ==== Retorno + # * Nenhum (renderiza view) + # + # ==== Efeitos Colaterais + # * Carrega @forms com todos os formulários ordenados por data de criação (DESC) + # * Renderiza view admin/forms/index.html.erb + # + # ==== Exemplo + # GET /admin/forms # => lista formulários mais recentes primeiro + # + def index + @forms = Form.all.order(created_at: :desc) + end + + ## + # Exibe detalhes de um formulário com estatísticas de respostas. + # + # ==== Argumentos + # * Nenhum (usa @form definido pelo before_action) + # + # ==== Retorno + # * Nenhum (renderiza view) + # + # ==== Efeitos Colaterais + # * Calcula @pending_count (respostas pendentes) via query COUNT + # * Calcula @completed_count (respostas completadas) via query COUNT + # * Renderiza view admin/forms/show.html.erb + # + # ==== Exemplo + # GET /admin/forms/1 # => exibe formulário com estatísticas + # + def show + @pending_count = @form.pending_responses.count + @completed_count = @form.completed_responses.count + end + + ## + # Exibe formulário para criação de novo formulário. + # + # ==== Argumentos + # * Nenhum + # + # ==== Retorno + # * Nenhum (renderiza view) + # + # ==== Efeitos Colaterais + # * Cria objeto Form em memória (@form) + # * Carrega @form_templates e @klasses para os selects do formulário + # * Renderiza view admin/forms/new.html.erb + # + # ==== Exemplo + # GET /admin/forms/new # => formulário de criação + # + def new + @form = Form.new + load_form_dependencies + end + + ## + # Cria um novo formulário associado a template e turma. + # + # ==== Argumentos + # * Nenhum (usa form_params dos parâmetros do request) + # + # ==== Retorno + # * Nenhum (redireciona ou renderiza view) + # + # ==== Efeitos Colaterais + # * Cria registro de Form no banco com status :draft + # * Associa FormTemplate e Klass ao formulário + # * Redireciona para show do formulário criado se salvou com sucesso + # * Renderiza view new com status 422 se validações falharem + # + # ==== Exemplo + # POST /admin/forms + # # com dados válidos => redirect e "Formulário criado com sucesso!" + # + def create + @form = build_form_from_params + + if @form.save + redirect_to admin_form_path(@form), notice: 'Formulário criado com sucesso!' + else + load_form_dependencies + render :new, status: :unprocessable_entity + end + end + + ## + # Exibe formulário de edição de formulário existente. + # + # ==== Argumentos + # * Nenhum (usa @form definido pelo before_action) + # + # ==== Retorno + # * Nenhum (renderiza view) + # + # ==== Efeitos Colaterais + # * Carrega @form_templates e @klasses para os selects + # * Renderiza view admin/forms/edit.html.erb + # + # ==== Exemplo + # GET /admin/forms/1/edit # => formulário de edição + # + def edit + load_form_dependencies + end + + ## + # Atualiza um formulário existente. + # + # ==== Argumentos + # * Nenhum (usa @form e form_params) + # + # ==== Retorno + # * Nenhum (redireciona ou renderiza view) + # + # ==== Efeitos Colaterais + # * Atualiza registro de Form no banco + # * Redireciona para show do formulário se atualizar com sucesso + # * Renderiza view edit com status 422 se validações falharem + # + # ==== Exemplo + # PATCH /admin/forms/1 + # # com dados válidos => redirect e "Formulário atualizado com sucesso!" + # + def update + if @form.update(form_params) + redirect_to admin_form_path(@form), notice: 'Formulário atualizado com sucesso!' + else + load_form_dependencies + render :edit, status: :unprocessable_entity + end + end + + ## + # Remove um formulário do sistema. + # + # ==== Argumentos + # * Nenhum (usa @form definido pelo before_action) + # + # ==== Retorno + # * Nenhum (redireciona) + # + # ==== Efeitos Colaterais + # * Deleta registro de Form do banco + # * Remove FormResponses e FormAnswers associados devido a dependent: :destroy + # * Redireciona para lista de formulários com mensagem de sucesso + # + # ==== Exemplo + # DELETE /admin/forms/1 # => deleta e redireciona + # + def destroy + @form.destroy + redirect_to admin_forms_url, notice: 'Formulário deletado com sucesso!' + end + + ## + # Publica um formulário, tornando-o disponível para os alunos. + # + # ==== Argumentos + # * Nenhum (usa @form definido pelo before_action) + # + # ==== Retorno + # * Nenhum (redireciona) + # + # ==== Efeitos Colaterais + # * Atualiza status do formulário para :published no banco + # * Torna o formulário visível e respondível pelos alunos da turma + # * Redireciona para show do formulário com mensagem de sucesso ou erro + # + # ==== Exemplo + # PATCH /admin/forms/1/publish # => muda status para published + # + def publish + if @form.update(status: :published) + redirect_to admin_form_path(@form), notice: 'Formulário publicado com sucesso!' + else + redirect_to admin_form_path(@form), alert: 'Erro ao publicar formulário' + end + end + + ## + # Fecha um formulário, impedindo novas respostas. + # + # ==== Argumentos + # * Nenhum (usa @form definido pelo before_action) + # + # ==== Retorno + # * Nenhum (redireciona) + # + # ==== Efeitos Colaterais + # * Atualiza status do formulário para :closed no banco + # * Impede que alunos enviem novas respostas + # * Redireciona para show do formulário com mensagem de sucesso ou erro + # + # ==== Exemplo + # PATCH /admin/forms/1/close # => muda status para closed + # + def close + if @form.update(status: :closed) + redirect_to admin_form_path(@form), notice: 'Formulário fechado com sucesso!' + else + redirect_to admin_form_path(@form), alert: 'Erro ao fechar formulário' + end + end + + ## + # Visualiza a resposta de um aluno específico ao formulário. + # + # ==== Argumentos + # * Nenhum (usa @form e @form_response definidos pelos before_actions) + # + # ==== Retorno + # * Nenhum (renderiza view) + # + # ==== Efeitos Colaterais + # * Renderiza view admin/forms/view_response.html.erb + # * Exibe todas as respostas do aluno ao formulário + # + # ==== Exemplo + # GET /admin/forms/1/responses/5 # => exibe respostas do aluno + # + def view_response + # @form_response já é setado pelo before_action + end + + private + + ## + # Constrói objeto Form a partir dos parâmetros do request. + # Define status inicial como :draft. + # + # ==== Argumentos + # * Nenhum (usa form_params) + # + # ==== Retorno + # * +Form+ - objeto Form não persistido (apenas em memória) + # + # ==== Efeitos Colaterais + # * Executa queries para buscar FormTemplate e Klass + # * Cria objeto Form em memória + # + def build_form_from_params + Form.new( + form_template: FormTemplate.find(form_params[:form_template_id]), + klass: Klass.find(form_params[:klass_id]), + title: form_params[:title], + description: form_params[:description], + due_date: form_params[:due_date], + status: :draft + ) + end + + ## + # Carrega dados necessários para os selects do formulário. + # + # ==== Argumentos + # * Nenhum + # + # ==== Retorno + # * Nenhum (define variáveis de instância) + # + # ==== Efeitos Colaterais + # * Executa queries no banco para carregar @form_templates e @klasses + # + def load_form_dependencies + @form_templates = FormTemplate.all + @klasses = Klass.all + end + + ## + # Busca e define o formulário a partir do ID nos parâmetros da URL. + # + # ==== Argumentos + # * Nenhum (usa params[:id]) + # + # ==== Retorno + # * Nenhum (define @form) + # + # ==== Efeitos Colaterais + # * Executa query no banco (Form.find) + # * Levanta ActiveRecord::RecordNotFound se ID não existir + # + def set_form + @form = Form.find(params[:id]) + end + + ## + # Busca e define a resposta a partir do ID nos parâmetros da URL. + # + # ==== Argumentos + # * Nenhum (usa params[:response_id]) + # + # ==== Retorno + # * Nenhum (define @form_response) + # + # ==== Efeitos Colaterais + # * Executa query no banco (FormResponse.find) + # * Levanta ActiveRecord::RecordNotFound se ID não existir + # + def set_form_response + @form_response = FormResponse.find(params[:response_id]) + end + + ## + # Define os parâmetros permitidos para criação/atualização de formulário. + # + # ==== Argumentos + # * Nenhum (usa params do request) + # + # ==== Retorno + # * +ActionController::Parameters+ - parâmetros filtrados e permitidos + # + # ==== Efeitos Colaterais + # * Nenhum (apenas filtragem de parâmetros) + # + def form_params + params.require(:form).permit(:form_template_id, :klass_id, :title, :description, :due_date, :status) + end + + ## + # Verifica se o usuário atual possui perfil de administrador. + # + # ==== Argumentos + # * Nenhum (usa current_user do Devise) + # + # ==== Retorno + # * Nenhum (void ou redireciona) + # + # ==== Efeitos Colaterais + # * Redireciona para root_path com alerta se não for admin + # + def check_admin + redirect_to root_path, alert: 'Acesso negado!' unless current_user.admin? + end + end +end diff --git a/app/controllers/admin/imports_controller.rb b/app/controllers/admin/imports_controller.rb new file mode 100644 index 0000000000..5573abe407 --- /dev/null +++ b/app/controllers/admin/imports_controller.rb @@ -0,0 +1,184 @@ +# app/controllers/admin/imports_controller.rb + +module Admin + ## + # Controller responsável pela importação de dados do SIGAA. + # Permite importar turmas e alunos através de arquivos JSON. + # + class ImportsController < ApplicationController + before_action :authenticate_user! + before_action :check_admin + + ## + # Exibe a página de importação com estatísticas atuais. + # + # ==== Argumentos + # * Nenhum + # + # ==== Retorno + # * Nenhum (renderiza view) + # + # ==== Efeitos Colaterais + # * Executa queries COUNT no banco para carregar @total_klasses e @total_users + # * Renderiza view admin/imports/index.html.erb + # + # ==== Exemplo + # GET /admin/imports # => renderiza página com estatísticas + # + def index + @total_klasses = Klass.count + @total_users = User.where(role: :user).count + end + + ## + # Processa o arquivo JSON de importação de turmas. + # Valida o arquivo e delega o processamento ao ImportService. + # + # ==== Argumentos + # * Nenhum (usa params[:file] do formulário) + # + # ==== Retorno + # * Nenhum (redireciona com mensagem de sucesso ou erro) + # + # ==== Efeitos Colaterais + # * Valida presença e tipo do arquivo enviado + # * Cria instância de ImportService e executa importação + # * Cria/atualiza registros de Klass, User e ClassMember no banco + # * Redireciona para admin_imports_path com mensagem de resultado + # + # ==== Exemplo + # POST /admin/imports/import_klasses + # # com arquivo válido => "✅ 5 turma(s) importada(s) com sucesso!" + # # sem arquivo => "Por favor, selecione um arquivo" + # + def import_klasses + return redirect_with_error('Por favor, selecione um arquivo') if params[:file].blank? + return redirect_with_error('Por favor, envie um arquivo JSON válido') unless valid_json_file? + + result = ImportService.new(params[:file].path).import_klasses + handle_import_result(result) + end + + private + + ## + # Verifica se o arquivo enviado é um JSON válido. + # + # ==== Argumentos + # * Nenhum (usa params[:file]) + # + # ==== Retorno + # * +Boolean+ - true se o content_type for 'application/json' ou extensão for '.json' + # + # ==== Efeitos Colaterais + # * Nenhum (apenas validação) + # + def valid_json_file? + file = params[:file] + file.content_type == 'application/json' || file.original_filename.end_with?('.json') + end + + ## + # Redireciona para página de importação com mensagem de erro. + # + # ==== Argumentos + # * +message+ - (String) mensagem de erro a ser exibida + # + # ==== Retorno + # * Nenhum (redireciona) + # + # ==== Efeitos Colaterais + # * Redireciona para admin_imports_path com alert + # + def redirect_with_error(message) + redirect_to admin_imports_path, alert: message + end + + ## + # Processa o resultado da importação e redireciona com mensagem apropriada. + # + # ==== Argumentos + # * +result+ - (Hash) resultado retornado pelo ImportService contendo: + # * +:success+ (Boolean) + # * +:imported+ (Integer) + # * +:errors+ (Array) + # * +:error+ (String) - apenas se success: false + # + # ==== Retorno + # * Nenhum (redireciona via handle_success ou redirect_with_error) + # + # ==== Efeitos Colaterais + # * Chama handle_success se resultado for positivo + # * Chama redirect_with_error se houver falha crítica + # + def handle_import_result(result) + if result[:success] + handle_success(result) + else + redirect_with_error("❌ Erro na importação: #{result[:error]}") + end + end + + ## + # Trata importações bem-sucedidas, incluindo avisos de erros parciais. + # + # ==== Argumentos + # * +result+ - (Hash) resultado da importação com :imported e :errors + # + # ==== Retorno + # * Nenhum (redireciona) + # + # ==== Efeitos Colaterais + # * Redireciona com notice se não houver erros parciais + # * Redireciona com alert contendo lista de erros se houver erros parciais + # + def handle_success(result) + message = "✅ #{result[:imported]} turma(s) importada(s) com sucesso!" + + if result[:errors].present? + redirect_to admin_imports_path, alert: build_error_message(message, result[:errors]) + else + redirect_to admin_imports_path, notice: message + end + end + + ## + # Constrói mensagem formatada com erros parciais da importação. + # + # ==== Argumentos + # * +base_message+ - (String) mensagem base de sucesso + # * +errors+ - (Array) lista de erros ocorridos durante importação + # + # ==== Retorno + # * +String+ - mensagem formatada com lista de erros + # + # ==== Efeitos Colaterais + # * Nenhum (apenas concatenação de strings) + # + # ==== Exemplo + # build_error_message("5 turmas importadas", ["Erro ao importar João"]) + # # => "5 turmas importadas\n\n⚠️ Aviso: 1 erro(s) durante importação:\n• Erro ao importar João" + # + def build_error_message(base_message, errors) + message = "#{base_message}\n\n⚠️ Aviso: #{errors.count} erro(s) durante importação:" + errors.each { |error| message += "\n• #{error}" } + message + end + + ## + # Verifica se o usuário atual possui perfil de administrador. + # + # ==== Argumentos + # * Nenhum (usa current_user do Devise) + # + # ==== Retorno + # * Nenhum (void ou redireciona) + # + # ==== Efeitos Colaterais + # * Redireciona para root_path com alerta se não for admin + # + def check_admin + redirect_to root_path, alert: 'Acesso negado!' unless current_user.admin? + end + end +end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb new file mode 100644 index 0000000000..f22a3c657e --- /dev/null +++ b/app/controllers/admin/users_controller.rb @@ -0,0 +1,170 @@ +# app/controllers/admin/users_controller.rb + +module Admin + ## + # Controller para gerenciamento de usuários pelos administradores. + # Permite listar, visualizar, editar e deletar usuários do sistema. + # + class UsersController < ApplicationController + before_action :authenticate_user! + before_action :check_admin + before_action :set_user, only: [:show, :edit, :update, :destroy] + + ## + # Lista todos os usuários cadastrados no sistema. + # + # ==== Argumentos + # * Nenhum + # + # ==== Retorno + # * Nenhum (renderiza view) + # + # ==== Efeitos Colaterais + # * Carrega @users com todos os registros de User via query no banco + # * Renderiza view admin/users/index.html.erb + # + # ==== Exemplo + # GET /admin/users # => renderiza lista de usuários + # + def index + @users = User.all + end + + ## + # Exibe detalhes de um usuário específico. + # + # ==== Argumentos + # * Nenhum (usa @user definido pelo before_action) + # + # ==== Retorno + # * Nenhum (renderiza view) + # + # ==== Efeitos Colaterais + # * Renderiza view admin/users/show.html.erb com dados de @user + # + # ==== Exemplo + # GET /admin/users/1 # => renderiza detalhes do usuário ID 1 + # + def show + end + + ## + # Exibe formulário de edição de usuário. + # + # ==== Argumentos + # * Nenhum (usa @user definido pelo before_action) + # + # ==== Retorno + # * Nenhum (renderiza view) + # + # ==== Efeitos Colaterais + # * Renderiza view admin/users/edit.html.erb com formulário + # + # ==== Exemplo + # GET /admin/users/1/edit # => renderiza formulário de edição + # + def edit + end + + ## + # Atualiza os dados de um usuário. + # + # ==== Argumentos + # * Nenhum (usa @user e params do formulário) + # + # ==== Retorno + # * Nenhum (redireciona ou renderiza view) + # + # ==== Efeitos Colaterais + # * Atualiza registro de User no banco de dados se validações passarem + # * Redireciona para admin_users_path com mensagem de sucesso se atualizar + # * Renderiza view edit com status 422 se validações falharem + # + # ==== Exemplo + # PATCH /admin/users/1 # => atualiza e redireciona ou mostra erros + # + def update + if @user.update(user_params) + redirect_to admin_users_path, notice: 'Usuário atualizado com sucesso' + else + render :edit, status: :unprocessable_entity + end + end + + ## + # Remove um usuário do sistema. + # + # ==== Argumentos + # * Nenhum (usa @user definido pelo before_action) + # + # ==== Retorno + # * Nenhum (redireciona) + # + # ==== Efeitos Colaterais + # * Deleta registro de User do banco de dados (DELETE) + # * Remove registros relacionados devido a dependent: :destroy nas associações + # * Redireciona para admin_users_path com mensagem de sucesso + # + # ==== Exemplo + # DELETE /admin/users/1 # => deleta usuário e redireciona + # + def destroy + @user.destroy + redirect_to admin_users_path, notice: 'Usuário deletado com sucesso' + end + + private + + ## + # Busca e define o usuário a partir do ID nos parâmetros da URL. + # + # ==== Argumentos + # * Nenhum (usa params[:id]) + # + # ==== Retorno + # * Nenhum (define @user) + # + # ==== Efeitos Colaterais + # * Executa query no banco (User.find) + # * Define variável de instância @user + # * Levanta ActiveRecord::RecordNotFound se ID não existir + # + def set_user + @user = User.find(params[:id]) + end + + ## + # Define os parâmetros permitidos para atualização de usuário. + # + # ==== Argumentos + # * Nenhum (usa params do request) + # + # ==== Retorno + # * +ActionController::Parameters+ - parâmetros filtrados e permitidos + # + # ==== Efeitos Colaterais + # * Nenhum (apenas filtragem de parâmetros) + # + def user_params + params.require(:user).permit(:name, :email, :role) + end + + ## + # Verifica se o usuário atual possui perfil de administrador. + # Redireciona para página inicial se não for admin. + # + # ==== Argumentos + # * Nenhum (usa current_user do Devise) + # + # ==== Retorno + # * Nenhum (void ou redireciona) + # + # ==== Efeitos Colaterais + # * Redireciona para root_path com mensagem de alerta se não for admin + # * Interrompe a execução da action se redirecionar + # + def check_admin + redirect_to root_path, alert: 'Acesso negado!' unless current_user.admin? + end + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb new file mode 100644 index 0000000000..a6de97ff58 --- /dev/null +++ b/app/controllers/application_controller.rb @@ -0,0 +1,10 @@ +# app/controllers/application_controller.rb + +## +# Controller base da aplicação, do qual todos os outros controllers herdam. +# Define configurações e comportamentos compartilhados por toda a aplicação. +# +class ApplicationController < ActionController::Base + # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has. + allow_browser versions: :modern +end diff --git a/app/controllers/concerns/.keep b/app/controllers/concerns/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb new file mode 100644 index 0000000000..0f1d65459e --- /dev/null +++ b/app/controllers/home_controller.rb @@ -0,0 +1,41 @@ +# app/controllers/home_controller.rb + +## +# Controller responsável pela página inicial do sistema. +# Redireciona administradores para o dashboard e exibe formulários para alunos. +# +class HomeController < ApplicationController + before_action :authenticate_user! + + ## + # Exibe a página inicial conforme o perfil do usuário. + # Administradores são redirecionados para o dashboard admin. + # Alunos visualizam seus formulários pendentes e completados. + # + # ==== Argumentos + # * Nenhum (usa current_user do Devise) + # + # ==== Retorno + # * Nenhum (renderiza view ou redireciona) + # + # ==== Efeitos Colaterais + # * Redireciona para admin_root_path se usuário for admin + # * Carrega @pending_forms e @completed_forms via queries no banco + # * Renderiza view home/index.html.erb para usuários dicentes + # + # ==== Exemplo + # # Admin visitando a home + # GET / # => redirect_to admin_root_path + # + # # Aluno visitando a home + # GET / # => renderiza home/index com formulários + # + def index + if current_user.admin? + redirect_to admin_root_path + else + @pending_forms = current_user.pending_forms.order(due_date: :asc) + @completed_forms = current_user.completed_forms.order(created_at: :desc) + end + end +end diff --git a/app/controllers/student/forms_controller.rb b/app/controllers/student/forms_controller.rb new file mode 100644 index 0000000000..706fe34ff6 --- /dev/null +++ b/app/controllers/student/forms_controller.rb @@ -0,0 +1,205 @@ +# app/controllers/student/forms_controller.rb - CORRIGIDO + +module Student + ## + # Controller para gerenciamento de formulários pelos alunos. + # Permite visualizar, responder e submeter formulários das suas turmas. + # + class FormsController < ApplicationController + before_action :authenticate_user! + before_action :check_student + before_action :set_form, only: [:show, :answer, :submit_answer] + before_action :check_form_accessible, only: [:show, :answer, :submit_answer] + + ## + # Lista formulários pendentes e completados do aluno atual. + # + # ==== Argumentos + # * Nenhum (usa current_user) + # + # ==== Retorno + # * Nenhum (renderiza view) + # + # ==== Efeitos Colaterais + # * Carrega @pending_forms ordenados por due_date ASC via query + # * Carrega @completed_forms ordenados por created_at DESC via query + # * Renderiza view student/forms/index.html.erb + # + # ==== Exemplo + # GET /student/forms # => lista formulários do aluno + # + def index + @pending_forms = current_user.pending_forms.order(due_date: :asc) + @completed_forms = current_user.completed_forms.order(created_at: :desc) + end + + ## + # Exibe detalhes de um formulário para o aluno. + # + # ==== Argumentos + # * Nenhum (usa @form definido pelo before_action) + # + # ==== Retorno + # * Nenhum (renderiza view) + # + # ==== Efeitos Colaterais + # * Busca ou cria FormResponse do aluno para este formulário + # * Define @form_response (existente ou novo objeto em memória) + # * Renderiza view student/forms/show.html.erb + # + # ==== Exemplo + # GET /student/forms/1 # => exibe formulário + # + def show + @form_response = @form.form_responses.find_by(user: current_user) + @form_response ||= FormResponse.new(form: @form, user: current_user) + end + + ## + # Prepara o formulário para ser respondido pelo aluno. + # Cria FormResponse e FormAnswers vazios se ainda não existirem. + # + # ==== Argumentos + # * Nenhum (usa @form e current_user) + # + # ==== Retorno + # * Nenhum (renderiza view) + # + # ==== Efeitos Colaterais + # * Cria FormResponse no banco se não existir (sem validação) + # * Cria FormAnswers vazios para todos os campos do template + # * Executa @form_response.reload para buscar dados atualizados + # * Renderiza view student/forms/answer.html.erb + # + # ==== Exemplo + # GET /student/forms/1/answer # => formulário pronto para responder + # + def answer + @form_response = @form.form_responses.find_by(user: current_user) + + if @form_response.nil? + @form_response = FormResponse.new(form: @form, user: current_user) + @form_response.save(validate: false) + @form_response.build_answers_for_fields + @form_response.save(validate: false) + end + + @form_response.reload + + # Se não há respostas, criar agora + if @form_response.form_answers.empty? + @form_response.build_answers_for_fields + @form_response.save(validate: false) + end + end + + ## + # Submete as respostas do aluno ao formulário. + # Atualiza FormAnswers e marca FormResponse como submetido. + # + # ==== Argumentos + # * Nenhum (usa @form e params do formulário) + # + # ==== Retorno + # * Nenhum (redireciona) + # + # ==== Efeitos Colaterais + # * Atualiza registros de FormAnswer no banco com as respostas do aluno + # * Marca FormResponse como submetido (atualiza submitted_at) + # * Redireciona para root_path com mensagem de sucesso se salvou + # * Redireciona para answer com erro se falhar + # + # ==== Exemplo + # POST /student/forms/1/submit_answer + # # com respostas válidas => redirect e "Formulário respondido com sucesso!" + # + def submit_answer + @form_response = @form.form_responses.find_by(user: current_user) + + if @form_response.nil? + redirect_to student_form_path(@form), alert: 'Resposta não encontrada' + return + end + + if update_answers && @form_response.submit! + redirect_to root_path, notice: 'Formulário respondido com sucesso!' + else + redirect_to answer_student_form_path(@form), alert: 'Erro ao salvar respostas' + end + end + + private + + ## + # Busca e define o formulário a partir do ID nos parâmetros da URL. + # + # ==== Argumentos + # * Nenhum (usa params[:id]) + # + # ==== Retorno + # * Nenhum (define @form) + # + # ==== Efeitos Colaterais + # * Executa query no banco (Form.find) + # * Levanta ActiveRecord::RecordNotFound se ID não existir + # + def set_form + @form = Form.find(params[:id]) + end + + ## + # Verifica se o usuário é aluno (não administrador). + # Redireciona administradores para o dashboard. + # + # ==== Argumentos + # * Nenhum (usa current_user) + # + # ==== Retorno + # * Nenhum (void ou redireciona) + # + # ==== Efeitos Colaterais + # * Redireciona para admin_root_path se usuário for admin + # + def check_student + redirect_to admin_root_path if current_user.admin? + end + + ## + # Verifica se o formulário está acessível para o aluno atual. + # Checa se aluno pertence à turma e se formulário está publicado. + # + # ==== Argumentos + # * Nenhum (usa current_user e @form) + # + # ==== Retorno + # * Nenhum (void ou redireciona) + # + # ==== Efeitos Colaterais + # * Executa query para verificar se aluno está na turma do formulário + # * Redireciona para root_path com alerta se acesso negado + # + def check_form_accessible + unless current_user.klasses.include?(@form.klass) && @form.published? + redirect_to root_path, alert: 'Acesso negado a este formulário' + end + end + + ## + # Atualiza as respostas dos campos do formulário com os dados do aluno. + # + # ==== Argumentos + # * Nenhum (usa params e @form_response) + # + # ==== Retorno + # * +Boolean+ - true se atualizou com sucesso, false caso contrário + # + # ==== Efeitos Colaterais + # * Atualiza registros de FormAnswer no banco via nested_attributes + # * Executa validações dos FormAnswers + # + def update_answers + form_answers_params = params.require(:form_response).permit(form_answers_attributes: [:id, :answer]) + @form_response.update(form_answers_params) + end + end +end diff --git a/app/helpers/admin/dashboard_helper.rb b/app/helpers/admin/dashboard_helper.rb new file mode 100644 index 0000000000..435969172b --- /dev/null +++ b/app/helpers/admin/dashboard_helper.rb @@ -0,0 +1,2 @@ +module Admin::DashboardHelper +end diff --git a/app/helpers/admin/form_templates_helper.rb b/app/helpers/admin/form_templates_helper.rb new file mode 100644 index 0000000000..30c146275c --- /dev/null +++ b/app/helpers/admin/form_templates_helper.rb @@ -0,0 +1,15 @@ +# app/helpers/admin/form_templates_helper.rb + +module Admin::FormTemplatesHelper + def link_to_add_fields(name, form, association, **options) + new_object = form.object.send(association).klass.new + id = new_object.object_id + fields = form.fields_for(association, new_object, child_index: id) do |builder| + render('form_template_field_form', f: builder) + end + + link_to name, '#', + onclick: "add_fields(this, '#{association}', '#{j(fields)}'); return false;", + class: options[:class] + end +end diff --git a/app/helpers/admin/users_helper.rb b/app/helpers/admin/users_helper.rb new file mode 100644 index 0000000000..047899e045 --- /dev/null +++ b/app/helpers/admin/users_helper.rb @@ -0,0 +1,2 @@ +module Admin::UsersHelper +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb new file mode 100644 index 0000000000..bf7774a466 --- /dev/null +++ b/app/helpers/application_helper.rb @@ -0,0 +1,2 @@ +module ApplicationHelper +end diff --git a/app/helpers/home_helper.rb b/app/helpers/home_helper.rb new file mode 100644 index 0000000000..cfc942e675 --- /dev/null +++ b/app/helpers/home_helper.rb @@ -0,0 +1,2 @@ +module HomeHelper +end diff --git a/app/javascript/application.js b/app/javascript/application.js new file mode 100644 index 0000000000..0c7e9d5b90 --- /dev/null +++ b/app/javascript/application.js @@ -0,0 +1,12 @@ +// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails +import "@hotwired/turbo-rails" +import "controllers" +// import "bootstrap" + +window.add_fields = function(link, association, content) { + var new_id = new Date().getTime(); + var regexp = new RegExp("new_" + association, "g"); + + // Insere o novo campo ANTES do botão clicado + link.insertAdjacentHTML('beforebegin', content.replace(regexp, new_id)); +} \ No newline at end of file diff --git a/app/javascript/controllers/application.js b/app/javascript/controllers/application.js new file mode 100644 index 0000000000..bffc2b182d --- /dev/null +++ b/app/javascript/controllers/application.js @@ -0,0 +1,9 @@ +import { Application } from "@hotwired/stimulus" + +const application = Application.start() + +// Configure Stimulus development experience +application.debug = false +window.Stimulus = application + +export { application } diff --git a/app/javascript/controllers/hello_controller.js b/app/javascript/controllers/hello_controller.js new file mode 100644 index 0000000000..345818ba5e --- /dev/null +++ b/app/javascript/controllers/hello_controller.js @@ -0,0 +1,7 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + connect() { + this.element.textContent = "Hello World!" + } +} diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js new file mode 100644 index 0000000000..db9997a592 --- /dev/null +++ b/app/javascript/controllers/index.js @@ -0,0 +1,4 @@ +// Import and register all your controllers from the importmap via controllers/**/*_controller +import { application } from "controllers/application" +import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading" +eagerLoadControllersFrom("controllers", application) diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb new file mode 100644 index 0000000000..8eb2b33e79 --- /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..6a922dff26 --- /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..1eb6ae6015 --- /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/class_member.rb b/app/models/class_member.rb new file mode 100644 index 0000000000..f91ce6415d --- /dev/null +++ b/app/models/class_member.rb @@ -0,0 +1,15 @@ +# app/models/class_member.rb + +## +# Representa a matrícula de um usuário em uma turma. +# Estabelece o relacionamento entre User e Klass, definindo o papel (dicente ou docente). +# +class ClassMember < ApplicationRecord + # Associações + belongs_to :user + belongs_to :klass + + # Validações + validates :user_id, uniqueness: { scope: :klass_id, message: 'já está inscrito nesta turma' } + validates :role, presence: true, inclusion: { in: %w(dicente docente), message: 'deve ser dicente ou docente' } +end diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/models/form.rb b/app/models/form.rb new file mode 100644 index 0000000000..ad9cf10e3a --- /dev/null +++ b/app/models/form.rb @@ -0,0 +1,84 @@ +# app/models/form.rb + +## +# Representa um formulário publicado para uma turma específica. +# Baseado em um FormTemplate, possui status (rascunho, publicado, fechado) +# e armazena as respostas dos alunos através de FormResponse. +# +class Form < ApplicationRecord + belongs_to :form_template + belongs_to :klass + has_many :form_responses, dependent: :destroy + + validates :form_template_id, presence: true + validates :klass_id, presence: true + validates :title, presence: true + + enum :status, { draft: 0, published: 1, closed: 2 } + + # Definir status padrão + before_create :set_default_status + + ## + # Retorna todos os alunos da turma que ainda não responderam o formulário. + # + # ==== Argumentos + # * Nenhum + # + # ==== Retorno + # * +Array+ - lista de alunos que ainda não submeteram respostas + # + # ==== Efeitos Colaterais + # * Executa queries no banco de dados (klass.students e form_responses) + # * Nenhuma alteração de dados + # + # ==== Exemplo + # form = Form.find(1) + # form.pending_responses # => [#] + # + def pending_responses + klass.students - form_responses.map(&:user) + end + + ## + # Retorna todos os alunos que já responderam o formulário. + # + # ==== Argumentos + # * Nenhum + # + # ==== Retorno + # * +Array+ - lista de alunos que já submeteram respostas + # + # ==== Efeitos Colaterais + # * Executa query no banco de dados através de form_responses + # * Nenhuma alteração de dados + # + # ==== Exemplo + # form = Form.find(1) + # form.completed_responses # => [#] + # + def completed_responses + form_responses.map(&:user) + end + + private + + ## + # Define o status padrão do formulário como 'draft' (rascunho) + # quando o formulário é criado pela primeira vez. + # Executado automaticamente pelo callback before_create. + # + # ==== Argumentos + # * Nenhum + # + # ==== Retorno + # * +Symbol+ - :draft (status padrão) + # + # ==== Efeitos Colaterais + # * Modifica o atributo status do objeto para :draft se ainda não estiver definido + # * Executado automaticamente antes de criar o registro no banco + # + def set_default_status + self.status ||= :draft + end +end diff --git a/app/models/form_answer.rb b/app/models/form_answer.rb new file mode 100644 index 0000000000..4068d0620b --- /dev/null +++ b/app/models/form_answer.rb @@ -0,0 +1,14 @@ +# app/models/form_answer.rb + +## +# Representa a resposta individual de um campo específico dentro de um formulário. +# Armazena o valor preenchido pelo usuário para cada FormTemplateField. +# +class FormAnswer < ApplicationRecord + belongs_to :form_response + belongs_to :form_template_field + + validates :form_response_id, presence: true + validates :form_template_field_id, presence: true + validates :answer, presence: true +end diff --git a/app/models/form_response.rb b/app/models/form_response.rb new file mode 100644 index 0000000000..f5e16ae2dd --- /dev/null +++ b/app/models/form_response.rb @@ -0,0 +1,107 @@ +# app/models/form_response.rb + +## +# Representa a resposta de um usuário a um formulário específico. +# Armazena o estado de submissão e gerencia as respostas individuais de cada campo (form_answers). +# +class FormResponse < ApplicationRecord + belongs_to :form + belongs_to :user + has_many :form_answers, dependent: :destroy + + validates :form_id, presence: true + validates :user_id, presence: true + validates :user_id, uniqueness: { scope: :form_id, message: 'já respondeu este formulário' } + + accepts_nested_attributes_for :form_answers + + ## + # Verifica se a resposta foi submetida (finalizada). + # + # ==== Argumentos + # * Nenhum + # + # ==== Retorno + # * +Boolean+ - true se submitted_at está preenchido, false caso contrário + # + # ==== Efeitos Colaterais + # * Nenhum (método de leitura) + # + # ==== Exemplo + # response = FormResponse.find(1) + # response.completed? # => true + # + def completed? + submitted_at.present? + end + + ## + # Verifica se a resposta ainda está pendente (não submetida). + # + # ==== Argumentos + # * Nenhum + # + # ==== Retorno + # * +Boolean+ - true se ainda não foi submetida, false caso contrário + # + # ==== Efeitos Colaterais + # * Nenhum (método de leitura) + # + # ==== Exemplo + # response = FormResponse.find(1) + # response.pending? # => false + # + def pending? + !completed? + end + + ## + # Cria objetos FormAnswer vazios para todos os campos do template associado. + # Utilizado para preparar o formulário antes do usuário preencher. + # + # ==== Argumentos + # * Nenhum + # + # ==== Retorno + # * +Array+ - lista de form_answers criados (ainda não salvos no banco) + # + # ==== Efeitos Colaterais + # * Cria objetos FormAnswer em memória (não persiste no banco até save) + # * Executa query para buscar form_template_fields ordenados por position + # * Não cria duplicatas (verifica se já existe form_answer para o campo) + # + # ==== Exemplo + # response = FormResponse.new(form: form, user: user) + # response.build_answers_for_fields + # response.form_answers.count # => 5 (número de campos do template) + # + def build_answers_for_fields + form.form_template.form_template_fields.order(:position).each do |field| + unless form_answers.exists?(form_template_field_id: field.id) + form_answers.build(form_template_field: field, answer: '') + end + end + end + + ## + # Marca a resposta como submetida, registrando o timestamp atual. + # + # ==== Argumentos + # * Nenhum + # + # ==== Retorno + # * +Boolean+ - true se salvou com sucesso, false caso contrário + # + # ==== Efeitos Colaterais + # * Atualiza o campo submitted_at no banco de dados com Time.current + # * Persiste a mudança imediatamente (executa UPDATE) + # + # ==== Exemplo + # response = FormResponse.find(1) + # response.submit! # => true + # response.completed? # => true + # + def submit! + update(submitted_at: Time.current) + end +end diff --git a/app/models/form_template.rb b/app/models/form_template.rb new file mode 100644 index 0000000000..8c9b5ba8ea --- /dev/null +++ b/app/models/form_template.rb @@ -0,0 +1,18 @@ +# app/models/form_template.rb + +## +# Representa um modelo/template reutilizável de formulário. +# Criado por administradores para servir de base na criação de múltiplos formulários. +# Contém campos personalizáveis que definem a estrutura do formulário. +# +class FormTemplate < ApplicationRecord + belongs_to :user + has_many :form_template_fields, dependent: :destroy + has_many :forms, dependent: :destroy + + validates :name, presence: true + validates :description, presence: true + validates :user_id, presence: true + + accepts_nested_attributes_for :form_template_fields, allow_destroy: true +end diff --git a/app/models/form_template_field.rb b/app/models/form_template_field.rb new file mode 100644 index 0000000000..e460a3393c --- /dev/null +++ b/app/models/form_template_field.rb @@ -0,0 +1,59 @@ +# app/models/form_template_field.rb + +## +# Representa um campo individual dentro de um template de formulário. +# Define o tipo, rótulo, posição e opções do campo (ex: text, select, checkbox). +# +class FormTemplateField < ApplicationRecord + belongs_to :form_template + has_many :form_answers, dependent: :destroy + + # Valores válidos para field_type + VALID_FIELD_TYPES = %w(text textarea email number date select radio checkbox).freeze + + validates :label, presence: true + validates :field_type, presence: true, inclusion: { in: VALID_FIELD_TYPES } + validates :position, presence: true, numericality: { only_integer: true, greater_than: 0 } + + # Validar options apenas para tipos que precisam + validates :options, presence: true, if: :requires_options? + + # Callback para garantir que position seja integer + before_validation :ensure_position_is_integer + + private + + ## + # Verifica se o tipo de campo requer opções (select, radio, checkbox). + # + # ==== Argumentos + # * Nenhum + # + # ==== Retorno + # * +Boolean+ - true se o campo requer opções, false caso contrário + # + # ==== Efeitos Colaterais + # * Nenhum (método auxiliar de validação) + # + def requires_options? + ['select', 'radio', 'checkbox'].include?(field_type) + end + + ## + # Garante que o atributo position seja convertido para inteiro. + # Executado automaticamente antes da validação (callback). + # + # ==== Argumentos + # * Nenhum + # + # ==== Retorno + # * +Integer+ - valor da posição convertido para inteiro + # + # ==== Efeitos Colaterais + # * Modifica o atributo position do objeto, convertendo-o para Integer + # * Executado automaticamente pelo callback before_validation + # + def ensure_position_is_integer + self.position = position.to_i if position.present? && position.is_a?(String) + end +end diff --git a/app/models/klass.rb b/app/models/klass.rb new file mode 100644 index 0000000000..a4127dcbf5 --- /dev/null +++ b/app/models/klass.rb @@ -0,0 +1,62 @@ +# app/models/klass.rb + +## +# Representa uma turma/disciplina do sistema (ex: "Banco de Dados"). +# Gerencia alunos, professores e formulários associados à turma. +# +class Klass < ApplicationRecord + # Associações + has_many :class_members, dependent: :destroy + has_many :users, through: :class_members + has_many :forms, dependent: :destroy + + # Validações + validates :code, presence: true, uniqueness: true + validates :name, presence: true + validates :semester, presence: true + + # Scopes + scope :active, -> { order(semester: :desc) } + + ## + # Retorna todos os alunos (dicentes) matriculados na turma. + # + # ==== Argumentos + # * Nenhum + # + # ==== Retorno + # * +Array+ - lista de usuários com perfil de aluno (role: 'dicente') + # + # ==== Efeitos Colaterais + # * Executa queries no banco de dados através da associação class_members + # * Nenhuma alteração de dados + # + # ==== Exemplo + # klass = Klass.find_by(code: 'CC001') + # klass.students # => [#, #] + # + def students + class_members.where(role: 'dicente').map(&:user) + end + + ## + # Retorna todos os professores (docentes) associados à turma. + # + # ==== Argumentos + # * Nenhum + # + # ==== Retorno + # * +Array+ - lista de usuários com perfil de professor (role: 'docente') + # + # ==== Efeitos Colaterais + # * Executa queries no banco de dados através da associação class_members + # * Nenhuma alteração de dados + # + # ==== Exemplo + # klass = Klass.find_by(code: 'CC001') + # klass.teachers # => [#] + # + def teachers + class_members.where(role: 'docente').map(&:user) + end +end diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 0000000000..27a67722cf --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,110 @@ +# app/models/user.rb + +## +# Representa um usuário do sistema, podendo ser administrador ou aluno (dicente). +# Gerencia autenticação, permissões e relacionamentos com turmas e formulários. +# +class User < ApplicationRecord + devise :database_authenticatable, + :recoverable, :validatable + + enum :role, { user: 0, admin: 1 } + + # Associações + has_many :class_members, dependent: :destroy + has_many :klasses, through: :class_members + has_many :form_templates, dependent: :destroy + has_many :form_responses, dependent: :destroy + + # Validações + validates :name, presence: true + validates :email, presence: true, uniqueness: true + + ## + # Verifica se o usuário possui perfil de administrador. + # + # ==== Argumentos + # * Nenhum + # + # ==== Retorno + # * +Boolean+ - true se o usuário é admin, false caso contrário + # + # ==== Efeitos Colaterais + # * Nenhum (método de leitura) + # + # ==== Exemplo + # user = User.find(1) + # user.admin? # => true + # + def admin? + role == 'admin' + end + + ## + # Verifica se o usuário possui perfil de aluno/dicente. + # + # ==== Argumentos + # * Nenhum + # + # ==== Retorno + # * +Boolean+ - true se o usuário é dicente, false caso contrário + # + # ==== Efeitos Colaterais + # * Nenhum (método de leitura) + # + # ==== Exemplo + # user = User.find(2) + # user.user? # => true + # + def user? + role == 'user' + end + + ## + # Retorna todos os formulários publicados que o usuário ainda não respondeu. + # Considera apenas formulários das turmas em que o usuário está inscrito. + # + # ==== Argumentos + # * Nenhum + # + # ==== Retorno + # * +ActiveRecord::Relation+ - coleção de objetos Form pendentes de resposta + # + # ==== Efeitos Colaterais + # * Executa queries no banco de dados para buscar formulários + # * Nenhuma alteração de dados + # + # ==== Exemplo + # user = User.find(2) + # user.pending_forms # => [#
, #] + # + def pending_forms + Form + .where(klass_id: klasses.pluck(:id), status: :published) + .where.not( + id: form_responses.where.not(submitted_at: nil).pluck(:form_id) + ) + end + + ## + # Retorna todos os formulários que o usuário já respondeu (submetidos). + # + # ==== Argumentos + # * Nenhum + # + # ==== Retorno + # * +ActiveRecord::Relation+ - coleção de objetos Form já respondidos + # + # ==== Efeitos Colaterais + # * Executa queries no banco de dados para buscar formulários + # * Nenhuma alteração de dados + # + # ==== Exemplo + # user = User.find(2) + # user.completed_forms # => [#, #] + # + def completed_forms + Form + .where(id: form_responses.where.not(submitted_at: nil).pluck(:form_id)) + end +end diff --git a/app/services/import_service.rb b/app/services/import_service.rb new file mode 100644 index 0000000000..f91b1b684b --- /dev/null +++ b/app/services/import_service.rb @@ -0,0 +1,262 @@ +# app/services/import_service.rb + +## +# Serviço responsável por importar turmas e alunos a partir de um arquivo JSON do SIGAA. +# Processa o arquivo, cria/atualiza registros de turmas, usuários e matrículas. +# Mantém controle de sucessos e erros durante a importação. +# +class ImportService + attr_reader :file_path, :imported_count, :errors + + ## + # Inicializa o serviço de importação. + # + # ==== Argumentos + # * +file_path+ - (String) Caminho completo do arquivo JSON a ser importado + # + # ==== Retorno + # * +ImportService+ - instância do serviço inicializada + # + # ==== Efeitos Colaterais + # * Inicializa contadores @imported_count = 0 e @errors = [] + # + # ==== Exemplo + # service = ImportService.new('/tmp/uploads/classes.json') + # + def initialize(file_path) + @file_path = file_path + @imported_count = 0 + @errors = [] + end + + ## + # Executa a importação completa de turmas e alunos. + # Processa cada turma do arquivo JSON, criando registros no banco. + # + # ==== Argumentos + # * Nenhum + # + # ==== Retorno + # * +Hash+ - resultado da importação com as chaves: + # * +:success+ (Boolean) - true se processou sem exceções críticas + # * +:imported+ (Integer) - quantidade de turmas importadas com sucesso + # * +:errors+ (Array) - lista de erros não-críticos ocorridos + # * +:error+ (String) - mensagem de erro crítico (apenas se success: false) + # + # ==== Efeitos Colaterais + # * Lê arquivo JSON do sistema de arquivos + # * Cria/atualiza registros de Klass, User e ClassMember no banco de dados + # * Incrementa @imported_count para cada turma processada + # * Adiciona mensagens em @errors para erros não-críticos + # + # ==== Exemplo + # service = ImportService.new('/tmp/classes.json') + # result = service.import_klasses + # # => { success: true, imported: 5, errors: ["Erro ao importar estudante João: Email inválido"] } + # + def import_klasses + data = parse_json_file + + data.each do |klass_data| + import_single_klass(klass_data) + end + + { success: true, imported: @imported_count, errors: @errors } + rescue StandardError => e + { success: false, error: e.message, imported: @imported_count, errors: @errors } + end + + private + + ## + # Faz parse do arquivo JSON e retorna os dados em formato Ruby. + # + # ==== Argumentos + # * Nenhum + # + # ==== Retorno + # * +Array+ - array de hashes contendo dados das turmas + # + # ==== Efeitos Colaterais + # * Lê o arquivo do sistema de arquivos via File.read + # * Levanta exceção se o JSON for inválido ou arquivo não existir + # + # ==== Exceções + # * Levanta RuntimeError com mensagem "Erro ao ler arquivo JSON: ..." se parse falhar + # * Levanta RuntimeError com mensagem "Arquivo não encontrado: ..." se arquivo não existir + # + def parse_json_file + JSON.parse(File.read(@file_path)) + rescue JSON::ParserError => e + raise "Erro ao ler arquivo JSON: #{e.message}" + rescue Errno::ENOENT + raise "Arquivo não encontrado: #{@file_path}" + end + + ## + # Importa uma única turma com seus alunos. + # + # ==== Argumentos + # * +klass_data+ - (Hash) dados da turma no formato JSON do SIGAA + # + # ==== Retorno + # * Nenhum (void) + # + # ==== Efeitos Colaterais + # * Cria/atualiza registro de Klass no banco de dados + # * Importa todos os alunos da turma via import_students + # * Incrementa @imported_count em 1 + # * Adiciona erro em @errors se falhar (não levanta exceção) + # + def import_single_klass(klass_data) + klass = find_or_create_klass(klass_data) + import_students(klass, klass_data['dicente']) + @imported_count += 1 + rescue StandardError => e + @errors << "Erro ao importar turma #{klass_data['code']}: #{e.message}" + end + + ## + # Busca ou cria uma turma no banco de dados. + # + # ==== Argumentos + # * +klass_data+ - (Hash) dados da turma contendo 'code', 'name' e 'class' + # + # ==== Retorno + # * +Klass+ - objeto da turma (existente ou recém-criado) + # + # ==== Efeitos Colaterais + # * Executa query no banco (find_or_create_by) + # * Cria novo registro de Klass se não existir com aquele código + # * Atualiza campos name, semester e description se criando novo registro + # + def find_or_create_klass(klass_data) + klass_info = klass_data['class'] + + Klass.find_or_create_by(code: klass_data['code']) do |klass| + klass.name = klass_data['name'] + klass.semester = klass_info['semester'] + klass.description = "Turma #{klass_info['classCode']} - #{klass_info['time']}" + end + end + + ## + # Importa todos os alunos de uma turma. + # + # ==== Argumentos + # * +klass+ - (Klass) objeto da turma onde os alunos serão matriculados + # * +students_data+ - (Array) lista de dados dos alunos em formato JSON + # + # ==== Retorno + # * Nenhum (void) - retorna nil se students_data estiver vazio + # + # ==== Efeitos Colaterais + # * Chama import_single_student para cada aluno na lista + # * Nenhuma alteração direta no banco (delegado ao import_single_student) + # + def import_students(klass, students_data) + return unless students_data.present? + + students_data.each do |student_data| + import_single_student(klass, student_data, 'dicente') + end + end + + ## + # Importa um único aluno e o matricula na turma. + # + # ==== Argumentos + # * +klass+ - (Klass) turma onde o aluno será matriculado + # * +student_data+ - (Hash) dados do aluno (nome, email, matrícula, etc) + # * +role+ - (String) papel do usuário na turma (geralmente 'dicente') + # + # ==== Retorno + # * Nenhum (void) + # + # ==== Efeitos Colaterais + # * Cria/busca registro de User via find_or_create_user + # * Cria registro de ClassMember vinculando user e klass + # * Adiciona erro em @errors se falhar (não levanta exceção) + # + def import_single_student(klass, student_data, role) + user = find_or_create_user(student_data) + + ClassMember.find_or_create_by(user: user, klass: klass) do |cm| + cm.role = role + end + rescue StandardError => e + @errors << "Erro ao importar estudante #{student_data['nome']}: #{e.message}" + end + + ## + # Busca usuário existente por email ou cria um novo. + # + # ==== Argumentos + # * +user_data+ - (Hash) dados do usuário contendo email, nome, matrícula, etc + # + # ==== Retorno + # * +User+ - objeto do usuário (existente ou recém-criado) + # + # ==== Efeitos Colaterais + # * Executa query no banco (find_by) + # * Pode criar novo User via create_new_user se não existir + # + def find_or_create_user(user_data) + User.find_by(email: user_data['email']) || create_new_user(user_data) + end + + ## + # Cria e persiste um novo usuário no banco de dados. + # + # ==== Argumentos + # * +user_data+ - (Hash) dados completos do usuário + # + # ==== Retorno + # * +User+ - objeto do usuário recém-criado e salvo + # + # ==== Efeitos Colaterais + # * Cria novo registro de User no banco via save + # * Levanta exceção se validações falharem + # + # ==== Exceções + # * Levanta RuntimeError com mensagens de validação se user.save falhar + # + def create_new_user(user_data) + user = build_user(user_data) + + if user.save + user + else + raise "Erro ao salvar usuário #{user_data['email']}: #{user.errors.full_messages.join(', ')}" + end + end + + ## + # Constrói objeto User em memória com os dados fornecidos. + # Define a matrícula como senha padrão do usuário. + # + # ==== Argumentos + # * +user_data+ - (Hash) dados do usuário do JSON + # + # ==== Retorno + # * +User+ - objeto User não persistido (apenas em memória) + # + # ==== Efeitos Colaterais + # * Nenhum (apenas cria objeto em memória, não salva no banco) + # + def build_user(user_data) + password = user_data['matricula'] + + User.new( + email: user_data['email'], + name: user_data['nome'], + matricula: user_data['matricula'], + curso: user_data['curso'], + formacao: user_data['formacao'], + ocupacao: user_data['ocupacao'], + password: password, + password_confirmation: password, + role: :user + ) + end +end diff --git a/app/views/admin/dashboard/index.html.erb b/app/views/admin/dashboard/index.html.erb new file mode 100644 index 0000000000..5122306ceb --- /dev/null +++ b/app/views/admin/dashboard/index.html.erb @@ -0,0 +1,128 @@ + + +
+ + +
+
+

Painel Administrativo

+

Gerencie usuários, formulários e integrações do sistema

+
+ + <%= button_to 'Sair', destroy_user_session_path, + method: :delete, + class: 'btn btn-outline-danger btn-lg rounded-pill px-4' %> +
+ + + +
+ +
+
+
+
Total de Usuários
+

<%= @total_users %>

+
+
+
+ +
+
+
+
Total de Admins
+

<%= @admin_count %>

+
+
+
+ +
+ + + +
+ + +
+
+
+
+
📤 Importar Dados do SIGAA
+

Importe turmas e alunos a partir de um arquivo JSON

+
+ + <%= link_to 'Importar', + admin_imports_path, + class: 'btn btn-primary btn-lg rounded-pill px-4' %> +
+
+
+ + +
+
+
+
+
📋 Gerenciar Templates de Formulários
+

Crie e gerencie modelos reutilizáveis

+
+ + <%= link_to 'Acessar', + admin_form_templates_path, + class: 'btn btn-primary btn-lg rounded-pill px-4' %> +
+
+
+ + +
+
+
+
+
📋 Gerenciar Formulários
+

Crie e atribua formulários às turmas

+
+ + <%= link_to 'Acessar', + admin_forms_path, + class: 'btn btn-primary btn-lg rounded-pill px-4' %> +
+
+
+ +
+ + + +
+

Usuários Recentes

+ +
+ + + + + + + + + + + <% @users.each do |user| %> + + + + + + <% end %> + +
NomeEmailRole
<%= user.name %><%= user.email %> + + <%= user.role.capitalize %> + +
+
+ +
+ +
diff --git a/app/views/admin/form_templates/_form_template_field_form.html.erb b/app/views/admin/form_templates/_form_template_field_form.html.erb new file mode 100644 index 0000000000..2f4cc72cdd --- /dev/null +++ b/app/views/admin/form_templates/_form_template_field_form.html.erb @@ -0,0 +1,57 @@ +
+
+ +
+ + <%= f.number_field :position, class: "form-control", min: "1", placeholder: "1" %> +
+ + +
+ + <%= f.select :field_type, + ['text', 'textarea', 'email', 'number', 'date', 'select', 'radio', 'checkbox'], + { prompt: 'Selecione' }, + class: "form-select" %> +
+ + +
+
+ <%= f.check_box :required, { class: "form-check-input" } %> + +
+
+ + +
+ <% if f.object.persisted? %> + <%= f.hidden_field :_destroy %> + + <% else %> + + <% end %> +
+
+ + +
+ + <%= f.text_field :label, class: "form-control", placeholder: "Ex: Seu Nome" %> +
+ + +
+ + Só para select/radio/checkbox. Exemplo: ["Opção 1", "Opção 2"] + <%= f.text_area :options, class: "form-control", rows: 2, placeholder: '["Opção 1", "Opção 2"]' %> +
+
diff --git a/app/views/admin/form_templates/edit.html.erb b/app/views/admin/form_templates/edit.html.erb new file mode 100644 index 0000000000..28f2e2d961 --- /dev/null +++ b/app/views/admin/form_templates/edit.html.erb @@ -0,0 +1,158 @@ + + +
+
+
+ + +
+
+

Editar Template

+

Gerencie os campos e informações deste template.

+
+ <%= link_to 'Cancelar', admin_form_templates_path, class: 'btn btn-outline-secondary' %> +
+ + +
+
+ + <%= form_with(model: [:admin, @form_template], local: true) do |form| %> + + + <% if @form_template.errors.any? %> +
+
+ <%= pluralize(@form_template.errors.count, "erro") %> encontrado(s): +
+
    + <% @form_template.errors.full_messages.each do |message| %> +
  • <%= message %>
  • + <% end %> +
+
+ <% end %> + + +

Informações do Template

+ +
+ <%= form.label :name, "Nome do Template", class: "form-label fw-semibold" %> + <%= form.text_field :name, class: "form-control form-control-lg", placeholder: "Ex: Avaliação Final" %> +
+ +
+ <%= form.label :description, "Descrição", class: "form-label fw-semibold" %> + <%= form.text_area :description, class: "form-control", rows: 3, placeholder: "Breve descrição do propósito deste template..." %> +
+ +
+ + +

Campos do Template

+ +
+ <%= form.fields_for :form_template_fields do |field_form| %> + <%= render 'form_template_field_form', f: field_form %> + <% end %> +
+ + +
+ +
+ + +
+ <%= form.submit "Salvar Alterações", + class: "btn btn-success btn-lg px-4 rounded-3" %> + <%= link_to 'Cancelar', + admin_form_templates_path, + class: "btn btn-secondary btn-lg px-4 rounded-3" %> +
+ + <% end %> + +
+
+ +
+
+
+ + diff --git a/app/views/admin/form_templates/index.html.erb b/app/views/admin/form_templates/index.html.erb new file mode 100644 index 0000000000..2d0e96dfb7 --- /dev/null +++ b/app/views/admin/form_templates/index.html.erb @@ -0,0 +1,108 @@ + + +
+ + +
+
+

Templates de Formulários

+

Organize, edite e gerencie seus templates personalizados.

+
+ +
+ <%= link_to 'Novo Template', + new_admin_form_template_path, + class: 'btn btn-primary btn-lg rounded-3' %> + + <%= link_to 'Voltar ao Dashboard', + admin_root_path, + class: 'btn btn-outline-secondary btn-lg rounded-3' %> +
+
+ + + + <% if @form_templates.any? %> +
+
+ +
+ + + + + + + + + + + + + <% @form_templates.each do |template| %> + + + + + + + + + + + + <% end %> + + +
NomeDescriçãoCamposCriado emAções
<%= template.name %><%= truncate(template.description, length: 60) %> + + <%= template.form_template_fields.count %> + + <%= l(template.created_at, format: :short) %> + +
+ + <%= link_to 'Ver', + admin_form_template_path(template), + class: 'btn btn-sm btn-outline-info rounded-3' %> + + <%= link_to 'Editar', + edit_admin_form_template_path(template), + class: 'btn btn-sm btn-outline-warning rounded-3' %> + + <%= button_to 'Deletar', + admin_form_template_path(template), + method: :delete, + onclick: "return confirm('Tem certeza que deseja apagar este template?');", + class: 'btn btn-sm btn-outline-danger rounded-3', + form: { style: 'display:inline-block;' } %> + +
+
+
+ +
+
+ + <% else %> + + +
+
Nenhum template criado ainda.
+

Você ainda não adicionou nenhum template de formulário.

+ <%= link_to 'Criar seu primeiro template', + new_admin_form_template_path, + class: 'btn btn-primary btn-lg rounded-3' %> +
+ + <% end %> + + + +
+ <%= link_to 'Voltar ao Dashboard', + admin_root_path, + class: 'btn btn-outline-secondary btn-lg rounded-3' %> +
+ +
diff --git a/app/views/admin/form_templates/new.html.erb b/app/views/admin/form_templates/new.html.erb new file mode 100644 index 0000000000..f9c8ea498c --- /dev/null +++ b/app/views/admin/form_templates/new.html.erb @@ -0,0 +1,185 @@ + + +
+
+ + + + + +
+ + <%= form_with(model: [:admin, @form_template], local: true, method: :post) do |form| %> + + <% if @form_template.errors.any? %> +
+
<%= pluralize(@form_template.errors.count, "erro") %> encontrado(s):
+
    + <% @form_template.errors.full_messages.each do |msg| %> +
  • <%= msg %>
  • + <% end %> +
+
+ <% end %> + + +
+ <%= form.label :name, "Nome do Template", class: "form-label fw-semibold" %> + <%= form.text_field :name, class: "form-control form-control-lg", placeholder: "Ex: Avaliação de Desempenho" %> +
+ + +
+ <%= form.label :description, "Descrição", class: "form-label fw-semibold" %> + <%= form.text_area :description, class: "form-control", rows: 4, placeholder: "Descreva o propósito deste template..." %> +
+ + +

Campos do Template

+ +
+ <%= form.fields_for :form_template_fields do |field_form| %> + <%= render 'form_template_field_form', f: field_form %> + <% end %> +
+ + + + +
+ <%= form.submit "Criar Template", class: "btn btn-primary btn-lg px-4" %> + <%= link_to "Cancelar", admin_form_templates_path, class: "btn btn-outline-secondary btn-lg px-4" %> +
+ + <% end %> + +
+
+
+ + + + + diff --git a/app/views/admin/form_templates/show.html.erb b/app/views/admin/form_templates/show.html.erb new file mode 100644 index 0000000000..925b7129fa --- /dev/null +++ b/app/views/admin/form_templates/show.html.erb @@ -0,0 +1,129 @@ + + +
+
+ + +
+
+

<%= @form_template.name %>

+

<%= @form_template.description %>

+
+ +
+ <%= link_to 'Editar Template', + edit_admin_form_template_path(@form_template), + class: 'btn btn-outline-primary px-4 py-2' %> + + <%= link_to 'Voltar', + admin_form_templates_path, + class: 'btn btn-secondary px-4 py-2' %> +
+
+ + +
+
+

+ Perguntas configuradas neste template +

+
+ +
+
+ + + + + + + + + + + + + <% @form_template.form_template_fields.order(:position).each do |field| %> + + + + + + + + + + + + <% end %> + + +
PosiçãoPerguntaTipoObrigatório?Opções
+ + <%= field.position %> + + <%= field.label %> + + <%= field.field_type %> + + + <% if field.required %> + + Sim + + <% else %> + + Opcional + + <% end %> + + <% if ['select', 'radio', 'checkbox'].include?(field.field_type) && field.options.present? %> + <%= field.options %> + <% else %> + + <% end %> +
+
+
+
+ + <% if @form_template.form_template_fields.empty? %> +
+ Este template ainda não possui nenhuma pergunta cadastrada. + <%= link_to "Adicionar perguntas", + edit_admin_form_template_path(@form_template), + class: "alert-link fw-semibold" %> +
+ <% end %> + +
+
+ + diff --git a/app/views/admin/forms/edit.html.erb b/app/views/admin/forms/edit.html.erb new file mode 100644 index 0000000000..66e67a5cd5 --- /dev/null +++ b/app/views/admin/forms/edit.html.erb @@ -0,0 +1,116 @@ + + +
+
+ + +
+
+

Editar Formulário

+

Atualize as informações deste formulário.

+
+ + <%= link_to 'Voltar', admin_forms_path, class: 'btn btn-secondary px-4 py-2' %> +
+ + +
+
+ + <%= form_with(model: [:admin, @form], local: true) do |form| %> + + <% if @form.errors.any? %> +
+
<%= pluralize(@form.errors.count, "erro") %> encontrado:
+
    + <% @form.errors.full_messages.each do |message| %> +
  • <%= message %>
  • + <% end %> +
+
+ <% end %> + + +
+ <%= form.label :form_template_id, "Template", class: "form-label fw-semibold" %> + <%= form.collection_select :form_template_id, @form_templates, :id, :name, + {}, class: 'form-select form-control-lg' %> +
+ + +
+ <%= form.label :klass_id, "Turma", class: "form-label fw-semibold" %> + <%= form.collection_select :klass_id, @klasses, :id, :name, + {}, class: 'form-select form-control-lg' %> +
+ + +
+ <%= form.label :title, "Título do Formulário", class: "form-label fw-semibold" %> + <%= form.text_field :title, class: 'form-control form-control-lg', placeholder: "Ex: Avaliação Parcial" %> +
+ + +
+ <%= form.label :description, "Descrição", class: "form-label fw-semibold" %> + <%= form.text_area :description, rows: 4, class: 'form-control form-control-lg', + placeholder: "Descreva em detalhes o propósito deste formulário..." %> +
+ + +
+ <%= form.label :due_date, "Prazo (opcional)", class: "form-label fw-semibold" %> +
+ <%= form.datetime_select :due_date, + { default: @form.due_date || Time.current }, + class: 'form-control' %> +
+
+ + +
+ <%= form.submit "Atualizar Formulário", + class: 'btn btn-success px-4 py-2 fw-semibold' %> + + <%= link_to 'Cancelar', admin_forms_path, + class: 'btn btn-outline-secondary px-4 py-2' %> +
+ + <% end %> + +
+
+ +
+
+ + diff --git a/app/views/admin/forms/index.html.erb b/app/views/admin/forms/index.html.erb new file mode 100644 index 0000000000..d79a4d9adc --- /dev/null +++ b/app/views/admin/forms/index.html.erb @@ -0,0 +1,165 @@ + + +
+
+ + +
+
+

Formulários

+

Gerencie todos os formulários criados no sistema.

+
+
+ <%= link_to 'Novo Formulário', new_admin_form_path, + class: 'btn btn-primary px-4 py-2 fw-semibold' %> + + <%= link_to 'Dashboard', admin_root_path, + class: 'btn btn-outline-secondary px-4 py-2' %> +
+
+ + + <% if @forms.any? %> + +
+
+
+ + + + + + + + + + + + + + + <% @forms.each do |form| %> + + + + + + + + + + + + + + + + + + + + + + + + + <% end %> + + +
TítuloTemplateTurmaStatusRespostasPrazoAções
+ <%= form.title %> + <%= form.form_template.name %><%= form.klass.name %> + <% status_label = + case form.status + when 'draft' then ['Rascunho', 'secondary'] + when 'published' then ['Publicado', 'success'] + when 'closed' then ['Fechado', 'danger'] + end + %> + + + <%= status_label[0] %> + + + + <%= form.completed_responses.count %> + + / + + <%= form.klass.students.count %> + + + <%= form.due_date.present? ? l(form.due_date, format: :short) : '-' %> + + + <%= link_to 'Ver', admin_form_path(form), + class: 'btn btn-sm btn-outline-primary me-1' %> + + <% if form.draft? %> + <%= link_to 'Editar', edit_admin_form_path(form), + class: 'btn btn-sm btn-warning me-1' %> + + <%= button_to 'Publicar', + publish_admin_form_path(form), + method: :patch, + class: 'btn btn-sm btn-success', + form_class: 'd-inline-block' %> + <% end %> + + <% if form.published? %> + <%= button_to 'Fechar', + close_admin_form_path(form), + method: :patch, + class: 'btn btn-sm btn-danger', + form_class: 'd-inline-block' %> + <% end %> + +
+
+
+
+ + <% else %> + +
+ Nenhum formulário criado ainda. + <%= link_to 'Criar seu primeiro formulário', + new_admin_form_path, + class: 'alert-link' %> +
+ + <% end %> + +
+
+ + diff --git a/app/views/admin/forms/new.html.erb b/app/views/admin/forms/new.html.erb new file mode 100644 index 0000000000..eaeea5d73e --- /dev/null +++ b/app/views/admin/forms/new.html.erb @@ -0,0 +1,80 @@ + + +
+
+
+ + +
+

Novo Formulário

+ <%= link_to 'Voltar', admin_forms_path, class: 'btn btn-secondary' %> +
+ + + <%= form_with(model: [:admin, @form], local: true) do |form| %> + + + <% if @form.errors.any? %> +
+

<%= pluralize(@form.errors.count, "erro") %> encontrado:

+
    + <% @form.errors.full_messages.each do |message| %> +
  • <%= message %>
  • + <% end %> +
+
+ <% end %> + + +
+ <%= form.label :form_template_id, "Template" %> + <%= form.collection_select :form_template_id, @form_templates, :id, :name, + { prompt: 'Selecione um template' }, class: 'form-select' %> + + Escolha o modelo base para este formulário. + +
+ + +
+ <%= form.label :klass_id, "Turma" %> + <%= form.collection_select :klass_id, @klasses, :id, :name, + { prompt: 'Selecione uma turma' }, class: 'form-select' %> + + A turma selecionada será responsável por responder o formulário. + +
+ + +
+ <%= form.label :title, "Título do Formulário" %> + <%= form.text_field :title, class: 'form-control', + placeholder: 'Ex: Avaliação de Desempenho' %> +
+ + +
+ <%= form.label :description, "Descrição" %> + <%= form.text_area :description, class: 'form-control', rows: 4, + placeholder: 'Descreva o propósito deste formulário...' %> +
+ + +
+ <%= form.label :due_date, "Prazo (opcional)" %> + <%= form.datetime_select :due_date, class: 'form-control' %> + + Deixe em branco caso o formulário não tenha prazo definido. + +
+ + +
+ <%= form.submit "Criar Formulário", class: 'btn btn-success' %> + <%= link_to 'Cancelar', admin_forms_path, class: 'btn btn-secondary' %> +
+ + <% end %> +
+
+
diff --git a/app/views/admin/forms/show.html.erb b/app/views/admin/forms/show.html.erb new file mode 100644 index 0000000000..713b56c998 --- /dev/null +++ b/app/views/admin/forms/show.html.erb @@ -0,0 +1,126 @@ + + +
+ + +
+
+

<%= @form.title %>

+ + +
+ + Turma: <%= @form.klass.name %> + + + + Template: <%= @form.form_template.name %> + + + + Status: + <% case @form.status %> + <% when 'draft' %> + Rascunho + <% when 'published' %> + Publicado + <% when 'closed' %> + Fechado + <% end %> + +
+ + +
+ + Prazo: + + <%= @form.due_date.present? ? l(@form.due_date, format: :long) : 'Sem prazo definido' %> + + +
+
+ + +
+ <%= link_to 'Voltar', admin_forms_path, class: 'btn btn-outline-secondary' %> + + <% if @form.draft? %> + <%= button_to 'Publicar', publish_admin_form_path(@form), + method: :patch, + class: 'btn btn-success' %> + + <%= link_to 'Editar', edit_admin_form_path(@form), + class: 'btn btn-warning' %> + <% elsif @form.published? %> + <%= button_to 'Fechar', close_admin_form_path(@form), + method: :patch, + class: 'btn btn-danger' %> + <% end %> +
+
+ + +
+ + +
+

Respostas

+ + <% if @form.form_responses.any? %> +
+ + + + + + + + + + + + <% @form.form_responses.each do |response| %> + + + + + + + + + + <% end %> + +
AlunoStatusRespondido emAções
<%= response.user.name %> + <% if response.completed? %> + Respondido + <% else %> + Pendente + <% end %> + + <%= response.submitted_at.present? ? + l(response.submitted_at, format: :short) : + '-' %> + + <% if response.completed? %> + <%= link_to 'Ver Respostas', + view_response_admin_form_path(@form, response_id: response.id), + class: 'btn btn-sm btn-info' %> + <% else %> + + <% end %> +
+
+ <% else %> +
+ Nenhum aluno respondeu ainda. +
+ <% end %> +
+ + +
+ <%= link_to 'Voltar', admin_forms_path, class: 'btn btn-secondary' %> +
+
diff --git a/app/views/admin/forms/view_response.html.erb b/app/views/admin/forms/view_response.html.erb new file mode 100644 index 0000000000..41803eec18 --- /dev/null +++ b/app/views/admin/forms/view_response.html.erb @@ -0,0 +1,106 @@ + + +
+ + +
+
+

Respostas de <%= @form_response.user.name %>

+ + +

+ Formulário: + <%= @form.title %> +

+
+ + <%= link_to 'Voltar ao Formulário', + admin_form_path(@form), + class: 'btn btn-outline-secondary' %> +
+ + +
+ + +
+
+
+
Aluno
+ +

+ <%= @form_response.user.name %> +

+ +

+ <%= @form_response.user.email %> +

+
+
+
+ + +
+
+
+
Data de Resposta
+ +

+ <%= l(@form_response.submitted_at, format: :long) %> +

+
+
+
+ +
+ + +
+ + +

Respostas

+ +
+ + + + + + + + + + <% @form_response.form_answers.each do |answer| %> + <% field = answer.form_template_field %> + + + + + + + + + + <% end %> + +
PerguntaResposta
+ <%= field.label %> + <% if field.required %> + * + <% end %> + + <% if answer.answer.present? %> + <%= simple_format(answer.answer) %> + <% else %> + Sem resposta + <% end %> +
+
+ + +
+ <%= link_to 'Voltar ao Formulário', + admin_form_path(@form), + class: 'btn btn-secondary' %> +
+
diff --git a/app/views/admin/imports/index.html.erb b/app/views/admin/imports/index.html.erb new file mode 100644 index 0000000000..1fc1f69cdf --- /dev/null +++ b/app/views/admin/imports/index.html.erb @@ -0,0 +1,173 @@ + + +
+ + +
+
+

Importar Dados do SIGAA

+

+ Gerencie a importação de turmas e alunos a partir do JSON do SIGAA +

+
+ + <%= link_to 'Sair', + destroy_user_session_path, + method: :delete, + class: 'btn btn-outline-danger' + %> +
+ + +
+ +
+
+
+
Total de Turmas
+ + <%= @total_klasses %> + +
+
+
+ +
+
+
+
Total de Alunos
+ + <%= @total_users %> + +
+
+
+ +
+ + +
+
+
📤 Importar Arquivo JSON
+
+ +
+ + <%= form_with url: import_klasses_admin_imports_path, + method: :post, + local: true, + multipart: true do |f| %> + + +
+ + + <%= f.file_field :file, + class: 'form-control', + accept: '.json', + required: true %> + + + O arquivo deve seguir a estrutura oficial do SIGAA. + +
+ + +
+ ℹ️ Informações Importantes + +
    +
  • As senhas dos alunos serão suas matrículas.
  • +
  • Alunos já existentes não serão duplicados.
  • +
  • Turmas existentes serão atualizadas.
  • +
  • Alunos repetidos na mesma turma serão ignorados.
  • +
+
+ + +
+ <%= f.submit "🚀 Importar Turmas e Alunos", + class: 'btn btn-primary btn-lg' %> +
+ + <% end %> + +
+
+ + +
+
+
📋 Instruções de Uso
+
+ +
+ +
Estrutura esperada do JSON:
+ +
[
+  {
+    "code": "CIC0097",
+    "name": "BANCOS DE DADOS",
+    "class": {
+      "classCode": "TA",
+      "semester": "2021.2",
+      "time": "35T45"
+    },
+    "dicente": [
+      {
+        "nome": "Ana Clara Jordao Perna",
+        "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC",
+        "matricula": "190084006",
+        "usuario": "190084006",
+        "formacao": "graduando",
+        "ocupacao": "dicente",
+        "email": "acjpjvjp@gmail.com"
+      }
+    ]
+  }
+]
+ +
Após a importação:
+
    +
  • ✅ Turmas serão criadas ou atualizadas.
  • +
  • ✅ Alunos receberão senha inicial igual à matrícula.
  • +
  • ✅ Alunos serão vinculados às suas turmas.
  • +
  • ℹ️ Erros ou inconsistências serão informados.
  • +
+ +
Acesso dos alunos:
+
    +
  • Email: conforme especificado no JSON.
  • +
  • Senha: matrícula (ex: 190084006).
  • +
  • ⚠️ Recomenda-se que o aluno altere a senha no primeiro acesso.
  • +
+ +
+
+ + +
+ <%= link_to '← Voltar ao Dashboard', + admin_root_path, + class: 'btn btn-secondary' %> +
+
+ + + diff --git a/app/views/admin/users/edit.html.erb b/app/views/admin/users/edit.html.erb new file mode 100644 index 0000000000..5da23c1578 --- /dev/null +++ b/app/views/admin/users/edit.html.erb @@ -0,0 +1,47 @@ + +
+
+
+

Editar Usuário

+ + <% if @user.errors.any? %> +
+

<%= pluralize(@user.errors.count, "erro") %> encontrado:

+
    + <% @user.errors.full_messages.each do |message| %> +
  • <%= message %>
  • + <% end %> +
+
+ <% end %> + + + + + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + Cancelar +
+ +
+
+
diff --git a/app/views/admin/users/index.html.erb b/app/views/admin/users/index.html.erb new file mode 100644 index 0000000000..bfc83994af --- /dev/null +++ b/app/views/admin/users/index.html.erb @@ -0,0 +1,39 @@ + +
+
+
+

Usuários

+
+
+ + <% if @users.any? %> +
+ + + + + + + + + + + <% @users.each do |user| %> + + + + + + + <% end %> + +
NomeEmailRoleAções
<%= user.name %><%= user.email %><%= user.role %> + <%= link_to 'Ver', admin_user_path(user), class: 'btn btn-sm btn-info' %> + <%= link_to 'Editar', edit_admin_user_path(user), class: 'btn btn-sm btn-warning' %> + <%= link_to 'Deletar', admin_user_path(user), method: :delete, data: { confirm: 'Tem certeza?' }, class: 'btn btn-sm btn-danger' %> +
+
+ <% else %> +

Nenhum usuário encontrado.

+ <% end %> +
diff --git a/app/views/admin/users/show.html.erb b/app/views/admin/users/show.html.erb new file mode 100644 index 0000000000..d78bd3aeef --- /dev/null +++ b/app/views/admin/users/show.html.erb @@ -0,0 +1,20 @@ + +
+
+
+

<%= @user.name %>

+
+
+ <%= link_to 'Editar', edit_admin_user_path(@user), class: 'btn btn-warning' %> + <%= link_to 'Voltar', admin_users_path, class: 'btn btn-secondary' %> +
+
+ +
+
+

Email: <%= @user.email %>

+

Role: <%= @user.role %>

+

Criado em: <%= @user.created_at.strftime('%d/%m/%Y %H:%M') %>

+
+
+
diff --git a/app/views/devise/confirmations/new.html.erb b/app/views/devise/confirmations/new.html.erb new file mode 100644 index 0000000000..b9bed0ecf9 --- /dev/null +++ b/app/views/devise/confirmations/new.html.erb @@ -0,0 +1,16 @@ +

Resend confirmation instructions

+ +<%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + +
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email", value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email) %> +
+ +
+ <%= f.submit "Resend confirmation instructions" %> +
+<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/views/devise/mailer/confirmation_instructions.html.erb b/app/views/devise/mailer/confirmation_instructions.html.erb new file mode 100644 index 0000000000..491d767fcc --- /dev/null +++ b/app/views/devise/mailer/confirmation_instructions.html.erb @@ -0,0 +1,5 @@ +

Welcome <%= @email %>!

+ +

You can confirm your account email through the link below:

+ +

<%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %>

diff --git a/app/views/devise/mailer/email_changed.html.erb b/app/views/devise/mailer/email_changed.html.erb new file mode 100644 index 0000000000..d9a6e51fe1 --- /dev/null +++ b/app/views/devise/mailer/email_changed.html.erb @@ -0,0 +1,7 @@ +

Hello <%= @email %>!

+ +<% if @resource.try(:unconfirmed_email?) %> +

We're contacting you to notify you that your email is being changed to <%= @resource.unconfirmed_email %>.

+<% else %> +

We're contacting you to notify you that your email has been changed to <%= @resource.email %>.

+<% end %> diff --git a/app/views/devise/mailer/password_change.html.erb b/app/views/devise/mailer/password_change.html.erb new file mode 100644 index 0000000000..51e6aaffb5 --- /dev/null +++ b/app/views/devise/mailer/password_change.html.erb @@ -0,0 +1,3 @@ +

Hello <%= @resource.email %>!

+ +

We're contacting you to notify you that your password has been changed.

diff --git a/app/views/devise/mailer/reset_password_instructions.html.erb b/app/views/devise/mailer/reset_password_instructions.html.erb new file mode 100644 index 0000000000..572feaa9e4 --- /dev/null +++ b/app/views/devise/mailer/reset_password_instructions.html.erb @@ -0,0 +1,8 @@ +

Hello <%= @resource.email %>!

+ +

Someone has requested a link to change your password. You can do this through the link below.

+ +

<%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %>

+ +

If you didn't request this, please ignore this email.

+

Your password won't change until you access the link above and create a new one.

diff --git a/app/views/devise/mailer/unlock_instructions.html.erb b/app/views/devise/mailer/unlock_instructions.html.erb new file mode 100644 index 0000000000..78b9581c6b --- /dev/null +++ b/app/views/devise/mailer/unlock_instructions.html.erb @@ -0,0 +1,7 @@ +

Hello <%= @resource.email %>!

+ +

Your account has been locked due to an excessive number of unsuccessful sign in attempts.

+ +

Click the link below to unlock your account:

+ +

<%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %>

diff --git a/app/views/devise/passwords/edit.html.erb b/app/views/devise/passwords/edit.html.erb new file mode 100644 index 0000000000..3c86fbae19 --- /dev/null +++ b/app/views/devise/passwords/edit.html.erb @@ -0,0 +1,25 @@ +

Change your password

+ +<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + <%= f.hidden_field :reset_password_token %> + +
+ <%= f.label :password, "New password" %>
+ <% if @minimum_password_length %> + (<%= @minimum_password_length %> characters minimum)
+ <% end %> + <%= f.password_field :password, autofocus: true, autocomplete: "new-password" %> +
+ +
+ <%= f.label :password_confirmation, "Confirm new password" %>
+ <%= f.password_field :password_confirmation, autocomplete: "new-password" %> +
+ +
+ <%= f.submit "Change my password" %> +
+<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/views/devise/passwords/new.html.erb b/app/views/devise/passwords/new.html.erb new file mode 100644 index 0000000000..8ba261e05b --- /dev/null +++ b/app/views/devise/passwords/new.html.erb @@ -0,0 +1,16 @@ +

Forgot your password?

+ +<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + +
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email" %> +
+ +
+ <%= f.submit "Send me reset password instructions" %> +
+<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb new file mode 100644 index 0000000000..cc7ee46dc8 --- /dev/null +++ b/app/views/devise/registrations/edit.html.erb @@ -0,0 +1,43 @@ +

Edit <%= resource_name.to_s.humanize %>

+ +<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + +
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email" %> +
+ + <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %> +
Currently waiting confirmation for: <%= resource.unconfirmed_email %>
+ <% end %> + +
+ <%= f.label :password %> (leave blank if you don't want to change it)
+ <%= f.password_field :password, autocomplete: "new-password" %> + <% if @minimum_password_length %> +
+ <%= @minimum_password_length %> characters minimum + <% end %> +
+ +
+ <%= f.label :password_confirmation %>
+ <%= f.password_field :password_confirmation, autocomplete: "new-password" %> +
+ +
+ <%= f.label :current_password %> (we need your current password to confirm your changes)
+ <%= f.password_field :current_password, autocomplete: "current-password" %> +
+ +
+ <%= f.submit "Update" %> +
+<% end %> + +

Cancel my account

+ +
Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?", turbo_confirm: "Are you sure?" }, method: :delete %>
+ +<%= link_to "Back", :back %> diff --git a/app/views/devise/registrations/new.html.erb b/app/views/devise/registrations/new.html.erb new file mode 100644 index 0000000000..d5d325ee2a --- /dev/null +++ b/app/views/devise/registrations/new.html.erb @@ -0,0 +1,29 @@ +

Sign up

+ +<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + +
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email" %> +
+ +
+ <%= f.label :password %> + <% if @minimum_password_length %> + (<%= @minimum_password_length %> characters minimum) + <% end %>
+ <%= f.password_field :password, autocomplete: "new-password" %> +
+ +
+ <%= f.label :password_confirmation %>
+ <%= f.password_field :password_confirmation, autocomplete: "new-password" %> +
+ +
+ <%= f.submit "Sign up" %> +
+<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb new file mode 100644 index 0000000000..f9ba5d59c5 --- /dev/null +++ b/app/views/devise/sessions/new.html.erb @@ -0,0 +1,24 @@ +

Log in

+ +<%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %> +
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email" %> +
+ +
+ <%= f.label :password %>
+ <%= f.password_field :password, autocomplete: "current-password" %> +
+ + <% if devise_mapping.rememberable? %> +
+ <%= f.check_box :remember_me %> + <%= f.label :remember_me %> +
+ <% end %> + +
+ <%= f.submit "Log in" %> +
+<% end %> diff --git a/app/views/devise/shared/_error_messages.html.erb b/app/views/devise/shared/_error_messages.html.erb new file mode 100644 index 0000000000..47a4a0ef59 --- /dev/null +++ b/app/views/devise/shared/_error_messages.html.erb @@ -0,0 +1,15 @@ +<% if resource.errors.any? %> +
+

+ <%= I18n.t("errors.messages.not_saved", + count: resource.errors.count, + resource: resource.class.model_name.human.downcase) + %> +

+
    + <% resource.errors.full_messages.each do |message| %> +
  • <%= message %>
  • + <% end %> +
+
+<% end %> diff --git a/app/views/devise/shared/_links.html.erb b/app/views/devise/shared/_links.html.erb new file mode 100644 index 0000000000..2262dc2976 --- /dev/null +++ b/app/views/devise/shared/_links.html.erb @@ -0,0 +1,25 @@ +<%- if controller_name != 'sessions' %> + <%= link_to "Log in", new_session_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.registerable? && controller_name != 'registrations' %> + <%= link_to "Sign up", new_registration_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %> + <%= link_to "Forgot your password?", new_password_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %> + <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %> + <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.omniauthable? %> + <%- resource_class.omniauth_providers.each do |provider| %> + <%= button_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider), data: { turbo: false } %>
+ <% end %> +<% end %> diff --git a/app/views/devise/unlocks/new.html.erb b/app/views/devise/unlocks/new.html.erb new file mode 100644 index 0000000000..7eb31c85a1 --- /dev/null +++ b/app/views/devise/unlocks/new.html.erb @@ -0,0 +1,16 @@ +

Resend unlock instructions

+ +<%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + +
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email" %> +
+ +
+ <%= f.submit "Resend unlock instructions" %> +
+<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/views/home/index.html.erb b/app/views/home/index.html.erb new file mode 100644 index 0000000000..44cba478f4 --- /dev/null +++ b/app/views/home/index.html.erb @@ -0,0 +1,149 @@ + + +
+ +
+ + +
+
+
+

Bem-vindo, <%= current_user.name %>!

+

Email: <%= current_user.email %>

+
+ <%= button_to 'Sair', destroy_user_session_path, + method: :delete, + class: 'btn btn-light px-4 py-2 fw-semibold', + style: "border-radius: 12px;" %> +
+
+ + + +
+ +
+

📋 Formulários Pendentes

+
+ + <% if @pending_forms.any? %> + +
+ + <% @pending_forms.each do |form| %> +
+
+ +
+ + +
<%= form.title %>
+ + +

+ Turma: <%= form.klass.name %> +

+ + +

+ <%= truncate(form.description, length: 100) %> +

+ + + <% if form.due_date.present? %> +

+ + Prazo: + + <%= l(form.due_date, format: :short) %> + + +

+ <% end %> + +
+ <%= link_to 'Responder Formulário', + answer_student_form_path(form), + class: 'btn text-white fw-semibold', + style: "background:#0A3D91; border-radius: 10px;" %> +
+ +
+
+
+ <% end %> + +
+ + <% else %> + +
+ ✓ Parabéns! + Você respondeu todos os formulários pendentes. +
+ + <% end %> + +
+ + + +
+ +

✓ Formulários Respondidos

+ + <% if @completed_forms.any? %> + +
+ + + + + + + + + + + + + <% @completed_forms.each do |form| %> + + + + + + + <% end %> + + +
TítuloTurmaRespondido emAções
<%= form.title %><%= form.klass.name %> + <% response = form.form_responses.find_by(user: current_user) %> + <%= l(response.submitted_at, format: :short) if response&.submitted_at %> + + <%= link_to 'Ver', + student_form_path(form), + class: 'btn btn-sm text-white', + style: "background:#5fcf7f; border-radius:8px;" %> +
+
+ + <% else %> + +
+ Nenhum formulário respondido ainda. +
+ + <% end %> + +
+ +
+ +
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb new file mode 100644 index 0000000000..ededd90659 --- /dev/null +++ b/app/views/layouts/application.html.erb @@ -0,0 +1,79 @@ + + + + <%= content_for(:title) || "Projeto Camaar" %> + + + + + + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + <%= yield :head %> + + + + + + <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %> + <%= javascript_importmap_tags %> + + + + + + + + +
+ <%= yield %> +
+ + +
+
+ Projeto Camaar • <%= Time.zone.now.year %> +
+
+ + + diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb new file mode 100644 index 0000000000..1a6ce69420 --- /dev/null +++ b/app/views/layouts/mailer.html.erb @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + +
+ + Projeto Camaar + +
+ <%= yield %> +
+

+ Enviado automaticamente pelo sistema Projeto Camaar. +

+

+ © <%= Time.zone.now.year %> Projeto Camaar +

+
+ +
+ + + + diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb new file mode 100644 index 0000000000..aab62e324b --- /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..c1758fdaa2 --- /dev/null +++ b/app/views/pwa/manifest.json.erb @@ -0,0 +1,22 @@ +{ + "name": "ProjetoCamaar", + "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": "ProjetoCamaar.", + "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..9f8b0b5605 --- /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/app/views/student/forms/answer.html.erb b/app/views/student/forms/answer.html.erb new file mode 100644 index 0000000000..fcfa6de8a4 --- /dev/null +++ b/app/views/student/forms/answer.html.erb @@ -0,0 +1,121 @@ +
+ +
+
+

<%= @form.title %>

+

<%= @form.klass.name %>

+
+ <%= link_to '← Voltar', root_path, class: 'btn btn-outline-secondary btn-lg rounded-pill px-4' %> +
+ + +
+ <% if @form.description.present? %> +
+
+
📘 Instruções
+
<%= simple_format(@form.description) %>
+
+
+ <% end %> + + <% if @form.due_date.present? %> +
+
+
+ ⏰ Prazo: <%= l(@form.due_date, format: :long) %> +
+
+
+ <% end %> +
+ + + <% if @form_response.form_answers.empty? %> +
+ Erro: Não há campos para preencher neste formulário. +
+ <% else %> + <%= form_with(model: @form_response, local: true, url: submit_answer_student_form_path(@form), method: :post, class: "mt-4") do |form| %> + + <% if @form_response.errors.any? %> +
+

<%= pluralize(@form_response.errors.count, "erro") %> encontrado:

+
    + <% @form_response.errors.full_messages.each do |message| %> +
  • <%= message %>
  • + <% end %> +
+
+ <% end %> + +
+ <%= form.fields_for :form_answers do |answer_form| %> + <% field = answer_form.object.form_template_field %> + + +
+
+ + + + <% case field.field_type + when 'text' %> + <%= answer_form.text_field :answer, class: 'form-control form-control-lg rounded-3', placeholder: field.label %> + + <% when 'textarea' %> + <%= answer_form.text_area :answer, class: 'form-control form-control-lg rounded-3', rows: 4, placeholder: field.label %> + + <% when 'email' %> + <%= answer_form.email_field :answer, class: 'form-control form-control-lg rounded-3', placeholder: field.label %> + + <% when 'number' %> + <%= answer_form.number_field :answer, class: 'form-control form-control-lg rounded-3', placeholder: field.label %> + + <% when 'date' %> + <%= answer_form.date_field :answer, class: 'form-control form-control-lg rounded-3' %> + + <% when 'select' %> + <% options = field.options.present? ? JSON.parse(field.options) : [] %> + <%= answer_form.select :answer, options, { prompt: 'Selecione uma opção' }, class: 'form-select form-select-lg rounded-3' %> + + <% when 'radio' %> + <% options = field.options.present? ? JSON.parse(field.options) : [] %> +
+ <% options.each do |option| %> +
+ <%= answer_form.radio_button :answer, option, class: 'form-check-input' %> + <%= answer_form.label :answer, option, class: 'form-check-label fs-6' %> +
+ <% end %> +
+ + <% when 'checkbox' %> + <% options = field.options.present? ? JSON.parse(field.options) : [] %> +
+ <% options.each do |option| %> +
+ <%= answer_form.check_box :answer, { multiple: true }, option, nil, class: 'form-check-input' %> + <%= answer_form.label :answer, option, class: 'form-check-label fs-6' %> +
+ <% end %> +
+ <% end %> +
+
+ <% end %> +
+ + +
+ <%= form.submit "Enviar Respostas", class: 'btn btn-success btn-lg px-5 rounded-pill shadow-sm fw-semibold' %> + <%= link_to 'Cancelar', root_path, class: 'btn btn-outline-secondary btn-lg px-4 rounded-pill' %> +
+ <% end %> + <% end %> +
diff --git a/app/views/student/forms/index.html.erb b/app/views/student/forms/index.html.erb new file mode 100644 index 0000000000..ebacc185f7 --- /dev/null +++ b/app/views/student/forms/index.html.erb @@ -0,0 +1,133 @@ + + +
+ +
+ + +
+
+
+

Meus Formulários

+

Responda os formulários da sua turma

+
+ <%= link_to 'Voltar ao Dashboard', root_path, class: 'btn btn-light px-4 py-2 fw-semibold', style: "border-radius: 12px;" %> +
+
+ + +
+ +
+

📋 Formulários Pendentes

+
+ + <% if @pending_forms.any? %> + +
+ + <% @pending_forms.each do |form| %> +
+
+ +
+ + +
<%= form.title %>
+ + +

+ Turma: <%= form.klass.name %> +

+ + +

+ <%= truncate(form.description, length: 100) %> +

+ + + <% if form.due_date.present? %> +

+ + Prazo: + + <%= l(form.due_date, format: :short) %> + + +

+ <% end %> + +
+ <%= link_to 'Responder Formulário', answer_student_form_path(form), class: 'btn text-white fw-semibold', style: "background:#0A3D91; border-radius: 10px;" %> +
+ +
+
+
+ <% end %> + +
+ + <% else %> + +
+ ✓ Parabéns! + Você respondeu todos os formulários pendentes. +
+ + <% end %> + +
+ + + +
+ +

✓ Formulários Respondidos

+ + <% if @completed_forms.any? %> + +
+ + + + + + + + + + + + + <% @completed_forms.each do |form| %> + + + + + + + <% end %> + + +
TítuloTurmaRespondido emAções
<%= form.title %><%= form.klass.name %> + <% response = form.form_responses.find_by(user: current_user) %> + <%= l(response.submitted_at, format: :short) if response&.submitted_at %> + + <%= link_to 'Ver', student_form_path(form), class: 'btn btn-sm text-white', style: "background:#5fcf7f; border-radius:8px;" %> +
+
+ + <% else %> + +
+ Nenhum formulário respondido ainda. +
+ + <% end %> + +
+ +
+ +
\ No newline at end of file diff --git a/app/views/student/forms/show.html.erb b/app/views/student/forms/show.html.erb new file mode 100644 index 0000000000..2efc48286b --- /dev/null +++ b/app/views/student/forms/show.html.erb @@ -0,0 +1,102 @@ + + +
+ + +
+
+
+ +

<%= @form.title %>

+

<%= @form.klass.name %>

+ + <%= link_to 'Voltar', root_path, class: 'hero-back-btn' %> + +
+
+
+ + + +
+ + +
+

Informações

+ +
+
+

Descrição

+

<%= @form.description.presence || "—" %>

+
+ +
+

Prazo

+

+ <% if @form.due_date.present? %> + <%= l(@form.due_date, format: :long) %> + <% else %> + Sem prazo + <% end %> +

+
+
+
+ + + + <% if @form_response&.completed? %> + +
+
+ +
+

Formulário respondido

+

<%= l(@form_response.submitted_at, format: :long) %>

+
+
+ +
+

Suas Respostas

+ +
+ + + + + + + + + + <% @form_response.form_answers.each do |answer| %> + + + + + <% end %> + +
PerguntaResposta
<%= answer.form_template_field.label %><%= answer.answer.presence || "—" %>
+
+
+ + <% else %> + +
+
!
+ +
+

Formulário pendente

+

Você ainda não respondeu este formulário.

+
+ + <%= link_to 'Responder agora', + answer_student_form_path(@form), + class: 'primary-action-btn' %> +
+ + <% end %> + +
+ +
diff --git a/bin/brakeman b/bin/brakeman new file mode 100644 index 0000000000..c88c99b01a --- /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/bundle b/bin/bundle new file mode 100644 index 0000000000..b189df99cc --- /dev/null +++ b/bin/bundle @@ -0,0 +1,109 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'bundle' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "rubygems" + +m = Module.new do + module_function + + def invoked_as_script? + File.expand_path($0) == File.expand_path(__FILE__) + end + + def env_var_version + ENV["BUNDLER_VERSION"] + end + + def cli_arg_version + return unless invoked_as_script? # don't want to hijack other binstubs + return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` + bundler_version = nil + update_index = nil + ARGV.each_with_index do |a, i| + if update_index && update_index.succ == i && a.match?(Gem::Version::ANCHORED_VERSION_PATTERN) + bundler_version = a + end + next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ + bundler_version = $1 + update_index = i + end + bundler_version + end + + def gemfile + gemfile = ENV["BUNDLE_GEMFILE"] + return gemfile if gemfile && !gemfile.empty? + + File.expand_path("../Gemfile", __dir__) + end + + def lockfile + lockfile = + case File.basename(gemfile) + when "gems.rb" then gemfile.sub(/\.rb$/, ".locked") + else "#{gemfile}.lock" + end + File.expand_path(lockfile) + end + + def lockfile_version + return unless File.file?(lockfile) + lockfile_contents = File.read(lockfile) + return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ + Regexp.last_match(1) + end + + def bundler_requirement + @bundler_requirement ||= + env_var_version || + cli_arg_version || + bundler_requirement_for(lockfile_version) + end + + def bundler_requirement_for(version) + return "#{Gem::Requirement.default}.a" unless version + + bundler_gem_version = Gem::Version.new(version) + + bundler_gem_version.approximate_recommendation + end + + def load_bundler! + ENV["BUNDLE_GEMFILE"] ||= gemfile + + activate_bundler + end + + def activate_bundler + gem_error = activation_error_handling do + gem "bundler", bundler_requirement + end + return if gem_error.nil? + require_error = activation_error_handling do + require "bundler/version" + end + return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) + warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" + exit 42 + end + + def activation_error_handling + yield + nil + rescue StandardError, LoadError => e + e + end +end + +m.load_bundler! + +if m.invoked_as_script? + load Gem.bin_path("bundler", "bundle") +end diff --git a/bin/cucumber b/bin/cucumber new file mode 100644 index 0000000000..f66aa9b822 --- /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 100644 index 0000000000..d67c5c69af --- /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 100644 index 0000000000..a54c836e53 --- /dev/null +++ b/bin/docker-entrypoint @@ -0,0 +1,14 @@ +#!/bin/bash -e + +# Enable jemalloc for reduced memory usage and latency. +if [ -z "${LD_PRELOAD+x}" ]; then + LD_PRELOAD=$(find /usr/lib -name libjemalloc.so.2 -print -quit) + export LD_PRELOAD +fi + +# If running the rails server then create or migrate existing database +if [ "${@: -2:1}" == "./bin/rails" ] && [ "${@: -1:1}" == "server" ]; then + ./bin/rails db:prepare +fi + +exec "${@}" diff --git a/bin/importmap b/bin/importmap new file mode 100644 index 0000000000..ced9093a1f --- /dev/null +++ b/bin/importmap @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby + +require_relative "../config/application" +require "importmap/commands" diff --git a/bin/jobs b/bin/jobs new file mode 100644 index 0000000000..ff7026792a --- /dev/null +++ b/bin/jobs @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby + +require_relative "../config/environment" +require "solid_queue/cli" + +SolidQueue::Cli.start(ARGV) diff --git a/bin/kamal b/bin/kamal new file mode 100644 index 0000000000..3cf88036f6 --- /dev/null +++ b/bin/kamal @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'kamal' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("kamal", "kamal") diff --git a/bin/rails b/bin/rails new file mode 100644 index 0000000000..4c8cea5178 --- /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 100644 index 0000000000..4ca7073f83 --- /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 100644 index 0000000000..11fa676929 --- /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 100644 index 0000000000..d86f655dc7 --- /dev/null +++ b/bin/setup @@ -0,0 +1,34 @@ +#!/usr/bin/env ruby +require "fileutils" + +APP_ROOT = File.expand_path("..", __dir__) + +def system!(*args) + system(*args, exception: true) +end + +FileUtils.chdir APP_ROOT do + # This script is a way to set up or update your development environment automatically. + # This script is idempotent, so that you can run it at any time and get an expectable outcome. + # Add necessary setup steps to this file. + + puts "== Installing dependencies ==" + system("bundle check") || system!("bundle install") + + # puts "\n== Copying sample files ==" + # unless File.exist?("config/database.yml") + # FileUtils.cp "config/database.yml.sample", "config/database.yml" + # end + + puts "\n== Preparing database ==" + system! "bin/rails db:prepare" + + puts "\n== Removing old logs and tempfiles ==" + system! "bin/rails log:clear tmp:clear" + + unless ARGV.include?("--skip-server") + puts "\n== Starting development server ==" + STDOUT.flush # flush the output before exec(2) so that it displays + exec "bin/dev" + end +end diff --git a/bin/thrust b/bin/thrust new file mode 100644 index 0000000000..7e1b8f6dd4 --- /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/check_coverage.sh b/check_coverage.sh new file mode 100755 index 0000000000..17ec248f02 --- /dev/null +++ b/check_coverage.sh @@ -0,0 +1,9 @@ +#!/bin/bash +echo "================================" +echo "COBERTURA POR ARQUIVO" +echo "================================" +bundle exec rspec --format json > /tmp/rspec.json 2>&1 +cat coverage/index.html | grep -E "\.rb|coverage" | head -30 +echo "" +echo "Abrindo relatório HTML..." +open coverage/index.html diff --git a/class_members.json b/class_members.json deleted file mode 100755 index 733560ef03..0000000000 --- a/class_members.json +++ /dev/null @@ -1,413 +0,0 @@ -[ - { - "code": "CIC0097", - "classCode": "TA", - "semester": "2021.2", - "dicente": [ - { - "nome": "Ana Clara Jordao Perna", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "190084006", - "usuario": "190084006", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "acjpjvjp@gmail.com" - }, - { - "nome": "Andre Carvalho de Roure", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "200033522", - "usuario": "200033522", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "andreCarvalhoroure@gmail.com" - }, - { - "nome": "André Carvalho Marques", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "150005491", - "usuario": "150005491", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "andre.acm97@outlook.com" - }, - { - "nome": "Antonio Vinicius de Moura Rodrigues", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "190084502", - "usuario": "190084502", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "antoniovmoura.r@gmail.com" - }, - { - "nome": "Arthur Barreiros de Oliveira Mota", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "190102829", - "usuario": "190102829", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "arthurbarreirosmota@gmail.com" - }, - { - "nome": "ARTHUR RODRIGUES NEVES", - "curso": "ENGENHARIA DE COMPUTAÇÃO/CIC", - "matricula": "202014403", - "usuario": "202014403", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "arthurcontroleambiental@gmail.com" - }, - { - "nome": "Bianca Glycia Boueri", - "curso": "ENGENHARIA MECATRÔNICA - CONTROLE E AUTOMAÇÃO/FTD", - "matricula": "170161561", - "usuario": "170161561", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "biancaglyciaboueri@gmail.com" - }, - { - "nome": "Caio Otávio Peluti Alencar", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "190085312", - "usuario": "190085312", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "peluticaio@gmail.com" - }, - { - "nome": "Camila Frealdo Fraga", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "170007561", - "usuario": "170007561", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "camilizx2021@gmail.com" - }, - { - "nome": "Claudio Roberto Oliveira Peres de Barros", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "190097591", - "usuario": "190097591", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "dinhobarros15@gmail.com" - }, - { - "nome": "Daltro Oliveira Vinuto", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "160025966", - "usuario": "160025966", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "daltroov777@gmail.com" - }, - { - "nome": "Davi de Moura Amaral", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "200016750", - "usuario": "200016750", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "davimouraamaral@gmail.com" - }, - { - "nome": "Eduardo Xavier Dantas", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "190086530", - "usuario": "190086530", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "eduardoxdantas@gmail.com" - }, - { - "nome": "Enzo Nunes Leal Sampaio", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "190062789", - "usuario": "190062789", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "enzonleal2016@hotmail.com" - }, - { - "nome": "Enzo Yoshio Niho", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "190027304", - "usuario": "190027304", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "enzoyn@hotmail.com" - }, - { - "nome": "Gabriel Faustino Lima da Rocha", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "190013249", - "usuario": "190013249", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "gabrielfaustino99@gmail.com" - }, - { - "nome": "Gabriel Ligoski", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "190087498", - "usuario": "190087498", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "gabriel.ligoski@gmail.com" - }, - { - "nome": "GABRIEL MENDES CIRIATICO GUIMARÃES", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "202033202", - "usuario": "202033202", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "gabrielciriatico@gmail.com" - }, - { - "nome": "Gustavo Rodrigues dos Santos", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "190014121", - "usuario": "190014121", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "190014121@aluno.unb.br" - }, - { - "nome": "Gustavo Rodrigues Gualberto", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "190108266", - "usuario": "190108266", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "gustavorgualberto@gmail.com" - }, - { - "nome": "Igor David Morais", - "curso": "ENGENHARIA MECATRÔNICA - CONTROLE E AUTOMAÇÃO/FTD", - "matricula": "180102141", - "usuario": "180102141", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "igordavid13@gmail.com" - }, - { - "nome": "Jefte Augusto Gomes Batista", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "180057570", - "usuario": "180057570", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "ndaffte@gmail.com" - }, - { - "nome": "Karolina de Souza Silva", - "curso": "ENGENHARIA DE COMPUTAÇÃO/CIC", - "matricula": "190046791", - "usuario": "190046791", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "karolinasouza@outlook.com" - }, - { - "nome": "Kléber Rodrigues da Costa Júnior", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "200053680", - "usuario": "200053680", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "kleberrjr7@gmail.com" - }, - { - "nome": "Luca Delpino Barbabella", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "180125559", - "usuario": "180125559", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "barbadluca@gmail.com" - }, - { - "nome": "Lucas de Almeida Abreu Faria", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "170016668", - "usuario": "170016668", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "lucasaafaria@gmail.com" - }, - { - "nome": "Lucas Gonçalves Ramalho", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "190098091", - "usuario": "190098091", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "lucasramalho29@gmail.com" - }, - { - "nome": "Lucas Monteiro Miranda", - "curso": "ENGENHARIA MECATRÔNICA - CONTROLE E AUTOMAÇÃO/FTD", - "matricula": "170149684", - "usuario": "170149684", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "luquinha_miranda@hotmail.com" - }, - { - "nome": "Lucas Resende Silveira Reis", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "180144421", - "usuario": "180144421", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "180144421@aluno.unb.br" - }, - { - "nome": "Luis Fernando Freitas Lamellas", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "190016841", - "usuario": "190016841", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "lflamellas@icloud.com" - }, - { - "nome": "Luiza de Araujo Nunes Gomes", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "190112794", - "usuario": "190112794", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "luizangomes@outlook.com" - }, - { - "nome": "Marcelo Aiache Postiglione", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "180126652", - "usuario": "180126652", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "180126652@aluno.unb.br" - }, - { - "nome": "Marcelo Junqueira Ferreira", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "200023624", - "usuario": "200023624", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "marcelojunqueiraf@gmail.com" - }, - { - "nome": "MARIA EDUARDA CARVALHO SANTOS", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "190092556", - "usuario": "190092556", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "auntduda@gmail.com" - }, - { - "nome": "Maria Eduarda Lacerda Dantas", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "200067184", - "usuario": "200067184", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "lacwerda@gmail.com" - }, - { - "nome": "Maylla Krislainy de Sousa Silva", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "190043873", - "usuario": "190043873", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "mayllak@hotmail.com" - }, - { - "nome": "Pedro Cesar Ribeiro Passos", - "curso": "ENGENHARIA MECATRÔNICA - CONTROLE E AUTOMAÇÃO/FTD", - "matricula": "180139312", - "usuario": "180139312", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "pedrocesarribeiro2013@gmail.com" - }, - { - "nome": "Rafael Mascarenhas Dal Moro", - "curso": "ENGENHARIA DE COMPUTAÇÃO/CIC", - "matricula": "170021041", - "usuario": "170021041", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "170021041@aluno.unb.br" - }, - { - "nome": "Rodrigo Mamedio Arrelaro", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "190095164", - "usuario": "190095164", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "arrelaro1@hotmail.com" - }, - { - "nome": "Thiago de Oliveira Albuquerque", - "curso": "ENGENHARIA DE COMPUTAÇÃO/CIC", - "matricula": "140177442", - "usuario": "140177442", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "thiago.work.ti@outlook.com" - }, - { - "nome": "Thiago Elias dos Reis", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "190126892", - "usuario": "190126892", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "thiagoeliasdosreis01@gmail.com" - }, - { - "nome": "Victor Hugo Rodrigues Fernandes", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "180132041", - "usuario": "180132041", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "aluno0sem.luz@gmail.com" - }, - { - "nome": "Vinicius Lima Passos", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "200028545", - "usuario": "200028545", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "viniciuslimapassos@gmail.com" - }, - { - "nome": "William Xavier dos Santos", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "190075384", - "usuario": "190075384", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "wilxavier@me.com" - } - ], - "docente": { - "nome": "MARISTELA TERTO DE HOLANDA", - "departamento": "DEPTO CIÊNCIAS DA COMPUTAÇÃO", - "formacao": "DOUTORADO", - "usuario": "83807519491", - "email": "mholanda@unb.br", - "ocupacao": "docente" - } - } -] diff --git a/class_members2.json b/class_members2.json new file mode 100644 index 0000000000..51b18547c3 --- /dev/null +++ b/class_members2.json @@ -0,0 +1,31 @@ +[ + { + "code": "CIC0105", + "name": "ENGENHARIA DE SOFTWARE", + "class": { + "classCode": "TA", + "semester": "2025.2", + "time": "24N34" + }, + "dicente": [ + { + "nome": "Bernardo Gomes Rodrigues", + "curso": "ENGENHARIA DA COMPUTAÇÃO/CIC", + "matricula": "231034190", + "usuario": "231034190", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "bgrodrigues2005@gmail.com" + }, + { + "nome": "Ana Clara Jordao Perna", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190084006", + "usuario": "190084006", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "acjpjvjp@gmail.com" + } + ] + } +] \ No newline at end of file diff --git a/class_members3.json b/class_members3.json new file mode 100644 index 0000000000..7ea97adf0d --- /dev/null +++ b/class_members3.json @@ -0,0 +1,31 @@ +[ + { + "code": "CIC0104", + "name": "SOFTWARE BASICO", + "class": { + "classCode": "TA", + "semester": "2025.2", + "time": "24M34" + }, + "dicente": [ + { + "nome": "Bernardo Gomes Rodrigues", + "curso": "ENGENHARIA DA COMPUTAÇÃO/CIC", + "matricula": "231034190", + "usuario": "231034190", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "bgrodrigues2005@gmail.com" + }, + { + "nome": "Ana Clara Jordao Perna", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190084006", + "usuario": "190084006", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "acjpjvjp@gmail.com" + } + ] + } +] \ No newline at end of file diff --git a/class_members4.json b/class_members4.json new file mode 100644 index 0000000000..7c0e55f38b --- /dev/null +++ b/class_members4.json @@ -0,0 +1,31 @@ +[ + { + "code": "CIC0189", + "name": "Projeto e Analise de Algoritmos", + "class": { + "classCode": "TA", + "semester": "2025.2", + "time": "24T23" + }, + "dicente": [ + { + "nome": "Bernardo Gomes Rodrigues", + "curso": "ENGENHARIA DA COMPUTAÇÃO/CIC", + "matricula": "231034191", + "usuario": "231034191", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "bgrodrigues2005@gmail.com" + }, + { + "nome": "Ana Clara Jordao Perna", + "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", + "matricula": "190084006", + "usuario": "190084006", + "formacao": "graduando", + "ocupacao": "dicente", + "email": "acjpjvjp@gmail.com" + } + ] + } +] \ No newline at end of file diff --git a/classes.json b/classes.json index c42c269750..c3326609e5 100755 --- a/classes.json +++ b/classes.json @@ -1,29 +1,29 @@ -[ - { - "code": "CIC0097", - "name": "BANCOS DE DADOS", - "class": { - "classCode": "TA", - "semester": "2021.2", - "time": "35T45" - } - }, - { - "code": "CIC0105", - "name": "ENGENHARIA DE SOFTWARE", - "class": { - "classCode": "TA", - "semester": "2021.2", - "time": "35M12" - } - }, - { - "code": "CIC0202", - "name": "PROGRAMAÇÃO CONCORRENTE", - "class": { - "classCode": "TA", - "semester": "2021.2", - "time": "35M34" - } - } -] +[ + { + "code": "CIC0097", + "name": "BANCOS DE DADOS", + "class": { + "classCode": "TA", + "semester": "2021.2", + "time": "35T45" + } + }, + { + "code": "CIC0105", + "name": "ENGENHARIA DE SOFTWARE", + "class": { + "classCode": "TA", + "semester": "2021.2", + "time": "35M12" + } + }, + { + "code": "CIC0202", + "name": "PROGRAMAÇÃO CONCORRENTE", + "class": { + "classCode": "TA", + "semester": "2021.2", + "time": "35M34" + } + } +] diff --git a/config.ru b/config.ru new file mode 100644 index 0000000000..fa780a6cf3 --- /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..5c819f666e --- /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 ProjetoCamaar + class Application < Rails::Application + # Initialize configuration defaults for originally generated Rails version. + config.load_defaults 8.0 + + # Please, add to the `ignore` list any other `lib` subdirectories that do + # not contain `.rb` files, or that should not be reloaded or eager loaded. + # Common ones are `templates`, `generators`, or `middleware`, for example. + config.autoload_lib(ignore: %w[assets tasks]) + + # Configuration for the application, engines, and railties goes here. + # + # These settings can be overridden in specific environments using the files + # in config/environments, which are processed later. + # + # config.time_zone = "Central Time (US & Canada)" + # config.eager_load_paths << Rails.root.join("extras") + end +end diff --git a/config/boot.rb b/config/boot.rb new file mode 100644 index 0000000000..9fa0430dc4 --- /dev/null +++ b/config/boot.rb @@ -0,0 +1,4 @@ +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +require "bundler/setup" # Set up gems listed in the Gemfile. +require "bootsnap/setup" # Speed up boot time by caching expensive operations. diff --git a/config/cable.yml b/config/cable.yml new file mode 100644 index 0000000000..eedcefa720 --- /dev/null +++ b/config/cable.yml @@ -0,0 +1,17 @@ +# Async adapter only works within the same process, so for manually triggering cable updates from a console, +# and seeing results in the browser, you must do so from the web console (running inside the dev process), +# not a terminal started via bin/rails console! Add "console" to any action or any ERB template view +# to make the web console appear. +development: + adapter: async + +test: + adapter: test + +production: + adapter: solid_cable + connects_to: + database: + writing: cable + polling_interval: 0.1.seconds + message_retention: 1.day diff --git a/config/cache.yml b/config/cache.yml new file mode 100644 index 0000000000..2dee2eae6c --- /dev/null +++ b/config/cache.yml @@ -0,0 +1,16 @@ +default: &default + store_options: + # Cap age of oldest cache entry to fulfill retention policies + # max_age: <%= 60.days.to_i %> + max_size: <%= 256.megabytes %> + namespace: <%= Rails.env %> + +development: + <<: *default + +test: + <<: *default + +production: + database: cache + <<: *default diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc new file mode 100644 index 0000000000..a5d0df1768 --- /dev/null +++ b/config/credentials.yml.enc @@ -0,0 +1 @@ +ctHs0gkoY+BU5N44M3TdJPGY8WdddNyhshywXQnUtSTir7HHVB16FPzt5cCS35Pt43gSnz89k/jUHaDyK+ovHMHav3vRLYbiEIvLjzAXmXRETGRK1xX+yHD4ddg4jJjrBL2KGNRea1dzgp6RM+nfY2dXSvOCljJ1Xm4RnTXdUYCUXQN86mwocT8+O5hw7lnb4CSagQDHpdWFgO1p/2lcnax4bdRNup6N4wop6ALHRozj6Iu82Jwc+Zk8hm/8bqflkw6bmPHHDsgQop9BwefWKJaBBdhvwxD0x1eMLdmzbYHhtiuU4TZkfUq7s8PacQGyQBJZ6XniNsYtT25tpVgCr2rnDIkxqemziHeOXDnU5nVbMd0RiljELgUZQ4eXYFmlUc1ULTmWDElHvBzrUK5dV7EZ+3bpG4F0CXwb2OIKL/VlHIPwDnqPtF56XofPZIZprvL8GkgMSPGvrZm2EwHX8m9YgN7UGLjCgaZUygybXihsQKRDNnk1ilFj--HmGOjo0474w4Pvp8--Ho7PmAnPSHMEvSmHl1sQkQ== \ No newline at end of file diff --git a/config/cucumber.yml b/config/cucumber.yml new file mode 100644 index 0000000000..5a890bc7c5 --- /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..729cfaf7a3 --- /dev/null +++ b/config/database.yml @@ -0,0 +1,41 @@ +# SQLite. Versions 3.8.0 and up are supported. +# gem install sqlite3 +# +# Ensure the SQLite 3 gem is defined in your Gemfile +# gem "sqlite3" +# +default: &default + adapter: sqlite3 + pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + timeout: 5000 + +development: + <<: *default + database: storage/development.sqlite3 + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + <<: *default + database: storage/test.sqlite3 + + +# Store production database in the storage/ directory, which by default +# is mounted as a persistent Docker volume in config/deploy.yml. +production: + primary: + <<: *default + database: storage/production.sqlite3 + cache: + <<: *default + database: storage/production_cache.sqlite3 + migrations_paths: db/cache_migrate + queue: + <<: *default + database: storage/production_queue.sqlite3 + migrations_paths: db/queue_migrate + cable: + <<: *default + database: storage/production_cable.sqlite3 + migrations_paths: db/cable_migrate diff --git a/config/deploy.yml b/config/deploy.yml new file mode 100644 index 0000000000..13f9b9d453 --- /dev/null +++ b/config/deploy.yml @@ -0,0 +1,116 @@ +# Name of your application. Used to uniquely configure containers. +service: projeto_camaar + +# Name of the container image. +image: your-user/projeto_camaar + +# Deploy to these servers. +servers: + web: + - 192.168.0.1 + # job: + # hosts: + # - 192.168.0.1 + # cmd: bin/jobs + +# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server. +# Remove this section when using multiple web servers and ensure you terminate SSL at your load balancer. +# +# Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption. +proxy: + ssl: true + host: app.example.com + +# Credentials for your image host. +registry: + # Specify the registry server, if you're not using Docker Hub + # server: registry.digitalocean.com / ghcr.io / ... + username: your-user + + # Always use an access token rather than real password when possible. + password: + - KAMAL_REGISTRY_PASSWORD + +# Inject ENV variables into containers (secrets come from .kamal/secrets). +env: + secret: + - RAILS_MASTER_KEY + clear: + # Run the Solid Queue Supervisor inside the web server's Puma process to do jobs. + # When you start using multiple servers, you should split out job processing to a dedicated machine. + SOLID_QUEUE_IN_PUMA: true + + # Set number of processes dedicated to Solid Queue (default: 1) + # JOB_CONCURRENCY: 3 + + # Set number of cores available to the application on each server (default: 1). + # WEB_CONCURRENCY: 2 + + # Match this to any external database server to configure Active Record correctly + # Use projeto_camaar-db for a db accessory server on same machine via local kamal docker network. + # DB_HOST: 192.168.0.2 + + # Log everything from Rails + # RAILS_LOG_LEVEL: debug + +# Aliases are triggered with "bin/kamal ". You can overwrite arguments on invocation: +# "bin/kamal logs -r job" will tail logs from the first server in the job section. +aliases: + console: app exec --interactive --reuse "bin/rails console" + shell: app exec --interactive --reuse "bash" + logs: app logs -f + dbc: app exec --interactive --reuse "bin/rails dbconsole" + + +# Use a persistent storage volume for sqlite database files and local Active Storage files. +# Recommended to change this to a mounted volume path that is backed up off server. +volumes: + - "projeto_camaar_storage:/rails/storage" + + +# Bridge fingerprinted assets, like JS and CSS, between versions to avoid +# hitting 404 on in-flight requests. Combines all files from new and old +# version inside the asset_path. +asset_path: /rails/public/assets + +# Configure the image builder. +builder: + arch: amd64 + + # # Build image via remote server (useful for faster amd64 builds on arm64 computers) + # remote: ssh://docker@docker-builder-server + # + # # Pass arguments and secrets to the Docker build process + # args: + # RUBY_VERSION: ruby-3.4.7 + # secrets: + # - GITHUB_TOKEN + # - RAILS_MASTER_KEY + +# Use a different ssh user than root +# ssh: +# user: app + +# Use accessory services (secrets come from .kamal/secrets). +# accessories: +# db: +# image: mysql:8.0 +# host: 192.168.0.2 +# # Change to 3306 to expose port to the world instead of just local network. +# port: "127.0.0.1:3306:3306" +# env: +# clear: +# MYSQL_ROOT_HOST: '%' +# secret: +# - MYSQL_ROOT_PASSWORD +# files: +# - config/mysql/production.cnf:/etc/mysql/my.cnf +# - db/production.sql:/docker-entrypoint-initdb.d/setup.sql +# directories: +# - data:/var/lib/mysql +# redis: +# image: redis:7.0 +# host: 192.168.0.2 +# port: 6379 +# directories: +# - data:/data diff --git a/config/environment.rb b/config/environment.rb new file mode 100644 index 0000000000..df9ac347b7 --- /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..e7934578f1 --- /dev/null +++ b/config/environments/development.rb @@ -0,0 +1,76 @@ +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 + + # 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..a99211e546 --- /dev/null +++ b/config/environments/production.rb @@ -0,0 +1,90 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Code is not reloaded between requests. + config.enable_reloading = false + + # Eager load code on boot for better performance and memory savings (ignored by Rake tasks). + config.eager_load = true + + # Full error reports are disabled. + config.consider_all_requests_local = false + + # Turn on fragment caching in view templates. + config.action_controller.perform_caching = true + + # Cache assets for far-future expiry since they are all digest stamped. + config.public_file_server.headers = { "cache-control" => "public, max-age=#{1.year.to_i}" } + + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.asset_host = "http://assets.example.com" + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + + # Assume all access to the app is happening through a SSL-terminating reverse proxy. + config.assume_ssl = true + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + config.force_ssl = true + + # Skip http-to-https redirect for the default health check endpoint. + # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } } + + # Log to STDOUT with the current request id as a default log tag. + config.log_tags = [ :request_id ] + config.logger = ActiveSupport::TaggedLogging.logger(STDOUT) + + # Change to "debug" to log everything (including potentially personally-identifiable information!) + config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") + + # Prevent health checks from clogging up the logs. + config.silence_healthcheck_path = "/up" + + # Don't log any deprecations. + config.active_support.report_deprecations = false + + # Replace the default in-process memory cache store with a durable alternative. + config.cache_store = :solid_cache_store + + # Replace the default in-process and non-durable queuing backend for Active Job. + config.active_job.queue_adapter = :solid_queue + config.solid_queue.connects_to = { database: { writing: :queue } } + + # Ignore bad email addresses and do not raise email delivery errors. + # Set this to true and configure the email server for immediate delivery to raise delivery errors. + # config.action_mailer.raise_delivery_errors = false + + # Set host to be used by links generated in mailer templates. + config.action_mailer.default_url_options = { host: "example.com" } + + # Specify outgoing SMTP server. Remember to add smtp/* credentials via rails credentials:edit. + # config.action_mailer.smtp_settings = { + # user_name: Rails.application.credentials.dig(:smtp, :user_name), + # password: Rails.application.credentials.dig(:smtp, :password), + # address: "smtp.example.com", + # port: 587, + # authentication: :plain + # } + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true + + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false + + # Only use :id for inspections in production. + config.active_record.attributes_for_inspect = [ :id ] + + # Enable DNS rebinding protection and other `Host` header attacks. + # config.hosts = [ + # "example.com", # Allow requests from example.com + # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` + # ] + # + # Skip DNS rebinding protection for the default health check endpoint. + # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } +end diff --git a/config/environments/test.rb b/config/environments/test.rb new file mode 100644 index 0000000000..1cd799ca8a --- /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..e9a3255b9e --- /dev/null +++ b/config/importmap.rb @@ -0,0 +1,11 @@ +# Pin npm packages by running ./bin/importmap + +pin "application" +pin "@hotwired/turbo-rails", to: "turbo.min.js" +pin "@hotwired/stimulus", to: "stimulus.min.js" +pin "@hotwired/stimulus-loading", to: "stimulus-loading.js" +pin_all_from "app/javascript/controllers", under: "controllers" + +pin "application", preload: true +pin_all_from "app/javascript/controllers", under: "controllers" + diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb new file mode 100644 index 0000000000..681584803b --- /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..669fa428e7 --- /dev/null +++ b/config/initializers/content_security_policy.rb @@ -0,0 +1,25 @@ +# Be sure to restart your server when you modify this file. + +# Define an application-wide content security policy. +# See the Securing Rails Applications Guide for more information: +# https://guides.rubyonrails.org/security.html#content-security-policy-header + +# Rails.application.configure do +# config.content_security_policy do |policy| +# policy.default_src :self, :https +# policy.font_src :self, :https, :data +# policy.img_src :self, :https, :data +# policy.object_src :none +# policy.script_src :self, :https +# policy.style_src :self, :https +# # Specify URI for violation reports +# # policy.report_uri "/csp-violation-report-endpoint" +# end +# +# # Generate session nonces for permitted importmap, inline scripts, and inline styles. +# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } +# config.content_security_policy_nonce_directives = %w(script-src style-src) +# +# # Report violations without enforcing the policy. +# # config.content_security_policy_report_only = true +# end diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb new file mode 100644 index 0000000000..4fce4f90e8 --- /dev/null +++ b/config/initializers/devise.rb @@ -0,0 +1,313 @@ +# frozen_string_literal: true + +# Assuming you have not yet modified this file, each configuration option below +# is set to its default value. Note that some are commented out while others +# are not: uncommented lines are intended to protect your configuration from +# breaking changes in upgrades (i.e., in the event that future versions of +# Devise change the default values for those options). +# +# Use this hook to configure devise mailer, warden hooks and so forth. +# Many of these configuration options can be set straight in your model. +Devise.setup do |config| + # The secret key used by Devise. Devise uses this key to generate + # random tokens. Changing this key will render invalid all existing + # confirmation, reset password and unlock tokens in the database. + # Devise will use the `secret_key_base` as its `secret_key` + # by default. You can change it below and use your own secret key. + # config.secret_key = '2ad97e09e6fff98164473ba02c88057fcf40f900ac26b54ead2301614d1f82e35cd938a1bdfdab1e8ab7734e87e09abd5a56219dc0e6d53055aa9995907d7f5a' + + # ==> Controller configuration + # Configure the parent class to the devise controllers. + # config.parent_controller = 'DeviseController' + + # ==> Mailer Configuration + # Configure the e-mail address which will be shown in Devise::Mailer, + # note that it will be overwritten if you use your own mailer class + # with default "from" parameter. + config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com' + + # Configure the class responsible to send e-mails. + # config.mailer = 'Devise::Mailer' + + # Configure the parent class responsible to send e-mails. + # config.parent_mailer = 'ActionMailer::Base' + + # ==> ORM configuration + # Load and configure the ORM. Supports :active_record (default) and + # :mongoid (bson_ext recommended) by default. Other ORMs may be + # available as additional gems. + require 'devise/orm/active_record' + + # ==> Configuration for any authentication mechanism + # Configure which keys are used when authenticating a user. The default is + # just :email. You can configure it to use [:username, :subdomain], so for + # authenticating a user, both parameters are required. Remember that those + # parameters are used only when authenticating and not when retrieving from + # session. If you need permissions, you should implement that in a before filter. + # You can also supply a hash where the value is a boolean determining whether + # or not authentication should be aborted when the value is not present. + # config.authentication_keys = [:email] + + # Configure parameters from the request object used for authentication. Each entry + # given should be a request method and it will automatically be passed to the + # find_for_authentication method and considered in your model lookup. For instance, + # if you set :request_keys to [:subdomain], :subdomain will be used on authentication. + # The same considerations mentioned for authentication_keys also apply to request_keys. + # config.request_keys = [] + + # Configure which authentication keys should be case-insensitive. + # These keys will be downcased upon creating or modifying a user and when used + # to authenticate or find a user. Default is :email. + config.case_insensitive_keys = [:email] + + # Configure which authentication keys should have whitespace stripped. + # These keys will have whitespace before and after removed upon creating or + # modifying a user and when used to authenticate or find a user. Default is :email. + config.strip_whitespace_keys = [:email] + + # Tell if authentication through request.params is enabled. True by default. + # It can be set to an array that will enable params authentication only for the + # given strategies, for example, `config.params_authenticatable = [:database]` will + # enable it only for database (email + password) authentication. + # config.params_authenticatable = true + + # Tell if authentication through HTTP Auth is enabled. False by default. + # It can be set to an array that will enable http authentication only for the + # given strategies, for example, `config.http_authenticatable = [:database]` will + # enable it only for database authentication. + # For API-only applications to support authentication "out-of-the-box", you will likely want to + # enable this with :database unless you are using a custom strategy. + # The supported strategies are: + # :database = Support basic authentication with authentication key + password + # config.http_authenticatable = false + + # If 401 status code should be returned for AJAX requests. True by default. + # config.http_authenticatable_on_xhr = true + + # The realm used in Http Basic Authentication. 'Application' by default. + # config.http_authentication_realm = 'Application' + + # It will change confirmation, password recovery and other workflows + # to behave the same regardless if the e-mail provided was right or wrong. + # Does not affect registerable. + # config.paranoid = true + + # By default Devise will store the user in session. You can skip storage for + # particular strategies by setting this option. + # Notice that if you are skipping storage for all authentication paths, you + # may want to disable generating routes to Devise's sessions controller by + # passing skip: :sessions to `devise_for` in your config/routes.rb + config.skip_session_storage = [:http_auth] + + # By default, Devise cleans up the CSRF token on authentication to + # avoid CSRF token fixation attacks. This means that, when using AJAX + # requests for sign in and sign up, you need to get a new CSRF token + # from the server. You can disable this option at your own risk. + # config.clean_up_csrf_token_on_authentication = true + + # When false, Devise will not attempt to reload routes on eager load. + # This can reduce the time taken to boot the app but if your application + # requires the Devise mappings to be loaded during boot time the application + # won't boot properly. + # config.reload_routes = true + + # ==> Configuration for :database_authenticatable + # For bcrypt, this is the cost for hashing the password and defaults to 12. If + # using other algorithms, it sets how many times you want the password to be hashed. + # The number of stretches used for generating the hashed password are stored + # with the hashed password. This allows you to change the stretches without + # invalidating existing passwords. + # + # Limiting the stretches to just one in testing will increase the performance of + # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use + # a value less than 10 in other environments. Note that, for bcrypt (the default + # algorithm), the cost increases exponentially with the number of stretches (e.g. + # a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation). + config.stretches = Rails.env.test? ? 1 : 12 + + # Set up a pepper to generate the hashed password. + # config.pepper = 'b1bb2e7e71e5d6e712355e53017528d30755ba9f83596f33463863b9b1d7eafa3403aa09e95983e6daef3c80f6e6be03d7e3ca2d5c8250b34dad18b4f1d1dba5' + + # Send a notification to the original email when the user's email is changed. + # config.send_email_changed_notification = false + + # Send a notification email when the user's password is changed. + # config.send_password_change_notification = false + + # ==> Configuration for :confirmable + # A period that the user is allowed to access the website even without + # confirming their account. For instance, if set to 2.days, the user will be + # able to access the website for two days without confirming their account, + # access will be blocked just in the third day. + # You can also set it to nil, which will allow the user to access the website + # without confirming their account. + # Default is 0.days, meaning the user cannot access the website without + # confirming their account. + # config.allow_unconfirmed_access_for = 2.days + + # A period that the user is allowed to confirm their account before their + # token becomes invalid. For example, if set to 3.days, the user can confirm + # their account within 3 days after the mail was sent, but on the fourth day + # their account can't be confirmed with the token any more. + # Default is nil, meaning there is no restriction on how long a user can take + # before confirming their account. + # config.confirm_within = 3.days + + # If true, requires any email changes to be confirmed (exactly the same way as + # initial account confirmation) to be applied. Requires additional unconfirmed_email + # db field (see migrations). Until confirmed, new email is stored in + # unconfirmed_email column, and copied to email column on successful confirmation. + config.reconfirmable = true + + # Defines which key will be used when confirming an account + # config.confirmation_keys = [:email] + + # ==> Configuration for :rememberable + # The time the user will be remembered without asking for credentials again. + # config.remember_for = 2.weeks + + # Invalidates all the remember me tokens when the user signs out. + config.expire_all_remember_me_on_sign_out = true + + # If true, extends the user's remember period when remembered via cookie. + # config.extend_remember_period = false + + # Options to be passed to the created cookie. For instance, you can set + # secure: true in order to force SSL only cookies. + # config.rememberable_options = {} + + # ==> Configuration for :validatable + # Range for password length. + config.password_length = 6..128 + + # Email regex used to validate email formats. It simply asserts that + # one (and only one) @ exists in the given string. This is mainly + # to give user feedback and not to assert the e-mail validity. + config.email_regexp = /\A[^@\s]+@[^@\s]+\z/ + + # ==> Configuration for :timeoutable + # The time you want to timeout the user session without activity. After this + # time the user will be asked for credentials again. Default is 30 minutes. + # config.timeout_in = 30.minutes + + # ==> Configuration for :lockable + # Defines which strategy will be used to lock an account. + # :failed_attempts = Locks an account after a number of failed attempts to sign in. + # :none = No lock strategy. You should handle locking by yourself. + # config.lock_strategy = :failed_attempts + + # Defines which key will be used when locking and unlocking an account + # config.unlock_keys = [:email] + + # Defines which strategy will be used to unlock an account. + # :email = Sends an unlock link to the user email + # :time = Re-enables login after a certain amount of time (see :unlock_in below) + # :both = Enables both strategies + # :none = No unlock strategy. You should handle unlocking by yourself. + # config.unlock_strategy = :both + + # Number of authentication tries before locking an account if lock_strategy + # is failed attempts. + # config.maximum_attempts = 20 + + # Time interval to unlock the account if :time is enabled as unlock_strategy. + # config.unlock_in = 1.hour + + # Warn on the last attempt before the account is locked. + # config.last_attempt_warning = true + + # ==> Configuration for :recoverable + # + # Defines which key will be used when recovering the password for an account + # config.reset_password_keys = [:email] + + # Time interval you can reset your password with a reset password key. + # Don't put a too small interval or your users won't have the time to + # change their passwords. + config.reset_password_within = 6.hours + + # When set to false, does not sign a user in automatically after their password is + # reset. Defaults to true, so a user is signed in automatically after a reset. + # config.sign_in_after_reset_password = true + + # ==> Configuration for :encryptable + # Allow you to use another hashing or encryption algorithm besides bcrypt (default). + # You can use :sha1, :sha512 or algorithms from others authentication tools as + # :clearance_sha1, :authlogic_sha512 (then you should set stretches above to 20 + # for default behavior) and :restful_authentication_sha1 (then you should set + # stretches to 10, and copy REST_AUTH_SITE_KEY to pepper). + # + # Require the `devise-encryptable` gem when using anything other than bcrypt + # config.encryptor = :sha512 + + # ==> Scopes configuration + # Turn scoped views on. Before rendering "sessions/new", it will first check for + # "users/sessions/new". It's turned off by default because it's slower if you + # are using only default views. + # config.scoped_views = false + + # Configure the default scope given to Warden. By default it's the first + # devise role declared in your routes (usually :user). + # config.default_scope = :user + + # Set this configuration to false if you want /users/sign_out to sign out + # only the current scope. By default, Devise signs out all scopes. + # config.sign_out_all_scopes = true + + # ==> Navigation configuration + # Lists the formats that should be treated as navigational. Formats like + # :html should redirect to the sign in page when the user does not have + # access, but formats like :xml or :json, should return 401. + # + # If you have any extra navigational formats, like :iphone or :mobile, you + # should add them to the navigational formats lists. + # + # The "*/*" below is required to match Internet Explorer requests. + # config.navigational_formats = ['*/*', :html, :turbo_stream] + + # The default HTTP method used to sign out a resource. Default is :delete. + config.sign_out_via = [:get, :delete] + + # ==> OmniAuth + # Add a new OmniAuth provider. Check the wiki for more information on setting + # up on your models and hooks. + # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' + + # ==> Warden configuration + # If you want to use other strategies, that are not supported by Devise, or + # change the failure app, you can configure them inside the config.warden block. + # + # config.warden do |manager| + # manager.intercept_401 = false + # manager.default_strategies(scope: :user).unshift :some_external_strategy + # end + + # ==> Mountable engine configurations + # When using Devise inside an engine, let's call it `MyEngine`, and this engine + # is mountable, there are some extra configurations to be taken into account. + # The following options are available, assuming the engine is mounted as: + # + # mount MyEngine, at: '/my_engine' + # + # The router that invoked `devise_for`, in the example above, would be: + # config.router_name = :my_engine + # + # When using OmniAuth, Devise cannot automatically set OmniAuth path, + # so you need to do it manually. For the users scope, it would be: + # config.omniauth_path_prefix = '/my_engine/users/auth' + + # ==> Hotwire/Turbo configuration + # When using Devise with Hotwire/Turbo, the http status for error responses + # and some redirects must match the following. The default in Devise for existing + # apps is `200 OK` and `302 Found` respectively, but new apps are generated with + # these new defaults that match Hotwire/Turbo behavior. + # Note: These might become the new default in future versions of Devise. + config.responder.error_status = :unprocessable_entity + config.responder.redirect_status = :see_other + + # ==> Configuration for :registerable + + # When set to false, does not sign a user in automatically after their password is + # changed. Defaults to true, so a user is signed in automatically after changing a password. + # config.sign_in_after_change_password = true +end diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb new file mode 100644 index 0000000000..a9e8f11d72 --- /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..25a09fb6d7 --- /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/devise.en.yml b/config/locales/devise.en.yml new file mode 100644 index 0000000000..a0ede6aa45 --- /dev/null +++ b/config/locales/devise.en.yml @@ -0,0 +1,65 @@ +# Additional translations at https://github.com/heartcombo/devise/wiki/I18n + +en: + devise: + confirmations: + confirmed: "Your email address has been successfully confirmed." + send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes." + send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes." + failure: + already_authenticated: "You are already signed in." + inactive: "Your account is not activated yet." + invalid: "Invalid %{authentication_keys} or password." + locked: "Your account is locked." + last_attempt: "You have one more attempt before your account is locked." + not_found_in_database: "Invalid %{authentication_keys} or password." + timeout: "Your session expired. Please sign in again to continue." + unauthenticated: "You need to sign in or sign up before continuing." + unconfirmed: "You have to confirm your email address before continuing." + mailer: + confirmation_instructions: + subject: "Confirmation instructions" + reset_password_instructions: + subject: "Reset password instructions" + unlock_instructions: + subject: "Unlock instructions" + email_changed: + subject: "Email Changed" + password_change: + subject: "Password Changed" + omniauth_callbacks: + failure: "Could not authenticate you from %{kind} because \"%{reason}\"." + success: "Successfully authenticated from %{kind} account." + passwords: + no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." + send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." + send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." + updated: "Your password has been changed successfully. You are now signed in." + updated_not_active: "Your password has been changed successfully." + registrations: + destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon." + signed_up: "Welcome! You have signed up successfully." + signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated." + signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked." + signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account." + update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirmation link to confirm your new email address." + updated: "Your account has been updated successfully." + updated_but_not_signed_in: "Your account has been updated successfully, but since your password was changed, you need to sign in again." + sessions: + signed_in: "Signed in successfully." + signed_out: "Signed out successfully." + already_signed_out: "Signed out successfully." + unlocks: + send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes." + send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes." + unlocked: "Your account has been unlocked successfully. Please sign in to continue." + errors: + messages: + already_confirmed: "was already confirmed, please try signing in" + confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one" + expired: "has expired, please request a new one" + not_found: "not found" + not_locked: "was not locked" + not_saved: + one: "1 error prohibited this %{resource} from being saved:" + other: "%{count} errors prohibited this %{resource} from being saved:" diff --git a/config/locales/en.yml b/config/locales/en.yml new file mode 100644 index 0000000000..6894d67159 --- /dev/null +++ b/config/locales/en.yml @@ -0,0 +1,31 @@ +# Files in the config/locales directory are used for internationalization and +# are automatically loaded by Rails. If you want to use locales other than +# English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t "hello" +# +# In views, this is aliased to just `t`: +# +# <%= t("hello") %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# To learn more about the API, please read the Rails Internationalization guide +# at https://guides.rubyonrails.org/i18n.html. +# +# Be aware that YAML interprets the following case-insensitive strings as +# booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings +# must be quoted to be interpreted as strings. For example: +# +# en: +# "yes": yup +# enabled: "ON" + +en: + hello: "Hello world" diff --git a/config/puma.rb b/config/puma.rb new file mode 100644 index 0000000000..a6a7823f0f --- /dev/null +++ b/config/puma.rb @@ -0,0 +1,41 @@ +# This configuration file will be evaluated by Puma. The top-level methods that +# are invoked here are part of Puma's configuration DSL. For more information +# about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. +# +# Puma starts a configurable number of processes (workers) and each process +# serves each request in a thread from an internal thread pool. +# +# You can control the number of workers using ENV["WEB_CONCURRENCY"]. You +# should only set this value when you want to run 2 or more workers. The +# default is already 1. +# +# The ideal number of threads per worker depends both on how much time the +# application spends waiting for IO operations and on how much you wish to +# prioritize throughput over latency. +# +# As a rule of thumb, increasing the number of threads will increase how much +# traffic a given process can handle (throughput), but due to CRuby's +# Global VM Lock (GVL) it has diminishing returns and will degrade the +# response time (latency) of the application. +# +# The default is set to 3 threads as it's deemed a decent compromise between +# throughput and latency for the average Rails application. +# +# Any libraries that use a connection pool or another resource pool should +# be configured to provide at least as many connections as the number of +# threads. This includes Active Record's `pool` parameter in `database.yml`. +threads_count = ENV.fetch("RAILS_MAX_THREADS", 3) +threads threads_count, threads_count + +# Specifies the `port` that Puma will listen on to receive requests; default is 3000. +port ENV.fetch("PORT", 3000) + +# Allow puma to be restarted by `bin/rails restart` command. +plugin :tmp_restart + +# Run the Solid Queue supervisor inside of Puma for single-server deployments +plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"] + +# Specify the PID file. Defaults to tmp/pids/server.pid in development. +# In other environments, only set the PID file if requested. +pidfile ENV["PIDFILE"] if ENV["PIDFILE"] diff --git a/config/queue.yml b/config/queue.yml new file mode 100644 index 0000000000..4dbaa79c7b --- /dev/null +++ b/config/queue.yml @@ -0,0 +1,18 @@ +default: &default + dispatchers: + - polling_interval: 1 + batch_size: 500 + workers: + - queues: "*" + threads: 3 + processes: <%= ENV.fetch("JOB_CONCURRENCY", 1) %> + polling_interval: 0.1 + +development: + <<: *default + +test: + <<: *default + +production: + <<: *default diff --git a/config/recurring.yml b/config/recurring.yml new file mode 100644 index 0000000000..e22a760473 --- /dev/null +++ b/config/recurring.yml @@ -0,0 +1,15 @@ +# examples: +# periodic_cleanup: +# class: CleanSoftDeletedRecordsJob +# queue: background +# args: [ 1000, { batch_size: 500 } ] +# schedule: every hour +# periodic_cleanup_with_command: +# command: "SoftDeletedRecord.due.delete_all" +# priority: 2 +# schedule: at 5am every day + +production: + clear_solid_queue_finished_jobs: + command: "SolidQueue::Job.clear_finished_in_batches(sleep_between_batches: 0.3)" + schedule: every hour at minute 12 diff --git a/config/routes.rb b/config/routes.rb new file mode 100644 index 0000000000..c350d707df --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,44 @@ +# config/routes.rb + +Rails.application.routes.draw do + devise_for :users, skip: [:registrations] + + root "home#index" + + namespace :admin do + root "dashboard#index" + resources :users + resources :imports, only: [:index] do + collection do + post :import_klasses + end + end + + # Form Templates + resources :form_templates do + resources :form_template_fields, only: [:create, :update, :destroy] + end + + # Forms + resources :forms do + member do + patch :publish + patch :close + get :view_response + end + end + end + + # Student namespace + namespace :student do + root "dashboard#index" + resources :forms, only: [:index, :show] do + member do + get :answer + post :submit_answer + end + end + end + + get "home", to: "home#index" +end diff --git a/config/storage.yml b/config/storage.yml new file mode 100644 index 0000000000..aafbb0cdb5 --- /dev/null +++ b/config/storage.yml @@ -0,0 +1,34 @@ +test: + service: Disk + root: <%= Rails.root.join("tmp/storage") %> + +local: + service: Disk + root: <%= Rails.root.join("storage") %> + +# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) +# amazon: +# service: S3 +# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> +# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> +# region: us-east-1 +# bucket: your_own_bucket-<%= Rails.env %> + +# Remember not to checkin your GCS keyfile to a repository +# google: +# service: GCS +# project: your_project +# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> +# bucket: your_own_bucket-<%= Rails.env %> + +# Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) +# microsoft: +# service: AzureStorage +# storage_account_name: your_account_name +# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> +# container: your_container_name-<%= Rails.env %> + +# mirror: +# service: Mirror +# primary: local +# mirrors: [ amazon, google, microsoft ] diff --git a/config/tailwind.config.js b/config/tailwind.config.js new file mode 100644 index 0000000000..cfc8b0b13d --- /dev/null +++ b/config/tailwind.config.js @@ -0,0 +1,13 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + './public/*.html', + './app/helpers/**/*.rb', + './app/javascript/**/*.js', + './app/views/**/*.{erb,haml,html,slim}' + ], + theme: { + extend: {}, + }, + plugins: [], +} diff --git a/coverage/.last_run.json b/coverage/.last_run.json new file mode 100644 index 0000000000..8017bb8d86 --- /dev/null +++ b/coverage/.last_run.json @@ -0,0 +1,5 @@ +{ + "result": { + "line": 98.57 + } +} diff --git a/coverage/.resultset.json b/coverage/.resultset.json new file mode 100644 index 0000000000..c5466879bd --- /dev/null +++ b/coverage/.resultset.json @@ -0,0 +1,793 @@ +{ + "RSpec": { + "coverage": { + "/home/bgrod/sprint3/CAMAAR-Grupo-10/app/helpers/admin/dashboard_helper.rb": { + "lines": [ + 1, + null + ] + }, + "/home/bgrod/sprint3/CAMAAR-Grupo-10/app/helpers/admin/users_helper.rb": { + "lines": [ + 1, + null + ] + }, + "/home/bgrod/sprint3/CAMAAR-Grupo-10/app/helpers/application_helper.rb": { + "lines": [ + 1, + null + ] + }, + "/home/bgrod/sprint3/CAMAAR-Grupo-10/app/helpers/home_helper.rb": { + "lines": [ + 1, + null + ] + }, + "/home/bgrod/sprint3/CAMAAR-Grupo-10/app/models/class_member.rb": { + "lines": [ + null, + null, + 1, + null, + 1, + 1, + null, + null, + 1, + 1, + null + ] + }, + "/home/bgrod/sprint3/CAMAAR-Grupo-10/app/models/application_record.rb": { + "lines": [ + 1, + 1, + null + ] + }, + "/home/bgrod/sprint3/CAMAAR-Grupo-10/app/models/form_answer.rb": { + "lines": [ + null, + null, + 1, + 1, + 1, + null, + 1, + 1, + 1, + null + ] + }, + "/home/bgrod/sprint3/CAMAAR-Grupo-10/app/models/form_response.rb": { + "lines": [ + null, + null, + 1, + 1, + 1, + 1, + null, + 1, + 1, + 1, + null, + 1, + null, + 1, + 12, + null, + null, + 1, + 2, + null, + null, + null, + 1, + 12, + 13, + 13, + null, + null, + null, + null, + null, + 1, + 8, + null, + null + ] + }, + "/home/bgrod/sprint3/CAMAAR-Grupo-10/app/models/form.rb": { + "lines": [ + null, + null, + 1, + 1, + 1, + 1, + null, + 1, + 1, + 1, + null, + 1, + null, + null, + 1, + null, + null, + 1, + 8, + null, + null, + null, + 1, + 10, + null, + null, + 1, + null, + 1, + 108, + null, + null + ] + }, + "/home/bgrod/sprint3/CAMAAR-Grupo-10/app/models/form_template_field.rb": { + "lines": [ + null, + null, + 1, + 1, + 1, + null, + null, + 1, + null, + 1, + 1, + 1, + null, + null, + 1, + null, + null, + 1, + null, + 1, + null, + 1, + 52, + null, + null, + 1, + 48, + null, + null + ] + }, + "/home/bgrod/sprint3/CAMAAR-Grupo-10/app/models/form_template.rb": { + "lines": [ + null, + null, + 1, + 1, + 1, + 1, + null, + 1, + 1, + 1, + null, + 1, + null + ] + }, + "/home/bgrod/sprint3/CAMAAR-Grupo-10/app/models/klass.rb": { + "lines": [ + null, + null, + 1, + null, + 1, + 1, + 1, + null, + null, + 1, + 1, + 1, + null, + null, + 2, + null, + null, + 1, + 15, + null, + null, + 1, + 3, + null, + null + ] + }, + "/home/bgrod/sprint3/CAMAAR-Grupo-10/app/models/user.rb": { + "lines": [ + null, + null, + 1, + 1, + null, + null, + 1, + null, + null, + 1, + 1, + 1, + 1, + null, + null, + 1, + 1, + null, + null, + 1, + 137, + null, + null, + 1, + 4, + null, + null, + null, + 1, + 20, + null, + null, + null, + null, + null, + null, + null, + 1, + 15, + null, + null, + null + ] + }, + "/home/bgrod/sprint3/CAMAAR-Grupo-10/app/services/import_service.rb": { + "lines": [ + null, + null, + 1, + 1, + null, + 1, + 25, + 25, + 25, + null, + null, + 1, + 25, + null, + 21, + 24, + null, + null, + 21, + null, + 4, + null, + null, + 1, + null, + 1, + 25, + null, + 3, + null, + 1, + null, + null, + 1, + 24, + 24, + 24, + null, + 0, + null, + null, + 1, + 24, + null, + 24, + 23, + 23, + 23, + null, + null, + null, + 1, + 24, + null, + 20, + 35, + null, + null, + null, + 1, + 35, + null, + 32, + 32, + null, + null, + 3, + null, + null, + 1, + 35, + null, + null, + 1, + 30, + null, + 30, + 27, + null, + 3, + null, + null, + null, + 1, + 30, + null, + 30, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ] + }, + "/home/bgrod/sprint3/CAMAAR-Grupo-10/app/controllers/application_controller.rb": { + "lines": [ + 1, + null, + 1, + null + ] + }, + "/home/bgrod/sprint3/CAMAAR-Grupo-10/app/controllers/admin/dashboard_controller.rb": { + "lines": [ + null, + null, + 1, + 1, + 1, + 1, + null, + 1, + 1, + 1, + 1, + null, + null, + 1, + null, + 1, + 1, + null, + null, + null + ] + }, + "/home/bgrod/sprint3/CAMAAR-Grupo-10/app/controllers/admin/forms_controller.rb": { + "lines": [ + null, + null, + 1, + 1, + 1, + 1, + 1, + 1, + null, + 1, + 3, + null, + null, + 1, + 3, + 3, + null, + null, + 1, + 2, + 2, + null, + null, + 1, + 4, + null, + 3, + 3, + null, + 0, + 0, + null, + null, + null, + 1, + 3, + null, + null, + 1, + 6, + 4, + null, + 2, + 2, + null, + null, + null, + 1, + 3, + 3, + null, + null, + 1, + 3, + 3, + null, + 0, + null, + null, + null, + 1, + 3, + 3, + null, + 0, + null, + null, + null, + 1, + null, + null, + null, + 1, + null, + 1, + 4, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 7, + 7, + null, + null, + 1, + 24, + null, + null, + 1, + 2, + null, + null, + 1, + 22, + null, + null, + 1, + 34, + null, + null, + null + ] + }, + "/home/bgrod/sprint3/CAMAAR-Grupo-10/app/controllers/admin/imports_controller.rb": { + "lines": [ + null, + null, + 1, + 1, + 1, + 1, + null, + 1, + 4, + 4, + null, + null, + 1, + 19, + 17, + null, + 15, + 15, + null, + null, + 1, + null, + 1, + 17, + 17, + null, + null, + 1, + 6, + null, + null, + 1, + 15, + 13, + null, + 2, + null, + null, + null, + 1, + 13, + null, + 13, + 1, + null, + 12, + null, + null, + null, + 1, + 1, + 2, + 1, + null, + null, + 1, + 24, + null, + null, + null + ] + }, + "/home/bgrod/sprint3/CAMAAR-Grupo-10/app/controllers/student/forms_controller.rb": { + "lines": [ + null, + null, + 1, + 1, + 1, + 1, + 1, + 1, + null, + 1, + 3, + 3, + null, + null, + 1, + 2, + 2, + null, + null, + 1, + 5, + null, + 5, + 4, + 4, + 4, + 4, + null, + null, + 5, + null, + null, + 5, + 4, + 4, + null, + null, + null, + 1, + 5, + null, + 5, + 1, + 1, + null, + null, + 4, + 3, + null, + 1, + null, + null, + null, + 1, + null, + 1, + 15, + null, + null, + 1, + 19, + null, + null, + 1, + 14, + 2, + null, + null, + null, + 1, + 4, + 4, + null, + null, + null + ] + }, + "/home/bgrod/sprint3/CAMAAR-Grupo-10/app/controllers/home_controller.rb": { + "lines": [ + null, + null, + 1, + 1, + null, + 1, + 6, + 2, + null, + 4, + 4, + null, + null, + null + ] + }, + "/home/bgrod/sprint3/CAMAAR-Grupo-10/app/controllers/admin/users_controller.rb": { + "lines": [ + null, + null, + 1, + 1, + 1, + 1, + 1, + null, + 1, + 2, + null, + null, + 1, + null, + null, + 1, + null, + null, + 1, + 6, + 4, + null, + 2, + null, + null, + null, + 1, + 2, + 2, + null, + null, + 1, + null, + 1, + 14, + null, + null, + 1, + 6, + null, + null, + 1, + 17, + null, + null, + null + ] + }, + "/home/bgrod/sprint3/CAMAAR-Grupo-10/app/controllers/admin/form_templates_controller.rb": { + "lines": [ + null, + null, + 1, + 1, + 1, + 1, + 1, + null, + 1, + 4, + null, + null, + 1, + null, + null, + 1, + 2, + 2, + null, + null, + 1, + 7, + null, + 7, + 4, + null, + 3, + null, + null, + null, + 1, + 4, + null, + null, + 1, + 6, + 4, + null, + 2, + null, + null, + null, + 1, + 3, + 3, + null, + null, + 1, + null, + 1, + 18, + null, + null, + 1, + 13, + null, + null, + null, + null, + null, + 1, + 32, + null, + null, + null + ] + } + }, + "timestamp": 1765850634 + } +} diff --git a/coverage/.resultset.json.lock b/coverage/.resultset.json.lock new file mode 100644 index 0000000000..e69de29bb2 diff --git a/coverage/assets/0.13.2/DataTables-1.10.20/images/sort_asc.png b/coverage/assets/0.13.2/DataTables-1.10.20/images/sort_asc.png new file mode 100644 index 0000000000..e1ba61a805 Binary files /dev/null and b/coverage/assets/0.13.2/DataTables-1.10.20/images/sort_asc.png differ diff --git a/coverage/assets/0.13.2/DataTables-1.10.20/images/sort_asc_disabled.png b/coverage/assets/0.13.2/DataTables-1.10.20/images/sort_asc_disabled.png new file mode 100644 index 0000000000..fb11dfe24a Binary files /dev/null and b/coverage/assets/0.13.2/DataTables-1.10.20/images/sort_asc_disabled.png differ diff --git a/coverage/assets/0.13.2/DataTables-1.10.20/images/sort_both.png b/coverage/assets/0.13.2/DataTables-1.10.20/images/sort_both.png new file mode 100644 index 0000000000..af5bc7c5a1 Binary files /dev/null and b/coverage/assets/0.13.2/DataTables-1.10.20/images/sort_both.png differ diff --git a/coverage/assets/0.13.2/DataTables-1.10.20/images/sort_desc.png b/coverage/assets/0.13.2/DataTables-1.10.20/images/sort_desc.png new file mode 100644 index 0000000000..0e156deb5f Binary files /dev/null and b/coverage/assets/0.13.2/DataTables-1.10.20/images/sort_desc.png differ diff --git a/coverage/assets/0.13.2/DataTables-1.10.20/images/sort_desc_disabled.png b/coverage/assets/0.13.2/DataTables-1.10.20/images/sort_desc_disabled.png new file mode 100644 index 0000000000..c9fdd8a150 Binary files /dev/null and b/coverage/assets/0.13.2/DataTables-1.10.20/images/sort_desc_disabled.png differ diff --git a/coverage/assets/0.13.2/application.css b/coverage/assets/0.13.2/application.css new file mode 100644 index 0000000000..a6f11a5e23 --- /dev/null +++ b/coverage/assets/0.13.2/application.css @@ -0,0 +1 @@ +html,body,div,span,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,code,del,dfn,em,img,q,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,dialog,figure,footer,header,hgroup,nav,section{margin:0;padding:0;border:0;font-weight:inherit;font-style:inherit;font-size:100%;font-family:inherit;vertical-align:baseline}article,aside,dialog,figure,footer,header,hgroup,nav,section{display:block}body{line-height:1.5}table{border-collapse:separate;border-spacing:0}caption,th,td{text-align:left;font-weight:normal}table,td,th{vertical-align:middle}blockquote:before,blockquote:after,q:before,q:after{content:""}blockquote,q{quotes:"" ""}a img{border:none}html{font-size:100.01%}body{font-size:82%;color:#222;background:#fff;font-family:"Helvetica Neue", Arial, Helvetica, sans-serif}h1,h2,h3,h4,h5,h6{font-weight:normal;color:#111}h1{font-size:3em;line-height:1;margin-bottom:0.5em}h2{font-size:2em;margin-bottom:0.75em}h3{font-size:1.5em;line-height:1;margin-bottom:1em}h4{font-size:1.2em;line-height:1.25;margin-bottom:1.25em}h5{font-size:1em;font-weight:bold;margin-bottom:1.5em}h6{font-size:1em;font-weight:bold}h1 img,h2 img,h3 img,h4 img,h5 img,h6 img{margin:0}p{margin:0 0 1.5em}p img.left{float:left;margin:1.5em 1.5em 1.5em 0;padding:0}p img.right{float:right;margin:1.5em 0 1.5em 1.5em}a:focus,a:hover{color:#000}a{color:#009;text-decoration:underline}blockquote{margin:1.5em;color:#666;font-style:italic}strong{font-weight:bold}em,dfn{font-style:italic}dfn{font-weight:bold}sup,sub{line-height:0}abbr,acronym{border-bottom:1px dotted #666}address{margin:0 0 1.5em;font-style:italic}del{color:#666}pre{margin:1.5em 0;white-space:pre}pre,code,tt{font:1em 'andale mono', 'lucida console', monospace;line-height:1.5}li ul,li ol{margin:0}ul,ol{margin:0 1.5em 1.5em 0;padding-left:3.333em}ul{list-style-type:disc}ol{list-style-type:decimal}dl{margin:0 0 1.5em 0}dl dt{font-weight:bold}dd{margin-left:1.5em}table{margin-bottom:1.4em;width:100%}th{font-weight:bold}thead th{background:#c3d9ff}th,td,caption{padding:4px 10px 4px 5px}tr.even td{background:#efefef}tfoot{font-style:italic}caption{background:#eee}.small{font-size:.8em;margin-bottom:1.875em;line-height:1.875em}.large{font-size:1.2em;line-height:2.5em;margin-bottom:1.25em}.hide{display:none}.quiet{color:#666}.loud{color:#000}.highlight{background:#ff0}.added{background:#060;color:#fff}.removed{background:#900;color:#fff}.first{margin-left:0;padding-left:0}.last{margin-right:0;padding-right:0}.top{margin-top:0;padding-top:0}.bottom{margin-bottom:0;padding-bottom:0}label{font-weight:bold}fieldset{padding:1.4em;margin:0 0 1.5em 0;border:1px solid #ccc}legend{font-weight:bold;font-size:1.2em}input[type=text],input[type=password],input.text,input.title,textarea,select{background-color:#fff;border:1px solid #bbb}input[type=text]:focus,input[type=password]:focus,input.text:focus,input.title:focus,textarea:focus,select:focus{border-color:#666}input[type=text],input[type=password],input.text,input.title,textarea,select{margin:0.5em 0}input.text,input.title{width:300px;padding:5px}input.title{font-size:1.5em}textarea{width:390px;height:250px;padding:5px}input[type=checkbox],input[type=radio],input.checkbox,input.radio{position:relative;top:.25em}form.inline{line-height:3}form.inline p{margin-bottom:0}.error,.notice,.success{padding:.8em;margin-bottom:1em;border:2px solid #ddd}.error{background:#FBE3E4;color:#8a1f11;border-color:#FBC2C4}.notice{background:#FFF6BF;color:#514721;border-color:#FFD324}.success{background:#E6EFC2;color:#264409;border-color:#C6D880}.error a{color:#8a1f11}.notice a{color:#514721}.success a{color:#264409}.box{padding:1.5em;margin-bottom:1.5em;background:#E5ECF9}hr{background:#ddd;color:#ddd;clear:both;float:none;width:100%;height:.1em;margin:0 0 1.45em;border:none}hr.space{background:#fff;color:#fff;visibility:hidden}.clearfix:after,.container:after{content:"\0020";display:block;height:0;clear:both;visibility:hidden;overflow:hidden}.clearfix,.container{display:block}.clear{clear:both}table.dataTable{width:100%;margin:0 auto;clear:both;border-collapse:separate;border-spacing:0}table.dataTable thead th,table.dataTable tfoot th{font-weight:bold}table.dataTable thead th,table.dataTable thead td{padding:10px 18px;border-bottom:1px solid #111}table.dataTable thead th:active,table.dataTable thead td:active{outline:none}table.dataTable tfoot th,table.dataTable tfoot td{padding:10px 18px 6px 18px;border-top:1px solid #111}table.dataTable thead .sorting,table.dataTable thead .sorting_asc,table.dataTable thead .sorting_desc,table.dataTable thead .sorting_asc_disabled,table.dataTable thead .sorting_desc_disabled{cursor:pointer;*cursor:hand;background-repeat:no-repeat;background-position:center right}table.dataTable thead .sorting{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAQAAADYWf5HAAAAkElEQVQoz7XQMQ5AQBCF4dWQSJxC5wwax1Cq1e7BAdxD5SL%2BTq%2FQCM1oNiJidwox0355mXnG%2FDrEtIQ6azioNZQxI0ykPhTQIwhCR%2BBmBYtlK7kLJYwWCcJA9M4qdrZrd8pPjZWPtOqdRQy320YSV17OatFC4euts6z39GYMKRPCTKY9UnPQ6P%2BGtMRfGtPnBCiqhAeJPmkqAAAAAElFTkSuQmCC)}table.dataTable thead .sorting_asc{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZ0lEQVQ4y2NgGLKgquEuFxBPAGI2ahhWCsS%2FgDibUoO0gPgxEP8H4ttArEyuQYxAPBdqEAxPBImTY5gjEL9DM%2BwTENuQahAvEO9DMwiGdwAxOymGJQLxTyD%2BjgWDxCMZRsEoGAVoAADeemwtPcZI2wAAAABJRU5ErkJggg%3D%3D)}table.dataTable thead .sorting_desc{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZUlEQVQ4y2NgGAWjYBSggaqGu5FA%2FBOIv2PBIPFEUgxjB%2BIdQPwfC94HxLykus4GiD%2BhGfQOiB3J8SojEE9EM2wuSJzcsFMG4ttQgx4DsRalkZENxL%2BAuJQaMcsGxBOAmGvopk8AVz1sLZgg0bsAAAAASUVORK5CYII%3D)}table.dataTable thead .sorting_asc_disabled{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAQAAADYWf5HAAAAW0lEQVQoz2NgoCm4w3Vnwh02wspK7%2Fy6k01Ikdadx3f%2B37l9RxmfIsY7c4GKQHDiHUbcyhzvvIMq%2B3THBpci3jv7oIpAcMcdduzKEu%2F8vPMdDn%2FeiWQYBYMKAAC3ykIEuYQJUgAAAABJRU5ErkJggg%3D%3D)}table.dataTable thead .sorting_desc_disabled{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAQAAADYWf5HAAAAWUlEQVQoz2NgGAWDCtyJvPPzznc4%2FHknEbsy9js77vyHw313eHGZZ3PnE1TRuzuOuK1lvDMRqmzuHUZ87lO%2Bcxuo6PEdLUIeyb7z604pYf%2By3Zlwh4u2YQoAc7ZCBHH4jigAAAAASUVORK5CYII%3D)}table.dataTable tbody tr{background-color:#ffffff}table.dataTable tbody tr.selected{background-color:#B0BED9}table.dataTable tbody th,table.dataTable tbody td{padding:8px 10px}table.dataTable.row-border tbody th,table.dataTable.row-border tbody td,table.dataTable.display tbody th,table.dataTable.display tbody td{border-top:1px solid #ddd}table.dataTable.row-border tbody tr:first-child th,table.dataTable.row-border tbody tr:first-child td,table.dataTable.display tbody tr:first-child th,table.dataTable.display tbody tr:first-child td{border-top:none}table.dataTable.cell-border tbody th,table.dataTable.cell-border tbody td{border-top:1px solid #ddd;border-right:1px solid #ddd}table.dataTable.cell-border tbody tr th:first-child,table.dataTable.cell-border tbody tr td:first-child{border-left:1px solid #ddd}table.dataTable.cell-border tbody tr:first-child th,table.dataTable.cell-border tbody tr:first-child td{border-top:none}table.dataTable.stripe tbody tr.odd,table.dataTable.display tbody tr.odd{background-color:#f9f9f9}table.dataTable.stripe tbody tr.odd.selected,table.dataTable.display tbody tr.odd.selected{background-color:#acbad4}table.dataTable.hover tbody tr:hover,table.dataTable.display tbody tr:hover{background-color:#f6f6f6}table.dataTable.hover tbody tr:hover.selected,table.dataTable.display tbody tr:hover.selected{background-color:#aab7d1}table.dataTable.order-column tbody tr>.sorting_1,table.dataTable.order-column tbody tr>.sorting_2,table.dataTable.order-column tbody tr>.sorting_3,table.dataTable.display tbody tr>.sorting_1,table.dataTable.display tbody tr>.sorting_2,table.dataTable.display tbody tr>.sorting_3{background-color:#fafafa}table.dataTable.order-column tbody tr.selected>.sorting_1,table.dataTable.order-column tbody tr.selected>.sorting_2,table.dataTable.order-column tbody tr.selected>.sorting_3,table.dataTable.display tbody tr.selected>.sorting_1,table.dataTable.display tbody tr.selected>.sorting_2,table.dataTable.display tbody tr.selected>.sorting_3{background-color:#acbad5}table.dataTable.display tbody tr.odd>.sorting_1,table.dataTable.order-column.stripe tbody tr.odd>.sorting_1{background-color:#f1f1f1}table.dataTable.display tbody tr.odd>.sorting_2,table.dataTable.order-column.stripe tbody tr.odd>.sorting_2{background-color:#f3f3f3}table.dataTable.display tbody tr.odd>.sorting_3,table.dataTable.order-column.stripe tbody tr.odd>.sorting_3{background-color:whitesmoke}table.dataTable.display tbody tr.odd.selected>.sorting_1,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_1{background-color:#a6b4cd}table.dataTable.display tbody tr.odd.selected>.sorting_2,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_2{background-color:#a8b5cf}table.dataTable.display tbody tr.odd.selected>.sorting_3,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_3{background-color:#a9b7d1}table.dataTable.display tbody tr.even>.sorting_1,table.dataTable.order-column.stripe tbody tr.even>.sorting_1{background-color:#fafafa}table.dataTable.display tbody tr.even>.sorting_2,table.dataTable.order-column.stripe tbody tr.even>.sorting_2{background-color:#fcfcfc}table.dataTable.display tbody tr.even>.sorting_3,table.dataTable.order-column.stripe tbody tr.even>.sorting_3{background-color:#fefefe}table.dataTable.display tbody tr.even.selected>.sorting_1,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_1{background-color:#acbad5}table.dataTable.display tbody tr.even.selected>.sorting_2,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_2{background-color:#aebcd6}table.dataTable.display tbody tr.even.selected>.sorting_3,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_3{background-color:#afbdd8}table.dataTable.display tbody tr:hover>.sorting_1,table.dataTable.order-column.hover tbody tr:hover>.sorting_1{background-color:#eaeaea}table.dataTable.display tbody tr:hover>.sorting_2,table.dataTable.order-column.hover tbody tr:hover>.sorting_2{background-color:#ececec}table.dataTable.display tbody tr:hover>.sorting_3,table.dataTable.order-column.hover tbody tr:hover>.sorting_3{background-color:#efefef}table.dataTable.display tbody tr:hover.selected>.sorting_1,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_1{background-color:#a2aec7}table.dataTable.display tbody tr:hover.selected>.sorting_2,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_2{background-color:#a3b0c9}table.dataTable.display tbody tr:hover.selected>.sorting_3,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_3{background-color:#a5b2cb}table.dataTable.no-footer{border-bottom:1px solid #111}table.dataTable.nowrap th,table.dataTable.nowrap td{white-space:nowrap}table.dataTable.compact thead th,table.dataTable.compact thead td{padding:4px 17px 4px 4px}table.dataTable.compact tfoot th,table.dataTable.compact tfoot td{padding:4px}table.dataTable.compact tbody th,table.dataTable.compact tbody td{padding:4px}table.dataTable th.dt-left,table.dataTable td.dt-left{text-align:left}table.dataTable th.dt-center,table.dataTable td.dt-center,table.dataTable td.dataTables_empty{text-align:center}table.dataTable th.dt-right,table.dataTable td.dt-right{text-align:right}table.dataTable th.dt-justify,table.dataTable td.dt-justify{text-align:justify}table.dataTable th.dt-nowrap,table.dataTable td.dt-nowrap{white-space:nowrap}table.dataTable thead th.dt-head-left,table.dataTable thead td.dt-head-left,table.dataTable tfoot th.dt-head-left,table.dataTable tfoot td.dt-head-left{text-align:left}table.dataTable thead th.dt-head-center,table.dataTable thead td.dt-head-center,table.dataTable tfoot th.dt-head-center,table.dataTable tfoot td.dt-head-center{text-align:center}table.dataTable thead th.dt-head-right,table.dataTable thead td.dt-head-right,table.dataTable tfoot th.dt-head-right,table.dataTable tfoot td.dt-head-right{text-align:right}table.dataTable thead th.dt-head-justify,table.dataTable thead td.dt-head-justify,table.dataTable tfoot th.dt-head-justify,table.dataTable tfoot td.dt-head-justify{text-align:justify}table.dataTable thead th.dt-head-nowrap,table.dataTable thead td.dt-head-nowrap,table.dataTable tfoot th.dt-head-nowrap,table.dataTable tfoot td.dt-head-nowrap{white-space:nowrap}table.dataTable tbody th.dt-body-left,table.dataTable tbody td.dt-body-left{text-align:left}table.dataTable tbody th.dt-body-center,table.dataTable tbody td.dt-body-center{text-align:center}table.dataTable tbody th.dt-body-right,table.dataTable tbody td.dt-body-right{text-align:right}table.dataTable tbody th.dt-body-justify,table.dataTable tbody td.dt-body-justify{text-align:justify}table.dataTable tbody th.dt-body-nowrap,table.dataTable tbody td.dt-body-nowrap{white-space:nowrap}table.dataTable,table.dataTable th,table.dataTable td{box-sizing:content-box}.dataTables_wrapper{position:relative;clear:both;*zoom:1;zoom:1}.dataTables_wrapper .dataTables_length{float:left}.dataTables_wrapper .dataTables_filter{float:right;text-align:right}.dataTables_wrapper .dataTables_filter input{margin-left:0.5em}.dataTables_wrapper .dataTables_info{clear:both;float:left;padding-top:0.755em}.dataTables_wrapper .dataTables_paginate{float:right;text-align:right;padding-top:0.25em}.dataTables_wrapper .dataTables_paginate .paginate_button{box-sizing:border-box;display:inline-block;min-width:1.5em;padding:0.5em 1em;margin-left:2px;text-align:center;text-decoration:none !important;cursor:pointer;*cursor:hand;color:#333 !important;border:1px solid transparent;border-radius:2px}.dataTables_wrapper .dataTables_paginate .paginate_button.current,.dataTables_wrapper .dataTables_paginate .paginate_button.current:hover{color:#333 !important;border:1px solid #979797;background-color:white;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #fff), color-stop(100%, #dcdcdc));background:-webkit-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:-moz-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:-ms-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:-o-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:linear-gradient(to bottom, #fff 0%, #dcdcdc 100%)}.dataTables_wrapper .dataTables_paginate .paginate_button.disabled,.dataTables_wrapper .dataTables_paginate .paginate_button.disabled:hover,.dataTables_wrapper .dataTables_paginate .paginate_button.disabled:active{cursor:default;color:#666 !important;border:1px solid transparent;background:transparent;box-shadow:none}.dataTables_wrapper .dataTables_paginate .paginate_button:hover{color:white !important;border:1px solid #111;background-color:#585858;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #585858), color-stop(100%, #111));background:-webkit-linear-gradient(top, #585858 0%, #111 100%);background:-moz-linear-gradient(top, #585858 0%, #111 100%);background:-ms-linear-gradient(top, #585858 0%, #111 100%);background:-o-linear-gradient(top, #585858 0%, #111 100%);background:linear-gradient(to bottom, #585858 0%, #111 100%)}.dataTables_wrapper .dataTables_paginate .paginate_button:active{outline:none;background-color:#2b2b2b;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #2b2b2b), color-stop(100%, #0c0c0c));background:-webkit-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-moz-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-ms-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-o-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:linear-gradient(to bottom, #2b2b2b 0%, #0c0c0c 100%);box-shadow:inset 0 0 3px #111}.dataTables_wrapper .dataTables_paginate .ellipsis{padding:0 1em}.dataTables_wrapper .dataTables_processing{position:absolute;top:50%;left:50%;width:100%;height:40px;margin-left:-50%;margin-top:-25px;padding-top:20px;text-align:center;font-size:1.2em;background-color:white;background:-webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255,255,255,0)), color-stop(25%, rgba(255,255,255,0.9)), color-stop(75%, rgba(255,255,255,0.9)), color-stop(100%, rgba(255,255,255,0)));background:-webkit-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:-moz-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:-ms-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:-o-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:linear-gradient(to right, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%)}.dataTables_wrapper .dataTables_length,.dataTables_wrapper .dataTables_filter,.dataTables_wrapper .dataTables_info,.dataTables_wrapper .dataTables_processing,.dataTables_wrapper .dataTables_paginate{color:#333}.dataTables_wrapper .dataTables_scroll{clear:both}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody{*margin-top:-1px;-webkit-overflow-scrolling:touch}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>th,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>td,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>th,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>td{vertical-align:middle}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>th>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>td>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>th>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>td>div.dataTables_sizing{height:0;overflow:hidden;margin:0 !important;padding:0 !important}.dataTables_wrapper.no-footer .dataTables_scrollBody{border-bottom:1px solid #111}.dataTables_wrapper.no-footer div.dataTables_scrollHead table.dataTable,.dataTables_wrapper.no-footer div.dataTables_scrollBody>table{border-bottom:none}.dataTables_wrapper:after{visibility:hidden;display:block;content:"";clear:both;height:0}@media screen and (max-width: 767px){.dataTables_wrapper .dataTables_info,.dataTables_wrapper .dataTables_paginate{float:none;text-align:center}.dataTables_wrapper .dataTables_paginate{margin-top:0.5em}}@media screen and (max-width: 640px){.dataTables_wrapper .dataTables_length,.dataTables_wrapper .dataTables_filter{float:none;text-align:center}.dataTables_wrapper .dataTables_filter{margin-top:0.5em}}pre .comment,pre .template_comment,pre .diff .header,pre .javadoc{color:#998;font-style:italic}pre .keyword,pre .css .rule .keyword,pre .winutils,pre .javascript .title,pre .lisp .title{color:#000;font-weight:bold}pre .number,pre .hexcolor{color:#458}pre .string,pre .tag .value,pre .phpdoc,pre .tex .formula{color:#d14}pre .subst{color:#712}pre .constant,pre .title,pre .id{color:#900;font-weight:bold}pre .javascript .title,pre .lisp .title,pre .subst{font-weight:normal}pre .class .title,pre .haskell .label,pre .tex .command{color:#458;font-weight:bold}pre .tag,pre .tag .title,pre .rules .property,pre .django .tag .keyword{color:#000080;font-weight:normal}pre .attribute,pre .variable,pre .instancevar,pre .lisp .body{color:#008080}pre .regexp{color:#009926}pre .class{color:#458;font-weight:bold}pre .symbol,pre .ruby .symbol .string,pre .ruby .symbol .keyword,pre .ruby .symbol .keymethods,pre .lisp .keyword,pre .tex .special,pre .input_number{color:#990073}pre .builtin,pre .built_in,pre .lisp .title{color:#0086b3}pre .preprocessor,pre .pi,pre .doctype,pre .shebang,pre .cdata{color:#999;font-weight:bold}pre .deletion{background:#fdd}pre .addition{background:#dfd}pre .diff .change{background:#0086b3}pre .chunk{color:#aaa}pre .tex .formula{opacity:0.5}.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{position:absolute;left:-99999999px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:after{content:".";display:block;height:0;clear:both;visibility:hidden}.ui-helper-clearfix{display:inline-block}* html .ui-helper-clearfix{height:1%}.ui-helper-clearfix{display:block}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-state-disabled{cursor:default !important}.ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-overlay{position:absolute;top:0;left:0;width:100%;height:100%}.ui-widget{font-family:Verdana,Arial,sans-serif;font-size:1.1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Verdana,Arial,sans-serif;font-size:1em}.ui-widget-content{border:1px solid #aaaaaa;background:#fff url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAABkCAYAAAD0ZHJ6AAAAeUlEQVRoge3OMQHAIBAAsVL%2Fnh8JDDfAkCjImpn5HvbfDpwIVoKVYCVYCVaClWAlWAlWgpVgJVgJVoKVYCVYCVaClWAlWAlWgpVgJVgJVoKVYCVYCVaClWAlWAlWgpVgJVgJVoKVYCVYCVaClWAlWAlWgpVgJVgJVhtqiwTEKTLXTgAAAABJRU5ErkJggg%3D%3D) 50% 50% repeat-x;color:#222222}.ui-widget-content a{color:#222222}.ui-widget-header{border:1px solid #aaaaaa;background:#ccc url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAABkCAYAAABHLFpgAAAALElEQVQYlWN49OjRfyYGBgaGIUT8%2F%2F8fSqBx0Yh%2F%2F%2F4RL8vAwAAVQ2MNOwIAl6g6KkOJwk8AAAAASUVORK5CYII%3D) 50% 50% repeat-x;color:#222222;font-weight:bold}.ui-widget-header a{color:#222222}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default{border:1px solid #d3d3d3;background:#e6e6e6 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAGQCAYAAABvWArbAAAANUlEQVQ4je3LMQoAIBADwb38%2F6t5wFXaWAiCtUiaYZvF9hBACOFbuntVVe11B0CSjjeE8BwThQIJ8dhEl0YAAAAASUVORK5CYII%3D) 50% 50% repeat-x;font-weight:normal;color:#555555}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited{color:#555555;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus{border:1px solid #999999;background:#dadada url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAGQCAYAAABvWArbAAAANklEQVQ4jWN48uTJfyYGBgaGUWKUGCWGLfHt2zcoi5GREYNgYmJCZiG42IiB98woMUqMEtgIAMdjCdyg%2BeEBAAAAAElFTkSuQmCC) 50% 50% repeat-x;font-weight:normal;color:#212121}.ui-state-hover a,.ui-state-hover a:hover{color:#212121;text-decoration:none}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active{border:1px solid #aaaaaa;background:#fff url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAGQCAYAAABvWArbAAAAMElEQVQ4je3LIQ4AIBTD0O3f%2F8wrFgmKhMy8pKJKwkhSKeVbbGuAPU9f4PIopTxgAeS0DRtI4yK0AAAAAElFTkSuQmCC) 50% 50% repeat-x;font-weight:normal;color:#212121}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#212121;text-decoration:none}.ui-widget :active{outline:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #fcefa1;background:#fbf9ee url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAGQCAYAAABvWArbAAAAP0lEQVQ4je3PMQrAMABC0a%2F3v2zX0mCXNkOgc6C4PARd5DqPGKCU8luS8SbAQhiCQRgJE56kZTfbbP9RSvnkBsWcEAZRWcgqAAAAAElFTkSuQmCC) 50% 50% repeat-x;color:#363636}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#363636}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #cd0a0a;background:#fef1ec url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAGQCAYAAABvWArbAAAAPklEQVQ4je3PMQqAMABD0Z%2Fc%2F7aCIAXjJIhD10LJ8vgZw30eMUApZV%2FGhZNgSTjoLYElY%2FhNMJ%2FS6gullCkPiCIPCr4NiEwAAAAASUVORK5CYII%3D) 50% 50% repeat-x;color:#cd0a0a}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#cd0a0a}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#cd0a0a}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-icon{width:16px;height:16px;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAADwCAMAAADYSUr5AAAA7VBMVEUkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiTww4gUAAAATnRSTlMAGBAyBAhQv4OZLiJUcEBmYBoSzQwgPBZCSEoeWiYwUiyFNIeBw2rJz8c4RBy9uXyrtaWNqa2zKP2fJO8KBgKPo2KVoa9s351GPm5%2BkWho0kj9AAAPhUlEQVR4nO1djWLbthEGyUiq5YSSLXtp7FpLOmfzkmxr126tmi2p03RJ1%2FXe%2F3EGgARxPyAgRbIk2%2FhkSz4CJO4%2BHsE7AJSVysjI2AMUUOxahZ2iANhzBtZWr4BoIRSYAVN5u4QwDwQDRbcwfUi5KS3wFuDmFnQLa4Dtb%2F%2FcqktwD5QEFFwfUs7PoCCA7y4bEJVFizcIob8KmhAplwwqVjt%2B9FBl3uINQniwEiryEyw9JHqGpQdEFNi%2BB4QQ7QOiHhysIPoAxUqxvdvvA9K42bsAv4S2fxfYOe57IJSRkZGRkZGxx7jxSHDHcRBXQMTyIjInBgHwBJ%2FbEx8PEANC%2BuhbpSSggCBAVODVabpI1S%2Fk4WLZpTn6NpMhoX9Y40hxYERFpMcqUs4AloCtDQdID1YhnyXZ2hLjAYWiO9Dy1PDB7tPhIqLx%2BuMB8grZaR%2BQxl2%2FC2RkZGRkZGRk7A7rBf7J0DR5%2FLUTjzUPIPSPGvQJiVJiB7kcQCiUOJrcFNtDZIf2xarQ3aGvLNxAVIFAabz90BFiBIlycTBhgWwOWCH0FLYHlPqwHaCvcIn2ZbosCevfPTRiFFcgvHukCjWwrc3GrGh1fsAof8EaUReKXkCB4%2FMzFNo97qLpFiKFYv%2FkNR5YQxQbQEofkZ2OuEOHqqT6gFTpru8CN7x%2F%2BjaZkZGRkZGRcV%2Bx%2FrLUNcMMqUAscgnFocmpqkTzqymwVAPxfJ5PnIUUQOUKT04tEdWZyv3JCQSn96WS4pD97QfyW25A7NhSAbyhmVj0FEltA4vdiygBibXhoUYgykCUP7HwPTDeEqAIcHVMkZg7Zx4k0uFANs63hPQXCoRLAwdgGsr9Az7Qv7sgQGgg1aPl%2FBJLExBWgG4RFRLFImGmIquPC%2FklEGyCG0AuAXaJJC%2BB8FVe9NYQDEcXB8g6AQcjYJ1goJIggHWCrFR0S6kRHN5%2B4BzFi8NaoN35NRxUvL%2BJJdZr7PV4wK6fj8nIyMjIyNhr3OxdXAYq7FHZwB6bDSzSh4sF0utChqo0NAvaT1hLzXwFinmCzmeDucEQK18TTaQoFgP7bNC%2BRZ4OT4T6gQogDFYk%2B1QxQlj19QGSAWKiLYp8P0Ag1Gbz1ULfWHLg9iUnQNK5QQJcukm04blKLH2GgEJCY%2BHzXAZWCvHKco3Bp6MIaCjSXXRJyOxeqhnzEaF93MfFGW%2FO16ZvDL5TM4MJIjujz%2FcHypkQuuzRwWJ93BKdIt%2BwCRAPl9kpe2Ikkb2mFgGlxh%2Fi40d3EHfdvoyMjIyMu43ylt%2FIAmGHnN5iIt7wKfbv01RAcJqFRl9lcjYQSnbQqKgC4fYOwSJt6N6trE0twZ9kN%2FPqNpTQeICvr4TLsDYC06U7BMjshS%2Bv1%2FaT7IwQYD5LcgRQXMT2FrBfBLjZ6151jDElk9tPFfpUgk2yregusX25BJbwAFEfM%2BYI6vGAti4bTtizB%2BTjfQCrERyhKb2X8D6A9wX75P4t4neBYJeP6pdhg%2FgQl8MWvytzeSTjgOQBynQdh%2FiXKdxOrGJ%2FRkZGRsb9QmXihGr5%2Bg8GGg9uTh%2BKoVZuNIzV%2BCwRucFBEyr1mVjx4irOxwM1BhirB6Q%2B2eNQi4eqR%2BaF6mELtoMzCR7V9RAFe%2FZvQogNiyY8FPSUTFsLp8TeTmMui5mtw7bcaT0Yw2AA4wFRQIlkgq%2B1DQrNhkmoxS5Jq%2Bu6bMAIGRECEANgXHTgWzwgBOhDH2l0oTQ4D8D5NMktBgNywAEMjo8rwATMZrPY7JGxBoJCkIBDQiAY09EGTUiBCWkUpISfGPR5AAwBfZiG2z7Ayc1yeKTxid39xBNwfHr4O0LA48ePFTvhYrF1r4tyAoz9n2MCqEuBtp%2F6GDR0oAYfG%2FR6wJExHYZHfhygsv7fEWCOj4bYmsP5A%2BpL4MkTfAnMlD4F%2Br3bobKvTyTA2P%2Fw7PN%2BAgq2QW8piqMCpTBwenoKvX0AHGkGtP2YAPvTEWA7QUTAudn7%2FNxtOG46wWNmDtpBEkBzN7rBEvAFHp%2BYTB%2Fq97qPAN4gHFqgBi8uLsC7qPCA6mg41G%2F%2BErByPwEXDdoNxRhOx%2BM5jPEzQugS0ht%2Bb1%2FY3gEnYMAIAOIBE29%2FhIDucE8tmMsNOgK4B1RHFu4UCRlMHzv0xzcajcfdXWDs2h8TArBCkoDUJYDLmz6w7ip3BFS0ve5wTRwAn6keMA9I3QYbfSZ0DKbyt%2B7OXjGI1idPcfNyAyfAMlCrzaGqphYrxHocLHRJVycnfGUcbtT%2BjIyMjIw9x7Nn8fJSzG0TmFtO8rZT%2BXT3S3ub%2BtKJbbLd5diTVp50%2BzahyeHSslJ%2FYPrU0fuazrZO2CZ92%2FZCCVXlGRiZKPJyPPRxyIFWeXLQBXJBKiq%2F3divEAN6ZwM200Qjm7EJBZeWm%2FPRWVCbYK7s7u2l4XaCz%2BlzgOfMfhMonXr7TWzeZb98dbgIzBT8Ub8eYYUqfZ4rVJ%2FMDbIDgPqTulJ%2FxvntWAtjIisqnwxOkGz0n077FARoY79GdA6HPE4rOy196NiMWHTZlSSApcOgXpy%2FfHV2joaNKu3ffsAnRcBf4K%2F6NcIG6tIxk3HyoXPjASqfUgXbYN5PzpL2njkR9QMjeDTVHDTCgRuxOegjoO0FvKzP%2Ft%2FgmVdI24%2BG7NIe8JX6Wv3dDyldMA%2B4YB5wwTygtd%2BdwRqaTqrLb1l73zTSN52CNpnHuQOYPsDblybgxfkXh%2FoVtr%2BN1DEBJdhRJyd%2FBd%2Fq1z%2BcbNrD17iVKyajcnv9arhOkRPgsruuD6DmNPwpDNrLw2CoTgHni4yALr0L29%2BtiKAEIPn868ejx%2F%2F8rpWP3OEOl5On9OwpcQm0MhafP%2Fey8f1uvDNIgGLQG8z4YO99ENgg95etwv4uYJYY8fUGHYH6j6fscHFZMftlAl9i%2B9XL73X3N%2Fn%2BZStOzfVfRvYXhrbdKOpEgVQTg%2FwsDuDD3kwOfQNMTJ5y%2B%2FltUDWLunyxnRF46IqlBzGMY4X7inggREFioIyMjIyMHWCIB6ZNKAcXseo3vLTQTkVE7348dlwJJSz0%2BwLfmi8BhZqfw3D4ww%2FwHVLnEd5%2FfgYvXsDZ3MlsvYUbbnDjDZ3MN3TJG4%2BbxjAaDl8TBri9qxEw1ccao2wTNAMLHo2f%2BsjrXwb%2F9qHoYqgPMBXJTVfOpmrZH23y6uvo0LHSyY6fHGwKfHJlAuMFvObjDYrIqxBgQi20h7Hd%2FnYVLmno%2BeaNUm%2FeeH2GCuopntnhBJAlI2AHo9CCh1I1QxUdAbqqGY9BBLwyc3W4wYVhvY8A4BoIc1l5M7vnPWphZW9%2FSes3n37y9a0uGqFwFQZsQQbd386DogpgEk%2BdzynsAZMJXq8%2Bns9NeukJ0PYrNATGGefJQlhkLo7DTXr%2By3bNiOsDvrXTz%2FC2q1DXZH84iRNwrP88Nj%2Bu2DjYEE6RBxD9Knj16ujVHC67A7422o02RwD3gB%2Bt7EblWvu9geOFxSnd3ROmT%2BnJyQkhoPlsxVONc%2F3TEdBos%2BjtA%2BZzcwHgTvD1cDjaYCcItA8w9i88A8b%2BmqSjc6Pvqd998QguEQPmQMeo23ODN86%2Bp0%2Fbn1buBkT6%2BoBhNZ%2FPYY4ZAHYb3PRd4LkZmPX68NRtMZn4ASvdA%2Bqf0jMA5MP9eeg28Nug9QiLnj5A33U1MAES6xHAUNpz%2F9zFAYE1gqQDMT3G6xI9pwdw%2FaIgKoHCS1YGlRnSq9yCjdXjgN3j%2BN27YyROHxmuNAeNKPpYuXIyIyMjYy0M8eros59MF%2FPT2c602T7eA7zvhJ9dr%2FvzDjXaLp4Yc5%2B0wllzxzHv3gdmMMM7%2FCcQzKgVBqYTmFn%2BZ%2BmKm8J7k0A5F%2FjgCfjQ1WBhQyiOqD0lYuqBb%2BAyzMw9Ha2G3m6c8qQx%2BAlqnIceQp%2BSb6i9UyQWbhr54%2BAjnZ0VzW2TAN0DmBT6PWmc6jDBE2PK2u%2BnF43dyP7Q0t1pOcX2fdRvH0mF2Q4JqN35rnHjVIeaXfIAVyUuw%2FaHCCiJy9iF5l1621zweI8KZrPZ9iJdb7DXJ3US0OSrtZ10imt7wHY7QesAzUMz1oZ3noB3qFJ%2FH18j97FYuw8QDN4oeKf30osvcSW2ExLo%2BVcbuAuo%2FsUIm8fMG9xocO3Ea19J9gFYivnHJ2KnyfovZlgW3v6ySx32abQiIyMjIyPjhlFDTLxpwIgFMnTp6A3g4IDKNY%2BstkwAMAoIAbasxBXqUWneSAWTMjt50lTqT29rFjvXohjsDNm2YPXDFlICmrJOZ3t6tHm8AiEAl0sCeLIIorIRt%2BcFbew%2FQRsoAXb4o1XSfoywzm0FTMAoYBNvLyFu8v8HpLBtD1iKgC17wHb7AI6d9wFbvguAIGTHd4E9wG7jgIyMjIyM%2B434c2R3HeV%2FFfx6jtZu6ijl8h59T655jhR%2BrdHzDOP6beABCheb8O8%2FWFXeOyzgf5oAhVYnKxP7CwaAf1afJu8bSrhS6tdaXeGnrRenOqOlz9d6QwYnA%2F3TLd%2BGE7qe3chA5YF5DfY0vK3adfOX%2FgyNp2BW25MHdxAB9qvRiiP3%2FXpQQFGYDU4%2BMi%2F%2F%2FXumXG8pjvaUAOsBGlf4jJt%2BYYEzeEzAdw06F19R3juM7D1wita86GR0CKfDHgLuXCc4Bri6vMLdfjMc4VNSUNsdodo2xu%2F1%2BXl%2FK5%2Baz8jIyMhYG%2Fz5gJTMF1GtKq%2Fa3rpyCvz5gJTMl9GtKq%2Fa3rpyCmfQ4WwZmS%2BkXFVetb115ST48wEf%2FAGcfG1iw%2BtWbpbS2vJ3nQxcVr3lH3z5h972FUTLzYpOVk7l5hD%2BeYcYwDcAnewOotrZ4OtrPDucqi%2FLRX0%2FRR4qx7Nn4U8g%2BqjffvuN6Gf%2BnC85vwauHjaYyubqvWYKY4VEfSUMitdnBCT1Ue63R5439m%2BOgCn6DroAAaHPVQxKth%2FwkJgHmG8bmQMsT0D6EjDfvhVRKO3ywOQUgRA7nmL1uawZmHf1k%2BDPBwQ6NdcJ%2Bk6Md1LA5f5ONdhJ8vZ5J0vLHT99srkGOjmJbd%2FG1r2Nriqnse1AZt1AalU5jW2HsuuG0qvKGRkZGRkZGRG0gcONyXsP9v8D0%2FIdJADiBNiXl3327WRGgOL%2F9HC%2F0XwlIURkRhC4tz6Z%2Ffu7fUf2gHvfB9z3u0BGRkZGRkbGplHcnkgguQoSqtUXuhbs%2FwPtMwqV0HUJAvj5vk32b8IDuL23yn7qAXZ5u32hbRX7d3o82Df1FZXvbh9QOfhyxldr%2F%2B3xgXU9oKmvsHyr7F%2FXA269%2FeveBXrsv7N9QALe%2FtvjA0kPWAXGbvebkbHn%2BD%2FJ5nMcHzx1UAAAAABJRU5ErkJggg%3D%3D)}.ui-widget-content .ui-icon{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAADwCAMAAADYSUr5AAAA7VBMVEUkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiTww4gUAAAATnRSTlMAGBAyBAhQv4OZLiJUcEBmYBoSzQwgPBZCSEoeWiYwUiyFNIeBw2rJz8c4RBy9uXyrtaWNqa2zKP2fJO8KBgKPo2KVoa9s351GPm5%2BkWho0kj9AAAPhUlEQVR4nO1djWLbthEGyUiq5YSSLXtp7FpLOmfzkmxr126tmi2p03RJ1%2FXe%2F3EGgARxPyAgRbIk2%2FhkSz4CJO4%2BHsE7AJSVysjI2AMUUOxahZ2iANhzBtZWr4BoIRSYAVN5u4QwDwQDRbcwfUi5KS3wFuDmFnQLa4Dtb%2F%2FcqktwD5QEFFwfUs7PoCCA7y4bEJVFizcIob8KmhAplwwqVjt%2B9FBl3uINQniwEiryEyw9JHqGpQdEFNi%2BB4QQ7QOiHhysIPoAxUqxvdvvA9K42bsAv4S2fxfYOe57IJSRkZGRkZGxx7jxSHDHcRBXQMTyIjInBgHwBJ%2FbEx8PEANC%2BuhbpSSggCBAVODVabpI1S%2Fk4WLZpTn6NpMhoX9Y40hxYERFpMcqUs4AloCtDQdID1YhnyXZ2hLjAYWiO9Dy1PDB7tPhIqLx%2BuMB8grZaR%2BQxl2%2FC2RkZGRkZGRk7A7rBf7J0DR5%2FLUTjzUPIPSPGvQJiVJiB7kcQCiUOJrcFNtDZIf2xarQ3aGvLNxAVIFAabz90BFiBIlycTBhgWwOWCH0FLYHlPqwHaCvcIn2ZbosCevfPTRiFFcgvHukCjWwrc3GrGh1fsAof8EaUReKXkCB4%2FMzFNo97qLpFiKFYv%2FkNR5YQxQbQEofkZ2OuEOHqqT6gFTpru8CN7x%2F%2BjaZkZGRkZGRcV%2Bx%2FrLUNcMMqUAscgnFocmpqkTzqymwVAPxfJ5PnIUUQOUKT04tEdWZyv3JCQSn96WS4pD97QfyW25A7NhSAbyhmVj0FEltA4vdiygBibXhoUYgykCUP7HwPTDeEqAIcHVMkZg7Zx4k0uFANs63hPQXCoRLAwdgGsr9Az7Qv7sgQGgg1aPl%2FBJLExBWgG4RFRLFImGmIquPC%2FklEGyCG0AuAXaJJC%2BB8FVe9NYQDEcXB8g6AQcjYJ1goJIggHWCrFR0S6kRHN5%2B4BzFi8NaoN35NRxUvL%2BJJdZr7PV4wK6fj8nIyMjIyNhr3OxdXAYq7FHZwB6bDSzSh4sF0utChqo0NAvaT1hLzXwFinmCzmeDucEQK18TTaQoFgP7bNC%2BRZ4OT4T6gQogDFYk%2B1QxQlj19QGSAWKiLYp8P0Ag1Gbz1ULfWHLg9iUnQNK5QQJcukm04blKLH2GgEJCY%2BHzXAZWCvHKco3Bp6MIaCjSXXRJyOxeqhnzEaF93MfFGW%2FO16ZvDL5TM4MJIjujz%2FcHypkQuuzRwWJ93BKdIt%2BwCRAPl9kpe2Ikkb2mFgGlxh%2Fi40d3EHfdvoyMjIyMu43ylt%2FIAmGHnN5iIt7wKfbv01RAcJqFRl9lcjYQSnbQqKgC4fYOwSJt6N6trE0twZ9kN%2FPqNpTQeICvr4TLsDYC06U7BMjshS%2Bv1%2FaT7IwQYD5LcgRQXMT2FrBfBLjZ6151jDElk9tPFfpUgk2yregusX25BJbwAFEfM%2BYI6vGAti4bTtizB%2BTjfQCrERyhKb2X8D6A9wX75P4t4neBYJeP6pdhg%2FgQl8MWvytzeSTjgOQBynQdh%2FiXKdxOrGJ%2FRkZGRsb9QmXihGr5%2Bg8GGg9uTh%2BKoVZuNIzV%2BCwRucFBEyr1mVjx4irOxwM1BhirB6Q%2B2eNQi4eqR%2BaF6mELtoMzCR7V9RAFe%2FZvQogNiyY8FPSUTFsLp8TeTmMui5mtw7bcaT0Yw2AA4wFRQIlkgq%2B1DQrNhkmoxS5Jq%2Bu6bMAIGRECEANgXHTgWzwgBOhDH2l0oTQ4D8D5NMktBgNywAEMjo8rwATMZrPY7JGxBoJCkIBDQiAY09EGTUiBCWkUpISfGPR5AAwBfZiG2z7Ayc1yeKTxid39xBNwfHr4O0LA48ePFTvhYrF1r4tyAoz9n2MCqEuBtp%2F6GDR0oAYfG%2FR6wJExHYZHfhygsv7fEWCOj4bYmsP5A%2BpL4MkTfAnMlD4F%2Br3bobKvTyTA2P%2Fw7PN%2BAgq2QW8piqMCpTBwenoKvX0AHGkGtP2YAPvTEWA7QUTAudn7%2FNxtOG46wWNmDtpBEkBzN7rBEvAFHp%2BYTB%2Fq97qPAN4gHFqgBi8uLsC7qPCA6mg41G%2F%2BErByPwEXDdoNxRhOx%2BM5jPEzQugS0ht%2Bb1%2FY3gEnYMAIAOIBE29%2FhIDucE8tmMsNOgK4B1RHFu4UCRlMHzv0xzcajcfdXWDs2h8TArBCkoDUJYDLmz6w7ip3BFS0ve5wTRwAn6keMA9I3QYbfSZ0DKbyt%2B7OXjGI1idPcfNyAyfAMlCrzaGqphYrxHocLHRJVycnfGUcbtT%2BjIyMjIw9x7Nn8fJSzG0TmFtO8rZT%2BXT3S3ub%2BtKJbbLd5diTVp50%2BzahyeHSslJ%2FYPrU0fuazrZO2CZ92%2FZCCVXlGRiZKPJyPPRxyIFWeXLQBXJBKiq%2F3divEAN6ZwM200Qjm7EJBZeWm%2FPRWVCbYK7s7u2l4XaCz%2BlzgOfMfhMonXr7TWzeZb98dbgIzBT8Ub8eYYUqfZ4rVJ%2FMDbIDgPqTulJ%2FxvntWAtjIisqnwxOkGz0n077FARoY79GdA6HPE4rOy196NiMWHTZlSSApcOgXpy%2FfHV2joaNKu3ffsAnRcBf4K%2F6NcIG6tIxk3HyoXPjASqfUgXbYN5PzpL2njkR9QMjeDTVHDTCgRuxOegjoO0FvKzP%2Ft%2FgmVdI24%2BG7NIe8JX6Wv3dDyldMA%2B4YB5wwTygtd%2BdwRqaTqrLb1l73zTSN52CNpnHuQOYPsDblybgxfkXh%2FoVtr%2BN1DEBJdhRJyd%2FBd%2Fq1z%2BcbNrD17iVKyajcnv9arhOkRPgsruuD6DmNPwpDNrLw2CoTgHni4yALr0L29%2BtiKAEIPn868ejx%2F%2F8rpWP3OEOl5On9OwpcQm0MhafP%2Fey8f1uvDNIgGLQG8z4YO99ENgg95etwv4uYJYY8fUGHYH6j6fscHFZMftlAl9i%2B9XL73X3N%2Fn%2BZStOzfVfRvYXhrbdKOpEgVQTg%2FwsDuDD3kwOfQNMTJ5y%2B%2FltUDWLunyxnRF46IqlBzGMY4X7inggREFioIyMjIyMHWCIB6ZNKAcXseo3vLTQTkVE7348dlwJJSz0%2BwLfmi8BhZqfw3D4ww%2FwHVLnEd5%2FfgYvXsDZ3MlsvYUbbnDjDZ3MN3TJG4%2BbxjAaDl8TBri9qxEw1ccao2wTNAMLHo2f%2BsjrXwb%2F9qHoYqgPMBXJTVfOpmrZH23y6uvo0LHSyY6fHGwKfHJlAuMFvObjDYrIqxBgQi20h7Hd%2FnYVLmno%2BeaNUm%2FeeH2GCuopntnhBJAlI2AHo9CCh1I1QxUdAbqqGY9BBLwyc3W4wYVhvY8A4BoIc1l5M7vnPWphZW9%2FSes3n37y9a0uGqFwFQZsQQbd386DogpgEk%2BdzynsAZMJXq8%2Bns9NeukJ0PYrNATGGefJQlhkLo7DTXr%2By3bNiOsDvrXTz%2FC2q1DXZH84iRNwrP88Nj%2Bu2DjYEE6RBxD9Knj16ujVHC67A7422o02RwD3gB%2Bt7EblWvu9geOFxSnd3ROmT%2BnJyQkhoPlsxVONc%2F3TEdBos%2BjtA%2BZzcwHgTvD1cDjaYCcItA8w9i88A8b%2BmqSjc6Pvqd998QguEQPmQMeo23ODN86%2Bp0%2Fbn1buBkT6%2BoBhNZ%2FPYY4ZAHYb3PRd4LkZmPX68NRtMZn4ASvdA%2Bqf0jMA5MP9eeg28Nug9QiLnj5A33U1MAES6xHAUNpz%2F9zFAYE1gqQDMT3G6xI9pwdw%2FaIgKoHCS1YGlRnSq9yCjdXjgN3j%2BN27YyROHxmuNAeNKPpYuXIyIyMjYy0M8eros59MF%2FPT2c602T7eA7zvhJ9dr%2FvzDjXaLp4Yc5%2B0wllzxzHv3gdmMMM7%2FCcQzKgVBqYTmFn%2BZ%2BmKm8J7k0A5F%2FjgCfjQ1WBhQyiOqD0lYuqBb%2BAyzMw9Ha2G3m6c8qQx%2BAlqnIceQp%2BSb6i9UyQWbhr54%2BAjnZ0VzW2TAN0DmBT6PWmc6jDBE2PK2u%2BnF43dyP7Q0t1pOcX2fdRvH0mF2Q4JqN35rnHjVIeaXfIAVyUuw%2FaHCCiJy9iF5l1621zweI8KZrPZ9iJdb7DXJ3US0OSrtZ10imt7wHY7QesAzUMz1oZ3noB3qFJ%2FH18j97FYuw8QDN4oeKf30osvcSW2ExLo%2BVcbuAuo%2FsUIm8fMG9xocO3Ea19J9gFYivnHJ2KnyfovZlgW3v6ySx32abQiIyMjIyPjhlFDTLxpwIgFMnTp6A3g4IDKNY%2BstkwAMAoIAbasxBXqUWneSAWTMjt50lTqT29rFjvXohjsDNm2YPXDFlICmrJOZ3t6tHm8AiEAl0sCeLIIorIRt%2BcFbew%2FQRsoAXb4o1XSfoywzm0FTMAoYBNvLyFu8v8HpLBtD1iKgC17wHb7AI6d9wFbvguAIGTHd4E9wG7jgIyMjIyM%2B434c2R3HeV%2FFfx6jtZu6ijl8h59T655jhR%2BrdHzDOP6beABCheb8O8%2FWFXeOyzgf5oAhVYnKxP7CwaAf1afJu8bSrhS6tdaXeGnrRenOqOlz9d6QwYnA%2F3TLd%2BGE7qe3chA5YF5DfY0vK3adfOX%2FgyNp2BW25MHdxAB9qvRiiP3%2FXpQQFGYDU4%2BMi%2F%2F%2FXumXG8pjvaUAOsBGlf4jJt%2BYYEzeEzAdw06F19R3juM7D1wita86GR0CKfDHgLuXCc4Bri6vMLdfjMc4VNSUNsdodo2xu%2F1%2BXl%2FK5%2Baz8jIyMhYG%2Fz5gJTMF1GtKq%2Fa3rpyCvz5gJTMl9GtKq%2Fa3rpyCmfQ4WwZmS%2BkXFVetb115ST48wEf%2FAGcfG1iw%2BtWbpbS2vJ3nQxcVr3lH3z5h972FUTLzYpOVk7l5hD%2BeYcYwDcAnewOotrZ4OtrPDucqi%2FLRX0%2FRR4qx7Nn4U8g%2BqjffvuN6Gf%2BnC85vwauHjaYyubqvWYKY4VEfSUMitdnBCT1Ue63R5439m%2BOgCn6DroAAaHPVQxKth%2FwkJgHmG8bmQMsT0D6EjDfvhVRKO3ywOQUgRA7nmL1uawZmHf1k%2BDPBwQ6NdcJ%2Bk6Md1LA5f5ONdhJ8vZ5J0vLHT99srkGOjmJbd%2FG1r2Nriqnse1AZt1AalU5jW2HsuuG0qvKGRkZGRkZGRG0gcONyXsP9v8D0%2FIdJADiBNiXl3327WRGgOL%2F9HC%2F0XwlIURkRhC4tz6Z%2Ffu7fUf2gHvfB9z3u0BGRkZGRkbGplHcnkgguQoSqtUXuhbs%2FwPtMwqV0HUJAvj5vk32b8IDuL23yn7qAXZ5u32hbRX7d3o82Df1FZXvbh9QOfhyxldr%2F%2B3xgXU9oKmvsHyr7F%2FXA269%2FeveBXrsv7N9QALe%2FtvjA0kPWAXGbvebkbHn%2BD%2FJ5nMcHzx1UAAAAABJRU5ErkJggg%3D%3D)}.ui-widget-header .ui-icon{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAADwCAMAAADYSUr5AAAA7VBMVEUkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiQkIiTww4gUAAAATnRSTlMAGBAyBAhQv4OZLiJUcEBmYBoSzQwgPBZCSEoeWiYwUiyFNIeBw2rJz8c4RBy9uXyrtaWNqa2zKP2fJO8KBgKPo2KVoa9s351GPm5%2BkWho0kj9AAAPhUlEQVR4nO1djWLbthEGyUiq5YSSLXtp7FpLOmfzkmxr126tmi2p03RJ1%2FXe%2F3EGgARxPyAgRbIk2%2FhkSz4CJO4%2BHsE7AJSVysjI2AMUUOxahZ2iANhzBtZWr4BoIRSYAVN5u4QwDwQDRbcwfUi5KS3wFuDmFnQLa4Dtb%2F%2FcqktwD5QEFFwfUs7PoCCA7y4bEJVFizcIob8KmhAplwwqVjt%2B9FBl3uINQniwEiryEyw9JHqGpQdEFNi%2BB4QQ7QOiHhysIPoAxUqxvdvvA9K42bsAv4S2fxfYOe57IJSRkZGRkZGxx7jxSHDHcRBXQMTyIjInBgHwBJ%2FbEx8PEANC%2BuhbpSSggCBAVODVabpI1S%2Fk4WLZpTn6NpMhoX9Y40hxYERFpMcqUs4AloCtDQdID1YhnyXZ2hLjAYWiO9Dy1PDB7tPhIqLx%2BuMB8grZaR%2BQxl2%2FC2RkZGRkZGRk7A7rBf7J0DR5%2FLUTjzUPIPSPGvQJiVJiB7kcQCiUOJrcFNtDZIf2xarQ3aGvLNxAVIFAabz90BFiBIlycTBhgWwOWCH0FLYHlPqwHaCvcIn2ZbosCevfPTRiFFcgvHukCjWwrc3GrGh1fsAof8EaUReKXkCB4%2FMzFNo97qLpFiKFYv%2FkNR5YQxQbQEofkZ2OuEOHqqT6gFTpru8CN7x%2F%2BjaZkZGRkZGRcV%2Bx%2FrLUNcMMqUAscgnFocmpqkTzqymwVAPxfJ5PnIUUQOUKT04tEdWZyv3JCQSn96WS4pD97QfyW25A7NhSAbyhmVj0FEltA4vdiygBibXhoUYgykCUP7HwPTDeEqAIcHVMkZg7Zx4k0uFANs63hPQXCoRLAwdgGsr9Az7Qv7sgQGgg1aPl%2FBJLExBWgG4RFRLFImGmIquPC%2FklEGyCG0AuAXaJJC%2BB8FVe9NYQDEcXB8g6AQcjYJ1goJIggHWCrFR0S6kRHN5%2B4BzFi8NaoN35NRxUvL%2BJJdZr7PV4wK6fj8nIyMjIyNhr3OxdXAYq7FHZwB6bDSzSh4sF0utChqo0NAvaT1hLzXwFinmCzmeDucEQK18TTaQoFgP7bNC%2BRZ4OT4T6gQogDFYk%2B1QxQlj19QGSAWKiLYp8P0Ag1Gbz1ULfWHLg9iUnQNK5QQJcukm04blKLH2GgEJCY%2BHzXAZWCvHKco3Bp6MIaCjSXXRJyOxeqhnzEaF93MfFGW%2FO16ZvDL5TM4MJIjujz%2FcHypkQuuzRwWJ93BKdIt%2BwCRAPl9kpe2Ikkb2mFgGlxh%2Fi40d3EHfdvoyMjIyMu43ylt%2FIAmGHnN5iIt7wKfbv01RAcJqFRl9lcjYQSnbQqKgC4fYOwSJt6N6trE0twZ9kN%2FPqNpTQeICvr4TLsDYC06U7BMjshS%2Bv1%2FaT7IwQYD5LcgRQXMT2FrBfBLjZ6151jDElk9tPFfpUgk2yregusX25BJbwAFEfM%2BYI6vGAti4bTtizB%2BTjfQCrERyhKb2X8D6A9wX75P4t4neBYJeP6pdhg%2FgQl8MWvytzeSTjgOQBynQdh%2FiXKdxOrGJ%2FRkZGRsb9QmXihGr5%2Bg8GGg9uTh%2BKoVZuNIzV%2BCwRucFBEyr1mVjx4irOxwM1BhirB6Q%2B2eNQi4eqR%2BaF6mELtoMzCR7V9RAFe%2FZvQogNiyY8FPSUTFsLp8TeTmMui5mtw7bcaT0Yw2AA4wFRQIlkgq%2B1DQrNhkmoxS5Jq%2Bu6bMAIGRECEANgXHTgWzwgBOhDH2l0oTQ4D8D5NMktBgNywAEMjo8rwATMZrPY7JGxBoJCkIBDQiAY09EGTUiBCWkUpISfGPR5AAwBfZiG2z7Ayc1yeKTxid39xBNwfHr4O0LA48ePFTvhYrF1r4tyAoz9n2MCqEuBtp%2F6GDR0oAYfG%2FR6wJExHYZHfhygsv7fEWCOj4bYmsP5A%2BpL4MkTfAnMlD4F%2Br3bobKvTyTA2P%2Fw7PN%2BAgq2QW8piqMCpTBwenoKvX0AHGkGtP2YAPvTEWA7QUTAudn7%2FNxtOG46wWNmDtpBEkBzN7rBEvAFHp%2BYTB%2Fq97qPAN4gHFqgBi8uLsC7qPCA6mg41G%2F%2BErByPwEXDdoNxRhOx%2BM5jPEzQugS0ht%2Bb1%2FY3gEnYMAIAOIBE29%2FhIDucE8tmMsNOgK4B1RHFu4UCRlMHzv0xzcajcfdXWDs2h8TArBCkoDUJYDLmz6w7ip3BFS0ve5wTRwAn6keMA9I3QYbfSZ0DKbyt%2B7OXjGI1idPcfNyAyfAMlCrzaGqphYrxHocLHRJVycnfGUcbtT%2BjIyMjIw9x7Nn8fJSzG0TmFtO8rZT%2BXT3S3ub%2BtKJbbLd5diTVp50%2BzahyeHSslJ%2FYPrU0fuazrZO2CZ92%2FZCCVXlGRiZKPJyPPRxyIFWeXLQBXJBKiq%2F3divEAN6ZwM200Qjm7EJBZeWm%2FPRWVCbYK7s7u2l4XaCz%2BlzgOfMfhMonXr7TWzeZb98dbgIzBT8Ub8eYYUqfZ4rVJ%2FMDbIDgPqTulJ%2FxvntWAtjIisqnwxOkGz0n077FARoY79GdA6HPE4rOy196NiMWHTZlSSApcOgXpy%2FfHV2joaNKu3ffsAnRcBf4K%2F6NcIG6tIxk3HyoXPjASqfUgXbYN5PzpL2njkR9QMjeDTVHDTCgRuxOegjoO0FvKzP%2Ft%2FgmVdI24%2BG7NIe8JX6Wv3dDyldMA%2B4YB5wwTygtd%2BdwRqaTqrLb1l73zTSN52CNpnHuQOYPsDblybgxfkXh%2FoVtr%2BN1DEBJdhRJyd%2FBd%2Fq1z%2BcbNrD17iVKyajcnv9arhOkRPgsruuD6DmNPwpDNrLw2CoTgHni4yALr0L29%2BtiKAEIPn868ejx%2F%2F8rpWP3OEOl5On9OwpcQm0MhafP%2Fey8f1uvDNIgGLQG8z4YO99ENgg95etwv4uYJYY8fUGHYH6j6fscHFZMftlAl9i%2B9XL73X3N%2Fn%2BZStOzfVfRvYXhrbdKOpEgVQTg%2FwsDuDD3kwOfQNMTJ5y%2B%2FltUDWLunyxnRF46IqlBzGMY4X7inggREFioIyMjIyMHWCIB6ZNKAcXseo3vLTQTkVE7348dlwJJSz0%2BwLfmi8BhZqfw3D4ww%2FwHVLnEd5%2FfgYvXsDZ3MlsvYUbbnDjDZ3MN3TJG4%2BbxjAaDl8TBri9qxEw1ccao2wTNAMLHo2f%2BsjrXwb%2F9qHoYqgPMBXJTVfOpmrZH23y6uvo0LHSyY6fHGwKfHJlAuMFvObjDYrIqxBgQi20h7Hd%2FnYVLmno%2BeaNUm%2FeeH2GCuopntnhBJAlI2AHo9CCh1I1QxUdAbqqGY9BBLwyc3W4wYVhvY8A4BoIc1l5M7vnPWphZW9%2FSes3n37y9a0uGqFwFQZsQQbd386DogpgEk%2BdzynsAZMJXq8%2Bns9NeukJ0PYrNATGGefJQlhkLo7DTXr%2By3bNiOsDvrXTz%2FC2q1DXZH84iRNwrP88Nj%2Bu2DjYEE6RBxD9Knj16ujVHC67A7422o02RwD3gB%2Bt7EblWvu9geOFxSnd3ROmT%2BnJyQkhoPlsxVONc%2F3TEdBos%2BjtA%2BZzcwHgTvD1cDjaYCcItA8w9i88A8b%2BmqSjc6Pvqd998QguEQPmQMeo23ODN86%2Bp0%2Fbn1buBkT6%2BoBhNZ%2FPYY4ZAHYb3PRd4LkZmPX68NRtMZn4ASvdA%2Bqf0jMA5MP9eeg28Nug9QiLnj5A33U1MAES6xHAUNpz%2F9zFAYE1gqQDMT3G6xI9pwdw%2FaIgKoHCS1YGlRnSq9yCjdXjgN3j%2BN27YyROHxmuNAeNKPpYuXIyIyMjYy0M8eros59MF%2FPT2c602T7eA7zvhJ9dr%2FvzDjXaLp4Yc5%2B0wllzxzHv3gdmMMM7%2FCcQzKgVBqYTmFn%2BZ%2BmKm8J7k0A5F%2FjgCfjQ1WBhQyiOqD0lYuqBb%2BAyzMw9Ha2G3m6c8qQx%2BAlqnIceQp%2BSb6i9UyQWbhr54%2BAjnZ0VzW2TAN0DmBT6PWmc6jDBE2PK2u%2BnF43dyP7Q0t1pOcX2fdRvH0mF2Q4JqN35rnHjVIeaXfIAVyUuw%2FaHCCiJy9iF5l1621zweI8KZrPZ9iJdb7DXJ3US0OSrtZ10imt7wHY7QesAzUMz1oZ3noB3qFJ%2FH18j97FYuw8QDN4oeKf30osvcSW2ExLo%2BVcbuAuo%2FsUIm8fMG9xocO3Ea19J9gFYivnHJ2KnyfovZlgW3v6ySx32abQiIyMjIyPjhlFDTLxpwIgFMnTp6A3g4IDKNY%2BstkwAMAoIAbasxBXqUWneSAWTMjt50lTqT29rFjvXohjsDNm2YPXDFlICmrJOZ3t6tHm8AiEAl0sCeLIIorIRt%2BcFbew%2FQRsoAXb4o1XSfoywzm0FTMAoYBNvLyFu8v8HpLBtD1iKgC17wHb7AI6d9wFbvguAIGTHd4E9wG7jgIyMjIyM%2B434c2R3HeV%2FFfx6jtZu6ijl8h59T655jhR%2BrdHzDOP6beABCheb8O8%2FWFXeOyzgf5oAhVYnKxP7CwaAf1afJu8bSrhS6tdaXeGnrRenOqOlz9d6QwYnA%2F3TLd%2BGE7qe3chA5YF5DfY0vK3adfOX%2FgyNp2BW25MHdxAB9qvRiiP3%2FXpQQFGYDU4%2BMi%2F%2F%2FXumXG8pjvaUAOsBGlf4jJt%2BYYEzeEzAdw06F19R3juM7D1wita86GR0CKfDHgLuXCc4Bri6vMLdfjMc4VNSUNsdodo2xu%2F1%2BXl%2FK5%2Baz8jIyMhYG%2Fz5gJTMF1GtKq%2Fa3rpyCvz5gJTMl9GtKq%2Fa3rpyCmfQ4WwZmS%2BkXFVetb115ST48wEf%2FAGcfG1iw%2BtWbpbS2vJ3nQxcVr3lH3z5h972FUTLzYpOVk7l5hD%2BeYcYwDcAnewOotrZ4OtrPDucqi%2FLRX0%2FRR4qx7Nn4U8g%2BqjffvuN6Gf%2BnC85vwauHjaYyubqvWYKY4VEfSUMitdnBCT1Ue63R5439m%2BOgCn6DroAAaHPVQxKth%2FwkJgHmG8bmQMsT0D6EjDfvhVRKO3ywOQUgRA7nmL1uawZmHf1k%2BDPBwQ6NdcJ%2Bk6Md1LA5f5ONdhJ8vZ5J0vLHT99srkGOjmJbd%2FG1r2Nriqnse1AZt1AalU5jW2HsuuG0qvKGRkZGRkZGRG0gcONyXsP9v8D0%2FIdJADiBNiXl3327WRGgOL%2F9HC%2F0XwlIURkRhC4tz6Z%2Ffu7fUf2gHvfB9z3u0BGRkZGRkbGplHcnkgguQoSqtUXuhbs%2FwPtMwqV0HUJAvj5vk32b8IDuL23yn7qAXZ5u32hbRX7d3o82Df1FZXvbh9QOfhyxldr%2F%2B3xgXU9oKmvsHyr7F%2FXA269%2FeveBXrsv7N9QALe%2FtvjA0kPWAXGbvebkbHn%2BD%2FJ5nMcHzx1UAAAAABJRU5ErkJggg%3D%3D)}.ui-state-default .ui-icon{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAADwCAMAAADYSUr5AAAA7VBMVEWMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMioyMiozJxoFPAAAATnRSTlMAGBAyBAhQv4OZLiJUcEBmYBoSzQwgPBZCSEoeWiYwUiyFNIeBw2rJz8c4RBy9uXyrtaWNqa2zKP2fJO8KBgKPo2KVoa9s351GPm5%2BkWho0kj9AAAPhUlEQVR4nO1djWLbthEGyUiq5YSSLXtp7FpLOmfzkmxr126tmi2p03RJ1%2FXe%2F3EGgARxPyAgRbIk2%2FhkSz4CJO4%2BHsE7AJSVysjI2AMUUOxahZ2iANhzBtZWr4BoIRSYAVN5u4QwDwQDRbcwfUi5KS3wFuDmFnQLa4Dtb%2F%2FcqktwD5QEFFwfUs7PoCCA7y4bEJVFizcIob8KmhAplwwqVjt%2B9FBl3uINQniwEiryEyw9JHqGpQdEFNi%2BB4QQ7QOiHhysIPoAxUqxvdvvA9K42bsAv4S2fxfYOe57IJSRkZGRkZGxx7jxSHDHcRBXQMTyIjInBgHwBJ%2FbEx8PEANC%2BuhbpSSggCBAVODVabpI1S%2Fk4WLZpTn6NpMhoX9Y40hxYERFpMcqUs4AloCtDQdID1YhnyXZ2hLjAYWiO9Dy1PDB7tPhIqLx%2BuMB8grZaR%2BQxl2%2FC2RkZGRkZGRk7A7rBf7J0DR5%2FLUTjzUPIPSPGvQJiVJiB7kcQCiUOJrcFNtDZIf2xarQ3aGvLNxAVIFAabz90BFiBIlycTBhgWwOWCH0FLYHlPqwHaCvcIn2ZbosCevfPTRiFFcgvHukCjWwrc3GrGh1fsAof8EaUReKXkCB4%2FMzFNo97qLpFiKFYv%2FkNR5YQxQbQEofkZ2OuEOHqqT6gFTpru8CN7x%2F%2BjaZkZGRkZGRcV%2Bx%2FrLUNcMMqUAscgnFocmpqkTzqymwVAPxfJ5PnIUUQOUKT04tEdWZyv3JCQSn96WS4pD97QfyW25A7NhSAbyhmVj0FEltA4vdiygBibXhoUYgykCUP7HwPTDeEqAIcHVMkZg7Zx4k0uFANs63hPQXCoRLAwdgGsr9Az7Qv7sgQGgg1aPl%2FBJLExBWgG4RFRLFImGmIquPC%2FklEGyCG0AuAXaJJC%2BB8FVe9NYQDEcXB8g6AQcjYJ1goJIggHWCrFR0S6kRHN5%2B4BzFi8NaoN35NRxUvL%2BJJdZr7PV4wK6fj8nIyMjIyNhr3OxdXAYq7FHZwB6bDSzSh4sF0utChqo0NAvaT1hLzXwFinmCzmeDucEQK18TTaQoFgP7bNC%2BRZ4OT4T6gQogDFYk%2B1QxQlj19QGSAWKiLYp8P0Ag1Gbz1ULfWHLg9iUnQNK5QQJcukm04blKLH2GgEJCY%2BHzXAZWCvHKco3Bp6MIaCjSXXRJyOxeqhnzEaF93MfFGW%2FO16ZvDL5TM4MJIjujz%2FcHypkQuuzRwWJ93BKdIt%2BwCRAPl9kpe2Ikkb2mFgGlxh%2Fi40d3EHfdvoyMjIyMu43ylt%2FIAmGHnN5iIt7wKfbv01RAcJqFRl9lcjYQSnbQqKgC4fYOwSJt6N6trE0twZ9kN%2FPqNpTQeICvr4TLsDYC06U7BMjshS%2Bv1%2FaT7IwQYD5LcgRQXMT2FrBfBLjZ6151jDElk9tPFfpUgk2yregusX25BJbwAFEfM%2BYI6vGAti4bTtizB%2BTjfQCrERyhKb2X8D6A9wX75P4t4neBYJeP6pdhg%2FgQl8MWvytzeSTjgOQBynQdh%2FiXKdxOrGJ%2FRkZGRsb9QmXihGr5%2Bg8GGg9uTh%2BKoVZuNIzV%2BCwRucFBEyr1mVjx4irOxwM1BhirB6Q%2B2eNQi4eqR%2BaF6mELtoMzCR7V9RAFe%2FZvQogNiyY8FPSUTFsLp8TeTmMui5mtw7bcaT0Yw2AA4wFRQIlkgq%2B1DQrNhkmoxS5Jq%2Bu6bMAIGRECEANgXHTgWzwgBOhDH2l0oTQ4D8D5NMktBgNywAEMjo8rwATMZrPY7JGxBoJCkIBDQiAY09EGTUiBCWkUpISfGPR5AAwBfZiG2z7Ayc1yeKTxid39xBNwfHr4O0LA48ePFTvhYrF1r4tyAoz9n2MCqEuBtp%2F6GDR0oAYfG%2FR6wJExHYZHfhygsv7fEWCOj4bYmsP5A%2BpL4MkTfAnMlD4F%2Br3bobKvTyTA2P%2Fw7PN%2BAgq2QW8piqMCpTBwenoKvX0AHGkGtP2YAPvTEWA7QUTAudn7%2FNxtOG46wWNmDtpBEkBzN7rBEvAFHp%2BYTB%2Fq97qPAN4gHFqgBi8uLsC7qPCA6mg41G%2F%2BErByPwEXDdoNxRhOx%2BM5jPEzQugS0ht%2Bb1%2FY3gEnYMAIAOIBE29%2FhIDucE8tmMsNOgK4B1RHFu4UCRlMHzv0xzcajcfdXWDs2h8TArBCkoDUJYDLmz6w7ip3BFS0ve5wTRwAn6keMA9I3QYbfSZ0DKbyt%2B7OXjGI1idPcfNyAyfAMlCrzaGqphYrxHocLHRJVycnfGUcbtT%2BjIyMjIw9x7Nn8fJSzG0TmFtO8rZT%2BXT3S3ub%2BtKJbbLd5diTVp50%2BzahyeHSslJ%2FYPrU0fuazrZO2CZ92%2FZCCVXlGRiZKPJyPPRxyIFWeXLQBXJBKiq%2F3divEAN6ZwM200Qjm7EJBZeWm%2FPRWVCbYK7s7u2l4XaCz%2BlzgOfMfhMonXr7TWzeZb98dbgIzBT8Ub8eYYUqfZ4rVJ%2FMDbIDgPqTulJ%2FxvntWAtjIisqnwxOkGz0n077FARoY79GdA6HPE4rOy196NiMWHTZlSSApcOgXpy%2FfHV2joaNKu3ffsAnRcBf4K%2F6NcIG6tIxk3HyoXPjASqfUgXbYN5PzpL2njkR9QMjeDTVHDTCgRuxOegjoO0FvKzP%2Ft%2FgmVdI24%2BG7NIe8JX6Wv3dDyldMA%2B4YB5wwTygtd%2BdwRqaTqrLb1l73zTSN52CNpnHuQOYPsDblybgxfkXh%2FoVtr%2BN1DEBJdhRJyd%2FBd%2Fq1z%2BcbNrD17iVKyajcnv9arhOkRPgsruuD6DmNPwpDNrLw2CoTgHni4yALr0L29%2BtiKAEIPn868ejx%2F%2F8rpWP3OEOl5On9OwpcQm0MhafP%2Fey8f1uvDNIgGLQG8z4YO99ENgg95etwv4uYJYY8fUGHYH6j6fscHFZMftlAl9i%2B9XL73X3N%2Fn%2BZStOzfVfRvYXhrbdKOpEgVQTg%2FwsDuDD3kwOfQNMTJ5y%2B%2FltUDWLunyxnRF46IqlBzGMY4X7inggREFioIyMjIyMHWCIB6ZNKAcXseo3vLTQTkVE7348dlwJJSz0%2BwLfmi8BhZqfw3D4ww%2FwHVLnEd5%2FfgYvXsDZ3MlsvYUbbnDjDZ3MN3TJG4%2BbxjAaDl8TBri9qxEw1ccao2wTNAMLHo2f%2BsjrXwb%2F9qHoYqgPMBXJTVfOpmrZH23y6uvo0LHSyY6fHGwKfHJlAuMFvObjDYrIqxBgQi20h7Hd%2FnYVLmno%2BeaNUm%2FeeH2GCuopntnhBJAlI2AHo9CCh1I1QxUdAbqqGY9BBLwyc3W4wYVhvY8A4BoIc1l5M7vnPWphZW9%2FSes3n37y9a0uGqFwFQZsQQbd386DogpgEk%2BdzynsAZMJXq8%2Bns9NeukJ0PYrNATGGefJQlhkLo7DTXr%2By3bNiOsDvrXTz%2FC2q1DXZH84iRNwrP88Nj%2Bu2DjYEE6RBxD9Knj16ujVHC67A7422o02RwD3gB%2Bt7EblWvu9geOFxSnd3ROmT%2BnJyQkhoPlsxVONc%2F3TEdBos%2BjtA%2BZzcwHgTvD1cDjaYCcItA8w9i88A8b%2BmqSjc6Pvqd998QguEQPmQMeo23ODN86%2Bp0%2Fbn1buBkT6%2BoBhNZ%2FPYY4ZAHYb3PRd4LkZmPX68NRtMZn4ASvdA%2Bqf0jMA5MP9eeg28Nug9QiLnj5A33U1MAES6xHAUNpz%2F9zFAYE1gqQDMT3G6xI9pwdw%2FaIgKoHCS1YGlRnSq9yCjdXjgN3j%2BN27YyROHxmuNAeNKPpYuXIyIyMjYy0M8eros59MF%2FPT2c602T7eA7zvhJ9dr%2FvzDjXaLp4Yc5%2B0wllzxzHv3gdmMMM7%2FCcQzKgVBqYTmFn%2BZ%2BmKm8J7k0A5F%2FjgCfjQ1WBhQyiOqD0lYuqBb%2BAyzMw9Ha2G3m6c8qQx%2BAlqnIceQp%2BSb6i9UyQWbhr54%2BAjnZ0VzW2TAN0DmBT6PWmc6jDBE2PK2u%2BnF43dyP7Q0t1pOcX2fdRvH0mF2Q4JqN35rnHjVIeaXfIAVyUuw%2FaHCCiJy9iF5l1621zweI8KZrPZ9iJdb7DXJ3US0OSrtZ10imt7wHY7QesAzUMz1oZ3noB3qFJ%2FH18j97FYuw8QDN4oeKf30osvcSW2ExLo%2BVcbuAuo%2FsUIm8fMG9xocO3Ea19J9gFYivnHJ2KnyfovZlgW3v6ySx32abQiIyMjIyPjhlFDTLxpwIgFMnTp6A3g4IDKNY%2BstkwAMAoIAbasxBXqUWneSAWTMjt50lTqT29rFjvXohjsDNm2YPXDFlICmrJOZ3t6tHm8AiEAl0sCeLIIorIRt%2BcFbew%2FQRsoAXb4o1XSfoywzm0FTMAoYBNvLyFu8v8HpLBtD1iKgC17wHb7AI6d9wFbvguAIGTHd4E9wG7jgIyMjIyM%2B434c2R3HeV%2FFfx6jtZu6ijl8h59T655jhR%2BrdHzDOP6beABCheb8O8%2FWFXeOyzgf5oAhVYnKxP7CwaAf1afJu8bSrhS6tdaXeGnrRenOqOlz9d6QwYnA%2F3TLd%2BGE7qe3chA5YF5DfY0vK3adfOX%2FgyNp2BW25MHdxAB9qvRiiP3%2FXpQQFGYDU4%2BMi%2F%2F%2FXumXG8pjvaUAOsBGlf4jJt%2BYYEzeEzAdw06F19R3juM7D1wita86GR0CKfDHgLuXCc4Bri6vMLdfjMc4VNSUNsdodo2xu%2F1%2BXl%2FK5%2Baz8jIyMhYG%2Fz5gJTMF1GtKq%2Fa3rpyCvz5gJTMl9GtKq%2Fa3rpyCmfQ4WwZmS%2BkXFVetb115ST48wEf%2FAGcfG1iw%2BtWbpbS2vJ3nQxcVr3lH3z5h972FUTLzYpOVk7l5hD%2BeYcYwDcAnewOotrZ4OtrPDucqi%2FLRX0%2FRR4qx7Nn4U8g%2BqjffvuN6Gf%2BnC85vwauHjaYyubqvWYKY4VEfSUMitdnBCT1Ue63R5439m%2BOgCn6DroAAaHPVQxKth%2FwkJgHmG8bmQMsT0D6EjDfvhVRKO3ywOQUgRA7nmL1uawZmHf1k%2BDPBwQ6NdcJ%2Bk6Md1LA5f5ONdhJ8vZ5J0vLHT99srkGOjmJbd%2FG1r2Nriqnse1AZt1AalU5jW2HsuuG0qvKGRkZGRkZGRG0gcONyXsP9v8D0%2FIdJADiBNiXl3327WRGgOL%2F9HC%2F0XwlIURkRhC4tz6Z%2Ffu7fUf2gHvfB9z3u0BGRkZGRkbGplHcnkgguQoSqtUXuhbs%2FwPtMwqV0HUJAvj5vk32b8IDuL23yn7qAXZ5u32hbRX7d3o82Df1FZXvbh9QOfhyxldr%2F%2B3xgXU9oKmvsHyr7F%2FXA269%2FeveBXrsv7N9QALe%2FtvjA0kPWAXGbvebkbHn%2BD%2FJ5nMcHzx1UAAAAABJRU5ErkJggg%3D%3D)}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAADwCAMAAADYSUr5AAAA7VBMVEVERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkQfbf86AAAATnRSTlMAGBAyBAhQv4OZLiJUcEBmYBoSzQwgPBZCSEoeWiYwUiyFNIeBw2rJz8c4RBy9uXyrtaWNqa2zKP2fJO8KBgKPo2KVoa9s351GPm5%2BkWho0kj9AAAPhUlEQVR4nO1djWLbthEGyUiq5YSSLXtp7FpLOmfzkmxr126tmi2p03RJ1%2FXe%2F3EGgARxPyAgRbIk2%2FhkSz4CJO4%2BHsE7AJSVysjI2AMUUOxahZ2iANhzBtZWr4BoIRSYAVN5u4QwDwQDRbcwfUi5KS3wFuDmFnQLa4Dtb%2F%2FcqktwD5QEFFwfUs7PoCCA7y4bEJVFizcIob8KmhAplwwqVjt%2B9FBl3uINQniwEiryEyw9JHqGpQdEFNi%2BB4QQ7QOiHhysIPoAxUqxvdvvA9K42bsAv4S2fxfYOe57IJSRkZGRkZGxx7jxSHDHcRBXQMTyIjInBgHwBJ%2FbEx8PEANC%2BuhbpSSggCBAVODVabpI1S%2Fk4WLZpTn6NpMhoX9Y40hxYERFpMcqUs4AloCtDQdID1YhnyXZ2hLjAYWiO9Dy1PDB7tPhIqLx%2BuMB8grZaR%2BQxl2%2FC2RkZGRkZGRk7A7rBf7J0DR5%2FLUTjzUPIPSPGvQJiVJiB7kcQCiUOJrcFNtDZIf2xarQ3aGvLNxAVIFAabz90BFiBIlycTBhgWwOWCH0FLYHlPqwHaCvcIn2ZbosCevfPTRiFFcgvHukCjWwrc3GrGh1fsAof8EaUReKXkCB4%2FMzFNo97qLpFiKFYv%2FkNR5YQxQbQEofkZ2OuEOHqqT6gFTpru8CN7x%2F%2BjaZkZGRkZGRcV%2Bx%2FrLUNcMMqUAscgnFocmpqkTzqymwVAPxfJ5PnIUUQOUKT04tEdWZyv3JCQSn96WS4pD97QfyW25A7NhSAbyhmVj0FEltA4vdiygBibXhoUYgykCUP7HwPTDeEqAIcHVMkZg7Zx4k0uFANs63hPQXCoRLAwdgGsr9Az7Qv7sgQGgg1aPl%2FBJLExBWgG4RFRLFImGmIquPC%2FklEGyCG0AuAXaJJC%2BB8FVe9NYQDEcXB8g6AQcjYJ1goJIggHWCrFR0S6kRHN5%2B4BzFi8NaoN35NRxUvL%2BJJdZr7PV4wK6fj8nIyMjIyNhr3OxdXAYq7FHZwB6bDSzSh4sF0utChqo0NAvaT1hLzXwFinmCzmeDucEQK18TTaQoFgP7bNC%2BRZ4OT4T6gQogDFYk%2B1QxQlj19QGSAWKiLYp8P0Ag1Gbz1ULfWHLg9iUnQNK5QQJcukm04blKLH2GgEJCY%2BHzXAZWCvHKco3Bp6MIaCjSXXRJyOxeqhnzEaF93MfFGW%2FO16ZvDL5TM4MJIjujz%2FcHypkQuuzRwWJ93BKdIt%2BwCRAPl9kpe2Ikkb2mFgGlxh%2Fi40d3EHfdvoyMjIyMu43ylt%2FIAmGHnN5iIt7wKfbv01RAcJqFRl9lcjYQSnbQqKgC4fYOwSJt6N6trE0twZ9kN%2FPqNpTQeICvr4TLsDYC06U7BMjshS%2Bv1%2FaT7IwQYD5LcgRQXMT2FrBfBLjZ6151jDElk9tPFfpUgk2yregusX25BJbwAFEfM%2BYI6vGAti4bTtizB%2BTjfQCrERyhKb2X8D6A9wX75P4t4neBYJeP6pdhg%2FgQl8MWvytzeSTjgOQBynQdh%2FiXKdxOrGJ%2FRkZGRsb9QmXihGr5%2Bg8GGg9uTh%2BKoVZuNIzV%2BCwRucFBEyr1mVjx4irOxwM1BhirB6Q%2B2eNQi4eqR%2BaF6mELtoMzCR7V9RAFe%2FZvQogNiyY8FPSUTFsLp8TeTmMui5mtw7bcaT0Yw2AA4wFRQIlkgq%2B1DQrNhkmoxS5Jq%2Bu6bMAIGRECEANgXHTgWzwgBOhDH2l0oTQ4D8D5NMktBgNywAEMjo8rwATMZrPY7JGxBoJCkIBDQiAY09EGTUiBCWkUpISfGPR5AAwBfZiG2z7Ayc1yeKTxid39xBNwfHr4O0LA48ePFTvhYrF1r4tyAoz9n2MCqEuBtp%2F6GDR0oAYfG%2FR6wJExHYZHfhygsv7fEWCOj4bYmsP5A%2BpL4MkTfAnMlD4F%2Br3bobKvTyTA2P%2Fw7PN%2BAgq2QW8piqMCpTBwenoKvX0AHGkGtP2YAPvTEWA7QUTAudn7%2FNxtOG46wWNmDtpBEkBzN7rBEvAFHp%2BYTB%2Fq97qPAN4gHFqgBi8uLsC7qPCA6mg41G%2F%2BErByPwEXDdoNxRhOx%2BM5jPEzQugS0ht%2Bb1%2FY3gEnYMAIAOIBE29%2FhIDucE8tmMsNOgK4B1RHFu4UCRlMHzv0xzcajcfdXWDs2h8TArBCkoDUJYDLmz6w7ip3BFS0ve5wTRwAn6keMA9I3QYbfSZ0DKbyt%2B7OXjGI1idPcfNyAyfAMlCrzaGqphYrxHocLHRJVycnfGUcbtT%2BjIyMjIw9x7Nn8fJSzG0TmFtO8rZT%2BXT3S3ub%2BtKJbbLd5diTVp50%2BzahyeHSslJ%2FYPrU0fuazrZO2CZ92%2FZCCVXlGRiZKPJyPPRxyIFWeXLQBXJBKiq%2F3divEAN6ZwM200Qjm7EJBZeWm%2FPRWVCbYK7s7u2l4XaCz%2BlzgOfMfhMonXr7TWzeZb98dbgIzBT8Ub8eYYUqfZ4rVJ%2FMDbIDgPqTulJ%2FxvntWAtjIisqnwxOkGz0n077FARoY79GdA6HPE4rOy196NiMWHTZlSSApcOgXpy%2FfHV2joaNKu3ffsAnRcBf4K%2F6NcIG6tIxk3HyoXPjASqfUgXbYN5PzpL2njkR9QMjeDTVHDTCgRuxOegjoO0FvKzP%2Ft%2FgmVdI24%2BG7NIe8JX6Wv3dDyldMA%2B4YB5wwTygtd%2BdwRqaTqrLb1l73zTSN52CNpnHuQOYPsDblybgxfkXh%2FoVtr%2BN1DEBJdhRJyd%2FBd%2Fq1z%2BcbNrD17iVKyajcnv9arhOkRPgsruuD6DmNPwpDNrLw2CoTgHni4yALr0L29%2BtiKAEIPn868ejx%2F%2F8rpWP3OEOl5On9OwpcQm0MhafP%2Fey8f1uvDNIgGLQG8z4YO99ENgg95etwv4uYJYY8fUGHYH6j6fscHFZMftlAl9i%2B9XL73X3N%2Fn%2BZStOzfVfRvYXhrbdKOpEgVQTg%2FwsDuDD3kwOfQNMTJ5y%2B%2FltUDWLunyxnRF46IqlBzGMY4X7inggREFioIyMjIyMHWCIB6ZNKAcXseo3vLTQTkVE7348dlwJJSz0%2BwLfmi8BhZqfw3D4ww%2FwHVLnEd5%2FfgYvXsDZ3MlsvYUbbnDjDZ3MN3TJG4%2BbxjAaDl8TBri9qxEw1ccao2wTNAMLHo2f%2BsjrXwb%2F9qHoYqgPMBXJTVfOpmrZH23y6uvo0LHSyY6fHGwKfHJlAuMFvObjDYrIqxBgQi20h7Hd%2FnYVLmno%2BeaNUm%2FeeH2GCuopntnhBJAlI2AHo9CCh1I1QxUdAbqqGY9BBLwyc3W4wYVhvY8A4BoIc1l5M7vnPWphZW9%2FSes3n37y9a0uGqFwFQZsQQbd386DogpgEk%2BdzynsAZMJXq8%2Bns9NeukJ0PYrNATGGefJQlhkLo7DTXr%2By3bNiOsDvrXTz%2FC2q1DXZH84iRNwrP88Nj%2Bu2DjYEE6RBxD9Knj16ujVHC67A7422o02RwD3gB%2Bt7EblWvu9geOFxSnd3ROmT%2BnJyQkhoPlsxVONc%2F3TEdBos%2BjtA%2BZzcwHgTvD1cDjaYCcItA8w9i88A8b%2BmqSjc6Pvqd998QguEQPmQMeo23ODN86%2Bp0%2Fbn1buBkT6%2BoBhNZ%2FPYY4ZAHYb3PRd4LkZmPX68NRtMZn4ASvdA%2Bqf0jMA5MP9eeg28Nug9QiLnj5A33U1MAES6xHAUNpz%2F9zFAYE1gqQDMT3G6xI9pwdw%2FaIgKoHCS1YGlRnSq9yCjdXjgN3j%2BN27YyROHxmuNAeNKPpYuXIyIyMjYy0M8eros59MF%2FPT2c602T7eA7zvhJ9dr%2FvzDjXaLp4Yc5%2B0wllzxzHv3gdmMMM7%2FCcQzKgVBqYTmFn%2BZ%2BmKm8J7k0A5F%2FjgCfjQ1WBhQyiOqD0lYuqBb%2BAyzMw9Ha2G3m6c8qQx%2BAlqnIceQp%2BSb6i9UyQWbhr54%2BAjnZ0VzW2TAN0DmBT6PWmc6jDBE2PK2u%2BnF43dyP7Q0t1pOcX2fdRvH0mF2Q4JqN35rnHjVIeaXfIAVyUuw%2FaHCCiJy9iF5l1621zweI8KZrPZ9iJdb7DXJ3US0OSrtZ10imt7wHY7QesAzUMz1oZ3noB3qFJ%2FH18j97FYuw8QDN4oeKf30osvcSW2ExLo%2BVcbuAuo%2FsUIm8fMG9xocO3Ea19J9gFYivnHJ2KnyfovZlgW3v6ySx32abQiIyMjIyPjhlFDTLxpwIgFMnTp6A3g4IDKNY%2BstkwAMAoIAbasxBXqUWneSAWTMjt50lTqT29rFjvXohjsDNm2YPXDFlICmrJOZ3t6tHm8AiEAl0sCeLIIorIRt%2BcFbew%2FQRsoAXb4o1XSfoywzm0FTMAoYBNvLyFu8v8HpLBtD1iKgC17wHb7AI6d9wFbvguAIGTHd4E9wG7jgIyMjIyM%2B434c2R3HeV%2FFfx6jtZu6ijl8h59T655jhR%2BrdHzDOP6beABCheb8O8%2FWFXeOyzgf5oAhVYnKxP7CwaAf1afJu8bSrhS6tdaXeGnrRenOqOlz9d6QwYnA%2F3TLd%2BGE7qe3chA5YF5DfY0vK3adfOX%2FgyNp2BW25MHdxAB9qvRiiP3%2FXpQQFGYDU4%2BMi%2F%2F%2FXumXG8pjvaUAOsBGlf4jJt%2BYYEzeEzAdw06F19R3juM7D1wita86GR0CKfDHgLuXCc4Bri6vMLdfjMc4VNSUNsdodo2xu%2F1%2BXl%2FK5%2Baz8jIyMhYG%2Fz5gJTMF1GtKq%2Fa3rpyCvz5gJTMl9GtKq%2Fa3rpyCmfQ4WwZmS%2BkXFVetb115ST48wEf%2FAGcfG1iw%2BtWbpbS2vJ3nQxcVr3lH3z5h972FUTLzYpOVk7l5hD%2BeYcYwDcAnewOotrZ4OtrPDucqi%2FLRX0%2FRR4qx7Nn4U8g%2BqjffvuN6Gf%2BnC85vwauHjaYyubqvWYKY4VEfSUMitdnBCT1Ue63R5439m%2BOgCn6DroAAaHPVQxKth%2FwkJgHmG8bmQMsT0D6EjDfvhVRKO3ywOQUgRA7nmL1uawZmHf1k%2BDPBwQ6NdcJ%2Bk6Md1LA5f5ONdhJ8vZ5J0vLHT99srkGOjmJbd%2FG1r2Nriqnse1AZt1AalU5jW2HsuuG0qvKGRkZGRkZGRG0gcONyXsP9v8D0%2FIdJADiBNiXl3327WRGgOL%2F9HC%2F0XwlIURkRhC4tz6Z%2Ffu7fUf2gHvfB9z3u0BGRkZGRkbGplHcnkgguQoSqtUXuhbs%2FwPtMwqV0HUJAvj5vk32b8IDuL23yn7qAXZ5u32hbRX7d3o82Df1FZXvbh9QOfhyxldr%2F%2B3xgXU9oKmvsHyr7F%2FXA269%2FeveBXrsv7N9QALe%2FtvjA0kPWAXGbvebkbHn%2BD%2FJ5nMcHzx1UAAAAABJRU5ErkJggg%3D%3D)}.ui-state-active .ui-icon{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAADwCAMAAADYSUr5AAAA7VBMVEVERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkRERkQfbf86AAAATnRSTlMAGBAyBAhQv4OZLiJUcEBmYBoSzQwgPBZCSEoeWiYwUiyFNIeBw2rJz8c4RBy9uXyrtaWNqa2zKP2fJO8KBgKPo2KVoa9s351GPm5%2BkWho0kj9AAAPhUlEQVR4nO1djWLbthEGyUiq5YSSLXtp7FpLOmfzkmxr126tmi2p03RJ1%2FXe%2F3EGgARxPyAgRbIk2%2FhkSz4CJO4%2BHsE7AJSVysjI2AMUUOxahZ2iANhzBtZWr4BoIRSYAVN5u4QwDwQDRbcwfUi5KS3wFuDmFnQLa4Dtb%2F%2FcqktwD5QEFFwfUs7PoCCA7y4bEJVFizcIob8KmhAplwwqVjt%2B9FBl3uINQniwEiryEyw9JHqGpQdEFNi%2BB4QQ7QOiHhysIPoAxUqxvdvvA9K42bsAv4S2fxfYOe57IJSRkZGRkZGxx7jxSHDHcRBXQMTyIjInBgHwBJ%2FbEx8PEANC%2BuhbpSSggCBAVODVabpI1S%2Fk4WLZpTn6NpMhoX9Y40hxYERFpMcqUs4AloCtDQdID1YhnyXZ2hLjAYWiO9Dy1PDB7tPhIqLx%2BuMB8grZaR%2BQxl2%2FC2RkZGRkZGRk7A7rBf7J0DR5%2FLUTjzUPIPSPGvQJiVJiB7kcQCiUOJrcFNtDZIf2xarQ3aGvLNxAVIFAabz90BFiBIlycTBhgWwOWCH0FLYHlPqwHaCvcIn2ZbosCevfPTRiFFcgvHukCjWwrc3GrGh1fsAof8EaUReKXkCB4%2FMzFNo97qLpFiKFYv%2FkNR5YQxQbQEofkZ2OuEOHqqT6gFTpru8CN7x%2F%2BjaZkZGRkZGRcV%2Bx%2FrLUNcMMqUAscgnFocmpqkTzqymwVAPxfJ5PnIUUQOUKT04tEdWZyv3JCQSn96WS4pD97QfyW25A7NhSAbyhmVj0FEltA4vdiygBibXhoUYgykCUP7HwPTDeEqAIcHVMkZg7Zx4k0uFANs63hPQXCoRLAwdgGsr9Az7Qv7sgQGgg1aPl%2FBJLExBWgG4RFRLFImGmIquPC%2FklEGyCG0AuAXaJJC%2BB8FVe9NYQDEcXB8g6AQcjYJ1goJIggHWCrFR0S6kRHN5%2B4BzFi8NaoN35NRxUvL%2BJJdZr7PV4wK6fj8nIyMjIyNhr3OxdXAYq7FHZwB6bDSzSh4sF0utChqo0NAvaT1hLzXwFinmCzmeDucEQK18TTaQoFgP7bNC%2BRZ4OT4T6gQogDFYk%2B1QxQlj19QGSAWKiLYp8P0Ag1Gbz1ULfWHLg9iUnQNK5QQJcukm04blKLH2GgEJCY%2BHzXAZWCvHKco3Bp6MIaCjSXXRJyOxeqhnzEaF93MfFGW%2FO16ZvDL5TM4MJIjujz%2FcHypkQuuzRwWJ93BKdIt%2BwCRAPl9kpe2Ikkb2mFgGlxh%2Fi40d3EHfdvoyMjIyMu43ylt%2FIAmGHnN5iIt7wKfbv01RAcJqFRl9lcjYQSnbQqKgC4fYOwSJt6N6trE0twZ9kN%2FPqNpTQeICvr4TLsDYC06U7BMjshS%2Bv1%2FaT7IwQYD5LcgRQXMT2FrBfBLjZ6151jDElk9tPFfpUgk2yregusX25BJbwAFEfM%2BYI6vGAti4bTtizB%2BTjfQCrERyhKb2X8D6A9wX75P4t4neBYJeP6pdhg%2FgQl8MWvytzeSTjgOQBynQdh%2FiXKdxOrGJ%2FRkZGRsb9QmXihGr5%2Bg8GGg9uTh%2BKoVZuNIzV%2BCwRucFBEyr1mVjx4irOxwM1BhirB6Q%2B2eNQi4eqR%2BaF6mELtoMzCR7V9RAFe%2FZvQogNiyY8FPSUTFsLp8TeTmMui5mtw7bcaT0Yw2AA4wFRQIlkgq%2B1DQrNhkmoxS5Jq%2Bu6bMAIGRECEANgXHTgWzwgBOhDH2l0oTQ4D8D5NMktBgNywAEMjo8rwATMZrPY7JGxBoJCkIBDQiAY09EGTUiBCWkUpISfGPR5AAwBfZiG2z7Ayc1yeKTxid39xBNwfHr4O0LA48ePFTvhYrF1r4tyAoz9n2MCqEuBtp%2F6GDR0oAYfG%2FR6wJExHYZHfhygsv7fEWCOj4bYmsP5A%2BpL4MkTfAnMlD4F%2Br3bobKvTyTA2P%2Fw7PN%2BAgq2QW8piqMCpTBwenoKvX0AHGkGtP2YAPvTEWA7QUTAudn7%2FNxtOG46wWNmDtpBEkBzN7rBEvAFHp%2BYTB%2Fq97qPAN4gHFqgBi8uLsC7qPCA6mg41G%2F%2BErByPwEXDdoNxRhOx%2BM5jPEzQugS0ht%2Bb1%2FY3gEnYMAIAOIBE29%2FhIDucE8tmMsNOgK4B1RHFu4UCRlMHzv0xzcajcfdXWDs2h8TArBCkoDUJYDLmz6w7ip3BFS0ve5wTRwAn6keMA9I3QYbfSZ0DKbyt%2B7OXjGI1idPcfNyAyfAMlCrzaGqphYrxHocLHRJVycnfGUcbtT%2BjIyMjIw9x7Nn8fJSzG0TmFtO8rZT%2BXT3S3ub%2BtKJbbLd5diTVp50%2BzahyeHSslJ%2FYPrU0fuazrZO2CZ92%2FZCCVXlGRiZKPJyPPRxyIFWeXLQBXJBKiq%2F3divEAN6ZwM200Qjm7EJBZeWm%2FPRWVCbYK7s7u2l4XaCz%2BlzgOfMfhMonXr7TWzeZb98dbgIzBT8Ub8eYYUqfZ4rVJ%2FMDbIDgPqTulJ%2FxvntWAtjIisqnwxOkGz0n077FARoY79GdA6HPE4rOy196NiMWHTZlSSApcOgXpy%2FfHV2joaNKu3ffsAnRcBf4K%2F6NcIG6tIxk3HyoXPjASqfUgXbYN5PzpL2njkR9QMjeDTVHDTCgRuxOegjoO0FvKzP%2Ft%2FgmVdI24%2BG7NIe8JX6Wv3dDyldMA%2B4YB5wwTygtd%2BdwRqaTqrLb1l73zTSN52CNpnHuQOYPsDblybgxfkXh%2FoVtr%2BN1DEBJdhRJyd%2FBd%2Fq1z%2BcbNrD17iVKyajcnv9arhOkRPgsruuD6DmNPwpDNrLw2CoTgHni4yALr0L29%2BtiKAEIPn868ejx%2F%2F8rpWP3OEOl5On9OwpcQm0MhafP%2Fey8f1uvDNIgGLQG8z4YO99ENgg95etwv4uYJYY8fUGHYH6j6fscHFZMftlAl9i%2B9XL73X3N%2Fn%2BZStOzfVfRvYXhrbdKOpEgVQTg%2FwsDuDD3kwOfQNMTJ5y%2B%2FltUDWLunyxnRF46IqlBzGMY4X7inggREFioIyMjIyMHWCIB6ZNKAcXseo3vLTQTkVE7348dlwJJSz0%2BwLfmi8BhZqfw3D4ww%2FwHVLnEd5%2FfgYvXsDZ3MlsvYUbbnDjDZ3MN3TJG4%2BbxjAaDl8TBri9qxEw1ccao2wTNAMLHo2f%2BsjrXwb%2F9qHoYqgPMBXJTVfOpmrZH23y6uvo0LHSyY6fHGwKfHJlAuMFvObjDYrIqxBgQi20h7Hd%2FnYVLmno%2BeaNUm%2FeeH2GCuopntnhBJAlI2AHo9CCh1I1QxUdAbqqGY9BBLwyc3W4wYVhvY8A4BoIc1l5M7vnPWphZW9%2FSes3n37y9a0uGqFwFQZsQQbd386DogpgEk%2BdzynsAZMJXq8%2Bns9NeukJ0PYrNATGGefJQlhkLo7DTXr%2By3bNiOsDvrXTz%2FC2q1DXZH84iRNwrP88Nj%2Bu2DjYEE6RBxD9Knj16ujVHC67A7422o02RwD3gB%2Bt7EblWvu9geOFxSnd3ROmT%2BnJyQkhoPlsxVONc%2F3TEdBos%2BjtA%2BZzcwHgTvD1cDjaYCcItA8w9i88A8b%2BmqSjc6Pvqd998QguEQPmQMeo23ODN86%2Bp0%2Fbn1buBkT6%2BoBhNZ%2FPYY4ZAHYb3PRd4LkZmPX68NRtMZn4ASvdA%2Bqf0jMA5MP9eeg28Nug9QiLnj5A33U1MAES6xHAUNpz%2F9zFAYE1gqQDMT3G6xI9pwdw%2FaIgKoHCS1YGlRnSq9yCjdXjgN3j%2BN27YyROHxmuNAeNKPpYuXIyIyMjYy0M8eros59MF%2FPT2c602T7eA7zvhJ9dr%2FvzDjXaLp4Yc5%2B0wllzxzHv3gdmMMM7%2FCcQzKgVBqYTmFn%2BZ%2BmKm8J7k0A5F%2FjgCfjQ1WBhQyiOqD0lYuqBb%2BAyzMw9Ha2G3m6c8qQx%2BAlqnIceQp%2BSb6i9UyQWbhr54%2BAjnZ0VzW2TAN0DmBT6PWmc6jDBE2PK2u%2BnF43dyP7Q0t1pOcX2fdRvH0mF2Q4JqN35rnHjVIeaXfIAVyUuw%2FaHCCiJy9iF5l1621zweI8KZrPZ9iJdb7DXJ3US0OSrtZ10imt7wHY7QesAzUMz1oZ3noB3qFJ%2FH18j97FYuw8QDN4oeKf30osvcSW2ExLo%2BVcbuAuo%2FsUIm8fMG9xocO3Ea19J9gFYivnHJ2KnyfovZlgW3v6ySx32abQiIyMjIyPjhlFDTLxpwIgFMnTp6A3g4IDKNY%2BstkwAMAoIAbasxBXqUWneSAWTMjt50lTqT29rFjvXohjsDNm2YPXDFlICmrJOZ3t6tHm8AiEAl0sCeLIIorIRt%2BcFbew%2FQRsoAXb4o1XSfoywzm0FTMAoYBNvLyFu8v8HpLBtD1iKgC17wHb7AI6d9wFbvguAIGTHd4E9wG7jgIyMjIyM%2B434c2R3HeV%2FFfx6jtZu6ijl8h59T655jhR%2BrdHzDOP6beABCheb8O8%2FWFXeOyzgf5oAhVYnKxP7CwaAf1afJu8bSrhS6tdaXeGnrRenOqOlz9d6QwYnA%2F3TLd%2BGE7qe3chA5YF5DfY0vK3adfOX%2FgyNp2BW25MHdxAB9qvRiiP3%2FXpQQFGYDU4%2BMi%2F%2F%2FXumXG8pjvaUAOsBGlf4jJt%2BYYEzeEzAdw06F19R3juM7D1wita86GR0CKfDHgLuXCc4Bri6vMLdfjMc4VNSUNsdodo2xu%2F1%2BXl%2FK5%2Baz8jIyMhYG%2Fz5gJTMF1GtKq%2Fa3rpyCvz5gJTMl9GtKq%2Fa3rpyCmfQ4WwZmS%2BkXFVetb115ST48wEf%2FAGcfG1iw%2BtWbpbS2vJ3nQxcVr3lH3z5h972FUTLzYpOVk7l5hD%2BeYcYwDcAnewOotrZ4OtrPDucqi%2FLRX0%2FRR4qx7Nn4U8g%2BqjffvuN6Gf%2BnC85vwauHjaYyubqvWYKY4VEfSUMitdnBCT1Ue63R5439m%2BOgCn6DroAAaHPVQxKth%2FwkJgHmG8bmQMsT0D6EjDfvhVRKO3ywOQUgRA7nmL1uawZmHf1k%2BDPBwQ6NdcJ%2Bk6Md1LA5f5ONdhJ8vZ5J0vLHT99srkGOjmJbd%2FG1r2Nriqnse1AZt1AalU5jW2HsuuG0qvKGRkZGRkZGRG0gcONyXsP9v8D0%2FIdJADiBNiXl3327WRGgOL%2F9HC%2F0XwlIURkRhC4tz6Z%2Ffu7fUf2gHvfB9z3u0BGRkZGRkbGplHcnkgguQoSqtUXuhbs%2FwPtMwqV0HUJAvj5vk32b8IDuL23yn7qAXZ5u32hbRX7d3o82Df1FZXvbh9QOfhyxldr%2F%2B3xgXU9oKmvsHyr7F%2FXA269%2FeveBXrsv7N9QALe%2FtvjA0kPWAXGbvebkbHn%2BD%2FJ5nMcHzx1UAAAAABJRU5ErkJggg%3D%3D)}.ui-state-highlight .ui-icon{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAADwCAMAAADYSUr5AAAA7VBMVEUsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvwsgvzLrJBNAAAATnRSTlMAGBAyBAhQv4OZLiJUcEBmYBoSzQwgPBZCSEoeWiYwUiyFNIeBw2rJz8c4RBy9uXyrtaWNqa2zKP2fJO8KBgKPo2KVoa9s351GPm5%2BkWho0kj9AAAPhUlEQVR4nO1djWLbthEGyUiq5YSSLXtp7FpLOmfzkmxr126tmi2p03RJ1%2FXe%2F3EGgARxPyAgRbIk2%2FhkSz4CJO4%2BHsE7AJSVysjI2AMUUOxahZ2iANhzBtZWr4BoIRSYAVN5u4QwDwQDRbcwfUi5KS3wFuDmFnQLa4Dtb%2F%2FcqktwD5QEFFwfUs7PoCCA7y4bEJVFizcIob8KmhAplwwqVjt%2B9FBl3uINQniwEiryEyw9JHqGpQdEFNi%2BB4QQ7QOiHhysIPoAxUqxvdvvA9K42bsAv4S2fxfYOe57IJSRkZGRkZGxx7jxSHDHcRBXQMTyIjInBgHwBJ%2FbEx8PEANC%2BuhbpSSggCBAVODVabpI1S%2Fk4WLZpTn6NpMhoX9Y40hxYERFpMcqUs4AloCtDQdID1YhnyXZ2hLjAYWiO9Dy1PDB7tPhIqLx%2BuMB8grZaR%2BQxl2%2FC2RkZGRkZGRk7A7rBf7J0DR5%2FLUTjzUPIPSPGvQJiVJiB7kcQCiUOJrcFNtDZIf2xarQ3aGvLNxAVIFAabz90BFiBIlycTBhgWwOWCH0FLYHlPqwHaCvcIn2ZbosCevfPTRiFFcgvHukCjWwrc3GrGh1fsAof8EaUReKXkCB4%2FMzFNo97qLpFiKFYv%2FkNR5YQxQbQEofkZ2OuEOHqqT6gFTpru8CN7x%2F%2BjaZkZGRkZGRcV%2Bx%2FrLUNcMMqUAscgnFocmpqkTzqymwVAPxfJ5PnIUUQOUKT04tEdWZyv3JCQSn96WS4pD97QfyW25A7NhSAbyhmVj0FEltA4vdiygBibXhoUYgykCUP7HwPTDeEqAIcHVMkZg7Zx4k0uFANs63hPQXCoRLAwdgGsr9Az7Qv7sgQGgg1aPl%2FBJLExBWgG4RFRLFImGmIquPC%2FklEGyCG0AuAXaJJC%2BB8FVe9NYQDEcXB8g6AQcjYJ1goJIggHWCrFR0S6kRHN5%2B4BzFi8NaoN35NRxUvL%2BJJdZr7PV4wK6fj8nIyMjIyNhr3OxdXAYq7FHZwB6bDSzSh4sF0utChqo0NAvaT1hLzXwFinmCzmeDucEQK18TTaQoFgP7bNC%2BRZ4OT4T6gQogDFYk%2B1QxQlj19QGSAWKiLYp8P0Ag1Gbz1ULfWHLg9iUnQNK5QQJcukm04blKLH2GgEJCY%2BHzXAZWCvHKco3Bp6MIaCjSXXRJyOxeqhnzEaF93MfFGW%2FO16ZvDL5TM4MJIjujz%2FcHypkQuuzRwWJ93BKdIt%2BwCRAPl9kpe2Ikkb2mFgGlxh%2Fi40d3EHfdvoyMjIyMu43ylt%2FIAmGHnN5iIt7wKfbv01RAcJqFRl9lcjYQSnbQqKgC4fYOwSJt6N6trE0twZ9kN%2FPqNpTQeICvr4TLsDYC06U7BMjshS%2Bv1%2FaT7IwQYD5LcgRQXMT2FrBfBLjZ6151jDElk9tPFfpUgk2yregusX25BJbwAFEfM%2BYI6vGAti4bTtizB%2BTjfQCrERyhKb2X8D6A9wX75P4t4neBYJeP6pdhg%2FgQl8MWvytzeSTjgOQBynQdh%2FiXKdxOrGJ%2FRkZGRsb9QmXihGr5%2Bg8GGg9uTh%2BKoVZuNIzV%2BCwRucFBEyr1mVjx4irOxwM1BhirB6Q%2B2eNQi4eqR%2BaF6mELtoMzCR7V9RAFe%2FZvQogNiyY8FPSUTFsLp8TeTmMui5mtw7bcaT0Yw2AA4wFRQIlkgq%2B1DQrNhkmoxS5Jq%2Bu6bMAIGRECEANgXHTgWzwgBOhDH2l0oTQ4D8D5NMktBgNywAEMjo8rwATMZrPY7JGxBoJCkIBDQiAY09EGTUiBCWkUpISfGPR5AAwBfZiG2z7Ayc1yeKTxid39xBNwfHr4O0LA48ePFTvhYrF1r4tyAoz9n2MCqEuBtp%2F6GDR0oAYfG%2FR6wJExHYZHfhygsv7fEWCOj4bYmsP5A%2BpL4MkTfAnMlD4F%2Br3bobKvTyTA2P%2Fw7PN%2BAgq2QW8piqMCpTBwenoKvX0AHGkGtP2YAPvTEWA7QUTAudn7%2FNxtOG46wWNmDtpBEkBzN7rBEvAFHp%2BYTB%2Fq97qPAN4gHFqgBi8uLsC7qPCA6mg41G%2F%2BErByPwEXDdoNxRhOx%2BM5jPEzQugS0ht%2Bb1%2FY3gEnYMAIAOIBE29%2FhIDucE8tmMsNOgK4B1RHFu4UCRlMHzv0xzcajcfdXWDs2h8TArBCkoDUJYDLmz6w7ip3BFS0ve5wTRwAn6keMA9I3QYbfSZ0DKbyt%2B7OXjGI1idPcfNyAyfAMlCrzaGqphYrxHocLHRJVycnfGUcbtT%2BjIyMjIw9x7Nn8fJSzG0TmFtO8rZT%2BXT3S3ub%2BtKJbbLd5diTVp50%2BzahyeHSslJ%2FYPrU0fuazrZO2CZ92%2FZCCVXlGRiZKPJyPPRxyIFWeXLQBXJBKiq%2F3divEAN6ZwM200Qjm7EJBZeWm%2FPRWVCbYK7s7u2l4XaCz%2BlzgOfMfhMonXr7TWzeZb98dbgIzBT8Ub8eYYUqfZ4rVJ%2FMDbIDgPqTulJ%2FxvntWAtjIisqnwxOkGz0n077FARoY79GdA6HPE4rOy196NiMWHTZlSSApcOgXpy%2FfHV2joaNKu3ffsAnRcBf4K%2F6NcIG6tIxk3HyoXPjASqfUgXbYN5PzpL2njkR9QMjeDTVHDTCgRuxOegjoO0FvKzP%2Ft%2FgmVdI24%2BG7NIe8JX6Wv3dDyldMA%2B4YB5wwTygtd%2BdwRqaTqrLb1l73zTSN52CNpnHuQOYPsDblybgxfkXh%2FoVtr%2BN1DEBJdhRJyd%2FBd%2Fq1z%2BcbNrD17iVKyajcnv9arhOkRPgsruuD6DmNPwpDNrLw2CoTgHni4yALr0L29%2BtiKAEIPn868ejx%2F%2F8rpWP3OEOl5On9OwpcQm0MhafP%2Fey8f1uvDNIgGLQG8z4YO99ENgg95etwv4uYJYY8fUGHYH6j6fscHFZMftlAl9i%2B9XL73X3N%2Fn%2BZStOzfVfRvYXhrbdKOpEgVQTg%2FwsDuDD3kwOfQNMTJ5y%2B%2FltUDWLunyxnRF46IqlBzGMY4X7inggREFioIyMjIyMHWCIB6ZNKAcXseo3vLTQTkVE7348dlwJJSz0%2BwLfmi8BhZqfw3D4ww%2FwHVLnEd5%2FfgYvXsDZ3MlsvYUbbnDjDZ3MN3TJG4%2BbxjAaDl8TBri9qxEw1ccao2wTNAMLHo2f%2BsjrXwb%2F9qHoYqgPMBXJTVfOpmrZH23y6uvo0LHSyY6fHGwKfHJlAuMFvObjDYrIqxBgQi20h7Hd%2FnYVLmno%2BeaNUm%2FeeH2GCuopntnhBJAlI2AHo9CCh1I1QxUdAbqqGY9BBLwyc3W4wYVhvY8A4BoIc1l5M7vnPWphZW9%2FSes3n37y9a0uGqFwFQZsQQbd386DogpgEk%2BdzynsAZMJXq8%2Bns9NeukJ0PYrNATGGefJQlhkLo7DTXr%2By3bNiOsDvrXTz%2FC2q1DXZH84iRNwrP88Nj%2Bu2DjYEE6RBxD9Knj16ujVHC67A7422o02RwD3gB%2Bt7EblWvu9geOFxSnd3ROmT%2BnJyQkhoPlsxVONc%2F3TEdBos%2BjtA%2BZzcwHgTvD1cDjaYCcItA8w9i88A8b%2BmqSjc6Pvqd998QguEQPmQMeo23ODN86%2Bp0%2Fbn1buBkT6%2BoBhNZ%2FPYY4ZAHYb3PRd4LkZmPX68NRtMZn4ASvdA%2Bqf0jMA5MP9eeg28Nug9QiLnj5A33U1MAES6xHAUNpz%2F9zFAYE1gqQDMT3G6xI9pwdw%2FaIgKoHCS1YGlRnSq9yCjdXjgN3j%2BN27YyROHxmuNAeNKPpYuXIyIyMjYy0M8eros59MF%2FPT2c602T7eA7zvhJ9dr%2FvzDjXaLp4Yc5%2B0wllzxzHv3gdmMMM7%2FCcQzKgVBqYTmFn%2BZ%2BmKm8J7k0A5F%2FjgCfjQ1WBhQyiOqD0lYuqBb%2BAyzMw9Ha2G3m6c8qQx%2BAlqnIceQp%2BSb6i9UyQWbhr54%2BAjnZ0VzW2TAN0DmBT6PWmc6jDBE2PK2u%2BnF43dyP7Q0t1pOcX2fdRvH0mF2Q4JqN35rnHjVIeaXfIAVyUuw%2FaHCCiJy9iF5l1621zweI8KZrPZ9iJdb7DXJ3US0OSrtZ10imt7wHY7QesAzUMz1oZ3noB3qFJ%2FH18j97FYuw8QDN4oeKf30osvcSW2ExLo%2BVcbuAuo%2FsUIm8fMG9xocO3Ea19J9gFYivnHJ2KnyfovZlgW3v6ySx32abQiIyMjIyPjhlFDTLxpwIgFMnTp6A3g4IDKNY%2BstkwAMAoIAbasxBXqUWneSAWTMjt50lTqT29rFjvXohjsDNm2YPXDFlICmrJOZ3t6tHm8AiEAl0sCeLIIorIRt%2BcFbew%2FQRsoAXb4o1XSfoywzm0FTMAoYBNvLyFu8v8HpLBtD1iKgC17wHb7AI6d9wFbvguAIGTHd4E9wG7jgIyMjIyM%2B434c2R3HeV%2FFfx6jtZu6ijl8h59T655jhR%2BrdHzDOP6beABCheb8O8%2FWFXeOyzgf5oAhVYnKxP7CwaAf1afJu8bSrhS6tdaXeGnrRenOqOlz9d6QwYnA%2F3TLd%2BGE7qe3chA5YF5DfY0vK3adfOX%2FgyNp2BW25MHdxAB9qvRiiP3%2FXpQQFGYDU4%2BMi%2F%2F%2FXumXG8pjvaUAOsBGlf4jJt%2BYYEzeEzAdw06F19R3juM7D1wita86GR0CKfDHgLuXCc4Bri6vMLdfjMc4VNSUNsdodo2xu%2F1%2BXl%2FK5%2Baz8jIyMhYG%2Fz5gJTMF1GtKq%2Fa3rpyCvz5gJTMl9GtKq%2Fa3rpyCmfQ4WwZmS%2BkXFVetb115ST48wEf%2FAGcfG1iw%2BtWbpbS2vJ3nQxcVr3lH3z5h972FUTLzYpOVk7l5hD%2BeYcYwDcAnewOotrZ4OtrPDucqi%2FLRX0%2FRR4qx7Nn4U8g%2BqjffvuN6Gf%2BnC85vwauHjaYyubqvWYKY4VEfSUMitdnBCT1Ue63R5439m%2BOgCn6DroAAaHPVQxKth%2FwkJgHmG8bmQMsT0D6EjDfvhVRKO3ywOQUgRA7nmL1uawZmHf1k%2BDPBwQ6NdcJ%2Bk6Md1LA5f5ONdhJ8vZ5J0vLHT99srkGOjmJbd%2FG1r2Nriqnse1AZt1AalU5jW2HsuuG0qvKGRkZGRkZGRG0gcONyXsP9v8D0%2FIdJADiBNiXl3327WRGgOL%2F9HC%2F0XwlIURkRhC4tz6Z%2Ffu7fUf2gHvfB9z3u0BGRkZGRkbGplHcnkgguQoSqtUXuhbs%2FwPtMwqV0HUJAvj5vk32b8IDuL23yn7qAXZ5u32hbRX7d3o82Df1FZXvbh9QOfhyxldr%2F%2B3xgXU9oKmvsHyr7F%2FXA269%2FeveBXrsv7N9QALe%2FtvjA0kPWAXGbvebkbHn%2BD%2FJ5nMcHzx1UAAAAABJRU5ErkJggg%3D%3D)}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAADwCAMAAADYSUr5AAAA7VBMVEXMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzMCgzrDkZjAAAATnRSTlMAGBAyBAhQv4OZLiJUcEBmYBoSzQwgPBZCSEoeWiYwUiyFNIeBw2rJz8c4RBy9uXyrtaWNqa2zKP2fJO8KBgKPo2KVoa9s351GPm5%2BkWho0kj9AAAPhUlEQVR4nO1djWLbthEGyUiq5YSSLXtp7FpLOmfzkmxr126tmi2p03RJ1%2FXe%2F3EGgARxPyAgRbIk2%2FhkSz4CJO4%2BHsE7AJSVysjI2AMUUOxahZ2iANhzBtZWr4BoIRSYAVN5u4QwDwQDRbcwfUi5KS3wFuDmFnQLa4Dtb%2F%2FcqktwD5QEFFwfUs7PoCCA7y4bEJVFizcIob8KmhAplwwqVjt%2B9FBl3uINQniwEiryEyw9JHqGpQdEFNi%2BB4QQ7QOiHhysIPoAxUqxvdvvA9K42bsAv4S2fxfYOe57IJSRkZGRkZGxx7jxSHDHcRBXQMTyIjInBgHwBJ%2FbEx8PEANC%2BuhbpSSggCBAVODVabpI1S%2Fk4WLZpTn6NpMhoX9Y40hxYERFpMcqUs4AloCtDQdID1YhnyXZ2hLjAYWiO9Dy1PDB7tPhIqLx%2BuMB8grZaR%2BQxl2%2FC2RkZGRkZGRk7A7rBf7J0DR5%2FLUTjzUPIPSPGvQJiVJiB7kcQCiUOJrcFNtDZIf2xarQ3aGvLNxAVIFAabz90BFiBIlycTBhgWwOWCH0FLYHlPqwHaCvcIn2ZbosCevfPTRiFFcgvHukCjWwrc3GrGh1fsAof8EaUReKXkCB4%2FMzFNo97qLpFiKFYv%2FkNR5YQxQbQEofkZ2OuEOHqqT6gFTpru8CN7x%2F%2BjaZkZGRkZGRcV%2Bx%2FrLUNcMMqUAscgnFocmpqkTzqymwVAPxfJ5PnIUUQOUKT04tEdWZyv3JCQSn96WS4pD97QfyW25A7NhSAbyhmVj0FEltA4vdiygBibXhoUYgykCUP7HwPTDeEqAIcHVMkZg7Zx4k0uFANs63hPQXCoRLAwdgGsr9Az7Qv7sgQGgg1aPl%2FBJLExBWgG4RFRLFImGmIquPC%2FklEGyCG0AuAXaJJC%2BB8FVe9NYQDEcXB8g6AQcjYJ1goJIggHWCrFR0S6kRHN5%2B4BzFi8NaoN35NRxUvL%2BJJdZr7PV4wK6fj8nIyMjIyNhr3OxdXAYq7FHZwB6bDSzSh4sF0utChqo0NAvaT1hLzXwFinmCzmeDucEQK18TTaQoFgP7bNC%2BRZ4OT4T6gQogDFYk%2B1QxQlj19QGSAWKiLYp8P0Ag1Gbz1ULfWHLg9iUnQNK5QQJcukm04blKLH2GgEJCY%2BHzXAZWCvHKco3Bp6MIaCjSXXRJyOxeqhnzEaF93MfFGW%2FO16ZvDL5TM4MJIjujz%2FcHypkQuuzRwWJ93BKdIt%2BwCRAPl9kpe2Ikkb2mFgGlxh%2Fi40d3EHfdvoyMjIyMu43ylt%2FIAmGHnN5iIt7wKfbv01RAcJqFRl9lcjYQSnbQqKgC4fYOwSJt6N6trE0twZ9kN%2FPqNpTQeICvr4TLsDYC06U7BMjshS%2Bv1%2FaT7IwQYD5LcgRQXMT2FrBfBLjZ6151jDElk9tPFfpUgk2yregusX25BJbwAFEfM%2BYI6vGAti4bTtizB%2BTjfQCrERyhKb2X8D6A9wX75P4t4neBYJeP6pdhg%2FgQl8MWvytzeSTjgOQBynQdh%2FiXKdxOrGJ%2FRkZGRsb9QmXihGr5%2Bg8GGg9uTh%2BKoVZuNIzV%2BCwRucFBEyr1mVjx4irOxwM1BhirB6Q%2B2eNQi4eqR%2BaF6mELtoMzCR7V9RAFe%2FZvQogNiyY8FPSUTFsLp8TeTmMui5mtw7bcaT0Yw2AA4wFRQIlkgq%2B1DQrNhkmoxS5Jq%2Bu6bMAIGRECEANgXHTgWzwgBOhDH2l0oTQ4D8D5NMktBgNywAEMjo8rwATMZrPY7JGxBoJCkIBDQiAY09EGTUiBCWkUpISfGPR5AAwBfZiG2z7Ayc1yeKTxid39xBNwfHr4O0LA48ePFTvhYrF1r4tyAoz9n2MCqEuBtp%2F6GDR0oAYfG%2FR6wJExHYZHfhygsv7fEWCOj4bYmsP5A%2BpL4MkTfAnMlD4F%2Br3bobKvTyTA2P%2Fw7PN%2BAgq2QW8piqMCpTBwenoKvX0AHGkGtP2YAPvTEWA7QUTAudn7%2FNxtOG46wWNmDtpBEkBzN7rBEvAFHp%2BYTB%2Fq97qPAN4gHFqgBi8uLsC7qPCA6mg41G%2F%2BErByPwEXDdoNxRhOx%2BM5jPEzQugS0ht%2Bb1%2FY3gEnYMAIAOIBE29%2FhIDucE8tmMsNOgK4B1RHFu4UCRlMHzv0xzcajcfdXWDs2h8TArBCkoDUJYDLmz6w7ip3BFS0ve5wTRwAn6keMA9I3QYbfSZ0DKbyt%2B7OXjGI1idPcfNyAyfAMlCrzaGqphYrxHocLHRJVycnfGUcbtT%2BjIyMjIw9x7Nn8fJSzG0TmFtO8rZT%2BXT3S3ub%2BtKJbbLd5diTVp50%2BzahyeHSslJ%2FYPrU0fuazrZO2CZ92%2FZCCVXlGRiZKPJyPPRxyIFWeXLQBXJBKiq%2F3divEAN6ZwM200Qjm7EJBZeWm%2FPRWVCbYK7s7u2l4XaCz%2BlzgOfMfhMonXr7TWzeZb98dbgIzBT8Ub8eYYUqfZ4rVJ%2FMDbIDgPqTulJ%2FxvntWAtjIisqnwxOkGz0n077FARoY79GdA6HPE4rOy196NiMWHTZlSSApcOgXpy%2FfHV2joaNKu3ffsAnRcBf4K%2F6NcIG6tIxk3HyoXPjASqfUgXbYN5PzpL2njkR9QMjeDTVHDTCgRuxOegjoO0FvKzP%2Ft%2FgmVdI24%2BG7NIe8JX6Wv3dDyldMA%2B4YB5wwTygtd%2BdwRqaTqrLb1l73zTSN52CNpnHuQOYPsDblybgxfkXh%2FoVtr%2BN1DEBJdhRJyd%2FBd%2Fq1z%2BcbNrD17iVKyajcnv9arhOkRPgsruuD6DmNPwpDNrLw2CoTgHni4yALr0L29%2BtiKAEIPn868ejx%2F%2F8rpWP3OEOl5On9OwpcQm0MhafP%2Fey8f1uvDNIgGLQG8z4YO99ENgg95etwv4uYJYY8fUGHYH6j6fscHFZMftlAl9i%2B9XL73X3N%2Fn%2BZStOzfVfRvYXhrbdKOpEgVQTg%2FwsDuDD3kwOfQNMTJ5y%2B%2FltUDWLunyxnRF46IqlBzGMY4X7inggREFioIyMjIyMHWCIB6ZNKAcXseo3vLTQTkVE7348dlwJJSz0%2BwLfmi8BhZqfw3D4ww%2FwHVLnEd5%2FfgYvXsDZ3MlsvYUbbnDjDZ3MN3TJG4%2BbxjAaDl8TBri9qxEw1ccao2wTNAMLHo2f%2BsjrXwb%2F9qHoYqgPMBXJTVfOpmrZH23y6uvo0LHSyY6fHGwKfHJlAuMFvObjDYrIqxBgQi20h7Hd%2FnYVLmno%2BeaNUm%2FeeH2GCuopntnhBJAlI2AHo9CCh1I1QxUdAbqqGY9BBLwyc3W4wYVhvY8A4BoIc1l5M7vnPWphZW9%2FSes3n37y9a0uGqFwFQZsQQbd386DogpgEk%2BdzynsAZMJXq8%2Bns9NeukJ0PYrNATGGefJQlhkLo7DTXr%2By3bNiOsDvrXTz%2FC2q1DXZH84iRNwrP88Nj%2Bu2DjYEE6RBxD9Knj16ujVHC67A7422o02RwD3gB%2Bt7EblWvu9geOFxSnd3ROmT%2BnJyQkhoPlsxVONc%2F3TEdBos%2BjtA%2BZzcwHgTvD1cDjaYCcItA8w9i88A8b%2BmqSjc6Pvqd998QguEQPmQMeo23ODN86%2Bp0%2Fbn1buBkT6%2BoBhNZ%2FPYY4ZAHYb3PRd4LkZmPX68NRtMZn4ASvdA%2Bqf0jMA5MP9eeg28Nug9QiLnj5A33U1MAES6xHAUNpz%2F9zFAYE1gqQDMT3G6xI9pwdw%2FaIgKoHCS1YGlRnSq9yCjdXjgN3j%2BN27YyROHxmuNAeNKPpYuXIyIyMjYy0M8eros59MF%2FPT2c602T7eA7zvhJ9dr%2FvzDjXaLp4Yc5%2B0wllzxzHv3gdmMMM7%2FCcQzKgVBqYTmFn%2BZ%2BmKm8J7k0A5F%2FjgCfjQ1WBhQyiOqD0lYuqBb%2BAyzMw9Ha2G3m6c8qQx%2BAlqnIceQp%2BSb6i9UyQWbhr54%2BAjnZ0VzW2TAN0DmBT6PWmc6jDBE2PK2u%2BnF43dyP7Q0t1pOcX2fdRvH0mF2Q4JqN35rnHjVIeaXfIAVyUuw%2FaHCCiJy9iF5l1621zweI8KZrPZ9iJdb7DXJ3US0OSrtZ10imt7wHY7QesAzUMz1oZ3noB3qFJ%2FH18j97FYuw8QDN4oeKf30osvcSW2ExLo%2BVcbuAuo%2FsUIm8fMG9xocO3Ea19J9gFYivnHJ2KnyfovZlgW3v6ySx32abQiIyMjIyPjhlFDTLxpwIgFMnTp6A3g4IDKNY%2BstkwAMAoIAbasxBXqUWneSAWTMjt50lTqT29rFjvXohjsDNm2YPXDFlICmrJOZ3t6tHm8AiEAl0sCeLIIorIRt%2BcFbew%2FQRsoAXb4o1XSfoywzm0FTMAoYBNvLyFu8v8HpLBtD1iKgC17wHb7AI6d9wFbvguAIGTHd4E9wG7jgIyMjIyM%2B434c2R3HeV%2FFfx6jtZu6ijl8h59T655jhR%2BrdHzDOP6beABCheb8O8%2FWFXeOyzgf5oAhVYnKxP7CwaAf1afJu8bSrhS6tdaXeGnrRenOqOlz9d6QwYnA%2F3TLd%2BGE7qe3chA5YF5DfY0vK3adfOX%2FgyNp2BW25MHdxAB9qvRiiP3%2FXpQQFGYDU4%2BMi%2F%2F%2FXumXG8pjvaUAOsBGlf4jJt%2BYYEzeEzAdw06F19R3juM7D1wita86GR0CKfDHgLuXCc4Bri6vMLdfjMc4VNSUNsdodo2xu%2F1%2BXl%2FK5%2Baz8jIyMhYG%2Fz5gJTMF1GtKq%2Fa3rpyCvz5gJTMl9GtKq%2Fa3rpyCmfQ4WwZmS%2BkXFVetb115ST48wEf%2FAGcfG1iw%2BtWbpbS2vJ3nQxcVr3lH3z5h972FUTLzYpOVk7l5hD%2BeYcYwDcAnewOotrZ4OtrPDucqi%2FLRX0%2FRR4qx7Nn4U8g%2BqjffvuN6Gf%2BnC85vwauHjaYyubqvWYKY4VEfSUMitdnBCT1Ue63R5439m%2BOgCn6DroAAaHPVQxKth%2FwkJgHmG8bmQMsT0D6EjDfvhVRKO3ywOQUgRA7nmL1uawZmHf1k%2BDPBwQ6NdcJ%2Bk6Md1LA5f5ONdhJ8vZ5J0vLHT99srkGOjmJbd%2FG1r2Nriqnse1AZt1AalU5jW2HsuuG0qvKGRkZGRkZGRG0gcONyXsP9v8D0%2FIdJADiBNiXl3327WRGgOL%2F9HC%2F0XwlIURkRhC4tz6Z%2Ffu7fUf2gHvfB9z3u0BGRkZGRkbGplHcnkgguQoSqtUXuhbs%2FwPtMwqV0HUJAvj5vk32b8IDuL23yn7qAXZ5u32hbRX7d3o82Df1FZXvbh9QOfhyxldr%2F%2B3xgXU9oKmvsHyr7F%2FXA269%2FeveBXrsv7N9QALe%2FtvjA0kPWAXGbvebkbHn%2BD%2FJ5nMcHzx1UAAAAABJRU5ErkJggg%3D%3D)}.ui-icon-carat-1-n{background-position:0 0}.ui-icon-carat-1-ne{background-position:-16px 0}.ui-icon-carat-1-e{background-position:-32px 0}.ui-icon-carat-1-se{background-position:-48px 0}.ui-icon-carat-1-s{background-position:-64px 0}.ui-icon-carat-1-sw{background-position:-80px 0}.ui-icon-carat-1-w{background-position:-96px 0}.ui-icon-carat-1-nw{background-position:-112px 0}.ui-icon-carat-2-n-s{background-position:-128px 0}.ui-icon-carat-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-64px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-64px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:0 -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-off{background-position:-96px -144px}.ui-icon-radio-on{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-tl{-moz-border-radius-topleft:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px}.ui-corner-tr{-moz-border-radius-topright:4px;-webkit-border-top-right-radius:4px;border-top-right-radius:4px}.ui-corner-bl{-moz-border-radius-bottomleft:4px;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px}.ui-corner-br{-moz-border-radius-bottomright:4px;-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px}.ui-corner-top{-moz-border-radius-topleft:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topright:4px;-webkit-border-top-right-radius:4px;border-top-right-radius:4px}.ui-corner-bottom{-moz-border-radius-bottomleft:4px;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-moz-border-radius-bottomright:4px;-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px}.ui-corner-right{-moz-border-radius-topright:4px;-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-moz-border-radius-bottomright:4px;-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px}.ui-corner-left{-moz-border-radius-topleft:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-bottomleft:4px;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px}.ui-corner-all{-moz-border-radius:4px;-webkit-border-radius:4px;border-radius:4px}.ui-widget-overlay{background:#aaa url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAABkCAYAAAD0ZHJ6AAAAe0lEQVRoge3OMQHAIBAAMcC%2FkjdZJHTI0A4XBdkz86wfO18H3hRUBVVBVVAVVAVVQVVQFVQFVUFVUBVUBVVBVVAVVAVVQVVQFVQFVUFVUBVUBVVBVVAVVAVVQVVQFVQFVUFVUBVUBVVBVVAVVAVVQVVQFVQFVUFVUBVUF8O8A8WdY6opAAAAAElFTkSuQmCC) 50% 50% repeat-x;opacity:.30;filter:Alpha(Opacity=30)}.ui-widget-shadow{margin:-8px 0 0 -8px;padding:8px;background:#aaa url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAABkCAYAAAD0ZHJ6AAAAe0lEQVRoge3OMQHAIBAAMcC%2FkjdZJHTI0A4XBdkz86wfO18H3hRUBVVBVVAVVAVVQVVQFVQFVUFVUBVUBVVBVVAVVAVVQVVQFVQFVUFVUBVUBVVBVVAVVAVVQVVQFVQFVUFVUBVUBVVBVVAVVAVVQVVQFVQFVUFVUBVUF8O8A8WdY6opAAAAAElFTkSuQmCC) 50% 50% repeat-x;opacity:.30;filter:Alpha(Opacity=30);-moz-border-radius:8px;-webkit-border-radius:8px;border-radius:8px}#colorbox,#cboxOverlay,#cboxWrapper{position:absolute;top:0;left:0;z-index:9999;overflow:hidden}#cboxOverlay{position:fixed;width:100%;height:100%}#cboxMiddleLeft,#cboxBottomLeft{clear:left}#cboxContent{position:relative}#cboxLoadedContent{overflow:auto}#cboxTitle{margin:0}#cboxLoadingOverlay,#cboxLoadingGraphic{position:absolute;top:0;left:0;width:100%;height:100%}#cboxPrevious,#cboxNext,#cboxClose,#cboxSlideshow{cursor:pointer}.cboxPhoto{float:left;margin:auto;border:0;display:block;max-width:none}.cboxIframe{width:100%;height:100%;display:block;border:0}#colorbox,#cboxContent,#cboxLoadedContent{box-sizing:content-box}#cboxOverlay{background:#000}#cboxTopLeft{width:14px;height:14px;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAOEAAABLCAMAAACx6hDAAAABj1BMVEVPT0%2Fe3t7b29vS0tK7urq5uLjq6uqZmZmSkpJaWlrU1NTj4%2BPFxcWvr6%2BgoKBbW1u3t7c9PT27u7vCwsKsrKxiYWGqqqq5ublbWlpeXV2Xl5fExMSbmpq6ubmNjY18fHzy8vIrKystLS0sLCxNTU0uLi4wMDDNzc05OTns6%2Bvl5eUvLy%2Fq6ekqKipMTExDQ0M4ODgyMjI2NjbZ2dk6OjrY2NjMzMxLS0vAwMBCQkLo5%2BdHR0cxMTFKSkpBQUHv7u43NzdISEhFRUVRUVHx8fE7Ozs8PDwzMzNJSUnp6elGRkZQUFDr6upeXl7t7e1gYGCoqKjv7%2B81NTWKiorn5uZERESCgoJdXV3p6OhOTk51dXVAQEA%2BPj6np6fu7e2%2Bvr5cXFxSUlKJiYnOzs7s7OxTU1P29vbw8PB2dnZfX1%2Fm5eV4eHifn59qamqmpqbQ0NCOjo7Kysqzs7P4%2BPiDg4Otra3z8%2FM%2FPz80NDSrq6u%2Fv7%2FPz890dHRpaWmBgYH5%2Bfn08%2FNoaGjPzs7%2F%2F%2F%2BioqIRuwm9AAAGHUlEQVR4XsTXZY%2FjPBAA4PwUQxjKzMzLjMfMzPfiD7%2Bxm23ibjZVu5u7%2BVBL7ljyI3scW9pZOv59%2BgdjZ%2BNOeyzlyzXtv9B408%2FUynlp3L4r7bzYWC5E4a4xvAQYGkEA2%2FDG2GL6biBmHbGoz9pVhTBkuRCEhx0TzVNKKNspBczXdHtLnTRa9yC78MdhAND%2B6xaLh3%2B7bf1mhO3guEpooJfbaH56h2i7gOx5oCmlckBkwFzqGIi%2B9LdocllofMSUUnzrndui6wo5bnxVzJyC8BztO06A0HFeM4IIxLgBRAZsYAeI%2FvSf6DxASAkhFIS8vbaQ6aSw4ExBWNoyDyjFAUKM6Q9zy1ddEwBSSnStY3c0ncCo78j2px%2BYW6VLQqIoCgEhb68p5L4jWY6xyIsR4yHLgASiJ4S5JohCaICQQn8756suI5vC0KlkNKRlFBiEU9mJkN7KdYaRCpkvVh00y8HRbA6qMZkRZ8L7aJMolmUpiMe6u215ENafOEPe6Qlbk0K22tvoqTCGwob1lpyn0zN0PzIhB8aqzdFevFgLjGJ8b9TMA3EmrJuPAaiqqvVgbe3g9rFp8834z%2B2DtbUHvF8h%2B151QfUliKVWWKgWSUBFekI3%2FTGqzwstV2jdgFCulj%2BEjfhSbLlEELIDv82A0wmzXffVYJMqOBTcLkQhrLo8om5VkhAg1BnQE16kt81HpWiEfAmb%2F4cPeV5rDWKu8BAVGpRJ2IQ5ERemQsy73X6%2BGXdnRK1bSfb7%2FWSlqwHQJ%2FTSCyD3hIorVG5CKFdHr8KF907j5ao8FRppBxMFgDDfpCgkU%2Fi0n2DHq0UbPXGFz5AtHEy%2B9KwRkRCWMP4tXKhlTlpVWZqtIVBAEryGSRYBa6jyP9T5NfTSYQ0j2qVH%2BXLx3gJh4nRvEAPh3Ys6nNUbq8OCWIccLtahlpmdNLom1qGb3k5HVofSUX5UWyDMpXpxVoggdM9SIPrO0olwllZUAL4XzlId6NOwFF08S3l6hGcpO2iqrZNFwu1esRljQ0K%2Bh3XEg%2Ffrm8L3MEEU6O4%2BgR9Y9IT%2Fe8jTyWYE30NB%2BGk5Ib%2FT6ErInUbzXVKMdAMTCF1Dmk4gcCM9d6dh6RELtQXCz11BGHovpYH3UgorZ8NqUhh1jGx%2FevC9FOQg5O1vFa72thj73xZ47m2xv9LbInqh%2BD4MffABUeLAp5wYwfswEiHEcKU3fnalN37kwl%2Fs2M9LAkEUB%2FC0ml12VgmKLh2%2BcwtBOkWRUdIvIpJayYNdgkIIukUDdYz8x2tnt96OjQMS8hbzexFhDn6QN2%2FeIyHTnoZByJD%2FKJwL58L5TdM%2FFY5uIZ3dQvx0C2l3C%2BHuFsNKmuF7%2Ftlm6vixdnR8vfm744tQV%2FOOX9UJEen4SBpDpKm85J9Mr7Ya4BACK4ZgAYGUaICAIdLxmroro7DVFQHGCFEX1ss7BRpi0wBTYrN4PBDdVumE5reOFSKsFqcnqZERVQaElh3r%2BA0dn5pwQ3mzOiIcqBgmjo1wZoiLE%2FA3LEBOLUzAMInVYMrCtUVv1m1hWyTIEgZfSYTZCIt6%2BiVEFqqurPoopiJHhEhUe7rCpSdvlkloLvwQViKziYpAoeoiogNIQoTysVUSYV9FGl4hUXoWkYAOIXREcl5hQwJeIaW4EQ4A0D3qEAKyMfP%2FYW%2B261Aw16H%2FLu3MyF36936o6Qpy9ENW4eRvmv6kbxpmIcO7lEHIMFuwCf3zYaSaE8%2BH%2FEL2GZ9BWOY9zWc7d%2BwSMQzFcbzDCTqVCnJ3ok4HdrpAKZRSWnInicUOQiVISYeem25O6Xz%2B4%2FYJWaokg8Px4P3%2Bgw%2Fp%2BL79p%2FAkQyEkIQlJSEISkpCEJCQhCUmofcJWIhXa%2B1KfcJlIBsIDSmEzCatLt%2FCW96pGLNzu2LlbeBcbe%2BeNURhs6r2%2BcQGvuczhVh%2BtsMsK1qdX769DuLqYLQyHdc%2Blht4CqfDnMy0LZmScjI%2B%2FN7Y8llpNT4hYGABRVSYSIp1PCBmZygJRCn1pV87UHsKurnnAK3Tnebu6zCDOgydEKZwnlts%2F%2BsrOBpY4hdbYOBtZACIWghHm7JyRCu3efEP6T4Vv0sK5wmQ8JLkAAAAASUVORK5CYII%3D) no-repeat 0 0}#cboxTopCenter{height:14px;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAABLCAMAAACGCB2xAAAAM1BMVEVSUlJHR0dPT09BQUFLS0tQUFA6OjpgYGCKioozMzPS0tJaWlpRUVHy8vKJiYn%2F%2F%2F%2Fm5eV3dK93AAAAK0lEQVR4XqXBhQ2AQBAAsJ48bvtPywyE1GLGYUgtlPuT0%2Bb5abealNDScL0YiAPSV%2FRH9wAAAABJRU5ErkJggg%3D%3D) repeat-x top left}#cboxTopRight{width:14px;height:14px;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAOEAAABLCAMAAACx6hDAAAABj1BMVEVPT0%2Fe3t7b29vS0tK7urq5uLjq6uqZmZmSkpJaWlrU1NTj4%2BPFxcWvr6%2BgoKBbW1u3t7c9PT27u7vCwsKsrKxiYWGqqqq5ublbWlpeXV2Xl5fExMSbmpq6ubmNjY18fHzy8vIrKystLS0sLCxNTU0uLi4wMDDNzc05OTns6%2Bvl5eUvLy%2Fq6ekqKipMTExDQ0M4ODgyMjI2NjbZ2dk6OjrY2NjMzMxLS0vAwMBCQkLo5%2BdHR0cxMTFKSkpBQUHv7u43NzdISEhFRUVRUVHx8fE7Ozs8PDwzMzNJSUnp6elGRkZQUFDr6upeXl7t7e1gYGCoqKjv7%2B81NTWKiorn5uZERESCgoJdXV3p6OhOTk51dXVAQEA%2BPj6np6fu7e2%2Bvr5cXFxSUlKJiYnOzs7s7OxTU1P29vbw8PB2dnZfX1%2Fm5eV4eHifn59qamqmpqbQ0NCOjo7Kysqzs7P4%2BPiDg4Otra3z8%2FM%2FPz80NDSrq6u%2Fv7%2FPz890dHRpaWmBgYH5%2Bfn08%2FNoaGjPzs7%2F%2F%2F%2BioqIRuwm9AAAGHUlEQVR4XsTXZY%2FjPBAA4PwUQxjKzMzLjMfMzPfiD7%2Bxm23ibjZVu5u7%2BVBL7ljyI3scW9pZOv59%2BgdjZ%2BNOeyzlyzXtv9B408%2FUynlp3L4r7bzYWC5E4a4xvAQYGkEA2%2FDG2GL6biBmHbGoz9pVhTBkuRCEhx0TzVNKKNspBczXdHtLnTRa9yC78MdhAND%2B6xaLh3%2B7bf1mhO3guEpooJfbaH56h2i7gOx5oCmlckBkwFzqGIi%2B9LdocllofMSUUnzrndui6wo5bnxVzJyC8BztO06A0HFeM4IIxLgBRAZsYAeI%2FvSf6DxASAkhFIS8vbaQ6aSw4ExBWNoyDyjFAUKM6Q9zy1ddEwBSSnStY3c0ncCo78j2px%2BYW6VLQqIoCgEhb68p5L4jWY6xyIsR4yHLgASiJ4S5JohCaICQQn8756suI5vC0KlkNKRlFBiEU9mJkN7KdYaRCpkvVh00y8HRbA6qMZkRZ8L7aJMolmUpiMe6u215ENafOEPe6Qlbk0K22tvoqTCGwob1lpyn0zN0PzIhB8aqzdFevFgLjGJ8b9TMA3EmrJuPAaiqqvVgbe3g9rFp8834z%2B2DtbUHvF8h%2B151QfUliKVWWKgWSUBFekI3%2FTGqzwstV2jdgFCulj%2BEjfhSbLlEELIDv82A0wmzXffVYJMqOBTcLkQhrLo8om5VkhAg1BnQE16kt81HpWiEfAmb%2F4cPeV5rDWKu8BAVGpRJ2IQ5ERemQsy73X6%2BGXdnRK1bSfb7%2FWSlqwHQJ%2FTSCyD3hIorVG5CKFdHr8KF907j5ao8FRppBxMFgDDfpCgkU%2Fi0n2DHq0UbPXGFz5AtHEy%2B9KwRkRCWMP4tXKhlTlpVWZqtIVBAEryGSRYBa6jyP9T5NfTSYQ0j2qVH%2BXLx3gJh4nRvEAPh3Ys6nNUbq8OCWIccLtahlpmdNLom1qGb3k5HVofSUX5UWyDMpXpxVoggdM9SIPrO0olwllZUAL4XzlId6NOwFF08S3l6hGcpO2iqrZNFwu1esRljQ0K%2Bh3XEg%2Ffrm8L3MEEU6O4%2BgR9Y9IT%2Fe8jTyWYE30NB%2BGk5Ib%2FT6ErInUbzXVKMdAMTCF1Dmk4gcCM9d6dh6RELtQXCz11BGHovpYH3UgorZ8NqUhh1jGx%2FevC9FOQg5O1vFa72thj73xZ47m2xv9LbInqh%2BD4MffABUeLAp5wYwfswEiHEcKU3fnalN37kwl%2Fs2M9LAkEUB%2FC0ml12VgmKLh2%2BcwtBOkWRUdIvIpJayYNdgkIIukUDdYz8x2tnt96OjQMS8hbzexFhDn6QN2%2FeIyHTnoZByJD%2FKJwL58L5TdM%2FFY5uIZ3dQvx0C2l3C%2BHuFsNKmuF7%2Ftlm6vixdnR8vfm744tQV%2FOOX9UJEen4SBpDpKm85J9Mr7Ya4BACK4ZgAYGUaICAIdLxmroro7DVFQHGCFEX1ss7BRpi0wBTYrN4PBDdVumE5reOFSKsFqcnqZERVQaElh3r%2BA0dn5pwQ3mzOiIcqBgmjo1wZoiLE%2FA3LEBOLUzAMInVYMrCtUVv1m1hWyTIEgZfSYTZCIt6%2BiVEFqqurPoopiJHhEhUe7rCpSdvlkloLvwQViKziYpAoeoiogNIQoTysVUSYV9FGl4hUXoWkYAOIXREcl5hQwJeIaW4EQ4A0D3qEAKyMfP%2FYW%2B261Aw16H%2FLu3MyF36936o6Qpy9ENW4eRvmv6kbxpmIcO7lEHIMFuwCf3zYaSaE8%2BH%2FEL2GZ9BWOY9zWc7d%2BwSMQzFcbzDCTqVCnJ3ok4HdrpAKZRSWnInicUOQiVISYeem25O6Xz%2B4%2FYJWaokg8Px4P3%2Bgw%2Fp%2BL79p%2FAkQyEkIQlJSEISkpCEJCQhCUmofcJWIhXa%2B1KfcJlIBsIDSmEzCatLt%2FCW96pGLNzu2LlbeBcbe%2BeNURhs6r2%2BcQGvuczhVh%2BtsMsK1qdX769DuLqYLQyHdc%2Blht4CqfDnMy0LZmScjI%2B%2FN7Y8llpNT4hYGABRVSYSIp1PCBmZygJRCn1pV87UHsKurnnAK3Tnebu6zCDOgydEKZwnlts%2F%2BsrOBpY4hdbYOBtZACIWghHm7JyRCu3efEP6T4Vv0sK5wmQ8JLkAAAAASUVORK5CYII%3D) no-repeat -36px 0}#cboxBottomLeft{width:14px;height:43px;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAOEAAABLCAMAAACx6hDAAAABj1BMVEVPT0%2Fe3t7b29vS0tK7urq5uLjq6uqZmZmSkpJaWlrU1NTj4%2BPFxcWvr6%2BgoKBbW1u3t7c9PT27u7vCwsKsrKxiYWGqqqq5ublbWlpeXV2Xl5fExMSbmpq6ubmNjY18fHzy8vIrKystLS0sLCxNTU0uLi4wMDDNzc05OTns6%2Bvl5eUvLy%2Fq6ekqKipMTExDQ0M4ODgyMjI2NjbZ2dk6OjrY2NjMzMxLS0vAwMBCQkLo5%2BdHR0cxMTFKSkpBQUHv7u43NzdISEhFRUVRUVHx8fE7Ozs8PDwzMzNJSUnp6elGRkZQUFDr6upeXl7t7e1gYGCoqKjv7%2B81NTWKiorn5uZERESCgoJdXV3p6OhOTk51dXVAQEA%2BPj6np6fu7e2%2Bvr5cXFxSUlKJiYnOzs7s7OxTU1P29vbw8PB2dnZfX1%2Fm5eV4eHifn59qamqmpqbQ0NCOjo7Kysqzs7P4%2BPiDg4Otra3z8%2FM%2FPz80NDSrq6u%2Fv7%2FPz890dHRpaWmBgYH5%2Bfn08%2FNoaGjPzs7%2F%2F%2F%2BioqIRuwm9AAAGHUlEQVR4XsTXZY%2FjPBAA4PwUQxjKzMzLjMfMzPfiD7%2Bxm23ibjZVu5u7%2BVBL7ljyI3scW9pZOv59%2BgdjZ%2BNOeyzlyzXtv9B408%2FUynlp3L4r7bzYWC5E4a4xvAQYGkEA2%2FDG2GL6biBmHbGoz9pVhTBkuRCEhx0TzVNKKNspBczXdHtLnTRa9yC78MdhAND%2B6xaLh3%2B7bf1mhO3guEpooJfbaH56h2i7gOx5oCmlckBkwFzqGIi%2B9LdocllofMSUUnzrndui6wo5bnxVzJyC8BztO06A0HFeM4IIxLgBRAZsYAeI%2FvSf6DxASAkhFIS8vbaQ6aSw4ExBWNoyDyjFAUKM6Q9zy1ddEwBSSnStY3c0ncCo78j2px%2BYW6VLQqIoCgEhb68p5L4jWY6xyIsR4yHLgASiJ4S5JohCaICQQn8756suI5vC0KlkNKRlFBiEU9mJkN7KdYaRCpkvVh00y8HRbA6qMZkRZ8L7aJMolmUpiMe6u215ENafOEPe6Qlbk0K22tvoqTCGwob1lpyn0zN0PzIhB8aqzdFevFgLjGJ8b9TMA3EmrJuPAaiqqvVgbe3g9rFp8834z%2B2DtbUHvF8h%2B151QfUliKVWWKgWSUBFekI3%2FTGqzwstV2jdgFCulj%2BEjfhSbLlEELIDv82A0wmzXffVYJMqOBTcLkQhrLo8om5VkhAg1BnQE16kt81HpWiEfAmb%2F4cPeV5rDWKu8BAVGpRJ2IQ5ERemQsy73X6%2BGXdnRK1bSfb7%2FWSlqwHQJ%2FTSCyD3hIorVG5CKFdHr8KF907j5ao8FRppBxMFgDDfpCgkU%2Fi0n2DHq0UbPXGFz5AtHEy%2B9KwRkRCWMP4tXKhlTlpVWZqtIVBAEryGSRYBa6jyP9T5NfTSYQ0j2qVH%2BXLx3gJh4nRvEAPh3Ys6nNUbq8OCWIccLtahlpmdNLom1qGb3k5HVofSUX5UWyDMpXpxVoggdM9SIPrO0olwllZUAL4XzlId6NOwFF08S3l6hGcpO2iqrZNFwu1esRljQ0K%2Bh3XEg%2Ffrm8L3MEEU6O4%2BgR9Y9IT%2Fe8jTyWYE30NB%2BGk5Ib%2FT6ErInUbzXVKMdAMTCF1Dmk4gcCM9d6dh6RELtQXCz11BGHovpYH3UgorZ8NqUhh1jGx%2FevC9FOQg5O1vFa72thj73xZ47m2xv9LbInqh%2BD4MffABUeLAp5wYwfswEiHEcKU3fnalN37kwl%2Fs2M9LAkEUB%2FC0ml12VgmKLh2%2BcwtBOkWRUdIvIpJayYNdgkIIukUDdYz8x2tnt96OjQMS8hbzexFhDn6QN2%2FeIyHTnoZByJD%2FKJwL58L5TdM%2FFY5uIZ3dQvx0C2l3C%2BHuFsNKmuF7%2Ftlm6vixdnR8vfm744tQV%2FOOX9UJEen4SBpDpKm85J9Mr7Ya4BACK4ZgAYGUaICAIdLxmroro7DVFQHGCFEX1ss7BRpi0wBTYrN4PBDdVumE5reOFSKsFqcnqZERVQaElh3r%2BA0dn5pwQ3mzOiIcqBgmjo1wZoiLE%2FA3LEBOLUzAMInVYMrCtUVv1m1hWyTIEgZfSYTZCIt6%2BiVEFqqurPoopiJHhEhUe7rCpSdvlkloLvwQViKziYpAoeoiogNIQoTysVUSYV9FGl4hUXoWkYAOIXREcl5hQwJeIaW4EQ4A0D3qEAKyMfP%2FYW%2B261Aw16H%2FLu3MyF36936o6Qpy9ENW4eRvmv6kbxpmIcO7lEHIMFuwCf3zYaSaE8%2BH%2FEL2GZ9BWOY9zWc7d%2BwSMQzFcbzDCTqVCnJ3ok4HdrpAKZRSWnInicUOQiVISYeem25O6Xz%2B4%2FYJWaokg8Px4P3%2Bgw%2Fp%2BL79p%2FAkQyEkIQlJSEISkpCEJCQhCUmofcJWIhXa%2B1KfcJlIBsIDSmEzCatLt%2FCW96pGLNzu2LlbeBcbe%2BeNURhs6r2%2BcQGvuczhVh%2BtsMsK1qdX769DuLqYLQyHdc%2Blht4CqfDnMy0LZmScjI%2B%2FN7Y8llpNT4hYGABRVSYSIp1PCBmZygJRCn1pV87UHsKurnnAK3Tnebu6zCDOgydEKZwnlts%2F%2BsrOBpY4hdbYOBtZACIWghHm7JyRCu3efEP6T4Vv0sK5wmQ8JLkAAAAASUVORK5CYII%3D) no-repeat 0 -32px}#cboxBottomCenter{height:43px;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAABLCAMAAACGCB2xAAAAM1BMVEVSUlJHR0dPT09BQUFLS0tQUFA6OjpgYGCKioozMzPS0tJaWlpRUVHy8vKJiYn%2F%2F%2F%2Fm5eV3dK93AAAAK0lEQVR4XqXBhQ2AQBAAsJ48bvtPywyE1GLGYUgtlPuT0%2Bb5abealNDScL0YiAPSV%2FRH9wAAAABJRU5ErkJggg%3D%3D) repeat-x bottom left}#cboxBottomRight{width:14px;height:43px;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAOEAAABLCAMAAACx6hDAAAABj1BMVEVPT0%2Fe3t7b29vS0tK7urq5uLjq6uqZmZmSkpJaWlrU1NTj4%2BPFxcWvr6%2BgoKBbW1u3t7c9PT27u7vCwsKsrKxiYWGqqqq5ublbWlpeXV2Xl5fExMSbmpq6ubmNjY18fHzy8vIrKystLS0sLCxNTU0uLi4wMDDNzc05OTns6%2Bvl5eUvLy%2Fq6ekqKipMTExDQ0M4ODgyMjI2NjbZ2dk6OjrY2NjMzMxLS0vAwMBCQkLo5%2BdHR0cxMTFKSkpBQUHv7u43NzdISEhFRUVRUVHx8fE7Ozs8PDwzMzNJSUnp6elGRkZQUFDr6upeXl7t7e1gYGCoqKjv7%2B81NTWKiorn5uZERESCgoJdXV3p6OhOTk51dXVAQEA%2BPj6np6fu7e2%2Bvr5cXFxSUlKJiYnOzs7s7OxTU1P29vbw8PB2dnZfX1%2Fm5eV4eHifn59qamqmpqbQ0NCOjo7Kysqzs7P4%2BPiDg4Otra3z8%2FM%2FPz80NDSrq6u%2Fv7%2FPz890dHRpaWmBgYH5%2Bfn08%2FNoaGjPzs7%2F%2F%2F%2BioqIRuwm9AAAGHUlEQVR4XsTXZY%2FjPBAA4PwUQxjKzMzLjMfMzPfiD7%2Bxm23ibjZVu5u7%2BVBL7ljyI3scW9pZOv59%2BgdjZ%2BNOeyzlyzXtv9B408%2FUynlp3L4r7bzYWC5E4a4xvAQYGkEA2%2FDG2GL6biBmHbGoz9pVhTBkuRCEhx0TzVNKKNspBczXdHtLnTRa9yC78MdhAND%2B6xaLh3%2B7bf1mhO3guEpooJfbaH56h2i7gOx5oCmlckBkwFzqGIi%2B9LdocllofMSUUnzrndui6wo5bnxVzJyC8BztO06A0HFeM4IIxLgBRAZsYAeI%2FvSf6DxASAkhFIS8vbaQ6aSw4ExBWNoyDyjFAUKM6Q9zy1ddEwBSSnStY3c0ncCo78j2px%2BYW6VLQqIoCgEhb68p5L4jWY6xyIsR4yHLgASiJ4S5JohCaICQQn8756suI5vC0KlkNKRlFBiEU9mJkN7KdYaRCpkvVh00y8HRbA6qMZkRZ8L7aJMolmUpiMe6u215ENafOEPe6Qlbk0K22tvoqTCGwob1lpyn0zN0PzIhB8aqzdFevFgLjGJ8b9TMA3EmrJuPAaiqqvVgbe3g9rFp8834z%2B2DtbUHvF8h%2B151QfUliKVWWKgWSUBFekI3%2FTGqzwstV2jdgFCulj%2BEjfhSbLlEELIDv82A0wmzXffVYJMqOBTcLkQhrLo8om5VkhAg1BnQE16kt81HpWiEfAmb%2F4cPeV5rDWKu8BAVGpRJ2IQ5ERemQsy73X6%2BGXdnRK1bSfb7%2FWSlqwHQJ%2FTSCyD3hIorVG5CKFdHr8KF907j5ao8FRppBxMFgDDfpCgkU%2Fi0n2DHq0UbPXGFz5AtHEy%2B9KwRkRCWMP4tXKhlTlpVWZqtIVBAEryGSRYBa6jyP9T5NfTSYQ0j2qVH%2BXLx3gJh4nRvEAPh3Ys6nNUbq8OCWIccLtahlpmdNLom1qGb3k5HVofSUX5UWyDMpXpxVoggdM9SIPrO0olwllZUAL4XzlId6NOwFF08S3l6hGcpO2iqrZNFwu1esRljQ0K%2Bh3XEg%2Ffrm8L3MEEU6O4%2BgR9Y9IT%2Fe8jTyWYE30NB%2BGk5Ib%2FT6ErInUbzXVKMdAMTCF1Dmk4gcCM9d6dh6RELtQXCz11BGHovpYH3UgorZ8NqUhh1jGx%2FevC9FOQg5O1vFa72thj73xZ47m2xv9LbInqh%2BD4MffABUeLAp5wYwfswEiHEcKU3fnalN37kwl%2Fs2M9LAkEUB%2FC0ml12VgmKLh2%2BcwtBOkWRUdIvIpJayYNdgkIIukUDdYz8x2tnt96OjQMS8hbzexFhDn6QN2%2FeIyHTnoZByJD%2FKJwL58L5TdM%2FFY5uIZ3dQvx0C2l3C%2BHuFsNKmuF7%2Ftlm6vixdnR8vfm744tQV%2FOOX9UJEen4SBpDpKm85J9Mr7Ya4BACK4ZgAYGUaICAIdLxmroro7DVFQHGCFEX1ss7BRpi0wBTYrN4PBDdVumE5reOFSKsFqcnqZERVQaElh3r%2BA0dn5pwQ3mzOiIcqBgmjo1wZoiLE%2FA3LEBOLUzAMInVYMrCtUVv1m1hWyTIEgZfSYTZCIt6%2BiVEFqqurPoopiJHhEhUe7rCpSdvlkloLvwQViKziYpAoeoiogNIQoTysVUSYV9FGl4hUXoWkYAOIXREcl5hQwJeIaW4EQ4A0D3qEAKyMfP%2FYW%2B261Aw16H%2FLu3MyF36936o6Qpy9ENW4eRvmv6kbxpmIcO7lEHIMFuwCf3zYaSaE8%2BH%2FEL2GZ9BWOY9zWc7d%2BwSMQzFcbzDCTqVCnJ3ok4HdrpAKZRSWnInicUOQiVISYeem25O6Xz%2B4%2FYJWaokg8Px4P3%2Bgw%2Fp%2BL79p%2FAkQyEkIQlJSEISkpCEJCQhCUmofcJWIhXa%2B1KfcJlIBsIDSmEzCatLt%2FCW96pGLNzu2LlbeBcbe%2BeNURhs6r2%2BcQGvuczhVh%2BtsMsK1qdX769DuLqYLQyHdc%2Blht4CqfDnMy0LZmScjI%2B%2FN7Y8llpNT4hYGABRVSYSIp1PCBmZygJRCn1pV87UHsKurnnAK3Tnebu6zCDOgydEKZwnlts%2F%2BsrOBpY4hdbYOBtZACIWghHm7JyRCu3efEP6T4Vv0sK5wmQ8JLkAAAAASUVORK5CYII%3D) no-repeat -36px -32px}#cboxMiddleLeft{width:14px;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAOEAAABLCAMAAACx6hDAAAABj1BMVEVPT0%2Fe3t7b29vS0tK7urq5uLjq6uqZmZmSkpJaWlrU1NTj4%2BPFxcWvr6%2BgoKBbW1u3t7c9PT27u7vCwsKsrKxiYWGqqqq5ublbWlpeXV2Xl5fExMSbmpq6ubmNjY18fHzy8vIrKystLS0sLCxNTU0uLi4wMDDNzc05OTns6%2Bvl5eUvLy%2Fq6ekqKipMTExDQ0M4ODgyMjI2NjbZ2dk6OjrY2NjMzMxLS0vAwMBCQkLo5%2BdHR0cxMTFKSkpBQUHv7u43NzdISEhFRUVRUVHx8fE7Ozs8PDwzMzNJSUnp6elGRkZQUFDr6upeXl7t7e1gYGCoqKjv7%2B81NTWKiorn5uZERESCgoJdXV3p6OhOTk51dXVAQEA%2BPj6np6fu7e2%2Bvr5cXFxSUlKJiYnOzs7s7OxTU1P29vbw8PB2dnZfX1%2Fm5eV4eHifn59qamqmpqbQ0NCOjo7Kysqzs7P4%2BPiDg4Otra3z8%2FM%2FPz80NDSrq6u%2Fv7%2FPz890dHRpaWmBgYH5%2Bfn08%2FNoaGjPzs7%2F%2F%2F%2BioqIRuwm9AAAGHUlEQVR4XsTXZY%2FjPBAA4PwUQxjKzMzLjMfMzPfiD7%2Bxm23ibjZVu5u7%2BVBL7ljyI3scW9pZOv59%2BgdjZ%2BNOeyzlyzXtv9B408%2FUynlp3L4r7bzYWC5E4a4xvAQYGkEA2%2FDG2GL6biBmHbGoz9pVhTBkuRCEhx0TzVNKKNspBczXdHtLnTRa9yC78MdhAND%2B6xaLh3%2B7bf1mhO3guEpooJfbaH56h2i7gOx5oCmlckBkwFzqGIi%2B9LdocllofMSUUnzrndui6wo5bnxVzJyC8BztO06A0HFeM4IIxLgBRAZsYAeI%2FvSf6DxASAkhFIS8vbaQ6aSw4ExBWNoyDyjFAUKM6Q9zy1ddEwBSSnStY3c0ncCo78j2px%2BYW6VLQqIoCgEhb68p5L4jWY6xyIsR4yHLgASiJ4S5JohCaICQQn8756suI5vC0KlkNKRlFBiEU9mJkN7KdYaRCpkvVh00y8HRbA6qMZkRZ8L7aJMolmUpiMe6u215ENafOEPe6Qlbk0K22tvoqTCGwob1lpyn0zN0PzIhB8aqzdFevFgLjGJ8b9TMA3EmrJuPAaiqqvVgbe3g9rFp8834z%2B2DtbUHvF8h%2B151QfUliKVWWKgWSUBFekI3%2FTGqzwstV2jdgFCulj%2BEjfhSbLlEELIDv82A0wmzXffVYJMqOBTcLkQhrLo8om5VkhAg1BnQE16kt81HpWiEfAmb%2F4cPeV5rDWKu8BAVGpRJ2IQ5ERemQsy73X6%2BGXdnRK1bSfb7%2FWSlqwHQJ%2FTSCyD3hIorVG5CKFdHr8KF907j5ao8FRppBxMFgDDfpCgkU%2Fi0n2DHq0UbPXGFz5AtHEy%2B9KwRkRCWMP4tXKhlTlpVWZqtIVBAEryGSRYBa6jyP9T5NfTSYQ0j2qVH%2BXLx3gJh4nRvEAPh3Ys6nNUbq8OCWIccLtahlpmdNLom1qGb3k5HVofSUX5UWyDMpXpxVoggdM9SIPrO0olwllZUAL4XzlId6NOwFF08S3l6hGcpO2iqrZNFwu1esRljQ0K%2Bh3XEg%2Ffrm8L3MEEU6O4%2BgR9Y9IT%2Fe8jTyWYE30NB%2BGk5Ib%2FT6ErInUbzXVKMdAMTCF1Dmk4gcCM9d6dh6RELtQXCz11BGHovpYH3UgorZ8NqUhh1jGx%2FevC9FOQg5O1vFa72thj73xZ47m2xv9LbInqh%2BD4MffABUeLAp5wYwfswEiHEcKU3fnalN37kwl%2Fs2M9LAkEUB%2FC0ml12VgmKLh2%2BcwtBOkWRUdIvIpJayYNdgkIIukUDdYz8x2tnt96OjQMS8hbzexFhDn6QN2%2FeIyHTnoZByJD%2FKJwL58L5TdM%2FFY5uIZ3dQvx0C2l3C%2BHuFsNKmuF7%2Ftlm6vixdnR8vfm744tQV%2FOOX9UJEen4SBpDpKm85J9Mr7Ya4BACK4ZgAYGUaICAIdLxmroro7DVFQHGCFEX1ss7BRpi0wBTYrN4PBDdVumE5reOFSKsFqcnqZERVQaElh3r%2BA0dn5pwQ3mzOiIcqBgmjo1wZoiLE%2FA3LEBOLUzAMInVYMrCtUVv1m1hWyTIEgZfSYTZCIt6%2BiVEFqqurPoopiJHhEhUe7rCpSdvlkloLvwQViKziYpAoeoiogNIQoTysVUSYV9FGl4hUXoWkYAOIXREcl5hQwJeIaW4EQ4A0D3qEAKyMfP%2FYW%2B261Aw16H%2FLu3MyF36936o6Qpy9ENW4eRvmv6kbxpmIcO7lEHIMFuwCf3zYaSaE8%2BH%2FEL2GZ9BWOY9zWc7d%2BwSMQzFcbzDCTqVCnJ3ok4HdrpAKZRSWnInicUOQiVISYeem25O6Xz%2B4%2FYJWaokg8Px4P3%2Bgw%2Fp%2BL79p%2FAkQyEkIQlJSEISkpCEJCQhCUmofcJWIhXa%2B1KfcJlIBsIDSmEzCatLt%2FCW96pGLNzu2LlbeBcbe%2BeNURhs6r2%2BcQGvuczhVh%2BtsMsK1qdX769DuLqYLQyHdc%2Blht4CqfDnMy0LZmScjI%2B%2FN7Y8llpNT4hYGABRVSYSIp1PCBmZygJRCn1pV87UHsKurnnAK3Tnebu6zCDOgydEKZwnlts%2F%2BsrOBpY4hdbYOBtZACIWghHm7JyRCu3efEP6T4Vv0sK5wmQ8JLkAAAAASUVORK5CYII%3D) repeat-y -175px 0}#cboxMiddleRight{width:14px;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAOEAAABLCAMAAACx6hDAAAABj1BMVEVPT0%2Fe3t7b29vS0tK7urq5uLjq6uqZmZmSkpJaWlrU1NTj4%2BPFxcWvr6%2BgoKBbW1u3t7c9PT27u7vCwsKsrKxiYWGqqqq5ublbWlpeXV2Xl5fExMSbmpq6ubmNjY18fHzy8vIrKystLS0sLCxNTU0uLi4wMDDNzc05OTns6%2Bvl5eUvLy%2Fq6ekqKipMTExDQ0M4ODgyMjI2NjbZ2dk6OjrY2NjMzMxLS0vAwMBCQkLo5%2BdHR0cxMTFKSkpBQUHv7u43NzdISEhFRUVRUVHx8fE7Ozs8PDwzMzNJSUnp6elGRkZQUFDr6upeXl7t7e1gYGCoqKjv7%2B81NTWKiorn5uZERESCgoJdXV3p6OhOTk51dXVAQEA%2BPj6np6fu7e2%2Bvr5cXFxSUlKJiYnOzs7s7OxTU1P29vbw8PB2dnZfX1%2Fm5eV4eHifn59qamqmpqbQ0NCOjo7Kysqzs7P4%2BPiDg4Otra3z8%2FM%2FPz80NDSrq6u%2Fv7%2FPz890dHRpaWmBgYH5%2Bfn08%2FNoaGjPzs7%2F%2F%2F%2BioqIRuwm9AAAGHUlEQVR4XsTXZY%2FjPBAA4PwUQxjKzMzLjMfMzPfiD7%2Bxm23ibjZVu5u7%2BVBL7ljyI3scW9pZOv59%2BgdjZ%2BNOeyzlyzXtv9B408%2FUynlp3L4r7bzYWC5E4a4xvAQYGkEA2%2FDG2GL6biBmHbGoz9pVhTBkuRCEhx0TzVNKKNspBczXdHtLnTRa9yC78MdhAND%2B6xaLh3%2B7bf1mhO3guEpooJfbaH56h2i7gOx5oCmlckBkwFzqGIi%2B9LdocllofMSUUnzrndui6wo5bnxVzJyC8BztO06A0HFeM4IIxLgBRAZsYAeI%2FvSf6DxASAkhFIS8vbaQ6aSw4ExBWNoyDyjFAUKM6Q9zy1ddEwBSSnStY3c0ncCo78j2px%2BYW6VLQqIoCgEhb68p5L4jWY6xyIsR4yHLgASiJ4S5JohCaICQQn8756suI5vC0KlkNKRlFBiEU9mJkN7KdYaRCpkvVh00y8HRbA6qMZkRZ8L7aJMolmUpiMe6u215ENafOEPe6Qlbk0K22tvoqTCGwob1lpyn0zN0PzIhB8aqzdFevFgLjGJ8b9TMA3EmrJuPAaiqqvVgbe3g9rFp8834z%2B2DtbUHvF8h%2B151QfUliKVWWKgWSUBFekI3%2FTGqzwstV2jdgFCulj%2BEjfhSbLlEELIDv82A0wmzXffVYJMqOBTcLkQhrLo8om5VkhAg1BnQE16kt81HpWiEfAmb%2F4cPeV5rDWKu8BAVGpRJ2IQ5ERemQsy73X6%2BGXdnRK1bSfb7%2FWSlqwHQJ%2FTSCyD3hIorVG5CKFdHr8KF907j5ao8FRppBxMFgDDfpCgkU%2Fi0n2DHq0UbPXGFz5AtHEy%2B9KwRkRCWMP4tXKhlTlpVWZqtIVBAEryGSRYBa6jyP9T5NfTSYQ0j2qVH%2BXLx3gJh4nRvEAPh3Ys6nNUbq8OCWIccLtahlpmdNLom1qGb3k5HVofSUX5UWyDMpXpxVoggdM9SIPrO0olwllZUAL4XzlId6NOwFF08S3l6hGcpO2iqrZNFwu1esRljQ0K%2Bh3XEg%2Ffrm8L3MEEU6O4%2BgR9Y9IT%2Fe8jTyWYE30NB%2BGk5Ib%2FT6ErInUbzXVKMdAMTCF1Dmk4gcCM9d6dh6RELtQXCz11BGHovpYH3UgorZ8NqUhh1jGx%2FevC9FOQg5O1vFa72thj73xZ47m2xv9LbInqh%2BD4MffABUeLAp5wYwfswEiHEcKU3fnalN37kwl%2Fs2M9LAkEUB%2FC0ml12VgmKLh2%2BcwtBOkWRUdIvIpJayYNdgkIIukUDdYz8x2tnt96OjQMS8hbzexFhDn6QN2%2FeIyHTnoZByJD%2FKJwL58L5TdM%2FFY5uIZ3dQvx0C2l3C%2BHuFsNKmuF7%2Ftlm6vixdnR8vfm744tQV%2FOOX9UJEen4SBpDpKm85J9Mr7Ya4BACK4ZgAYGUaICAIdLxmroro7DVFQHGCFEX1ss7BRpi0wBTYrN4PBDdVumE5reOFSKsFqcnqZERVQaElh3r%2BA0dn5pwQ3mzOiIcqBgmjo1wZoiLE%2FA3LEBOLUzAMInVYMrCtUVv1m1hWyTIEgZfSYTZCIt6%2BiVEFqqurPoopiJHhEhUe7rCpSdvlkloLvwQViKziYpAoeoiogNIQoTysVUSYV9FGl4hUXoWkYAOIXREcl5hQwJeIaW4EQ4A0D3qEAKyMfP%2FYW%2B261Aw16H%2FLu3MyF36936o6Qpy9ENW4eRvmv6kbxpmIcO7lEHIMFuwCf3zYaSaE8%2BH%2FEL2GZ9BWOY9zWc7d%2BwSMQzFcbzDCTqVCnJ3ok4HdrpAKZRSWnInicUOQiVISYeem25O6Xz%2B4%2FYJWaokg8Px4P3%2Bgw%2Fp%2BL79p%2FAkQyEkIQlJSEISkpCEJCQhCUmofcJWIhXa%2B1KfcJlIBsIDSmEzCatLt%2FCW96pGLNzu2LlbeBcbe%2BeNURhs6r2%2BcQGvuczhVh%2BtsMsK1qdX769DuLqYLQyHdc%2Blht4CqfDnMy0LZmScjI%2B%2FN7Y8llpNT4hYGABRVSYSIp1PCBmZygJRCn1pV87UHsKurnnAK3Tnebu6zCDOgydEKZwnlts%2F%2BsrOBpY4hdbYOBtZACIWghHm7JyRCu3efEP6T4Vv0sK5wmQ8JLkAAAAASUVORK5CYII%3D) repeat-y -211px 0}#cboxContent{background:#fff;overflow:visible}.cboxIframe{background:#fff}#cboxError{padding:50px;border:1px solid #ccc}#cboxLoadedContent{margin-bottom:5px}#cboxLoadingOverlay{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoBAMAAAB%2B0KVeAAAAElBMVEX%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F8AAAD%2F%2F%2F%2F%2F%2F%2F9H1zSfAAAABXRSTlPvgBAAz5JLnNUAAAA%2BSURBVHhe7dMhAQAgEEPRIfAYEkCCi0ACEOtfBc8WAHFfPr0hGp%2FKwKS00BUPquIGTZ9gYqIdrZ23PYK9zAX6sAYavSqAMgAAAABJRU5ErkJggg%3D%3D) no-repeat center center}#cboxLoadingGraphic{background:url(data:image/gif;base64,R0lGODlhIAAgAPYAAP%2F%2F%2F%2F9VAP77%2Bv7j1v7m2v78%2FP7Quv6qgP6wiv7UwP749v7v6P6viP6ofv6%2FoP7u5v6fcP6LUv6rgv7s5P728v6nfP7Aov7Irv54Nv57Ov5%2FQP6bav7n3P739P6mev7Dpv76%2BP7ayP58PP6cbP7w6v6%2Bnv6keP7Tvv7g0v53NP56OP7HrP7Yxv7czP7z7v7i1P50MP7MtP7SvP7EqP708P6ebv62kv7k2P7r4v6uhv5gEv5fEP5sJP5eDv5zLv67mv7q4P7o3v7y7P7KsP68nP64lv6WYv6zjv63lP6DRv6HTP6KUP6CRP6GSv60kP7ezv6ESP6AQv7f0P7Wwv6ITv66mP5mGv5vKP52Mv5jFv5iFP7PuP6QWv6MVP7CpP6gcv6PWP6TXv6XZP6SXP6OVv5rIv5qIP5oHv5wKv7byv7XxP6aaP7Otv6YZv5yLP7Gqv5kGP6UYP5nHP6idP6jdv7Lsv5uJv6shP5%2BPv6yjP5cDAAAAAAAAAAAAAAAAAAAACH%2FC05FVFNDQVBFMi4wAwEAAAAh%2FhpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh%2BQQJBQAAACwAAAAAIAAgAAAH%2F4AAgoOEhYaHiImKi4yNjo%2BQkZKECzk2NJOCDxchgwU1OjsSmQoQGCIWghQiOz01npALERkYGQ4AFBqtP4ILN0ACjgISGhkpGDIANjw%2BKABCKNEujxMbGiowowAEHIIT0SgUkBwjGiIzhkIvKDiSJCsxwYYdmI8KFB0FjfqLAgYMEiSUEJeoAJABBAgiGnCgQQUPJlgoIgGuWyICCBhoRNBCEbRoFhEVSODAwocTIBQVwEEgiMJEChSkzNTPRQdEFF46KsABxYtphUisAxLpW7QJgkDMxAFO5yIC0V5gEjrg5kcUQB098ElCEFQURAH4CiLvEQUFg25ECwKLpiCmKBC6ui0kYILcuXjz6t3Ld1IgACH5BAkFAAAALAAAAAAgACAAAAf%2FgACCg4SFhoeIiYqLjI2Ohw8Tj44XKlhbk4sKEVZZXAWZgwsxLYMdTJ1RCqEAIA1JSjOCFKhaUSCCoI8kRkpMULIKVFZaXaALN0C6jAVHS01RTFMAVVc8XgBCKNsujwsmS1AaCIJSpQAT2ygUk0AeS0oXhkIvKDihQjEyy4QdNJMgOqxqxC9RCyJFkKwYiKgAkAEE2CWi4CChDSdSFJFQx0ERiCEWQlq4oUjbto6KgCQwIOOJAEUFcBAIInGRgIKsGrrogIhCzUcFgqB40a0QiXpAMj1QJ6kVLgA41P1kxGHbi39HB%2FA0iaKoo6MvSAgisC0pAGRBXk4SOOjGtiCDFXCGSodCSM6GC7ze3cu3r9%2B%2FgAcFAgAh%2BQQJBQAAACwAAAAAIAAgAAAH%2F4AAgoOEhYaHiImKi4yNjoYkTj8Uj40SPGUMlYsdSzxmSiCbg0IyKIM0TTxnTAqjACAIYGNDgh1Uq1CiAB2VLl9hZGAXsGSrXAUKEjNABY4FRGJjXV0sAD8%2BaB8ANmItKC6PJAxiXBFIAAIhIYJVUygolI8TCNIxhkAvKDijLidTzgx1oLEJxC5GAReRkLFixZSDhwoAGUBAXiIWQy6smMFBEQl4KDoqenKi5Al%2BiYSAFJmIwgAUL5opKoCDQBCLM189c9HrEAWcz4LADFeIhD4gmxaAnCDIoCAcIIEuEgqToNEBvVTCI%2BrIxYAXJAQRgIcUwIIbQQQUPHiD7KCEOhMBTIAnJG7EBVzt6t3Lt6%2FfvYEAACH5BAkFAAAALAAAAAAgACAAAAf%2FgACCg4SFhoeIiYqLjI2OhiRVDhSPjQhYPkeViwpjWG5dIJuDBTdBgxRkWGhKCqOCK18QW4IdXKsRogAPHY8FNl8bG2wAIEarRgUKDW4ROI8XHl9rbS0ADhkYbwBIWj1wU48uPx4QYg4ABS1pgm09ZUc0lQtE5SeGR1hEz5sUIWkFDAkAIq9SAQGOAjIC8YLFFBQIExUAMoAAJUU41oVQs0ARCRQgOSyaABKkC0VCSopUJADHjRsTFhXAQSDIRZmvErrodYjCTV9BULw4WYjECxRANn0EGbNYRBwlfzIiKVSe0Ru9UpqsRGHAABKCCIBMCmCBqYiPBKC9MZZUTkJUEIW8PVRgAdG5ePPq3ctXbyAAIfkECQUAAAAsAAAAACAAIAAAB%2F%2BAAIKDhIWGh4iJiouMjY6GQhZDHY%2BNSFEiRZWLCmtRGXEgm4QgCoMdYhoZYKajAA9ETmqCnRoqY6IACy6VCQgHDQkAIBAaGCMAChIpShyPTzYMDR4oADNQUUMAVXJZOj%2BPHRdOOR4rAAVST4Ij3joXlS7jOSyGNnA7YRSbHSgvhyAMvBHiqlEBgxNu3MCxqACQAQT2KXKBoiIKGopIWHQ20eJFRUI2NsShcMJIAkEkNixo0AWlQxRUPioQxB%2BvQiReoACySWNFk8MECMJhUSajCRVfYMx5g1LIijcdKSAwgIQgAhV56roBRGilAgcF3cg6KCxLAEhREDxbqACJqGwI48qdS7fuqEAAIfkECQUAAAAsAAAAACAAIAAAB%2F%2BAAIKDhIWGh4iJiouMjY6GLitsCo%2BNJRFUM5WLICYRTSMCm4kdc59iIIIgLw%2BVT2woggp0EVBrogtfblFSjhNeP0hpAAINEUl0AApfZWdyTr4rFkVOBAB1YBFsAD92zlZ1jiBTbw42WwAFL7ECRmZycEYUjxRqbyW9hUfwRiSbIEGCHKLwxoKQUY1AUCjQiAQBAhMWFWjRgkCHRRRQaERBQxGJjRwwbuSoSAhIRg9u3IioqAAOAkAuMmKIsFEBFzINUZi3qUAQFC9cGCKxDsimjxpZghAFAMdGno4eaHzRkeiNiyY1Cn0EgsAAfwAIaDQKYMENIEwr0QRwY%2BygtTUUAUzQeDCuoQIkttrdy7ev3799AwEAIfkECQUAAAAsAAAAACAAIAAAB%2F%2BAAIKDhIWGh4iJiouMjY6GBQMDj45sI20ylIsgDG1jBwWaiQp3nl8ggiAyQxSPJCgPqZ1cdAIAJB4pbkeOCmoxF5MCR21cEgAKFTBodmO2jB0hqzM4ADIjRpkOKcw8P48cLAYrIQAFN5MFI252ZRutjiAELFschkVXZWskmgUkC4coXPjgQlQjEDj4MSJBgMCERRPA2MlgYJGCFygy0lCE5MwVH21QjcKoUREBNglY3GC04MaNh4oK4CAARIHBm4gKuOiAiAI8SgWCoHhRsBAJjEA0vcoIE8QzHBlR%2FGz0IOOLjUdv8BQStWg8AjcUEsiYFEBLIM%2BADrpBdlAonIIRJmQUAhcSCa918%2Brdy7evqEAAIfkECQUAAAAsAAAAACAAIAAAB%2F%2BAAIKDhIWGh4iJiouMjY6HIAKPjkFFP0CTjB8VXx%2BZigI%2FFRAMkgACCWwdjwVCNIICRKMHkkJ3URlIj0FPITgABQ4VNUcFIDl4KiliposCLygtUyQAIXd0LQAzuClYDo9AKFIhN4ITmAV0GSkwX6uOIBziC4ZEKT4QQpmtr4YddStcfGoEYoI%2BRkIIEJiwaEIYNxpkLAIBDQWKfojy6NiYRIEiihYvKjrSo2QTEIsW3LjBUNEDD1SohBgIqlmjAi7eGaJA4VOBICheCCxEAhqmSSRCtowkCEfIno8eWHzxquiNVUJCDoVH4AY1AAQsHlUJpIDPQTfEDjJLc9AEiwcP2xYqQGKr3Lt48%2BrdizcQACH5BAkFAAAALAAAAAAgACAAAAf%2FgACCg4SFhoeIiYqLjI2Oj5CHCmkhCpGLU0gMMpeJBUOaPwWCAiwyHZAdlgACF0g5NgIALkcRTSWPEy8DQgAFdUh3uCBOVFBMELKMBTcoKC8UAC8%2FCC8AQ11NTBozj0DOKA%2BCJOIFEtp4FaiOIBzPLoZeTHge8JAFLtGGHVt1NJ2MQEzoxUgIAQITFj1og4EJm0UCBoD7l8iGHCtWlIBQFHGiIhtZQmpcZPBGQkUPxIhY8hDgoQIUlDnCt84QBX33grwzROIFCiCRSIA7CUIZDnA4Gz1w9uJfzxuohICzx47ADRKCCDgDCmDBDRyjIoUF0OznoLEuJzgj6LJQARJUCtvKnUu3rt25gQAAIfkECQUAAAAsAAAAACAAIAAAB%2F%2BAAIKDhIWGh4iJiouMjY6PkIgkC5GMHEMzN5WKLBcOQ4MCL2oKkCAgggWdJR8FADREbWMfjyQvA0KCaRdEFwACJUZcXQ2ujRwoKC8UAEB1FhwABrJdS76OOMkoD4I0JIJOY11UOaWOIMgvNIYXZOTrkAUuzIYKJ1vwm4oCD0FCxomEECAwYRGQGhpUJPmSz5CAAdoaGrpjpyKPKzISFYCYTGIhBGZCmrFjQJELAjcKKnqwIQoTJk4E6DNUoIPNR%2FI6IGIxRGe8IMpcGCKR4EsbobW0qQQhE0A2KQ5QQHqQTB0AWzd0CtGW6xEIlN8AEEgGRNCCGzgA4hx0g%2BwgtfoTJiTrOrNQARJI6%2Brdy7evX76BAAAh%2BQQJBQAAACwAAAAAIAAgAAAH%2F4AAgoOEhYaHiImKi4yNjo%2BQiCACkYxCTywklYoEaTIsgwUcQJEgBYM3aQYygh1vHiYtj0IvN0KCnVtTAAUrJhBrDo8cKCgvFABCLQYTAGoVwGJbjzjFKA%2BCCjSCDl9rRkgKjyDEL9uFWxtxNuePBS7IhiAsJ%2FGbigILQED2iEIEBJop4jCHShImYlAkEjDAWrtDOVKkwEIRwilEBBwquuOmY0cIilwQuCEwEQ4ISpRQmUPgnqECHWJeZPSuwyEQQ4bYhFQgiDEXhhxo0TIG6CMS1gROEpQGih4dMSA9KGYOAIlaNoUYwKOHCCQQIzUByIiCFIAFMiqUdIeqmFleLhQHTSh2K26hAiSM2t3Lt6%2Ffv5sCAQAh%2BQQJBQAAACwAAAAAIAAgAAAH%2F4AAgoOEhYaHiImKi4yNjo%2BQiAWRjRQ3BAqUihwoKByEIJOQBaIABJ0vggoJRBeZjjQ3N0KCp1IDAAUyRzkHKI9BqBQAQgMoLgBSNgwNDZ%2BOOJ0oC4Igr3XMJl6ljCCcL8OFagd0Dh2RBS7hhSBPIeeaiwIkODjriC4EBBOLQAdjZLpAwJXoVCcaio4wicJQgwdFBlEgTJQng0WLDxNRIHCDn6IJHsiAAVPhWTxCBTp0eNUoHbxCAmLEeOmoQLAXyAoxsCLHSE5HJKR5BCFAUJgdWqywgfQAFUISL26cQ6IDqQNIIDiSqNUJCAAFDdyI8Thq0I2ugx4UPQlgQidabA4LFSDxM67du3jz6qUUCAAh%2BQQJBQAAACwAAAAAIAAgAAAH%2F4AAgoOEhYaHiImKi4yNjo%2BQkZKECkBAApOJQCgoD5mDBQWDBJwcggUDUwSQHTc3QoKkKEGCTzMODjSPOJwvHQBCAwMUAEErDkVVLo8TnCgLggIggiwWRUd1kCAcKC%2FEhVJVeRcKkQUu34UCNwPln4kFQg8Pv4oUBAQTixN5NW1iDVYlkoVCV6IfZLp0iRAhhyKCBhEVaUKR4h17BG7oU%2FTgjpiPOWi9o6TAXaNz9dRt2ZLSUYEg3ZYVysPjyoaIjUg42wgCEwAjVs7YMQDpQS9dJF7c%2BFXESlAv2jKSiMUJCAAFErBwMWVu0I2qgxZMe9cMBayRhAqQkIm2rdu3cATjNgoEACH5BAkFAAAALAAAAAAgACAAAAf%2FgACCg4SFhoeIiYqLjI2Oj5CRkoQKQDgCk4k4KCgPmYMFBYMEnByDJBwUkB03N0KCpChBgkAsBiGQE5wvHQBCAwOqJCEydWyYjg%2BcKAuCAiCCHMUzuI8CHCgvqoU4dR8J0JAFLtuGOEHhn4gFNCQkyIkUBAQTiwtEBx4mSECKsSg0FH3YsKaNQST%2BlgVM5GDMmDAObSiSd6OeIhJHvnyZYwOHukIKFKRjNK6XIQpvLph8VCBINheGjrjBMufVIxLLLIIIKIALDzQ%2B6Ch4pCxbQBIvvrABgIQHjytYTjwCQeAGCVgoPJApoOBLmadeIokSdAMFka0AaHjAomTAJ10XFIiA4nD1UwESC0Z%2B3Mu3r9%2B%2FkAIBACH5BAkFAAAALAAAAAAgACAAAAf%2FgACCg4SFhoeIiYqLjI2Oj5CRkoQCEwsFk4k4KCgLmYOYgwScHIMULpEdBDdCgqMoQYITLyg4kBOcLx0AQgMDFLycLS%2BQC5ydggIgsigtakCQBRwoL8CFQi1TKKGPBS7WhkKXn4unHdyIFAQEE4tCK0VONh%2Btia8oNIoxBw0VFR5bFN3Ll%2BjCl4MHYyhSd6OdIiFEJNy54wAVOUIgMnZzscuQixVsOnYLQs0iIRsZNDQw2YjEMYdPSinggkUFngMiGT3IlQ%2BICjQBq%2FjAggGPl0cgVpEQ9ELFjjEFQHgYimGEgGiDWvjYQQaTEAg%2BUvz49OKKjiKm2IT8ROFIlZwXCOPKnUu3LqRAACH5BAkFAAAALAAAAAAgACAAAAf%2FgACCg4SFhoeIiYqLjI2Oj5CRkoQFJCSTijgoKAuYiASbHIMdHZEKHARCgqAoQYITLy%2BXjw%2BbL6VCAwMUAEKbrZALv50AAiCvv6qPBRwoL7yFvig4kgUu0IYUNJ6MChTHixQEBBOLHVMrHytSi6wo24ksVUVISD%2Fwn7%2F4h1MM%2Fgw2XCgSd6PcwDdIbBBhx62QAAUClrkoZYhGDBkKIhUI4kxgoR9NIiDYx4jEr3ICWrgCIUYDFCp5KDaq5WxbDjlYDABwIEJDEiorHoEgcOMSBRU64BgpAEJCzyQmCkCSCoAEjKRhpLrwICKKBU9tkv4YRMEARk8TjvyQ2bCt27dwBONGCgQAIfkECQUAAAAsAAAAACAAIAAAB%2F%2BAAIKDhIWGh4iJiouMjY6PkJGShAUkJJOKEygoC5iIBJscgyAgkQocBEKCoChBgg8vAzSQD5svHQBCAzcUuZsoOJALv50AAgKCmpuqjwUcKC%2B9hUKbwZEFLtKGFLOeiwIgBYwUBAQT3y9qCSzMiawo3Yg3dUMXFyeL7%2FGHUhb%2BFgYWUeBw45yiDgZmvIlxyVshAeKaucBliIYMNaUgFQgCzYUhL2PaVNHWiMSvcwKeAAEA4ksELnGqKHhUC9osBDxE4PtAJQKYODEegSBw4xIFPFbKbCgAIo8SnzkiOoooBEPSNuJo3KHS5Y2nEVZ4lBjUIc2UmZgm2HCA1qHbt3AF48qVFAgAIfkECQUAAAAsAAAAACAAIAAAB%2F%2BAAIKDhIWGh4iJiouMjY6PkJGShAUkQpOKDygoC5iIBJscgyAFkQocBJcAoChBgg8vNx2Qmigvs0IDNxQAQpsoD5ALv50AAgKCE7%2BqjgUctryFQi8oOJIFLtGGHTSejAWljBQEBBOLBUADA0DIiqwo3YkPTy1padbuv%2FGIQTL%2BMq4UUeBww5wiEC1OnJACwpshcJCwzdrG4knDiEFQSAlh6AIEDx8mOnKx6cgcYyFQGDvQpgadDxcbaXqDxQsAJz7wGAAwJE6bEXMSPALxQgwDARSS2IFhwliVMD9%2FQBJQDAcWOz7aIKPgxEibGJgWqMCqVZCCjTEjUVBix80dh4UQLuChkgZuoQck7Ordy5dQIAAh%2BQQJBQAAACwAAAAAIAAgAAAH%2F4AAgoOEhYaHiImKi4yNjo%2BQkZKEBSQuk4oPKCgkmIgEmxyDAgWRChwEQoKgKEGCDwMEIJCaKC8dAEIDNxS5mygLkAu%2FwQCkghO%2Fqo8FHLa9hUIvKDiSBS7Qhh00noyljRQEBBOLBUC71YusKNyJw7%2FZn7%2FtiO%2Bb8YcUHDfkigVBLwak60bwWhABhkCguIEQUrMiWH4YksHAxhYFkIQgMLMDgrE0L4w5qXDnCJuGjWZY6QFnBoAiGZQkAGBgDsk8LR6lyeAmj4AOS1LguWPMyxwPEthAIvFAEAkmKUR8KdXBgok7UjA9jVrjm4AbrjC5aJIigwmChTxEfYOW0IISbwgwtp1Lt66gQAAh%2BQQJBQAAACwAAAAAIAAgAAAH%2F4AAgoOEhYaHiImKi4yNjo%2BQkZKEBUIuk4oPKCgkmIgEmxyDBZIKHARCgqAoQYIPAxwCkJooLx0AQgM3FLibKKmPC74LggKkABO%2BvI8FHLXLhEIvKDiSBS7QhR00nozHjBQEBBOLBUC6xIurKNyJwpu26r7tiEK%2B8YoUHDfkigU4BDgA60YQSAkZsgoJCILjm6MJSXrIKWEohIMVaRI6qrJDB5w5AAQ8uSFoho0SH1pAMqEjS5kVAIg0GcMCgBoENoh8ePCohYYUTgR0GBNliRMABergJAIEkpB0QpZEoXKAFIgtPwyAwBQ1ipIK3255okHG6x2Che54rYOWEIkPdQi2tp1Lt66gQAAh%2BQQJBQAAACwAAAAAIAAgAAAH%2F4AAgoOEhYaHiImKi4yNjo%2BQkZKEBUIuk4oPKCgkmIgEmxyDBZIKHARCgqAoQYILN0ECkJooLx0AQgM3FLibKKmPC74LggKkABO%2BvI8FHLXLhEIvKDiSBS7QhR00nozHjBQEBBOLBUC6nYurKNyJwpsDsorr7YhCvvGLFBw35IoFOAhwqNetGw4HJ%2BQVInEp0gQlWXhYMHRDBosg3xodgSOnTAUABV60AnBixZYpIx15kGPGzRAAXrjUeAJAioUVbNSAePQECp4iAhSs6WKkBMgpXlac2PlICDEALsJ0iXOElIAXCaphchGnS5g8GbvREOPVRsFCR7waOBvtggGmbAbjyp0LIBAAIfkECQUAAAAsAAAAACAAIAAAB%2F%2BAAIKDhIWGh4iJiouMjY6PkJGShAVCLpOKDygoJJiIBJscgwWSChwEQoKgKEGCCzdApI%2BaKC8dAEIDNxS4myi8jwu%2BC4ICshO%2BwI4FHLXKg0IvKDiSBS7PhB00noyyjBQEBBOLBUC6qYurKNuJJL433ogDagkxnYlC7%2FGHLWFNJrcSFcBBIAi7RR2E7ONGCAeRISAOubgUKUgXM24cGKIV6xGJMGWu%2BJAAoAABagBQhJCC4sEjByHdqFgB4EINCQMABDmxksAjCXbcpMgjQIGJNSZopuQpypGUCFGK3KJRYw0djSWBAFEAycU4QTQgrJlDhCEhCnPWfLFglpADtWoN2g6iIIOFALl48%2BYNBAAh%2BQQJBQAAACwAAAAAIAAgAAAH%2F4AAgoOEhYaHiImKi4yNjo%2BQkZKEBUIuk4oPKCgkmIgEmxyDBZIKHARCgqAoQYILN0Ckj5ooLx0AQgM3FLibKLyPC74LggKyE77AjgUctcqDQi8oOJIFLs%2BEHTSejLKMuTcTiwVAupeKQmBKNRI3iiS%2BBIskKT09Ox%2Fo8YwXTCk12AoVwEEgSMBDHVx442ZogoUYIA65OAcJyBgfKvIVgoci1iMhbXykEJEHADliAIAMe%2BQExkgodQBskVClFUcUohqB4JIiQxQHBUAwaODkhKAJ0h48YpBBg5OIFCQ0yBNTEAWKjSjIOKHA6p0GCIYwJAQiD9gtYwkZOOAkZ1qTHAeovZ1Ll24gACH5BAkFAAAALAAAAAAgACAAAAf%2FgACCg4SFhoeIiYqLjI2Oj5CRkoQFQi6Tig8oKCSYiASbHJ4ACkEEQoKgKEGCJARABZCaKC8dAEIDNxS3myi7jwu9C4ICsQATvb%2BOBRy0yoNCLyg4kgUuz4QdNJFCqI3GjCsYMGudiQVAuduKQhg772%2BKJL0EiyQZWVlwM%2By9ootDmoiYg61QARwEghQ8pMAFuFGGHswwAOIQhYWLcLQRAeWCIRLSYD0SAgEPEypVWl0CAETYoyomlXAxAEDNjyHDhPQC4ghEGyZNuswoIIBIkRlSBD148cJbIydNIhCpSMNGkQ8sBnVQAKnDFDVcAXQoUsSLGoiEBHwoYgEFWkI4DS4kWPdW0MO6ePPWDQQAIfkECQUAAAAsAAAAACAAIAAAB%2F%2BAAIKDhIWGh4iJiouMjY6PkJGShAVCLpOKDygoJJiIBJscngAKQQRCgqAoQYIkBEAFkJooLx0AQgM3FLebKLuPC70LggKxABO9v44FHLTKg0IvKDiSBS7PhB00kS6ojcaMQyIYI52JBUADBNiGQnhWcHAXiiS9oopCUWZmZW%2F49oxidEnigR0lHASCGDSkgAa4UYYWXEgg4BCFhYomzFHChY0hEtKAQHJRgQqZOF4E0VAgCEgvb40cLCETZoQaAFJipNklpNcERyDm0FwTo4CAIUPUUAPw4MUAjIaIhGnzpmKHGUOm3CMFAlKHEC2MgbgwJMFWiIJYDDkxDO0gBTcKfrqdS7euXUOBAAAh%2BQQJBQAAACwAAAAAIAAgAAAH%2F4AAgoOEhYaHiImKi4yNjo%2BQkZKEBUIuk4oPKCgkmIgEmxyeAApBBEKCoChBgiQEQAWQMi0oLx0AQgM3FLibKLyPORC0C4ICsQATvsCOQFBfT8yDQi8oOJI4DsWHHTSPBS4kQgKNyIokXxoZIhuoiQVAAwS3iV52djw8ZQ7nvqKJM9wIFOhFkRBfrBKRoNMEypIGl97heKVgUSUSEUchIsEmBDlDFKQ5WnAgTo0EhkhUAwKJBoI4G%2BjUEaQAhCAgvtw1emNkwxwJTwAEeTLg1sFN2xgJkLDhS4UTAAqwoMUSwAN5FR3NcMqGnAA1tP4BOAZJgZQXyAqkoaqxEJAnLw1EtqWQta3du3jzKgoEACH5BAkFAAAALAAAAAAgACAAAAf%2FgACCg4SFhoeIiYqLjI2Oj5CRkoQFQi6Tig8oKCSYgx0FgwSbHJ4AaU0%2FQoKjKEGCJARAoY9zPSkGHQBCAzcUu5sov48SOz1GD4ICtBPBw444STtlT4ZCLyg4kjg%2FbLSFHTSPBTSWAo3fiSwbTUxJX52JBUADBLqIIEZY%2BzAwSIokgr3CtyGDQYMOFAkJBkRRiw1kyIxhEA9RARyyQCwCIUSIOFOJXCR4km4QhWePSDiZc6eFIRLYGj6iUIXOgTwJBIHQCABHsI%2BN2Jg4gODHDQAwB%2BhauGnBIyIHGCBxCaCVzAX1eDZSk6eImlAFbmwaCKBASUYTkonapA0kIV4EDRS4LWR2rt27ePMeCgQAIfkECQUAAAAsAAAAACAAIAAAB%2F%2BAAIKDhIWGh4iJiouMjY6PkJGShAVCLpOKDygoJJiDFEKDBJscngAtTSlFgqMoQYIkBEAFkB5ZOlYGAEIDNxS7myi%2FjwxwWjsSggK0ABPBw444VHBnF4ZCLyg4khMlW8yFHTSPBTRCNOCK6Yhpc2RLER6hiQVAAwQdiSA1UVEaGniIKCIR7BUiAXSaKFQ4Q5GQYEAUSTHRps0IG%2FMQFcAhC8QiEC5cQDN1iEaaG%2BsEURjpyIWFPD9uGCKRLeIjEG%2BOVPmAQhAIjwBwBBvnCIWTKl5iPABAc0C%2Bh5s6Fa1i4cIAVptsLrgHtJGCE2xkAihwY5PBsSkZCSDEYdMCkoUOKHDg0BWu3bt48%2BpdFAgAIfkECQUAAAAsAAAAACAAIAAAB%2F%2BAAIKDhIWGh4iJiouMjY6PkJGShAVCLpOKDygoJJiDNEKDBJscngAtUBlVgqMoQYIkBEAFkAdmVmUyAEIDNxS7myi%2Fj0c8Z1Y5ggK0ABPBw44TZDx2dYZCLyg4khNeMsyFHTSPBRQuNOCK6YhSB2JhcTnjiQVAAwQKiQIVXV0RS0suKCIRDIi%2BO2MSJhyiSEhBRQMYmDDRwME8RAVwyAKxSAAFGh1MKerwwuAhCtAeUYjhhc0DQySymXx04kOdKdsAgOAIAMezRyRW1DnxZFzMASEdbrrkyAUbGWleAmhlcsGNIAIg2esEoMCNTa8ErZsUZNMCkYUUBJkwFq3bt3AF48pFFAgAIfkECQUAAAAsAAAAACAAIAAAB%2F%2BAAIKDhIWGh4iJiouMjY6PkJGShA8XLpOECxOEX01SJJgAU0l4JYIUKkpSHKEVblduRAAUGWQoQYIkBEAFj04wbnZoBgBObTcUAEIozMmOD2EwaDwVghO9ABPMKM6ON9E%2BFoZCLyg4kg8fFwKHHTSQ7hTYi%2FOJL0dzEBBO74kFQAMIKEgkIM%2BaNm3EGGGjiMQ2IP6QfJk4kViiZcwgJuJQBQECJxe6HSqAYxeIRQI6UBgYSpECHEIQURDpCESIBE8uFSJRTuOjF1OeoNgEAMRJADi20XQZQuiLdzwHdFC2TWejAgNQvAAFgEBGQQtu4KjHSMECqzeY4RJEdhIQZgsPWhoSMOGa3Lt48%2BrdiykQACH5BAkFAAAALAAAAAAgACAAAAf%2FgACCg4SFhoeIiYqLjI2Oj5CRkoQLRTMKk4JCFyGEdDs6R5kCBxgiFoIUeDs9Jpk0XBkpKg4AFBqsRIIkBEAFjwwaGVgYMgA2PFgoAEIozhSPExsaKjASggQPghPOKNCPHCMaIjOGQi8oOJIkKzEChx00kAoUHb%2BM94pCFjkSEiXfEBUAMoAApkRDGlTw4MFEAkUkugFRFIOBRYss9ElU5IKNAwcfTnRQVABHLxCMFChAmWmRABcjD1EI%2BKgABxQvXBgigW4iJG7OJggCwRJHN5qMCDh7IY%2FngJHNnkECgpMENmc%2BF9xQB6mAi4MAbjgLMihfS6MorLY0JOCB2rVwB%2BPKnUtXbiAAOwAAAAAAAAAAAA%3D%3D) no-repeat center center}#cboxTitle{position:absolute;bottom:-25px;left:0;text-align:center;width:100%;font-weight:bold;color:#7C7C7C}#cboxCurrent{position:absolute;bottom:-25px;left:58px;font-weight:bold;color:#7C7C7C}#cboxPrevious,#cboxNext,#cboxClose,#cboxSlideshow{position:absolute;bottom:-29px;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAOEAAABLCAMAAACx6hDAAAABj1BMVEVPT0%2Fe3t7b29vS0tK7urq5uLjq6uqZmZmSkpJaWlrU1NTj4%2BPFxcWvr6%2BgoKBbW1u3t7c9PT27u7vCwsKsrKxiYWGqqqq5ublbWlpeXV2Xl5fExMSbmpq6ubmNjY18fHzy8vIrKystLS0sLCxNTU0uLi4wMDDNzc05OTns6%2Bvl5eUvLy%2Fq6ekqKipMTExDQ0M4ODgyMjI2NjbZ2dk6OjrY2NjMzMxLS0vAwMBCQkLo5%2BdHR0cxMTFKSkpBQUHv7u43NzdISEhFRUVRUVHx8fE7Ozs8PDwzMzNJSUnp6elGRkZQUFDr6upeXl7t7e1gYGCoqKjv7%2B81NTWKiorn5uZERESCgoJdXV3p6OhOTk51dXVAQEA%2BPj6np6fu7e2%2Bvr5cXFxSUlKJiYnOzs7s7OxTU1P29vbw8PB2dnZfX1%2Fm5eV4eHifn59qamqmpqbQ0NCOjo7Kysqzs7P4%2BPiDg4Otra3z8%2FM%2FPz80NDSrq6u%2Fv7%2FPz890dHRpaWmBgYH5%2Bfn08%2FNoaGjPzs7%2F%2F%2F%2BioqIRuwm9AAAGHUlEQVR4XsTXZY%2FjPBAA4PwUQxjKzMzLjMfMzPfiD7%2Bxm23ibjZVu5u7%2BVBL7ljyI3scW9pZOv59%2BgdjZ%2BNOeyzlyzXtv9B408%2FUynlp3L4r7bzYWC5E4a4xvAQYGkEA2%2FDG2GL6biBmHbGoz9pVhTBkuRCEhx0TzVNKKNspBczXdHtLnTRa9yC78MdhAND%2B6xaLh3%2B7bf1mhO3guEpooJfbaH56h2i7gOx5oCmlckBkwFzqGIi%2B9LdocllofMSUUnzrndui6wo5bnxVzJyC8BztO06A0HFeM4IIxLgBRAZsYAeI%2FvSf6DxASAkhFIS8vbaQ6aSw4ExBWNoyDyjFAUKM6Q9zy1ddEwBSSnStY3c0ncCo78j2px%2BYW6VLQqIoCgEhb68p5L4jWY6xyIsR4yHLgASiJ4S5JohCaICQQn8756suI5vC0KlkNKRlFBiEU9mJkN7KdYaRCpkvVh00y8HRbA6qMZkRZ8L7aJMolmUpiMe6u215ENafOEPe6Qlbk0K22tvoqTCGwob1lpyn0zN0PzIhB8aqzdFevFgLjGJ8b9TMA3EmrJuPAaiqqvVgbe3g9rFp8834z%2B2DtbUHvF8h%2B151QfUliKVWWKgWSUBFekI3%2FTGqzwstV2jdgFCulj%2BEjfhSbLlEELIDv82A0wmzXffVYJMqOBTcLkQhrLo8om5VkhAg1BnQE16kt81HpWiEfAmb%2F4cPeV5rDWKu8BAVGpRJ2IQ5ERemQsy73X6%2BGXdnRK1bSfb7%2FWSlqwHQJ%2FTSCyD3hIorVG5CKFdHr8KF907j5ao8FRppBxMFgDDfpCgkU%2Fi0n2DHq0UbPXGFz5AtHEy%2B9KwRkRCWMP4tXKhlTlpVWZqtIVBAEryGSRYBa6jyP9T5NfTSYQ0j2qVH%2BXLx3gJh4nRvEAPh3Ys6nNUbq8OCWIccLtahlpmdNLom1qGb3k5HVofSUX5UWyDMpXpxVoggdM9SIPrO0olwllZUAL4XzlId6NOwFF08S3l6hGcpO2iqrZNFwu1esRljQ0K%2Bh3XEg%2Ffrm8L3MEEU6O4%2BgR9Y9IT%2Fe8jTyWYE30NB%2BGk5Ib%2FT6ErInUbzXVKMdAMTCF1Dmk4gcCM9d6dh6RELtQXCz11BGHovpYH3UgorZ8NqUhh1jGx%2FevC9FOQg5O1vFa72thj73xZ47m2xv9LbInqh%2BD4MffABUeLAp5wYwfswEiHEcKU3fnalN37kwl%2Fs2M9LAkEUB%2FC0ml12VgmKLh2%2BcwtBOkWRUdIvIpJayYNdgkIIukUDdYz8x2tnt96OjQMS8hbzexFhDn6QN2%2FeIyHTnoZByJD%2FKJwL58L5TdM%2FFY5uIZ3dQvx0C2l3C%2BHuFsNKmuF7%2Ftlm6vixdnR8vfm744tQV%2FOOX9UJEen4SBpDpKm85J9Mr7Ya4BACK4ZgAYGUaICAIdLxmroro7DVFQHGCFEX1ss7BRpi0wBTYrN4PBDdVumE5reOFSKsFqcnqZERVQaElh3r%2BA0dn5pwQ3mzOiIcqBgmjo1wZoiLE%2FA3LEBOLUzAMInVYMrCtUVv1m1hWyTIEgZfSYTZCIt6%2BiVEFqqurPoopiJHhEhUe7rCpSdvlkloLvwQViKziYpAoeoiogNIQoTysVUSYV9FGl4hUXoWkYAOIXREcl5hQwJeIaW4EQ4A0D3qEAKyMfP%2FYW%2B261Aw16H%2FLu3MyF36936o6Qpy9ENW4eRvmv6kbxpmIcO7lEHIMFuwCf3zYaSaE8%2BH%2FEL2GZ9BWOY9zWc7d%2BwSMQzFcbzDCTqVCnJ3ok4HdrpAKZRSWnInicUOQiVISYeem25O6Xz%2B4%2FYJWaokg8Px4P3%2Bgw%2Fp%2BL79p%2FAkQyEkIQlJSEISkpCEJCQhCUmofcJWIhXa%2B1KfcJlIBsIDSmEzCatLt%2FCW96pGLNzu2LlbeBcbe%2BeNURhs6r2%2BcQGvuczhVh%2BtsMsK1qdX769DuLqYLQyHdc%2Blht4CqfDnMy0LZmScjI%2B%2FN7Y8llpNT4hYGABRVSYSIp1PCBmZygJRCn1pV87UHsKurnnAK3Tnebu6zCDOgydEKZwnlts%2F%2BsrOBpY4hdbYOBtZACIWghHm7JyRCu3efEP6T4Vv0sK5wmQ8JLkAAAAASUVORK5CYII%3D) no-repeat 0px 0px;width:23px;height:23px;text-indent:-9999px}#cboxPrevious{left:0px;background-position:-51px -25px}#cboxPrevious:hover{background-position:-51px 0px}#cboxNext{left:27px;background-position:-75px -25px}#cboxNext:hover{background-position:-75px 0px}#cboxClose{right:0;background-position:-100px -25px}#cboxClose:hover{background-position:-100px 0px}.cboxSlideshow_on #cboxSlideshow{background-position:-125px 0px;right:27px}.cboxSlideshow_on #cboxSlideshow:hover{background-position:-150px 0px}.cboxSlideshow_off #cboxSlideshow{background-position:-150px -25px;right:27px}.cboxSlideshow_off #cboxSlideshow:hover{background-position:-125px 0px}#loading{position:fixed;left:40%;top:50%}a{color:#333;text-decoration:none}a:hover{color:#000;text-decoration:underline}body{font-family:"Lucida Grande", Helvetica, "Helvetica Neue", Arial, sans-serif;padding:12px;background-color:#333}h1,h2,h3,h4{color:#1C2324;margin:0;padding:0;margin-bottom:12px}table{width:100%}#content{clear:left;background-color:white;border:2px solid #ddd;border-top:8px solid #ddd;padding:18px;-webkit-border-bottom-left-radius:5px;-webkit-border-bottom-right-radius:5px;-webkit-border-top-right-radius:5px;-moz-border-radius-bottomleft:5px;-moz-border-radius-bottomright:5px;-moz-border-radius-topright:5px;border-bottom-left-radius:5px;border-bottom-right-radius:5px;border-top-right-radius:5px}.dataTables_filter,.dataTables_info{padding:2px 6px}abbr.timeago{text-decoration:none;border:none;font-weight:bold}.timestamp{float:right;color:#ddd}.group_tabs{list-style:none;float:left;margin:0;padding:0}.group_tabs li{display:inline;float:left}.group_tabs li a{font-family:Helvetica, Arial, sans-serif;display:block;float:left;text-decoration:none;padding:4px 8px;background-color:#aaa;background:-webkit-gradient(linear, 0 0, 0 bottom, from(#ddd), to(#aaa));background:-moz-linear-gradient(#ddd, #aaa);background:linear-gradient(#ddd, #aaa);text-shadow:#e5e5e5 1px 1px 0px;border-bottom:none;color:#333;font-weight:bold;margin-right:8px;border-top:1px solid #efefef;-webkit-border-top-left-radius:2px;-webkit-border-top-right-radius:2px;-moz-border-radius-topleft:2px;-moz-border-radius-topright:2px;border-top-left-radius:2px;border-top-right-radius:2px}.group_tabs li a:hover{background-color:#ccc;background:-webkit-gradient(linear, 0 0, 0 bottom, from(#eee), to(#aaa));background:-moz-linear-gradient(#eee, #aaa);background:linear-gradient(#eee, #aaa)}.group_tabs li a:active{padding-top:5px;padding-bottom:3px}.group_tabs li.active a{color:black;text-shadow:#fff 1px 1px 0px;background-color:#ddd;background:-webkit-gradient(linear, 0 0, 0 bottom, from(#fff), to(#ddd));background:-moz-linear-gradient(#fff, #ddd);background:linear-gradient(#fff, #ddd)}.file_list{margin-bottom:18px}.file_list--responsive{overflow-x:auto;overflow-y:hidden}a.src_link{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAPCAYAAADQ4S5JAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw%2FeHBhY2tldCBiZWdpbj0i77u%2FIiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8%2BIDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowNTgwMTE3NDA3MjA2ODExODBENEVBMTkyQ0U2NTYzMSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo1NzdBM0ZCN0E0NzQxMURGQTFBM0FBMTZCRTNFQjA0QiIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo1NzdBM0ZCNkE0NzQxMURGQTFBM0FBMTZCRTNFQjA0QiIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1IE1hY2ludG9zaCI%2BIDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjA1ODAxMTc0MDcyMDY4MTE4MEQ0RUExOTJDRTY1NjMxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjAxODAxMTc0MDcyMDY4MTE4MEQ0RUExOTJDRTY1NjMxIi8%2BIDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY%2BIDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8%2BI%2B%2FpuQAAAUVJREFUeNqEksFKhFAUhu%2FVIdBJorUtYkAQiQgqWrVq0RNY9Aq9hPou0SKfYRbSJmrRohBDCKKR1tIUgtr0HzmXxCb64cPj9b%2BHc%2F%2BrFFAQBLIoiqM8zydZlq3Smuu6c8dxnm3bnkZRtIBoWUgyp2l6Esfxhlgi3%2FdnnuddhWHY7dCoM5vn4AZcgEtwCz7oG3lUgxGNwfUDuAM6c08NwV7PIzQ1M5Szgcwj5oU%2B9DydQUn2npI3%2FpJGaXDt8HPBtBSWSkxKKQiNomPjFtgHa8ACO2Cz6%2FTjERrlTNGhpjkPwBk4BbtgbJpmXZblVG3QkyShrFN0MS3LGldVtWIYhmiaRrRtK%2Bu61nFPX%2FDOugOqG%2ByLZuURj3npk%2B%2FnXRN%2F6xG8cW2Cw2Gsy3TdqynFyX8bXsGT%2Biu61OgMQwZaB%2Bdgm16%2BBRgApCh%2B7pwD4GQAAAAASUVORK5CYII%3D) no-repeat left 50%;padding-left:18px}tr,td{margin:0;padding:0}th{white-space:nowrap}th.ui-state-default{cursor:pointer}th span.ui-icon{float:left}td{padding:4px 8px}td.strong{font-weight:bold}.cell--number{text-align:right}.source_table h3,.source_table h4{padding:0;margin:0;margin-bottom:4px}.source_table .header{padding:10px}.source_table pre{margin:0;padding:0;white-space:normal;color:#000;font-family:"Monaco", "Inconsolata", "Consolas", monospace}.source_table code{color:#000;font-family:"Monaco", "Inconsolata", "Consolas", monospace}.source_table pre{background-color:#333}.source_table pre ol{margin:0px;padding:0px;margin-left:45px;font-size:12px;color:white}.source_table pre li{margin:0px;padding:2px 6px;border-left:5px solid white}.source_table pre li:hover{cursor:pointer;text-decoration:underline black}.source_table pre li code{white-space:pre;white-space:pre-wrap}.source_table pre .hits{float:right;margin-left:10px;padding:2px 4px;background-color:#444;background:-webkit-gradient(linear, 0 0, 0 bottom, from(#222), to(#666));background:-moz-linear-gradient(#222, #666);background:linear-gradient(#222, #666);color:white;font-family:Helvetica, "Helvetica Neue", Arial, sans-serif;font-size:10px;font-weight:bold;text-align:center;border-radius:6px}#cboxClose{position:absolute;top:-14px;right:-14px;width:30px;height:30px;background:#000;border:4px solid #fff;border-radius:100%}#cboxClose::before{text-indent:0;content:'×';color:#fff;font-size:23px;position:absolute;top:50%;left:50%;transform:translate(-50%, -50%)}#footer{color:#ddd;font-size:12px;font-weight:bold;margin-top:12px;text-align:right}#footer a{color:#eee;text-decoration:underline}#footer a:hover{color:#fff;text-decoration:none}.green{color:#090}.red{color:#900}.yellow{color:#da0}.blue{color:blue}thead th{background:white}.source_table .covered{border-color:#090}.source_table .missed{border-color:#900}.source_table .never{border-color:black}.source_table .skipped{border-color:#fc0}.source_table .missed-branch{border-color:#bf0000}.source_table .covered:nth-child(odd){background-color:#CDF2CD}.source_table .covered:nth-child(even){background-color:#DBF2DB}.source_table .missed:nth-child(odd){background-color:#F7C0C0}.source_table .missed:nth-child(even){background-color:#F7CFCF}.source_table .never:nth-child(odd){background-color:#efefef}.source_table .never:nth-child(even){background-color:#f4f4f4}.source_table .skipped:nth-child(odd){background-color:#FBF0C0}.source_table .skipped:nth-child(even){background-color:#FBFfCf}.source_table .missed-branch:nth-child(odd){background-color:#cc8e8e}.source_table .missed-branch:nth-child(even){background-color:#cc6e6e} diff --git a/coverage/assets/0.13.2/application.js b/coverage/assets/0.13.2/application.js new file mode 100644 index 0000000000..57b4005582 --- /dev/null +++ b/coverage/assets/0.13.2/application.js @@ -0,0 +1,7 @@ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(le,e){"use strict";function g(e,t,n){var r,a,i=(n=n||xe).createElement("script");if(i.text=e,t)for(r in we)(a=t[r]||t.getAttribute&&t.getAttribute(r))&&i.setAttribute(r,a);n.head.appendChild(i).parentNode.removeChild(i)}function m(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?se[he.call(e)]||"object":typeof e}function s(e){var t=!!e&&"length"in e&&e.length,n=m(e);return!ye(e)&&!be(e)&&("array"===n||0===t||"number"==typeof t&&0D.cacheLength&&delete n[r.shift()],n[e+" "]=t}var r=[];return n}function n(e){return e[R]=!0,e}function r(e){var t=k.createElement("fieldset");try{return!!e(t)}catch(n){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function a(t){return function(e){return ue(e,"input")&&e.type===t}}function i(t){return function(e){return(ue(e,"input")||ue(e,"button"))&&e.type===t}}function o(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&oe(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function s(o){return n(function(i){return i=+i,n(function(e,t){for(var n,r=o([],e.length,i),a=r.length;a--;)e[n=r[a]]&&(e[n]=!(t[n]=e[n]))})})}function g(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}function x(e){var t,n=e?e.ownerDocument||e:Ie;return n!=k&&9===n.nodeType&&n.documentElement&&(N=(k=n).documentElement,j=!De.isXMLDoc(k),L=N.matches||N.webkitMatchesSelector||N.msMatchesSelector,N.msMatchesSelector&&Ie!=k&&(t=k.defaultView)&&t.top!==t&&t.addEventListener("unload",ie),ve.getById=r(function(e){return N.appendChild(e).id=De.expando,!k.getElementsByName||!k.getElementsByName(De.expando).length}),ve.disconnectedMatch=r(function(e){return L.call(e,"*")}),ve.scope=r(function(){return k.querySelectorAll(":scope")}),ve.cssHas=r(function(){try{return k.querySelector(":has(*,:jqfake)"),!1}catch(e){return!0}}),ve.getById?(D.filter.ID=function(e){var t=e.replace(re,ae);return function(e){return e.getAttribute("id")===t}},D.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&j){var n=t.getElementById(e);return n?[n]:[]}}):(D.filter.ID=function(e){var n=e.replace(re,ae);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},D.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&j){var n,r,a,i=t.getElementById(e);if(i){if((n=i.getAttributeNode("id"))&&n.value===e)return[i];for(a=t.getElementsByName(e),r=0;i=a[r++];)if((n=i.getAttributeNode("id"))&&n.value===e)return[i]}return[]}}),D.find.TAG=function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):t.querySelectorAll(e)},D.find.CLASS=function(e,t){if("undefined"!=typeof t.getElementsByClassName&&j)return t.getElementsByClassName(e)},I=[],r(function(e){var t;N.appendChild(e).innerHTML="",e.querySelectorAll("[selected]").length||I.push("\\["+ke+"*(?:value|"+$+")"),e.querySelectorAll("[id~="+R+"-]").length||I.push("~="),e.querySelectorAll("a#"+R+"+*").length||I.push(".#.+[+~]"),e.querySelectorAll(":checked").length||I.push(":checked"),(t=k.createElement("input")).setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),N.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&I.push(":enabled",":disabled"),(t=k.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||I.push("\\["+ke+"*name"+ke+"*="+ke+"*(?:''|\"\")")}),ve.cssHas||I.push(":has"),I=I.length&&new RegExp(I.join("|")),W=function(e,t){if(e===t)return A=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!ve.sortDetached&&t.compareDocumentPosition(e)===n?e===k||e.ownerDocument==Ie&&p.contains(Ie,e)?-1:t===k||t.ownerDocument==Ie&&p.contains(Ie,t)?1:_?de.call(_,e)-de.call(_,t):0:4&n?-1:1)}),k}function l(){}function m(e,t){var n,r,a,i,o,s,l,u=M[e+" "];if(u)return t?0:u.slice(0);for(o=e,s=[],l=D.preFilter;o;){for(i in n&&!(r=V.exec(o))||(r&&(o=o.slice(r[0].length)||o),s.push(a=[])),n=!1,(r=G.exec(o))&&(n=r.shift(),a.push({value:n,type:r[0].replace(Ne," ")}),o=o.slice(n.length)),D.filter)!(r=K[i].exec(o))||l[i]&&!(r=l[i](r))||(n=r.shift(),a.push({value:n,type:i,matches:r}),o=o.slice(n.length));if(!n)break}return t?o.length:o?p.error(e):M(e,s).slice(0)}function v(e){for(var t=0,n=e.length,r="";t+~]|"+ke+")"+ke+"*"),J=new RegExp(ke+"|>"),Y=new RegExp(z),Z=new RegExp("^"+B+"$"),K={ID:new RegExp("^#("+B+")"),CLASS:new RegExp("^\\.("+B+")"),TAG:new RegExp("^("+B+"|[*])"),ATTR:new RegExp("^"+U),PSEUDO:new RegExp("^"+z),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+ke+"*(even|odd|(([+-]|)(\\d*)n|)"+ke+"*(?:([+-]|)"+ke+"*(\\d+)|))"+ke+"*\\)|)","i"),bool:new RegExp("^(?:"+$+")$","i"),needsContext:new RegExp("^"+ke+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+ke+"*((?:-\\d)?\\d*)"+ke+"*\\)|)(?=[^-]|$)","i")},Q=/^(?:input|select|textarea|button)$/i,ee=/^h\d$/i,te=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ne=/[+~]/,re=new RegExp("\\\\[\\da-fA-F]{1,6}"+ke+"?|\\\\([^\\r\\n\\f])","g"),ae=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},ie=function(){x()},oe=f(function(e){return!0===e.disabled&&ue(e,"fieldset")},{dir:"parentNode",next:"legend"});try{E.apply(ce=fe.call(Ie.childNodes),Ie.childNodes),ce[Ie.childNodes.length].nodeType}catch(se){E={apply:function(e,t){Le.apply(e,fe.call(t))},call:function(e){Le.apply(e,fe.call(arguments,1))}}}for(c in p.matches=function(e,t){return p(e,null,null,t)},p.matchesSelector=function(e,t){if(x(e),j&&!q[t+" "]&&(!I||!I.test(t)))try{var n=L.call(e,t);if(n||ve.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(se){q(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(re,ae),e[3]=(e[3]||e[4]||e[5]||"").replace(re,ae),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||p.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&p.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return K.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&Y.test(n)&&(t=m(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(re,ae).toLowerCase();return"*"===e?function(){return!0}:function(e){return ue(e,t)}},CLASS:function(e){var t=H[e+" "];return t||(t=new RegExp("(^|"+ke+")"+e+"("+ke+"|$)"))&&H(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,a){return function(e){var t=p.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===a:"!="===r?t!==a:"^="===r?a&&0===t.indexOf(a):"*="===r?a&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;De.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?De.find.matchesSelector(r,e)?[r]:[]:De.find.matches(e,De.grep(t,function(e){return 1===e.nodeType}))},De.fn.extend({find:function(e){var t,n,r=this.length,a=this;if("string"!=typeof e)return this.pushStack(De(e).filter(function(){for(t=0;t)[^>]*|#([\w-]+))$/;(De.fn.init=function(e,t,n){var r,a;if(!e)return this;if(n=n||He,"string"!=typeof e)return e.nodeType?(this[0]=e,this.length=1,this):ye(e)?n.ready!==undefined?n.ready(e):e(De):De.makeArray(e,this);if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:Me.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof De?t[0]:t,De.merge(this,De.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:xe,!0)),Pe.test(r[1])&&De.isPlainObject(t))for(r in t)ye(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(a=xe.getElementById(r[2]))&&(this[0]=a,this.length=1),this}).prototype=De.fn,He=De(xe);var Oe=/^(?:parents|prev(?:Until|All))/,qe={children:!0,contents:!0,next:!0,prev:!0};De.fn.extend({has:function(e){var t=De(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,ct=/^$|^module$|\/(?:java|ecma)script/i;ot=xe.createDocumentFragment().appendChild(xe.createElement("div")),(st=xe.createElement("input")).setAttribute("type","radio"),st.setAttribute("checked","checked"),st.setAttribute("name","t"),ot.appendChild(st),ve.checkClone=ot.cloneNode(!0).cloneNode(!0).lastChild.checked,ot.innerHTML="",ve.noCloneChecked=!!ot.cloneNode(!0).lastChild.defaultValue,ot.innerHTML="",ve.option=!!ot.lastChild;var ft={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ft.tbody=ft.tfoot=ft.colgroup=ft.caption=ft.thead,ft.th=ft.td,ve.option||(ft.optgroup=ft.option=[1,""]);var dt=/<|&#?\w+;/,ht=/^([^.]*)(?:\.(.+)|)/;De.event={global:{},add:function(t,e,n,r,a){var i,o,s,l,u,c,f,d,h,p,g,m=Ge.get(t);if(Ve(t))for(n.handler&&(n=(i=n).handler,a=i.selector),a&&De.find.matchesSelector(tt,a),n.guid||(n.guid=De.guid++),(l=m.events)||(l=m.events=Object.create(null)),(o=m.handle)||(o=m.handle=function(e){return void 0!==De&&De.event.triggered!==e.type?De.event.dispatch.apply(t,arguments):undefined}),u=(e=(e||"").match(We)||[""]).length;u--;)h=g=(s=ht.exec(e[u])||[])[1],p=(s[2]||"").split(".").sort(),h&&(f=De.event.special[h]||{},h=(a?f.delegateType:f.bindType)||h,f=De.event.special[h]||{},c=De.extend({type:h,origType:g,data:r,handler:n,guid:n.guid,selector:a,needsContext:a&&De.expr.match.needsContext.test(a),namespace:p.join(".")},i),(d=l[h])||((d=l[h]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(t,r,p,o)||t.addEventListener&&t.addEventListener(h,o)),f.add&&(f.add.call(t,c),c.handler.guid||(c.handler.guid=n.guid)),a?d.splice(d.delegateCount++,0,c):d.push(c),De.event.global[h]=!0)},remove:function(e,t,n,r,a){var i,o,s,l,u,c,f,d,h,p,g,m=Ge.hasData(e)&&Ge.get(e);if(m&&(l=m.events)){for(u=(t=(t||"").match(We)||[""]).length;u--;)if(h=g=(s=ht.exec(t[u])||[])[1],p=(s[2]||"").split(".").sort(),h){for(f=De.event.special[h]||{},d=l[h=(r?f.delegateType:f.bindType)||h]||[],s=s[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),o=i=d.length;i--;)c=d[i],!a&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(d.splice(i,1),c.selector&&d.delegateCount--,f.remove&&f.remove.call(e,c));o&&!d.length&&(f.teardown&&!1!==f.teardown.call(e,p,m.handle)||De.removeEvent(e,h,m.handle),delete l[h])}else for(h in l)De.event.remove(e,h+t[u],n,r,!0);De.isEmptyObject(l)&&Ge.remove(e,"handle events")}},dispatch:function(e){var t,n,r,a,i,o,s=new Array(arguments.length),l=De.event.fix(e),u=(Ge.get(this,"events")||Object.create(null))[l.type]||[],c=De.event.special[l.type]||{};for(s[0]=l,t=1;t\s*$/g;De.extend({htmlPrefilter:function(e){return e},clone:function(e,t,n){var r,a,i,o,s=e.cloneNode(!0),l=nt(e);if(!(ve.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||De.isXMLDoc(e)))for(o=x(s),r=0,a=(i=x(e)).length;r").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",a=function(e){r.remove(),a=null,e&&t("error"===e.type?404:200,e.type)}),xe.head.appendChild(r[0])},abort:function(){a&&a()}}});var ln,un=[],cn=/(=)\?(?=&|$)|\?\?/;De.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=un.pop()||De.expando+"_"+qt.guid++;return this[e]=!0,e}}),De.ajaxPrefilter("json jsonp",function(e,t,n){var r,a,i,o=!1!==e.jsonp&&(cn.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&cn.test(e.data)&&"data");if(o||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=ye(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,o?e[o]=e[o].replace(cn,"$1"+r):!1!==e.jsonp&&(e.url+=(Wt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return i||De.error(r+" was not called"),i[0]},e.dataTypes[0]="json",a=le[r],le[r]=function(){i=arguments},n.always(function(){a===undefined?De(le).removeProp(r):le[r]=a,e[r]&&(e.jsonpCallback=t.jsonpCallback,un.push(r)),i&&ye(a)&&a(i[0]),i=a=undefined}),"script"}),ve.createHTMLDocument=((ln=xe.implementation.createHTMLDocument("").body).innerHTML="
",2===ln.childNodes.length),De.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(ve.createHTMLDocument?((r=(t=xe.implementation.createHTMLDocument("")).createElement("base")).href=xe.location.href,t.head.appendChild(r)):t=xe),i=!n&&[],(a=Pe.exec(e))?[t.createElement(a[1])]:(a=S([e],t,i),i&&i.length&&De(i).remove(),De.merge([],a.childNodes)));var r,a,i},De.fn.load=function(e,t,n){var r,a,i,o=this,s=e.indexOf(" ");return-1").append(De.parseHTML(e)).find(r):e)}).always(n&&function(e,t){o.each(function(){n.apply(this,i||[e.responseText,t,e])})}),this},De.expr.pseudos.animated=function(t){return De.grep(De.timers,function(e){return t===e.elem}).length},De.offset={setOffset:function(e,t,n){var r,a,i,o,s,l,u=De.css(e,"position"),c=De(e),f={};"static"===u&&(e.style.position="relative"),s=c.offset(),i=De.css(e,"top"),l=De.css(e,"left"),("absolute"===u||"fixed"===u)&&-1<(i+l).indexOf("auto")?(o=(r=c.position()).top,a=r.left):(o=parseFloat(i)||0,a=parseFloat(l)||0),ye(t)&&(t=t.call(e,n,De.extend({},s))),null!=t.top&&(f.top=t.top-s.top+o),null!=t.left&&(f.left=t.left-s.left+a),"using"in t?t.using.call(e,f):c.css(f)}},De.fn.extend({offset:function(t){if(arguments.length)return t===undefined?this:this.each(function(e){De.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],a={top:0,left:0};if("fixed"===De.css(r,"position"))t=r.getBoundingClientRect();else{for(t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;e&&(e===n.body||e===n.documentElement)&&"static"===De.css(e,"position");)e=e.parentNode;e&&e!==r&&1===e.nodeType&&((a=De(e).offset()).top+=De.css(e,"borderTopWidth",!0),a.left+=De.css(e,"borderLeftWidth",!0))}return{top:t.top-a.top-De.css(r,"marginTop",!0),left:t.left-a.left-De.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){for(var e=this.offsetParent;e&&"static"===De.css(e,"position");)e=e.offsetParent;return e||tt})}}),De.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,a){var i="pageYOffset"===a;De.fn[t]=function(e){return Ue(this,function(e,t,n){var r;if(be(e)?r=e:9===e.nodeType&&(r=e.defaultView),n===undefined)return r?r[a]:e[t];r?r.scrollTo(i?r.pageXOffset:n,i?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),De.each(["top","left"],function(e,n){De.cssHooks[n]=F(ve.pixelPosition,function(e,t){if(t)return t=R(e,n),vt.test(t)?De(e).position()[n]+"px":t})}),De.each({Height:"height",Width:"width"},function(o,s){De.each({padding:"inner"+o,content:s,"":"outer"+o},function(r,i){De.fn[i]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),a=r||(!0===e||!0===t?"margin":"border");return Ue(this,function(e,t,n){var r;return be(e)?0===i.indexOf("outer")?e["inner"+o]:e.document.documentElement["client"+o]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+o],r["scroll"+o],e.body["offset"+o],r["offset"+o],r["client"+o])):n===undefined?De.css(e,t,a):De.style(e,t,n,a)},s,n?e:undefined,n)}})}),De.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){De.fn[t]=function(e){return this.on(t,e)}}),De.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.on("mouseenter",e).on("mouseleave",t||e)}}),De.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){De.fn[n]=function(e,t){return 0"}for(var i=0,o="",s=[];e.length||t.length;){var l=r().splice(0,1)[0];if(o+=w(n.substr(i,l.offset-i)),i=l.offset,"start"==l.event)o+=a(l.node),s.push(l.node);else if("stop"==l.event){var u=s.length;do{var c=s[--u];o+=""}while(c!=l.node);for(s.splice(u,1);u'+w(a[0])+""):n+=w(a[0]),r=t.lR.lastIndex,a=t.lR.exec(e)}return n+=w(e.substr(r,e.length-r))}function f(e,t){if(t.sL&&D[t.sL]){var n=T(t.sL,e);return g+=n.keyword_count,n.value}return r(e,t)}function d(e,t){var n=e.cN?'':"";e.rB?(m+=n,e.buffer=""):e.eB?(m+=w(t)+n,e.buffer=""):(m+=n,e.buffer=t),h.push(e),p+=e.r}function i(e,t,n){var r=h[h.length-1];if(n)return m+=f(r.buffer+e,r),!1;var a=l(t,r);if(a)return m+=f(r.buffer+e,r),d(a,t),a.rB;var i=u(h.length-1,t);if(i){var o=r.cN?"":"";for(r.rE?m+=f(r.buffer+e,r)+o:r.eE?m+=f(r.buffer+e,r)+o+w(t):m+=f(r.buffer+e+t,r)+o;1":"",m+=o,i--,h.length--;var s=h[h.length-1];return h.length--,h[h.length-1].buffer="",s.starts&&d(s.starts,""),r.rE}if(c(t,r))throw"Illegal"}var s=D[e],h=[s.dM],p=0,g=0,m="";try{var v=0;s.dM.buffer="";do{var y=n(t,v),b=i(y[0],y[1],y[2]);v+=y[0].length,b||(v+=y[1].length)}while(!y[2]);if(1o.keyword_count+o.r&&(o=l),l.keyword_count+l.r>i.keyword_count+i.r&&(o=i,i=l)}}var u=e.className;u.match(i.language)||(u=u?u+" "+i.language:i.language);var c=g(e);if(c.length)(f=document.createElement("pre")).innerHTML=i.value,i.value=m(c,g(f),r);if(n&&(i.value=i.value.replace(/^((<[^>]+>|\t)+)/gm,function(e,t){return t.replace(/\t/g,n)})),t&&(i.value=i.value.replace(/\n/g,"
")),/MSIE [678]/.test(navigator.userAgent)&&"CODE"==e.tagName&&"PRE"==e.parentNode.tagName){var f=e.parentNode,d=document.createElement("div");d.innerHTML="
"+i.value+"
",e=d.firstChild.firstChild,d.firstChild.cN=f.cN,f.parentNode.replaceChild(d.firstChild,f)}else e.innerHTML=i.value;e.className=u,e.dataset={},e.dataset.result={language:i.language,kw:i.keyword_count,re:i.r},o&&o.language&&(e.dataset.second_best={language:o.language,kw:o.keyword_count,re:o.r})}}function i(){if(!i.called){i.called=!0,v();for(var e=document.getElementsByTagName("pre"),t=0;t|>=|>>|>>=|>>>|>>>=|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",this.BE={b:"\\\\.",r:0},this.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[this.BE],r:0},this.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[this.BE],r:0},this.CLCM={cN:"comment",b:"//",e:"$"},this.CBLCLM={cN:"comment",b:"/\\*",e:"\\*/"},this.HCM={cN:"comment",b:"#",e:"$"},this.NM={cN:"number",b:this.NR,r:0},this.CNM={cN:"number",b:this.CNR,r:0},this.inherit=function(e,t){var n={};for(var r in e)n[r]=e[r];if(t)for(var r in t)n[r]=t[r];return n}};hljs.LANGUAGES.ruby=function(){var e="[a-zA-Z_][a-zA-Z0-9_]*(\\!|\\?)?",t="[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?",n={keyword:{and:1,"false":1,then:1,defined:1,module:1,"in":1,"return":1,redo:1,"if":1,BEGIN:1,retry:1,end:1,"for":1,"true":1,self:1,when:1,next:1,until:1,"do":1,begin:1,unless:1,END:1,rescue:1,nil:1,"else":1,"break":1,undef:1,not:1,"super":1,"class":1,"case":1,require:1,"yield":1,alias:1,"while":1,ensure:1,elsif:1,or:1,def:1},keymethods:{__id__:1,__send__:1,abort:1,abs:1,"all?":1,allocate:1,ancestors:1,"any?":1,arity:1,assoc:1,at:1,at_exit:1,autoload:1,"autoload?":1,"between?":1,binding:1,binmode:1,"block_given?":1,call:1,callcc:1,caller:1,capitalize:1,"capitalize!":1,casecmp:1,"catch":1,ceil:1,center:1,chomp:1,"chomp!":1,chop:1,"chop!":1,chr:1,"class":1,class_eval:1,"class_variable_defined?":1,class_variables:1,clear:1,clone:1,close:1,close_read:1,close_write:1,"closed?":1,coerce:1,collect:1,"collect!":1,compact:1,"compact!":1,concat:1,"const_defined?":1,const_get:1,const_missing:1,const_set:1,constants:1,count:1,crypt:1,"default":1,default_proc:1,"delete":1,"delete!":1,delete_at:1,delete_if:1,detect:1,display:1,div:1,divmod:1,downcase:1,"downcase!":1,downto:1,dump:1,dup:1,each:1,each_byte:1,each_index:1,each_key:1,each_line:1,each_pair:1,each_value:1,each_with_index:1,"empty?":1,entries:1,eof:1,"eof?":1,"eql?":1,"equal?":1,eval:1,exec:1,exit:1,"exit!":1,extend:1,fail:1,fcntl:1,fetch:1,fileno:1,fill:1,find:1,find_all:1,first:1,flatten:1,"flatten!":1,floor:1,flush:1,for_fd:1,foreach:1,fork:1,format:1,freeze:1,"frozen?":1,fsync:1,getc:1,gets:1,global_variables:1,grep:1,gsub:1,"gsub!":1,"has_key?":1,"has_value?":1,hash:1,hex:1,id:1,include:1,"include?":1,included_modules:1,index:1,indexes:1,indices:1,induced_from:1,inject:1,insert:1,inspect:1,instance_eval:1,instance_method:1,instance_methods:1,"instance_of?":1,"instance_variable_defined?":1,instance_variable_get:1,instance_variable_set:1,instance_variables:1,"integer?":1,intern:1,invert:1,ioctl:1,"is_a?":1,isatty:1,"iterator?":1,join:1,"key?":1,keys:1,"kind_of?":1,lambda:1,last:1,length:1,lineno:1,ljust:1,load:1,local_variables:1,loop:1,lstrip:1,"lstrip!":1,map:1,"map!":1,match:1,max:1,"member?":1,merge:1,"merge!":1,method:1,"method_defined?":1,method_missing:1,methods:1,min:1,module_eval:1,modulo:1,name:1,nesting:1,"new":1,next:1,"next!":1,"nil?":1,nitems:1,"nonzero?":1,object_id:1,oct:1,open:1,pack:1,partition:1,pid:1,pipe:1,pop:1,popen:1,pos:1,prec:1,prec_f:1,prec_i:1,print:1,printf:1,private_class_method:1,private_instance_methods:1,"private_method_defined?":1,private_methods:1,proc:1,protected_instance_methods:1,"protected_method_defined?":1,protected_methods:1,public_class_method:1,public_instance_methods:1,"public_method_defined?":1,public_methods:1,push:1,putc:1,puts:1,quo:1,raise:1,rand:1,rassoc:1,read:1,read_nonblock:1,readchar:1,readline:1,readlines:1,readpartial:1,rehash:1,reject:1,"reject!":1,remainder:1,reopen:1,replace:1,require:1,"respond_to?":1,reverse:1,"reverse!":1,reverse_each:1,rewind:1,rindex:1,rjust:1,round:1,rstrip:1,"rstrip!":1,scan:1,seek:1,select:1,send:1,set_trace_func:1,shift:1,singleton_method_added:1,singleton_methods:1,size:1,sleep:1,slice:1,"slice!":1,sort:1,"sort!":1,sort_by:1,split:1,sprintf:1,squeeze:1,"squeeze!":1,srand:1,stat:1,step:1,store:1,strip:1,"strip!":1,sub:1,"sub!":1,succ:1,"succ!":1,sum:1,superclass:1,swapcase:1,"swapcase!":1,sync:1,syscall:1,sysopen:1,sysread:1,sysseek:1,system:1,syswrite:1,taint:1,"tainted?":1,tell:1,test:1,"throw":1,times:1,to_a:1,to_ary:1,to_f:1,to_hash:1,to_i:1,to_int:1,to_io:1,to_proc:1,to_s:1,to_str:1,to_sym:1,tr:1,"tr!":1,tr_s:1,"tr_s!":1,trace_var:1,transpose:1,trap:1,truncate:1,"tty?":1,type:1,ungetc:1,uniq:1,"uniq!":1,unpack:1,unshift:1,untaint:1,untrace_var:1,upcase:1,"upcase!":1, +update:1,upto:1,"value?":1,values:1,values_at:1,warn:1,write:1,write_nonblock:1,"zero?":1,zip:1}},r={cN:"yardoctag",b:"@[A-Za-z]+"},a={cN:"comment",b:"#",e:"$",c:[r]},i={cN:"comment",b:"^\\=begin",e:"^\\=end",c:[r],r:10},o={cN:"comment",b:"^__END__",e:"\\n$"},s={cN:"subst",b:"#\\{",e:"}",l:e,k:n},l=[hljs.BE,s],u={cN:"string",b:"'",e:"'",c:l,r:0},c={cN:"string",b:'"',e:'"',c:l,r:0},f={cN:"string",b:"%[qw]?\\(",e:"\\)",c:l,r:10},d={cN:"string",b:"%[qw]?\\[",e:"\\]",c:l,r:10},h={cN:"string",b:"%[qw]?{",e:"}",c:l,r:10},p={cN:"string",b:"%[qw]?<",e:">",c:l,r:10},g={cN:"string",b:"%[qw]?/",e:"/",c:l,r:10},m={cN:"string",b:"%[qw]?%",e:"%",c:l,r:10},v={cN:"string",b:"%[qw]?-",e:"-",c:l,r:10},y={cN:"string",b:"%[qw]?\\|",e:"\\|",c:l,r:10},b={cN:"function",b:"\\bdef\\s+",e:" |$|;",l:e,k:n,c:[{cN:"title",b:t,l:e,k:n},{cN:"params",b:"\\(",e:"\\)",l:e,k:n},a,i,o]},x={cN:"identifier",b:e,l:e,k:n,r:0},w=[a,i,o,u,c,f,d,h,p,g,m,v,y,{cN:"class",b:"\\b(class|module)\\b",e:"$|;",k:{"class":1,module:1},c:[{cN:"title",b:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?",r:0},{cN:"inheritance",b:"<\\s*",c:[{cN:"parent",b:"("+hljs.IR+"::)?"+hljs.IR}]},a,i,o]},b,{cN:"constant",b:"(::)?([A-Z]\\w*(::)?)+",r:0},{cN:"symbol",b:":",c:[u,c,f,d,h,p,g,m,v,y,x],r:0},{cN:"number",b:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",r:0},{cN:"number",b:"\\?\\w"},{cN:"variable",b:"(\\$\\W)|((\\$|\\@\\@?)(\\w+))"},x,{b:"("+hljs.RSR+")\\s*",c:[a,i,o,{cN:"regexp",b:"/",e:"/[a-z]*",i:"\\n",c:[hljs.BE]}],r:0}];return s.c=w,{dM:{l:e,k:n,c:b.c[1].c=w}}}(),function(c,s,o){function l(e,t,n){var r=s.createElement(e);return t&&(r.id=te+t),n&&(r.style.cssText=n),c(r)}function f(){return o.innerHeight?o.innerHeight:c(o).height()}function u(e,n){n!==Object(n)&&(n={}),this.cache={},this.el=e,this.value=function(e){var t;return this.cache[e]===undefined&&((t=c(this.el).attr("data-cbox-"+e))!==undefined?this.cache[e]=t:n[e]!==undefined?this.cache[e]=n[e]:Q[e]!==undefined&&(this.cache[e]=Q[e])),this.cache[e]},this.get=function(e){var t=this.value(e);return c.isFunction(t)?t.call(this.el,this):t}}function i(e){var t=N.length,n=(X+e)%t;return n<0?t+n:n}function d(e,t){return Math.round((/%/.test(e)?("x"===t?j.width():f())/100:1)*parseInt(e,10))}function h(e,t){return e.get("photo")||e.get("photoRegex").test(t)}function p(e,t){return e.get("retinaUrl")&&1"),x()}}function a(){S||(t=!1,j=c(o),S=l(ce).attr({id:ee,"class":!1===c.support.opacity?te+"IE":"",role:"dialog",tabindex:"-1"}).hide(),w=l(ce,"Overlay").hide(),E=c([l(ce,"LoadingOverlay")[0],l(ce,"LoadingGraphic")[0]]),T=l(ce,"Wrapper"),D=l(ce,"Content").append(R=l(ce,"Title"),F=l(ce,"Current"),M=c('