diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..325bfc036d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,51 @@ +# See https://docs.docker.com/engine/reference/builder/#dockerignore-file for more about ignoring files. + +# Ignore git directory. +/.git/ +/.gitignore + +# Ignore bundler config. +/.bundle + +# Ignore all environment files. +/.env* + +# Ignore all default key files. +/config/master.key +/config/credentials/*.key + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/.keep + +# Ignore storage (uploaded files in development and any SQLite databases). +/storage/* +!/storage/.keep +/tmp/storage/* +!/tmp/storage/.keep + +# Ignore assets. +/node_modules/ +/app/assets/builds/* +!/app/assets/builds/.keep +/public/assets + +# Ignore CI service files. +/.github + +# Ignore Kamal files. +/config/deploy*.yml +/.kamal + +# Ignore development files +/.devcontainer + +# Ignore Docker-related files +/.dockerignore +/Dockerfile* diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..8dc4323435 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +# See https://git-scm.com/docs/gitattributes for more about git attribute files. + +# Mark the database schema as having been generated. +db/schema.rb linguist-generated + +# Mark any vendored files as having been vendored. +vendor/* linguist-vendored +config/credentials/*.yml.enc diff=rails_credentials +config/credentials.yml.enc diff=rails_credentials diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..83610cfa4c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: +- package-ecosystem: bundler + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 10 +- package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 10 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000..9babd98a2a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,67 @@ +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: + bundler-cache: true + + - name: Scan for common Rails security vulnerabilities using static analysis + run: bin/brakeman --no-pager + + - name: Scan for known security vulnerabilities in gems used + run: bin/bundler-audit + + scan_js: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Scan for security vulnerabilities in JavaScript dependencies + run: bin/importmap audit + + lint: + runs-on: ubuntu-latest + env: + RUBOCOP_CACHE_ROOT: tmp/rubocop + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Prepare RuboCop cache + uses: actions/cache@v4 + env: + DEPENDENCIES_HASH: ${{ hashFiles('.ruby-version', '**/.rubocop.yml', '**/.rubocop_todo.yml', 'Gemfile.lock') }} + with: + path: ${{ env.RUBOCOP_CACHE_ROOT }} + key: rubocop-${{ runner.os }}-${{ env.DEPENDENCIES_HASH }}-${{ github.ref_name == github.event.repository.default_branch && github.run_id || 'default' }} + restore-keys: | + rubocop-${{ runner.os }}-${{ env.DEPENDENCIES_HASH }}- + + - name: Lint code for consistent style + run: bin/rubocop -f github + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..da0d47b24e --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# 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 key files for decrypting credentials and more. +/config/*.key + +roadmap.pdf +entrega-terceira-sprint.pdf diff --git a/.kamal/hooks/docker-setup.sample b/.kamal/hooks/docker-setup.sample new file mode 100755 index 0000000000..2fb07d7d7a --- /dev/null +++ b/.kamal/hooks/docker-setup.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Docker set up on $KAMAL_HOSTS..." diff --git a/.kamal/hooks/post-app-boot.sample b/.kamal/hooks/post-app-boot.sample new file mode 100755 index 0000000000..70f9c4bc95 --- /dev/null +++ b/.kamal/hooks/post-app-boot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Booted app version $KAMAL_VERSION on $KAMAL_HOSTS..." diff --git a/.kamal/hooks/post-deploy.sample b/.kamal/hooks/post-deploy.sample new file mode 100755 index 0000000000..fd364c2a77 --- /dev/null +++ b/.kamal/hooks/post-deploy.sample @@ -0,0 +1,14 @@ +#!/bin/sh + +# A sample post-deploy hook +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_ROLES (if set) +# KAMAL_DESTINATION (if set) +# KAMAL_RUNTIME + +echo "$KAMAL_PERFORMER deployed $KAMAL_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds" diff --git a/.kamal/hooks/post-proxy-reboot.sample b/.kamal/hooks/post-proxy-reboot.sample new file mode 100755 index 0000000000..1435a677f2 --- /dev/null +++ b/.kamal/hooks/post-proxy-reboot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Rebooted kamal-proxy on $KAMAL_HOSTS" diff --git a/.kamal/hooks/pre-app-boot.sample b/.kamal/hooks/pre-app-boot.sample new file mode 100755 index 0000000000..45f7355045 --- /dev/null +++ b/.kamal/hooks/pre-app-boot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Booting app version $KAMAL_VERSION on $KAMAL_HOSTS..." diff --git a/.kamal/hooks/pre-build.sample b/.kamal/hooks/pre-build.sample new file mode 100755 index 0000000000..c5a55678b2 --- /dev/null +++ b/.kamal/hooks/pre-build.sample @@ -0,0 +1,51 @@ +#!/bin/sh + +# A sample pre-build hook +# +# Checks: +# 1. We have a clean checkout +# 2. A remote is configured +# 3. The branch has been pushed to the remote +# 4. The version we are deploying matches the remote +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_ROLES (if set) +# KAMAL_DESTINATION (if set) + +if [ -n "$(git status --porcelain)" ]; then + echo "Git checkout is not clean, aborting..." >&2 + git status --porcelain >&2 + exit 1 +fi + +first_remote=$(git remote) + +if [ -z "$first_remote" ]; then + echo "No git remote set, aborting..." >&2 + exit 1 +fi + +current_branch=$(git branch --show-current) + +if [ -z "$current_branch" ]; then + echo "Not on a git branch, aborting..." >&2 + exit 1 +fi + +remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1) + +if [ -z "$remote_head" ]; then + echo "Branch not pushed to remote, aborting..." >&2 + exit 1 +fi + +if [ "$KAMAL_VERSION" != "$remote_head" ]; then + echo "Version ($KAMAL_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2 + exit 1 +fi + +exit 0 diff --git a/.kamal/hooks/pre-connect.sample b/.kamal/hooks/pre-connect.sample new file mode 100755 index 0000000000..77744bdca8 --- /dev/null +++ b/.kamal/hooks/pre-connect.sample @@ -0,0 +1,47 @@ +#!/usr/bin/env ruby + +# A sample pre-connect check +# +# Warms DNS before connecting to hosts in parallel +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_ROLES (if set) +# KAMAL_DESTINATION (if set) +# KAMAL_RUNTIME + +hosts = ENV["KAMAL_HOSTS"].split(",") +results = nil +max = 3 + +elapsed = Benchmark.realtime do + results = hosts.map do |host| + Thread.new do + tries = 1 + + begin + Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME) + rescue SocketError + if tries < max + puts "Retrying DNS warmup: #{host}" + tries += 1 + sleep rand + retry + else + puts "DNS warmup failed: #{host}" + host + end + end + + tries + end + end.map(&:value) +end + +retries = results.sum - hosts.size +nopes = results.count { |r| r == max } + +puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ] diff --git a/.kamal/hooks/pre-deploy.sample b/.kamal/hooks/pre-deploy.sample new file mode 100755 index 0000000000..05b3055b72 --- /dev/null +++ b/.kamal/hooks/pre-deploy.sample @@ -0,0 +1,122 @@ +#!/usr/bin/env ruby + +# A sample pre-deploy hook +# +# Checks the Github status of the build, waiting for a pending build to complete for up to 720 seconds. +# +# Fails unless the combined status is "success" +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_COMMAND +# KAMAL_SUBCOMMAND +# KAMAL_ROLES (if set) +# KAMAL_DESTINATION (if set) + +# Only check the build status for production deployments +if ENV["KAMAL_COMMAND"] == "rollback" || ENV["KAMAL_DESTINATION"] != "production" + exit 0 +end + +require "bundler/inline" + +# true = install gems so this is fast on repeat invocations +gemfile(true, quiet: true) do + source "https://rubygems.org" + + gem "octokit" + gem "faraday-retry" +end + +MAX_ATTEMPTS = 72 +ATTEMPTS_GAP = 10 + +def exit_with_error(message) + $stderr.puts message + exit 1 +end + +class GithubStatusChecks + attr_reader :remote_url, :git_sha, :github_client, :combined_status + + def initialize + @remote_url = github_repo_from_remote_url + @git_sha = `git rev-parse HEAD`.strip + @github_client = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"]) + refresh! + end + + def refresh! + @combined_status = github_client.combined_status(remote_url, git_sha) + end + + def state + combined_status[:state] + end + + def first_status_url + first_status = combined_status[:statuses].find { |status| status[:state] == state } + first_status && first_status[:target_url] + end + + def complete_count + combined_status[:statuses].count { |status| status[:state] != "pending"} + end + + def total_count + combined_status[:statuses].count + end + + def current_status + if total_count > 0 + "Completed #{complete_count}/#{total_count} checks, see #{first_status_url} ..." + else + "Build not started..." + end + end + + private + def github_repo_from_remote_url + url = `git config --get remote.origin.url`.strip.delete_suffix(".git") + if url.start_with?("https://github.com/") + url.delete_prefix("https://github.com/") + elsif url.start_with?("git@github.com:") + url.delete_prefix("git@github.com:") + else + url + end + end +end + + +$stdout.sync = true + +begin + puts "Checking build status..." + + attempts = 0 + checks = GithubStatusChecks.new + + loop do + case checks.state + when "success" + puts "Checks passed, see #{checks.first_status_url}" + exit 0 + when "failure" + exit_with_error "Checks failed, see #{checks.first_status_url}" + when "pending" + attempts += 1 + end + + exit_with_error "Checks are still pending, gave up after #{MAX_ATTEMPTS * ATTEMPTS_GAP} seconds" if attempts == MAX_ATTEMPTS + + puts checks.current_status + sleep(ATTEMPTS_GAP) + checks.refresh! + end +rescue Octokit::NotFound + exit_with_error "Build status could not be found" +end diff --git a/.kamal/hooks/pre-proxy-reboot.sample b/.kamal/hooks/pre-proxy-reboot.sample new file mode 100755 index 0000000000..061f8059e6 --- /dev/null +++ b/.kamal/hooks/pre-proxy-reboot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Rebooting kamal-proxy on $KAMAL_HOSTS..." diff --git a/.kamal/secrets b/.kamal/secrets new file mode 100644 index 0000000000..b3089d6f5a --- /dev/null +++ b/.kamal/secrets @@ -0,0 +1,20 @@ +# Secrets defined here are available for reference under registry/password, env/secret, builder/secrets, +# and accessories/*/env/secret in config/deploy.yml. All secrets should be pulled from either +# password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git. + +# Example of extracting secrets from 1password (or another compatible pw manager) +# SECRETS=$(kamal secrets fetch --adapter 1password --account your-account --from Vault/Item KAMAL_REGISTRY_PASSWORD RAILS_MASTER_KEY) +# KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD ${SECRETS}) +# RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY ${SECRETS}) + +# Example of extracting secrets from Rails credentials +# KAMAL_REGISTRY_PASSWORD=$(rails credentials:fetch kamal.registry_password) + +# Use a GITHUB_TOKEN if private repositories are needed for the image +# GITHUB_TOKEN=$(gh config get -h github.com oauth_token) + +# Grab the registry password from ENV +# KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD + +# Improve security by using a password manager. Never check config/master.key into git! +RAILS_MASTER_KEY=$(cat config/master.key) diff --git a/.rdoc_options b/.rdoc_options new file mode 100644 index 0000000000..2f09de8e05 --- /dev/null +++ b/.rdoc_options @@ -0,0 +1,36 @@ +--- +encoding: UTF-8 +static_path: [] +rdoc_include: [] +page_dir: +apply_default_exclude: true +autolink_excluded_words: [] +canonical_root: +charset: UTF-8 +class_module_path_prefix: +embed_mixins: false +exclude: +- "(bin|config|db|features|log|public|spec|storage|tmp|vendor|coverage|doc)" +- "\\.(lock|pdf|json|css|js|scss|sass|erb|html|haml|slim|txt|md|rake|yaml|yml|sh|gitignore|gemspec)$" +- "app/(assets|javascript|views|mailers|jobs|helpers)" +- "/Dockerfile" +- "/Gemfile" +- "/Rakefile" +- "/README" +file_path_prefix: +footer_content: +hyperlink_all: false +line_numbers: false +locale_dir: locale +locale_name: +main_page: README.md +markup: rdoc +output_decoration: true +show_hash: false +skip_tests: true +tab_width: 8 +template_stylesheets: [] +title: CAMAAR +visibility: :protected +warn_missing_rdoc_ref: true +webcvs: diff --git a/.rspec b/.rspec new file mode 100644 index 0000000000..c99d2e7396 --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--require spec_helper diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000000..ec2a962778 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,11 @@ +# 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 + +Metrics/AbcSize: + Max: 20 diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000000..47b322c971 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.4.1 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..5317c4269b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,77 @@ +# syntax=docker/dockerfile:1 +# check=error=true + +# This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand: +# docker build -t camaar . +# docker run -d -p 80:80 -e RAILS_MASTER_KEY= --name camaar camaar + +# For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html + +# Make sure RUBY_VERSION matches the Ruby version in .ruby-version +ARG RUBY_VERSION=3.4.1 +FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base + +# Rails app lives here +WORKDIR /rails + +# Install base packages +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 && \ + ln -s /usr/lib/$(uname -m)-linux-gnu/libjemalloc.so.2 /usr/local/lib/libjemalloc.so && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Set production environment variables and enable jemalloc for reduced memory usage and latency. +ENV RAILS_ENV="production" \ + BUNDLE_DEPLOYMENT="1" \ + BUNDLE_PATH="/usr/local/bundle" \ + BUNDLE_WITHOUT="development" \ + LD_PRELOAD="/usr/local/lib/libjemalloc.so" + +# Throw-away build stage to reduce size of final image +FROM base AS build + +# Install packages needed to build gems +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y build-essential git libvips libyaml-dev pkg-config && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Install application gems +COPY vendor/* ./vendor/ +COPY Gemfile Gemfile.lock ./ + +RUN bundle install && \ + rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \ + # -j 1 disable parallel compilation to avoid a QEMU bug: https://github.com/rails/bootsnap/issues/495 + bundle exec bootsnap precompile -j 1 --gemfile + +# Copy application code +COPY . . + +# Precompile bootsnap code for faster boot times. +# -j 1 disable parallel compilation to avoid a QEMU bug: https://github.com/rails/bootsnap/issues/495 +RUN bundle exec bootsnap precompile -j 1 app/ lib/ + +# Precompiling assets for production without requiring secret RAILS_MASTER_KEY +RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile + + + + +# Final stage for app image +FROM base + +# Run and own only the runtime files as a non-root user for security +RUN groupadd --system --gid 1000 rails && \ + useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash +USER 1000:1000 + +# Copy built artifacts: gems, application +COPY --chown=rails:rails --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}" +COPY --chown=rails:rails --from=build /rails /rails + +# Entrypoint prepares the database. +ENTRYPOINT ["/rails/bin/docker-entrypoint"] + +# Start server via Thruster by default, this can be overwritten at runtime +EXPOSE 80 +CMD ["./bin/thrust", "./bin/rails", "server"] diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000000..84556f2d5e --- /dev/null +++ b/Gemfile @@ -0,0 +1,65 @@ +source "https://rubygems.org" + +# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" +gem "rails", "~> 8.1.3" +# The modern asset pipeline for Rails [https://github.com/rails/propshaft] +gem "propshaft" +# Use sqlite3 as the database for Active Record +gem "sqlite3", ">= 2.1" +# Use the Puma web server [https://github.com/puma/puma] +gem "puma", ">= 5.0" +# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails] +gem "importmap-rails" +# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev] +gem "turbo-rails" +# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev] +gem "stimulus-rails" +# Build JSON APIs with ease [https://github.com/rails/jbuilder] +gem "jbuilder" +# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] +gem "bcrypt", "~> 3.1.7" +# CSV support for Ruby 3.4+ +gem "csv" +# Windows does not include zoneinfo files, so bundle the tzinfo-data gem +gem "tzinfo-data", platforms: %i[ windows jruby ] +# Use the database-backed adapters for Rails.cache, Active Job, and Action Cable +gem "solid_cache" +gem "solid_queue" +gem "solid_cable" +# Reduces boot times through caching; required in config/boot.rb +gem "bootsnap", require: false +# Deploy this application anywhere as a Docker container [https://kamal-deploy.org] +gem "kamal", require: false +# Add HTTP asset caching/compression and X-Sendfile acceleration to Puma +gem "thruster", require: false +# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] +gem "image_processing", "~> 1.2" + +group :development do + # Use console on exceptions pages [https://github.com/rails/web-console] + gem "web-console" +end + +group :development, :test do + # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem + gem "debug", platforms: %i[ mri windows ], require: "debug/prelude" + # Audits gems for known security defects + gem "bundler-audit", require: false + # Static analysis for security vulnerabilities [https://brakemanscanner.org/] + gem "brakeman", require: false + # Omakase Ruby styling + gem "rubocop-rails-omakase", require: false + # Testes e análise de código + gem "rspec-rails" + gem "factory_bot_rails" + gem "rubycritic", require: false + gem "rdoc" +end + +group :test do + gem "cucumber-rails", require: false + gem "database_cleaner" + gem "capybara" + gem "selenium-webdriver" + gem "simplecov", require: false +end diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000000..bd120fac56 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,745 @@ +GEM + remote: https://rubygems.org/ + specs: + action_text-trix (2.1.19) + railties + actioncable (8.1.3) + actionpack (= 8.1.3) + activesupport (= 8.1.3) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + zeitwerk (~> 2.6) + actionmailbox (8.1.3) + actionpack (= 8.1.3) + activejob (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) + mail (>= 2.8.0) + actionmailer (8.1.3) + actionpack (= 8.1.3) + actionview (= 8.1.3) + activejob (= 8.1.3) + activesupport (= 8.1.3) + mail (>= 2.8.0) + rails-dom-testing (~> 2.2) + actionpack (8.1.3) + actionview (= 8.1.3) + activesupport (= 8.1.3) + nokogiri (>= 1.8.5) + rack (>= 2.2.4) + rack-session (>= 1.0.1) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) + actiontext (8.1.3) + action_text-trix (~> 2.1.15) + actionpack (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (8.1.3) + activesupport (= 8.1.3) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (8.1.3) + activesupport (= 8.1.3) + globalid (>= 0.3.6) + activemodel (8.1.3) + activesupport (= 8.1.3) + activerecord (8.1.3) + activemodel (= 8.1.3) + activesupport (= 8.1.3) + timeout (>= 0.4.0) + activestorage (8.1.3) + actionpack (= 8.1.3) + activejob (= 8.1.3) + activerecord (= 8.1.3) + activesupport (= 8.1.3) + marcel (~> 1.0) + activesupport (8.1.3) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + json + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) + ast (2.4.3) + 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.22) + bcrypt_pbkdf (1.1.2) + bigdecimal (4.1.2) + bindex (0.8.1) + bootsnap (1.24.4) + msgpack (~> 1.2) + brakeman (8.0.4) + racc + builder (3.3.0) + bundler-audit (0.9.3) + bundler (>= 1.2.0) + thor (~> 1.0) + capybara (3.40.0) + addressable + matrix + mini_mime (>= 0.1.3) + nokogiri (~> 1.11) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + regexp_parser (>= 1.5, < 3.0) + xpath (~> 3.2) + childprocess (5.1.0) + logger (~> 1.5) + coercible (1.0.0) + descendants_tracker (~> 0.0.1) + concurrent-ruby (1.3.7) + connection_pool (3.0.2) + crass (1.0.6) + csv (3.3.5) + cucumber (10.2.0) + base64 (~> 0.2) + builder (~> 3.2) + cucumber-ci-environment (> 9, < 12) + cucumber-core (> 15, < 17) + cucumber-cucumber-expressions (> 17, < 20) + cucumber-html-formatter (> 21, < 23) + diff-lcs (~> 1.5) + logger (~> 1.6) + mini_mime (~> 1.1) + multi_test (~> 1.1) + sys-uname (~> 1.3) + cucumber-ci-environment (11.0.0) + cucumber-core (16.2.0) + cucumber-gherkin (> 36, < 40) + cucumber-messages (> 31, < 33) + cucumber-tag-expressions (> 6, < 9) + cucumber-cucumber-expressions (19.0.0) + bigdecimal + cucumber-gherkin (39.1.0) + cucumber-messages (>= 31, < 33) + cucumber-html-formatter (22.3.0) + cucumber-messages (> 23, < 33) + cucumber-messages (32.3.1) + cucumber-rails (4.0.1) + capybara (>= 3.25, < 4) + cucumber (>= 7, < 11) + railties (>= 6.1, < 9) + cucumber-tag-expressions (8.1.0) + database_cleaner (2.1.0) + database_cleaner-active_record (>= 2, < 3) + database_cleaner-active_record (2.2.2) + activerecord (>= 5.a) + database_cleaner-core (~> 2.0) + database_cleaner-core (2.0.1) + date (3.5.1) + debug (1.11.1) + irb (~> 1.10) + reline (>= 0.3.8) + descendants_tracker (0.0.4) + thread_safe (~> 0.3, >= 0.3.1) + diff-lcs (1.6.2) + docile (1.4.1) + dotenv (3.2.0) + drb (2.2.3) + dry-configurable (1.4.0) + dry-core (~> 1.0) + zeitwerk (~> 2.6) + dry-core (1.2.0) + concurrent-ruby (~> 1.0) + logger + zeitwerk (~> 2.6) + dry-inflector (1.3.1) + 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.16.0) + concurrent-ruby (~> 1.0) + dry-configurable (~> 1.0, >= 1.0.1) + dry-core (~> 1.1) + dry-initializer (~> 3.2) + dry-logic (~> 1.6) + dry-types (~> 1.9, >= 1.9.1) + zeitwerk (~> 2.6) + dry-types (1.9.1) + 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.4) + erubi (1.13.1) + et-orbi (1.4.0) + tzinfo + factory_bot (6.6.0) + activesupport (>= 6.1.0) + factory_bot_rails (6.5.1) + factory_bot (~> 6.5) + railties (>= 6.1.0) + ffi (1.17.4-x64-mingw-ucrt) + ffi (1.17.4-x86_64-linux-gnu) + flay (2.14.4) + erubi (~> 1.10) + path_expander (~> 2.0) + prism (~> 1.7) + sexp_processor (~> 4.0) + flog (4.9.4) + path_expander (~> 2.0) + prism (~> 1.7) + 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.8) + concurrent-ruby (~> 1.0) + ice_nine (0.11.2) + image_processing (1.14.0) + mini_magick (>= 4.9.5, < 6) + ruby-vips (>= 2.0.17, < 3) + importmap-rails (2.2.3) + actionpack (>= 6.0.0) + activesupport (>= 6.0.0) + railties (>= 6.0.0) + io-console (0.8.2) + irb (1.18.0) + pp (>= 0.6.0) + prism (>= 1.3.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + jbuilder (2.15.0) + actionview (>= 7.0.0) + activesupport (>= 7.0.0) + json (2.19.5) + kamal (2.11.0) + activesupport (>= 7.0) + base64 (~> 0.2) + bcrypt_pbkdf (~> 1.0) + concurrent-ruby (~> 1.2) + dotenv (~> 3.1) + ed25519 (~> 1.4) + net-ssh (~> 7.3) + sshkit (>= 1.23.0, < 2.0) + thor (~> 1.3) + zeitwerk (>= 2.6.18, < 3.0) + language_server-protocol (3.17.0.5) + launchy (3.1.1) + addressable (~> 2.8) + childprocess (~> 5.0) + logger (~> 1.6) + lint_roller (1.1.0) + logger (1.7.0) + loofah (2.25.1) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mail (2.9.0) + logger + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + marcel (1.1.1) + matrix (0.4.3) + memoist3 (1.0.0) + mini_magick (5.3.1) + logger + mini_mime (1.1.5) + minitest (6.0.6) + drb (~> 2.0) + prism (~> 1.5) + msgpack (1.8.0) + multi_test (1.1.0) + net-imap (0.6.4.1) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-scp (4.1.0) + net-ssh (>= 2.6.5, < 8.0.0) + net-sftp (4.0.0) + net-ssh (>= 5.0.0, < 8.0.0) + net-smtp (0.5.1) + net-protocol + net-ssh (7.3.2) + nio4r (2.7.5) + nokogiri (1.19.4-x64-mingw-ucrt) + racc (~> 1.4) + nokogiri (1.19.4-x86_64-linux-gnu) + racc (~> 1.4) + ostruct (0.6.3) + parallel (2.1.0) + parser (3.3.11.1) + ast (~> 2.4.1) + racc + path_expander (2.0.1) + pp (0.6.3) + prettyprint + prettyprint (0.2.0) + prism (1.9.0) + propshaft (1.3.2) + actionpack (>= 7.0.0) + activesupport (>= 7.0.0) + rack + psych (5.3.1) + date + stringio + public_suffix (7.0.5) + puma (8.0.2) + nio4r (~> 2.0) + raabro (1.4.0) + racc (1.8.1) + rack (3.2.6) + rack-session (2.1.2) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rack-test (2.2.0) + rack (>= 1.3) + rackup (2.3.1) + rack (>= 3) + rails (8.1.3) + actioncable (= 8.1.3) + actionmailbox (= 8.1.3) + actionmailer (= 8.1.3) + actionpack (= 8.1.3) + actiontext (= 8.1.3) + actionview (= 8.1.3) + activejob (= 8.1.3) + activemodel (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) + bundler (>= 1.15.0) + railties (= 8.1.3) + rails-dom-testing (2.3.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.7.0) + loofah (~> 2.25) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + railties (8.1.3) + actionpack (= 8.1.3) + activesupport (= 8.1.3) + irb (~> 1.13) + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + tsort (>= 0.2) + zeitwerk (~> 2.6) + rainbow (3.1.1) + rake (13.4.2) + rdoc (7.2.0) + erb + psych (>= 4.0.0) + tsort + 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.12.0) + reline (0.6.3) + io-console (~> 0.5) + rexml (3.4.4) + rspec-core (3.13.6) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.8) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-rails (8.0.4) + actionpack (>= 7.2) + activesupport (>= 7.2) + railties (>= 7.2) + rspec-core (>= 3.13.0, < 5.0.0) + rspec-expectations (>= 3.13.0, < 5.0.0) + rspec-mocks (>= 3.13.0, < 5.0.0) + rspec-support (>= 3.13.0, < 5.0.0) + rspec-support (3.13.7) + rubocop (1.86.2) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (>= 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.49.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.49.1) + parser (>= 3.3.7.2) + prism (~> 1.7) + rubocop-performance (1.26.1) + lint_roller (~> 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.47.1, < 2.0) + rubocop-rails (2.35.2) + activesupport (>= 4.2.0) + lint_roller (~> 1.1) + rack (>= 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.44.0, < 2.0) + rubocop-rails-omakase (1.1.0) + rubocop (>= 1.72) + rubocop-performance (>= 1.24) + rubocop-rails (>= 2.30) + ruby-progressbar (1.13.0) + ruby-vips (2.3.0) + ffi (~> 1.12) + logger + ruby_parser (3.22.0) + racc (~> 1.5) + sexp_processor (~> 4.16) + rubycritic (5.0.0) + flay (~> 2.13) + flog (~> 4.7) + launchy (>= 2.5.2) + ostruct + parser (>= 3.3.0.5) + prism (>= 1.6.0) + 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.3.0) + securerandom (0.4.1) + selenium-webdriver (4.44.0) + base64 (~> 0.2) + logger (~> 1.4) + rexml (~> 3.2, >= 3.2.5) + rubyzip (>= 1.2.2, < 4.0) + websocket (~> 1.0) + sexp_processor (4.17.5) + simplecov (0.22.0) + docile (~> 1.1) + simplecov-html (~> 0.11) + simplecov_json_formatter (~> 0.1) + 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.4.0) + activejob (>= 7.1) + activerecord (>= 7.1) + concurrent-ruby (>= 1.3.1) + fugit (~> 1.11) + railties (>= 7.1) + thor (>= 1.3.1) + sqlite3 (2.9.4-x64-mingw-ucrt) + sqlite3 (2.9.4-x86_64-linux-gnu) + sshkit (1.25.0) + base64 + logger + net-scp (>= 1.1.2) + net-sftp (>= 2.1.2) + net-ssh (>= 2.8.0) + ostruct + stimulus-rails (1.3.4) + railties (>= 6.0.0) + stringio (3.2.0) + sys-uname (1.5.1) + ffi (~> 1.1) + memoist3 (~> 1.0.0) + sys-uname (1.5.1-universal-mingw32) + ffi (~> 1.1) + memoist3 (~> 1.0.0) + win32ole + thor (1.5.0) + thread_safe (0.3.6) + thruster (0.1.21) + thruster (0.1.21-x86_64-linux) + timeout (0.6.1) + tsort (0.2.0) + tty-which (0.5.0) + turbo-rails (2.0.23) + actionpack (>= 7.1.0) + railties (>= 7.1.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + tzinfo-data (1.2026.2) + tzinfo (>= 1.0.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.2.0) + uri (1.1.1) + useragent (0.16.11) + virtus (2.0.0) + axiom-types (~> 0.1) + coercible (~> 1.0) + descendants_tracker (~> 0.0, >= 0.0.3) + web-console (4.3.0) + actionview (>= 8.0.0) + bindex (>= 0.4.0) + railties (>= 8.0.0) + websocket (1.2.11) + websocket-driver (0.8.0) + base64 + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + win32ole (1.9.3) + xpath (3.2.0) + nokogiri (~> 1.8) + zeitwerk (2.8.1) + +PLATFORMS + x64-mingw-ucrt + x86_64-linux + +DEPENDENCIES + bcrypt (~> 3.1.7) + bootsnap + brakeman + bundler-audit + capybara + csv + cucumber-rails + database_cleaner + debug + factory_bot_rails + image_processing (~> 1.2) + importmap-rails + jbuilder + kamal + propshaft + puma (>= 5.0) + rails (~> 8.1.3) + rdoc + rspec-rails + rubocop-rails-omakase + rubycritic + selenium-webdriver + simplecov + solid_cable + solid_cache + solid_queue + sqlite3 (>= 2.1) + stimulus-rails + thruster + turbo-rails + tzinfo-data + web-console + +CHECKSUMS + action_text-trix (2.1.19) sha256=7012f59421009cf284aa651294896414d653a61a2417c9b8714c8476d2f74009 + actioncable (8.1.3) sha256=e5bc7f75e44e6a22de29c4f43176927c3a9ce4824464b74ed18d8226e75a80f0 + actionmailbox (8.1.3) sha256=df7da474eaa0e70df4ed5a6fef66eb3b3b0f2dbf7f14518deee8d77f1b4aae59 + actionmailer (8.1.3) sha256=831f724891bb70d0aaa4d76581a6321124b6a752cb655c9346aae5479318448d + actionpack (8.1.3) sha256=af998cae4d47c5d581a2cc363b5c77eb718b7c4b45748d81b1887b25621c29a3 + actiontext (8.1.3) sha256=d291019c00e1ea9e6463011fa214f6081a56d7b9a1d224e7d3f6384c1dafc7d2 + actionview (8.1.3) sha256=1347c88c7f3edb38100c5ce0e9fb5e62d7755f3edc1b61cce2eb0b2c6ea2fd5d + activejob (8.1.3) sha256=a149b1766aa8204c3c3da7309e4becd40fcd5529c348cffbf6c9b16b565fe8d3 + activemodel (8.1.3) sha256=90c05cbe4cef3649b8f79f13016191ea94c4525ce4a5c0fb7ef909c4b91c8219 + activerecord (8.1.3) sha256=8003be7b2466ba0a2a670e603eeb0a61dd66058fccecfc49901e775260ac70ab + activestorage (8.1.3) sha256=0564ce9309143951a67615e1bb4e090ee54b8befed417133cae614479b46384d + activesupport (8.1.3) sha256=21a5e0dfbd4c3ddd9e1317ec6a4d782fa226e7867dc70b0743acda81a1dca20e + addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af + ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383 + axiom-types (0.1.1) sha256=c1ff113f3de516fa195b2db7e0a9a95fd1b08475a502ff660d04507a09980383 + base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b + bcrypt (3.1.22) sha256=1f0072e88c2d705d94aff7f2c5cb02eb3f1ec4b8368671e19112527489f29032 + bcrypt_pbkdf (1.1.2) sha256=c2414c23ce66869b3eb9f643d6a3374d8322dfb5078125c82792304c10b94cf6 + bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd + bindex (0.8.1) sha256=7b1ecc9dc539ed8bccfc8cb4d2732046227b09d6f37582ff12e50a5047ceb17e + bootsnap (1.24.4) sha256=a4d939fc2cc5242a83d3a7cb4fb97743ac58475afe91e0600479a3df6f117541 + brakeman (8.0.4) sha256=7bf921fa9638544835df9aa7b3e720a9a72c0267f34f92135955edd80d4dcf6f + builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f + bundler-audit (0.9.3) sha256=81c8766c71e47d0d28a0f98c7eed028539f21a6ea3cd8f685eb6f42333c9b4e9 + capybara (3.40.0) sha256=42dba720578ea1ca65fd7a41d163dd368502c191804558f6e0f71b391054aeef + childprocess (5.1.0) sha256=9a8d484be2fd4096a0e90a0cd3e449a05bc3aa33f8ac9e4d6dcef6ac1455b6ec + coercible (1.0.0) sha256=5081ad24352cc8435ce5472bc2faa30260c7ea7f2102cc6a9f167c4d9bffaadc + concurrent-ruby (1.3.7) sha256=4412caec3a5ea2e5fdc52076724c071a81f2c0593d83b2ac8cbb8ca63b3151b0 + connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a + crass (1.0.6) sha256=dc516022a56e7b3b156099abc81b6d2b08ea1ed12676ac7a5657617f012bd45d + csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f + cucumber (10.2.0) sha256=fdedbd31ecf40858b60f04853f2aa15c44f5c30bbac29c6a227fa1e7005a8158 + cucumber-ci-environment (11.0.0) sha256=0df79a9e1d0b015b3d9def680f989200d96fef206f4d19ccf86a338c4f71d1e2 + cucumber-core (16.2.0) sha256=592b58a95cf42feef8e5a349f68e363784ba3b6568ffbcf6776e38e136cf970b + cucumber-cucumber-expressions (19.0.0) sha256=33208ff204732ac9bed42b46993a0a243054f71ece08579d57e53df6a1c9d93a + cucumber-gherkin (39.1.0) sha256=aed12a0c955d8563d80a012633c1a72075525f4d64d4cc983001df2181b379ed + cucumber-html-formatter (22.3.0) sha256=f9768ed05588dbd73a5f3824c2cc648bd86b00206e6972d743af8051281d0729 + cucumber-messages (32.3.1) sha256=ddc88e4c1cf7afb96c06005b92a4a6f221a2fa435a8b4ca04677d215fd82771c + cucumber-rails (4.0.1) sha256=bd3513ec47dc06188cc05703648cbc3560fb115f3f5cfb8b616065b4d6e8024d + cucumber-tag-expressions (8.1.0) sha256=9bd8c4b6654f8e5bf2a9c99329b6f32136a75e50cd39d4cfb3927d0fa9f52e21 + database_cleaner (2.1.0) sha256=1dcba26e3b1576da692fc6bac10136a4744da5bcc293d248aae19640c65d89cd + database_cleaner-active_record (2.2.2) sha256=88296b9f3088c31f7c0d4fcec10f68e4b71c96698043916de59b04debec10388 + database_cleaner-core (2.0.1) sha256=8646574c32162e59ed7b5258a97a208d3c44551b854e510994f24683865d846c + date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0 + debug (1.11.1) sha256=2e0b0ac6119f2207a6f8ac7d4a73ca8eb4e440f64da0a3136c30343146e952b6 + descendants_tracker (0.0.4) sha256=e9c41dd4cfbb85829a9301ea7e7c48c2a03b26f09319db230e6479ccdc780897 + diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962 + docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e + dotenv (3.2.0) sha256=e375b83121ea7ca4ce20f214740076129ab8514cd81378161f11c03853fe619d + drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373 + dry-configurable (1.4.0) sha256=e35d1b5f3c081753ef361f564919db79000f32cfa6f20ee3a3ba5921b41b73ce + dry-core (1.2.0) sha256=0cc5a7da88df397f153947eeeae42e876e999c1e30900f3c536fb173854e96a1 + dry-inflector (1.3.1) sha256=7fb0c2bb04f67638f25c52e7ba39ab435d922a3a5c3cd196120f63accb682dcc + dry-initializer (3.2.0) sha256=37d59798f912dc0a1efe14a4db4a9306989007b302dcd5f25d0a2a20c166c4e3 + dry-logic (1.6.0) sha256=da6fedbc0f90fc41f9b0cc7e6f05f5d529d1efaef6c8dcc8e0733f685745cea2 + dry-schema (1.16.0) sha256=cd3aaeabc0f1af66ec82a29096d4c4fb92a0a58b9dae29a22b1bbceb78985727 + dry-types (1.9.1) sha256=baebeecdb9f8395d6c9d227b62011279440943e3ef2468fe8ccc1ba11467f178 + ed25519 (1.4.0) sha256=16e97f5198689a154247169f3453ef4cfd3f7a47481fde0ae33206cdfdcac506 + erb (6.0.4) sha256=38e3803694be357fe2bfe312487c74beaf9fb4e5beb3e22498952fe1645b95d9 + erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9 + et-orbi (1.4.0) sha256=6c7e3c90779821f9e3b324c5e96fda9767f72995d6ae435b96678a4f3e2de8bc + factory_bot (6.6.0) sha256=1fc1b3b5620ec980a6a27aec1b6ec8c250ca82962e970e8a40f93e8d388d4b89 + factory_bot_rails (6.5.1) sha256=d3cc4851eae4dea8a665ec4a4516895045e710554d2b5ac9e68b94d351bc6d68 + ffi (1.17.4-x64-mingw-ucrt) sha256=f6ff9618cfccc494138bddade27aa06c74c6c7bc367a1ea1103d80c2fcb9ed35 + ffi (1.17.4-x86_64-linux-gnu) sha256=9d3db14c2eae074b382fa9c083fe95aec6e0a1451da249eab096c34002bc752d + flay (2.14.4) sha256=a62d96a51d1da185aa41ba95b696966df9f7d1d91a457709277f24515895de77 + flog (4.9.4) sha256=12cc054fab7a2cbd2a906514397c4d7788954d530564782d6f14939dc2dfbcbb + fugit (1.12.1) sha256=5898f478ede9b415f0804e42b8f3fd53f814bd85eebffceebdbc34e1107aaf68 + globalid (1.3.0) sha256=05c639ad6eb4594522a0b07983022f04aa7254626ab69445a0e493aa3786ff11 + i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5 + ice_nine (0.11.2) sha256=5d506a7d2723d5592dc121b9928e4931742730131f22a1a37649df1c1e2e63db + image_processing (1.14.0) sha256=754cc169c9c262980889bec6bfd325ed1dafad34f85242b5a07b60af004742fb + importmap-rails (2.2.3) sha256=7101be2a4dc97cf1558fb8f573a718404c5f6bcfe94f304bf1f39e444feeb16a + io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc + irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3 + jbuilder (2.15.0) sha256=fe36cd45b47dd88cb2cbebc3adc4348f041825f580d503578594e8255817f889 + json (2.19.5) sha256=218a18553e4801d579ca7e0f5bc72bafd776d7397238a1fb4e74db5b0a812c59 + kamal (2.11.0) sha256=1408864425e0dec7e0a14d712a3b13f614e9f3a425b7661d3f9d287a51d7dd75 + language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc + launchy (3.1.1) sha256=72b847b5cc961589dde2c395af0108c86ff0119f42d4648d25b5440ebb10059e + lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87 + logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 + loofah (2.25.1) sha256=d436c73dbd0c1147b16c4a41db097942d217303e1f7728704b37e4df9f6d2e04 + mail (2.9.0) sha256=6fa6673ecd71c60c2d996260f9ee3dd387d4673b8169b502134659ece6d34941 + marcel (1.1.1) sha256=16eb578814fc914a43a48887189568f9eb9a2de8274313050c2b00f0018614f7 + matrix (0.4.3) sha256=a0d5ab7ddcc1973ff690ab361b67f359acbb16958d1dc072b8b956a286564c5b + memoist3 (1.0.0) sha256=686e42402cf150a362050c23143dc57b0ef88f8c344943ff8b7845792b50d56f + mini_magick (5.3.1) sha256=29395dfd76badcabb6403ee5aff6f681e867074f8f28ce08d78661e9e4a351c4 + mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef + minitest (6.0.6) sha256=153ea36d1d987a62942382b61075745042a2b3123b1cd48f4c3675af9cc7d6f1 + msgpack (1.8.0) sha256=e64ce0212000d016809f5048b48eb3a65ffb169db22238fb4b72472fecb2d732 + multi_test (1.1.0) sha256=e9e550cdd863fb72becfe344aefdcd4cbd26ebf307847f4a6c039a4082324d10 + net-imap (0.6.4.1) sha256=29f0360d75a7efd3539f16ac1957dea5c0a51ddeceb348db4553c3120914ea0d + net-pop (0.1.2) sha256=848b4e982013c15b2f0382792268763b748cce91c9e91e36b0f27ed26420dff3 + net-protocol (0.2.2) sha256=aa73e0cba6a125369de9837b8d8ef82a61849360eba0521900e2c3713aa162a8 + net-scp (4.1.0) sha256=a99b0b92a1e5d360b0de4ffbf2dc0c91531502d3d4f56c28b0139a7c093d1a5d + net-sftp (4.0.0) sha256=65bb91c859c2f93b09826757af11b69af931a3a9155050f50d1b06d384526364 + net-smtp (0.5.1) sha256=ed96a0af63c524fceb4b29b0d352195c30d82dd916a42f03c62a3a70e5b70736 + net-ssh (7.3.2) sha256=65029e213c380e20e5fd92ece663934ab0a0fe888e0cd7cc6a5b664074362dd4 + nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1 + nokogiri (1.19.4-x64-mingw-ucrt) sha256=051da97b8eccfdb5444fed40246a35e10d7298b9efe759b4cd25455ea04c587e + nokogiri (1.19.4-x86_64-linux-gnu) sha256=379fae440b28915e3f19d752ce2dcf8465ed2b2fbefd2a7ca0dd497bc981a06a + ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912 + parallel (2.1.0) sha256=b35258865c2e31134c5ecb708beaaf6772adf9d5efae28e93e99260877b09356 + parser (3.3.11.1) sha256=d17ace7aabe3e72c3cc94043714be27cc6f852f104d81aa284c2281aecc65d54 + path_expander (2.0.1) sha256=2de201164bff4719cc4d0b3767286e9977cc832a59c4d70abab571ec86cb41e4 + pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6 + prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193 + prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85 + propshaft (1.3.2) sha256=1d56a3e56a92c21bfc29caf07406b5386b00d4c47ddf357cf989a5a234b1389e + psych (5.3.1) sha256=eb7a57cef10c9d70173ff74e739d843ac3b2c019a003de48447b2963d81b1974 + public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623 + puma (8.0.2) sha256=c8ed871dfbbe66448ea9ffd46692342d9804d4071522b52b5331b7b6e7b686fb + raabro (1.4.0) sha256=d4fa9ff5172391edb92b242eed8be802d1934b1464061ae5e70d80962c5da882 + racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f + rack (3.2.6) sha256=5ed78e1f73b2e25679bec7d45ee2d4483cc4146eb1be0264fc4d94cb5ef212c2 + rack-session (2.1.2) sha256=595434f8c0c3473ae7d7ac56ecda6cc6dfd9d37c0b2b5255330aa1576967ffe8 + rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463 + rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868 + rails (8.1.3) sha256=6d017ba5348c98fc909753a8169b21d44de14d2a0b92d140d1a966834c3c9cd3 + rails-dom-testing (2.3.0) sha256=8acc7953a7b911ca44588bf08737bc16719f431a1cc3091a292bca7317925c1d + rails-html-sanitizer (1.7.0) sha256=28b145cceaf9cc214a9874feaa183c3acba036c9592b19886e0e45efc62b1e89 + railties (8.1.3) sha256=913eb0e0cb520aac687ffd74916bd726d48fa21f47833c6292576ef6a286de22 + rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a + rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701 + rdoc (7.2.0) sha256=8650f76cd4009c3b54955eb5d7e3a075c60a57276766ebf36f9085e8c9f23192 + reek (6.5.0) sha256=d26d3a492773b2bbc228888067a21afe33ac07954a17dbd64cdeae42c4c69be1 + regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb + reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835 + rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142 + rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d + rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836 + rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47 + rspec-rails (8.0.4) sha256=06235692fc0892683d3d34977e081db867434b3a24ae0dd0c6f3516bad4e22df + rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c + rubocop (1.86.2) sha256=bb2e97f635eda42c448f2588f4a6ff78f221b8bdfdf65b1e9b07fbd57521b45d + rubocop-ast (1.49.1) sha256=4412f3ee70f6fe4546cc489548e0f6fcf76cafcfa80fa03af67098ffed755035 + rubocop-performance (1.26.1) sha256=cd19b936ff196df85829d264b522fd4f98b6c89ad271fa52744a8c11b8f71834 + rubocop-rails (2.35.2) sha256=088865be9675922a5c8f13c00055a71ab768ea5eed211437cffd2a8b46b64ac2 + rubocop-rails-omakase (1.1.0) sha256=2af73ac8ee5852de2919abbd2618af9c15c19b512c4cfc1f9a5d3b6ef009109d + ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33 + ruby-vips (2.3.0) sha256=e685ec02c13969912debbd98019e50492e12989282da5f37d05f5471442f5374 + ruby_parser (3.22.0) sha256=1eb4937cd9eb220aa2d194e352a24dba90aef00751e24c8dfffdb14000f15d23 + rubycritic (5.0.0) sha256=7f3877556d7f52bf0980496b7c907a827a9592ba690b80a593403a60bd6cfb42 + rubyzip (3.3.0) sha256=a372fc67892a4f8c0bc8ec906b720353d8e48807a64b2e63adf99b1e3583a034 + securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1 + selenium-webdriver (4.44.0) sha256=6f1df072529af369589c46f0e01132952aabb250cfd683c274d74dc1eb5d8477 + sexp_processor (4.17.5) sha256=ae2b48ba98353d5d465ce8759836b7a05f2e12c5879fcd14d7815b026de32f0e + simplecov (0.22.0) sha256=fe2622c7834ff23b98066bb0a854284b2729a569ac659f82621fc22ef36213a5 + simplecov-html (0.13.2) sha256=bd0b8e54e7c2d7685927e8d6286466359b6f16b18cb0df47b508e8d73c777246 + simplecov_json_formatter (0.1.4) sha256=529418fbe8de1713ac2b2d612aa3daa56d316975d307244399fa4838c601b428 + solid_cable (3.0.12) sha256=a168a54731a455d5627af48d8441ea3b554b8c1f6e6cd6074109de493e6b0460 + solid_cache (1.0.10) sha256=bc05a2fb3ac78a6f43cbb5946679cf9db67dd30d22939ededc385cb93e120d41 + solid_queue (1.4.0) sha256=e6a18d196f0b27cb6e3c77c5b31258b05fb634f8ed64fb1866ed164047216c2a + sqlite3 (2.9.4-x64-mingw-ucrt) sha256=40997c549b19e2fdfcc5e271f6bdd4d502179742c0bfd678da23d0d09b929848 + sqlite3 (2.9.4-x86_64-linux-gnu) sha256=537a3eda71b1df1336d0055cbebe55a7317c34870c192c7b6b9d8d0be6871847 + sshkit (1.25.0) sha256=c8c6543cdb60f91f1d277306d585dd11b6a064cb44eab0972827e4311ff96744 + stimulus-rails (1.3.4) sha256=765676ffa1f33af64ce026d26b48e8ffb2e0b94e0f50e9119e11d6107d67cb06 + stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1 + sys-uname (1.5.1) sha256=784d7e6491b0393c25cbbe5ac38324ac7be9fda083a6094832648af669386d7b + sys-uname (1.5.1-universal-mingw32) sha256=aceb618e3276da5eae0ce368e9f6fae8c1f3e9ef23a0595cb88db7b6ecd45f62 + thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73 + thread_safe (0.3.6) sha256=9ed7072821b51c57e8d6b7011a8e282e25aeea3a4065eab326e43f66f063b05a + thruster (0.1.21) sha256=dc67928f36e5894844579a95e45637a5091db7a7ea05468ee8c2c6eb0a3f77cf + thruster (0.1.21-x86_64-linux) sha256=6e2fbcf826540a72d3710ae4db072c2333287ac2ee57e7e52f35bc10900d74a7 + timeout (0.6.1) sha256=78f57368a7e7bbadec56971f78a3f5ecbcfb59b7fcbb0a3ed6ddc08a5094accb + tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f + tty-which (0.5.0) sha256=5824055f0d6744c97e7c4426544f01d519c40d1806ef2ef47d9854477993f466 + turbo-rails (2.0.23) sha256=ee0d90733aafff056cf51ff11e803d65e43cae258cc55f6492020ec1f9f9315f + tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b + tzinfo-data (1.2026.2) sha256=7db0d3d3d53b8d7601fc183fccc8c6d056a3004e14eb59ea995bf6aec4ae10bc + unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42 + unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f + uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6 + useragent (0.16.11) sha256=700e6413ad4bb954bb63547fa098dddf7b0ebe75b40cc6f93b8d54255b173844 + virtus (2.0.0) sha256=8841dae4eb7fcc097320ba5ea516bf1839e5d056c61ee27138aa4bddd6e3d1c2 + web-console (4.3.0) sha256=e13b71301cdfc2093f155b5aa3a622db80b4672d1f2f713119cc7ec7ac6a6da4 + websocket (1.2.11) sha256=b7e7a74e2410b5e85c25858b26b3322f29161e300935f70a0e0d3c35e0462737 + websocket-driver (0.8.0) sha256=ed0dba4b943c22f17f9a734817e808bc84cdce6a7e22045f5315aa57676d4962 + websocket-extensions (0.1.5) sha256=1c6ba63092cda343eb53fc657110c71c754c56484aad42578495227d717a8241 + win32ole (1.9.3) sha256=01f43dc5dc13806e6e58204f538b4a28f3d85968ea89074abc9a3cd118e94d96 + xpath (3.2.0) sha256=6dfda79d91bb3b949b947ecc5919f042ef2f399b904013eb3ef6d20dd3a4082e + zeitwerk (2.8.1) sha256=1c85e0f28954d68cd16e575da37f26846f609b68d80b5942ccfd31030c2449d5 + +BUNDLED WITH + 4.0.10 diff --git a/README.md b/README.md index 9d7fe1bf53..9246c2573c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,98 @@ # CAMAAR Sistema para avaliação de atividades acadêmicas remotas do CIC + +# 1. Como fazer o login como Administrador? + +Você pode utilizar o e-mail ou a matrícula do administrador: + +- **E-mail:** `admin@unb.br` +- **Matrícula:** `admin_matricula` +- **Senha:** `admin` + +> Nota: em cenários de testes automatizados, a senha pode ser `admin123`. + + +--- + +# 2. Como fazer o login como Docente (Professor)? + +O docente padrão carregado a partir do arquivo `class_members.json` possui os seguintes dados de acesso: + +- **E-mail:** `mholanda@unb.br` +- **Matrícula:** `83807519491` +- **Senha:** `senha123` + + +--- + +# 3. Como fazer o login como Discente (Aluno)? + +Os discentes importados do arquivo `class_members.json` utilizam a própria matrícula como senha padrão. + +Segue um exemplo de conta de aluno disponível: + +## Exemplo 1 (Ana Clara Jordao Perna) + +- **E-mail:** `acjpjvjp@gmail.com` +- **Matrícula:** `190084006` +- **Senha:** `190084006` + +--- + +# 4. Como rodar o projeto? + +Certifique-se de ter o Ruby (versão compatível com o `.ruby-version` ou Gemfile) e o Node.js instalados. + +1. **Instale as dependências:** + ```bash + bundle install + yarn install # Se houver dependências de frontend via yarn + ``` + +2. **Configure o banco de dados:** + ```bash + bin/rails db:create db:migrate db:seed + ``` + *Nota: O comando `db:seed` irá popular o banco de dados com usuários e templates padrões, além de dados provenientes do arquivo `class_members.json`.* + +3. **Inicie o servidor local:** + ```bash + bin/rails server + ``` + +4. **Acesse a aplicação:** + Abra o navegador em `http://localhost:3000`. + +--- + +# 5. Como executar os testes? + +O projeto utiliza **RSpec** para testes de unidade/integração, **Cucumber** para testes de comportamento (BDD) e **RuboCop** para padronização de código. Recomendamos sempre utilizar os binários da pasta `bin/` para garantir que o ambiente correto (Spring/Binstubs) seja carregado. + +1. **Executar a suíte de testes do RSpec:** + ```bash + bin/rspec + ``` + +2. **Executar a suíte de testes do Cucumber:** + ```bash + bin/cucumber + ``` + +3. **Verificar a formatação do código (RuboCop):** + ```bash + bin/rubocop + ``` + *Para tentar corrigir automaticamente as infrações, utilize `bin/rubocop -a` (safe auto-correct) ou `bin/rubocop -A` (all auto-correct).* + +# 6. Como testar a redefinição de senha? + +1. Acesse a página de login (`/login`). +2. Clique no link **"Esqueci minha senha"**. +3. Informe o e‑mail cadastrado do usuário (ex.: admin@unb.br) e envie. +4. O sistema enviará um e‑mail (simulado em `log/development.log`) contendo um token de redefinição. +5. Copie o link do e‑mail e abra no navegador. +6. Defina a nova senha e confirme. +7. Faça login com a nova senha para validar que a troca foi bem‑sucedida. + +> **Dica:** Em ambiente de desenvolvimento, o e‑mail é impresso no log `log/development.log`. Use `tail -f log/development.log` para visualizá‑lo. diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000000..9a5ea7383a --- /dev/null +++ b/Rakefile @@ -0,0 +1,6 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require_relative "config/application" + +Rails.application.load_tasks diff --git a/app/assets/images/.keep b/app/assets/images/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css new file mode 100644 index 0000000000..fe93333c0f --- /dev/null +++ b/app/assets/stylesheets/application.css @@ -0,0 +1,10 @@ +/* + * This is a manifest file that'll be compiled into application.css. + * + * With Propshaft, assets are served efficiently without preprocessing steps. You can still include + * application-wide styles in this file, but keep in mind that CSS precedence will follow the standard + * cascading order, meaning styles declared later in the document or manifest will override earlier ones, + * depending on specificity. + * + * Consider organizing styles into separate files for maintainability. + */ diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb new file mode 100644 index 0000000000..0f2d626c29 --- /dev/null +++ b/app/controllers/admin_controller.rb @@ -0,0 +1,359 @@ +class AdminController < ApplicationController + before_action :require_admin + + # Variável de classe utilizada para sinalizar bloqueio/processamento de atualização concorrente no banco de dados. + @@sigaa_updating = false + + # Exibe o painel inicial do administrador e as estatísticas gerais do sistema. + # + # Parâmetros: + # * +params[:release_lock]+ - Se definido como "true", destrava o processo de atualização concorrente (String). + # + # Retorno: + # * Renderiza a view do dashboard de administração. + # + # Efeitos Colaterais: + # * Pode redefinir a variável @@sigaa_updating para +false+. + def dashboard + if params[:release_lock] == "true" + @@sigaa_updating = false + end + + @stats = { + deps: Departamento.count, + discs: Disciplina.count, + turmas: Turma.count, + mats: Matricula.count, + users: Usuario.count + } + end + + # Carrega dados fictícios ou iniciais para testes da aplicação. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Redirecionamento para a página do dashboard. + # + # Efeitos Colaterais: + # * Popula tabelas no banco de dados usando TestDataLoader e define notificações de sucesso ou erro no +flash+. + def carregar_dados_teste + return if check_sigaa_simulated_errors(admin_dashboard_path) + + begin + TestDataLoader.call + flash[:notice] = "Importação concluída com sucesso" + rescue => e + flash[:alert] = "Erro ao carregar dados de teste: #{e.message}" + end + redirect_to admin_dashboard_path + end + + # Renderiza a página do console de integração com o SIGAA. + # + # Parâmetros: + # * +params[:release_lock]+ - Se definido como "true", libera a trava concorrente (String). + # + # Retorno: + # * Renderiza a página console de importação do SIGAA. + # + # Efeitos Colaterais: + # * Pode definir @@sigaa_updating para +false+. + def import_console + if params[:release_lock] == "true" + @@sigaa_updating = false + end + end + + # Lista todas as turmas vinculadas ao departamento do administrador atual, ou todas as turmas se não houver vínculo específico. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Renderiza a view contendo a listagem das turmas. + # + # Efeitos Colaterais: + # * Define uma mensagem informativa no +flash.now+ caso nenhuma turma seja encontrada. + def turmas + @departamento = current_user.departamento + if @departamento + @turmas = Turma.where(departamento: @departamento) + else + @turmas = Turma.all + end + + if @turmas.empty? + @aviso_mensagem = "Nenhuma turma encontrada para o seu departamento neste semestre. Verifique o status da sincronização com o SIGAA." + end + end + + # Exibe os formulários de avaliação ativos de uma turma específica. + # + # Parâmetros: + # * +params[:id]+ - O ID da turma desejada (Integer/String). + # + # Retorno: + # * Redireciona para a lista de turmas se houver acesso negado, ou renderiza os formulários da turma. + # + # Efeitos Colaterais: + # * Define alertas no +flash+ caso a turma pertença a outro departamento e o administrador tenha perfil restrito por departamento. + def turma_avaliacoes + @turma = Turma.find(params[:id]) + if current_user.departamento && @turma.departamento != current_user.departamento + flash[:alert] = "Acesso negado: Você tem permissão para gerenciar apenas as turmas vinculadas ao seu departamento." + redirect_to admin_turmas_path and return + end + + @formularios = @turma.formularios + end + + # Apresenta métricas e médias de desempenho das turmas vinculadas ao departamento do administrador atual ou de todas as turmas. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Renderiza a view de desempenho semestral. + # + # Efeitos Colaterais: + # * Define a variável de instância +@desempenho_turmas+ mapeada com as estatísticas de média e número de respostas. + def desempenho_semestral + @departamento = current_user.departamento + @turmas = @departamento ? Turma.where(departamento: @departamento) : Turma.all + + @desempenho_turmas = @turmas.map do |turma| + { + turma: turma, + media: turma.calcular_media_desempenho, + respostas_count: turma.respostas_count + } + end + end + + # Simula a importação básica de dados do SIGAA. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Redirecionamento para a página do console de importação. + # + # Efeitos Colaterais: + # * Verifica erros simulados do SIGAA, alterando o +flash+ de alerta, ou adiciona notificação de sucesso se correr normalmente. + def sigaa_import + if @@sigaa_updating + flash[:alert] = "Uma atualização já está em andamento. Aguarde a conclusão." + redirect_to admin_import_console_path and return + end + + return if check_sigaa_simulated_errors(admin_import_console_path) + + # Perform normal import logic + flash[:notice] = "Importação concluída com sucesso" + redirect_to admin_import_console_path + end + + # Simula o fluxo de atualização das matrículas e turmas do SIGAA (podendo liberar travas, processar alto volume ou simular cenários de cancelamento). + # + # Parâmetros: + # * +params[:release_lock]+ - Se definido como "true", destrava o processo (String). + # * +params[:large_volume]+ - Se definido como "true", ativa processamento assíncrono simulado de alto volume (String). + # + # Retorno: + # * Redirecionamento para o console de importação. + # + # Efeitos Colaterais: + # * Altera o banco de dados via simulação de desvinculação (+simulate_sigaa_unenrollment+), define variáveis de trava concorrente, e notifica via +flash+. + def sigaa_update + return handle_release_lock if params[:release_lock] == "true" + return if handle_sigaa_updating + return handle_large_volume if large_volume_request? + return if check_sigaa_simulated_errors(admin_import_console_path) + + simulate_sigaa_unenrollment + flash[:notice] = "Base de dados atualizada com sucesso" + redirect_to admin_import_console_path + ensure + @@sigaa_updating = false unless large_volume_request? + end + + # Libera a trava concorrente de atualização e redireciona com confirmação. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Redirecionamento para o console de importação. + # + # Efeitos Colaterais: + # * Redefine @@sigaa_updating para +false+ e insere mensagem de sucesso no +flash+. + def handle_release_lock + @@sigaa_updating = false + flash[:notice] = "Trava liberada com sucesso." + redirect_to admin_import_console_path + end + + # Simula o processamento em lote de grande volume de dados ativando a trava persistente. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Redirecionamento para o console de importação. + # + # Efeitos Colaterais: + # * Define @@sigaa_updating como +true+ e adiciona mensagem explicativa no +flash+. + def handle_large_volume + @@sigaa_updating = true + flash[:notice] = "Processando atualização de grande volume do SIGAA" + redirect_to admin_import_console_path + end + + # Verifica se a requisição atual solicita processamento de grande volume. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Boolean (+true+ se for grande volume, +false+ caso contrário). + # + # Efeitos Colaterais: + # * Nenhum. + def large_volume_request? + params[:large_volume] == "true" || ENV["SIGAA_LARGE_VOLUME"] == "true" + end + + private + + # Método interno para interceptar e tratar comportamentos de falha simulada (API offline, dados corrompidos, códigos ausentes, semestre vazio). + # + # Parâmetros: + # * +redirect_path+ - Rota para redirecionar em caso de erro simulado (String). + # + # Retorno: + # * Boolean (+true+ se algum erro ocorreu e redirecionou, +false+ caso contrário). + # + # Efeitos Colaterais: + # * Define alertas apropriados no +flash+ se um erro for detectado. + def check_sigaa_simulated_errors(redirect_path) + error_msg = find_sigaa_error + if error_msg + flash[:alert] = error_msg + redirect_to redirect_path + return true + end + false + end + + # Identifica a mensagem de erro específica baseada nas condições e variáveis de simulação configuradas no ambiente. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * String com a mensagem de erro apropriada, ou +nil+ se não houver erros simulados. + # + # Efeitos Colaterais: + # * Nenhum. + def find_sigaa_error + return sigaa_offline_error if sigaa_status_matches?("offline", :sigaa_api_status, "SIGAA_API_STATUS") + return sigaa_corrupted_error if sigaa_data_matches?("corrupted") + return sigaa_missing_codes_error if sigaa_data_matches?("missing_codes") + return sigaa_empty_error if semestre_futuro_or_empty? + nil + end + + # Mensagem de erro de conexão simulada. + def sigaa_offline_error = "Erro de conexão com o SIGAA. Tente novamente mais tarde." + + # Mensagem de erro de dados corrompidos simulada. + def sigaa_corrupted_error = "Erro de compatibilidade de dados. Atualização cancelada." + + # Mensagem de erro de códigos faltantes simulada. + def sigaa_missing_codes_error = "Falha na importação: Códigos de disciplina ausentes" + + # Mensagem de erro de semestre vazio simulada. + def sigaa_empty_error = "Nenhum dado novo encontrado para importação neste período." + + # Compara um valor com as variáveis de requisição ou ambiente de status do SIGAA. + # + # Parâmetros: + # * +val+ - Valor esperado (String). + # * +param_key+ - Chave de parâmetro na requisição (Symbol). + # * +env_key+ - Chave de variável de ambiente (String). + # + # Retorno: + # * Boolean. + def sigaa_status_matches?(val, param_key, env_key) + params[param_key] == val || ENV[env_key] == val + end + + # Compara um valor com o estado dos dados do SIGAA. + # + # Parâmetros: + # * +val+ - Valor esperado (String). + # + # Retorno: + # * Boolean. + def sigaa_data_matches?(val) + params[:sigaa_data] == val || ENV["SIGAA_DATA_STATUS"] == val + end + + # Identifica se o semestre informado é futuro ou se o status dos dados está configurado como vazio. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Boolean. + def semestre_futuro_or_empty? + params[:semestre] == "futuro" || ENV["SIGAA_DATA_STATUS"] == "empty" + end + + # Intercepta a requisição caso já exista uma atualização de dados do SIGAA em progresso. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Boolean (+true+ se já estava em progresso, disparando redirecionamento, +false+ caso contrário). + # + # Efeitos Colaterais: + # * Define alertas no +flash+. + def handle_sigaa_updating + if @@sigaa_updating || ENV["SIGAA_UPDATING_MOCK"] == "true" + flash[:alert] = "Uma atualização já está em andamento. Aguarde a conclusão." + redirect_to admin_import_console_path + return true + end + false + end + + # Simula a desvinculação ("unenrollment") ou inativação de um discente no SIGAA. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * +nil+ + # + # Efeitos Colaterais: + # * Altera a primeira matrícula encontrada do usuário "aluno@teste.com" para trancado ou inativo. + def simulate_sigaa_unenrollment + return unless ENV["SIGAA_DATA_STATUS"] == "missing_aluno" + + student = Usuario.find_by(email: "aluno@teste.com") + return unless student + + matricula = student.matriculas.first + return unless matricula + + if matricula.has_attribute?(:trancado) + matricula.update!(trancado: true) + else + matricula.update!(papel_na_turma: "inativo") + end + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb new file mode 100644 index 0000000000..3f417fc38d --- /dev/null +++ b/app/controllers/application_controller.rb @@ -0,0 +1,89 @@ +class ApplicationController < ActionController::Base + # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has. + allow_browser versions: :modern + + # Changes to the importmap will invalidate the etag for HTML responses + stale_when_importmap_changes + + helper_method :current_user, :logged_in? + + before_action :setup_test_session, if: -> { Rails.env.test? && Thread.current[:test_usuario_id] } + + private + + # Configura a sessão de teste a partir da thread atual. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Integer representando o ID do usuário na sessão ou nil. + # + # Efeitos Colaterais: + # * Define a chave +:usuario_id+ no hash da sessão. + def setup_test_session + session[:usuario_id] = Thread.current[:test_usuario_id] + end + + # Recupera o usuário atualmente autenticado a partir do ID salvo na sessão. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Objeto +Usuario+ se autenticado, ou +nil+ caso contrário. + # + # Efeitos Colaterais: + # * Define a variável de instância +@current_user+. + def current_user + @current_user ||= Usuario.find_by(id: session[:usuario_id]) if session[:usuario_id] + end + + # Verifica se existe um usuário autenticado na sessão. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Boolean (+true+ se logado, +false+ caso contrário). + # + # Efeitos Colaterais: + # * Nenhum. + def logged_in? + current_user.present? + end + + # Filtro/before_action para exigir que o usuário esteja autenticado. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * +nil+ se já estiver logado, ou redirecionamento em caso contrário. + # + # Efeitos Colaterais: + # * Define uma mensagem de alerta no +flash+ e redireciona para a tela de login se não estiver logado. + def require_user + unless logged_in? + flash[:alert] = "Você precisa estar logado para acessar esta página." + redirect_to login_path + end + end + + # Filtro/before_action para exigir que o usuário logado seja um administrador. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * +nil+ se o usuário for administrador, ou redirecionamento em caso contrário. + # + # Efeitos Colaterais: + # * Define uma mensagem de alerta no +flash+ e redireciona para a raiz (+root_path+) se não for administrador. + def require_admin + unless logged_in? && current_user.perfil == "administrador" + flash[:alert] = "Acesso negado. Esta área é restrita para administradores." + redirect_to root_path + end + end +end diff --git a/app/controllers/avaliacoes_controller.rb b/app/controllers/avaliacoes_controller.rb new file mode 100644 index 0000000000..893dc42812 --- /dev/null +++ b/app/controllers/avaliacoes_controller.rb @@ -0,0 +1,266 @@ +class AvaliacoesController < ApplicationController + before_action :require_user + before_action :set_formulario, only: [ :new, :create ] + + # Lista as pesquisas e avaliações pendentes ou respondidas baseadas no perfil do usuário atual. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Renderiza a listagem de avaliações. + # + # Efeitos Colaterais: + # * Define as variáveis +@pesquisas_pendentes+ e +@pesquisas_respondidas+. + def index + case current_user.perfil + when "administrador" + load_admin_pesquisas + when "docente" + load_docente_pesquisas + else + load_discente_pesquisas + end + end + + # Exibe o formulário de avaliação para ser preenchido (new). + # + # Parâmetros: + # * +params[:id]+ - O ID do formulário de avaliação (Integer/String). + # + # Retorno: + # * Renderiza a página para responder à avaliação. + # + # Efeitos Colaterais: + # * Define a variável +@formulario+ via before_action. + def new + # Already set by before_action + end + + # Processa e salva as respostas submetidas pelo usuário para uma determinada avaliação. + # + # Parâmetros: + # * +params[:id]+ - ID do formulário respondido (Integer/String). + # * +params[:respostas]+ - Hash contendo as respostas fornecidas pelo usuário. + # + # Retorno: + # * Redirecionamento para a lista de avaliações ou de volta para a resposta em caso de erros. + # + # Efeitos Colaterais: + # * Cria um registro de +Resposta+ e os itens correspondentes (+RespostaItem+) associados no banco de dados. Define mensagens de erro/sucesso no +flash+. + def create + return if reject_duplicate_submission + return if reject_missing_fields + + ActiveRecord::Base.transaction do + @resposta = Resposta.create!( + formulario: @formulario, + usuario: current_user, + enviado_em: Time.current + ) + create_resposta_items + end + + flash[:notice] = "Sua avaliação foi enviada com sucesso! Muito obrigado por contribuir." + redirect_to avaliacoes_path + rescue => e + flash[:alert] = "Erro ao enviar avaliação: #{e.message}" + redirect_to responder_avaliacao_path(@formulario) + end + + private + + # Intercepta a submissão caso o usuário já tenha respondido anteriormente ao formulário. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Boolean (+true+ se já foi respondido e ocorreu redirecionamento, +false+ caso contrário). + # + # Efeitos Colaterais: + # * Define alerta de duplicidade no +flash+ e redireciona para a lista de avaliações. + def reject_duplicate_submission + return false unless Resposta.exists?(formulario: @formulario, usuario: current_user) + + flash[:alert] = "Você já respondeu a este formulário" + redirect_to avaliacoes_path + true + end + + # Intercepta a submissão caso existam perguntas obrigatórias sem resposta. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Boolean (+true+ se faltarem respostas e ocorreu redirecionamento, +false+ caso contrário). + # + # Efeitos Colaterais: + # * Define mensagem de alerta de campos obrigatórios no +flash+ e redireciona para a tela do formulário. + def reject_missing_fields + return false unless missing_required_fields? + + flash[:alert] = "Preencha todos os campos obrigatórios" + redirect_to responder_avaliacao_path(@formulario) + true + end + + # Carrega pesquisas para o perfil Administrador. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * +nil+ + # + # Efeitos Colaterais: + # * Preenche +@pesquisas_pendentes+ com todos os formulários abertos e +@pesquisas_respondidas+ com coleções vazias. + def load_admin_pesquisas + @pesquisas_pendentes = Formulario.where(status: "aberto") + @pesquisas_respondidas = Formulario.none + end + + # Carrega as pesquisas/avaliações associadas às turmas ministradas pelo docente logado. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * +nil+ + # + # Efeitos Colaterais: + # * Define as variáveis de instância correspondentes chamando +load_pesquisas_for+. + def load_docente_pesquisas + turmas = Turma.where(docente_id: current_user.id).pluck(:id) + load_pesquisas_for(turmas, [ "docente", "docentes" ]) + end + + # Carrega as pesquisas/avaliações associadas às turmas em que o discente logado está matriculado. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * +nil+ + # + # Efeitos Colaterais: + # * Define as variáveis de instância correspondentes chamando +load_pesquisas_for+. + def load_discente_pesquisas + turmas = current_user.matriculas.pluck(:turma_id) + load_pesquisas_for(turmas, [ "discente", "discentes" ]) + end + + # Carrega formulários ativos direcionados ao público-alvo específico para uma lista de turmas. + # + # Parâmetros: + # * +turma_ids+ - Lista de IDs de turmas vinculadas (Array de Integers). + # * +publico_alvo+ - Array de strings contendo as tags de público-alvo esperadas (Array de Strings). + # + # Retorno: + # * +nil+ + # + # Efeitos Colaterais: + # * Preenche as variáveis de instância +@pesquisas_pendentes+ e +@pesquisas_respondidas+. + def load_pesquisas_for(turma_ids, publico_alvo) + now = Time.current + answered_ids = current_user.respostas.pluck(:formulario_id) + + @pesquisas_pendentes = Formulario.where( + turma_id: turma_ids, + status: "aberto", + publico_alvo: publico_alvo + ).where("data_inicio <= ? AND data_limite >= ?", now, now) + + @pesquisas_respondidas = Formulario.where(id: answered_ids) + @pesquisas_pendentes = @pesquisas_pendentes.where.not(id: answered_ids) if answered_ids.any? + end + + # Verifica se alguma das perguntas obrigatórias do template não foi preenchida. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Boolean (+true+ se alguma pergunta obrigatória ficou em branco, +false+ caso contrário). + # + # Efeitos Colaterais: + # * Nenhum. + def missing_required_fields? + @formulario.template.perguntas.each_with_index.any? do |pergunta, idx| + pergunta.obrigatoria? && question_missing_answer?(pergunta, params.dig(:respostas, idx.to_s)) + end + end + + # Determina se uma pergunta específica ficou sem resposta de acordo com seu tipo. + # + # Parâmetros: + # * +pergunta+ - Instância do modelo +QuestaoTemplate+. + # * +resp_param+ - Hash contendo os valores submetidos para a pergunta. + # + # Retorno: + # * Boolean (+true+ se faltar a resposta, +false+ caso contrário). + def question_missing_answer?(pergunta, resp_param) + return true if resp_param.nil? + + if pergunta.tipo == "likert" + resp_param[:nota].blank? + else + resp_param[:texto].blank? + end + end + + # Varre o template de perguntas e chama a persistência para cada item de resposta recebido. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * +nil+ + # + # Efeitos Colaterais: + # * Cria registros na tabela +resposta_itens+. + def create_resposta_items + @formulario.template.perguntas.each_with_index do |pergunta, idx| + resp_param = params.dig(:respostas, idx.to_s) + next if resp_param.nil? || question_missing_answer?(pergunta, resp_param) + + build_resposta_item(pergunta, resp_param) + end + end + + # Cria efetivamente um item de resposta (+RespostaItem+) no banco de dados. + # + # Parâmetros: + # * +pergunta+ - Objeto +QuestaoTemplate+. + # * +resp_param+ - Hash contendo as respostas dadas para a pergunta (+:nota+ e/ou +:texto+). + # + # Retorno: + # * Objeto +RespostaItem+ criado e salvo. + # + # Efeitos Colaterais: + # * Grava um novo registro de +RespostaItem+ associado a +@resposta+. + def build_resposta_item(pergunta, resp_param) + RespostaItem.create!( + resposta: @resposta, + questao_template: pergunta, + valor_numerico: resp_param[:nota], + valor_texto: resp_param[:texto], + valor_opcao: resp_param[:nota].present? ? "Nota #{resp_param[:nota]}" : nil + ) + end + + # Localiza e define o formulário atual a partir dos parâmetros de rota. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Objeto +Formulario+ encontrado. + # + # Efeitos Colaterais: + # * Define a variável de instância +@formulario+. + def set_formulario + @formulario = Formulario.find(params[:id]) + end +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/formularios_controller.rb b/app/controllers/formularios_controller.rb new file mode 100644 index 0000000000..ccc3b53e0f --- /dev/null +++ b/app/controllers/formularios_controller.rb @@ -0,0 +1,316 @@ +class FormulariosController < ApplicationController + before_action :require_admin + + # Renderiza a página para criação/distribuição de novos formulários de avaliação. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Renderiza a view contendo templates e turmas. + # + # Efeitos Colaterais: + # * Inicializa +@templates+, +@turmas+ e um novo objeto +@formulario+. + def new + @templates = Template.all + @turmas = Turma.all + @formulario = Formulario.new + end + + # Processa a criação de um único formulário (para uma turma específica) ou em lote (para múltiplas turmas). + # + # Parâmetros: + # * +params[:formulario]+ - Hash com os parâmetros enviados pelo formulário de criação. + # + # Retorno: + # * Redireciona para o dashboard ou renderiza a tela com erros de validação. + # + # Efeitos Colaterais: + # * Cria um ou mais registros de +Formulario+ no banco de dados. Define mensagens de sucesso/erro no +flash+. + def create + form_params = extract_formulario_params + + if form_params[:turma_id].present? + create_single(form_params) + else + create_batch(form_params) + end + end + + private + + # Extrai os parâmetros necessários para criação do formulário a partir da requisição. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Hash mapeado contendo os atributos tratados do formulário. + def extract_formulario_params + { + publico_alvo: parse_publico_alvo, + template_id: resolve_param(:template_id), + status: resolve_param(:status) || "aberto", + data_inicio: parse_time(resolve_param(:data_inicio)), + data_limite: parse_time(resolve_param(:data_limite)), + turma_id: resolve_param(:turma_id), + turma_ids: params[:turma_ids] + } + end + + # Analisa e sanitiza o parâmetro do público-alvo. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * String contendo "discente", "docente" ou o valor original sanitizado. + def parse_publico_alvo + raw = resolve_param(:publico_alvo).to_s.downcase + return "discente" if raw.include?("discente") + return "docente" if raw.include?("docente") + raw + end + + # Resolve se o parâmetro está localizado no primeiro nível de chaves ou aninhado em +:formulario+. + # + # Parâmetros: + # * +key+ - A chave de parâmetro desejada (Symbol). + # + # Retorno: + # * O valor associado à chave ou +nil+. + def resolve_param(key) + params[key] || params[:formulario]&.[](key) + end + + # Tenta converter uma string de data/tempo para um objeto Time válido. + # + # Parâmetros: + # * +val+ - O valor a ser convertido. + # + # Retorno: + # * Objeto +Time+ ou o valor original em caso de erro/vazio. + def parse_time(val) + return Time.parse(val) if val.is_a?(String) && val.present? + val + rescue StandardError + val + end + + # Cria um único formulário para uma turma específica. + # + # Parâmetros: + # * +form_params+ - Hash de parâmetros higienizados. + # + # Retorno: + # * Redirecionamento ou chamada para tratar o erro de persistência. + # + # Efeitos Colaterais: + # * Salva um novo registro na tabela +formularios+ e define flash. + def create_single(form_params) + @formulario = Formulario.new( + template: Template.find_by(id: form_params[:template_id]), + turma_id: form_params[:turma_id], + criado_por: current_user, + publico_alvo: form_params[:publico_alvo], + status: form_params[:status], + data_inicio: form_params[:data_inicio], + data_limite: form_params[:data_limite] + ) + + if @formulario.save + flash[:notice] = "Formulário criado com sucesso!" + redirect_to admin_dashboard_path + else + handle_single_creation_error + end + end + + # Define erros de validação e re-renderiza o formulário de criação unitária. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Renderiza a view +:new+ com status +:unprocessable_entity+. + # + # Efeitos Colaterais: + # * Recarrega +@templates+ e +@turmas+. Adiciona erro ao +flash.now+. + def handle_single_creation_error + @templates = Template.all + @turmas = Turma.all + flash.now[:alert] = extract_validation_message + render :new, status: :unprocessable_entity + end + + # Extrai a mensagem descritiva de erro de validação. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * String com a mensagem de erro. + def extract_validation_message + formulario_error_message || @formulario.errors.full_messages.first + end + + # Mapeia mensagens customizadas ou detalhadas sobre erros de negócio específicos. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * String de erro amigável ou +nil+. + def formulario_error_message + msgs = @formulario.errors.full_messages + alvo_errs = @formulario.errors[:publico_alvo] + return template_inativo_msg(msgs) if template_inativo_error?(msgs) + return alvo_error_msg(alvo_errs) if alvo_errs.any? + return discentes_msg(msgs) if msgs.any? { |m| m.include?("discentes vinculados") } + return formulario_ativo_msg(msgs) if msgs.any? { |m| m.include?("Já existe um formulário ativo") } + return "Datas de início e limite são obrigatórias" if dates_blank? + nil + end + + # Mensagem explicativa para template inativo. + def template_inativo_msg(_msgs) = "Não é possível publicar formulários usando templates inativos" + # Mensagem explicativa para ausência de discentes vinculados na turma. + def discentes_msg(_msgs) = "Ação inválida: Esta turma ainda não possui discentes vinculados no SIGAA para responderem à avaliação." + # Mensagem explicativa para conflito de formulários ativos simultâneos. + def formulario_ativo_msg(_msgs) = "Atenção: Já existe um formulário ativo para os discentes desta turma. Encerre o atual antes de publicar um novo." + + # Processa e mapeia erros na definição de público alvo do formulário. + # + # Parâmetros: + # * +errs+ - Lista de erros do campo de público alvo (Array de Strings). + # + # Retorno: + # * String de erro amigável ou +nil+. + def alvo_error_msg(errs) + return "Obrigatório: Selecione se o formulário é destinado a docentes ou discentes." if errs.any? { |e| e.include?("Obrigatório") } + return "Público alvo inválido ou muito longo" if errs.any? { |e| e.include?("muito longo") } + nil + end + + # Determina se alguma das datas obrigatórias está ausente. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Boolean. + def dates_blank? + @formulario.errors[:data_inicio].any? || @formulario.errors[:data_limite].any? + end + + # Determina se houve falha de validação gerada pelo uso de template inativo. + # + # Parâmetros: + # * +msgs+ - Lista completa de erros do formulário (Array de Strings). + # + # Retorno: + # * Boolean. + def template_inativo_error?(msgs) + @formulario.errors[:template_id].any? { |e| e.include?("templates inativos") } || + msgs.any? { |m| m.include?("templates inativos") } + end + + # Dispara a criação e distribuição de formulários em lote (batch). + # + # Parâmetros: + # * +form_params+ - Hash de parâmetros de formulário. + # + # Retorno: + # * Redirecionamento de rotas. + # + # Efeitos Colaterais: + # * Cria múltiplos registros na tabela +formularios+. + def create_batch(form_params) + return redirect_no_turmas if form_params[:turma_ids].blank? + + template = Template.find_by(id: form_params[:template_id]) + errors, created_count = run_batch_transaction(template, form_params) + handle_batch_result(errors, created_count, template) + end + + # Redireciona e alerta o usuário caso tente disparar em lote sem selecionar turmas. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Redireciona para a tela de novo formulário. + # + # Efeitos Colaterais: + # * Insere alerta no +flash+. + def redirect_no_turmas + flash[:alert] = "Selecione pelo menos uma turma para distribuir os formulários." + redirect_to new_formulario_path + end + + # Executa a transação para criação em lote de formulários de maneira atômica. + # + # Parâmetros: + # * +template+ - Instância do modelo +Template+. + # * +form_params+ - Hash de parâmetros dos formulários. + # + # Retorno: + # * Array no formato +[errors_array, created_count_integer]+. + # + # Efeitos Colaterais: + # * Realiza rollback completo caso qualquer um dos formulários do lote falhe na validação. + def run_batch_transaction(template, form_params) + errors = [] + created_count = 0 + ActiveRecord::Base.transaction do + form_params[:turma_ids].each do |t_id| + form = build_batch_formulario(template, t_id, form_params) + form.save ? (created_count += 1) : (errors << form.errors.full_messages.first; raise ActiveRecord::Rollback) + end + end + [ errors, created_count ] + end + + # Inicializa uma instância de formulário com os valores customizados ou defaults do lote. + # + # Parâmetros: + # * +template+ - Instância do modelo +Template+. + # * +t_id+ - ID da turma (Integer/String). + # * +form_params+ - Hash de parâmetros originais. + # + # Retorno: + # * Nova instância não persistida de +Formulario+. + def build_batch_formulario(template, t_id, form_params) + Formulario.new( + template: template, + turma_id: t_id, + criado_por: current_user, + publico_alvo: form_params[:publico_alvo] || template&.perfil_alvo || "discente", + status: form_params[:status], + data_inicio: form_params[:data_inicio] || Time.current, + data_limite: form_params[:data_limite] || (Time.current + 7.days) + ) + end + + # Trata e notifica o resultado final da execução do lote de formulários. + # + # Parâmetros: + # * +errors+ - Array contendo strings com mensagens de erros ocorridos. + # * +created_count+ - Quantidade de formulários criados (Integer). + # * +template+ - Instância do modelo +Template+ utilizado. + # + # Retorno: + # * Redirecionamento de rotas. + # + # Efeitos Colaterais: + # * Define mensagens de aviso/sucesso no +flash+. + def handle_batch_result(errors, created_count, template) + if errors.any? + flash[:alert] = "Erro ao disparar formulários: #{errors.join(', ')}" + redirect_to new_formulario_path + else + flash[:notice] = "Formulário '#{template&.titulo}' disparado para #{created_count} turmas com sucesso!" + redirect_to admin_dashboard_path + end + end +end diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb new file mode 100644 index 0000000000..4c56232ec8 --- /dev/null +++ b/app/controllers/passwords_controller.rb @@ -0,0 +1,385 @@ +class PasswordsController < ApplicationController + # No authentication required for these actions + skip_before_action :require_user, raise: false + + # Renderiza a página de configuração inicial da senha para um usuário novo. + # + # Parâmetros: + # * +params[:token]+ - O token de setup enviado por e-mail (String). + # + # Retorno: + # * +nil+ ou renderiza página de erro caso token seja inválido/expirado. + # + # Efeitos Colaterais: + # * Define as variáveis de instância +@token+ e +@user+. + def setup + @token = params[:token] + @user = Usuario.find_by(setup_token: @token) + validate_setup_token! + end + + # Valida se o token de setup é válido, não utilizado e não expirado. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * +nil+ se válido, ou renderização da tela de erro em caso de problemas. + # + # Efeitos Colaterais: + # * Pode definir alerta no +flash.now+ e renderizar a view +:setup_error+. + def validate_setup_token! + return render_token_error("Link inválido") if @user.nil? + return render_token_error("Este link já foi utilizado. Faça login normalmente") if @user.setup_token_used? + render_token_error("Link expirado. Solicite um novo e-mail de cadastro ao administrador") if setup_token_expired? + end + + # Verifica se o link de setup do usuário expirou (limite de 24 horas). + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Boolean (+true+ se expirado, +false+ caso contrário). + # + # Efeitos Colaterais: + # * Nenhum. + def setup_token_expired? + @user.setup_token_sent_at.nil? || @user.setup_token_sent_at < 24.hours.ago + end + + # Renderiza a página de erro com a mensagem de token inválido. + # + # Parâmetros: + # * +msg+ - Mensagem explicativa do erro (String). + # + # Retorno: + # * Renderiza o template +:setup_error+. + # + # Efeitos Colaterais: + # * Adiciona alerta ao +flash.now+. + def render_token_error(msg) + flash.now[:alert] = msg + render :setup_error + end + + # Processa o envio da definição de senha inicial. + # + # Parâmetros: + # * +params[:token]+ - Token de configuração (String). + # * +params[:password]+ - Senha desejada (String). + # * +params[:password_confirmation]+ - Confirmação da senha (String). + # + # Retorno: + # * Redirecionamento ou renderização com erro de validação. + # + # Efeitos Colaterais: + # * Atualiza e salva a senha do usuário no banco de dados e inicia sua sessão. + def setup_update + return if handle_invalid_link unless load_user_by_token(:setup_token) + return if handle_missing_passwords(:setup) + + assign_passwords + + if @user.valid? + finalize_setup + else + handle_invalid_password(:setup) + end + end + + # Renderiza a página de solicitação de recuperação de senha ("Esqueci minha senha"). + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Renderiza a view correspondente. + # + # Efeitos Colaterais: + # * Nenhum. + def forgot + # Render forgot page + end + + # Processa o envio do e-mail para geração do link de redefinição de senha. + # + # Parâmetros: + # * +params[:email]+ - O e-mail do usuário que deseja redefinir a senha (String). + # + # Retorno: + # * Redirecionamento para a tela de login ou renderiza erro caso e-mail seja inválido. + # + # Efeitos Colaterais: + # * Gera e salva token de reset no usuário e envia e-mail com instruções se e-mail estiver cadastrado. + def forgot_send + email = params[:email]&.strip + return render_forgot_blank_email if email.blank? + return render_forgot_invalid_email(email) unless email.include?("@") + + user = Usuario.find_by(email: email) + user&.generate_reset_token! + + flash[:notice] = "Se este e-mail estiver cadastrado, você receberá as instruções em breve" + redirect_to login_path + end + + # Exibe erro caso o campo de e-mail na recuperação esteja em branco. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Renderiza a view +:forgot+ com status +:unprocessable_entity+. + # + # Efeitos Colaterais: + # * Adiciona alerta ao +flash.now+. + def render_forgot_blank_email + flash.now[:alert] = "Preencha o campo de e-mail" + render :forgot, status: :unprocessable_entity + end + + # Exibe erro caso o formato do e-mail informado seja inválido. + # + # Parâmetros: + # * +email+ - E-mail inválido digitado (String). + # + # Retorno: + # * Renderiza a view +:forgot+ com status +:unprocessable_entity+. + # + # Efeitos Colaterais: + # * Adiciona alerta ao +flash.now+. + def render_forgot_invalid_email(email) + flash.now[:alert] = "E-mail inválido" + render :forgot, status: :unprocessable_entity + end + + # Renderiza a tela para o usuário definir uma nova senha após clicar no link do e-mail. + # + # Parâmetros: + # * +params[:token]+ - O token de redefinição de senha (String). + # + # Retorno: + # * +nil+ ou renderiza tela de erro caso o link seja inválido/expirado. + # + # Efeitos Colaterais: + # * Define as variáveis de instância +@token+ e +@user+. + def reset + @token = params[:token] + @user = Usuario.find_by(reset_token: @token) + validate_reset_token! + end + + # Valida se o token de reset é válido, não utilizado e dentro do prazo de expiração. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * +nil+ se válido, ou renderiza tela de erro em caso contrário. + # + # Efeitos Colaterais: + # * Pode definir alerta no +flash.now+ e renderizar a view de erro. + def validate_reset_token! + return render_token_error("Link inválido") if @user.nil? + return render_token_error("Este link já foi utilizado. Solicite uma nova redefinição de senha") if @user.reset_token_used? + render_token_error("Link expirado. Solicite uma nova redefinição de senha") if reset_token_expired? + end + + # Verifica se o link de redefinição de senha do usuário expirou (limite de 24 horas). + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Boolean (+true+ se expirado, +false+ caso contrário). + # + # Efeitos Colaterais: + # * Nenhum. + def reset_token_expired? + @user.reset_token_sent_at.nil? || @user.reset_token_sent_at < 24.hours.ago + end + + # Processa e valida a atualização de redefinição da senha do usuário. + # + # Parâmetros: + # * +params[:token]+ - Token de reset (String). + # * +params[:password]+ - Nova senha (String). + # * +params[:password_confirmation]+ - Confirmação da nova senha (String). + # + # Retorno: + # * Redirecionamento ou renderização com mensagens de erro. + # + # Efeitos Colaterais: + # * Atualiza e salva a senha do usuário no banco de dados. + def reset_update + return if handle_invalid_link unless load_user_by_token(:reset_token) + return if handle_expired_reset_link + return if handle_missing_passwords(:reset) + return if handle_same_password + + assign_passwords + + if @user.valid? + finalize_reset + else + handle_invalid_password(:reset) + end + end + + private + + # Carrega o usuário utilizando o token contido na requisição baseado no campo indicado. + # + # Parâmetros: + # * +field+ - Símbolo indicando se busca por +:setup_token+ ou +:reset_token+ (Symbol). + # + # Retorno: + # * Objeto +Usuario+ encontrado ou +nil+. + # + # Efeitos Colaterais: + # * Preenche as variáveis de instância +@token+ e +@user+. + def load_user_by_token(field) + @token = params[:token] + @user = Usuario.find_by(field => @token) + end + + # Trata o caso em que o link fornecido é inválido. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Sempre retorna +true+. + # + # Efeitos Colaterais: + # * Define alerta de link inválido no +flash.now+ e redireciona para a tela de login. + def handle_invalid_link + flash.now[:alert] = "Link inválido" + redirect_to login_path + true + end + + # Verifica e trata se algum campo de senha ou confirmação de senha foi enviado em branco. + # + # Parâmetros: + # * +action+ - Símbolo representando a view a ser renderizada em caso de erro (Symbol). + # + # Retorno: + # * Boolean (+true+ se algum campo estiver em branco, +false+ caso contrário). + # + # Efeitos Colaterais: + # * Se houver erro, adiciona alerta no +flash.now+ e renderiza a view do +action+ informado. + def handle_missing_passwords(action) + return false unless params[:password].blank? || params[:password_confirmation].blank? + + flash.now[:alert] = "Preencha todos os campos obrigatórios" + render action + true + end + + # Atribui a nova senha e sua confirmação ao usuário, habilitando validação de regras de força da senha. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * +nil+ + # + # Efeitos Colaterais: + # * Altera os atributos +validating_password_rules+, +password+ e +password_confirmation+ do objeto +@user+. + def assign_passwords + @user.validating_password_rules = true + @user.password = params[:password] + @user.password_confirmation = params[:password_confirmation] + end + + # Finaliza o processo de setup de senha marcando o token como utilizado, ativando a conta e logando o usuário. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Redirecionamento para a página de avaliações. + # + # Efeitos Colaterais: + # * Salva o usuário com +setup_token_used = true+ e +ativo = true+, armazena o ID do usuário na sessão e define mensagem de boas-vindas no +flash+. + def finalize_setup + @user.setup_token_used = true + @user.ativo = true + @user.save! + session[:usuario_id] = @user.id + flash[:notice] = "Senha definida com sucesso. Bem-vindo!" + redirect_to avaliacoes_path + end + + # Renderiza a tela de ação informada adicionando o erro de validação da senha. + # + # Parâmetros: + # * +action+ - Símbolo da view a ser renderizada (Symbol). + # + # Retorno: + # * Renderiza a view com status +:unprocessable_entity+. + # + # Efeitos Colaterais: + # * Adiciona o erro de validação de senha ao +flash.now+. + def handle_invalid_password(action) + flash.now[:alert] = @user.errors[:password].first + render action, status: :unprocessable_entity + end + + # Verifica e trata se o link de redefinição de senha está expirado no momento da atualização. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Boolean (+true+ se estiver expirado, +false+ caso contrário). + # + # Efeitos Colaterais: + # * Se expirado, define alerta no +flash.now+ e renderiza a view +:reset+. + def handle_expired_reset_link + if @user.reset_token_sent_at.nil? || @user.reset_token_sent_at < 24.hours.ago + flash.now[:alert] = "Link expirado. Solicite uma nova redefinição de senha" + render :reset, status: :unprocessable_entity + return true + end + false + end + + # Verifica se a nova senha escolhida é idêntica à senha antiga cadastrada. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Boolean (+true+ se for a mesma senha, +false+ caso contrário). + # + # Efeitos Colaterais: + # * Se for igual, define alerta no +flash.now+ e renderiza a view +:reset+. + def handle_same_password + if @user.authenticate(params[:password]) + flash.now[:alert] = "A nova senha não pode ser igual à senha anterior" + render :reset, status: :unprocessable_entity + return true + end + false + end + + # Conclui a redefinição de senha marcando o token de reset como utilizado e salvando as alterações. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Redirecionamento para a página de login. + # + # Efeitos Colaterais: + # * Salva o usuário marcando +reset_token_used = true+ e define mensagem de sucesso no +flash+. + def finalize_reset + @user.reset_token_used = true + @user.save! + flash[:notice] = "Senha redefinida com sucesso. Faça login com sua nova senha" + redirect_to login_path + end +end diff --git a/app/controllers/resultados_controller.rb b/app/controllers/resultados_controller.rb new file mode 100644 index 0000000000..09ecde50cc --- /dev/null +++ b/app/controllers/resultados_controller.rb @@ -0,0 +1,322 @@ +require "csv" + +class ResultadosController < ApplicationController + before_action :require_admin_for_resultados, only: [ :show, :relatorios ] + + # Lista todos os formulários existentes para os quais resultados podem ser avaliados. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Renderiza a view de listagem contendo a coleção de todos os formulários. + # + # Efeitos Colaterais: + # * Define a variável de instância +@formularios+. + def index + @formularios = Formulario.all + end + + # Exibe os resultados agregados e comentários de um formulário de avaliação específico. + # + # Parâmetros: + # * +params[:id]+ - O ID do formulário de avaliação (Integer/String). + # + # Retorno: + # * Renderiza a view com gráficos de métricas e comentários textuais do formulário. + # + # Efeitos Colaterais: + # * Define +@formulario+, +@respostas+, +@mensagem_aviso+, +@perguntas_metricas+ e +@comentarios+. + def show + @formulario = Formulario.find(params[:id]) + @respostas = @formulario.respostas + @mensagem_aviso = "Este formulário ainda não recebeu respostas" if @respostas.empty? + + @perguntas_metricas = [] + @comentarios = [] + + @formulario.template.perguntas.each do |pergunta| + process_pergunta(pergunta) + end + end + + # Exibe a lista de relatórios disponíveis para exportação. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Renderiza a view com todos os formulários para exportação. + # + # Efeitos Colaterais: + # * Define a variável de instância +@formularios+. + def relatorios + @formularios = Formulario.all + end + + # Gera e exporta um arquivo CSV contendo os dados brutos de respostas de uma avaliação. + # + # Parâmetros: + # * +params[:id]+ ou +params[:formulario_id]+ - ID do formulário associado (Integer/String). + # * +params[:turma_id]+ - Filtro opcional para limitar resultados a uma turma específica (Integer/String). + # + # Retorno: + # * Envio de dados de download do arquivo CSV ou redirecionamento em caso de erro/sem dados. + # + # Efeitos Colaterais: + # * Envia uma resposta HTTP do tipo file stream contendo o arquivo CSV compilado. Define mensagens no +flash+ caso bloqueado ou sem dados. + def export_csv_resultado + return if require_admin_export_access + + @formulario = Formulario.find(params[:id] || params[:formulario_id]) + template = @formulario.template + formularios = filter_formularios(template.formularios) + + return if check_empty_responses(formularios) + + csv_data = generate_csv_data(formularios) + send_csv_response(csv_data, template) + end + + private + + # Separa e processa uma pergunta individual do template de acordo com o seu tipo. + # + # Parâmetros: + # * +pergunta+ - Objeto +QuestaoTemplate+. + # + # Retorno: + # * +nil+ + # + # Efeitos Colaterais: + # * Popula os arrays de instância +@perguntas_metricas+ ou +@comentarios+. + def process_pergunta(pergunta) + itens = RespostaItem.where(questao_template: pergunta, resposta: @respostas) + if pergunta.tipo == "likert" + @perguntas_metricas << process_likert_question(pergunta, itens) + else + @comentarios << process_text_question(pergunta, itens) + end + end + + # Calcula e estrutura as estatísticas agregadas para uma pergunta do tipo escala Likert. + # + # Parâmetros: + # * +pergunta+ - Objeto +QuestaoTemplate+. + # * +itens+ - Coleção de objetos +RespostaItem+. + # + # Retorno: + # * Hash formatado contendo +:pergunta+, +:media+, +:distribuicao+ (percentual de votos de 1 a 5) e +:count+ (total de votos). + def process_likert_question(pergunta, itens) + valores = itens.pluck(:valor_numerico).compact + + { + pergunta: pergunta, + media: calcular_media(valores), + distribuicao: calcular_distribuicao(valores), + count: valores.size + } + end + + # Calcula a média aritmética de uma lista de valores numéricos de respostas. + # + # Parâmetros: + # * +valores+ - Array de inteiros. + # + # Retorno: + # * Float arredondado para uma casa decimal. + def calcular_media(valores) + valores.any? ? (valores.sum.to_f / valores.size).round(1) : 0.0 + end + + # Calcula a frequência percentual de cada nota (de 1 a 5) em um conjunto de respostas. + # + # Parâmetros: + # * +valores+ - Array de inteiros representando as notas coletadas. + # + # Retorno: + # * Hash mapeando as notas de 1 a 5 para seus respectivos percentuais (inteiros). + def calcular_distribuicao(valores) + (1..5).to_h do |num| + count = valores.count(num) + percentage = valores.any? ? (count.to_f / valores.size * 100).round : 0 + [ num, percentage ] + end + end + + # Extrai as respostas textuais não em branco para uma pergunta dissertativa. + # + # Parâmetros: + # * +pergunta+ - Objeto +QuestaoTemplate+. + # * +itens+ - Coleção de +RespostaItem+. + # + # Retorno: + # * Hash contendo a pergunta e um array contendo os textos das respostas. + def process_text_question(pergunta, itens) + { + pergunta: pergunta, + respostas: itens.pluck(:valor_texto).reject(&:blank?) + } + end + + # Verifica e impede acesso à exportação caso o usuário logado não seja administrador. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Boolean (+true+ se acesso negado com redirecionamento ativo, +false+ caso contrário). + # + # Efeitos Colaterais: + # * Define flash de alerta e redireciona para a raiz. + def require_admin_export_access + unless logged_in? && current_user.perfil == "administrador" + flash[:alert] = "Acesso negado. Apenas administradores podem gerar este relatório." + redirect_to "/" + return true + end + false + end + + # Impede a geração caso não existam respostas registradas para a coleção de formulários. + # + # Parâmetros: + # * +formularios+ - Coleção de objetos +Formulario+. + # + # Retorno: + # * Boolean (+true+ se não houver respostas suficientes, +false+ caso contrário). + # + # Efeitos Colaterais: + # * Redireciona com alerta no +flash+ caso não existam dados. + def check_empty_responses(formularios) + if formularios.joins(:respostas).count == 0 + flash[:alert] = "Não há dados suficientes para gerar o relatório desta avaliação." + redirect_to admin_relatorios_path + return true + end + false + end + + # Filtra a coleção de formulários aplicando restrição de turma se o parâmetro correspondente estiver presente. + # + # Parâmetros: + # * +formularios+ - Coleção de objetos +Formulario+. + # + # Retorno: + # * Coleção de +Formulario+ filtrada. + def filter_formularios(formularios) + params[:turma_id].present? ? formularios.where(turma_id: params[:turma_id]) : formularios + end + + # Prepara e envia os dados do arquivo CSV como anexo para download. + # + # Parâmetros: + # * +csv_data+ - String contendo o CSV codificado. + # * +template+ - O template associado à avaliação para compor o nome do arquivo. + # + # Retorno: + # * Envia a resposta HTTP de download de arquivo. + # + # Efeitos Colaterais: + # * Dispara o download de arquivo no navegador do usuário e insere confirmação no +flash+. + def send_csv_response(csv_data, template) + safe_name = template.titulo.downcase.gsub(/[^a-z0-9]/, "_").squeeze("_") + filename = "resultados_#{safe_name}.csv" + flash[:notice] = "Relatório gerado com sucesso." + send_data csv_data, filename: filename, type: "text/csv; charset=utf-8" + end + + # Constrói a estrutura e o conteúdo do CSV de resultados compilando dados de turmas, disciplinas e respostas de alunos. + # + # Parâmetros: + # * +formularios+ - Coleção de objetos +Formulario+. + # + # Retorno: + # * String contendo os registros CSV compilados. + def generate_csv_data(formularios) + CSV.generate(headers: true, col_sep: ",", encoding: "UTF-8") do |csv| + csv << [ "Matrícula", "Turma", "Disciplina", "Respostas" ] + + formularios.each do |form| + form.respostas.each do |resp| + next unless valid_turma_response?(resp, params[:turma_id]) + append_resposta_itens_to_csv(csv, form, resp) + end + end + end + end + + # Valida se a resposta do discente pertence à turma informada na filtragem. + # + # Parâmetros: + # * +resp+ - Objeto +Resposta+. + # * +turma_id_param+ - Parâmetro opcional de ID da turma (String/Integer). + # + # Retorno: + # * Boolean. + def valid_turma_response?(resp, turma_id_param) + return true if turma_id_param.blank? + resp.usuario.matriculas.pluck(:turma_id).include?(turma_id_param.to_i) + end + + # Varre os itens de respostas individuais anexando-os como linhas no objeto CSV. + # + # Parâmetros: + # * +csv+ - Instância do gerador de CSV. + # * +form+ - Objeto +Formulario+ avaliado. + # * +resp+ - Objeto +Resposta+. + # + # Retorno: + # * +nil+ + # + # Efeitos Colaterais: + # * Adiciona novas linhas ao objeto acumulador do CSV. + def append_resposta_itens_to_csv(csv, form, resp) + aluno_turma = extract_aluno_turma(form, resp) + matricula = resp.usuario.matricula || "Anônimo" + + resp.resposta_itens.each do |item| + csv << [ matricula, aluno_turma.codigo_turma, aluno_turma.disciplina.nome, extract_item_value(item) ] + end + end + + # Localiza a turma em que o discente realizou a avaliação ou retorna a turma padrão do formulário. + # + # Parâmetros: + # * +form+ - Objeto +Formulario+. + # * +resp+ - Objeto +Resposta+. + # + # Retorno: + # * Instância de +Turma+. + def extract_aluno_turma(form, resp) + resp.usuario.matriculas.find_by(turma_id: form.turma_id)&.turma || form.turma + end + + # Recupera a nota numérica ou o texto correspondente a um item de resposta individual. + # + # Parâmetros: + # * +item+ - Objeto +RespostaItem+. + # + # Retorno: + # * String correspondente ao valor. + def extract_item_value(item) + item.questao_template.tipo == "likert" ? item.valor_numerico.to_s : item.valor_texto + end + + # Filtro/before_action interno para restringir o acesso a resultados de avaliações a administradores. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * +nil+ se autorizado, ou redirecionamento em caso contrário. + # + # Efeitos Colaterais: + # * Redireciona com alerta no +flash+ caso não seja administrador logado. + def require_admin_for_resultados + unless logged_in? && current_user.perfil == "administrador" + flash[:alert] = "Acesso negado: Perfil não autorizado" + redirect_to "/" + end + end +end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb new file mode 100644 index 0000000000..1368d7758e --- /dev/null +++ b/app/controllers/sessions_controller.rb @@ -0,0 +1,213 @@ +class SessionsController < ApplicationController + # Renderiza a página de login se o usuário não estiver autenticado, ou redireciona-o caso contrário. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * +nil+ ou redirecionamento se o usuário já estiver logado. + # + # Efeitos Colaterais: + # * Redireciona o usuário logado para o painel apropriado (admin ou avaliações). + def new + return unless logged_in? + + if current_user.perfil == "administrador" + redirect_to admin_dashboard_path + else + redirect_to avaliacoes_path + end + end + + # Processa a tentativa de login autenticando as credenciais fornecidas. + # + # Parâmetros: + # * +params[:email]+ - E-mail ou matrícula digitados pelo usuário (String). + # * +params[:password]+ - Senha digitada pelo usuário (String). + # + # Retorno: + # * Renderização de template de login em caso de falha, ou redirecionamento em caso de sucesso. + # + # Efeitos Colaterais: + # * Pode definir mensagens de erro no +flash+ ou estabelecer uma sessão de usuário em caso de sucesso. + def create + credencial = params[:email]&.strip + senha = params[:password] + + return render_missing_fields if credencial.blank? || senha.blank? + + authenticate_and_login(credencial, senha) + end + + # Destrói a sessão atual do usuário (logout). + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Redirecionamento para a página de login. + # + # Efeitos Colaterais: + # * Define +session[:usuario_id]+ como +nil+ e gera mensagem de notificação de saída no +flash+. + def destroy + session[:usuario_id] = nil + flash[:notice] = "Você saiu do sistema com sucesso." + redirect_to login_path + end + + private + + # Método auxiliar para encapsular o fluxo de autenticação e validação do estado do usuário. + # + # Parâmetros: + # * +credencial+ - E-mail ou matrícula (String). + # * +senha+ - Senha do usuário (String). + # + # Retorno: + # * Redirecionamento ou renderização dependendo do resultado da autenticação. + # + # Efeitos Colaterais: + # * Cria sessão ou define erros no +flash+. + def authenticate_and_login(credencial, senha) + usuario = find_usuario(credencial) + return handle_invalid_credentials(credencial) unless usuario + return handle_pending_setup if pending_setup?(usuario) + return handle_authentication_failure(credencial) unless usuario.authenticate(senha) + return handle_inactive_user unless usuario.ativo? + + log_in_user(usuario) + end + + # Exibe erro caso campos obrigatórios de login não sejam preenchidos. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Renderiza a view de login com status +:unprocessable_entity+. + # + # Efeitos Colaterais: + # * Adiciona alerta ao +flash.now+. + def render_missing_fields + flash.now[:alert] = "Preencha todos os campos obrigatórios" + render :new, status: :unprocessable_entity + end + + # Busca usuário por e-mail ou matrícula. + # + # Parâmetros: + # * +credencial+ - E-mail ou matrícula (String). + # + # Retorno: + # * Objeto +Usuario+ se encontrado, ou +nil+. + # + # Efeitos Colaterais: + # * Nenhum. + def find_usuario(credencial) + Usuario.find_by(email: credencial) || Usuario.find_by(matricula: credencial) + end + + # Verifica se o cadastro do usuário ainda precisa de configuração de senha inicial. + # + # Parâmetros: + # * +usuario+ - Instância de +Usuario+. + # + # Retorno: + # * Boolean indicando se o cadastro está pendente (+true+) ou concluído (+false+). + # + # Efeitos Colaterais: + # * Nenhum. + def pending_setup?(usuario) + usuario.senha_hash.blank? || (usuario.setup_token.present? && !usuario.setup_token_used?) + end + + # Trata o caso em que o setup inicial do usuário está pendente. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Renderiza a view de login com status +:unprocessable_entity+. + # + # Efeitos Colaterais: + # * Adiciona alerta ao +flash.now+. + def handle_pending_setup + flash.now[:alert] = "Cadastro pendente: Verifique seu e-mail para definir sua senha de acesso." + render :new, status: :unprocessable_entity + end + + # Trata o fluxo caso o usuário/credencial não exista no banco de dados. + # + # Parâmetros: + # * +credencial+ - E-mail ou matrícula testada (String). + # + # Retorno: + # * Renderiza a view de login com status +:unprocessable_entity+. + # + # Efeitos Colaterais: + # * Adiciona alerta de erro específico ao +flash.now+ baseado no tipo de credencial. + def handle_invalid_credentials(credencial) + if credencial.match?(/^\d+$/) + flash.now[:alert] = "Matrícula ou senha inválidos" + elsif !credencial.include?("@") + flash.now[:alert] = "E-mail inválido" + else + flash.now[:alert] = "E-mail ou senha inválidos" + end + render :new, status: :unprocessable_entity + end + + # Trata falha na validação de senha de um usuário existente. + # + # Parâmetros: + # * +credencial+ - E-mail ou matrícula (String). + # + # Retorno: + # * Renderiza a view de login com status +:unprocessable_entity+. + # + # Efeitos Colaterais: + # * Define mensagem genérica de erro no +flash.now+. + def handle_authentication_failure(credencial) + if credencial.match?(/^\d+$/) + flash.now[:alert] = "Matrícula ou senha inválidos" + else + flash.now[:alert] = "E-mail ou senha inválidos" + end + render :new, status: :unprocessable_entity + end + + # Trata a tentativa de autenticação por um usuário que está marcado como inativo. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Renderiza a view de login com status +:unprocessable_entity+. + # + # Efeitos Colaterais: + # * Adiciona mensagem de usuário inativo ao +flash.now+. + def handle_inactive_user + flash.now[:alert] = "Usuário inativo. Entre em contato com o administrador" + render :new, status: :unprocessable_entity + end + + # Efetiva o login salvando o ID do usuário na sessão e redirecionando. + # + # Parâmetros: + # * +usuario+ - Objeto +Usuario+ autenticado. + # + # Retorno: + # * Redirecionamento de rota. + # + # Efeitos Colaterais: + # * Grava +session[:usuario_id]+ e define mensagem de boas-vindas no +flash+. + def log_in_user(usuario) + session[:usuario_id] = usuario.id + flash[:notice] = "Bem-vindo, #{usuario.nome}!" + if usuario.perfil == "administrador" + redirect_to admin_dashboard_path + else + redirect_to avaliacoes_path + end + end +end diff --git a/app/controllers/templates_controller.rb b/app/controllers/templates_controller.rb new file mode 100644 index 0000000000..3e5febf6a8 --- /dev/null +++ b/app/controllers/templates_controller.rb @@ -0,0 +1,243 @@ +class TemplatesController < ApplicationController + before_action :require_admin + + # Lista todos os templates de avaliação cadastrados e inicia uma nova instância vazia. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Renderiza a view de listagem/criação de templates. + # + # Efeitos Colaterais: + # * Define as variáveis de instância +@templates+ e +@template+. + def index + @templates = Template.all + @template = Template.new + end + + # Cria um novo template e suas respectivas perguntas associadas em uma transação atômica. + # + # Parâmetros: + # * +params[:template]+ - Hash contendo o título, descrição, perfil alvo e perguntas enviadas. + # + # Retorno: + # * Redirecionamento para a lista de templates ou re-renderização com status de erro. + # + # Efeitos Colaterais: + # * Cria um registro na tabela +templates+ e múltiplos registros na tabela +questao_templates+ caso válido. Define mensagens no +flash+. + def create + @template = build_template + + return if handle_missing_title + + valid_questions = extract_valid_questions + return if handle_missing_questions(valid_questions) + + @template.skip_questions_validation = true + save_template_and_questions(valid_questions) + rescue => e + handle_save_error("Erro ao criar template: #{e.message}") + end + + # Renderiza a página para edição de dados básicos de um template existente (título e descrição). + # + # Parâmetros: + # * +params[:id]+ - O ID do template desejado (Integer/String). + # + # Retorno: + # * Renderiza a view de edição. + # + # Efeitos Colaterais: + # * Define a variável de instância +@template+. + def edit + @template = Template.find(params[:id]) + end + + # Processa a atualização de dados básicos de um template. + # + # Parâmetros: + # * +params[:id]+ - O ID do template desejado (Integer/String). + # * +params[:template]+ - Hash contendo os dados modificados do template (título, descrição). + # + # Retorno: + # * Redirecionamento para a listagem ou re-renderização em caso de falha de validação. + # + # Efeitos Colaterais: + # * Atualiza e salva o registro do template no banco de dados. Define mensagens de sucesso/erro no +flash+. + def update + @template = Template.find(params[:id]) + if @template.update(template_params) + flash[:notice] = "Template atualizado com sucesso!" + redirect_to templates_path + else + flash.now[:alert] = "Não foi possível atualizar o template." + render :edit, status: :unprocessable_entity + end + end + + # Exclui um template caso este não tenha nenhum formulário de avaliação ativo/vinculado. + # + # Parâmetros: + # * +params[:id]+ - O ID do template desejado (Integer/String). + # + # Retorno: + # * Redirecionamento para a listagem de templates. + # + # Efeitos Colaterais: + # * Exclui o registro do template (e suas perguntas em cascata) do banco de dados se permitido. Define alertas/avisos no +flash+. + def destroy + @template = Template.find(params[:id]) + if @template.formularios.exists? + flash[:alert] = "Não é possível deletar este template pois ele já possui formulários de avaliações vinculados." + else + @template.destroy + flash[:notice] = "Template excluído com sucesso!" + end + redirect_to templates_path + end + + private + + # Salva o template e cria suas respectivas perguntas em uma transação atômica do banco de dados. + # + # Parâmetros: + # * +valid_questions+ - Array contendo os hashes das perguntas validadas. + # + # Retorno: + # * Redirecionamento para a listagem de templates ou chamada para tratamento de erro de salvamento. + # + # Efeitos Colaterais: + # * Commita a transação no banco de dados e define mensagem de sucesso no +flash+. + def save_template_and_questions(valid_questions) + ActiveRecord::Base.transaction do + if @template.save + create_questions(valid_questions) + flash[:notice] = "Template criado com sucesso!" + redirect_to templates_path + else + handle_save_error("Não foi possível salvar o template.") + end + end + end + + # Permite apenas os parâmetros seguros de modificação do template. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Instância de +ActionController::Parameters+ contendo os campos permitidos. + def template_params + params.require(:template).permit(:titulo, :descricao) + end + + # Constrói uma nova instância de template a partir dos parâmetros de requisição. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Instância não persistida de +Template+. + def build_template + p = params[:template] + Template.new( + titulo: p[:titulo], + perfil_alvo: p[:perfil_alvo], + criador: current_user, + descricao: p[:descricao] || "Template para avaliações de #{p[:perfil_alvo]}" + ) + end + + # Intercepta e trata se o template submetido está sem título. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Boolean (+true+ se faltar título com re-renderização ativa, +false+ caso contrário). + # + # Efeitos Colaterais: + # * Carrega a lista completa de +@templates+, insere alerta no +flash.now+ e renderiza a view +:index+. + def handle_missing_title + if params[:template][:titulo].blank? + @templates = Template.all + flash.now[:alert] = "Nome do Template não pode ficar em branco" + render :index, status: :unprocessable_entity + true + else + false + end + end + + # Filtra a lista de perguntas enviadas removendo as que possuem o enunciado/texto em branco. + # + # Parâmetros: + # * Nenhum. + # + # Retorno: + # * Array filtrado contendo apenas perguntas com texto preenchido. + def extract_valid_questions + return [] unless params[:template][:perguntas].present? + params[:template][:perguntas].reject { |p_param| p_param[:texto].blank? } + end + + # Intercepta e trata se nenhuma pergunta válida foi inserida no template. + # + # Parâmetros: + # * +valid_questions+ - Coleção de perguntas analisadas (Array). + # + # Retorno: + # * Boolean (+true+ se não houver perguntas válidas e re-renderização for efetuada, +false+ caso contrário). + # + # Efeitos Colaterais: + # * Recarrega +@templates+, define alerta no +flash.now+ e renderiza a view +:index+. + def handle_missing_questions(valid_questions) + if valid_questions.empty? + @templates = Template.all + flash.now[:alert] = "O template deve ter ao menos uma pergunta" + render :index, status: :unprocessable_entity + true + else + false + end + end + + # Cria e vincula as perguntas válidas ao template recém-criado. + # + # Parâmetros: + # * +valid_questions+ - Coleção de parâmetros das perguntas (Array de Hashes). + # + # Retorno: + # * +nil+ + # + # Efeitos Colaterais: + # * Grava múltiplos registros no banco de dados na tabela +questao_templates+. + def create_questions(valid_questions) + valid_questions.each_with_index do |p_param, index| + QuestaoTemplate.create!( + template: @template, + enunciado: p_param[:texto], + tipo: p_param[:tipo], + obrigatoria: true, + ordem: index + 1 + ) + end + end + + # Trata e notifica falhas ocorridas na transação ou no salvamento do template. + # + # Parâmetros: + # * +message+ - Mensagem descritiva do erro (String). + # + # Retorno: + # * Renderiza a view +:index+ com status +:unprocessable_entity+. + # + # Efeitos Colaterais: + # * Recarrega +@templates+ e define alerta no +flash.now+. + def handle_save_error(message) + @templates = Template.all + flash.now[:alert] = message + render :index, status: :unprocessable_entity + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb new file mode 100644 index 0000000000..de6be7945c --- /dev/null +++ b/app/helpers/application_helper.rb @@ -0,0 +1,2 @@ +module ApplicationHelper +end diff --git a/app/javascript/application.js b/app/javascript/application.js new file mode 100644 index 0000000000..0d7b49404c --- /dev/null +++ b/app/javascript/application.js @@ -0,0 +1,3 @@ +// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails +import "@hotwired/turbo-rails" +import "controllers" diff --git a/app/javascript/controllers/application.js b/app/javascript/controllers/application.js new file mode 100644 index 0000000000..1213e85c7a --- /dev/null +++ b/app/javascript/controllers/application.js @@ -0,0 +1,9 @@ +import { Application } from "@hotwired/stimulus" + +const application = Application.start() + +// Configure Stimulus development experience +application.debug = false +window.Stimulus = application + +export { application } diff --git a/app/javascript/controllers/hello_controller.js b/app/javascript/controllers/hello_controller.js new file mode 100644 index 0000000000..5975c0789d --- /dev/null +++ b/app/javascript/controllers/hello_controller.js @@ -0,0 +1,7 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + connect() { + this.element.textContent = "Hello World!" + } +} diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js new file mode 100644 index 0000000000..1156bf8362 --- /dev/null +++ b/app/javascript/controllers/index.js @@ -0,0 +1,4 @@ +// Import and register all your controllers from the importmap via controllers/**/*_controller +import { application } from "controllers/application" +import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading" +eagerLoadControllersFrom("controllers", application) diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb new file mode 100644 index 0000000000..d394c3d106 --- /dev/null +++ b/app/jobs/application_job.rb @@ -0,0 +1,7 @@ +class ApplicationJob < ActiveJob::Base + # Automatically retry jobs that encountered a deadlock + # retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + # discard_on ActiveJob::DeserializationError +end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb new file mode 100644 index 0000000000..3c34c8148f --- /dev/null +++ b/app/mailers/application_mailer.rb @@ -0,0 +1,4 @@ +class ApplicationMailer < ActionMailer::Base + default from: "from@example.com" + layout "mailer" +end diff --git a/app/models/application_record.rb b/app/models/application_record.rb new file mode 100644 index 0000000000..b63caeb8a5 --- /dev/null +++ b/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + primary_abstract_class +end diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/models/curso.rb b/app/models/curso.rb new file mode 100644 index 0000000000..ed799a1994 --- /dev/null +++ b/app/models/curso.rb @@ -0,0 +1,4 @@ +class Curso < ApplicationRecord + self.table_name = "cursos" + validates :nome, presence: true, uniqueness: true +end diff --git a/app/models/departamento.rb b/app/models/departamento.rb new file mode 100644 index 0000000000..65d7e2144b --- /dev/null +++ b/app/models/departamento.rb @@ -0,0 +1,23 @@ +class Departamento < ApplicationRecord + self.table_name = "departamentos" + + has_many :usuarios, dependent: :destroy + has_many :turmas, dependent: :destroy + + validates :nome, presence: true, uniqueness: true + + # Retorna ou calcula o código abreviado do departamento. + # + # = Parâmetros: + # * Nenhum. + # + # = Retorno: + # * Retorna uma string com a sigla do departamento (ex: "CIC" ou iniciais). + # + # = Efeitos Colaterais: + # * Nenhum. + def codigo + return "CIC" if nome.to_s.upcase.include?("COMPUTAÇÃO") + nome.to_s.split.map { |word| word[0] }.join.upcase[0..4] rescue "DEP" + end +end diff --git a/app/models/disciplina.rb b/app/models/disciplina.rb new file mode 100644 index 0000000000..eb125da79c --- /dev/null +++ b/app/models/disciplina.rb @@ -0,0 +1,22 @@ +class Disciplina < ApplicationRecord + self.table_name = "disciplinas" + + has_many :turmas, dependent: :destroy + + validates :codigo, presence: true, uniqueness: true + validates :nome, presence: true + + # Retorna o departamento associado à disciplina, baseado na primeira turma, ou o primeiro departamento cadastrado. + # + # = Parâmetros: + # * Nenhum. + # + # = Retorno: + # * Retorna um objeto da classe Departamento ou nil. + # + # = Efeitos Colaterais: + # * Realiza consultas no banco de dados. + def departamento + turmas.first&.departamento || Departamento.first + end +end diff --git a/app/models/formulario.rb b/app/models/formulario.rb new file mode 100644 index 0000000000..583d2031b5 --- /dev/null +++ b/app/models/formulario.rb @@ -0,0 +1,150 @@ +class Formulario < ApplicationRecord + self.table_name = "formularios" + + belongs_to :template + belongs_to :turma + belongs_to :criado_por, class_name: "Usuario", foreign_key: "criado_por_id" + has_many :respostas, dependent: :destroy + + validates :publico_alvo, presence: { message: "Obrigatório: Selecione se o formulário é destinado a docentes ou discentes." } + validates :publico_alvo, length: { maximum: 20, message: "Público alvo inválido ou muito longo" } + + validate :data_limite_after_data_inicio + validate :template_must_be_active + validate :turma_has_students_for_discente_form + validate :no_active_form_for_same_public + validates :data_inicio, presence: { message: "Data de início não pode ficar em branco" } + validates :data_limite, presence: { message: "Data limite não pode ficar em branco" } + + private + + # Valida se a data limite é posterior à data de início. + # + # = Parâmetros: + # * Nenhum. + # + # = Retorno: + # * Retorna nil (adiciona erro internamente, se falhar). + # + # = Efeitos Colaterais: + # * Adiciona erros na validação do model. + def data_limite_after_data_inicio + return if data_inicio.blank? || data_limite.blank? + if data_limite <= data_inicio + errors.add(:data_limite, "deve ser posterior à data de início") + end + end + + # Garante que o formulário utilize um template ativo. + # + # = Parâmetros: + # * Nenhum. + # + # = Retorno: + # * Retorna nil. + # + # = Efeitos Colaterais: + # * Adiciona erros na validação do model se o template for inativo. + def template_must_be_active + if template.present? && !template.ativo + errors.add(:template_id, "Não é possível publicar formulários usando templates inativos") + end + end + + # Verifica se a turma possui discentes antes de publicar o formulário. + # + # = Parâmetros: + # * Nenhum. + # + # = Retorno: + # * Retorna nil. + # + # = Efeitos Colaterais: + # * Adiciona erros na validação se a turma não tiver alunos. + def turma_has_students_for_discente_form + if publico_alvo.to_s.downcase.include?("discente") && turma.present? + # Check count of discentes in the class + discentes_count = turma.matriculas.where(papel_na_turma: [ "aluno", "discente" ]).count + if discentes_count == 0 + errors.add(:base, "Ação inválida: Esta turma ainda não possui discentes vinculados no SIGAA para responderem à avaliação.") + end + end + end + + # Evita que mais de um formulário ativo coexista para o mesmo público. + # + # = Parâmetros: + # * Nenhum. + # + # = Retorno: + # * Retorna nil. + # + # = Efeitos Colaterais: + # * Adiciona erros de validação se já existir formulário duplicado. + def no_active_form_for_same_public + return unless check_active_form_conditions + + if has_active_duplicate? + errors.add(:base, "Atenção: Já existe um formulário ativo para os discentes desta turma. Encerre o atual antes de publicar um novo.") + end + end + + # Checa as condições para verificar se o formulário atual tenta ser ativo validamente. + # + # = Parâmetros: + # * Nenhum. + # + # = Retorno: + # * Retorna boolean indicando se o status é "aberto" e se a turma/público existem. + # + # = Efeitos Colaterais: + # * Nenhum. + def check_active_form_conditions + status == "aberto" && turma.present? && publico_alvo.present? + end + + # Avalia se há formulários duplicados ativos na mesma turma. + # + # = Parâmetros: + # * Nenhum. + # + # = Retorno: + # * Retorna boolean (true se houver duplicado, false caso contrário). + # + # = Efeitos Colaterais: + # * Acessa banco de dados implicitamente pela query. + def has_active_duplicate? + target_norm = normalized_publico_alvo + active_forms_in_class.any? do |form| + form.publico_alvo.to_s.downcase.pluralize == target_norm + end + end + + # Busca outros formulários abertos na mesma turma. + # + # = Parâmetros: + # * Nenhum. + # + # = Retorno: + # * Retorna um ActiveRecord::Relation com os formulários. + # + # = Efeitos Colaterais: + # * Realiza consulta ao banco de dados. + def active_forms_in_class + Formulario.where(turma_id: turma_id, status: "aberto").where.not(id: id) + end + + # Normaliza a string de público alvo. + # + # = Parâmetros: + # * Nenhum. + # + # = Retorno: + # * Retorna uma string contendo o público alvo em letras minúsculas e no plural. + # + # = Efeitos Colaterais: + # * Nenhum. + def normalized_publico_alvo + publico_alvo.to_s.downcase.pluralize + end +end diff --git a/app/models/matricula.rb b/app/models/matricula.rb new file mode 100644 index 0000000000..00a4f59038 --- /dev/null +++ b/app/models/matricula.rb @@ -0,0 +1,9 @@ +class Matricula < ApplicationRecord + self.table_name = "matriculas" + + belongs_to :turma + belongs_to :usuario + + validates :papel_na_turma, presence: true + validates :usuario_id, uniqueness: { scope: :turma_id } +end diff --git a/app/models/questao_template.rb b/app/models/questao_template.rb new file mode 100644 index 0000000000..cec61f3909 --- /dev/null +++ b/app/models/questao_template.rb @@ -0,0 +1,41 @@ +class QuestaoTemplate < ApplicationRecord + self.table_name = "questoes_template" + + belongs_to :template + has_many :resposta_itens, class_name: "RespostaItem", foreign_key: :questao_template_id, dependent: :destroy + + # Retorna o enunciado da questão. + # + # = Parâmetros: + # * Nenhum. + # + # = Retorno: + # * Retorna uma string com o texto do enunciado. + # + # = Efeitos Colaterais: + # * Nenhum. + def texto + enunciado + end + + # Support Hash-like access for view compatibility with pregunta["texto"] + # Permite acesso ao objeto como um Hash, retornando atributos específicos. + # + # = Parâmetros: + # * +key+ - A chave ou atributo a ser acessado (ex: "texto" ou "tipo"). + # + # = Retorno: + # * Retorna o valor do atributo correspondente ou chama super. + # + # = Efeitos Colaterais: + # * Nenhum. + def [](key) + if key.to_s == "texto" + enunciado + elsif key.to_s == "tipo" + tipo + else + super + end + end +end diff --git a/app/models/resposta.rb b/app/models/resposta.rb new file mode 100644 index 0000000000..c8c847a3af --- /dev/null +++ b/app/models/resposta.rb @@ -0,0 +1,7 @@ +class Resposta < ApplicationRecord + self.table_name = "respostas" + + belongs_to :formulario + belongs_to :usuario + has_many :resposta_itens, class_name: "RespostaItem", dependent: :destroy +end diff --git a/app/models/resposta_item.rb b/app/models/resposta_item.rb new file mode 100644 index 0000000000..2a836abd9e --- /dev/null +++ b/app/models/resposta_item.rb @@ -0,0 +1,6 @@ +class RespostaItem < ApplicationRecord + self.table_name = "resposta_itens" + + belongs_to :resposta + belongs_to :questao_template, class_name: "QuestaoTemplate", foreign_key: "questao_template_id" +end diff --git a/app/models/template.rb b/app/models/template.rb new file mode 100644 index 0000000000..b2a6dd0247 --- /dev/null +++ b/app/models/template.rb @@ -0,0 +1,31 @@ +class Template < ApplicationRecord + self.table_name = "templates" + + belongs_to :criador, class_name: "Usuario", foreign_key: "criador_id" + has_many :perguntas, class_name: "QuestaoTemplate", dependent: :destroy + has_many :formularios, dependent: :destroy + + attr_accessor :skip_questions_validation + + validates :titulo, presence: { message: "Nome do Template não pode ficar em branco" } + validate :must_have_at_least_one_question + + private + + # Valida se o template tem pelo menos uma pergunta, a menos que seja ignorado. + # + # = Parâmetros: + # * Nenhum. + # + # = Retorno: + # * Nil. + # + # = Efeitos Colaterais: + # * Adiciona erro de validação ao model. + def must_have_at_least_one_question + return if skip_questions_validation + if perguntas.empty? + errors.add(:base, "O template deve ter ao menos uma pergunta") + end + end +end diff --git a/app/models/turma.rb b/app/models/turma.rb new file mode 100644 index 0000000000..73b45894aa --- /dev/null +++ b/app/models/turma.rb @@ -0,0 +1,85 @@ +class Turma < ApplicationRecord + self.table_name = "turmas" + + belongs_to :disciplina + belongs_to :departamento + belongs_to :docente, class_name: "Usuario", foreign_key: "docente_id", optional: true + has_many :matriculas, dependent: :destroy + has_many :usuarios, through: :matriculas + has_many :formularios, dependent: :destroy + + validates :codigo_turma, presence: true, uniqueness: { scope: :semestre } + validates :semestre, presence: true + # Retorna o código da disciplina vinculada à turma. + # + # = Parâmetros: + # * Nenhum. + # + # = Retorno: + # * Retorna uma string com o código da disciplina ou nil. + # + # = Efeitos Colaterais: + # * Nenhum. + def codigo + disciplina&.codigo + end + + # Retorna o código de identificação da turma (nome/apelido). + # + # = Parâmetros: + # * Nenhum. + # + # = Retorno: + # * Retorna string contendo o código da turma. + # + # = Efeitos Colaterais: + # * Nenhum. + def nome + codigo_turma + end + + # Retorna a quantidade total de matrículas nesta turma. + # + # = Parâmetros: + # * Nenhum. + # + # = Retorno: + # * Retorna integer com a contagem. + # + # = Efeitos Colaterais: + # * Realiza consulta no banco de dados. + def matriculas_count + matriculas.count + end + + # Retorna a quantidade total de respostas associadas aos formulários da turma. + # + # = Parâmetros: + # * Nenhum. + # + # = Retorno: + # * Retorna integer com a contagem de respostas. + # + # = Efeitos Colaterais: + # * Realiza consulta via junção no banco de dados. + def respostas_count + formularios.joins(:respostas).count + end + + # Calcula a média de desempenho das avaliações do tipo "likert" desta turma. + # + # = Parâmetros: + # * Nenhum. + # + # = Retorno: + # * Retorna float representando a média (com uma casa decimal) ou 0.0 caso não haja notas. + # + # = Efeitos Colaterais: + # * Múltiplas consultas complexas ao banco de dados utilizando joins. + def calcular_media_desempenho + respostas = Resposta.joins(:formulario).where(formularios: { turma_id: id }) + itens = RespostaItem.where(resposta: respostas).joins(:questao_template).where(questoes_template: { tipo: "likert" }) + valores = itens.pluck(:valor_numerico).compact + valores.any? ? (valores.sum.to_f / valores.size).round(1) : 0.0 + end +end diff --git a/app/models/usuario.rb b/app/models/usuario.rb new file mode 100644 index 0000000000..e927fa8ba1 --- /dev/null +++ b/app/models/usuario.rb @@ -0,0 +1,189 @@ +class Usuario < ApplicationRecord + self.table_name = "usuarios" + + belongs_to :curso, optional: true + belongs_to :departamento, optional: true + has_many :matriculas, dependent: :destroy + has_many :turmas, through: :matriculas + has_many :respostas, dependent: :destroy + + attr_reader :password + attr_accessor :password_confirmation, :validating_password_rules + + validates :email, presence: true + + # Custom password hashing using bcrypt to work with existing 'senha_hash' column + # Autentica o usuário comparando a senha não criptografada. + # + # = Parâmetros: + # * +unencrypted_password+ - A senha em texto puro que o usuário tenta usar. + # + # = Retorno: + # * Retorna um boolean, true se a senha for válida, false caso contrário. + # + # = Efeitos Colaterais: + # * Nenhum. + def authenticate(unencrypted_password) + return false if senha_hash.blank? + BCrypt::Password.new(self.senha_hash) == unencrypted_password + rescue BCrypt::Errors::InvalidHash + false + end + + # Atribui uma nova senha e gera o hash com bcrypt. + # + # = Parâmetros: + # * +unencrypted_password+ - A nova senha em texto puro. + # + # = Retorno: + # * Retorna o próprio hash gerado caso haja senha, ou nil. + # + # = Efeitos Colaterais: + # * Atualiza o atributo `senha_hash` da instância (não salva no banco automaticamente). + def password=(unencrypted_password) + @password = unencrypted_password + self.senha_hash = BCrypt::Password.create(unencrypted_password) if unencrypted_password.present? + end + + validate :password_security_rules, if: :validating_password_rules + + # Verifica se o usuário tem permissão para gerenciar uma determinada turma. + # + # = Parâmetros: + # * +turma+ - Objeto do tipo Turma. + # + # = Retorno: + # * Retorna um boolean indicando se o usuário pertence ao mesmo departamento da turma. + # + # = Efeitos Colaterais: + # * Nenhum. + def pode_gerenciar_turma?(turma) + departamento_id == turma.departamento_id + end + + # Helpers to generate tokens + # Gera um token para a configuração inicial do usuário. + # + # = Parâmetros: + # * Nenhum. + # + # = Retorno: + # * Retorna true caso a operação de salvar seja bem-sucedida. + # + # = Efeitos Colaterais: + # * Altera o banco de dados (salva setup_token, setup_token_sent_at, e setup_token_used na base). + def generate_setup_token! + self.setup_token = SecureRandom.hex(20) + self.setup_token_sent_at = Time.current + self.setup_token_used = false + save! + end + + # Gera um token para o reset de senha do usuário. + # + # = Parâmetros: + # * Nenhum. + # + # = Retorno: + # * Retorna true caso a operação de salvar seja bem-sucedida. + # + # = Efeitos Colaterais: + # * Altera o banco de dados (salva reset_token, reset_token_sent_at, e reset_token_used na base). + def generate_reset_token! + self.reset_token = SecureRandom.hex(20) + self.reset_token_sent_at = Time.current + self.reset_token_used = false + save! + end + + private + + # Aplica as regras de segurança para senhas durante a validação. + # + # = Parâmetros: + # * Nenhum. + # + # = Retorno: + # * Nil, apenas adiciona erros na instância de usuário. + # + # = Efeitos Colaterais: + # * Adiciona mensagens de erro à instância se houver violações das regras. + def password_security_rules + return handle_missing_passwords if password.blank? || password_confirmation.blank? + + check_password_match + check_password_length + check_password_numbers + check_password_lowercase + end + + # Lida com casos de senhas não preenchidas. + # + # = Parâmetros: + # * Nenhum. + # + # = Retorno: + # * Retorna um array de erros ou nil. + # + # = Efeitos Colaterais: + # * Adiciona erro de campo obrigatório na instância. + def handle_missing_passwords + errors.add(:password, "Preencha todos os campos obrigatórios") + end + + # Checa se a senha e a confirmação coincidem. + # + # = Parâmetros: + # * Nenhum. + # + # = Retorno: + # * Retorna um array de erros se falhar, ou nil se passar. + # + # = Efeitos Colaterais: + # * Adiciona erro se as senhas não coincidirem. + def check_password_match + errors.add(:password, "As senhas não coincidem") if password != password_confirmation + end + + # Verifica se o tamanho da senha é adequado. + # + # = Parâmetros: + # * Nenhum. + # + # = Retorno: + # * Retorna um array de erros se falhar, ou nil. + # + # = Efeitos Colaterais: + # * Adiciona erro se a senha for muito curta. + def check_password_length + errors.add(:password, "Senha deve ter no mínimo 6 caracteres") if password.length < 6 + end + + # Verifica a presença de números na senha. + # + # = Parâmetros: + # * Nenhum. + # + # = Retorno: + # * Retorna um array de erros se falhar, ou nil. + # + # = Efeitos Colaterais: + # * Adiciona erro caso a senha não tenha números. + def check_password_numbers + errors.add(:password, "A senha deve conter pelo menos um número") unless password.match(/\d/) + end + + # Verifica se a senha possui apenas minúsculas indevidamente. + # + # = Parâmetros: + # * Nenhum. + # + # = Retorno: + # * Retorna um array de erros se falhar, ou nil. + # + # = Efeitos Colaterais: + # * Adiciona erro caso possua letra maiúscula (verificar o comportamento desejado, está barrando maiúsculas). + def check_password_lowercase + errors.add(:password, "A senha deve conter apenas letras minúsculas") if password.match(/[A-Z]/) + end +end diff --git a/app/services/test_data_loader.rb b/app/services/test_data_loader.rb new file mode 100644 index 0000000000..d422d7b261 --- /dev/null +++ b/app/services/test_data_loader.rb @@ -0,0 +1,137 @@ +class TestDataLoader + def self.call + ActiveRecord::Base.transaction do + destroy_existing_records + admin = ensure_admin_exists + dcc = create_dcc_department + + load_disciplines_and_classes(dcc) + load_class_members(dcc) + end + end + + private + + def self.destroy_existing_records + RespostaItem.destroy_all + Resposta.destroy_all + Formulario.destroy_all + QuestaoTemplate.destroy_all + Template.destroy_all + Matricula.destroy_all + Turma.destroy_all + Usuario.where.not(perfil: "administrador").destroy_all + Disciplina.destroy_all + Curso.destroy_all + Departamento.destroy_all + end + + def self.ensure_admin_exists + Usuario.find_by(perfil: "administrador") || Usuario.create!( + nome: "Administrador CAMAAR", + email: "admin@unb.br", + matricula: "admin_matricula", + perfil: "administrador", + ativo: true, + password: "admin" + ) + end + + def self.create_dcc_department + Departamento.find_or_create_by!(nome: "DEPTO CIÊNCIAS DA COMPUTAÇÃO") + end + + def self.load_disciplines_and_classes(dcc) + classes_file = Rails.root.join("classes.json") + return unless File.exist?(classes_file) + + JSON.parse(File.read(classes_file)).each do |c_item| + process_discipline_and_class(c_item, dcc) + end + end + + def self.process_discipline_and_class(c_item, dcc) + discipline = Disciplina.find_or_create_by!( + codigo: c_item["code"], + nome: c_item["name"] + ) + + Turma.find_or_create_by!( + codigo_turma: "#{c_item["class"]["classCode"]}-#{c_item["code"]}", + semestre: c_item["class"]["semester"], + disciplina: discipline, + departamento: dcc + ) + end + + def self.load_class_members(dcc) + members_file = Rails.root.join("class_members.json") + return unless File.exist?(members_file) + + members_data = JSON.parse(File.read(members_file)) + members_data.each do |m_item| + process_class_member(m_item, dcc) + end + end + + def self.process_class_member(m_item, dcc) + discipline = Disciplina.find_by(codigo: m_item["code"]) + return unless discipline + + turma = find_turma(m_item, discipline) + return unless turma + + create_or_update_docente(m_item["docente"], turma, dcc) if m_item["docente"].present? + create_discentes_and_matriculas(m_item["dicente"], turma, dcc) if m_item["dicente"].present? + end + + def self.find_turma(m_item, discipline) + Turma.find_by( + codigo_turma: "#{m_item["classCode"]}-#{m_item["code"]}", + semestre: m_item["semester"], + disciplina: discipline + ) + end + + def self.create_or_update_docente(doc_data, turma, dcc) + doc_user = Usuario.find_by(email: doc_data["email"]) + unless doc_user + doc_user = Usuario.create!( + nome: doc_data["nome"], + email: doc_data["email"], + matricula: doc_data["usuario"], + perfil: "docente", + ativo: true, + departamento: dcc, + password: "senha123" + ) + end + turma.update!(docente: doc_user) + end + + def self.create_discentes_and_matriculas(dicentes_data, turma, dcc) + dicentes_data.each do |disc_data| + process_discente_data(disc_data, turma, dcc) + end + end + + def self.process_discente_data(disc_data, turma, dcc) + curso = Curso.find_or_create_by!(nome: disc_data["curso"]) if disc_data["curso"].present? + + disc_user = Usuario.find_or_create_by!(email: disc_data["email"]) do |u| + u.nome = disc_data["nome"] + u.matricula = disc_data["matricula"] + u.perfil = "discente" + u.ativo = true + u.curso = curso + u.departamento = dcc + u.password = disc_data["matricula"] || "senha123" + end + + Matricula.find_or_create_by!( + usuario: disc_user, + turma: turma, + papel_na_turma: "aluno" + ) + end +end diff --git a/app/views/admin/dashboard.html.erb b/app/views/admin/dashboard.html.erb new file mode 100644 index 0000000000..4dcf4e69f3 --- /dev/null +++ b/app/views/admin/dashboard.html.erb @@ -0,0 +1,149 @@ + +<% content_for :page_title, "Painel de Gerenciamento" %> + + +
+ +
+
+

Console de Controle Geral

+

Mapeamento de formulários do Colegiado e status de banco de dados do DCC.

+
+ +
+
+
+ + <% if flash[:notice] %> +
+ + + + <%= flash[:notice] %> +
+ <% end %> + + <% if flash[:alert] %> +
+ + + + <%= flash[:alert] %> +
+ <% end %> + + +
+
+ Deps +

<%= @stats[:deps] rescue 0 %>

+
+
+ Disciplinas +

<%= @stats[:discs] rescue 0 %>

+
+
+ Turmas +

<%= @stats[:turmas] rescue 0 %>

+
+
+ Matrículas +

<%= @stats[:mats] rescue 0 %>

+
+
+ Membros +

<%= @stats[:users] rescue 0 %>

+
+
+ + +
+ + + <%= link_to templates_path, class: "group bg-white rounded-3xl p-6 md:p-8 border border-gray-100 hover:border-purple-200 shadow-md hover:shadow-xl transition duration-150 flex flex-col justify-between" do %> +
+
+ + + +
+
+

Gerenciar Templates de Avaliação

+

Desenvolva grades parametrizadas de perguntas discentes / docentes no formato Likert ou de comentários livres para o Colegiado.

+
+
+
+ Acessar Módulo + + + +
+ <% end %> + + + <%= link_to new_formulario_path, class: "group bg-white rounded-3xl p-6 md:p-8 border border-gray-100 hover:border-purple-200 shadow-md hover:shadow-xl transition duration-150 flex flex-col justify-between" do %> +
+
+ + + +
+
+

Enviar / Distribuir Formulários

+

Dispare formulários de resposta parametrizados para turmas e semestres específicos, ativando pesquisas para todos os discentes.

+
+
+
+ Acessar Módulo + + + +
+ <% end %> + + + <%= link_to resultados_path, class: "group bg-white rounded-3xl p-6 md:p-8 border border-gray-100 hover:border-purple-200 shadow-md hover:shadow-xl transition duration-150 flex flex-col justify-between" do %> +
+
+ + + + +
+
+

Análise de Resultados de Avaliações

+

Monitore relatórios estatísticos consolidados por docente e comentários qualitativos anônimos sobre o andamento das matérias.

+
+
+
+ Acessar Módulo + + + +
+ <% end %> + + + <%= link_to admin_import_console_path, class: "group bg-white rounded-3xl p-6 md:p-8 border border-gray-100 hover:border-purple-200 shadow-md hover:shadow-xl transition duration-150 flex flex-col justify-between" do %> +
+
+ + + +
+
+

Importação Conforme SIGAA (XML/JSON)

+

Faça upload de dados agregados do departamento (membros, turmas e matrículas) para atualizar autonomamente o banco de dados.

+
+
+
+ Acessar Módulo + + + +
+ <% end %> + +
+ +
diff --git a/app/views/admin/desempenho_semestral.html.erb b/app/views/admin/desempenho_semestral.html.erb new file mode 100644 index 0000000000..af2bcab390 --- /dev/null +++ b/app/views/admin/desempenho_semestral.html.erb @@ -0,0 +1,45 @@ + +<% content_for :page_title, "Relatório de Desempenho" %> + +
+ +
+ <%= link_to admin_turmas_path, class: "p-2 bg-white hover:bg-gray-100 border border-gray-200 text-gray-700 rounded-xl shadow-sm transition" do %> + + + + <% end %> +
+ Relatórios / Desempenho +

Desempenho de Turmas do DCC

+
+
+ +
+

Métricas Consolidadas (DCC)

+ +
+ + + + + + + + + + + <% @desempenho_turmas.each do |dt| %> + + + + + + + <% end %> + +
TurmaDisciplinaRespostasMédia Geral (Likert 1-5)
<%= dt[:turma].codigo_turma %><%= dt[:turma].disciplina.nome %><%= dt[:respostas_count] %><%= dt[:media] %>
+
+
+ +
diff --git a/app/views/admin/import_console.html.erb b/app/views/admin/import_console.html.erb new file mode 100644 index 0000000000..1a8b7e709d --- /dev/null +++ b/app/views/admin/import_console.html.erb @@ -0,0 +1,54 @@ + +<% content_for :page_title, "Central de Importação SIGRA" %> + +
+ <% if flash[:notice] %> +
+ <%= flash[:notice] %> +
+ <% end %> + <% if flash[:alert] %> +
+ <%= flash[:alert] %> +
+ <% end %> +
+

Console de Importação e Carga de Dados

+

Módulo de sincronização direta utilizando formatos padronizados (JSON ou XML).

+
+ +
+

Sincronização Ativa

+

Utilize os botões abaixo para realizar a carga de dados no sistema a partir dos arquivos estruturados do departamento.

+ +
+
+
+

Dados de Teste do CIC

+

Importação automática a partir de `classes.json` e `class_members.json` fornecidos.

+
+ <%= button_to "Carregar Dados do CIC", carregar_dados_teste_path, method: :post, class: "px-4 py-2 bg-[#702562] hover:bg-[#832d73] text-white text-xs font-bold rounded-xl transition cursor-pointer" %> +
+ +
+
+

Atualizar Dados do CIC

+

Sincroniza os dados atuais sem apagar históricos.

+
+ <% if AdminController.class_variable_get(:@@sigaa_updating) || ENV["SIGAA_UPDATING_MOCK"] == "true" %> + + <% else %> + <%= button_to "Atualizar Base", sigaa_update_path, method: :post, class: "px-4 py-2 bg-[#702562] hover:bg-[#832d73] text-white text-xs font-bold rounded-xl transition cursor-pointer" %> + <% end %> +
+ +
+
+

Sincronização Direta via SIGAA XML

+

Sincronização em lote em tempo real com o ambiente corporativo do SIGAA.

+
+ +
+
+
+
diff --git a/app/views/admin/turma_avaliacoes.html.erb b/app/views/admin/turma_avaliacoes.html.erb new file mode 100644 index 0000000000..cc3753d091 --- /dev/null +++ b/app/views/admin/turma_avaliacoes.html.erb @@ -0,0 +1,46 @@ + +<% content_for :page_title, "Avaliações da Turma" %> + +
+ +
+ <%= link_to admin_turmas_path, class: "p-2 bg-white hover:bg-gray-100 border border-gray-200 text-gray-700 rounded-xl shadow-sm transition" do %> + + + + <% end %> +
+ Turmas / Avaliações +

Avaliações da Turma <%= @turma.codigo_turma %>

+
+
+ +
+

<%= @turma.disciplina.nome %>

+

Docente: <%= @turma.docente.nome rescue "Não atribuído" %> • Semestre Letivo: <%= @turma.semestre %>

+
+ + +
+ <% if @formularios.blank? %> +
+

Nenhum formulário ativo direcionado a esta turma.

+
+ <% else %> + <% @formularios.each do |form| %> +
+
+ <%= form.publico_alvo %> +

<%= form.template.titulo %>

+

Status: <%= form.status %> • Período: <%= form.data_inicio.strftime('%d/%m/%Y') rescue 'N/A' %> a <%= form.data_limite.strftime('%d/%m/%Y') rescue 'N/A' %>

+
+ +
+ <%= link_to "Ver Resultados", resultado_path(form), class: "text-xs font-bold text-[#702562] hover:underline" %> +
+
+ <% end %> + <% end %> +
+ +
diff --git a/app/views/admin/turmas.html.erb b/app/views/admin/turmas.html.erb new file mode 100644 index 0000000000..12624476a3 --- /dev/null +++ b/app/views/admin/turmas.html.erb @@ -0,0 +1,66 @@ + +<% content_for :page_title, "Gerenciamento de Turmas" %> + +
+ +
+
+ <%= link_to admin_dashboard_path, class: "p-2 bg-white hover:bg-gray-100 border border-gray-200 text-gray-700 rounded-xl shadow-sm transition" do %> + + + + <% end %> +
+ Colegiado / Acadêmico +

Turmas Sincronizadas

+
+
+ + <% if @aviso_mensagem.present? %> + + <% else %> + <%= link_to "Gerar Relatório de Desempenho", admin_desempenho_semestral_path, class: "px-4 py-2 bg-[#702562] hover:bg-[#832d73] text-white text-xs font-bold rounded-xl shadow transition" %> + <% end %> +
+ + <% if flash[:alert] %> +
+ + + + <%= flash[:alert] %> +
+ <% end %> + + <% if @aviso_mensagem.present? %> +
+ + + +

<%= @aviso_mensagem %>

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

<%= turma.disciplina.nome %>

+

Turma <%= turma.nome %> • Semestre <%= turma.semestre %>

+

Docente: <%= turma.docente.nome rescue "Não atribuído" %>

+
+
+
+ +
+ <%= link_to "Visualizar Avaliações", admin_turma_avaliacoes_path(turma), class: "text-xs font-bold text-[#702562] hover:underline" %> +
+
+ <% end %> +
+ +
diff --git a/app/views/avaliacoes/index.html.erb b/app/views/avaliacoes/index.html.erb new file mode 100644 index 0000000000..2d8323d6b9 --- /dev/null +++ b/app/views/avaliacoes/index.html.erb @@ -0,0 +1,122 @@ + +<% content_for :page_title, "Pesquisas de Avaliação" %> + +
+ +
+
+

Suas Pesquisas Ativas

+

Selecione uma matéria correspondente para preencher o inquérito de opinião do Colegiado.

+
+
+ + <% if flash[:notice] %> +
+ + + + <%= flash[:notice] %> +
+ <% end %> + + <% if flash[:alert] %> +
+ + + + <%= flash[:alert] %> +
+ <% end %> + + +
+ + <% if @pesquisas_pendentes.nil? || @pesquisas_pendentes.blank? %> +
+
+ + + +
+

Você não possui formulários pendentes para responder no momento

+

O Colegiado do CIC agradece sua primordial participação.

+
+ <% else %> + <% @pesquisas_pendentes.each do |formulario| %> +
+
+
+
+ + <%= formulario.turma.codigo rescue "N/C" %> + +

+ <%= formulario.turma.disciplina.nome rescue "N/C" %> +

+

+ Prof: <%= formulario.turma.docente.nome rescue "Não atribuído" %> +

+
+
+ + +
+ Formulário: <%= formulario.template.titulo %> +
+
+ + +
+ Status: Pendente + <%= link_to responder_avaliacao_path(formulario), class: "px-4 py-2 bg-[#702562] hover:bg-[#832d73] text-white font-extrabold text-[10px] uppercase tracking-wider rounded-xl transition cursor-pointer" do %> + Preencher + <% end %> +
+
+ <% end %> + <% end %> + +
+ + <% if current_user.perfil != 'administrador' && @pesquisas_respondidas.present? && @pesquisas_respondidas.any? %> +
+

Pesquisas Já Respondidas

+
+ <% @pesquisas_respondidas.each do |formulario| %> +
+
+
+
+ + <%= formulario.turma.codigo rescue "N/C" %> + +

+ <%= formulario.turma.disciplina.nome rescue "N/C" %> +

+

+ Prof: <%= formulario.turma.docente.nome rescue "Não atribuído" %> +

+
+
+ + + +
+
+ + +
+ Formulário: <%= formulario.template.titulo %> +
+
+ +
+ Status: Respondido +
+
+ <% end %> +
+
+ <% end %> + +
diff --git a/app/views/avaliacoes/new.html.erb b/app/views/avaliacoes/new.html.erb new file mode 100644 index 0000000000..041ba1c2ea --- /dev/null +++ b/app/views/avaliacoes/new.html.erb @@ -0,0 +1,101 @@ + +<% content_for :page_title, "Preencher Avaliação" %> + +
+ + +
+ <%= link_to avaliacoes_path, class: "p-2 bg-white hover:bg-gray-100 border border-gray-200 text-gray-700 rounded-xl shadow-sm transition" do %> + + + + <% end %> +
+ Questionário / Auto-Opinião +

Avaliação De Desempenho Escolar

+
+
+ + <% if flash[:alert] %> +
+ + + + <%= flash[:alert] %> +
+ <% end %> + + +
+
+ <%= @formulario.turma.codigo rescue "N/C" %> +

<%= @formulario.turma.disciplina.nome rescue "N/C" %>

+

Semestre de Referência: 2026/1 • Docente Responsável: <%= @formulario.turma.docente.nome rescue "Não atribuído" %>

+
+ +
+ + + + + Formulário de Resposta 100% Anônimo +
+
+ + + <%= form_with url: responder_avaliacao_path(@formulario), local: true, method: :post, class: "space-y-6" do |f| %> + + + <% @formulario.template.perguntas.each_with_index do |pergunta, idx| %> +
+ +
+ Questão <%= idx + 1 %> +

<%= pergunta["texto"] %>

+
+ + <% if pergunta["tipo"] == 'likert' %> + +
+ Sua Classificação de Desempenho + +
+ <% (1..5).each do |num| %> + + <% end %> +
+
+ + <% else %> + +
+ + +
+ <% end %> + +
+ <% end %> + + +
+
+
+ Responda todas para liberar +
+ + <%= f.submit "Submeter Avaliação", class: "px-6 py-2.5 bg-[#702562] hover:bg-[#832d73] active:scale-95 text-white text-xs font-bold uppercase tracking-widest rounded-full shadow-lg transition duration-200 cursor-pointer" %> +
+ + <% end %> + +
diff --git a/app/views/formularios/new.html.erb b/app/views/formularios/new.html.erb new file mode 100644 index 0000000000..a01c02d5d2 --- /dev/null +++ b/app/views/formularios/new.html.erb @@ -0,0 +1,134 @@ + +<% content_for :page_title, "Distribuir Formulários de Pesquisa" %> + +
+ + +
+ <%= link_to admin_dashboard_path, class: "p-2 bg-white hover:bg-gray-100 border border-gray-200 text-gray-700 rounded-xl shadow-sm transition" do %> + + + + <% end %> +
+ Administração / Distribuidor +

Ativação de Pesquisas

+
+
+ + <% if flash[:alert] %> +
+ + + + <%= flash[:alert] %> +
+ <% end %> + +
+ <%= form_with url: formularios_path, local: true, method: :post, class: "space-y-6" do |f| %> + +
+ + +
+ + <%= f.select :template_id, @templates.map { |t| [t.titulo, t.id] }, {}, id: "template_id", class: "w-full px-3 py-2 bg-gray-50 border border-gray-200 rounded-xl text-xs focus:outline-none focus:border-[#702562] text-gray-700 font-semibold" %> +
+ + +
+ + <%= f.select :turma_id, @turmas.map { |t| ["#{t.codigo} - Turma #{t.nome}", t.id] }, { include_blank: "Selecione uma turma (ou use a tabela abaixo)" }, id: "turma_id", class: "w-full px-3 py-2 bg-gray-50 border border-gray-200 rounded-xl text-xs focus:outline-none focus:border-[#702562] text-gray-700" %> +
+ + +
+ + <%= f.select :publico_alvo, [['Discentes', 'discente'], ['Docentes', 'docente']], { include_blank: "Selecione..." }, id: "publico_alvo", class: "w-full px-3 py-2 bg-gray-50 border border-gray-200 rounded-xl text-xs focus:outline-none focus:border-[#702562] text-gray-700" %> +
+ + +
+ + <%= f.select :status, [['Aberto', 'aberto'], ['Fechado', 'fechado']], {}, id: "status", class: "w-full px-3 py-2 bg-gray-50 border border-gray-200 rounded-xl text-xs focus:outline-none focus:border-[#702562] text-gray-700" %> +
+ + +
+ + <%= f.text_field :data_inicio, id: "data_inicio", placeholder: "YYYY-MM-DD", class: "w-full px-3 py-2 bg-gray-50 border border-gray-200 rounded-xl text-xs focus:outline-none focus:border-[#702562] text-gray-700" %> +
+ + +
+ + <%= f.text_field :data_limite, id: "data_limite", placeholder: "YYYY-MM-DD", class: "w-full px-3 py-2 bg-gray-50 border border-gray-200 rounded-xl text-xs focus:outline-none focus:border-[#702562] text-gray-700" %> +
+ +
+ + +
+
+

Selecione as Turmas Ativas (Para envio em Lote)

+
+ + +
+
+ +
+ + + + + + + + + + + + <% @turmas.each do |turma| %> + + + + + + + + <% end %> + +
Selec.Código / TurmaDisciplinaDocente ResponsávelInscritos
+ + + <%= turma.codigo %> - Turma <%= turma.nome %> + + <%= turma.disciplina.nome rescue "N/C" %> + + <%= turma.docente.nome rescue "Não atribuído" %> + + <%= turma.matriculas_count rescue 0 %> +
+
+
+ + +
+ <%= f.submit "Criar Formulário", class: "px-6 py-2.5 bg-[#702562] hover:bg-[#832d73] text-white font-bold text-xs uppercase tracking-widest rounded-xl shadow transition cursor-pointer" %> + <%= f.submit "Salvar e Publicar", class: "px-6 py-2.5 bg-blue-600 hover:bg-blue-700 text-white font-bold text-xs uppercase tracking-widest rounded-xl shadow transition cursor-pointer" %> + <%= f.submit "Disparar Questionários", class: "px-6 py-2.5 bg-green-600 hover:bg-green-700 text-white font-bold text-xs uppercase tracking-widest rounded-xl shadow transition cursor-pointer" %> +
+ + <% end %> +
+ +
+ + diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb new file mode 100644 index 0000000000..60ad7974ce --- /dev/null +++ b/app/views/layouts/application.html.erb @@ -0,0 +1,54 @@ + + + + CAMAAR - Sistema de Avaliação do Colegiado (CIC/UnB) + + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + + + + + + + + + + + <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> + <%= javascript_importmap_tags %> + + + + + + <% if current_user.present? %> + + <%= render "shared/header" %> + +
+ + <%= render "shared/sidebar" %> + + +
+
+ <%= yield %> +
+
+
+ <% else %> + +
+ <%= yield %> +
+ <% end %> + + diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb new file mode 100644 index 0000000000..3aac9002ed --- /dev/null +++ b/app/views/layouts/mailer.html.erb @@ -0,0 +1,13 @@ + + + + + + + + + <%= yield %> + + diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb new file mode 100644 index 0000000000..37f0bddbd7 --- /dev/null +++ b/app/views/layouts/mailer.text.erb @@ -0,0 +1 @@ +<%= yield %> diff --git a/app/views/passwords/forgot.html.erb b/app/views/passwords/forgot.html.erb new file mode 100644 index 0000000000..ad38f2dff0 --- /dev/null +++ b/app/views/passwords/forgot.html.erb @@ -0,0 +1,26 @@ + +
+
+

Recuperar Senha

+

Insira seu e-mail cadastrado para receber instruções de redefinição.

+
+ + <% if flash[:alert] %> +
+ <%= flash[:alert] %> +
+ <% end %> + + <%= form_with url: forgot_password_path, local: true, class: "space-y-4" do |f| %> +
+ + <%= f.text_field :email, id: "email", placeholder: "usuario@unb.br", class: "w-full px-3 py-2 bg-gray-50 border border-gray-200 rounded-xl text-xs focus:outline-none focus:border-[#702562]" %> +
+ + <%= f.submit "Enviar e-mail de redefinição", class: "w-full py-3 bg-[#702562] hover:bg-[#832d73] text-white font-bold text-xs uppercase tracking-widest rounded-xl transition cursor-pointer" %> + <% end %> + +
+ <%= link_to "Voltar para o Login", login_path, class: "text-xs font-bold text-[#702562] hover:underline" %> +
+
diff --git a/app/views/passwords/reset.html.erb b/app/views/passwords/reset.html.erb new file mode 100644 index 0000000000..99215e9b64 --- /dev/null +++ b/app/views/passwords/reset.html.erb @@ -0,0 +1,27 @@ + +
+
+

Redefinir Senha

+

Escolha uma nova senha forte para acessar sua conta.

+
+ + <% if flash[:alert] %> +
+ <%= flash[:alert] %> +
+ <% end %> + + <%= form_with url: reset_password_path(token: @token), local: true, class: "space-y-4" do |f| %> +
+ + <%= f.password_field :password, id: "password", placeholder: "••••••••", class: "w-full px-3 py-2 bg-gray-50 border border-gray-200 rounded-xl text-xs focus:outline-none focus:border-[#702562]" %> +
+ +
+ + <%= f.password_field :password_confirmation, id: "password_confirmation", placeholder: "••••••••", class: "w-full px-3 py-2 bg-gray-50 border border-gray-200 rounded-xl text-xs focus:outline-none focus:border-[#702562]" %> +
+ + <%= f.submit "Redefinir senha", class: "w-full py-3 bg-[#702562] hover:bg-[#832d73] text-white font-bold text-xs uppercase tracking-widest rounded-xl transition cursor-pointer" %> + <% end %> +
diff --git a/app/views/passwords/setup.html.erb b/app/views/passwords/setup.html.erb new file mode 100644 index 0000000000..28cf42c4df --- /dev/null +++ b/app/views/passwords/setup.html.erb @@ -0,0 +1,27 @@ + +
+
+

Definir Nova Senha

+

Insira sua senha de acesso para ativar seu cadastro.

+
+ + <% if flash[:alert] %> +
+ <%= flash[:alert] %> +
+ <% end %> + + <%= form_with url: setup_password_path(token: @token), local: true, class: "space-y-4" do |f| %> +
+ + <%= f.password_field :password, id: "password", placeholder: "••••••••", class: "w-full px-3 py-2 bg-gray-50 border border-gray-200 rounded-xl text-xs focus:outline-none focus:border-[#702562]" %> +
+ +
+ + <%= f.password_field :password_confirmation, id: "password_confirmation", placeholder: "••••••••", class: "w-full px-3 py-2 bg-gray-50 border border-gray-200 rounded-xl text-xs focus:outline-none focus:border-[#702562]" %> +
+ + <%= f.submit "Definir senha", class: "w-full py-3 bg-[#702562] hover:bg-[#832d73] text-white font-bold text-xs uppercase tracking-widest rounded-xl transition cursor-pointer" %> + <% end %> +
diff --git a/app/views/passwords/setup_error.html.erb b/app/views/passwords/setup_error.html.erb new file mode 100644 index 0000000000..70bf0eff31 --- /dev/null +++ b/app/views/passwords/setup_error.html.erb @@ -0,0 +1,20 @@ + +
+
+ + + +
+ +

Erro de Acesso

+ +

<%= flash[:alert] %>

+ + <% if flash[:alert]&.include?("Link expirado") %> +
+ +
+ <% end %> + + <%= link_to "Voltar para o Login", login_path, class: "block text-xs font-bold text-[#702562] hover:underline" %> +
diff --git a/app/views/pwa/manifest.json.erb b/app/views/pwa/manifest.json.erb new file mode 100644 index 0000000000..fca522bbe4 --- /dev/null +++ b/app/views/pwa/manifest.json.erb @@ -0,0 +1,22 @@ +{ + "name": "Camaar", + "icons": [ + { + "src": "/icon.png", + "type": "image/png", + "sizes": "512x512" + }, + { + "src": "/icon.png", + "type": "image/png", + "sizes": "512x512", + "purpose": "maskable" + } + ], + "start_url": "/", + "display": "standalone", + "scope": "/", + "description": "Camaar.", + "theme_color": "red", + "background_color": "red" +} diff --git a/app/views/pwa/service-worker.js b/app/views/pwa/service-worker.js new file mode 100644 index 0000000000..b3a13fb7bb --- /dev/null +++ b/app/views/pwa/service-worker.js @@ -0,0 +1,26 @@ +// Add a service worker for processing Web Push notifications: +// +// self.addEventListener("push", async (event) => { +// const { title, options } = await event.data.json() +// event.waitUntil(self.registration.showNotification(title, options)) +// }) +// +// self.addEventListener("notificationclick", function(event) { +// event.notification.close() +// event.waitUntil( +// clients.matchAll({ type: "window" }).then((clientList) => { +// for (let i = 0; i < clientList.length; i++) { +// let client = clientList[i] +// let clientPath = (new URL(client.url)).pathname +// +// if (clientPath == event.notification.data.path && "focus" in client) { +// return client.focus() +// } +// } +// +// if (clients.openWindow) { +// return clients.openWindow(event.notification.data.path) +// } +// }) +// ) +// }) diff --git a/app/views/resultados/index.html.erb b/app/views/resultados/index.html.erb new file mode 100644 index 0000000000..945a5d04ae --- /dev/null +++ b/app/views/resultados/index.html.erb @@ -0,0 +1,70 @@ + +<% content_for :page_title, "Resultados Analíticos" %> + +
+ +
+
+ <%= link_to admin_dashboard_path, class: "p-2 bg-white hover:bg-gray-100 border border-gray-200 text-gray-700 rounded-xl shadow-sm transition" do %> + + + + <% end %> +
+ Colegiado / Análise +

Avaliações consolidadas do Semestre

+
+
+
+ +
+ + <% if @formularios.blank? %> +
+

Nenhum formulário cadastrado no sistema.

+
+ <% else %> + <% @formularios.each do |formulario| %> +
+
+
+ <%= formulario.turma.codigo %> +

<%= formulario.template.titulo %>

+

Turma <%= formulario.turma.nome %> • <%= formulario.turma.docente.nome rescue "Não atribuído" %>

+
+ + +
+
+ Respostas Obtidas + <%= formulario.respostas.count %> / <%= formulario.turma.matriculas_count %> +
+
+ <% percentage = (formulario.turma.matriculas_count > 0 ? (formulario.respostas.count.to_f / formulario.turma.matriculas_count * 100).round : 0) %> +
+
+
+
+ +
+ <%= link_to export_csv_resultado_path(formulario), class: "inline-flex items-center gap-1 px-3 py-1.5 bg-[#1cb05e] hover:bg-[#15944d] text-white font-bold text-[10px] uppercase rounded-lg shadow-sm whitespace-nowrap cursor-pointer" do %> + + + + Baixar CSV + <% end %> + + <%= link_to resultado_path(formulario), class: "text-[10px] uppercase font-bold text-gray-400 hover:text-[#702562] transition flex items-center gap-1 whitespace-nowrap cursor-pointer" do %> + Ver Relatório + + + + <% end %> +
+
+ <% end %> + <% end %> + +
+ +
diff --git a/app/views/resultados/relatorios.html.erb b/app/views/resultados/relatorios.html.erb new file mode 100644 index 0000000000..14fac86337 --- /dev/null +++ b/app/views/resultados/relatorios.html.erb @@ -0,0 +1,71 @@ + +<% content_for :page_title, "Relatórios de Avaliação" %> + +
+ +
+ <%= link_to root_path, class: "p-2 bg-white hover:bg-gray-100 border border-gray-200 text-gray-700 rounded-xl shadow-sm transition" do %> + + + + <% end %> +
+ Colegiado / Exportador +

Relatórios de Avaliação

+
+
+ + <% if flash[:alert] %> +
+ + + + <%= flash[:alert] %> +
+ <% end %> + + <% if flash[:notice] %> +
+ + + + <%= flash[:notice] %> +
+ <% end %> + + +
+ <% if @formularios.blank? %> +
+

Nenhum formulário ativo encontrado.

+
+ <% else %> + <% @formularios.uniq { |f| f.template_id }.each do |formulario| %> +
+
+ TEMPLATE +

<%= formulario.template.titulo %>

+

<%= formulario.template.descricao %>

+
+ + <%= form_with url: admin_relatorios_csv_path, method: :get, local: true, class: "space-y-4" do |f| %> + <%= hidden_field_tag :id, formulario.id %> + +
+ + +
+ + <%= f.submit "Exportar Resultados (CSV)", class: "w-full py-2.5 bg-[#702562] hover:bg-[#832d73] text-white font-bold text-xs uppercase tracking-widest rounded-xl shadow transition cursor-pointer" %> + <% end %> +
+ <% end %> + <% end %> +
+ +
diff --git a/app/views/resultados/show.html.erb b/app/views/resultados/show.html.erb new file mode 100644 index 0000000000..6da71ba5d6 --- /dev/null +++ b/app/views/resultados/show.html.erb @@ -0,0 +1,111 @@ + +<% content_for :page_title, "Análise Comercial de Classe" %> + +
+ + +
+ <%= link_to resultados_path, class: "p-2 bg-white hover:bg-gray-100 border border-gray-200 text-gray-700 rounded-xl shadow-sm transition" do %> + + + + <% end %> +
+ Resultados / Relatório Individual +

Turma <%= @formulario.turma.nome %> - <%= @formulario.turma.disciplina.nome rescue "N/C" %>

+
+
+ + +
+
+ <%= @formulario.turma.codigo %> +

<%= @formulario.turma.disciplina.nome rescue "N/A" %>

+

+ Docente: <%= @formulario.turma.docente.nome rescue "Não atribuído" %> • Departamento: <%= @formulario.turma.disciplina.departamento.codigo rescue "DCC" %> +

+
+ +
+

Média Geral Consolidada

+ <% + all_medias = @perguntas_metricas.map { |pm| pm[:media] } + media_geral = all_medias.any? ? (all_medias.sum / all_medias.size).round(1) : 0.0 + %> +

<%= media_geral %>

+

Métrica baseada na escala Likert 1-5

+
+
+ + <% if @mensagem_aviso.present? %> + +
+ + + +

<%= @mensagem_aviso %>

+

Aguarde até que os estudantes enviem suas respostas para ver as consolidações.

+
+ <% else %> + +
+

Distribuição Quantitativa

+ +
+ <% @perguntas_metricas.each_with_index do |pm, index| %> +
+
+ Pergunta <%= index + 1 %> +

<%= pm[:pergunta].enunciado %>

+ Média: <%= pm[:media] %> (com <%= pm[:count] %> respostas) +
+ +
+ <% (1..5).reverse_each do |num| %> + <% percentage = pm[:distribuicao][num] %> +
+ <%= num %> +
+
+
+ <%= percentage %>% +
+ <% end %> +
+
+ <% end %> +
+
+ + +
+

Avaliação Qualitativa (Opiniões Anônimas)

+ +
+ <% @comentarios.each do |c| %> +
+

+ + + + <%= c[:pergunta].enunciado %> +

+ +
+ <% if c[:respostas].empty? %> +

Nenhum comentário enviado para esta questão.

+ <% else %> + <% c[:respostas].each do |texto| %> +
+ "<%= texto %>" +
+ <% end %> + <% end %> +
+
+ <% end %> +
+
+ <% end %> + +
diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb new file mode 100644 index 0000000000..bf67c01b76 --- /dev/null +++ b/app/views/sessions/new.html.erb @@ -0,0 +1,69 @@ + +<% content_for :page_title, "Identificação" rescue nil %> + +
+ +
+
+ + + +
+

CAMAAR

+

CIC | UNB

+
+ + <% if flash[:notice] %> +
+ + + + <%= flash[:notice] %> +
+ <% end %> + + <% if flash[:alert] %> +
+ + + + <%= flash[:alert] %> +
+ <% end %> + + + <%= form_with url: login_path, local: true, class: "space-y-5" do |f| %> + +
+ +
+ + + + + + <%= f.text_field :email, id: "email", placeholder: "usuario@unb.br ou matrícula", class: "w-full pl-10 pr-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:border-[#702562] focus:bg-white text-xs text-gray-700 transition" %> +
+
+ + +
+
+ + <%= link_to "Esqueci minha senha", forgot_password_path, class: "text-[11px] font-bold text-[#702562] hover:underline" %> +
+
+ + + + + + <%= f.password_field :password, id: "password", placeholder: "••••••••", class: "w-full pl-10 pr-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:border-[#702562] focus:bg-white text-xs text-gray-700 transition" %> +
+
+ + + <%= f.submit "Entrar", class: "w-full py-3 mt-2 bg-[#702562] hover:bg-[#832d73] text-white font-bold text-xs uppercase tracking-widest rounded-xl shadow-lg hover:shadow-xl active:scale-98 transition duration-150 cursor-pointer" %> + <% end %> + +
diff --git a/app/views/shared/_header.html.erb b/app/views/shared/_header.html.erb new file mode 100644 index 0000000000..b4c29ee221 --- /dev/null +++ b/app/views/shared/_header.html.erb @@ -0,0 +1,88 @@ + +
+ + +
+ +

+ <%= yield :page_title if content_for?(:page_title) %> +

+
+ + + + + +
+ + + + + + + +
+
+ + diff --git a/app/views/shared/_sidebar.html.erb b/app/views/shared/_sidebar.html.erb new file mode 100644 index 0000000000..13a354e3f7 --- /dev/null +++ b/app/views/shared/_sidebar.html.erb @@ -0,0 +1,46 @@ + + diff --git a/app/views/templates/edit.html.erb b/app/views/templates/edit.html.erb new file mode 100644 index 0000000000..bbf26e3c5c --- /dev/null +++ b/app/views/templates/edit.html.erb @@ -0,0 +1,41 @@ + +<% content_for :page_title, "Editar Template" %> + + +
+
+ <%= link_to templates_path, class: "p-2 bg-white hover:bg-gray-100 border border-gray-200 text-gray-700 rounded-xl shadow-sm transition" do %> + + + + <% end %> +
+ Administração / Templates +

Editar Template

+
+
+ + <% if flash[:alert] %> +
+ <%= flash[:alert] %> +
+ <% end %> + +
+ <%= form_with model: @template, url: template_path(@template), local: true, method: :put, class: "space-y-6" do |f| %> +
+
+ <%= f.label :titulo, "Título", class: "text-xs font-bold text-gray-500 uppercase tracking-wider" %> + <%= f.text_field :titulo, id: "template_titulo", placeholder: "Escreva o título...", class: "w-full px-3 py-2 bg-gray-50 border border-gray-200 rounded-xl text-xs focus:outline-none focus:border-[#702562] text-gray-700" %> +
+ +
+ <%= f.label :descricao, "Descrição", class: "text-xs font-bold text-gray-500 uppercase tracking-wider" %> + <%= f.text_area :descricao, id: "template_descricao", placeholder: "Escreva a descrição...", class: "w-full h-24 px-3 py-2 bg-gray-50 border border-gray-200 rounded-xl text-xs focus:outline-none focus:border-[#702562] text-gray-700" %> +
+
+ + <%= f.submit "Atualizar Template", class: "px-6 py-2.5 bg-[#702562] hover:bg-[#832d73] text-white font-bold text-xs uppercase tracking-widest rounded-xl transition cursor-pointer" %> + <% end %> +
+
diff --git a/app/views/templates/index.html.erb b/app/views/templates/index.html.erb new file mode 100644 index 0000000000..09224b659d --- /dev/null +++ b/app/views/templates/index.html.erb @@ -0,0 +1,142 @@ + +<% content_for :page_title, "Gerenciamento de Templates" %> + +
+ + +
+ <%= link_to admin_dashboard_path, class: "p-2 bg-white hover:bg-gray-100 border border-gray-200 text-gray-700 rounded-xl shadow-sm transition" do %> + + + + <% end %> +
+ Administração / Colegiado +

Templates de Inquérito

+
+
+ + <% if flash[:notice] %> +
+ + + + <%= flash[:notice] %> +
+ <% end %> + + <% if flash[:alert] %> +
+ + + + <%= flash[:alert] %> +
+ <% end %> + +
+ + +
+
+

Templates Ativos

+ +
+ <% if @templates.empty? %> + +
Nenhum template de formulário foi encontrado.
+ <% end %> + + <% @templates.each do |template| %> +
+
+

<%= template.titulo %>

+ <%= template.perfil_alvo %> • <%= template.perguntas.count %> questões +
+
+ <%= link_to "Editar", edit_template_path(template), class: "text-[10px] font-bold text-blue-600 hover:underline" %> + <%= link_to "Deletar", template_path(template), data: { turbo_method: :delete, turbo_confirm: 'Tem certeza?' }, class: "text-[10px] font-bold text-red-600 hover:underline" %> +
+
+ <% end %> +
+
+
+ + +
+
+

Criar Novo Template

+ + <%= form_with model: @template, url: templates_path, local: true, class: "space-y-6" do |f| %> +
+ +
+ <%= f.label :titulo, "Título", class: "text-xs font-bold text-gray-500 uppercase tracking-wider" %> + <%= f.text_field :titulo, placeholder: "Ex: Avaliação Geral de Disciplinas Práticas", class: "w-full px-3 py-2 bg-gray-50 border border-gray-200 rounded-xl text-xs focus:outline-none focus:border-[#702562] text-gray-700 transition" %> +
+ +
+ <%= f.label :perfil_alvo, "Público Alvo", class: "text-xs font-bold text-gray-500 uppercase tracking-wider" %> + <%= f.select :perfil_alvo, [['Discente (Alunos)', 'discente'], ['Docente (Autoavaliação)', 'docente']], {}, class: "w-full px-3 py-2 bg-gray-50 border border-gray-200 rounded-xl text-xs focus:outline-none focus:border-[#702562] text-gray-700 transition" %> +
+ +
+ + +
+
+

Perguntas Cadastradas

+ +
+ + +
+
+
+ + +
+
+ + +
+
+
+ +
+ + <%= f.submit "Salvar Template", class: "px-6 py-2.5 bg-[#702562] hover:bg-[#832d73] text-white font-bold text-xs uppercase tracking-widest rounded-xl transition cursor-pointer" %> + <% end %> + +
+
+ +
+ +
+ + diff --git a/bin/brakeman b/bin/brakeman new file mode 100755 index 0000000000..171ac121a9 --- /dev/null +++ b/bin/brakeman @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("brakeman", "brakeman") diff --git a/bin/bundler-audit b/bin/bundler-audit new file mode 100755 index 0000000000..e2ef22690c --- /dev/null +++ b/bin/bundler-audit @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby +require_relative "../config/boot" +require "bundler/audit/cli" + +ARGV.concat %w[ --config config/bundler-audit.yml ] if ARGV.empty? || ARGV.include?("check") +Bundler::Audit::CLI.start diff --git a/bin/ci b/bin/ci new file mode 100755 index 0000000000..4137ad5bb0 --- /dev/null +++ b/bin/ci @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby +require_relative "../config/boot" +require "active_support/continuous_integration" + +CI = ActiveSupport::ContinuousIntegration +require_relative "../config/ci.rb" diff --git a/bin/cucumber b/bin/cucumber new file mode 100755 index 0000000000..eb5e962e86 --- /dev/null +++ b/bin/cucumber @@ -0,0 +1,11 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +vendored_cucumber_bin = Dir["#{File.dirname(__FILE__)}/../vendor/{gems,plugins}/cucumber*/bin/cucumber"].first +if vendored_cucumber_bin + load File.expand_path(vendored_cucumber_bin) +else + require 'rubygems' unless ENV['NO_RUBYGEMS'] + require 'cucumber' + load Cucumber::BINARY +end diff --git a/bin/dev b/bin/dev new file mode 100755 index 0000000000..5f91c20545 --- /dev/null +++ b/bin/dev @@ -0,0 +1,2 @@ +#!/usr/bin/env ruby +exec "./bin/rails", "server", *ARGV diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint new file mode 100755 index 0000000000..ed31659f40 --- /dev/null +++ b/bin/docker-entrypoint @@ -0,0 +1,8 @@ +#!/bin/bash -e + +# If running the rails server then create or migrate existing database +if [ "${@: -2:1}" == "./bin/rails" ] && [ "${@: -1:1}" == "server" ]; then + ./bin/rails db:prepare +fi + +exec "${@}" diff --git a/bin/importmap b/bin/importmap new file mode 100755 index 0000000000..36502ab16c --- /dev/null +++ b/bin/importmap @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby + +require_relative "../config/application" +require "importmap/commands" diff --git a/bin/jobs b/bin/jobs new file mode 100755 index 0000000000..dcf59f309a --- /dev/null +++ b/bin/jobs @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby + +require_relative "../config/environment" +require "solid_queue/cli" + +SolidQueue::Cli.start(ARGV) diff --git a/bin/kamal b/bin/kamal new file mode 100755 index 0000000000..d9ba276702 --- /dev/null +++ b/bin/kamal @@ -0,0 +1,16 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'kamal' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("kamal", "kamal") diff --git a/bin/rails b/bin/rails new file mode 100755 index 0000000000..efc0377492 --- /dev/null +++ b/bin/rails @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +APP_PATH = File.expand_path("../config/application", __dir__) +require_relative "../config/boot" +require "rails/commands" diff --git a/bin/rake b/bin/rake new file mode 100755 index 0000000000..4fbf10b960 --- /dev/null +++ b/bin/rake @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +require_relative "../config/boot" +require "rake" +Rake.application.run diff --git a/bin/rspec b/bin/rspec new file mode 100755 index 0000000000..93e191c2ff --- /dev/null +++ b/bin/rspec @@ -0,0 +1,16 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'rspec' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("rspec-core", "rspec") diff --git a/bin/rubocop b/bin/rubocop new file mode 100755 index 0000000000..5a20504716 --- /dev/null +++ b/bin/rubocop @@ -0,0 +1,8 @@ +#!/usr/bin/env ruby +require "rubygems" +require "bundler/setup" + +# Explicit RuboCop config increases performance slightly while avoiding config confusion. +ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__)) + +load Gem.bin_path("rubocop", "rubocop") diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000000..81be011e87 --- /dev/null +++ b/bin/setup @@ -0,0 +1,35 @@ +#!/usr/bin/env ruby +require "fileutils" + +APP_ROOT = File.expand_path("..", __dir__) + +def system!(*args) + system(*args, exception: true) +end + +FileUtils.chdir APP_ROOT do + # This script is a way to set up or update your development environment automatically. + # This script is idempotent, so that you can run it at any time and get an expectable outcome. + # Add necessary setup steps to this file. + + puts "== Installing dependencies ==" + system("bundle check") || system!("bundle install") + + # puts "\n== Copying sample files ==" + # unless File.exist?("config/database.yml") + # FileUtils.cp "config/database.yml.sample", "config/database.yml" + # end + + puts "\n== Preparing database ==" + system! "bin/rails db:prepare" + system! "bin/rails db:reset" if ARGV.include?("--reset") + + puts "\n== Removing old logs and tempfiles ==" + system! "bin/rails log:clear tmp:clear" + + unless ARGV.include?("--skip-server") + puts "\n== Starting development server ==" + STDOUT.flush # flush the output before exec(2) so that it displays + exec "bin/dev" + end +end diff --git a/bin/thrust b/bin/thrust new file mode 100755 index 0000000000..36bde2d832 --- /dev/null +++ b/bin/thrust @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("thruster", "thrust") diff --git a/config.ru b/config.ru new file mode 100644 index 0000000000..4a3c09a688 --- /dev/null +++ b/config.ru @@ -0,0 +1,6 @@ +# This file is used by Rack-based servers to start the application. + +require_relative "config/environment" + +run Rails.application +Rails.application.load_server diff --git a/config/application.rb b/config/application.rb new file mode 100644 index 0000000000..6fc486721d --- /dev/null +++ b/config/application.rb @@ -0,0 +1,42 @@ +require_relative "boot" + +require "rails" +# Pick the frameworks you want: +require "active_model/railtie" +require "active_job/railtie" +require "active_record/railtie" +require "active_storage/engine" +require "action_controller/railtie" +require "action_mailer/railtie" +require "action_mailbox/engine" +require "action_text/engine" +require "action_view/railtie" +require "action_cable/engine" +# require "rails/test_unit/railtie" + +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(*Rails.groups) + +module Camaar + class Application < Rails::Application + # Initialize configuration defaults for originally generated Rails version. + config.load_defaults 8.1 + + # Please, add to the `ignore` list any other `lib` subdirectories that do + # not contain `.rb` files, or that should not be reloaded or eager loaded. + # Common ones are `templates`, `generators`, or `middleware`, for example. + config.autoload_lib(ignore: %w[assets tasks]) + + # Configuration for the application, engines, and railties goes here. + # + # These settings can be overridden in specific environments using the files + # in config/environments, which are processed later. + # + # config.time_zone = "Central Time (US & Canada)" + # config.eager_load_paths << Rails.root.join("extras") + + # Don't generate system test files. + config.generators.system_tests = nil + end +end diff --git a/config/boot.rb b/config/boot.rb new file mode 100644 index 0000000000..988a5ddc46 --- /dev/null +++ b/config/boot.rb @@ -0,0 +1,4 @@ +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +require "bundler/setup" # Set up gems listed in the Gemfile. +require "bootsnap/setup" # Speed up boot time by caching expensive operations. diff --git a/config/bundler-audit.yml b/config/bundler-audit.yml new file mode 100644 index 0000000000..e74b3af949 --- /dev/null +++ b/config/bundler-audit.yml @@ -0,0 +1,5 @@ +# Audit all gems listed in the Gemfile for known security problems by running bin/bundler-audit. +# CVEs that are not relevant to the application can be enumerated on the ignore list below. + +ignore: + - CVE-THAT-DOES-NOT-APPLY diff --git a/config/cable.yml b/config/cable.yml new file mode 100644 index 0000000000..b9adc5aa3a --- /dev/null +++ b/config/cable.yml @@ -0,0 +1,17 @@ +# Async adapter only works within the same process, so for manually triggering cable updates from a console, +# and seeing results in the browser, you must do so from the web console (running inside the dev process), +# not a terminal started via bin/rails console! Add "console" to any action or any ERB template view +# to make the web console appear. +development: + adapter: async + +test: + adapter: test + +production: + adapter: solid_cable + connects_to: + database: + writing: cable + polling_interval: 0.1.seconds + message_retention: 1.day diff --git a/config/cache.yml b/config/cache.yml new file mode 100644 index 0000000000..19d490843b --- /dev/null +++ b/config/cache.yml @@ -0,0 +1,16 @@ +default: &default + store_options: + # Cap age of oldest cache entry to fulfill retention policies + # max_age: <%= 60.days.to_i %> + max_size: <%= 256.megabytes %> + namespace: <%= Rails.env %> + +development: + <<: *default + +test: + <<: *default + +production: + database: cache + <<: *default diff --git a/config/ci.rb b/config/ci.rb new file mode 100644 index 0000000000..239b343986 --- /dev/null +++ b/config/ci.rb @@ -0,0 +1,20 @@ +# Run using bin/ci + +CI.run do + step "Setup", "bin/setup --skip-server" + + step "Style: Ruby", "bin/rubocop" + + step "Security: Gem audit", "bin/bundler-audit" + step "Security: Importmap vulnerability audit", "bin/importmap audit" + step "Security: Brakeman code analysis", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error" + + + # Optional: set a green GitHub commit status to unblock PR merge. + # Requires the `gh` CLI and `gh extension install basecamp/gh-signoff`. + # if success? + # step "Signoff: All systems go. Ready for merge and deploy.", "gh signoff" + # else + # failure "Signoff: CI failed. Do not merge or deploy.", "Fix the issues and try again." + # end +end diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc new file mode 100644 index 0000000000..1411001b38 --- /dev/null +++ b/config/credentials.yml.enc @@ -0,0 +1 @@ +fywyrcFDqmtENeDC5ANr/ky1CpsjrHCFSB4VTeaF27jH0IdBkytPbheOEvGqG09Olpf1R2KV9fyAqfq4uhxWPdgLTJCBH7VbDOTg2RipmVTTN4aXnVHsyKCWwuHO+PgQwWyT1tz83DRCNSmft+IfF1pekwuElVN8sENL5fFtepO0quOfX79nDs9c0dV8Q1GvHLU+nvxuTi2XMZmMsQ8yD+sDZGPR31Um6kbnrLQioPbWPEbeAKctUHxaKaUxRWC4xn+opbXAVniO6LnNPjMtK2ksoZUH64m2eSVb22IGmSRFzK/IBkHrlsAijSe0i++arWQshzLHycK+Fh4ZwXJGz9J4ya69NTFwvHNyOBD+PZzaUcNXf7toutigeI8VCCkeEk/55TVUE1TJh7hVHm5a6K/FOsRrLHyAh24UIng2O7LtvMjPORdJsmz8/1fy2Y+YXWqGrfNATOOuSs1E+X2OQU7qoQcxtGPOtsCsYR5nqN8WPZ3y7/N2mAeh--eysr38NJDZEiVAHG--SOA/qLlPSAzzWQQYXnlz4A== \ No newline at end of file diff --git a/config/cucumber.yml b/config/cucumber.yml new file mode 100644 index 0000000000..47a4663ae2 --- /dev/null +++ b/config/cucumber.yml @@ -0,0 +1,8 @@ +<% +rerun = File.file?('rerun.txt') ? IO.read('rerun.txt') : "" +rerun = rerun.strip.gsub /\s/, ' ' +rerun_opts = rerun.empty? ? "--format #{ENV['CUCUMBER_FORMAT'] || 'progress'} features" : "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} #{rerun}" +std_opts = "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} --strict --tags 'not @wip'" +%> +default: <%= std_opts %> features +rerun: <%= rerun_opts %> --format rerun --out rerun.txt --strict --tags 'not @wip' diff --git a/config/database.yml b/config/database.yml new file mode 100644 index 0000000000..302d638c96 --- /dev/null +++ b/config/database.yml @@ -0,0 +1,40 @@ +# SQLite. Versions 3.8.0 and up are supported. +# gem install sqlite3 +# +# Ensure the SQLite 3 gem is defined in your Gemfile +# gem "sqlite3" +# +default: &default + adapter: sqlite3 + max_connections: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + timeout: 5000 + +development: + <<: *default + database: storage/development.sqlite3 + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + <<: *default + database: storage/test.sqlite3 + +# Store production database in the storage/ directory, which by default +# is mounted as a persistent Docker volume in config/deploy.yml. +production: + primary: + <<: *default + database: storage/production.sqlite3 + cache: + <<: *default + database: storage/production_cache.sqlite3 + migrations_paths: db/cache_migrate + queue: + <<: *default + database: storage/production_queue.sqlite3 + migrations_paths: db/queue_migrate + cable: + <<: *default + database: storage/production_cable.sqlite3 + migrations_paths: db/cable_migrate diff --git a/config/deploy.yml b/config/deploy.yml new file mode 100644 index 0000000000..fc1a441f14 --- /dev/null +++ b/config/deploy.yml @@ -0,0 +1,119 @@ +# Name of your application. Used to uniquely configure containers. +service: camaar + +# Name of the container image (use your-user/app-name on external registries). +image: 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. +# If used with Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption. +# +# Using an SSL proxy like this requires turning on config.assume_ssl and config.force_ssl in production.rb! +# +# Don't use this when deploying to multiple web servers (then you have to terminate SSL at your load balancer). +# +# proxy: +# ssl: true +# host: app.example.com + +# Where you keep your container images. +registry: + # Alternatives: hub.docker.com / registry.digitalocean.com / ghcr.io / ... + server: localhost:5555 + + # Needed for authenticated registries. + # username: your-user + + # Always use an access token rather than real password when possible. + # password: + # - KAMAL_REGISTRY_PASSWORD + +# Inject ENV variables into containers (secrets come from .kamal/secrets). +env: + secret: + - RAILS_MASTER_KEY + clear: + # Run the Solid Queue Supervisor inside the web server's Puma process to do jobs. + # When you start using multiple servers, you should split out job processing to a dedicated machine. + SOLID_QUEUE_IN_PUMA: true + + # Set number of processes dedicated to Solid Queue (default: 1) + # JOB_CONCURRENCY: 3 + + # Set number of cores available to the application on each server (default: 1). + # WEB_CONCURRENCY: 2 + + # Match this to any external database server to configure Active Record correctly + # Use 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 --include-password" + +# Use a persistent storage volume for sqlite database files and local Active Storage files. +# Recommended to change this to a mounted volume path that is backed up off server. +volumes: + - "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: 3.4.1 + # secrets: + # - GITHUB_TOKEN + # - RAILS_MASTER_KEY + +# Use a different ssh user than root +# ssh: +# user: app + +# Use accessory services (secrets come from .kamal/secrets). +# accessories: +# db: +# image: mysql:8.0 +# host: 192.168.0.2 +# # Change to 3306 to expose port to the world instead of just local network. +# port: "127.0.0.1:3306:3306" +# env: +# clear: +# MYSQL_ROOT_HOST: '%' +# secret: +# - MYSQL_ROOT_PASSWORD +# files: +# - config/mysql/production.cnf:/etc/mysql/my.cnf +# - db/production.sql:/docker-entrypoint-initdb.d/setup.sql +# directories: +# - data:/var/lib/mysql +# redis: +# image: valkey/valkey:8 +# host: 192.168.0.2 +# port: 6379 +# directories: +# - data:/data diff --git a/config/environment.rb b/config/environment.rb new file mode 100644 index 0000000000..cac5315775 --- /dev/null +++ b/config/environment.rb @@ -0,0 +1,5 @@ +# Load the Rails application. +require_relative "application" + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/config/environments/development.rb b/config/environments/development.rb new file mode 100644 index 0000000000..4a6ea19c9c --- /dev/null +++ b/config/environments/development.rb @@ -0,0 +1,82 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Configure 'rails notes' to inspect Cucumber files + config.annotations.register_directories("features") + config.annotations.register_extensions("feature") { |tag| /#\s*(#{tag}):?\s*(.*)$/ } + + # Settings specified here will take precedence over those in config/application.rb. + + # Make code changes take effect immediately without server restart. + config.enable_reloading = true + + # Do not eager load code on boot. + config.eager_load = false + + # Show full error reports. + config.consider_all_requests_local = true + + # Enable server timing. + config.server_timing = true + + # Enable/disable Action Controller caching. By default Action Controller caching is disabled. + # Run rails dev:cache to toggle Action Controller caching. + if Rails.root.join("tmp/caching-dev.txt").exist? + config.action_controller.perform_caching = true + config.action_controller.enable_fragment_cache_logging = true + config.public_file_server.headers = { "cache-control" => "public, max-age=#{2.days.to_i}" } + else + config.action_controller.perform_caching = false + end + + # Change to :null_store to avoid any caching. + config.cache_store = :memory_store + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + + # Don't care if the mailer can't send. + config.action_mailer.raise_delivery_errors = false + + # Make template changes take effect immediately. + config.action_mailer.perform_caching = false + + # Set localhost to be used by links generated in mailer templates. + config.action_mailer.default_url_options = { host: "localhost", port: 3000 } + + # Print deprecation notices to the Rails logger. + config.active_support.deprecation = :log + + # Raise an error on page load if there are pending migrations. + config.active_record.migration_error = :page_load + + # Highlight code that triggered database queries in logs. + config.active_record.verbose_query_logs = true + + # Append comments with runtime information tags to SQL queries in logs. + config.active_record.query_log_tags_enabled = true + + # Highlight code that enqueued background job in logs. + config.active_job.verbose_enqueue_logs = true + + # Highlight code that triggered redirect in logs. + config.action_dispatch.verbose_redirect_logs = true + + # Suppress logger output for asset requests. + config.assets.quiet = true + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + config.action_view.annotate_rendered_view_with_filenames = true + + # Uncomment if you wish to allow Action Cable access from any origin. + # config.action_cable.disable_request_forgery_protection = true + + # Raise error when a before_action's only/except options reference missing actions. + config.action_controller.raise_on_missing_callback_actions = true + + # Apply autocorrection by RuboCop to files generated by `bin/rails generate`. + # config.generators.apply_rubocop_autocorrect_after_generate! +end diff --git a/config/environments/production.rb b/config/environments/production.rb new file mode 100644 index 0000000000..f5763e04e5 --- /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 bin/rails credentials:edit. + # config.action_mailer.smtp_settings = { + # user_name: Rails.application.credentials.dig(:smtp, :user_name), + # password: Rails.application.credentials.dig(:smtp, :password), + # address: "smtp.example.com", + # port: 587, + # authentication: :plain + # } + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true + + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false + + # Only use :id for inspections in production. + config.active_record.attributes_for_inspect = [ :id ] + + # Enable DNS rebinding protection and other `Host` header attacks. + # config.hosts = [ + # "example.com", # Allow requests from example.com + # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` + # ] + # + # Skip DNS rebinding protection for the default health check endpoint. + # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } +end diff --git a/config/environments/test.rb b/config/environments/test.rb new file mode 100644 index 0000000000..29d195b837 --- /dev/null +++ b/config/environments/test.rb @@ -0,0 +1,57 @@ +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! + +Rails.application.configure do + # Configure 'rails notes' to inspect Cucumber files + config.annotations.register_directories("features") + config.annotations.register_extensions("feature") { |tag| /#\s*(#{tag}):?\s*(.*)$/ } + + # Settings specified here will take precedence over those in config/application.rb. + + # While tests run files are not watched, reloading is not necessary. + config.enable_reloading = false + + # Eager loading loads your entire application. When running a single test locally, + # this is usually not necessary, and can slow down your test suite. However, it's + # recommended that you enable it in continuous integration systems to ensure eager + # loading is working properly before deploying your code. + config.eager_load = ENV["CI"].present? + + # Configure public file server for tests with cache-control for performance. + config.public_file_server.headers = { "cache-control" => "public, max-age=3600" } + + # Show full error reports. + config.consider_all_requests_local = true + config.cache_store = :null_store + + # Render exception templates for rescuable exceptions and raise for other exceptions. + config.action_dispatch.show_exceptions = :rescuable + + # Disable request forgery protection in test environment. + config.action_controller.allow_forgery_protection = false + + # Store uploaded files on the local file system in a temporary directory. + config.active_storage.service = :test + + # Tell Action Mailer not to deliver emails to the real world. + # The :test delivery method accumulates sent emails in the + # ActionMailer::Base.deliveries array. + config.action_mailer.delivery_method = :test + + # Set host to be used by links generated in mailer templates. + config.action_mailer.default_url_options = { host: "example.com" } + + # Print deprecation notices to the stderr. + config.active_support.deprecation = :stderr + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true + + # Raise error when a before_action's only/except options reference missing actions. + config.action_controller.raise_on_missing_callback_actions = true +end diff --git a/config/importmap.rb b/config/importmap.rb new file mode 100644 index 0000000000..909dfc542d --- /dev/null +++ b/config/importmap.rb @@ -0,0 +1,7 @@ +# Pin npm packages by running ./bin/importmap + +pin "application" +pin "@hotwired/turbo-rails", to: "turbo.min.js" +pin "@hotwired/stimulus", to: "stimulus.min.js" +pin "@hotwired/stimulus-loading", to: "stimulus-loading.js" +pin_all_from "app/javascript/controllers", under: "controllers" diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb new file mode 100644 index 0000000000..487324424f --- /dev/null +++ b/config/initializers/assets.rb @@ -0,0 +1,7 @@ +# Be sure to restart your server when you modify this file. + +# Version of your assets, change this if you want to expire all your assets. +Rails.application.config.assets.version = "1.0" + +# Add additional assets to the asset load path. +# Rails.application.config.assets.paths << Emoji.images_path diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb new file mode 100644 index 0000000000..d51d713979 --- /dev/null +++ b/config/initializers/content_security_policy.rb @@ -0,0 +1,29 @@ +# Be sure to restart your server when you modify this file. + +# Define an application-wide content security policy. +# See the Securing Rails Applications Guide for more information: +# https://guides.rubyonrails.org/security.html#content-security-policy-header + +# Rails.application.configure do +# config.content_security_policy do |policy| +# policy.default_src :self, :https +# policy.font_src :self, :https, :data +# policy.img_src :self, :https, :data +# policy.object_src :none +# policy.script_src :self, :https +# policy.style_src :self, :https +# # Specify URI for violation reports +# # policy.report_uri "/csp-violation-report-endpoint" +# end +# +# # Generate session nonces for permitted importmap, inline scripts, and inline styles. +# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } +# config.content_security_policy_nonce_directives = %w(script-src style-src) +# +# # Automatically add `nonce` to `javascript_tag`, `javascript_include_tag`, and `stylesheet_link_tag` +# # if the corresponding directives are specified in `content_security_policy_nonce_directives`. +# # config.content_security_policy_nonce_auto = true +# +# # Report violations without enforcing the policy. +# # config.content_security_policy_report_only = true +# end diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb new file mode 100644 index 0000000000..c0b717f7ec --- /dev/null +++ b/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. +# Use this to limit dissemination of sensitive information. +# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. +Rails.application.config.filter_parameters += [ + :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc +] diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb new file mode 100644 index 0000000000..3860f659ea --- /dev/null +++ b/config/initializers/inflections.rb @@ -0,0 +1,16 @@ +# Be sure to restart your server when you modify this file. + +# Add new inflection rules using the following format. Inflections +# are locale specific, and you may define rules for as many different +# locales as you wish. All of these examples are active by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.plural /^(ox)$/i, "\\1en" +# inflect.singular /^(ox)en/i, "\\1" +# inflect.irregular "person", "people" +# inflect.uncountable %w( fish sheep ) +# end + +# These inflection rules are supported but not enabled by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.acronym "RESTful" +# end diff --git a/config/locales/en.yml b/config/locales/en.yml new file mode 100644 index 0000000000..6c349ae5e3 --- /dev/null +++ b/config/locales/en.yml @@ -0,0 +1,31 @@ +# Files in the config/locales directory are used for internationalization and +# are automatically loaded by Rails. If you want to use locales other than +# English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t "hello" +# +# In views, this is aliased to just `t`: +# +# <%= t("hello") %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# To learn more about the API, please read the Rails Internationalization guide +# at https://guides.rubyonrails.org/i18n.html. +# +# Be aware that YAML interprets the following case-insensitive strings as +# booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings +# must be quoted to be interpreted as strings. For example: +# +# en: +# "yes": yup +# enabled: "ON" + +en: + hello: "Hello world" diff --git a/config/puma.rb b/config/puma.rb new file mode 100644 index 0000000000..38c4b86596 --- /dev/null +++ b/config/puma.rb @@ -0,0 +1,42 @@ +# This configuration file will be evaluated by Puma. The top-level methods that +# are invoked here are part of Puma's configuration DSL. For more information +# about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. +# +# Puma starts a configurable number of processes (workers) and each process +# serves each request in a thread from an internal thread pool. +# +# You can control the number of workers using ENV["WEB_CONCURRENCY"]. You +# should only set this value when you want to run 2 or more workers. The +# default is already 1. You can set it to `auto` to automatically start a worker +# for each available processor. +# +# The ideal number of threads per worker depends both on how much time the +# application spends waiting for IO operations and on how much you wish to +# prioritize throughput over latency. +# +# As a rule of thumb, increasing the number of threads will increase how much +# traffic a given process can handle (throughput), but due to CRuby's +# Global VM Lock (GVL) it has diminishing returns and will degrade the +# response time (latency) of the application. +# +# The default is set to 3 threads as it's deemed a decent compromise between +# throughput and latency for the average Rails application. +# +# Any libraries that use a connection pool or another resource pool should +# be configured to provide at least as many connections as the number of +# threads. This includes Active Record's `pool` parameter in `database.yml`. +threads_count = ENV.fetch("RAILS_MAX_THREADS", 3) +threads threads_count, threads_count + +# Specifies the `port` that Puma will listen on to receive requests; default is 3000. +port ENV.fetch("PORT", 3000) + +# Allow puma to be restarted by `bin/rails restart` command. +plugin :tmp_restart + +# Run the Solid Queue supervisor inside of Puma for single-server deployments. +plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"] + +# Specify the PID file. Defaults to tmp/pids/server.pid in development. +# In other environments, only set the PID file if requested. +pidfile ENV["PIDFILE"] if ENV["PIDFILE"] diff --git a/config/queue.yml b/config/queue.yml new file mode 100644 index 0000000000..9eace59c41 --- /dev/null +++ b/config/queue.yml @@ -0,0 +1,18 @@ +default: &default + dispatchers: + - polling_interval: 1 + batch_size: 500 + workers: + - queues: "*" + threads: 3 + processes: <%= ENV.fetch("JOB_CONCURRENCY", 1) %> + polling_interval: 0.1 + +development: + <<: *default + +test: + <<: *default + +production: + <<: *default diff --git a/config/recurring.yml b/config/recurring.yml new file mode 100644 index 0000000000..b4207f9b07 --- /dev/null +++ b/config/recurring.yml @@ -0,0 +1,15 @@ +# examples: +# periodic_cleanup: +# class: CleanSoftDeletedRecordsJob +# queue: background +# args: [ 1000, { batch_size: 500 } ] +# schedule: every hour +# periodic_cleanup_with_command: +# command: "SoftDeletedRecord.due.delete_all" +# priority: 2 +# schedule: at 5am every day + +production: + clear_solid_queue_finished_jobs: + command: "SolidQueue::Job.clear_finished_in_batches(sleep_between_batches: 0.3)" + schedule: every hour at minute 12 diff --git a/config/routes.rb b/config/routes.rb new file mode 100644 index 0000000000..51dde42f02 --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,53 @@ +Rails.application.routes.draw do + # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. + get "up" => "rails/health#show", as: :rails_health_check + + # Session management routes + get "login" => "sessions#new", as: :login + post "login" => "sessions#create" + get "logout" => "sessions#destroy", as: :logout + + # Passwords (setup and reset) + get "usuarios/definir_senha" => "passwords#setup", as: :setup_password + post "usuarios/definir_senha" => "passwords#setup_update" + get "usuarios/esqueci_senha" => "passwords#forgot", as: :forgot_password + post "usuarios/esqueci_senha" => "passwords#forgot_send" + get "usuarios/redefinir_senha" => "passwords#reset", as: :reset_password + post "usuarios/redefinir_senha" => "passwords#reset_update" + + # Admin general panel & loader console + get "admin/dashboard" => "admin#dashboard", as: :admin_dashboard + post "admin/carregar_dados_teste" => "admin#carregar_dados_teste", as: :carregar_dados_teste + get "admin/import_console" => "admin#import_console", as: :admin_import_console + + # Admin special department actions + get "admin/turmas" => "admin#turmas", as: :admin_turmas + get "admin/turmas/:id/avaliacoes" => "admin#turma_avaliacoes", as: :admin_turma_avaliacoes + get "admin/desempenho_semestral" => "admin#desempenho_semestral", as: :admin_desempenho_semestral + + # Admin CSV Report + get "admin/relatorios/csv" => "resultados#export_csv_resultado", as: :admin_relatorios_csv + get "admin/relatorios" => "resultados#relatorios", as: :admin_relatorios + + # SIGAA Integration + post "admin/sigaa_import" => "admin#sigaa_import", as: :sigaa_import + post "admin/sigaa_update" => "admin#sigaa_update", as: :sigaa_update + + # Templates CRUD and endpoints + resources :templates, only: [ :index, :create, :destroy, :edit, :update ] + # Formularios (Distribution) + resources :formularios, only: [ :new, :create ] + + # Resultados (Consolidated reports) + get "resultados" => "resultados#index", as: :resultados + get "resultados/:id" => "resultados#show", as: :resultado + get "resultados/:id/export_csv" => "resultados#export_csv_resultado", as: :export_csv_resultado + + # Avaliacoes (Student prefilling & submittal) + get "avaliacoes" => "avaliacoes#index", as: :avaliacoes + get "avaliacoes/:id/responder" => "avaliacoes#new", as: :responder_avaliacao + post "avaliacoes/:id/responder" => "avaliacoes#create" + + # Root redirection + root to: "sessions#new" +end diff --git a/config/storage.yml b/config/storage.yml new file mode 100644 index 0000000000..927dc537c8 --- /dev/null +++ b/config/storage.yml @@ -0,0 +1,27 @@ +test: + service: Disk + root: <%= Rails.root.join("tmp/storage") %> + +local: + service: Disk + root: <%= Rails.root.join("storage") %> + +# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) +# amazon: +# service: S3 +# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> +# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> +# region: us-east-1 +# bucket: your_own_bucket-<%= Rails.env %> + +# Remember not to checkin your GCS keyfile to a repository +# google: +# service: GCS +# project: your_project +# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> +# bucket: your_own_bucket-<%= Rails.env %> + +# mirror: +# service: Mirror +# primary: local +# mirrors: [ amazon, google, microsoft ] diff --git a/coverage/.last_run.json b/coverage/.last_run.json new file mode 100644 index 0000000000..1795ce93ae --- /dev/null +++ b/coverage/.last_run.json @@ -0,0 +1,5 @@ +{ + "result": { + "line": 91.04 + } +} diff --git a/coverage/.resultset.json b/coverage/.resultset.json new file mode 100644 index 0000000000..b95d64eade --- /dev/null +++ b/coverage/.resultset.json @@ -0,0 +1,1562 @@ +{ + "RSpec": { + "coverage": { + "/home/rick/UnB/Engenharia_de_Software/Projeto_Final/CAMAAR/app/helpers/application_helper.rb": { + "lines": [ + 1, + null + ] + }, + "/home/rick/UnB/Engenharia_de_Software/Projeto_Final/CAMAAR/app/models/curso.rb": { + "lines": [ + 1, + 1, + 1, + null + ] + }, + "/home/rick/UnB/Engenharia_de_Software/Projeto_Final/CAMAAR/app/models/application_record.rb": { + "lines": [ + 1, + 1, + null + ] + }, + "/home/rick/UnB/Engenharia_de_Software/Projeto_Final/CAMAAR/app/models/departamento.rb": { + "lines": [ + 1, + 1, + null, + 1, + 1, + null, + 1, + null, + 1, + 5, + 11, + null, + null + ] + }, + "/home/rick/UnB/Engenharia_de_Software/Projeto_Final/CAMAAR/app/models/disciplina.rb": { + "lines": [ + 1, + 1, + null, + 1, + null, + 1, + 1, + null, + 1, + 4, + null, + null + ] + }, + "/home/rick/UnB/Engenharia_de_Software/Projeto_Final/CAMAAR/app/models/formulario.rb": { + "lines": [ + 1, + 1, + null, + 1, + 1, + 1, + 1, + null, + 1, + 1, + null, + 1, + 1, + 1, + 1, + 1, + 1, + null, + 1, + null, + 1, + 33, + 30, + 0, + null, + null, + null, + 1, + 33, + 1, + null, + null, + null, + 1, + 33, + null, + 29, + 29, + 1, + null, + null, + null, + null, + 1, + 33, + null, + 30, + 1, + null, + null, + null, + 1, + 33, + null, + null, + 1, + 30, + 30, + 1, + null, + null, + null, + 1, + 30, + null, + null, + 1, + 30, + null, + null + ] + }, + "/home/rick/UnB/Engenharia_de_Software/Projeto_Final/CAMAAR/app/models/matricula.rb": { + "lines": [ + 1, + 1, + null, + 1, + 1, + null, + 1, + 1, + null + ] + }, + "/home/rick/UnB/Engenharia_de_Software/Projeto_Final/CAMAAR/app/models/questao_template.rb": { + "lines": [ + 1, + 1, + null, + 1, + 1, + null, + 1, + 1, + null, + null, + null, + 1, + 40, + 5, + 35, + 5, + null, + 30, + null, + null, + null + ] + }, + "/home/rick/UnB/Engenharia_de_Software/Projeto_Final/CAMAAR/app/models/resposta_item.rb": { + "lines": [ + 1, + 1, + null, + 1, + 1, + null + ] + }, + "/home/rick/UnB/Engenharia_de_Software/Projeto_Final/CAMAAR/app/models/resposta.rb": { + "lines": [ + 1, + 1, + null, + 1, + 1, + 1, + null + ] + }, + "/home/rick/UnB/Engenharia_de_Software/Projeto_Final/CAMAAR/app/models/template.rb": { + "lines": [ + 1, + 1, + null, + 1, + 1, + 1, + null, + 1, + null, + 1, + 1, + null, + 1, + null, + 1, + 46, + 3, + 2, + null, + null, + null + ] + }, + "/home/rick/UnB/Engenharia_de_Software/Projeto_Final/CAMAAR/app/models/turma.rb": { + "lines": [ + 1, + 1, + null, + 1, + 1, + 1, + 1, + 1, + 1, + null, + 1, + 1, + 1, + 21, + null, + null, + 1, + 11, + null, + null, + 1, + 5, + null, + null, + 1, + 3, + null, + null, + 1, + 1, + 1, + 1, + 1, + null, + null + ] + }, + "/home/rick/UnB/Engenharia_de_Software/Projeto_Final/CAMAAR/app/models/usuario.rb": { + "lines": [ + 1, + 1, + null, + 1, + 1, + 1, + 1, + 1, + null, + 1, + 1, + null, + 1, + null, + null, + 1, + 84, + 84, + null, + 0, + null, + null, + 1, + 167, + 167, + null, + null, + 1, + null, + 1, + 2, + null, + null, + null, + 1, + 11, + 11, + 11, + 11, + null, + null, + 1, + 9, + 9, + 9, + 9, + null, + null, + 1, + null, + 1, + 9, + null, + 9, + 9, + 9, + 9, + null, + null, + 1, + 0, + null, + null, + 1, + 9, + null, + null, + 1, + 9, + null, + null, + 1, + 9, + null, + null, + 1, + 9, + null, + null + ] + }, + "/home/rick/UnB/Engenharia_de_Software/Projeto_Final/CAMAAR/app/controllers/sessions_controller.rb": { + "lines": [ + 1, + 1, + 4, + null, + 2, + 1, + null, + 1, + null, + null, + null, + 1, + 86, + 86, + null, + 86, + null, + 85, + null, + null, + 1, + 1, + 1, + 1, + null, + null, + 1, + null, + 1, + 85, + 85, + 81, + 80, + 79, + null, + 78, + null, + null, + 1, + 1, + 1, + null, + null, + 1, + 85, + null, + null, + 1, + 81, + null, + null, + 1, + 1, + 1, + null, + null, + 1, + 4, + 2, + 2, + 1, + null, + 1, + null, + 4, + null, + null, + 1, + 1, + 0, + null, + 1, + null, + 1, + null, + null, + 1, + 1, + 1, + null, + null, + 1, + 78, + 78, + 78, + 56, + null, + 22, + null, + null, + null + ] + }, + "/home/rick/UnB/Engenharia_de_Software/Projeto_Final/CAMAAR/app/controllers/application_controller.rb": { + "lines": [ + 1, + null, + 1, + null, + null, + 1, + null, + 1, + null, + 200, + null, + 1, + null, + 1, + 0, + null, + null, + 1, + 610, + null, + null, + 1, + 89, + null, + null, + 1, + 21, + 1, + 1, + null, + null, + null, + 1, + 56, + 2, + 2, + null, + null, + null + ] + }, + "/home/rick/UnB/Engenharia_de_Software/Projeto_Final/CAMAAR/app/controllers/admin_controller.rb": { + "lines": [ + 1, + 1, + null, + null, + 1, + null, + 1, + 3, + 1, + null, + null, + null, + 3, + null, + null, + null, + null, + null, + null, + null, + 1, + 3, + null, + null, + 2, + 1, + null, + 1, + null, + 2, + null, + null, + 1, + 1, + 1, + null, + null, + null, + 1, + 6, + 6, + 3, + null, + 3, + null, + null, + 6, + 4, + null, + null, + null, + 1, + 2, + 2, + 1, + 1, + null, + null, + 1, + null, + null, + 1, + 2, + 2, + null, + 2, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + 1, + 4, + 1, + 1, + null, + null, + 3, + null, + null, + 1, + 1, + null, + null, + 1, + 14, + 6, + 5, + 3, + null, + 1, + 1, + 1, + null, + 14, + null, + null, + 1, + 8, + 8, + 8, + null, + null, + 1, + 2, + 2, + 2, + null, + null, + 1, + 19, + null, + null, + 1, + null, + 1, + 9, + 9, + 5, + 5, + 5, + null, + 4, + null, + null, + 1, + 9, + 7, + 6, + 5, + 4, + null, + null, + 1, + 1, + 1, + 1, + null, + 1, + 9, + null, + null, + 1, + 13, + null, + null, + 1, + 5, + null, + null, + 1, + 6, + 1, + 1, + 1, + null, + 5, + null, + null, + 1, + 1, + null, + 1, + 1, + null, + 1, + 1, + null, + 1, + 0, + null, + 1, + null, + null, + null + ] + }, + "/home/rick/UnB/Engenharia_de_Software/Projeto_Final/CAMAAR/app/services/test_data_loader.rb": { + "lines": [ + 1, + 1, + 0, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + 1, + null, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + 1, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + 1, + 0, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + null, + 0, + 0, + 0, + null, + null, + null, + 1, + 0, + 0, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + 1, + 0, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + null, + 1, + 0, + null, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null + ] + }, + "/home/rick/UnB/Engenharia_de_Software/Projeto_Final/CAMAAR/app/controllers/avaliacoes_controller.rb": { + "lines": [ + 1, + 1, + 1, + null, + 1, + 12, + null, + 1, + null, + 4, + null, + 7, + null, + null, + null, + 1, + null, + null, + null, + 1, + 5, + 4, + null, + 2, + 2, + null, + null, + null, + null, + 2, + null, + null, + 2, + 2, + null, + 0, + 0, + null, + null, + 1, + null, + 1, + 5, + null, + 1, + 1, + 1, + null, + null, + 1, + 4, + null, + 2, + 2, + 2, + null, + null, + 1, + 1, + 1, + null, + null, + 1, + 4, + 4, + null, + null, + 1, + 7, + 7, + null, + null, + 1, + 11, + 11, + null, + 11, + null, + null, + null, + null, + null, + 11, + 11, + null, + null, + 1, + 4, + 6, + null, + null, + null, + 1, + 9, + null, + 9, + 3, + null, + 6, + null, + null, + null, + 1, + 2, + 3, + 3, + null, + 3, + null, + null, + null, + 1, + 3, + null, + null, + null, + null, + 3, + null, + null, + null, + 1, + null, + 1, + 8, + null, + null + ] + }, + "/home/rick/UnB/Engenharia_de_Software/Projeto_Final/CAMAAR/app/controllers/formularios_controller.rb": { + "lines": [ + 1, + 1, + null, + 1, + 1, + 1, + 1, + null, + null, + 1, + 6, + null, + 6, + 3, + null, + 3, + null, + null, + null, + 1, + null, + 1, + null, + 6, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 6, + 6, + 1, + 1, + null, + null, + 1, + 36, + null, + null, + 1, + 12, + 8, + null, + 0, + null, + null, + 1, + 3, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 3, + 0, + 0, + null, + 3, + null, + null, + null, + 1, + 3, + 3, + 3, + 3, + null, + null, + 1, + 3, + null, + null, + 1, + 3, + 3, + 3, + 2, + 3, + 3, + 1, + 0, + null, + null, + 1, + 1, + 1, + null, + 1, + 2, + 0, + 0, + null, + null, + 1, + 1, + null, + null, + 1, + 4, + 3, + null, + null, + 1, + 3, + null, + 2, + 2, + 2, + null, + null, + 1, + 1, + 1, + null, + null, + 1, + 2, + 2, + 2, + 2, + 3, + 3, + null, + null, + 2, + null, + null, + 1, + 3, + null, + null, + null, + null, + null, + null, + 3, + null, + null, + null, + 1, + 2, + 1, + 1, + null, + 1, + 1, + null, + null, + null + ] + }, + "/home/rick/UnB/Engenharia_de_Software/Projeto_Final/CAMAAR/app/controllers/passwords_controller.rb": { + "lines": [ + 1, + null, + 1, + null, + 1, + 4, + 4, + 4, + null, + null, + 1, + 4, + 3, + 2, + null, + null, + 1, + 2, + null, + null, + 1, + 4, + 4, + null, + null, + 1, + 7, + 6, + null, + 5, + null, + 5, + 1, + null, + 4, + null, + null, + null, + 1, + null, + null, + null, + 1, + 3, + 3, + 2, + null, + 1, + 1, + null, + 1, + 1, + null, + null, + 1, + 1, + 1, + null, + null, + 1, + 1, + 1, + null, + null, + 1, + 2, + 2, + 2, + null, + null, + 1, + 2, + 2, + 2, + null, + null, + 1, + 2, + null, + null, + 1, + 5, + 5, + 4, + 3, + null, + 2, + null, + 2, + 1, + null, + 1, + null, + null, + null, + 1, + null, + 1, + 12, + 12, + null, + null, + 1, + 1, + 1, + 1, + null, + null, + 1, + 10, + null, + 2, + 2, + 2, + null, + null, + 1, + 7, + 7, + 7, + null, + null, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + null, + null, + 1, + 5, + 5, + null, + null, + 1, + 5, + 1, + 1, + 1, + null, + 4, + null, + null, + 1, + 3, + 1, + 1, + 1, + null, + 2, + null, + null, + 1, + 1, + 1, + 1, + 1, + null, + null + ] + }, + "/home/rick/UnB/Engenharia_de_Software/Projeto_Final/CAMAAR/app/controllers/resultados_controller.rb": { + "lines": [ + 1, + null, + 1, + 1, + null, + 1, + 1, + null, + null, + 1, + 2, + 2, + 2, + null, + 2, + 2, + null, + 2, + 3, + null, + null, + null, + 1, + 1, + null, + null, + 1, + 4, + null, + 3, + 3, + 3, + null, + 3, + null, + 2, + 2, + null, + null, + 1, + null, + 1, + 3, + 3, + 1, + null, + 2, + null, + null, + null, + 1, + 1, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + 1, + 1, + 5, + 5, + 5, + null, + null, + null, + 1, + null, + 2, + null, + null, + null, + null, + 1, + 4, + 1, + 1, + 1, + null, + 3, + null, + null, + 1, + 3, + 1, + 1, + 1, + null, + 2, + null, + null, + 1, + 3, + null, + null, + 1, + 2, + 2, + 2, + 2, + null, + null, + 1, + 2, + 2, + null, + 2, + 2, + 2, + 2, + null, + null, + null, + null, + null, + 1, + 2, + 1, + null, + null, + 1, + 2, + 2, + null, + 2, + 2, + null, + null, + null, + 1, + 2, + null, + null, + 1, + 2, + null, + null, + 1, + 4, + 1, + 1, + null, + null, + null + ] + }, + "/home/rick/UnB/Engenharia_de_Software/Projeto_Final/CAMAAR/app/controllers/templates_controller.rb": { + "lines": [ + 1, + 1, + null, + 1, + 2, + 2, + null, + null, + 1, + 5, + null, + 5, + null, + 4, + 4, + null, + 3, + 3, + null, + 1, + null, + null, + 1, + 1, + null, + null, + 1, + 2, + 2, + 1, + 1, + null, + 1, + 1, + null, + null, + null, + 1, + 2, + 2, + 1, + null, + 1, + 1, + null, + 2, + null, + null, + 1, + null, + 1, + 3, + 3, + 1, + 1, + 1, + null, + 1, + null, + null, + null, + null, + 1, + 2, + null, + null, + 1, + 5, + 5, + null, + null, + null, + null, + null, + null, + null, + 1, + 5, + 1, + 1, + 1, + 1, + null, + 4, + null, + null, + null, + 1, + 4, + 8, + null, + null, + 1, + 4, + 1, + 1, + 1, + 1, + null, + 3, + null, + null, + null, + 1, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 2, + 2, + 2, + null, + null + ] + }, + "/home/rick/UnB/Engenharia_de_Software/Projeto_Final/CAMAAR/app/jobs/application_job.rb": { + "lines": [ + 0, + null, + null, + null, + null, + null, + 0 + ], + "branches": {} + }, + "/home/rick/UnB/Engenharia_de_Software/Projeto_Final/CAMAAR/app/mailers/application_mailer.rb": { + "lines": [ + 0, + 0, + 0, + 0 + ], + "branches": {} + } + }, + "timestamp": 1782513152 + } +} 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('