diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..325bfc036d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,51 @@ +# See https://docs.docker.com/engine/reference/builder/#dockerignore-file for more about ignoring files. + +# Ignore git directory. +/.git/ +/.gitignore + +# Ignore bundler config. +/.bundle + +# Ignore all environment files. +/.env* + +# Ignore all default key files. +/config/master.key +/config/credentials/*.key + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/.keep + +# Ignore storage (uploaded files in development and any SQLite databases). +/storage/* +!/storage/.keep +/tmp/storage/* +!/tmp/storage/.keep + +# Ignore assets. +/node_modules/ +/app/assets/builds/* +!/app/assets/builds/.keep +/public/assets + +# Ignore CI service files. +/.github + +# Ignore Kamal files. +/config/deploy*.yml +/.kamal + +# Ignore development files +/.devcontainer + +# Ignore Docker-related files +/.dockerignore +/Dockerfile* diff --git a/.gitgnore b/.gitgnore new file mode 100644 index 0000000000..d3a75ab722 --- /dev/null +++ b/.gitgnore @@ -0,0 +1,75 @@ +# Created by https://www.toptal.com/developers/gitignore/api/rails +# Edit at https://www.toptal.com/developers/gitignore?templates=rails + +### Rails ### +*.rbc +capybara-*.html +.rspec +/db/*.sqlite3 +/db/*.sqlite3-journal +/db/*.sqlite3-[0-9]* +/public/system +/coverage/ +/spec/tmp +*.orig +rerun.txt +pickle-email-*.html + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# TODO Comment out this rule if you are OK with secrets being uploaded to the repo +config/initializers/secret_token.rb +config/master.key + +# Only include if you have production secrets in this file, which is no longer a Rails default +# config/secrets.yml + +# dotenv, dotenv-rails +# TODO Comment out these rules if environment variables can be committed +.env +.env*.local + +## Environment normalization: +/.bundle +/vendor/bundle + +# these should all be checked in to normalize the environment: +# Gemfile.lock, .ruby-version, .ruby-gemset + +# unless supporting rvm < 1.11.0 or doing something fancy, ignore this: +.rvmrc + +# if using bower-rails ignore default bower_components path bower.json files +/vendor/assets/bower_components +*.bowerrc +bower.json + +# Ignore pow environment settings +.powenv + +# Ignore Byebug command history file. +.byebug_history + +# Ignore node_modules +node_modules/ + +# Ignore precompiled javascript packs +/public/packs +/public/packs-test +/public/assets + +# Ignore yarn files +/yarn-error.log +yarn-debug.log* +.yarn-integrity + +# Ignore uploaded files in development +/storage/* +!/storage/.keep +/public/uploads + +# End of https://www.toptal.com/developers/gitignore/api/rails \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..83610cfa4c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: +- package-ecosystem: bundler + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 10 +- package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 10 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000..0514d9c999 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,124 @@ +name: CI + +on: + pull_request: + push: + branches: [ master ] + +jobs: + scan_ruby: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Scan for common Rails security vulnerabilities using static analysis + run: bin/brakeman --no-pager + + - name: Scan for known security vulnerabilities in gems used + run: bin/bundler-audit + + scan_js: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Scan for security vulnerabilities in JavaScript dependencies + run: bin/importmap audit + + lint: + runs-on: ubuntu-latest + env: + RUBOCOP_CACHE_ROOT: tmp/rubocop + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Prepare RuboCop cache + uses: actions/cache@v4 + env: + DEPENDENCIES_HASH: ${{ hashFiles('.ruby-version', '**/.rubocop.yml', '**/.rubocop_todo.yml', 'Gemfile.lock') }} + with: + path: ${{ env.RUBOCOP_CACHE_ROOT }} + key: rubocop-${{ runner.os }}-${{ env.DEPENDENCIES_HASH }}-${{ github.ref_name == github.event.repository.default_branch && github.run_id || 'default' }} + restore-keys: | + rubocop-${{ runner.os }}-${{ env.DEPENDENCIES_HASH }}- + + - name: Lint code for consistent style + run: bin/rubocop -f github + + test: + runs-on: ubuntu-latest + + # services: + # redis: + # image: valkey/valkey:8 + # ports: + # - 6379:6379 + # options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Run tests + env: + RAILS_ENV: test + # RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }} + # REDIS_URL: redis://localhost:6379/0 + run: bin/rails db:test:prepare test + + system-test: + runs-on: ubuntu-latest + + # services: + # redis: + # image: valkey/valkey:8 + # ports: + # - 6379:6379 + # options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Run System Tests + env: + RAILS_ENV: test + # RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }} + # REDIS_URL: redis://localhost:6379/0 + run: bin/rails db:test:prepare test:system + + - name: Keep screenshots from failed system tests + uses: actions/upload-artifact@v4 + if: failure() + with: + name: screenshots + path: ${{ github.workspace }}/tmp/screenshots + if-no-files-found: ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..b0a15087c7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,64 @@ +# See https://help.github.com/articles/ignoring-files for more about ignoring files. +# +# If you find yourself ignoring temporary files generated by your text editor +# or operating system, you probably want to add a global ignore instead: +# git config --global core.excludesfile '~/.gitignore_global' + +# Ignore bundler config. +/.bundle + +# Ignore all environment files (except templates). +/.env* +!/.env*.erb + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep +!/tmp/pids/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/ +!/tmp/pids/.keep + +# Ignore storage (uploaded files in development and any SQLite databases). +/storage/* +!/storage/.keep +/tmp/storage/* +!/tmp/storage/ +!/tmp/storage/.keep + +# Ignore master key for decrypting credentials and more. +/config/master.key + +# Ignore assets. +/app/assets/builds/* +!/app/assets/builds/.keep +/public/assets + +# Ignore CI service files. +/.github +/figma +/docs/sprint2-plan.md +# Ignore development database files +*.sqlite3 +*.sqlite3-journal +*.sqlite3-shm +*.sqlite3-wal + +# Ignore bootsnap cache +/tmp/cache/ + +# Ignore node_modules +/node_modules + +# Ignore yarn files +/yarn-error.log +yarn-debug.log* +.yarn-integrity + +# Ignore uploaded files in development +/storage/development.sqlite3 +/storage/test.sqlite3 diff --git a/.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..f9d86d4a54 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,8 @@ +# Omakase Ruby styling for Rails +inherit_gem: { rubocop-rails-omakase: rubocop.yml } + +# Overwrite or add rules to create your own house style +# +# # Use `[a, [b, c]]` not `[ a, [ b, c ] ]` +# Layout/SpaceInsideArrayLiteralBrackets: +# Enabled: false diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000000..408069ae67 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +ruby-3.4.1 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..f63ab2f47b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,76 @@ +# syntax=docker/dockerfile:1 +# check=error=true + +# This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand: +# docker build -t camaar . +# docker run -d -p 80:80 -e RAILS_MASTER_KEY= --name camaar camaar + +# For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html + +# Make sure RUBY_VERSION matches the Ruby version in .ruby-version +ARG RUBY_VERSION=3.4.1 +FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base + +# Rails app lives here +WORKDIR /rails + +# Install base packages +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 && \ + ln -s /usr/lib/$(uname -m)-linux-gnu/libjemalloc.so.2 /usr/local/lib/libjemalloc.so && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Set production environment variables and enable jemalloc for reduced memory usage and latency. +ENV RAILS_ENV="production" \ + BUNDLE_DEPLOYMENT="1" \ + BUNDLE_PATH="/usr/local/bundle" \ + BUNDLE_WITHOUT="development" \ + LD_PRELOAD="/usr/local/lib/libjemalloc.so" + +# Throw-away build stage to reduce size of final image +FROM base AS build + +# Install packages needed to build gems +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y build-essential git libyaml-dev pkg-config && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Install application gems +COPY Gemfile Gemfile.lock vendor ./ + +RUN bundle install && \ + rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \ + # -j 1 disable parallel compilation to avoid a QEMU bug: https://github.com/rails/bootsnap/issues/495 + bundle exec bootsnap precompile -j 1 --gemfile + +# Copy application code +COPY . . + +# Precompile bootsnap code for faster boot times. +# -j 1 disable parallel compilation to avoid a QEMU bug: https://github.com/rails/bootsnap/issues/495 +RUN bundle exec bootsnap precompile -j 1 app/ lib/ + +# Precompiling assets for production without requiring secret RAILS_MASTER_KEY +RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile + + + + +# Final stage for app image +FROM base + +# Run and own only the runtime files as a non-root user for security +RUN groupadd --system --gid 1000 rails && \ + useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash +USER 1000:1000 + +# Copy built artifacts: gems, application +COPY --chown=rails:rails --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}" +COPY --chown=rails:rails --from=build /rails /rails + +# Entrypoint prepares the database. +ENTRYPOINT ["/rails/bin/docker-entrypoint"] + +# Start server via Thruster by default, this can be overwritten at runtime +EXPOSE 80 +CMD ["./bin/thrust", "./bin/rails", "server"] diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000000..5684e6cb51 --- /dev/null +++ b/Gemfile @@ -0,0 +1,81 @@ +source "https://rubygems.org" + +# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" +gem "rails", "~> 8.1.1" +gem "csv" +# 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" + +# Windows does not include zoneinfo files, so bundle the tzinfo-data gem +gem "tzinfo-data", platforms: %i[ windows jruby ] + +# Use the database-backed adapters for Rails.cache, Active Job, and Action Cable +gem "solid_cache" +gem "solid_queue" +gem "solid_cable" + +# Reduces boot times through caching; required in config/boot.rb +gem "bootsnap", require: false + +# Deploy this application anywhere as a Docker container [https://kamal-deploy.org] +gem "kamal", require: false + +# Add HTTP asset caching/compression and X-Sendfile acceleration to Puma [https://github.com/basecamp/thruster/] +gem "thruster", require: false + +# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] +gem "image_processing", "~> 1.2" + +group :development, :test do + # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem + gem "debug", platforms: %i[ mri windows ], require: "debug/prelude" + + # Audits gems for known security defects (use config/bundler-audit.yml to ignore issues) + gem "bundler-audit", require: false + + # Static analysis for security vulnerabilities [https://brakemanscanner.org/] + gem "brakeman", require: false + + # Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/] + gem "rubocop-rails-omakase", require: false + + gem "rspec-rails", "~> 6.1.0" + gem "factory_bot_rails" + gem "faker" + gem "shoulda-matchers", "~> 6.0" + gem "rubycritic", require: false + gem "simplecov", require: false + gem "rdoc" + gem "metric_fu-Saikuro", require: false +end + +group :development do + # Use console on exceptions pages [https://github.com/rails/web-console] + gem "web-console" +end + +group :test do + # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing] + gem "capybara" + gem "selenium-webdriver" +end + +group :test do + gem "cucumber-rails", "~> 4.0", require: false + gem "database_cleaner-active_record", "~> 2.2" +end diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000000..808cce0160 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,583 @@ +GEM + remote: https://rubygems.org/ + specs: + action_text-trix (2.1.15) + railties + actioncable (8.1.1) + actionpack (= 8.1.1) + activesupport (= 8.1.1) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + zeitwerk (~> 2.6) + actionmailbox (8.1.1) + actionpack (= 8.1.1) + activejob (= 8.1.1) + activerecord (= 8.1.1) + activestorage (= 8.1.1) + activesupport (= 8.1.1) + mail (>= 2.8.0) + actionmailer (8.1.1) + actionpack (= 8.1.1) + actionview (= 8.1.1) + activejob (= 8.1.1) + activesupport (= 8.1.1) + mail (>= 2.8.0) + rails-dom-testing (~> 2.2) + actionpack (8.1.1) + actionview (= 8.1.1) + activesupport (= 8.1.1) + nokogiri (>= 1.8.5) + rack (>= 2.2.4) + rack-session (>= 1.0.1) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) + actiontext (8.1.1) + action_text-trix (~> 2.1.15) + actionpack (= 8.1.1) + activerecord (= 8.1.1) + activestorage (= 8.1.1) + activesupport (= 8.1.1) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (8.1.1) + activesupport (= 8.1.1) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (8.1.1) + activesupport (= 8.1.1) + globalid (>= 0.3.6) + activemodel (8.1.1) + activesupport (= 8.1.1) + activerecord (8.1.1) + activemodel (= 8.1.1) + activesupport (= 8.1.1) + timeout (>= 0.4.0) + activestorage (8.1.1) + actionpack (= 8.1.1) + activejob (= 8.1.1) + activerecord (= 8.1.1) + activesupport (= 8.1.1) + marcel (~> 1.0) + activesupport (8.1.1) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + json + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + ast (2.4.3) + axiom-types (0.1.1) + descendants_tracker (~> 0.0.4) + ice_nine (~> 0.11.0) + thread_safe (~> 0.3, >= 0.3.1) + base64 (0.3.0) + bcrypt (3.1.20) + bcrypt_pbkdf (1.1.1) + bigdecimal (3.3.1) + bindex (0.8.1) + bootsnap (1.19.0) + msgpack (~> 1.2) + brakeman (7.1.1) + racc + builder (3.3.0) + bundler-audit (0.9.2) + bundler (>= 1.2.0, < 3) + thor (~> 1.0) + capybara (3.40.0) + addressable + matrix + mini_mime (>= 0.1.3) + nokogiri (~> 1.11) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + regexp_parser (>= 1.5, < 3.0) + xpath (~> 3.2) + childprocess (5.1.0) + logger (~> 1.5) + coercible (1.0.0) + descendants_tracker (~> 0.0.1) + concurrent-ruby (1.3.5) + connection_pool (2.5.4) + crass (1.0.6) + csv (3.3.5) + cucumber (10.1.1) + base64 (~> 0.2) + builder (~> 3.2) + cucumber-ci-environment (> 9, < 11) + cucumber-core (> 15, < 17) + cucumber-cucumber-expressions (> 17, < 19) + cucumber-html-formatter (> 20.3, < 22) + diff-lcs (~> 1.5) + logger (~> 1.6) + mini_mime (~> 1.1) + multi_test (~> 1.1) + sys-uname (~> 1.3) + cucumber-ci-environment (10.0.1) + cucumber-core (15.3.0) + cucumber-gherkin (> 27, < 35) + cucumber-messages (> 26, < 30) + cucumber-tag-expressions (> 5, < 9) + cucumber-cucumber-expressions (18.0.1) + bigdecimal + cucumber-gherkin (34.0.0) + cucumber-messages (> 25, < 29) + cucumber-html-formatter (21.15.1) + cucumber-messages (> 19, < 28) + cucumber-messages (27.2.0) + cucumber-rails (4.0.0) + capybara (>= 3.25, < 4) + cucumber (>= 7, < 11) + railties (>= 6.1, < 9) + cucumber-tag-expressions (8.0.0) + database_cleaner-active_record (2.2.2) + activerecord (>= 5.a) + database_cleaner-core (~> 2.0) + database_cleaner-core (2.0.1) + date (3.5.0) + debug (1.11.0) + irb (~> 1.10) + reline (>= 0.3.8) + descendants_tracker (0.0.4) + thread_safe (~> 0.3, >= 0.3.1) + diff-lcs (1.6.2) + docile (1.4.1) + dotenv (3.1.8) + drb (2.2.3) + dry-configurable (1.3.0) + dry-core (~> 1.1) + zeitwerk (~> 2.6) + dry-core (1.1.0) + concurrent-ruby (~> 1.0) + logger + zeitwerk (~> 2.6) + dry-inflector (1.2.0) + dry-initializer (3.2.0) + dry-logic (1.6.0) + bigdecimal + concurrent-ruby (~> 1.0) + dry-core (~> 1.1) + zeitwerk (~> 2.6) + dry-schema (1.14.1) + concurrent-ruby (~> 1.0) + dry-configurable (~> 1.0, >= 1.0.1) + dry-core (~> 1.1) + dry-initializer (~> 3.2) + dry-logic (~> 1.5) + dry-types (~> 1.8) + zeitwerk (~> 2.6) + dry-types (1.8.3) + bigdecimal (~> 3.0) + concurrent-ruby (~> 1.0) + dry-core (~> 1.0) + dry-inflector (~> 1.0) + dry-logic (~> 1.4) + zeitwerk (~> 2.6) + ed25519 (1.4.0) + erb (6.0.0) + erubi (1.13.1) + et-orbi (1.4.0) + tzinfo + factory_bot (6.5.6) + activesupport (>= 6.1.0) + factory_bot_rails (6.5.1) + factory_bot (~> 6.5) + railties (>= 6.1.0) + faker (3.5.3) + i18n (>= 1.8.11, < 2) + ffi (1.17.2-aarch64-linux-gnu) + ffi (1.17.2-aarch64-linux-musl) + ffi (1.17.2-arm-linux-gnu) + ffi (1.17.2-arm-linux-musl) + ffi (1.17.2-arm64-darwin) + ffi (1.17.2-x86_64-linux-gnu) + ffi (1.17.2-x86_64-linux-musl) + flay (2.14.0) + erubi (~> 1.10) + path_expander (~> 2.0) + prism (~> 1.5) + sexp_processor (~> 4.0) + flog (4.9.0) + path_expander (~> 2.0) + prism (~> 1.5) + sexp_processor (~> 4.8) + fugit (1.12.1) + et-orbi (~> 1.4) + raabro (~> 1.4) + globalid (1.3.0) + activesupport (>= 6.1) + i18n (1.14.7) + concurrent-ruby (~> 1.0) + ice_nine (0.11.2) + image_processing (1.14.0) + mini_magick (>= 4.9.5, < 6) + ruby-vips (>= 2.0.17, < 3) + importmap-rails (2.2.2) + actionpack (>= 6.0.0) + activesupport (>= 6.0.0) + railties (>= 6.0.0) + io-console (0.8.1) + irb (1.15.3) + pp (>= 0.6.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + jbuilder (2.14.1) + actionview (>= 7.0.0) + activesupport (>= 7.0.0) + json (2.16.0) + kamal (2.8.2) + activesupport (>= 7.0) + base64 (~> 0.2) + bcrypt_pbkdf (~> 1.0) + concurrent-ruby (~> 1.2) + dotenv (~> 3.1) + ed25519 (~> 1.4) + net-ssh (~> 7.3) + sshkit (>= 1.23.0, < 2.0) + thor (~> 1.3) + zeitwerk (>= 2.6.18, < 3.0) + language_server-protocol (3.17.0.5) + launchy (3.1.1) + addressable (~> 2.8) + childprocess (~> 5.0) + logger (~> 1.6) + lint_roller (1.1.0) + logger (1.7.0) + loofah (2.24.1) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mail (2.9.0) + logger + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + marcel (1.1.0) + matrix (0.4.3) + memoist3 (1.0.0) + metric_fu-Saikuro (1.1.3) + mini_magick (5.3.1) + logger + mini_mime (1.1.5) + minitest (5.26.1) + msgpack (1.8.0) + multi_test (1.1.0) + net-imap (0.5.12) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-scp (4.1.0) + net-ssh (>= 2.6.5, < 8.0.0) + net-sftp (4.0.0) + net-ssh (>= 5.0.0, < 8.0.0) + net-smtp (0.5.1) + net-protocol + net-ssh (7.3.0) + nio4r (2.7.5) + nokogiri (1.18.10-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.10-aarch64-linux-musl) + racc (~> 1.4) + nokogiri (1.18.10-arm-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.10-arm-linux-musl) + racc (~> 1.4) + nokogiri (1.18.10-arm64-darwin) + racc (~> 1.4) + nokogiri (1.18.10-x86_64-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.10-x86_64-linux-musl) + racc (~> 1.4) + ostruct (0.6.3) + parallel (1.27.0) + parser (3.3.10.0) + ast (~> 2.4.1) + racc + path_expander (2.0.0) + pp (0.6.3) + prettyprint + prettyprint (0.2.0) + prism (1.6.0) + propshaft (1.3.1) + actionpack (>= 7.0.0) + activesupport (>= 7.0.0) + rack + psych (5.2.6) + date + stringio + public_suffix (6.0.2) + puma (7.1.0) + nio4r (~> 2.0) + raabro (1.4.0) + racc (1.8.1) + rack (3.2.4) + rack-session (2.1.1) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rack-test (2.2.0) + rack (>= 1.3) + rackup (2.2.1) + rack (>= 3) + rails (8.1.1) + actioncable (= 8.1.1) + actionmailbox (= 8.1.1) + actionmailer (= 8.1.1) + actionpack (= 8.1.1) + actiontext (= 8.1.1) + actionview (= 8.1.1) + activejob (= 8.1.1) + activemodel (= 8.1.1) + activerecord (= 8.1.1) + activestorage (= 8.1.1) + activesupport (= 8.1.1) + bundler (>= 1.15.0) + railties (= 8.1.1) + rails-dom-testing (2.3.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.6.2) + loofah (~> 2.21) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + railties (8.1.1) + actionpack (= 8.1.1) + activesupport (= 8.1.1) + irb (~> 1.13) + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + tsort (>= 0.2) + zeitwerk (~> 2.6) + rainbow (3.1.1) + rake (13.3.1) + rdoc (6.15.1) + erb + psych (>= 4.0.0) + tsort + reek (6.5.0) + dry-schema (~> 1.13) + logger (~> 1.6) + parser (~> 3.3.0) + rainbow (>= 2.0, < 4.0) + rexml (~> 3.1) + regexp_parser (2.11.3) + reline (0.6.3) + io-console (~> 0.5) + rexml (3.4.4) + rspec-core (3.13.6) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.7) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-rails (6.1.5) + actionpack (>= 6.1) + activesupport (>= 6.1) + railties (>= 6.1) + rspec-core (~> 3.13) + rspec-expectations (~> 3.13) + rspec-mocks (~> 3.13) + rspec-support (~> 3.13) + rspec-support (3.13.6) + rubocop (1.81.7) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.47.1, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.48.0) + parser (>= 3.3.7.2) + prism (~> 1.4) + rubocop-performance (1.26.1) + lint_roller (~> 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.47.1, < 2.0) + rubocop-rails (2.34.0) + activesupport (>= 4.2.0) + lint_roller (~> 1.1) + rack (>= 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.44.0, < 2.0) + rubocop-rails-omakase (1.1.0) + rubocop (>= 1.72) + rubocop-performance (>= 1.24) + rubocop-rails (>= 2.30) + ruby-progressbar (1.13.0) + ruby-vips (2.2.5) + ffi (~> 1.12) + logger + ruby_parser (3.21.1) + racc (~> 1.5) + sexp_processor (~> 4.16) + rubycritic (4.11.0) + flay (~> 2.13) + flog (~> 4.7) + launchy (>= 2.5.2) + parser (>= 3.3.0.5) + rainbow (~> 3.1.1) + reek (~> 6.5.0, < 7.0) + rexml + ruby_parser (~> 3.21) + simplecov (>= 0.22.0) + tty-which (~> 0.5.0) + virtus (~> 2.0) + rubyzip (3.2.2) + securerandom (0.4.1) + selenium-webdriver (4.38.0) + base64 (~> 0.2) + logger (~> 1.4) + rexml (~> 3.2, >= 3.2.5) + rubyzip (>= 1.2.2, < 4.0) + websocket (~> 1.0) + sexp_processor (4.17.4) + shoulda-matchers (6.5.0) + activesupport (>= 5.2.0) + 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.2.4) + activejob (>= 7.1) + activerecord (>= 7.1) + concurrent-ruby (>= 1.3.1) + fugit (~> 1.11) + railties (>= 7.1) + thor (>= 1.3.1) + sqlite3 (2.8.0-aarch64-linux-gnu) + sqlite3 (2.8.0-aarch64-linux-musl) + sqlite3 (2.8.0-arm-linux-gnu) + sqlite3 (2.8.0-arm-linux-musl) + sqlite3 (2.8.0-arm64-darwin) + sqlite3 (2.8.0-x86_64-linux-gnu) + sqlite3 (2.8.0-x86_64-linux-musl) + sshkit (1.24.0) + base64 + logger + net-scp (>= 1.1.2) + net-sftp (>= 2.1.2) + net-ssh (>= 2.8.0) + ostruct + stimulus-rails (1.3.4) + railties (>= 6.0.0) + stringio (3.1.8) + sys-uname (1.4.1) + ffi (~> 1.1) + memoist3 (~> 1.0.0) + thor (1.4.0) + thread_safe (0.3.6) + thruster (0.1.16) + thruster (0.1.16-aarch64-linux) + thruster (0.1.16-arm64-darwin) + thruster (0.1.16-x86_64-linux) + timeout (0.4.4) + tsort (0.2.0) + tty-which (0.5.0) + turbo-rails (2.0.20) + actionpack (>= 7.1.0) + railties (>= 7.1.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.1.0) + uri (1.1.1) + useragent (0.16.11) + virtus (2.0.0) + axiom-types (~> 0.1) + coercible (~> 1.0) + descendants_tracker (~> 0.0, >= 0.0.3) + web-console (4.2.1) + actionview (>= 6.0.0) + activemodel (>= 6.0.0) + bindex (>= 0.4.0) + railties (>= 6.0.0) + websocket (1.2.11) + websocket-driver (0.8.0) + base64 + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + xpath (3.2.0) + nokogiri (~> 1.8) + zeitwerk (2.7.3) + +PLATFORMS + aarch64-linux + aarch64-linux-gnu + aarch64-linux-musl + arm-linux-gnu + arm-linux-musl + arm64-darwin-25 + x86_64-linux-gnu + x86_64-linux-musl + +DEPENDENCIES + bcrypt (~> 3.1) + bootsnap + brakeman + bundler-audit + capybara + csv + cucumber-rails (~> 4.0) + database_cleaner-active_record (~> 2.2) + debug + factory_bot_rails + faker + image_processing (~> 1.2) + importmap-rails + jbuilder + kamal + metric_fu-Saikuro + propshaft + puma (>= 5.0) + rails (~> 8.1.1) + rdoc + rspec-rails (~> 6.1.0) + rubocop-rails-omakase + rubycritic + selenium-webdriver + shoulda-matchers (~> 6.0) + simplecov + solid_cable + solid_cache + solid_queue + sqlite3 (>= 2.1) + stimulus-rails + thruster + turbo-rails + tzinfo-data + web-console + +BUNDLED WITH + 2.6.2 diff --git a/README.md b/README.md index 9d7fe1bf53..f124852e03 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,334 @@ -# CAMAAR -Sistema para avaliação de atividades acadêmicas remotas do CIC +# 📊 Sprint 3 - Refatoração e Documentação + +**Projeto:** CAMAAR - Sistema de Avaliação Acadêmica +**Branch:** sprint-3 +**Data de Entrega:** 15/12/2025 + +--- + +## 🎯 Objetivos da Sprint + +| Objetivo | Meta | Status | +|----------|------|--------| +| ABC Score | < 20 por método | ✅ **100% ATINGIDO** | +| Complexidade Ciclomática | < 10 por método | ✅ **100% ATINGIDO** | +| Cobertura de Testes | > 90% | ✅ **92.83% ATINGIDO** | +| Happy/Sad Path | Todos os cenários | ✅ **ATINGIDO** | +| Documentação RDoc | 100% documentado | ✅ **100% ATINGIDO** | + +--- + +## 📈 Métricas Finais + +### 1. ABC Score (Flog) - ✅ META ATINGIDA + +**Ferramenta:** [Flog](https://github.com/seattlerb/flog) + +``` +Média ABC: 6.3 (antes: 7.6) +Total Flog: 1392.3 (antes: 1372.3) +``` + +#### Comparativo Antes/Depois da Refatoração + +| Método | ABC Antes | ABC Depois | Redução | +|--------|-----------|------------|---------| +| `AvaliacoesController#responder` | 33.6 | 9.0 | ⬇️ 73% | +| `SigaaImporter#import_docente_dicentes_and_matriculas` | 30.3 | 3.3 | ⬇️ 89% | +| `TemplateQuestion#normalize_fields` | 28.3 | 4.9 | ⬇️ 83% | +| `SigaaImportsController#update_database` | 23.8 | 9.5 | ⬇️ 60% | +| `AvaliacoesController#create` | 23.4 | 6.5 | ⬇️ 72% | +| `TemplateQuestion#validate_likert_scale` | 22.9 | 6.9 | ⬇️ 70% | +| `SigaaImporter::ImportResult#summary_message` | 22.7 | 3.4 | ⬇️ 85% | +| `SessionsController#create` | 22.6 | 9.1 | ⬇️ 60% | +| `TemplatesController#build_placeholder_question` | 20.6 | 5.2 | ⬇️ 75% | + +**Resultado:** ✅ **TODOS os métodos do código < 20** (único >= 20 é `Avaliacao#none` - método gerado pelo Rails) + +--- + +### 2. Complexidade Ciclomática (RuboCop) - ✅ META ATINGIDA + +**Ferramenta:** RuboCop `Metrics/CyclomaticComplexity` +**Nota:** Saikuro incompatível com Ruby 3.4.1 + +#### Top 10 Métodos Mais Complexos (Após Refatoração) + +| # | Método | Complexidade | Status | +|---|--------|--------------|--------| +| 1 | `TemplateQuestion#normalize_by_question_type` | 4 | ✅ < 10 | +| 2 | `SigaaImporter#find_turma_for_member_data` | 3 | ✅ < 10 | +| 3 | `SessionsController#create` | 3 | ✅ < 10 | +| 4 | `EvaluationBatchCreator#call` | 3 | ✅ < 10 | +| 5 | `AvaliacoesController#responder` | 3 | ✅ < 10 | +| 6 | `PasswordsController#create` | 2 | ✅ < 10 | +| 7 | `ReportGenerator#totals` | 2 | ✅ < 10 | +| 8 | `EvaluationResultsExporter#call` | 2 | ✅ < 10 | + +**Resultado:** ✅ **0 métodos com complexidade >= 10** + +--- + +### 3. Cobertura de Testes (SimpleCov) - ✅ META ATINGIDA + +**Ferramenta:** [SimpleCov](https://github.com/colszowka/simplecov) + +``` +Total de Linhas: 935 +Linhas Cobertas: 868 +Cobertura: 92.83% +``` + +#### Cobertura por Tipo de Arquivo + +**Controllers:** +| Arquivo | Cobertura | Status | +|---------|-----------|--------| +| ApplicationController | 100% | ✅ | +| ResultadosController | 97.22% | ✅ | +| SessionsController | 96% | ✅ | +| SigaaImportsController | 96.43% | ✅ | +| AvaliacoesController | 90.10% | ✅ | +| TemplatesController | 91.80% | ✅ | +| PasswordsController | 90.24% | ✅ | + +**Models (100%):** +| Modelo | Cobertura | +|--------|-----------| +| Usuario | 100% | +| Docente | 100% | +| Dicente | 100% | +| Avaliacao | 100% | +| Template | 100% | +| TemplateQuestion | 97.78% | +| Questao | 100% | +| Resposta | 100% | +| RespostaItem | 100% | +| Materia | 100% | +| Turma | 100% | +| Matricula | 100% | + +**Services:** +| Service | Cobertura | Status | +|---------|-----------|--------| +| EvaluationResultsExporter | 100% | ✅ | +| EvaluationResultAggregator | 100% | ✅ | +| EvaluationBatchCreator | 97.67% | ✅ | +| ReportGenerator | 95.83% | ✅ | +| SigaaImporter | 85.60% | ✅ | + +--- + +### 4. Happy Path & Sad Path - ✅ IMPLEMENTADO + +#### Estatísticas de Testes + +| Framework | Exemplos | Status | +|-----------|----------|--------| +| RSpec | 253 specs | ✅ 0 falhas | +| Cucumber | 59 scenarios, 388 steps | ✅ 0 falhas | + +#### Exemplos de Cobertura Happy/Sad Path + +**Sessions (Login):** +- ✅ Happy: Login com credenciais válidas → redireciona para dashboard +- ✅ Sad: Login com senha incorreta → renderiza erro +- ✅ Sad: Login com conta pendente → alerta de ativação + +**Avaliacoes (Submissão):** +- ✅ Happy: Dicente envia respostas válidas → sucesso +- ✅ Sad: Dicente não matriculado → acesso negado +- ✅ Sad: Avaliação já respondida → alerta de duplicidade +- ✅ Sad: Questões obrigatórias em branco → erro de validação + +**Templates:** +- ✅ Happy: Docente cria template válido → sucesso +- ✅ Sad: Template sem questões → erro de validação +- ✅ Sad: Nome duplicado → erro de unicidade + +**SigaaImporter:** +- ✅ Happy: Importação com JSON válido → sucesso +- ✅ Sad: Arquivo não encontrado → erro +- ✅ Sad: JSON inválido → erro de parsing + +--- + +### 5. Documentação RDoc - ✅ 100% ATINGIDO + +**Ferramenta:** [RDoc](https://github.com/ruby/rdoc) + +``` +Classes: 30 (30 documentadas) - 100% +Modules: 1 (1 documentado) - 100% +Constants: 4 (4 documentadas) - 100% +Attributes: 9 (9 documentados) - 100% +Methods: 64 (64 documentados) - 100% +Total: 108 (108 documentados) - 100% +``` + +#### Classes Documentadas + +**Models:** +- ✅ Usuario - Base STI com atributos e associações +- ✅ Docente - Professor com turmas e templates +- ✅ Dicente - Aluno com matrículas e respostas +- ✅ Avaliacao - Formulário de avaliação +- ✅ Template - Template reutilizável +- ✅ TemplateQuestion - Questão do template +- ✅ Questao - Questão da avaliação +- ✅ Resposta - Resposta do aluno +- ✅ RespostaItem - Item individual da resposta +- ✅ Materia - Disciplina +- ✅ Turma - Turma/classe +- ✅ Matricula - Matrícula do aluno + +**Controllers:** +- ✅ AvaliacoesController - Gerenciamento de avaliações +- ✅ SessionsController - Autenticação +- ✅ PasswordsController - Configuração de senha +- ✅ TemplatesController - Gerenciamento de templates + +**Services:** +- ✅ SigaaImporter - Importação de dados SIGAA +- ✅ EvaluationBatchCreator - Criação em lote +- ✅ EvaluationResultAggregator - Agregação de resultados +- ✅ EvaluationResultsExporter - Exportação CSV +- ✅ ReportGenerator - Geração de relatórios + +#### Como Acessar a Documentação + +```bash +# Gerar documentação +bundle exec rdoc app/ --output doc/rdoc + +# Abrir no navegador +open doc/rdoc/index.html +``` + +--- + +## 🛠️ Técnicas de Refatoração Aplicadas + +### 1. Extract Method +Métodos longos foram divididos em métodos menores e focados: + +```ruby +# Antes +def responder + # 30+ linhas de código +end + +# Depois +def responder + return unless validate_responder_permission + return unless validate_enrollment_in_turma + return unless validate_not_already_responded + load_responder_data +end +``` + +### 2. Replace Conditional with Guard Clauses +Condicionais aninhados substituídos por early returns: + +```ruby +# Antes +if user && user.authenticate(password) + if !user.pending_activation + login_user(user) + else + render_pending_activation + end +else + render_invalid_credentials +end + +# Depois +return render_invalid_credentials unless user&.authenticate(password) +return render_pending_activation if pending_activation?(user) +login_user(user) +``` + +### 3. Decompose Conditional +Condições complexas extraídas para métodos predicados: + +```ruby +# Antes +unless min_value.present? && max_value.present? && min_value >= 1 && max_value <= 5 && min_value < max_value + +# Depois +def valid_likert_configuration? + values_present? && values_in_range? && min_less_than_max? +end +``` + +### 4. Single Responsibility Principle +Classes e métodos com uma única responsabilidade: + +```ruby +# SigaaImporter agora delega para métodos específicos: +# - find_turma_for_member_data +# - import_docente_for_turma +# - import_dicentes_for_turma +# - update_turma_docente +``` + +--- + +## 📁 Arquivos Modificados + +### Controllers +- `app/controllers/avaliacoes_controller.rb` - 15 novos métodos +- `app/controllers/sessions_controller.rb` - 8 novos métodos +- `app/controllers/templates_controller.rb` - 6 novos métodos +- `app/controllers/sigaa_imports_controller.rb` - 4 novos métodos + +### Models +- `app/models/template_question.rb` - 10 novos métodos +- `app/models/usuario.rb` - Documentação RDoc +- `app/models/avaliacao.rb` - Documentação RDoc +- Todos os demais models - Documentação RDoc + +### Services +- `app/services/sigaa_importer.rb` - 12 novos métodos +- `app/services/evaluation_batch_creator.rb` - Documentação completa +- `app/services/evaluation_result_aggregator.rb` - Documentação completa +- `app/services/evaluation_results_exporter.rb` - Documentação completa +- `app/services/report_generator.rb` - Documentação completa + +--- + +## 📊 Resumo das Melhorias + +| Métrica | Antes | Depois | Melhoria | +|---------|-------|--------|----------| +| Métodos com ABC >= 20 | 9 | 0 | ✅ -100% | +| Métodos com Complexidade >= 10 | 0 | 0 | ✅ Mantido | +| Cobertura de Testes | 91.49% | 92.83% | ✅ +1.34% | +| Documentação RDoc | 16% | 100% | ✅ +84% | +| Métodos extraídos | - | ~60 | ✅ Novos | + +--- + +## 🔧 Ferramentas Utilizadas + +| Ferramenta | Versão | Propósito | +|------------|--------|-----------| +| RubyCritic | 4.11.0 | Análise de qualidade | +| Flog | 4.9.0 | ABC Score | +| SimpleCov | 0.22.0 | Cobertura de testes | +| RDoc | (bundled) | Documentação | +| RuboCop | via rubocop-rails-omakase | Complexidade ciclomática | + +--- + +## ✅ Conclusão + +A Sprint 3 foi concluída com **100% dos objetivos atingidos**: + +1. ✅ **ABC Score < 20**: Todos os métodos refatorados +2. ✅ **Complexidade < 10**: Nenhum método excede o limite +3. ✅ **Cobertura > 90%**: 92.83% de cobertura +4. ✅ **Happy/Sad Path**: 253 specs + 59 cenários Cucumber +5. ✅ **RDoc 100%**: 108/108 elementos documentados + +O código agora está mais limpo, testado e documentado, seguindo as melhores práticas do capítulo 9 do livro. 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/SPRINT3_RESUMO.md b/SPRINT3_RESUMO.md new file mode 100644 index 0000000000..0f6bd22ed8 --- /dev/null +++ b/SPRINT3_RESUMO.md @@ -0,0 +1,302 @@ +# 📊 RESUMO COMPLETO - SPRINT 3 +**Projeto:** CAMAAR - Sistema de Avaliação +**Branch:** sprint-3 + +--- + +## 🎯 OBJETIVOS DA SPRINT + +1. ✅ **ABC Score < 20** por método +2. ✅ **Cobertura de Testes > 90%** +3. ✅ **Complexidade Ciclomática < 10** por método +4. ✅ **Happy Path e Sad Path** nos testes +5. ✅ **Documentação RDoc** dos métodos criados + +--- + +## 📈 MÉTRICAS ATUAIS + +--- + +### 1️⃣ COMPLEXIDADE CICLOMÁTICA (RUBOCOP) - ✅ META ATINGIDA! + +**Status:** 🎉 **TODOS os métodos < 10!** + +**Ferramenta:** RuboCop `Metrics/CyclomaticComplexity` +**Motivo:** Saikuro incompatível com Ruby 3.4.1 + +#### Métodos com Maior Complexidade (Top 10): + +| # | Método | Complexidade | Status | Arquivo | +|---|--------|--------------|--------|---------| +| 1 | `TemplateQuestion#normalize_fields` | 9 | ✅ < 10 | template_question.rb:32 | +| 2 | `SigaaImporter#import_docente_dicentes_and_matriculas` | 8 | ✅ < 10 | sigaa_importer.rb:278 | +| 3 | `TemplateQuestion#validate_likert_scale` | 7 | ✅ < 10 | template_question.rb:75 | +| 4 | `SigaaImporter::ImportResult#summary_message` | 6 | ✅ < 10 | sigaa_importer.rb:82 | +| 5 | `SigaaImporter#docente_needs_update?` | 6 | ✅ < 10 | sigaa_importer.rb:378 | +| 6 | `SigaaImporter#import_turma` | 6 | ✅ < 10 | sigaa_importer.rb:237 | +| 7 | `EvaluationResultsExporter#call` | 6 | ✅ < 10 | evaluation_results_exporter.rb:14 | +| 8 | `EvaluationBatchCreator#call` | 6 | ✅ < 10 | evaluation_batch_creator.rb:18 | +| 9 | `AvaliacoesController#responder` | 6 | ✅ < 10 | avaliacoes_controller.rb:37 | +| 10 | `SessionsController#create` | 5 | ✅ < 10 | sessions_controller.rb:13 | + +**Resultados:** +- ✅ **Arquivos inspecionados:** 25 +- ✅ **Métodos com complexidade >= 10:** 0 (ZERO!) +- ✅ **Método mais complexo:** 9 (normalize_fields) +- ✅ **Maioria dos métodos:** complexidade <= 6 + +--- + +### 2️⃣ ABC SCORE (FLOG) - ✅ META PARCIALMENTE ATINGIDA + +**Status Geral:** +- **Média ABC:** 7.7 (antes: 8.3 → inicial: 10.9) → ⬇️ **29% de redução total** +- **Total Flog:** 1362.1 (antes: 1391.4) +- **Métodos refatorados:** 9 métodos principais ✨ + +#### 🏆 Refatorações Concluídas (Redução de 83-94%): + +| # | Método | ABC Antes | ABC Depois | Redução | Arquivo | +|---|--------|-----------|------------|---------|---------| +| 1 | `import_dicente_and_matricula` | **106.4** | **5.9** | ⬇️ 94% | sigaa_importer.rb:408 | +| 2 | `import_docente` | **71.8** | **7.8** | ⬇️ 89% | sigaa_importer.rb:327 | +| 3 | `submeter` | **70.2** | **8.2** | ⬇️ 88% | avaliacoes_controller.rb:84 | +| 4 | `processar_respostas` | **46.0** | **5.0** | ⬇️ 89% | avaliacoes_controller.rb:203 | +| 5 | `create` (PasswordsController) | **43.1** | **7.2** | ⬇️ 83% | passwords_controller.rb:16 | +| 6 | `import_materia_and_turma` | **41.3** | **5.2** | ⬇️ 87% | sigaa_importer.rb:115 | +| 7 | **`call` (EvaluationResultsExporter)** | **41.3** | **dividido** | ⬇️ ~90% | evaluation_results_exporter.rb:14 | +| 8 | **`import_turma`** | **37.4** | **dividido** | ⬇️ ~88% | sigaa_importer.rb:237 | +| 9 | **`call` (EvaluationBatchCreator)** | **35.0** | **dividido** | ⬇️ ~87% | evaluation_batch_creator.rb:18 | + +**Técnicas de Refatoração Aplicadas:** +- ✅ Extração de métodos (Extract Method) +- ✅ Decomposição de condicionais complexas +- ✅ Separação de responsabilidades (Single Responsibility) +- ✅ Eliminação de código duplicado (DRY) +- ✅ Decomposição de loops complexos + +#### ⚠️ Métodos que Ainda Precisam de Refatoração (ABC >= 20): + +| # | Método | ABC Score | Arquivo | +|---|--------|-----------|---------| +| 1 | `AvaliacoesController#responder` | 33.6 | avaliacoes_controller.rb:37 | +| 2 | `SigaaImporter#import_docente_dicentes_and_matriculas` | 30.3 | sigaa_importer.rb:278 | +| 3 | `TemplateQuestion#normalize_fields` | 28.3 | template_question.rb:32 | +| 4 | `SigaaImportsController#update_database` | 23.8 | sigaa_imports_controller.rb:29 | +| 5 | `AvaliacoesController#create` | 23.4 | avaliacoes_controller.rb:155 | +| 6 | `TemplateQuestion#validate_likert_scale` | 22.9 | template_question.rb:75 | +| 7 | `SigaaImporter::ImportResult#summary_message` | 22.7 | sigaa_importer.rb:82 | +| 8 | `SessionsController#create` | 22.6 | sessions_controller.rb:13 | +| 9 | `TemplatesController#build_placeholder_question` | 20.6 | templates_controller.rb:95 | + +**Total:** 9 métodos ainda precisam refatoração (⬇️ 3 métodos eliminados!) + + +### 3️⃣ COBERTURA DE TESTES (SIMPLECOV) - ✅ META ATINGIDA! + +**Status:** 🎉 **91.49% - Acima da meta de 90%!** ⬆️ + +#### Estatísticas Gerais: +- **Total de linhas:** 870 (⬆️ +29 linhas de código refatorado) +- **Linhas cobertas:** 796 (⬆️ +28 linhas) +- **Linhas não cobertas:** 74 +- **Testes RSpec:** 253 specs, 0 falhas ✅ + +#### Cobertura por Tipo de Arquivo: + +**📊 Controllers:** +| Arquivo | Cobertura | Status | +|---------|-----------|--------| +| ApplicationController | 100% | ✅ | +| ResultadosController | 97.22% | ✅ | +| SessionsController | 96% | ✅ | +| SigaaImportsController | 96.43% | ✅ | +| TemplatesController | 88.52% | ⚠️ Precisa melhorar | +| PasswordsController | 85.37% | ⚠️ Precisa melhorar | +| AvaliacoesController | 85.15% | ⚠️ Precisa melhorar | + +**📊 Models (100% - PERFEITO!):** +- ✅ Usuario, Dicente, Docente, Materia, Turma +- ✅ Avaliacao, Questao, Resposta, RespostaItem, Matricula +- ✅ Template, TemplateQuestion (97.78%) +- ✅ ApplicationRecord + +**📊 Services:** +| Arquivo | Cobertura | Status | +|---------|-----------|--------| +| EvaluationResultsExporter | 100% | ✅ | +| EvaluationResultAggregator | 100% | ✅ | +| EvaluationBatchCreator | 97.67% | ✅ | +| ReportGenerator | 95.83% | ✅ | +| SigaaImporter | 78.6% | ⚠️ **Precisa melhorar** | + +#### Arquivos que Precisam de Mais Testes: + +| Arquivo | Cobertura Atual | Linhas Faltando | Prioridade | +|---------|-----------------|-----------------|------------| +| SigaaImporter | 78.6% (191/243) | 52 linhas | 🔴 ALTA | +| AvaliacoesController | 85.15% (86/101) | 15 linhas | 🟡 MÉDIA | +| PasswordsController | 85.37% (35/41) | 6 linhas | 🟡 MÉDIA | +| TemplatesController | 88.52% (54/61) | 7 linhas | 🟡 MÉDIA | + +--- + +### 4️⃣ HAPPY PATH & SAD PATH - ✅ IMPLEMENTADO + +**Status:** Cenários positivos e negativos cobertos + +#### Exemplos de Cobertura: + +**Request Specs:** +- ✅ Avaliacoes: sucesso e falha na submissão +- ✅ Resultados: acesso autorizado/não autorizado +- ✅ SigaaImports: importação válida/inválida +- ✅ Passwords: configuração com token válido/expirado + +**System/Feature Specs:** +- ✅ Login: credenciais corretas/incorretas +- ✅ Formulários: dados válidos/inválidos +- ✅ Permissions: acesso permitido/negado + +**Total de Specs:** 253 examples, 0 failures + +--- + +### 5️⃣ DOCUMENTAÇÃO RDOC - ✅ INICIADA + +**Status:** 16% documentado + +#### Estatísticas: +- **Classes:** 4 de 27 documentadas (15%) +- **Métodos:** 12 de 60 documentados (20%) +- **Constantes:** 0 de 4 documentadas +- **Atributos:** 0 de 9 documentados + +#### O Que Foi Documentado: + +**✅ SigaaImporter (Completo):** +- Classe principal com descrição e exemplo de uso +- `self.call` - método de classe +- `initialize` - inicializador +- `import` - método principal de importação +- `import_docente` - importação de docentes +- `import_dicente_and_matricula` - importação de alunos + +**✅ SigaaImporter::ImportResult (Completo):** +- Classe com descrição +- `initialize` - construtor +- `success?` - verificador +- `total_created`, `total_updated`, `total_skipped` - contadores +- `summary_message` - gerador de mensagem + +**✅ AvaliacoesController (Parcial):** +- Classe com descrição +- `index` - listagem +- `pendentes` - pendentes para aluno +- `submeter` - submissão de resposta + +**📁 Documentação Gerada:** +- Localização: `doc/rdoc/index.html` +- Formato: HTML (Darkfish) +- Acessível via navegador + +#### O Que Ainda Precisa Ser Documentado: + +**Prioridade ALTA:** +- 🔴 Models (23 classes sem documentação) +- 🔴 Métodos privados dos controllers refatorados +- 🔴 Demais Services (EvaluationBatchCreator, etc.) + +**Prioridade MÉDIA:** +- 🟡 Helpers +- 🟡 Mailers + +--- + +## 🛠️ TECNOLOGIAS E FERRAMENTAS UTILIZADAS + +### Gemas de Análise Instaladas: +- ✅ **RubyCritic** (4.11.0) - Análise geral de qualidade +- ✅ **Flog** (4.9.0) - ABC Score +- ✅ **SimpleCov** (0.22.0) - Cobertura de testes +- ✅ **RDoc** - Documentação +- ✅ **RuboCop** (via rubocop-rails-omakase) - Complexidade ciclomática +- ⚠️ **Saikuro** - Tentado mas incompatível com Ruby 3.4 + +### Ambiente: +- **Ruby:** 3.4.1 +- **Rails:** 8.1.1 +- **RSpec:** 6.1.0 +- **Database:** SQLite3 +- **Git Branch:** sprint-3 + +--- + +## 📋 SUMÁRIO DE MUDANÇAS + +### Arquivos Modificados: +1. `Gemfile` - Adicionadas gemas de análise +2. `spec/spec_helper.rb` - Configurado SimpleCov +3. `app/services/sigaa_importer.rb` - Refatoração massiva (**9 métodos** refatorados) +4. `app/controllers/avaliacoes_controller.rb` - Refatoração (2 métodos) +5. `app/controllers/passwords_controller.rb` - Refatoração (1 método) +6. `app/services/evaluation_results_exporter.rb` - Refatoração completa ✨ +7. `app/services/evaluation_batch_creator.rb` - Refatoração completa ✨ + +### Novos Métodos Criados (após refatoração): + +**SigaaImporter (20+ métodos):** +- `process_dicente`, `create_new_dicente`, `assign_dicente_attributes` +- `setup_dicente_activation`, `update_existing_dicente`, `dicente_needs_update?` +- `process_matricula`, `create_new_matricula`, `update_matricula_if_needed` +- `create_new_docente`, `assign_docente_attributes`, `setup_docente_activation` +- `update_existing_docente`, `docente_needs_update?` +- `find_or_create_materia`, `create_materia`, `update_materia_if_needed` +- `find_or_initialize_turma`, `create_new_turma`, `update_turma_if_needed` ✨ + +**AvaliacoesController (10 métodos):** +- `validate_submission_permissions`, `validate_not_already_submitted` +- `find_or_initialize_resposta`, `submit_resposta`, `save_submitted_resposta` +- `render_submission_error`, `validate_mandatory_questions` +- `save_all_resposta_items`, `valid_questao_for_avaliacao?` +- `skip_blank_optional_questao?`, `save_resposta_item` + +**PasswordsController (7 métodos):** +- `validate_user_and_token`, `validate_password_confirmation` +- `update_user_password`, `assign_new_password_attributes` +- `save_user_password`, `login_user_after_password_set`, `render_password_error` + +**EvaluationResultsExporter (6 métodos):** ✨ +- `validate_export_conditions`, `generate_csv_report` +- `add_csv_header`, `add_csv_rows` +- `add_questao_rows`, `build_csv_row` + +**EvaluationBatchCreator (6 métodos):** ✨ +- `error_result`, `load_template_and_turmas` +- `process_turmas`, `process_single_turma` +- `create_avaliacao_for_turma`, `handle_record_invalid` + +**Total:** ~50 novos métodos pequenos e focados (⬆️ +15 métodos) + +--- + +## 🎯 PRÓXIMOS PASSOS PARA 100% + +### Curto Prazo (Próximas Sessões): + +1. **Refatorar 9 Métodos Restantes (ABC >= 20):** 🔥 + - 🔴 Prioridade 1: `AvaliacoesController#responder` (33.6) + - 🔴 Prioridade 2: `SigaaImporter#import_docente_dicentes_and_matriculas` (30.3) + - 🟡 Prioridade 3: `TemplateQuestion#normalize_fields` (28.3) + - 🟡 Restantes: 6 métodos entre 20-24 + +2. **Expandir Documentação RDoc:** + - ✅ SigaaImporter e ImportResult documentados + - ✅ AvaliacoesController principais métodos + - 🔴 Documentar EvaluationResultsExporter (refatorado) ✨ + - 🔴 Documentar EvaluationBatchCreator (refatorado) ✨ + - 🔴 Documentar Models (23 classes) + - 🟡 Documentar métodos privados criados + - Meta: 16% → 60%+ + 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..7c7e4121d2 --- /dev/null +++ b/app/assets/stylesheets/application.css @@ -0,0 +1,1135 @@ +/* + * Base styles for the sprint 2 templates flow. + */ + +:root { + font-family: "Inter", system-ui, -apple-system, blinkmacsystemfont, "Segoe UI", sans-serif; + color: #1f2933; + background-color: #f6f7fb; +} + +body { + margin: 0; + background-color: #f6f7fb; + min-height: 100vh; +} + +.app-header { + background: #0d47a1; + color: #fff; + padding: 1rem; +} + +.app-header__content { + max-width: 1200px; + margin: 0 auto; + font-weight: 600; + letter-spacing: 0.08em; + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; +} + +.app-logo { + font-size: 1.1rem; + letter-spacing: 0.1em; +} + +.app-nav { + display: flex; + gap: 1rem; +} + +.app-nav__link { + color: rgba(255, 255, 255, 0.85); + text-decoration: none; + font-weight: 500; + padding-bottom: 0.2rem; + border-bottom: 2px solid transparent; +} + +.app-nav__link--active { + color: #fff; + border-color: #fff; +} + +.app-nav__link--logout { + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 0.375rem; + padding: 0.4rem 1rem; + transition: all 0.2s ease; + font-family: inherit; + font-size: 0.95rem; + cursor: pointer; +} + +.app-nav__link--logout:hover { + background: rgba(255, 255, 255, 0.1); + border-color: rgba(255, 255, 255, 0.5); +} + +.app-main { + max-width: 1200px; + margin: 0 auto; + padding: 2rem 1.5rem 3rem; +} + +.flash { + border-radius: 0.5rem; + padding: 0.9rem 1.2rem; + margin-bottom: 1rem; + font-weight: 500; +} + +.flash--notice { + background: #e3f8e0; + color: #0f6c2b; +} + +.flash--alert { + background: #fdecea; + color: #b42318; +} + +.templates-page__intro { + display: flex; + justify-content: space-between; + gap: 1rem; + align-items: flex-start; +} + +.templates-page__grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); + gap: 1.5rem; + margin-top: 1.5rem; +} + +.templates-form, +.templates-list { + background: #fff; + border-radius: 1rem; + padding: 1.5rem; + box-shadow: 0 10px 30px rgba(15, 23, 42, 0.05); +} + +.field { + display: flex; + flex-direction: column; + gap: 0.35rem; + margin-bottom: 1rem; +} + +.field__label { + font-size: 0.9rem; + color: #4b5563; +} + +.field__input, +.field select, +.field textarea { + border: 1px solid #d1d5db; + border-radius: 0.5rem; + padding: 0.65rem 0.75rem; + font-size: 1rem; + font-family: inherit; +} + +.field__input[type="file"] { + padding: 0.5rem; + cursor: pointer; +} + +.field__input[type="file"]:focus { + outline: 2px solid #1d4ed8; + outline-offset: 2px; +} + +.field--inline { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + gap: 1rem; +} + +.field--grow { + flex: 1; +} + +.checkbox { + display: flex; + gap: 0.5rem; + align-items: center; + font-size: 0.95rem; +} + +.checkbox--danger { + color: #b42318; +} + +.questions-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + margin: 1.5rem 0 1rem; +} + +.questions-list { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.question-card { + border: 1px solid #e5e7eb; + border-radius: 0.75rem; + padding: 1rem; + background: #fdfdfd; +} + +.question-card__header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.75rem; + font-weight: 500; +} + +.button { + border-radius: 999px; + border: none; + cursor: pointer; + font-weight: 600; + font-size: 0.95rem; + padding: 0.5rem 1.25rem; +} + +.button--primary { + background: #1d4ed8; + color: #fff; +} + +.button--ghost { + background: transparent; + color: #1d4ed8; + border: 1px solid #bcd1ff; +} + +.button--ghost:hover { + background: #eff6ff; +} + +.button--small { + padding: 0.35rem 0.85rem; + font-size: 0.85rem; +} + +.button--danger { + background: #dc2626; + color: #fff; +} + +.button--danger:hover { + background: #b91c1c; +} + +.form-banner { + background: #eff6ff; + border: 1px solid #bcd1ff; + border-radius: 0.5rem; + padding: 0.85rem 1rem; + margin-bottom: 1rem; + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; +} + +.form-banner p { + margin: 0; + color: #1e40af; + font-size: 0.9rem; +} + +.form-errors { + background: #fff5f5; + border: 1px solid #fecaca; + padding: 1rem; + border-radius: 0.75rem; + margin-bottom: 1rem; +} + +.form-actions { + margin-top: 1.5rem; + display: flex; + justify-content: flex-end; +} + +.templates-list__grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 1.25rem; +} + +.template-card { + border: 1px solid #e5e7eb; + border-radius: 1rem; + padding: 1rem; + background: #fafafa; +} + +.template-card__docente { + color: #4b5563; + font-size: 0.85rem; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.template-card__description { + color: #4b5563; + font-size: 0.95rem; +} + +.template-card__stats { + display: flex; + justify-content: space-between; + margin: 1rem 0; + font-size: 0.9rem; +} + +.template-card__stats dt { + color: #6b7280; +} + +.template-card__questions { + margin: 0.5rem 0 0; + padding-left: 1.25rem; +} + +.blank-state { + padding: 1rem; + background: #eef2ff; + border-radius: 0.75rem; + color: #3730a3; +} + +details summary { + cursor: pointer; + font-weight: 600; +} + +.meta-pill { + background: #e0e7ff; + color: #1d4ed8; + padding: 0.35rem 0.8rem; + border-radius: 999px; + font-size: 0.85rem; +} + +.results-page__intro { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; +} + +.results-filter { + display: flex; + gap: 1rem; + align-items: flex-end; + margin: 1.5rem 0; +} + +.results-filter__actions { + align-self: flex-end; +} + +.results-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 1rem; +} + +.results-card { + background: #fff; + border-radius: 0.75rem; + padding: 1.25rem; + border: 1px solid #e5e7eb; + box-shadow: 0 10px 30px rgba(15, 23, 42, 0.05); + display: flex; + flex-direction: column; + gap: 1rem; +} + +.results-card__semester { + text-transform: uppercase; + font-size: 0.85rem; + letter-spacing: 0.08em; + color: #6366f1; + margin: 0; +} + +.results-card__meta { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.75rem; + margin: 0; +} + +.results-card__meta dt { + font-size: 0.75rem; + text-transform: uppercase; + color: #6b7280; +} + +.results-card__meta dd { + margin: 0.15rem 0 0; + font-weight: 600; +} + +.results-show__header { + background: #fff; + border-radius: 1rem; + padding: 1.5rem; + box-shadow: 0 15px 35px rgba(15, 23, 42, 0.08); + display: flex; + justify-content: space-between; + gap: 2rem; + margin-bottom: 1.5rem; +} + +.results-show__stats { + display: flex; + gap: 1.25rem; + align-items: flex-end; +} + +.results-show__stats span { + display: block; + font-size: 0.85rem; + color: #6b7280; +} + +.results-show__stats strong { + font-size: 1.5rem; +} + +.results-back-link { + display: inline-flex; + align-items: center; + gap: 0.35rem; + color: #1d4ed8; + text-decoration: none; + margin-bottom: 1rem; +} + +.results-chart__grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 1rem; +} + +.question-stat { + background: #fff; + border-radius: 0.75rem; + padding: 1rem; + border: 1px solid #e5e7eb; +} + +.question-stat__row { + display: grid; + grid-template-columns: 70px 1fr 40px; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.progress-bar { + background: #eef2ff; + border-radius: 999px; + height: 8px; + overflow: hidden; +} + +.progress-bar span { + display: block; + height: 100%; + background: #6366f1; +} + +.question-stat__value { + text-align: right; + font-weight: 600; +} + +.results-show__semester { + text-transform: uppercase; + letter-spacing: 0.1em; + color: #818cf8; + margin: 0 0 0.25rem; +} + +.evaluations-page__intro { + display: flex; + justify-content: space-between; + gap: 1rem; + align-items: center; +} + +.evaluations-grid { + display: grid; + grid-template-columns: minmax(320px, 1fr) minmax(280px, 0.8fr); + gap: 1.5rem; + margin-top: 1.5rem; +} + +.evaluations-form, +.evaluations-list { + background: #fff; + border-radius: 1rem; + padding: 1.5rem; + box-shadow: 0 10px 30px rgba(15, 23, 42, 0.05); +} + +.evaluations-actions { + display: flex; + gap: 0.5rem; +} + +.helper-text { + color: #6b7280; + margin: 0.25rem 0 0; +} + +.turma-list { + display: flex; + flex-direction: column; + gap: 0.75rem; + max-height: 340px; + overflow-y: auto; + padding-right: 0.25rem; +} + +.turma-card { + display: flex; + gap: 0.75rem; + border: 1px solid #e5e7eb; + border-radius: 0.75rem; + padding: 0.85rem; + align-items: flex-start; + cursor: pointer; + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.turma-card:hover { + border-color: #93c5fd; + box-shadow: 0 6px 18px rgba(59, 130, 246, 0.15); +} + +.turma-card__checkbox { + margin-top: 0.35rem; +} + +.turma-card__title { + margin: 0; + font-weight: 600; +} + +.turma-card__meta { + margin: 0.25rem 0 0; + color: #6b7280; + font-size: 0.9rem; +} + +.evaluations-list__grid { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.evaluation-card { + border: 1px solid #e5e7eb; + border-radius: 0.75rem; + padding: 1rem; + background: #fafafa; +} + +.evaluation-card__template { + text-transform: uppercase; + color: #6366f1; + font-size: 0.85rem; + letter-spacing: 0.08em; + margin: 0 0 0.25rem; +} + +.evaluation-card__stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + gap: 0.75rem; + margin: 1rem 0 0; +} + +.evaluation-card__stats dt { + font-size: 0.8rem; + color: #6b7280; +} + +.evaluation-card__stats dd { + margin: 0.2rem 0 0; + font-weight: 600; +} + +/* Pending Forms Page */ +.pending-forms-page__intro { + display: flex; + justify-content: space-between; + gap: 1rem; + align-items: center; + margin-bottom: 1.5rem; +} + +.pending-forms-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 1.25rem; +} + +.pending-form-card { + background: #fff; + border-radius: 1rem; + padding: 1.25rem; + border: 1px solid #e5e7eb; + box-shadow: 0 10px 30px rgba(15, 23, 42, 0.05); + display: flex; + flex-direction: column; + gap: 1rem; + transition: box-shadow 0.2s ease, border-color 0.2s ease; +} + +.pending-form-card:hover { + box-shadow: 0 15px 40px rgba(15, 23, 42, 0.1); +} + +.pending-form-card--urgent { + border-color: #fbbf24; + background: #fffbeb; +} + +.pending-form-card--overdue { + border-color: #f87171; + background: #fef2f2; +} + +.pending-form-card__template { + text-transform: uppercase; + color: #6366f1; + font-size: 0.85rem; + letter-spacing: 0.08em; + margin: 0 0 0.25rem; +} + +.pending-form-card__meta { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.85rem; + margin: 0; +} + +.pending-form-card__meta dt { + font-size: 0.8rem; + color: #6b7280; + margin-bottom: 0.2rem; +} + +.pending-form-card__meta dd { + margin: 0; + font-weight: 600; + font-size: 0.95rem; +} + +.pending-form-card__actions { + display: flex; + justify-content: flex-end; + margin-top: 0.5rem; +} + +.due-badge { + display: inline-block; + padding: 0.2rem 0.6rem; + border-radius: 999px; + font-size: 0.75rem; + font-weight: 600; + margin-left: 0.5rem; +} + +.due-badge--urgent { + background: #fef3c7; + color: #92400e; +} + +.due-badge--overdue { + background: #fee2e2; + color: #991b1b; +} + +@media (max-width: 1024px) { + .evaluations-grid { + grid-template-columns: 1fr; + } + + .pending-forms-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 768px) { + .templates-page__intro { + flex-direction: column; + } + + .templates-page__grid { + grid-template-columns: 1fr; + } + + .results-filter { + flex-direction: column; + align-items: stretch; + } + + .results-show__header { + flex-direction: column; + } + + .results-show__stats { + flex-direction: column; + align-items: flex-start; + } + + .results-card__meta { + grid-template-columns: 1fr; + } +} + +/* SIGAA Imports Page */ +.sigaa-imports-page__intro { + display: flex; + justify-content: space-between; + gap: 1rem; + align-items: flex-start; + margin-bottom: 1.5rem; +} + +.sigaa-imports-form { + background: #fff; + border-radius: 1rem; + padding: 1.5rem; + box-shadow: 0 10px 30px rgba(15, 23, 42, 0.05); + max-width: 600px; + margin-top: 1.5rem; +} + +.sigaa-imports-form h2 { + margin: 0 0 1.25rem; + font-size: 1.25rem; +} + +.sigaa-imports-page__intro h1 { + margin: 0 0 0.5rem; +} + +.sigaa-imports-page__intro p { + margin: 0; + color: #6b7280; +} + +.sigaa-imports-page__meta { + display: flex; + align-items: center; +} + +.sigaa-imports-page__actions { + display: flex; + gap: 1rem; + margin-bottom: 1.5rem; + flex-wrap: wrap; +} + +.sigaa-imports-page__update-form { + display: inline-block; +} + +.info-banner { + background: #fef3c7; + border-left: 4px solid #f59e0b; + padding: 1rem 1.25rem; + border-radius: 0.5rem; + margin-bottom: 1.5rem; +} + +.info-banner p { + margin: 0; + font-size: 0.9rem; + color: #92400e; + line-height: 1.6; +} + +.info-banner code { + background: #fde68a; + padding: 0.15rem 0.35rem; + border-radius: 0.25rem; + font-size: 0.85em; + font-family: 'Monaco', 'Courier New', monospace; +} + +.sigaa-imports-page__stats { + background: #fff; + border-radius: 1rem; + padding: 1.5rem; + box-shadow: 0 10px 30px rgba(15, 23, 42, 0.05); +} + +.sigaa-imports-page__stats h2 { + margin: 0 0 1.25rem; + font-size: 1.25rem; +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 1rem; +} + +.stat-card { + border: 1px solid #e5e7eb; + border-radius: 0.75rem; + padding: 1rem; + background: #fafafa; +} + +.stat-card header { + margin-bottom: 0.5rem; +} + +.stat-card h3 { + margin: 0; + font-size: 0.9rem; + color: #6b7280; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.stat-card__value { + margin: 0; + font-size: 2rem; + font-weight: 700; + color: #1d4ed8; +} + +/* Answer Form Page */ +.answer-form-page__header { + margin-bottom: 2rem; +} + +.answer-form-page__meta { + color: #6b7280; + font-size: 0.95rem; + margin: 0.5rem 0; +} + +.answer-form-page__due-date { + color: #6b7280; + font-size: 0.9rem; + margin: 0.25rem 0 0; +} + +.answer-form { + background: #fff; + border-radius: 1rem; + padding: 2rem; + box-shadow: 0 10px 30px rgba(15, 23, 42, 0.05); +} + +.questions-container { + display: flex; + flex-direction: column; + gap: 2rem; + margin-bottom: 2rem; +} + +.question-block { + border-bottom: 1px solid #e5e7eb; + padding-bottom: 1.5rem; +} + +.question-block:last-child { + border-bottom: none; +} + +.question-header h3 { + margin: 0 0 1rem; + font-size: 1.1rem; + color: #1f2933; +} + +.required-badge { + color: #dc2626; + font-weight: bold; + margin-left: 0.25rem; +} + +.question-answer { + margin-top: 1rem; +} + +.likert-scale { + display: flex; + gap: 1rem; + justify-content: space-between; + margin-bottom: 0.5rem; +} + +.likert-option { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + cursor: pointer; + padding: 0.75rem; + border-radius: 0.5rem; + transition: background-color 0.2s ease; + flex: 1; +} + +.likert-option:hover { + background-color: #f3f4f6; +} + +.likert-option input[type="radio"] { + margin: 0; + cursor: pointer; +} + +.likert-option input[type="radio"]:checked + .likert-value { + color: #1d4ed8; + font-weight: 600; +} + +.likert-value { + font-size: 1.25rem; + font-weight: 500; + color: #6b7280; + transition: color 0.2s ease; +} + +.likert-labels { + display: flex; + justify-content: space-between; + font-size: 0.85rem; + color: #9ca3af; + margin-top: 0.5rem; +} + +.multiple-choice-options { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.choice-option { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem; + border: 1px solid #e5e7eb; + border-radius: 0.5rem; + cursor: pointer; + transition: all 0.2s ease; +} + +.choice-option:hover { + background-color: #f9fafb; + border-color: #93c5fd; +} + +.choice-option input[type="radio"] { + margin: 0; + cursor: pointer; +} + +.choice-option input[type="radio"]:checked ~ span { + color: #1d4ed8; + font-weight: 600; +} + +.choice-option input[type="radio"]:checked { + accent-color: #1d4ed8; +} + +.text-answer textarea { + width: 100%; + min-height: 120px; + resize: vertical; +} + +@media (max-width: 768px) { + .sigaa-imports-page__intro { + flex-direction: column; + } + + .sigaa-imports-page__actions { + flex-direction: column; + } + + .sigaa-imports-page__update-form { + width: 100%; + } + + .sigaa-imports-page__update-form .button { + width: 100%; + } + + .stats-grid { + grid-template-columns: 1fr; + } + + .likert-scale { + flex-wrap: wrap; + gap: 0.5rem; + } + + .likert-option { + flex: 0 0 calc(20% - 0.4rem); + min-width: 50px; + } +} + +/* API Simulation Overlay */ +.api-simulation-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.6); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; + backdrop-filter: blur(4px); +} + +.api-simulation-modal { + background: #fff; + border-radius: 1rem; + padding: 2rem; + max-width: 500px; + width: 90%; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + animation: slideIn 0.3s ease-out; +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(-20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.api-simulation-header { + margin-bottom: 1.5rem; +} + +.api-simulation-header h3 { + margin: 0; + font-size: 1.25rem; + color: #1f2933; +} + +.api-simulation-steps { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.api-step { + display: flex; + align-items: center; + gap: 1rem; + padding: 1rem; + border-radius: 0.75rem; + transition: all 0.3s ease; +} + +.api-step--pending { + background: #f9fafb; + opacity: 0.6; +} + +.api-step--active { + background: #eff6ff; + border: 2px solid #3b82f6; + box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2); +} + +.api-step--complete { + background: #f0fdf4; + border: 2px solid #22c55e; +} + +.api-step__icon { + font-size: 1.5rem; + flex-shrink: 0; +} + +.api-step__content { + flex: 1; +} + +.api-step__title { + font-weight: 600; + color: #1f2933; + margin-bottom: 0.25rem; +} + +.api-step__description { + font-size: 0.85rem; + color: #6b7280; +} + +.api-step__status { + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + font-size: 1rem; +} + +.api-step--complete .api-step__status { + color: #22c55e; + font-weight: bold; +} + +/* Spinner */ +.spinner { + width: 20px; + height: 20px; + border: 3px solid #e5e7eb; + border-top-color: #3b82f6; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb new file mode 100644 index 0000000000..2b6ba7fee1 --- /dev/null +++ b/app/controllers/application_controller.rb @@ -0,0 +1,61 @@ +## +# Base controller for all application controllers. +# Provides authentication and user session management functionality. +# +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 + + before_action :require_login + + helper_method :current_user, :logged_in? + + private + + ## + # Returns the currently logged in user. + # + # ==== Returns + # * Usuario - The current user object if logged in, nil otherwise + # + # ==== Side Effects + # * None - This is a read-only method that caches the result in @current_user + # + def current_user + @current_user ||= Usuario.find_by(id: session[:user_id]) if session[:user_id] + end + + ## + # Checks if a user is currently logged in. + # + # ==== Returns + # * Boolean - true if a user is logged in, false otherwise + # + # ==== Side Effects + # * None + # + def logged_in? + current_user.present? + end + + ## + # Requires user to be logged in before accessing controller actions. + # Redirects to login page if user is not authenticated. + # + # ==== Returns + # * Redirects to login_path if not logged in + # + # ==== Side Effects + # * Sets flash[:alert] with error message + # * Redirects to login_path if user is not logged in + # + def require_login + unless logged_in? + flash[:alert] = "Por favor, faça login para continuar" + redirect_to login_path + end + end +end diff --git a/app/controllers/avaliacoes_controller.rb b/app/controllers/avaliacoes_controller.rb new file mode 100644 index 0000000000..57464d5427 --- /dev/null +++ b/app/controllers/avaliacoes_controller.rb @@ -0,0 +1,322 @@ +require "ostruct" + +## +# Controller for managing evaluations (Avaliacoes). +# Handles listing pending evaluations, creating evaluation batches, +# and submitting student responses. +# +class AvaliacoesController < ApplicationController + before_action :load_collections, only: %i[index create] + before_action :build_form_object, only: :index + + ## + # Lists all evaluations (admin view). + # + # ==== Returns + # * Renders index view with evaluation form and list + # + def index; end + + ## + # Lists pending evaluations for the current student. + # + # ==== Returns + # * Renders pendentes view with list of pending evaluations for the current dicente + # + def pendentes + if current_user&.dicente? + @avaliacoes_pendentes = Avaliacao.pending_for_dicente(current_user) + .includes(:turma, :docente, :template, turma: :materia) + .order("avaliacoes.due_date ASC") + .distinct + else + @avaliacoes_pendentes = [] + end + end + + ## + # Displays the form for a student to respond to an evaluation. + # + # ==== Parameters + # * +id+ - Avaliacao ID (from params) + # + # ==== Returns + # * Renders responder view with the evaluation form + # * Redirects to formularios_pendentes_path if user lacks permission + # + # ==== Side Effects + # * Sets @avaliacao, @questoes, @resposta instance variables + # + def responder + @avaliacao = Avaliacao.find(params[:id]) + + return unless validate_responder_permission + return unless validate_enrollment_in_turma + return unless validate_not_already_responded + + load_responder_data + end + + private + + def validate_responder_permission + return true if current_user.dicente? + + redirect_with_alert("Você não tem permissão para responder este formulário") + false + end + + def validate_enrollment_in_turma + return true if current_user.turmas.include?(@avaliacao.turma) + + redirect_with_alert("Você não tem permissão para responder este formulário") + false + end + + def validate_not_already_responded + @resposta_existente = Resposta.find_by(avaliacao: @avaliacao, dicente: current_user) + return true unless @resposta_existente&.submitted? + + redirect_with_alert("Você já respondeu este formulário") + false + end + + def redirect_with_alert(message) + flash[:alert] = message + redirect_to formularios_pendentes_path + end + + def load_responder_data + @questoes = @avaliacao.questoes.order(:position) + @resposta = @resposta_existente || Resposta.new(avaliacao: @avaliacao, dicente: current_user, status: :pending) + end + + public + + ## + # Submits a student's response to an evaluation. + # + # ==== Parameters + # * +id+ - Avaliacao ID (from params) + # * +respostas+ - Hash of responses indexed by questao_id (from params) + # + # ==== Returns + # * Redirects to pendentes path on success + # * Renders responder form with errors on failure + # + # ==== Side Effects + # * Creates or updates Resposta record + # * Creates RespostaItem records for each question + # * Validates mandatory questions are answered + # + def submeter + @avaliacao = Avaliacao.find(params[:id]) + + return unless validate_submission_permissions + return unless validate_not_already_submitted + + @resposta = find_or_initialize_resposta + submit_resposta + end + + ## + # Refactored validation: checks if user is a dicente + # + def validate_submission_permissions + validate_responder_permission && validate_enrollment_in_turma + end + + ## + # Checks if the current user has already submitted a response + # + def validate_not_already_submitted + resposta_existente = Resposta.find_by(avaliacao: @avaliacao, dicente: current_user) + + if resposta_existente&.submitted? + redirect_with_alert("Você já respondeu este formulário") + return false + end + + @resposta_existente = resposta_existente + true + end + + ## + # Finds existing response or initializes a new one for the current student. + # + # ==== Returns + # * Resposta - Existing response or new Resposta instance + # + def find_or_initialize_resposta + @resposta_existente || Resposta.new(avaliacao: @avaliacao, dicente: current_user) + end + + ## + # Processes and submits the student's response. + # + # ==== Side Effects + # * Calls save_submitted_resposta on success + # * Calls render_submission_error on failure + # + def submit_resposta + if processar_respostas(@resposta) + save_submitted_resposta + else + render_submission_error + end + end + + ## + # Saves the submitted response with status and timestamp. + # + # ==== Side Effects + # * Updates resposta status to :submitted + # * Sets submitted_at timestamp + # * Redirects to formularios_pendentes_path on success + # * Calls render_submission_error on failure + # + def save_submitted_resposta + @resposta.status = :submitted + @resposta.submitted_at = Time.current + + if @resposta.save + flash[:notice] = "Avaliação enviada com sucesso!" + redirect_to formularios_pendentes_path + else + render_submission_error + end + end + + ## + # Renders the response form with error messages. + # + # ==== Side Effects + # * Sets @questoes instance variable + # * Sets flash[:alert] with error message + # * Renders :responder view with unprocessable_entity status + # + def render_submission_error + @questoes = @avaliacao.questoes.order(:position) + error_message = @resposta.errors.full_messages.to_sentence.presence || + "Erro ao enviar o formulário. Verifique se todas as perguntas obrigatórias foram respondidas." + flash.now[:alert] = error_message + render :responder, status: :unprocessable_entity + end + + ## + # Creates evaluation batches for multiple turmas. + # + # ==== Parameters + # * +avaliacao_batch+ - Hash with template_id, due_date, turma_ids + # + # ==== Side Effects + # * Creates Avaliacao records for each selected turma + # + def create + @avaliacao_batch = OpenStruct.new(batch_params.to_h) + result = execute_batch_creation + + handle_batch_result(result) + end + + private + + def execute_batch_creation + EvaluationBatchCreator.call(**batch_params.to_h.symbolize_keys) + end + + def handle_batch_result(result) + if result.success? + redirect_to avaliacoes_path, notice: success_message(result) + else + flash.now[:alert] = result.errors.to_sentence + render :index, status: :unprocessable_entity + end + end + + def batch_params + params.require(:avaliacao_batch).permit(:template_id, :due_date, turma_ids: []) + rescue ActionController::ParameterMissing + { template_id: nil, due_date: nil, turma_ids: [] } + end + + def load_collections + @current_semester = current_semester + @templates = Template.includes(:docente).order(:name) + @turmas = Turma.includes(:materia, :docente).where(semester: @current_semester).order(:class_code) + @avaliacoes = Avaliacao.includes(:turma, :docente, :template).order(created_at: :desc) + end + + def build_form_object + @avaliacao_batch = OpenStruct.new(template_id: nil, due_date: default_due_date) + end + + def default_due_date + Time.zone.today.end_of_month + end + + def current_semester + date = Time.zone.today + term = date.month <= 6 ? 1 : 2 + format("%d.%d", year: date.year, term: term) + end + + def success_message(result) + created = result.created.size + skipped = result.skipped.size + "#{created} formulário(s) criado(s) e #{skipped} turma(s) ignoradas por já possuírem o formulário." + end + + def processar_respostas(resposta) + return false unless params[:respostas].present? + return false unless validate_mandatory_questions(resposta) + + save_all_resposta_items(resposta) + end + + def validate_mandatory_questions(resposta) + questoes_obrigatorias = @avaliacao.questoes.where(mandatory: true) + + questoes_obrigatorias.each do |questao| + valor = params[:respostas][questao.id.to_s] + if valor.blank? + resposta.errors.add(:base, "A pergunta '#{questao.prompt}' é obrigatória") + return false + end + end + + true + end + + def save_all_resposta_items(resposta) + params[:respostas].each do |questao_id, valor| + questao = Questao.find_by(id: questao_id) + next unless valid_questao_for_avaliacao?(questao) + next if skip_blank_optional_questao?(questao, valor) + + return false unless save_resposta_item(resposta, questao, valor) + end + + true + end + + def valid_questao_for_avaliacao?(questao) + questao && questao.avaliacao == @avaliacao + end + + def skip_blank_optional_questao?(questao, valor) + valor.blank? && !questao.mandatory + end + + def save_resposta_item(resposta, questao, valor) + resposta_item = resposta.resposta_items.find_or_initialize_by(questao: questao) + resposta_item.valor = valor.to_s + + unless resposta_item.save + resposta.errors.add(:base, "Erro ao salvar resposta para '#{questao.prompt}'") + return false + end + + true + 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/passwords_controller.rb b/app/controllers/passwords_controller.rb new file mode 100644 index 0000000000..e33d72d902 --- /dev/null +++ b/app/controllers/passwords_controller.rb @@ -0,0 +1,222 @@ +# frozen_string_literal: true + +## +# Controller for password setup and reset functionality. +# Handles password configuration for new users and password resets. +# +class PasswordsController < ApplicationController + skip_before_action :require_login + + ## + # Displays the password setup form for new users. + # + # ==== Parameters + # * +params[:token]+ - String with password reset token + # + # ==== Returns + # * Renders new view with password setup form + # * Redirects to request_new_password_path if token is invalid or expired + # + # ==== Side Effects + # * Sets @token and @user instance variables + # * Sets flash[:alert] and redirects if token is invalid + # + def new + @token = params[:token] + @user = Usuario.find_by(password_reset_token: @token) + + if @user.nil? || token_expired?(@user) + flash[:alert] = "Link de configuração de senha é inválido ou expirado" + redirect_to request_new_password_path + end + end + + ## + # Processes password setup submission. + # + # ==== Parameters + # * +params[:token]+ - String with password reset token + # * +params[:password]+ - String with new password + # * +params[:password_confirmation]+ - String with password confirmation + # + # ==== Returns + # * Redirects to appropriate page on success + # * Renders new view with errors on failure + # + # ==== Side Effects + # * Updates user password in database + # * Clears password_reset_token and pending_activation + # * Creates user session on success + # * Sets flash messages + # + def create + @user = Usuario.find_by(password_reset_token: params[:token]) + + return unless validate_user_and_token + return unless validate_password_confirmation + + update_user_password + end + + ## + # Validates that user exists and token is valid. + # + # ==== Returns + # * Boolean - true if valid, false otherwise + # + # ==== Side Effects + # * Sets flash[:alert] and redirects if invalid + # + def validate_user_and_token + if @user.nil? || token_expired?(@user) + flash[:alert] = "Link de configuração de senha é inválido ou expirado" + redirect_to request_new_password_path + return false + end + true + end + + ## + # Validates that password and confirmation match. + # + # ==== Returns + # * Boolean - true if passwords match, false otherwise + # + # ==== Side Effects + # * Sets @token instance variable + # * Sets flash.now[:alert] and renders form if passwords don't match + # + def validate_password_confirmation + if params[:password] != params[:password_confirmation] + @token = params[:token] + flash.now[:alert] = "A confirmação de senha não corresponde" + render :new, status: :unprocessable_entity + return false + end + true + end + + ## + # Updates user password attributes and saves. + # + # ==== Returns + # * Redirects on success or renders error on failure + # + # ==== Side Effects + # * Calls assign_new_password_attributes and save_user_password + # + def update_user_password + assign_new_password_attributes + save_user_password + end + + ## + # Assigns new password attributes to user object. + # + # ==== Side Effects + # * Updates @user.password, pending_activation, password_reset_token, password_reset_sent_at + # + def assign_new_password_attributes + @user.password = params[:password] + @user.pending_activation = false + @user.password_reset_token = nil + @user.password_reset_sent_at = nil + end + + ## + # Saves user password and handles result. + # + # ==== Returns + # * Redirects on success or renders error on failure + # + # ==== Side Effects + # * Saves user to database + # * Creates session on success + # * Renders error form on failure + # + def save_user_password + if @user.save + login_user_after_password_set + else + render_password_error + end + end + + ## + # Logs in user after successful password setup. + # + # ==== Side Effects + # * Sets session[:user_id] + # * Sets flash[:notice] + # * Redirects to appropriate page based on user type + # + def login_user_after_password_set + session[:user_id] = @user.id + flash[:notice] = "Senha definida com sucesso! Você está conectado." + redirect_to_appropriate_page(@user) + end + + ## + # Renders password form with error messages. + # + # ==== Side Effects + # * Sets @token instance variable + # * Sets flash.now[:alert] with error messages + # * Renders new view with unprocessable_entity status + # + def render_password_error + @token = params[:token] + flash.now[:alert] = @user.errors.full_messages.join(", ") + render :new, status: :unprocessable_entity + end + + ## + # Placeholder for requesting a new password reset link. + # + # ==== Returns + # * Renders request_new view + # + def request_new + # Placeholder para permitir solicitar novo link + end + + private + + ## + # Checks if password reset token has expired. + # + # ==== Parameters + # * +user+ - Usuario object + # + # ==== Returns + # * Boolean - true if token is older than 24 hours, false otherwise + # + def token_expired?(user) + return false if user.password_reset_sent_at.nil? + user.password_reset_sent_at < 24.hours.ago + end + + ## + # Redirects user to appropriate page based on their role. + # + # ==== Parameters + # * +user+ - Usuario object + # + # ==== Returns + # * Redirects to avaliacoes_path for dicentes + # * Redirects to templates_path for docentes + # * Redirects to root_path for other types + # + # ==== Side Effects + # * Performs HTTP redirect + # + def redirect_to_appropriate_page(user) + if user.dicente? + redirect_to avaliacoes_path + elsif user.docente? + redirect_to templates_path + else + redirect_to root_path + end + end +end diff --git a/app/controllers/resultados_controller.rb b/app/controllers/resultados_controller.rb new file mode 100644 index 0000000000..9a8f965ef9 --- /dev/null +++ b/app/controllers/resultados_controller.rb @@ -0,0 +1,160 @@ +## +# Controller for viewing and exporting evaluation results. +# Handles displaying evaluation statistics and exporting data to CSV. +# +class ResultadosController < ApplicationController + before_action :load_avaliacao, only: %i[show export] + + ## + # Lists all evaluations with optional search and semester filtering. + # + # ==== Parameters + # * +params[:q]+ - String with search query (optional) + # * +params[:semester]+ - String with semester filter (optional) + # + # ==== Returns + # * Renders index view with filtered evaluations list + # + # ==== Side Effects + # * Sets @query, @semester_filter, @avaliacoes, @search_performed, @semester_options instance variables + # + def index + @query = params[:q].to_s.strip + @semester_filter = params[:semester].presence + @avaliacoes = filtered_avaliacoes + @search_performed = @query.present? + @semester_options = semester_filters + end + + ## + # Displays detailed results for a specific evaluation. + # + # ==== Parameters + # * +params[:id]+ - Integer with Avaliacao ID + # + # ==== Returns + # * Renders show view with evaluation summary statistics + # + # ==== Side Effects + # * Sets @avaliacao and @summary instance variables + # + def show + @summary = EvaluationResultAggregator.new(@avaliacao).summary + end + + ## + # Exports evaluation results to CSV format. + # + # ==== Parameters + # * +params[:id]+ - Integer with Avaliacao ID + # + # ==== Returns + # * Sends CSV file as download attachment on success + # * Redirects to resultado_path with error message on failure + # + # ==== Side Effects + # * Generates CSV file with evaluation responses + # * Sets flash[:alert] on error + # + def export + exporter = EvaluationResultsExporter.new(@avaliacao) + send_data exporter.call, + type: "text/csv", + disposition: "attachment", + filename: exporter.filename + rescue EvaluationResultsExporter::ExportError => e + redirect_to resultado_path(@avaliacao), alert: export_error_message(e) + rescue StandardError + redirect_to resultado_path(@avaliacao), alert: "Não foi possível gerar o arquivo. Tente novamente mais tarde." + end + + private + + ## + # Filters evaluations based on search query and semester. + # + # ==== Returns + # * ActiveRecord::Relation - Filtered collection of Avaliacao records + # + # ==== Side Effects + # * None - This is a query method + # + def filtered_avaliacoes + scope = Avaliacao.includes(:turma, :docente).order(created_at: :desc) + scope = scope.where(turma: { semester: @semester_filter }) if @semester_filter + return scope unless @query.present? + + query = "%#{@query.downcase}%" + scope.left_joins(:docente).where( + "LOWER(avaliacoes.title) LIKE :q OR LOWER(usuarios.nome) LIKE :q", + q: query + ) + end + + ## + # Loads the avaliacao from database and handles not found cases. + # + # ==== Parameters + # * +params[:id]+ - Integer with Avaliacao ID + # + # ==== Returns + # * nil - Sets @avaliacao instance variable + # + # ==== Side Effects + # * Sets @avaliacao instance variable + # * Redirects to resultados_path with error message if not found + # + def load_avaliacao + @avaliacao = Avaliacao.includes(:turma, :docente, questoes: { resposta_items: :resposta }).find_by(id: params[:id]) + return if @avaliacao + + redirect_to resultados_path, alert: "O formulário solicitado não foi encontrado" + end + + ## + # Generates semester filter options for the view. + # + # ==== Returns + # * Array - Array of arrays with ["Label", "value"] format + # + # ==== Side Effects + # * None + # + def semester_filters + current = current_semester + [ [ "Todos", "" ], [ current, current ] ] + end + + ## + # Calculates the current semester based on today's date. + # + # ==== Returns + # * String - Semester in format "YYYY.T" (e.g., "2024.1" or "2024.2") + # + # ==== Side Effects + # * None + # + def current_semester + date = Time.zone.today + term = date.month <= 6 ? 1 : 2 + format("%d.%d", year: date.year, term: term) + end + + ## + # Generates error message for export failures. + # + # ==== Parameters + # * +error+ - Exception object (usually ExportError) + # + # ==== Returns + # * String - Error message to display to user + # + # ==== Side Effects + # * None + # + def export_error_message(error) + return error.message if error.message == "Ainda não há respostas disponíveis" + + "Não foi possível gerar o arquivo. Tente novamente mais tarde." + end +end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb new file mode 100644 index 0000000000..779dc62df6 --- /dev/null +++ b/app/controllers/sessions_controller.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +## +# Controller for handling user authentication sessions. +# Manages login, logout, and session management. +# +class SessionsController < ApplicationController + skip_before_action :require_login, only: [ :new, :create ] + + ## + # Displays the login form. + # + # ==== Returns + # * Renders new view with login form + # * Redirects to appropriate page if already logged in + # + # ==== Side Effects + # * Redirects to appropriate page based on user type if already logged in + # + def new + redirect_to_appropriate_page(current_user) if logged_in? + end + + ## + # Authenticates user and creates a session. + # + # ==== Parameters + # * +params[:email]+ - String with user email + # * +params[:password]+ - String with user password + # + # ==== Returns + # * Redirects to appropriate page on success + # * Renders new view with error on failure + # + # ==== Side Effects + # * Sets session[:user_id] on successful authentication + # * Sets flash[:alert] on failure or if account is pending activation + # * Redirects to login_path if account is pending activation + # + def create + user = find_user_by_email + + return render_invalid_credentials unless user&.authenticate(params[:password]) + return render_pending_activation if pending_activation?(user) + + login_user(user) + end + + ## + # Destroys the current session and logs out the user. + # + # ==== Returns + # * Redirects to login_path with success notice + # + # ==== Side Effects + # * Clears session[:user_id] + # * Sets flash[:notice] with logout confirmation message + # + def destroy + session[:user_id] = nil + redirect_to login_path, notice: "Sessão encerrada com sucesso" + end + + private + + def find_user_by_email + Usuario.find_by(email: params[:email]) + end + + def pending_activation?(user) + user.respond_to?(:pending_activation) && user.pending_activation + end + + def render_invalid_credentials + flash.now[:alert] = "Invalid email or password" + render :new, status: :unprocessable_entity + end + + def render_pending_activation + flash[:alert] = "Por favor, ative sua conta primeiro" + redirect_to login_path + end + + def login_user(user) + session[:user_id] = user.id + redirect_to_appropriate_page(user) + end + + ## + # Redirects user to the appropriate page based on their role. + # + # ==== Parameters + # * +user+ - Usuario object (can be Dicente or Docente) + # + # ==== Returns + # * Redirects to formularios_pendentes_path for dicentes + # * Redirects to templates_path for docentes + # * Redirects to root_path for other user types + # + # ==== Side Effects + # * Performs HTTP redirect + # + def redirect_to_appropriate_page(user) + if user.dicente? + redirect_to formularios_pendentes_path + elsif user.docente? + redirect_to templates_path + else + redirect_to root_path + end + end +end diff --git a/app/controllers/sigaa_imports_controller.rb b/app/controllers/sigaa_imports_controller.rb new file mode 100644 index 0000000000..b868294fbb --- /dev/null +++ b/app/controllers/sigaa_imports_controller.rb @@ -0,0 +1,158 @@ +# app/controllers/sigaa_imports_controller.rb +## +# Controller for importing data from SIGAA JSON files. +# Handles file uploads and database updates from SIGAA system. +# +class SigaaImportsController < ApplicationController + before_action :require_admin + + ## + # Displays the form for uploading SIGAA import files. + # + # ==== Returns + # * Renders new view with import form + # + # ==== Side Effects + # * None + # + def new + # Renderiza formulário de importação + end + + ## + # Lists import history and status. + # + # ==== Returns + # * Renders index view with import history + # + # ==== Side Effects + # * None + # + def index + # Renderiza página de listagem/histórico de importações + end + + ## + # Processes uploaded SIGAA files and imports data into the database. + # + # ==== Parameters + # * +params[:classes_file]+ - UploadedFile with classes JSON data + # * +params[:class_members_file]+ - UploadedFile with class members JSON data + # + # ==== Returns + # * Redirects to sigaa_imports_path on success + # * Renders new view with errors on failure + # + # ==== Side Effects + # * Creates/updates records in database (Materia, Turma, Docente, Dicente, Matricula) + # * Sends password setup emails to newly created users + # * Sets flash[:notice] on success or flash[:alert] on failure + # + def create + result = SigaaImporter.call( + classes_file: params[:classes_file], + class_members_file: params[:class_members_file], + operation_type: "Importação" + ) + + if result.success? + flash[:notice] = result.summary_message + redirect_to sigaa_imports_path + else + flash.now[:alert] = result.errors.to_sentence + render :new, status: :unprocessable_entity + end + end + + ## + # Updates database using JSON files from the repository root. + # + # ==== Returns + # * Redirects to sigaa_imports_path + # + # ==== Side Effects + # * Reads classes.json and class_members.json from repository root + # * Creates/updates records in database + # * Sets flash[:notice] on success or flash[:alert] on failure + # + def update_database + return unless validate_json_files_exist + + execute_database_update + end + + private + + ## + # Validates that required JSON files exist in the repository. + # + def validate_json_files_exist + unless json_files_present? + flash[:alert] = json_files_missing_message + redirect_to sigaa_imports_path + return false + end + true + end + + ## + # Checks if both JSON files are present. + # + def json_files_present? + File.exist?(classes_json_path) && File.exist?(members_json_path) + end + + def classes_json_path + Rails.root.join("classes.json") + end + + def members_json_path + Rails.root.join("class_members.json") + end + + def json_files_missing_message + "Arquivos JSON não encontrados no repositório. Certifique-se de que classes.json e class_members.json estão na raiz do projeto." + end + + ## + # Executes the database update and handles result. + # + def execute_database_update + result = SigaaImporter.call( + classes_file: classes_json_path.to_s, + class_members_file: members_json_path.to_s, + operation_type: "Atualização" + ) + + handle_update_result(result) + redirect_to sigaa_imports_path + end + + ## + # Sets flash message based on update result. + # + def handle_update_result(result) + if result.success? + flash[:notice] = result.summary_message + else + flash[:alert] = "Erro durante atualização: #{result.errors.join(', ')}" + end + end + + ## + # Requires current user to be an admin. + # + # ==== Returns + # * Redirects to root_path if user is not admin + # + # ==== Side Effects + # * Sets flash[:alert] with access denied message + # * Redirects to root_path if user is not admin + # + def require_admin + unless current_user&.admin? + flash[:alert] = "Acesso negado" + redirect_to root_path + end + end +end diff --git a/app/controllers/templates_controller.rb b/app/controllers/templates_controller.rb new file mode 100644 index 0000000000..d3bf302bda --- /dev/null +++ b/app/controllers/templates_controller.rb @@ -0,0 +1,253 @@ +## +# Controller for managing evaluation templates. +# Handles CRUD operations for templates and their associated questions. +# +class TemplatesController < ApplicationController + before_action :set_current_admin_id + before_action :set_template, only: %i[edit update destroy] + before_action :ensure_template_owner!, only: %i[edit update destroy] + before_action :load_dependencies, only: %i[index edit] + + ## + # Lists all templates and displays form for creating new template. + # + # ==== Returns + # * Renders index view with templates list and new template form + # + # ==== Side Effects + # * Sets @template, @docentes, @templates instance variables + # + def index + @template = Template.new(status: Template::STATUS[:draft]) + build_placeholder_question + end + + ## + # Displays form for editing an existing template. + # + # ==== Parameters + # * +params[:id]+ - Integer with Template ID + # + # ==== Returns + # * Renders index view with edit form + # + # ==== Side Effects + # * Sets @template instance variable + # + def edit + build_placeholder_question + render :index + end + + ## + # Creates a new template. + # + # ==== Parameters + # * +params[:template]+ - Hash with template attributes (name, description, docente_id, template_questions_attributes) + # + # ==== Returns + # * Redirects to templates_redirect_path on success + # * Renders index view with errors on failure + # + # ==== Side Effects + # * Creates Template record in database + # * Creates TemplateQuestion records in database + # * Sets flash[:notice] on success or flash[:alert] on failure + # + def create + @template = Template.new(template_params) + + if @template.save + @current_admin_id ||= @template.docente_id + redirect_to templates_redirect_path, notice: "Template criado com sucesso." + else + load_dependencies + build_placeholder_question + flash.now[:alert] = "Não foi possível salvar o template. Corrija os campos destacados." + render :index, status: :unprocessable_entity + end + end + + ## + # Updates an existing template. + # + # ==== Parameters + # * +params[:id]+ - Integer with Template ID + # * +params[:template]+ - Hash with template attributes + # + # ==== Returns + # * Redirects to templates_redirect_path on success + # * Renders index view with errors on failure + # + # ==== Side Effects + # * Updates Template record in database + # * Updates or destroys TemplateQuestion records + # * Sets flash[:notice] on success or flash[:alert] on failure + # + def update + if @template.update(template_params) + @current_admin_id ||= @template.docente_id + redirect_to templates_redirect_path, notice: "Template atualizado com sucesso." + else + load_dependencies + build_placeholder_question + flash.now[:alert] = "Não foi possível atualizar o template. Corrija os campos destacados." + render :index, status: :unprocessable_entity + end + end + + ## + # Deletes a template. + # + # ==== Parameters + # * +params[:id]+ - Integer with Template ID + # + # ==== Returns + # * Redirects to templates_redirect_path + # + # ==== Side Effects + # * Destroys Template record from database + # * Sets flash[:notice] on success or flash[:alert] on failure + # + def destroy + @template.destroy! + @current_admin_id ||= @template.docente_id + redirect_to templates_redirect_path, notice: "Template removido com sucesso." + rescue StandardError + redirect_to templates_redirect_path, + alert: "Não foi possível remover o template. Tente novamente mais tarde." + end + + private + + ## + # Extracts and sanitizes template parameters. + # + # ==== Returns + # * ActionController::Parameters - Permitted template parameters + # + def template_params + params.require(:template).permit( + :name, + :description, + :docente_id, + template_questions_attributes: %i[id prompt question_type position required min_value max_value options_text _destroy] + ) + end + + ## + # Loads dependencies needed for index and edit views. + # + # ==== Side Effects + # * Sets @docentes and @templates instance variables + # + def load_dependencies + @docentes = Docente.order(:nome) + scope = Template.includes(:template_questions, :docente).order(created_at: :desc) + scope = scope.where(docente_id: @current_admin_id) if @current_admin_id + @templates = scope + end + + ## + # Sets the current admin ID from parameters. + # + # ==== Side Effects + # * Sets @current_admin_id instance variable + # + def set_current_admin_id + @current_admin_id = params[:admin_id].presence + end + + ## + # Loads template from database. + # + # ==== Parameters + # * +params[:id]+ - Integer with Template ID + # + # ==== Side Effects + # * Sets @template and @current_admin_id instance variables + # + def set_template + @template = Template.includes(:template_questions).find(params[:id]) + @current_admin_id ||= @template.docente_id + end + + ## + # Ensures current user owns the template being accessed. + # + # ==== Returns + # * Redirects to templates_redirect_path if user doesn't own template + # + # ==== Side Effects + # * Sets flash[:alert] and redirects if access denied + # + def ensure_template_owner! + return unless @current_admin_id + + return if @template.docente_id.to_s == @current_admin_id.to_s + + redirect_to templates_redirect_path, alert: "Template não encontrado para este administrador." + end + + ## + # Determines redirect path based on admin context. + # + # ==== Returns + # * String - Path to redirect to (management_templates_path or templates_path) + # + def templates_redirect_path + if @current_admin_id + management_templates_path(admin_id: @current_admin_id) + else + templates_path + end + end + + ## + # Builds placeholder questions for the template form. + # Ensures minimum number of question slots are available. + # + # ==== Side Effects + # * Adds TemplateQuestion objects to @template.template_questions + # + def build_placeholder_question + slots_needed = calculate_slots_needed + add_placeholder_questions(slots_needed) + end + + ## + # Calculates how many question slots are needed. + # + def calculate_slots_needed + base_slots = @template.persisted? ? 1 : 3 + active_count = count_active_questions + [ active_count, base_slots ].max - active_count + end + + ## + # Counts non-destroyed questions. + # + def count_active_questions + @template.template_questions.reject(&:marked_for_destruction?).size + end + + ## + # Adds the specified number of placeholder questions. + # + def add_placeholder_questions(count) + count.times { build_single_placeholder_question } + end + + ## + # Builds a single placeholder question with default values. + # + def build_single_placeholder_question + @template.template_questions.build( + question_type: TemplateQuestion::QUESTION_TYPES[:likert], + position: @template.template_questions.size + 1, + required: true, + min_value: 1, + max_value: 5 + ) + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb new file mode 100644 index 0000000000..f24a930c0a --- /dev/null +++ b/app/helpers/application_helper.rb @@ -0,0 +1,14 @@ +# Módulo auxiliar da aplicação para helpers de view. +# +# Este módulo contém métodos auxiliares que estão disponíveis +# em todas as views da aplicação CAMAAR. +# +# == Uso +# Os métodos definidos aqui podem ser chamados diretamente nas views ERB. +# +# == Exemplo +# # Na view: +# <%= helper_method_name %> +# +module ApplicationHelper +end diff --git a/app/javascript/application.js b/app/javascript/application.js new file mode 100644 index 0000000000..21795e9f54 --- /dev/null +++ b/app/javascript/application.js @@ -0,0 +1,5 @@ +// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails + +import "./templates_form"; +import "./evaluations"; +import "./sigaa_imports"; diff --git a/app/javascript/evaluations.js b/app/javascript/evaluations.js new file mode 100644 index 0000000000..d26f2ff261 --- /dev/null +++ b/app/javascript/evaluations.js @@ -0,0 +1,57 @@ +const setupEvaluationsForm = () => { + const controller = document.querySelector('[data-controller="evaluations"]'); + if (!controller) return; + + const container = controller.querySelector('[data-evaluations-target="container"]'); + const counter = controller.querySelector('[data-evaluations-target="counter"]'); + const selectAllBtn = controller.querySelector('[data-action="evaluations#selectAll"]'); + const clearAllBtn = controller.querySelector('[data-action="evaluations#clearAll"]'); + + const checkboxSelector = 'input[type="checkbox"][name="avaliacao_batch[turma_ids][]"]'; + + const selectedLabel = (count) => { + if (count === 0) return 'Nenhuma turma selecionada'; + if (count === 1) return '1 turma selecionada'; + return `${count} turmas selecionadas`; + }; + + const updateCounter = () => { + if (!counter) return; + const count = container ? container.querySelectorAll(`${checkboxSelector}:checked`).length : 0; + counter.textContent = selectedLabel(count); + }; + + const toggleAll = (checked) => { + if (!container) return; + container.querySelectorAll(checkboxSelector).forEach((checkbox) => { + checkbox.checked = checked; + }); + updateCounter(); + }; + + selectAllBtn?.addEventListener('click', (event) => { + event.preventDefault(); + toggleAll(true); + }); + + clearAllBtn?.addEventListener('click', (event) => { + event.preventDefault(); + toggleAll(false); + }); + + container?.addEventListener('change', (event) => { + if (event.target.matches(checkboxSelector)) { + updateCounter(); + } + }); + + updateCounter(); +}; + +const initEvaluationsForm = () => { + setupEvaluationsForm(); +}; + +['turbo:load', 'DOMContentLoaded'].forEach((eventName) => { + document.addEventListener(eventName, initEvaluationsForm); +}); diff --git a/app/javascript/sigaa_imports.js b/app/javascript/sigaa_imports.js new file mode 100644 index 0000000000..a92d37f8fd --- /dev/null +++ b/app/javascript/sigaa_imports.js @@ -0,0 +1,177 @@ +const setupSigaaImports = () => { + const updateForm = document.querySelector('.sigaa-imports-page__update-form'); + if (!updateForm) return; + + const submitButton = updateForm.querySelector('button[type="submit"], input[type="submit"]'); + const originalButtonText = submitButton?.textContent || submitButton?.value; + + // Criar overlay de loading + const createLoadingOverlay = () => { + const overlay = document.createElement('div'); + overlay.className = 'api-simulation-overlay'; + overlay.innerHTML = ` +
+
+

🔄 Sincronizando com SIGAA

+
+
+
+
+
🔌
+
+
Conectando ao SIGAA...
+
Estabelecendo conexão com a API
+
+
+
+
+
📚
+
+
Buscando dados de matérias e turmas...
+
Obtendo classes.json
+
+
+
+
+
👥
+
+
Buscando dados de participantes...
+
Obtendo class_members.json
+
+
+
+
+
⚙️
+
+
Processando dados...
+
Atualizando base de dados
+
+
+
+
+
+
+
Sincronização concluída!
+
Redirecionando...
+
+
+
+
+
+
+ `; + document.body.appendChild(overlay); + return overlay; + }; + + // Atualizar status do step + const updateStep = (stepName, status) => { + const step = document.querySelector(`[data-step="${stepName}"]`); + if (!step) return; + + const statusEl = step.querySelector('.api-step__status'); + step.classList.remove('api-step--pending', 'api-step--active', 'api-step--complete'); + + switch (status) { + case 'pending': + step.classList.add('api-step--pending'); + statusEl.textContent = ''; + break; + case 'active': + step.classList.add('api-step--active'); + statusEl.innerHTML = '
'; + break; + case 'complete': + step.classList.add('api-step--complete'); + statusEl.textContent = '✓'; + break; + } + }; + + // Simular processo de API + const simulateApiCall = async () => { + const overlay = createLoadingOverlay(); + + // Desabilitar botão + if (submitButton) { + submitButton.disabled = true; + if (submitButton.tagName === 'INPUT') { + submitButton.value = 'Sincronizando...'; + } else { + submitButton.textContent = 'Sincronizando...'; + } + } + + try { + // Step 1: Conectar + updateStep('connect', 'active'); + await new Promise(resolve => setTimeout(resolve, 800)); + + // Step 2: Buscar classes + updateStep('connect', 'complete'); + updateStep('fetch-classes', 'active'); + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Step 3: Buscar members + updateStep('fetch-classes', 'complete'); + updateStep('fetch-members', 'active'); + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Step 4: Processar + updateStep('fetch-members', 'complete'); + updateStep('process', 'active'); + await new Promise(resolve => setTimeout(resolve, 1200)); + + // Step 5: Completar + updateStep('process', 'complete'); + updateStep('complete', 'active'); + await new Promise(resolve => setTimeout(resolve, 500)); + + // Submeter o formulário após a simulação + updateForm.submit(); + } catch (error) { + console.error('Erro na simulação:', error); + overlay.remove(); + if (submitButton) { + submitButton.disabled = false; + if (submitButton.tagName === 'INPUT') { + submitButton.value = originalButtonText; + } else { + submitButton.textContent = originalButtonText; + } + } + } + }; + + // Interceptar submit do formulário + updateForm.addEventListener('submit', (event) => { + // Verificar se já está em processo + if (submitButton?.disabled) { + event.preventDefault(); + return false; + } + + // Pedir confirmação antes de iniciar a simulação + const confirmed = window.confirm( + 'Deseja atualizar a base de dados com os arquivos classes.json e class_members.json do repositório? Esta ação irá atualizar registros existentes e adicionar novos.' + ); + + if (confirmed) { + event.preventDefault(); + simulateApiCall(); + } else { + event.preventDefault(); + } + + return false; + }); +}; + +const initSigaaImports = () => { + setupSigaaImports(); +}; + +['turbo:load', 'DOMContentLoaded'].forEach((eventName) => { + document.addEventListener(eventName, initSigaaImports); +}); + diff --git a/app/javascript/templates_form.js b/app/javascript/templates_form.js new file mode 100644 index 0000000000..71b054259f --- /dev/null +++ b/app/javascript/templates_form.js @@ -0,0 +1,89 @@ +const setupTemplateForm = () => { + const formContainer = document.querySelector('[data-template-form="container"]'); + if (!formContainer) return; + + const questionsContainer = formContainer.querySelector('[data-template-form-target="questions"]'); + const blueprint = formContainer.querySelector('#question-fields-template'); + const addButton = formContainer.querySelector('[data-action="add-question"]'); + + if (!questionsContainer || !blueprint || !addButton) return; + + const toggleExtras = (block) => { + const currentType = block.querySelector('[data-testid="question-type"]')?.value; + block.querySelectorAll('[data-question-extra]').forEach((element) => { + element.style.display = element.dataset.questionExtra === currentType ? '' : 'none'; + }); + }; + + const updatePositions = () => { + const activeBlocks = Array.from(questionsContainer.querySelectorAll('[data-testid="question-block"]')) + .filter((block) => block.dataset.removed !== 'true'); + + activeBlocks.forEach((block, index) => { + const positionInput = block.querySelector('[data-testid="question-position"]'); + const indexLabel = block.querySelector('[data-testid="question-index"]'); + if (positionInput) positionInput.value = index + 1; + if (indexLabel) indexLabel.textContent = index + 1; + }); + }; + + const handleRemovalToggle = (block) => { + const checkbox = block.querySelector('[data-testid="question-remove-checkbox"]'); + if (!checkbox) return; + + checkbox.addEventListener('change', () => { + if (checkbox.checked) { + block.dataset.removed = 'true'; + block.style.display = 'none'; + } else { + block.dataset.removed = 'false'; + block.style.display = ''; + } + updatePositions(); + }); + }; + + const setupBlock = (block) => { + block.dataset.removed = 'false'; + const typeSelect = block.querySelector('[data-testid="question-type"]'); + if (typeSelect) { + typeSelect.addEventListener('change', () => toggleExtras(block)); + } + toggleExtras(block); + handleRemovalToggle(block); + }; + + questionsContainer.querySelectorAll('[data-testid="question-block"]').forEach(setupBlock); + updatePositions(); + + addButton.addEventListener('click', (event) => { + event.preventDefault(); + const html = blueprint.innerHTML.replace(/NEW_RECORD/g, Date.now().toString()); + const wrapper = document.createElement('div'); + wrapper.innerHTML = html.trim(); + const block = wrapper.firstElementChild; + questionsContainer.appendChild(block); + setupBlock(block); + updatePositions(); + }); + + questionsContainer.addEventListener('click', (event) => { + const button = event.target.closest('[data-action="remove-question"]'); + if (!button) return; + event.preventDefault(); + const block = button.closest('[data-testid="question-block"]'); + if (!block) return; + const checkbox = block.querySelector('[data-testid="question-remove-checkbox"]'); + if (checkbox && !checkbox.checked) { + checkbox.checked = true; + checkbox.dispatchEvent(new Event('change')); + } + }); +}; + +const initTemplateForm = () => { + setupTemplateForm(); +}; + +document.addEventListener('turbo:load', initTemplateForm); +document.addEventListener('DOMContentLoaded', initTemplateForm); diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb new file mode 100644 index 0000000000..534a3bcf74 --- /dev/null +++ b/app/jobs/application_job.rb @@ -0,0 +1,28 @@ +# Classe base para todos os jobs assíncronos da aplicação. +# +# Esta classe fornece configurações padrão para processamento +# de tarefas em background na aplicação CAMAAR usando ActiveJob. +# +# == Herança +# Todos os jobs da aplicação devem herdar desta classe. +# +# == Configurações disponíveis +# - retry_on: Configura retentativas automáticas para erros específicos +# - discard_on: Descarta jobs quando determinados erros ocorrem +# +# == Exemplo +# class ProcessDataJob < ApplicationJob +# queue_as :default +# +# def perform(data) +# # Processamento assíncrono +# end +# end +# +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..29c8166f5d --- /dev/null +++ b/app/mailers/application_mailer.rb @@ -0,0 +1,23 @@ +# Classe base para todos os mailers da aplicação. +# +# Esta classe fornece configurações padrão para o envio de e-mails +# na aplicação CAMAAR. +# +# == Configurações +# - from: Endereço de e-mail remetente padrão +# - layout: Template de layout para e-mails +# +# == Herança +# Todos os mailers da aplicação devem herdar desta classe. +# +# == Exemplo +# class NotificationMailer < ApplicationMailer +# def notify(user) +# mail(to: user.email, subject: 'Notificação') +# end +# end +# +class ApplicationMailer < ActionMailer::Base + default from: "from@example.com" + layout "mailer" +end diff --git a/app/mailers/password_setup_mailer.rb b/app/mailers/password_setup_mailer.rb new file mode 100644 index 0000000000..17bc6b67d0 --- /dev/null +++ b/app/mailers/password_setup_mailer.rb @@ -0,0 +1,36 @@ +# Mailer responsável pelo envio de e-mails de configuração de senha. +# +# Este mailer envia instruções para que novos usuários possam +# configurar suas senhas no sistema CAMAAR. +# +# == Uso +# PasswordSetupMailer.setup_instructions(user).deliver_later +# +# == E-mail remetente +# Configurado para 'noreply@camaar.unb.br' +# +class PasswordSetupMailer < ApplicationMailer + default from: "noreply@camaar.unb.br" + + # Envia e-mail com instruções para configuração de senha. + # + # == Parâmetros + # * +user+ - Usuário que receberá as instruções (deve ter password_reset_token) + # + # == Variáveis de instância disponíveis na view + # * +@user+ - O usuário destinatário + # * +@setup_url+ - URL para configuração da senha + # + # == Retorno + # Retorna um objeto Mail::Message pronto para envio. + # + def setup_instructions(user) + @user = user + @setup_url = password_setup_url(token: user.password_reset_token, host: default_url_options[:host]) + + mail( + to: @user.email, + subject: "Configure sua senha - Sistema CAMAAR" + ) + end +end diff --git a/app/models/application_record.rb b/app/models/application_record.rb new file mode 100644 index 0000000000..5f83bdd592 --- /dev/null +++ b/app/models/application_record.rb @@ -0,0 +1,17 @@ +# Classe base abstrata para todos os modelos ActiveRecord da aplicação. +# +# Esta classe serve como classe pai para todos os modelos da aplicação CAMAAR, +# fornecendo configurações e comportamentos comuns. +# +# == Herança +# Todos os modelos da aplicação devem herdar desta classe em vez de +# herdar diretamente de ActiveRecord::Base. +# +# == Exemplo +# class Usuario < ApplicationRecord +# # ... +# end +# +class ApplicationRecord < ActiveRecord::Base + primary_abstract_class +end diff --git a/app/models/avaliacao.rb b/app/models/avaliacao.rb new file mode 100644 index 0000000000..ca18811e6e --- /dev/null +++ b/app/models/avaliacao.rb @@ -0,0 +1,61 @@ +## +# Model representing an evaluation/assessment form. +# Contains questions that students must answer. +# An evaluation is created from a template and assigned to one or more classes (turmas). +# +# ==== Attributes +# * +title+ - Evaluation title +# * +due_date+ - Deadline for responses +# * +status+ - Current status (draft, published, closed) +# * +max_score+ - Maximum possible score +# +# ==== Associations +# * belongs_to :turma - The class this evaluation is assigned to +# * belongs_to :docente - The teacher who created this evaluation +# * belongs_to :template - The template used to create this evaluation (optional) +# * has_many :questoes - Questions in this evaluation +# * has_many :respostas - Student responses to this evaluation +# +class Avaliacao < ApplicationRecord + self.table_name = "avaliacoes" + + enum :status, { + draft: "draft", + published: "published", + closed: "closed" + } + + belongs_to :turma + belongs_to :docente + belongs_to :template, optional: true + + has_many :questoes, dependent: :destroy + has_many :respostas, dependent: :destroy + + accepts_nested_attributes_for :questoes, allow_destroy: true + + validates :title, :due_date, presence: true + validates :max_score, numericality: { greater_than_or_equal_to: 0 } + + ## + # Finds all pending evaluations for a given student. + # + # ==== Parameters + # * +dicente+ - Dicente object representing the student + # + # ==== Returns + # * ActiveRecord::Relation - Collection of published Avaliacao records + # that the student is enrolled in but hasn't responded to yet + # + # ==== Side Effects + # * None - This is a query scope + # + scope :pending_for_dicente, lambda { |dicente| + published + .joins(turma: :matriculas) + .where(matriculas: { dicente_id: dicente.id }) + .where.not( + id: Resposta.where(dicente_id: dicente.id).select(:avaliacao_id) + ) + } +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/dicente.rb b/app/models/dicente.rb new file mode 100644 index 0000000000..0ac699aef8 --- /dev/null +++ b/app/models/dicente.rb @@ -0,0 +1,22 @@ +## +# Model representing a student in the system. +# Inherits from Usuario (STI). +# +# ==== Attributes +# * +matricula+ - Student registration number +# * +curso+ - Student's course/program +# +# ==== Associations +# * +matriculas+ - Enrollments in classes +# * +turmas+ - Classes the student is enrolled in (through matriculas) +# * +avaliacoes+ - Evaluations available to this student (through turmas) +# * +respostas+ - Responses submitted by this student +# +class Dicente < Usuario + has_many :matriculas, dependent: :destroy + has_many :turmas, through: :matriculas + has_many :avaliacoes, through: :turmas + has_many :respostas, dependent: :destroy + + validates :matricula, :curso, presence: true +end diff --git a/app/models/docente.rb b/app/models/docente.rb new file mode 100644 index 0000000000..e28d7ddc95 --- /dev/null +++ b/app/models/docente.rb @@ -0,0 +1,20 @@ +## +# Model representing a teacher/professor in the system. +# Inherits from Usuario (STI). +# +# ==== Attributes +# * +departamento+ - Teacher's department +# * +titulacao+ - Academic title/degree +# +# ==== Associations +# * +turmas+ - Classes taught by this teacher +# * +templates+ - Evaluation templates created by this teacher +# * +avaliacoes+ - Evaluations associated with this teacher +# +class Docente < Usuario + has_many :turmas, dependent: :nullify + has_many :templates, dependent: :destroy + has_many :avaliacoes, dependent: :nullify + + validates :departamento, :titulacao, presence: true +end diff --git a/app/models/materia.rb b/app/models/materia.rb new file mode 100644 index 0000000000..4055fab32f --- /dev/null +++ b/app/models/materia.rb @@ -0,0 +1,18 @@ +## +# Model representing a subject/course in the system. +# +# ==== Attributes +# * +code+ - Subject code (unique identifier, e.g., "MAT001") +# * +name+ - Subject name +# +# ==== Associations +# * +turmas+ - Class sections of this subject +# +class Materia < ApplicationRecord + self.table_name = "materias" + + has_many :turmas, dependent: :destroy + + validates :code, :name, presence: true + validates :code, uniqueness: true +end diff --git a/app/models/matricula.rb b/app/models/matricula.rb new file mode 100644 index 0000000000..50702451b6 --- /dev/null +++ b/app/models/matricula.rb @@ -0,0 +1,26 @@ +## +# Model representing a student enrollment in a class. +# Links a Dicente to a Turma. +# +# ==== Attributes +# * +status+ - Enrollment status (ativo, trancado, concluido) +# +# ==== Associations +# * +dicente+ - Student enrolled +# * +turma+ - Class the student is enrolled in +# +# ==== Validations +# * One enrollment per student per class (unique dicente_id + turma_id) +# +class Matricula < ApplicationRecord + belongs_to :dicente + belongs_to :turma + + enum :status, { + ativo: "ativo", + trancado: "trancado", + concluido: "concluido" + } + + validates :dicente_id, uniqueness: { scope: :turma_id } +end diff --git a/app/models/questao.rb b/app/models/questao.rb new file mode 100644 index 0000000000..08f14cf47f --- /dev/null +++ b/app/models/questao.rb @@ -0,0 +1,44 @@ +## +# Model representing a question in an evaluation. +# Questions are created from template questions when an evaluation is generated. +# +# ==== Attributes +# * +prompt+ - Question text/statement +# * +question_type+ - Type of question (likert, multiple_choice, text) +# * +position+ - Order position in the evaluation +# * +weight+ - Score weight for this question +# * +mandatory+ - Whether the question must be answered +# * +options+ - JSON array of options (for multiple_choice) +# * +min_value+ / +max_value+ - Scale range (for likert) +# +# ==== Associations +# * belongs_to :avaliacao - The evaluation this question belongs to +# * belongs_to :template_question - The template question this was created from (optional) +# * has_many :resposta_items - Student responses to this question +# +class Questao < ApplicationRecord + self.table_name = "questoes" + + # Constante com os tipos de questão suportados. + # Referencia TemplateQuestion::QUESTION_TYPES para manter consistência. + QUESTION_TYPES = TemplateQuestion::QUESTION_TYPES + + belongs_to :avaliacao + belongs_to :template_question, optional: true + + has_many :resposta_items, dependent: :destroy + + validates :prompt, :question_type, :position, :weight, presence: true + validates :question_type, inclusion: { in: QUESTION_TYPES.values } + validates :position, numericality: { greater_than: 0 } + + ## + # Checks if this question accepts numeric responses (Likert scale). + # + # ==== Returns + # * Boolean - true if question_type is likert, false otherwise + # + def numeric? + question_type == QUESTION_TYPES[:likert] + end +end diff --git a/app/models/resposta.rb b/app/models/resposta.rb new file mode 100644 index 0000000000..3ade5afd49 --- /dev/null +++ b/app/models/resposta.rb @@ -0,0 +1,34 @@ +## +# Model representing a student's response to an evaluation. +# Contains the submission metadata and links to individual answer items. +# +# ==== Attributes +# * +status+ - Response status (pending, submitted, reviewed) +# * +submitted_at+ - Timestamp when response was submitted +# +# ==== Associations +# * belongs_to :avaliacao - The evaluation being responded to +# * belongs_to :dicente - The student who submitted this response +# * has_many :resposta_items - Individual answers to each question +# +# ==== Validations +# * One response per student per evaluation (unique dicente_id + avaliacao_id) +# +class Resposta < ApplicationRecord + self.table_name = "respostas" + + enum :status, { + pending: "pending", + submitted: "submitted", + reviewed: "reviewed" + } + + belongs_to :avaliacao + belongs_to :dicente + + has_many :resposta_items, dependent: :destroy + + accepts_nested_attributes_for :resposta_items + + validates :dicente_id, uniqueness: { scope: :avaliacao_id } +end diff --git a/app/models/resposta_item.rb b/app/models/resposta_item.rb new file mode 100644 index 0000000000..48cccb2bd9 --- /dev/null +++ b/app/models/resposta_item.rb @@ -0,0 +1,21 @@ +## +# Model representing an individual answer to a question. +# Each RespostaItem corresponds to one question's answer in a Resposta. +# +# ==== Attributes +# * +valor+ - The answer value (text, number, or selected option) +# +# ==== Associations +# * +resposta+ - Parent response this answer belongs to +# * +questao+ - Question being answered +# +# ==== Validations +# * One answer per question per response (unique questao_id + resposta_id) +# +class RespostaItem < ApplicationRecord + belongs_to :resposta + belongs_to :questao + + validates :valor, presence: true + validates :questao_id, uniqueness: { scope: :resposta_id } +end diff --git a/app/models/template.rb b/app/models/template.rb new file mode 100644 index 0000000000..7b0821b7ff --- /dev/null +++ b/app/models/template.rb @@ -0,0 +1,64 @@ +## +# Model representing an evaluation template. +# Templates define reusable question sets that can be applied to create evaluations. +# Templates define the structure and questions for evaluations that can be reused. +# +# ==== Attributes +# * +name+ - Template name (unique per docente) +# * +status+ - Current status (draft, published, archived) +# +# ==== Associations +# * belongs_to :docente - The teacher who owns this template +# * has_many :template_questions - Questions defined in this template +# * has_many :avaliacoes - Evaluations created from this template +# +class Template < ApplicationRecord + # Constante com os status possíveis de um template. + # * +draft+ - Rascunho, em edição + # * +published+ - Publicado, disponível para uso + # * +archived+ - Arquivado, não disponível + STATUS = { + draft: "draft", + published: "published", + archived: "archived" + }.freeze + + belongs_to :docente + has_many :template_questions, dependent: :destroy, inverse_of: :template + has_many :avaliacoes, dependent: :nullify + + accepts_nested_attributes_for :template_questions, allow_destroy: true + + before_validation :normalize_name + + validates :name, presence: { message: "Informe o nome do template" }, uniqueness: { scope: :docente_id, case_sensitive: false, message: "Já existe um template com esse nome para o docente selecionado" } + validates :docente, presence: { message: "Selecione o responsável pelo template" } + validates :status, inclusion: { in: STATUS.values } + validate :must_have_questions + + enum :status, STATUS + + private + + ## + # Normalizes template name by stripping whitespace. + # + # ==== Side Effects + # * Modifies self.name attribute + # + def normalize_name + self.name = name.to_s.strip + end + + ## + # Validates that template has at least one non-destroyed question. + # + # ==== Side Effects + # * Adds error to base if no questions exist + # + def must_have_questions + return if template_questions.reject(&:marked_for_destruction?).any? + + errors.add(:base, "Adicione pelo menos uma questão") + end +end diff --git a/app/models/template_question.rb b/app/models/template_question.rb new file mode 100644 index 0000000000..f4e4146d9f --- /dev/null +++ b/app/models/template_question.rb @@ -0,0 +1,202 @@ +# Modelo que representa uma questão de template de avaliação. +# +# Esta classe define as questões que compõem um template de avaliação, +# suportando diferentes tipos como Likert, múltipla escolha e texto. +# +# == Tipos de Questão Suportados +# * +likert+ - Escala numérica de 1 a 5 +# * +multiple_choice+ - Múltipla escolha com opções definidas +# * +text+ - Resposta em texto livre +# +# == Associações +# * belongs_to :template +# +# == Validações +# * prompt, question_type e position são obrigatórios +# * question_type deve ser um dos tipos válidos +# * position deve ser maior que 0 +# * Múltipla escolha requer pelo menos 2 opções +# * Likert requer escala válida de 1-5 +# +class TemplateQuestion < ApplicationRecord + # Constante com os tipos de questão suportados. + QUESTION_TYPES = { + likert: "likert", + multiple_choice: "multiple_choice", + text: "text" + }.freeze + + # Atributo virtual para entrada de opções como texto. + attr_accessor :options_text + + belongs_to :template, inverse_of: :template_questions + + before_validation :normalize_fields + + validates :prompt, :question_type, :position, presence: true + validates :question_type, inclusion: { in: QUESTION_TYPES.values } + validates :position, numericality: { greater_than: 0 } + validate :validate_multiple_choice_options + validate :validate_likert_scale + + # Retorna as opções como array. + # + # == Retorno + # Array de strings com as opções, ou array vazio se não houver opções. + # + def options_array + (options.presence && JSON.parse(options)) || [] + rescue JSON::ParserError + [] + end + + # Retorna as opções formatadas como texto. + # + # == Retorno + # String com as opções separadas por quebra de linha. + # + def options_text + @options_text.presence || options_array.join("\n") + end + + private + + ## + # Normalizes and cleans field values before validation. + # Delegates to type-specific normalizers. + # + def normalize_fields + normalize_basic_fields + normalize_by_question_type + end + + ## + # Normalizes common fields for all question types. + # + def normalize_basic_fields + self.question_type = question_type.presence&.downcase + self.position ||= calculate_default_position + end + + ## + # Calculates default position based on existing questions. + # + def calculate_default_position + template&.template_questions&.size.to_i + 1 + end + + ## + # Normalizes fields based on question type. + # + def normalize_by_question_type + case question_type + when QUESTION_TYPES[:multiple_choice] + normalize_multiple_choice_fields + when QUESTION_TYPES[:likert] + normalize_likert_fields + else + clear_type_specific_fields + end + end + + ## + # Normalizes fields for multiple choice questions. + # + def normalize_multiple_choice_fields + store_options_payload + self.min_value = nil + self.max_value = nil + end + + ## + # Normalizes fields for Likert scale questions. + # + def normalize_likert_fields + self.min_value = (min_value.presence || 1).to_i + self.max_value = (max_value.presence || 5).to_i + self.options = nil + end + + ## + # Clears type-specific fields (for text questions). + # + def clear_type_specific_fields + self.options = nil + self.min_value = nil + self.max_value = nil + end + + ## + # Armazena as opções como JSON. + # + def store_options_payload + collection = parsed_options_from_accessor + self.options = collection.any? ? collection.to_json : nil + end + + ## + # Converte o texto de opções em array limpo. + # + # == Retorno + # Array de strings únicas e não vazias. + # + def parsed_options_from_accessor + source = if @options_text.nil? + options_array + else + @options_text.to_s.split(/\r?\n/) + end + + source.map(&:strip).reject(&:blank?).uniq + end + + ## + # Valida que múltipla escolha tem pelo menos 2 opções. + # + def validate_multiple_choice_options + return unless question_type == QUESTION_TYPES[:multiple_choice] + + if options_array.length < 2 + errors.add(:options, "Adicione pelo menos duas opções") + end + end + + ## + # Validates Likert scale configuration. + # Delegates to specific validators for clarity. + # + def validate_likert_scale + return unless question_type == QUESTION_TYPES[:likert] + return if valid_likert_configuration? + + errors.add(:base, "A escala numérica deve ser de 1 a 5") + end + + ## + # Checks if Likert scale has valid configuration. + # + def valid_likert_configuration? + values_present? && values_in_range? && min_less_than_max? + end + + ## + # Verifica se os valores min/max estão presentes. + # + def values_present? + min_value.present? && max_value.present? + end + + ## + # Verifica se os valores estão no intervalo permitido (1-5). + # + def values_in_range? + min_value >= 1 && max_value <= 5 + end + + ## + # Verifica se min_value é menor que max_value. + # + def min_less_than_max? + min_value < max_value + end +end diff --git a/app/models/turma.rb b/app/models/turma.rb new file mode 100644 index 0000000000..420457967c --- /dev/null +++ b/app/models/turma.rb @@ -0,0 +1,29 @@ +## +# Model representing a class section (turma). +# A turma is a specific offering of a subject in a given semester. +# +# ==== Attributes +# * +class_code+ - Class section code (e.g., "A", "B", "01") +# * +semester+ - Semester identifier (e.g., "2024.1", "2024.2") +# +# ==== Associations +# * +materia+ - Subject this class belongs to +# * +docente+ - Teacher assigned to this class +# * +matriculas+ - Student enrollments in this class +# * +dicentes+ - Students enrolled in this class (through matriculas) +# * +avaliacoes+ - Evaluations for this class +# +# ==== Validations +# * Unique class_code per materia per semester +# +class Turma < ApplicationRecord + belongs_to :materia + belongs_to :docente + + has_many :matriculas, dependent: :destroy + has_many :dicentes, through: :matriculas + has_many :avaliacoes, dependent: :destroy + + validates :class_code, :semester, presence: true + validates :class_code, uniqueness: { scope: [ :materia_id, :semester ] } +end diff --git a/app/models/usuario.rb b/app/models/usuario.rb new file mode 100644 index 0000000000..76e08bad75 --- /dev/null +++ b/app/models/usuario.rb @@ -0,0 +1,61 @@ +## +# Base model for all system users (STI parent class). +# Uses Single Table Inheritance (STI) with Docente and Dicente subclasses. +# +# ==== Attributes +# * +identifier+ - Unique user identifier (login/username) +# * +nome+ - User's full name +# * +email+ - User's email address (must be unique) +# * +matricula+ - Registration number (optional, unique if present) +# * +type+ - STI discriminator column (Docente or Dicente) +# * +password_digest+ - Encrypted password (via has_secure_password) +# * +pending_activation+ - Whether user needs to set up password +# * +activation_token+ - Token for password setup +# * +activation_token_expires_at+ - Token expiration datetime +# +class Usuario < ApplicationRecord + self.inheritance_column = :type + + has_secure_password validations: false + + validates :identifier, :nome, :email, :type, presence: true + validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }, uniqueness: true + validates :identifier, uniqueness: true + validates :matricula, uniqueness: true, allow_blank: true + + ## + # Finds all teachers (docentes). + # + # ==== Returns + # * ActiveRecord::Relation - Collection of Docente records + # + scope :docentes, -> { where(type: "Docente") } + + ## + # Finds all students (dicentes). + # + # ==== Returns + # * ActiveRecord::Relation - Collection of Dicente records + # + scope :dicentes, -> { where(type: "Dicente") } + + ## + # Checks if user is a teacher (docente). + # + # ==== Returns + # * Boolean - true if user is a Docente, false otherwise + # + def docente? + is_a?(Docente) + end + + ## + # Checks if user is a student (dicente). + # + # ==== Returns + # * Boolean - true if user is a Dicente, false otherwise + # + def dicente? + is_a?(Dicente) + end +end diff --git a/app/services/evaluation_batch_creator.rb b/app/services/evaluation_batch_creator.rb new file mode 100644 index 0000000000..8ba41f4a2e --- /dev/null +++ b/app/services/evaluation_batch_creator.rb @@ -0,0 +1,314 @@ +## +# Service for creating evaluation batches from a template. +# Creates one Avaliacao per selected Turma, copying questions from the template. +# Creates multiple Avaliacao records and their associated Questao records +# for multiple classes (turmas) at once. +# +# ==== Usage +# result = EvaluationBatchCreator.call( +# template_id: 1, +# turma_ids: [1, 2, 3], +# due_date: "2024-12-31" +# ) +# +# if result.success? +# puts "Created: #{result.created.size}, Skipped: #{result.skipped.size}" +# else +# puts "Errors: #{result.errors.join(', ')}" +# end +# +class EvaluationBatchCreator + ## + # Result object containing creation statistics. + # * +created+ - Array of created Avaliacao records + # * +skipped+ - Array of Turma records skipped (already had evaluation) + # * +errors+ - Array of error messages + # + Result = Struct.new(:created, :skipped, :errors, keyword_init: true) do + ## + # Checks if the batch creation was successful. + # + # ==== Returns + # * Boolean - true if no errors occurred, false otherwise + # + def success? + errors.blank? + end + end + + ## + # Class method to create evaluation batches. + # + # ==== Parameters + # * +template_id:+ - Integer with Template ID to use + # * +turma_ids:+ - Array of Integer with Turma IDs to create evaluations for + # * +due_date:+ - String or Date with due date (optional, defaults to end of month) + # + # ==== Returns + # * Result - Object with created, skipped arrays and errors array + # + # ==== Side Effects + # * Creates Avaliacao records in database + # * Creates Questao records for each avaliacao + # * Skips turmas that already have an evaluation for the given template + # + def self.call(**kwargs) + new(**kwargs).call + end + + ## + # Initializes a new EvaluationBatchCreator. + # + # ==== Parameters + # * +template_id:+ - Integer with Template ID + # * +turma_ids:+ - Array of Integer with Turma IDs + # * +due_date:+ - String or Date with due date (optional) + # + def initialize(template_id:, turma_ids:, due_date: nil) + @template_id = template_id + @turma_ids = Array(turma_ids).reject(&:blank?) + @due_date_param = due_date + end + + ## + # Executes the batch creation process. + # + # ==== Returns + # * Result - Object with created, skipped arrays and errors array + # + # ==== Side Effects + # * Creates Avaliacao and Questao records in database + # * Wraps operations in a database transaction + # + def call + return error_result("Selecione ao menos um template e uma turma") if invalid_params? + + template, turmas = load_template_and_turmas + created, skipped = process_turmas(template, turmas) + + Result.new(created:, skipped:, errors: []) + rescue ActiveRecord::RecordInvalid => e + handle_record_invalid(e) + rescue ActiveRecord::RecordNotFound + error_result("Template selecionado não foi encontrado") + end + + private + + ## + # Creates an error result object. + # + # ==== Parameters + # * +message+ - String with error message + # + # ==== Returns + # * Result - Result object with error message + # + def error_result(message) + Result.new(created: [], skipped: [], errors: [ message ]) + end + + ## + # Loads template and turmas from database. + # + # ==== Returns + # * Array - [Template, ActiveRecord::Relation] + # + # ==== Side Effects + # * Queries database for Template and Turma records + # + def load_template_and_turmas + template = Template.includes(:template_questions).find(@template_id) + turmas = Turma.includes(:docente).where(id: @turma_ids) + [ template, turmas ] + end + + ## + # Processes all turmas and creates evaluations. + # + # ==== Parameters + # * +template+ - Template object + # * +turmas+ - ActiveRecord::Relation with Turma records + # + # ==== Returns + # * Array - [Array, Array] with created and skipped records + # + # ==== Side Effects + # * Creates Avaliacao records in database + # * Wraps operations in a transaction + # + def process_turmas(template, turmas) + created = [] + skipped = [] + + ApplicationRecord.transaction do + turmas.each do |turma| + process_single_turma(turma, template, created, skipped) + end + end + + [ created, skipped ] + end + + ## + # Processes a single turma and creates evaluation if needed. + # + # ==== Parameters + # * +turma+ - Turma object + # * +template+ - Template object + # * +created+ - Array to append created Avaliacao to + # * +skipped+ - Array to append skipped Turma to + # + # ==== Returns + # * nil + # + # ==== Side Effects + # * Creates Avaliacao record if not duplicate + # * Modifies created and skipped arrays + # + def process_single_turma(turma, template, created, skipped) + if duplicate?(turma, template) + skipped << turma + else + created << create_avaliacao_for_turma(turma, template) + end + end + + ## + # Creates an Avaliacao for a turma and copies questions from template. + # + # ==== Parameters + # * +turma+ - Turma object + # * +template+ - Template object + # + # ==== Returns + # * Avaliacao - Newly created Avaliacao object + # + # ==== Side Effects + # * Creates Avaliacao record in database + # * Creates Questao records for the avaliacao + # + def create_avaliacao_for_turma(turma, template) + avaliacao = Avaliacao.create!(build_evaluation_attributes(template, turma)) + copy_questions(template, avaliacao) + avaliacao + end + + ## + # Handles RecordInvalid exceptions. + # + # ==== Parameters + # * +error+ - ActiveRecord::RecordInvalid exception + # + # ==== Returns + # * Result - Result object with error message + # + def handle_record_invalid(error) + message = error.record.errors.full_messages.to_sentence.presence || error.message + error_result(message) + end + + ## + # Checks if parameters are invalid. + # + # ==== Returns + # * Boolean - true if template_id is blank or turma_ids is empty + # + def invalid_params? + @template_id.blank? || @turma_ids.empty? + end + + ## + # Checks if an evaluation already exists for the turma and template. + # + # ==== Parameters + # * +turma+ - Turma object + # * +template+ - Template object + # + # ==== Returns + # * Boolean - true if duplicate exists, false otherwise + # + # ==== Side Effects + # * Queries database for existing Avaliacao + # + def duplicate?(turma, template) + Avaliacao.exists?(turma_id: turma.id, template_id: template.id) + end + + ## + # Builds attributes hash for creating an Avaliacao. + # + # ==== Parameters + # * +template+ - Template object + # * +turma+ - Turma object + # + # ==== Returns + # * Hash - Attributes hash for Avaliacao creation + # + def build_evaluation_attributes(template, turma) + { + template: template, + turma: turma, + docente: turma.docente, + title: "#{template.name} - #{turma.class_code}/#{turma.semester}", + description: template.description, + due_date: normalized_due_date, + max_score: max_score_for(template) + } + end + + ## + # Calculates max score based on number of questions. + # + # ==== Parameters + # * +template+ - Template object + # + # ==== Returns + # * Integer - Max score (number of questions * 5) + # + def max_score_for(template) + template.template_questions.count * 5 + end + + ## + # Normalizes due date parameter to a Date object. + # + # ==== Returns + # * Date - Parsed due date or end of current month as default + # + def normalized_due_date + parsed = if @due_date_param.present? + Time.zone.parse(@due_date_param) rescue nil + end + parsed || Time.zone.today.end_of_month + end + + ## + # Copies questions from template to avaliacao. + # + # ==== Parameters + # * +template+ - Template object + # * +avaliacao+ - Avaliacao object + # + # ==== Returns + # * nil + # + # ==== Side Effects + # * Creates Questao records in database + # + def copy_questions(template, avaliacao) + template.template_questions.order(:position).each do |template_question| + avaliacao.questoes.create!( + prompt: template_question.prompt, + question_type: template_question.question_type, + position: template_question.position, + mandatory: template_question.required, + min_value: template_question.min_value, + max_value: template_question.max_value, + options: template_question.options, + template_question: template_question, + weight: 1 + ) + end + end +end diff --git a/app/services/evaluation_result_aggregator.rb b/app/services/evaluation_result_aggregator.rb new file mode 100644 index 0000000000..62742b232b --- /dev/null +++ b/app/services/evaluation_result_aggregator.rb @@ -0,0 +1,108 @@ +## +# Service for aggregating evaluation results and statistics. +# Provides summary data for a single evaluation including response rates, +# average scores, and per-question statistics. +# Provides summary data including response counts, completion rates, and question statistics. +# +# ==== Usage +# aggregator = EvaluationResultAggregator.new(avaliacao) +# summary = aggregator.summary +# # => { total_responses: 25, completion_rate: 80, average_score: 4.2, question_stats: [...] } +# +class EvaluationResultAggregator + ## + # @!attribute [r] avaliacao + # A avaliação para a qual os resultados serão agregados. + attr_reader :avaliacao + + ## + # Initializes a new aggregator for the given evaluation. + # + # ==== Parameters + # * +avaliacao+ - Avaliacao object to aggregate results for + # + def initialize(avaliacao) + @avaliacao = avaliacao + end + + ## + # Generates a comprehensive summary of evaluation results. + # + # ==== Returns + # * Hash - Contains: + # * :total_responses (Integer) - Number of submitted responses + # * :completion_rate (Integer) - Percentage of enrolled students who responded + # * :average_score (Float) - Average score across all responses + # * :question_stats (Array) - Statistics for each question + # + # ==== Side Effects + # * None - This is a read-only operation + # + def summary + { + total_responses: total_responses, + completion_rate: completion_rate, + average_score: average_score, + question_stats: question_stats + } + end + + private + + ## + # Counts total number of submitted responses. + # + # ==== Returns + # * Integer - Number of respostas with submitted status + # + def total_responses + @total_responses ||= avaliacao.respostas.count + end + + ## + # Calculates the completion rate as percentage of enrolled students who responded. + # + # ==== Returns + # * Integer - Completion rate percentage (0-100), or 0 if no enrollments + # + def completion_rate + matriculas = avaliacao.turma.matriculas.count + return 0 if matriculas.zero? + + ((total_responses.to_f / matriculas) * 100).round + end + + ## + # Calculates the average score across all responses. + # + # ==== Returns + # * Float - Average score rounded to 2 decimal places, or 0.0 if no responses + # + def average_score + avaliacao.respostas.average(:score)&.to_f&.round(2) || 0.0 + end + + ## + # Generates statistics for each question in the evaluation. + # + # ==== Returns + # * Array - Array of hashes, each containing: + # * :id (Integer) - Question ID + # * :prompt (String) - Question text + # * :total (Integer) - Total number of responses to this question + # * :distribution (Hash) - Distribution of response values + # + def question_stats + avaliacao.questoes.order(:position).map do |questao| + distribution = questao.resposta_items.group(:valor).count + total = distribution.values.sum + + { + id: questao.id, + prompt: questao.prompt, + total: total, + distribution: distribution + } + end + end +end diff --git a/app/services/evaluation_results_exporter.rb b/app/services/evaluation_results_exporter.rb new file mode 100644 index 0000000000..018fcff7fb --- /dev/null +++ b/app/services/evaluation_results_exporter.rb @@ -0,0 +1,156 @@ +require "csv" + +## +# Service for exporting evaluation results to CSV format. +# Generates a downloadable report with all responses. +# +# ==== Usage +# exporter = EvaluationResultsExporter.new(avaliacao) +# csv_content = exporter.call +# filename = exporter.filename +# +# ==== Raises +# * +ExportError+ - When there are no responses or export fails +# +class EvaluationResultsExporter + ## + # Exception raised when export fails. + # + class ExportError < StandardError; end + + class << self + ## + # @!attribute [rw] force_failure + # Atributo para forçar falhas em testes. + attr_accessor :force_failure + end + + ## + # Initializes a new exporter for the given evaluation. + # + # ==== Parameters + # * +avaliacao+ - Avaliacao object to export results for + # + def initialize(avaliacao) + @avaliacao = avaliacao + end + + ## + # Generates CSV content with evaluation results. + # + # ==== Returns + # * String - CSV content with headers and all response data + # + # ==== Raises + # * ExportError - If export conditions are not met or an error occurs + # + # ==== Side Effects + # * Queries database for responses and questions + # + def call + validate_export_conditions + generate_csv_report + rescue ExportError + raise + rescue StandardError => e + raise ExportError, e.message + ensure + self.class.force_failure = false + end + + ## + # Generates filename for the exported CSV file. + # + # ==== Returns + # * String - Filename based on evaluation title (e.g., "avaliacao-titulo-resultados.csv") + # + def filename + parameterized = @avaliacao.title.parameterize + "#{parameterized}-resultados.csv" + end + + private + + ## + # Validates that export can proceed. + # + # ==== Raises + # * StandardError - If service is forced to fail (testing) + # * ExportError - If no responses are available + # + def validate_export_conditions + raise StandardError, "Serviço de exportação indisponível" if self.class.force_failure + raise ExportError, "Ainda não há respostas disponíveis" if @avaliacao.respostas.none? + end + + ## + # Generates CSV content with headers and rows. + # + # ==== Returns + # * String - Complete CSV content + # + def generate_csv_report + CSV.generate(headers: true) do |csv| + add_csv_header(csv) + add_csv_rows(csv) + end + end + + ## + # Adds CSV header row. + # + # ==== Parameters + # * +csv+ - CSV object to write to + # + # ==== Side Effects + # * Writes header row to CSV + # + def add_csv_header(csv) + csv << [ "Questão", "Aluno", "Resposta", "Enviado em" ] + end + + ## + # Adds all data rows to CSV. + # + # ==== Parameters + # * +csv+ - CSV object to write to + # + # ==== Side Effects + # * Writes data rows to CSV + # + def add_csv_rows(csv) + @avaliacao.questoes.includes(resposta_items: :resposta).each do |questao| + add_questao_rows(csv, questao) + end + end + + ## + # Adds rows for a specific question. + # + # ==== Parameters + # * +csv+ - CSV object to write to + # * +questao+ - Questao object + # + # ==== Side Effects + # * Writes rows for each response to the question + # + def add_questao_rows(csv, questao) + questao.resposta_items.each do |item| + csv << build_csv_row(questao, item) + end + end + + ## + # Builds a CSV row from question and response item. + # + # ==== Parameters + # * +questao+ - Questao object + # * +item+ - RespostaItem object + # + # ==== Returns + # * Array - CSV row data [question prompt, student name, response value, submitted_at] + # + def build_csv_row(questao, item) + [ questao.prompt, item.resposta.dicente.nome, item.valor, item.resposta.submitted_at ] + end +end diff --git a/app/services/report_generator.rb b/app/services/report_generator.rb new file mode 100644 index 0000000000..eb9eb3dda5 --- /dev/null +++ b/app/services/report_generator.rb @@ -0,0 +1,123 @@ +## +# Service for generating administrative reports across evaluations. +# Aggregates data from multiple evaluations for dashboard/reporting purposes. +# Aggregates statistics from multiple evaluations for administrative reporting. +# +# ==== Usage +# # All evaluations +# report = ReportGenerator.new +# +# # Specific scope +# report = ReportGenerator.new(Avaliacao.where(status: :published)) +# +# summary = report.summary # Array of evaluation summaries +# totals = report.totals # Aggregate totals +# +class ReportGenerator + ## + # Initializes a new report generator. + # + # ==== Parameters + # * +scope+ - ActiveRecord::Relation or Array of Avaliacao records (default: Avaliacao.all) + # + def initialize(scope = Avaliacao.all) + @scope = scope + end + + ## + # Generates summary statistics for all evaluations in scope. + # + # ==== Returns + # * Array - Array of hashes, each containing: + # * :avaliacao_id (Integer) - Evaluation ID + # * :title (String) - Evaluation title + # * :docente (String) - Teacher name + # * :semester (String) - Semester code + # * :total_responses (Integer) - Number of responses + # * :average_score (Float) - Average score + # * :completion_rate (Integer) - Completion percentage + # + # ==== Side Effects + # * None - This is a read-only operation + # + def summary + @summary ||= evaluations.map do |avaliacao| + metrics = aggregator_for(avaliacao).summary + + { + avaliacao_id: avaliacao.id, + title: avaliacao.title, + docente: avaliacao.docente&.nome, + semester: avaliacao.turma&.semester, + total_responses: metrics[:total_responses], + average_score: metrics[:average_score], + completion_rate: metrics[:completion_rate] + } + end + end + + ## + # Calculates aggregate totals across all evaluations. + # + # ==== Returns + # * Hash - Contains: + # * :total_forms (Integer) - Total number of evaluations + # * :total_responses (Integer) - Total responses across all evaluations + # * :average_completion_rate (Integer) - Average completion rate percentage + # + # ==== Side Effects + # * None - This is a read-only operation + # + def totals + data = summary + total_forms = data.size + total_responses = data.sum { |row| row[:total_responses].to_i } + average_completion = if total_forms.zero? + 0 + else + (data.sum { |row| row[:completion_rate].to_i }.to_f / total_forms).round + end + + { + total_forms: total_forms, + total_responses: total_responses, + average_completion_rate: average_completion + } + end + + private + + ## + # @!attribute [r] scope + # Escopo de avaliações para geração de relatórios. + attr_reader :scope + + ## + # Loads evaluations from scope with necessary associations. + # + # ==== Returns + # * Array - Array of Avaliacao objects with associations loaded + # + def evaluations + @evaluations ||= begin + if scope.respond_to?(:includes) + scope.includes(:docente, turma: :materia).to_a + else + Array(scope) + end + end + end + + ## + # Creates an aggregator instance for a specific evaluation. + # + # ==== Parameters + # * +avaliacao+ - Avaliacao object + # + # ==== Returns + # * EvaluationResultAggregator - Aggregator instance + # + def aggregator_for(avaliacao) + EvaluationResultAggregator.new(avaliacao) + end +end diff --git a/app/services/sigaa_importer.rb b/app/services/sigaa_importer.rb new file mode 100644 index 0000000000..95e90c3569 --- /dev/null +++ b/app/services/sigaa_importer.rb @@ -0,0 +1,595 @@ +# app/services/sigaa_importer.rb +## +# Service responsible for importing data from SIGAA JSON files into the system. +# Handles creation and update of subjects (matérias), classes (turmas), teachers (docentes), +# students (dicentes), and enrollments (matrículas). +# +# Example: +# result = SigaaImporter.call( +# classes_file: File.open('turmas.json'), +# class_members_file: File.open('turma_docente_dicentes.json') +# ) +# puts result.summary_message +# +class SigaaImporter + ## + # Result object that tracks the import operation statistics and errors. + # + class ImportResult + ## + # @!attribute [rw] success + # Indica se a operação foi bem sucedida. + # @!attribute [rw] errors + # Array de erros ocorridos durante a importação. + # @!attribute [rw] created + # Hash com contagem de registros criados por tipo. + # @!attribute [rw] updated + # Hash com contagem de registros atualizados por tipo. + # @!attribute [rw] skipped + # Hash com contagem de registros ignorados por tipo. + # @!attribute [rw] operation_type + # Tipo da operação (ex: 'Importação'). + attr_accessor :success, :errors, :created, :updated, :skipped, :operation_type + + ## + # Initializes a new import result tracker. + # + # ==== Parameters + # * +operation_type+ - String describing the type of operation (default: 'Importação') + # + def initialize(operation_type: "Importação") + @success = true + @errors = [] + @created = { materias: 0, turmas: 0, docentes: 0, dicentes: 0, matriculas: 0 } + @updated = { materias: 0, turmas: 0, docentes: 0, dicentes: 0, matriculas: 0 } + @skipped = { materias: 0, turmas: 0, docentes: 0, dicentes: 0, matriculas: 0 } + @operation_type = operation_type + end + + ## + # Checks if the import was successful. + # + # ==== Returns + # * Boolean - true if successful and no errors occurred + # + def success? + @success && @errors.empty? + end + + ## + # Returns the total number of created records across all types. + # + # ==== Returns + # * Integer - sum of all created records + # + def total_created + @created.values.sum + end + + ## + # Returns the total number of updated records across all types. + # + # ==== Returns + # * Integer - sum of all updated records + # + def total_updated + @updated.values.sum + end + + ## + # Returns the total number of skipped records across all types. + # + # ==== Returns + # * Integer - sum of all skipped records + # + def total_skipped + @skipped.values.sum + end + + ## + # Generates a human-readable summary message of the import operation. + # + # ==== Returns + # * String - summary message with operation results or errors + # + def summary_message + success? ? success_summary : error_summary + end + + private + + def success_summary + parts = build_summary_parts + parts.any? ? "#{@operation_type} concluída: #{parts.join(', ')}" : no_changes_message + end + + def build_summary_parts + [].tap do |parts| + parts << "#{total_created} novos registros criados" if total_created.positive? + parts << "#{total_updated} registros atualizados" if total_updated.positive? + parts << "#{total_skipped} registros ignorados por já existirem" if total_skipped.positive? + end + end + + def no_changes_message + "#{@operation_type} concluída: nenhuma alteração necessária" + end + + def error_summary + "Erros na #{@operation_type.downcase}: #{@errors.join(', ')}" + end + end + + ## + # Class method to perform SIGAA data import. + # + # ==== Parameters + # * +classes_file+ - File object containing JSON data for classes (turmas) + # * +class_members_file+ - File object containing JSON data for class members (docentes and dicentes) + # * +operation_type+ - String describing the operation type (default: 'Importação') + # + # ==== Returns + # * ImportResult object with operation statistics and errors + # + # ==== Side Effects + # * Creates/updates records in the database (Materia, Turma, Docente, Dicente, Matricula) + # * Sends password setup emails to newly created users + # + def self.call(classes_file: nil, class_members_file: nil, operation_type: "Importação") + new(classes_file, class_members_file, operation_type).import + end + + ## + # Initializes a new SigaaImporter instance. + # + # ==== Parameters + # * +classes_file+ - File object with classes JSON data + # * +class_members_file+ - File object with class members JSON data + # * +operation_type+ - String describing the operation (default: 'Importação') + # + def initialize(classes_file, class_members_file, operation_type = "Importação") + @classes_file = classes_file + @class_members_file = class_members_file + @result = ImportResult.new(operation_type: operation_type) + end + + ## + # Performs the import operation by reading JSON files and creating/updating database records. + # + # ==== Returns + # * ImportResult object with statistics and any errors + # + # ==== Side Effects + # * Validates input files + # * Imports classes (turmas) and their subjects (matérias) + # * Imports class members (teachers and students) and enrollments + # + def import + validate_files + return @result unless @result.success? + + ActiveRecord::Base.transaction do + import_classes if @classes_file + import_class_members if @class_members_file + end + + @result + rescue JSON::ParserError => e + @result.success = false + @result.errors << "Não foi possível processar o arquivo JSON: #{e.message}" + @result + rescue StandardError => e + @result.success = false + @result.errors << "Erro durante importação: #{e.message}" + @result + end + + private + + def validate_files + if @classes_file.nil? && @class_members_file.nil? + @result.success = false + @result.errors << "Selecione ao menos um arquivo JSON para importação" + end + end + + def import_classes + classes_data = parse_json_file(@classes_file) + return unless classes_data + + classes_data.each do |class_data| + import_materia_and_turma(class_data) + end + end + + def import_class_members + members_data = parse_json_file(@class_members_file) + return unless members_data + + members_data.each do |member_data| + import_docente_dicentes_and_matriculas(member_data) + end + end + + def parse_json_file(file) + content = file.respond_to?(:read) ? file.read : File.read(file) + JSON.parse(content) + rescue JSON::ParserError => e + @result.success = false + @result.errors << "Arquivo JSON inválido: #{e.message}" + nil + end + + def import_materia_and_turma(class_data) + materia = find_or_create_materia(class_data) + import_turma(materia, class_data["class"]) if class_data["class"] + end + + def find_or_create_materia(class_data) + materia = Materia.find_or_initialize_by(code: class_data["code"]) + + if materia.new_record? + create_materia(materia, class_data) + else + update_materia_if_needed(materia, class_data) + end + + materia + end + + def create_materia(materia, class_data) + materia.name = class_data["name"] + if materia.save + @result.created[:materias] += 1 + else + @result.errors << "Erro ao criar matéria #{class_data['code']}: #{materia.errors.full_messages.join(', ')}" + end + end + + def update_materia_if_needed(materia, class_data) + return @result.skipped[:materias] += 1 if materia.name == class_data["name"] + + materia.name = class_data["name"] + if materia.save + @result.updated[:materias] += 1 + else + @result.errors << "Erro ao atualizar matéria #{class_data['code']}: #{materia.errors.full_messages.join(', ')}" + end + end + + def import_turma(materia, class_info) + turma = find_or_initialize_turma(materia, class_info) + + if turma.new_record? + create_new_turma(turma, class_info) + else + update_turma_if_needed(turma, class_info) + end + end + + def find_or_initialize_turma(materia, class_info) + Turma.find_or_initialize_by( + materia: materia, + class_code: class_info["classCode"], + semester: class_info["semester"] + ) + end + + def create_new_turma(turma, class_info) + turma.time_slot = class_info["time"] + turma.docente = find_or_create_placeholder_docente + + if turma.save + @result.created[:turmas] += 1 + else + @result.errors << "Erro ao criar turma: #{turma.errors.full_messages.join(', ')}" + end + end + + def update_turma_if_needed(turma, class_info) + return @result.skipped[:turmas] += 1 if turma.time_slot == class_info["time"] + + turma.time_slot = class_info["time"] + if turma.save + @result.updated[:turmas] += 1 + else + @result.errors << "Erro ao atualizar turma: #{turma.errors.full_messages.join(', ')}" + end + end + + ## + # Imports docente, dicentes, and their matriculas for a given class. + # + # ==== Parameters + # * +member_data+ - Hash containing code, classCode, semester, docente, dicente arrays + # + # ==== Side Effects + # * Creates/updates Docente, Dicente, and Matricula records + # * Updates @result counters + # + def import_docente_dicentes_and_matriculas(member_data) + turma = find_turma_for_member_data(member_data) + return unless turma + + import_docente_for_turma(member_data, turma) + import_dicentes_for_turma(member_data, turma) + end + + ## + # Finds the turma for the given member data. + # + def find_turma_for_member_data(member_data) + materia = Materia.find_by(code: member_data["code"]) + unless materia + @result.errors << "Matéria #{member_data['code']} não encontrada" + return nil + end + + turma = Turma.find_by( + materia: materia, + class_code: member_data["classCode"], + semester: member_data["semester"] + ) + + unless turma + @result.errors << "Turma #{member_data['code']}-#{member_data['classCode']} não encontrada" + return nil + end + + turma + end + + ## + # Imports/updates docente for a turma if present in member_data. + # + def import_docente_for_turma(member_data, turma) + return unless member_data["docente"] + + docente = import_docente(member_data["docente"]) + update_turma_docente(turma, docente) if docente + end + + ## + # Updates turma docente if different from current. + # + def update_turma_docente(turma, docente) + return if turma.docente == docente + + turma.update(docente: docente) + @result.updated[:turmas] += 1 + end + + ## + # Imports dicentes and their matriculas for a turma. + # + def import_dicentes_for_turma(member_data, turma) + return unless member_data["dicente"] + + member_data["dicente"].each do |dicente_data| + import_dicente_and_matricula(dicente_data, turma) + end + end + + ## + # Imports or updates a teacher (docente) from SIGAA data. + # + # ==== Parameters + # * +docente_data+ - Hash containing teacher information (nome, email, departamento, formacao, ocupacao) + # + # ==== Returns + # * Docente object (created or updated) + # + # ==== Side Effects + # * Creates or updates Docente record in database + # * Sends password setup email to newly created teachers + # * Updates import result counters + # + def import_docente(docente_data) + identifier = docente_data["usuario"] || docente_data["matricula"] + docente = Docente.find_or_initialize_by(identifier: identifier) + + if docente.new_record? + create_new_docente(docente, docente_data, identifier) + else + update_existing_docente(docente, docente_data, identifier) + end + + docente + end + + def create_new_docente(docente, docente_data, identifier) + assign_docente_attributes(docente, docente_data) + setup_docente_activation(docente) + + if docente.save + @result.created[:docentes] += 1 + send_password_setup_email(docente) + else + @result.errors << "Erro ao criar docente #{identifier}: #{docente.errors.full_messages.join(', ')}" + end + end + + def assign_docente_attributes(docente, docente_data) + docente.nome = docente_data["nome"] + docente.email = docente_data["email"] + docente.departamento = docente_data["departamento"] || "Não informado" + docente.titulacao = docente_data["formacao"] || "Não informado" + docente.ocupacao = docente_data["ocupacao"] || "docente" + end + + def setup_docente_activation(docente) + docente.pending_activation = true if docente.respond_to?(:pending_activation) + docente.password_reset_token = SecureRandom.urlsafe_base64(32) + docente.password_reset_sent_at = Time.current + end + + def update_existing_docente(docente, docente_data, identifier) + return unless docente_needs_update?(docente, docente_data) + + assign_docente_attributes(docente, docente_data) + + if docente.save + @result.updated[:docentes] += 1 + else + @result.errors << "Erro ao atualizar docente #{identifier}: #{docente.errors.full_messages.join(', ')}" + end + end + + def docente_needs_update?(docente, docente_data) + attributes_to_check = { + nome: docente_data["nome"], + email: docente_data["email"], + departamento: docente_data["departamento"] || "Não informado", + titulacao: docente_data["formacao"] || "Não informado", + ocupacao: docente_data["ocupacao"] || "docente" + } + + changed = attributes_to_check.any? { |attr, new_value| docente.send(attr) != new_value } + @result.skipped[:docentes] += 1 unless changed + changed + end + + ## + # Imports or updates a student (dicente) and creates their enrollment (matrícula) in a class. + # + # ==== Parameters + # * +dicente_data+ - Hash containing student information (nome, email, matricula, curso, ocupacao, formacao) + # * +turma+ - Turma object representing the class to enroll the student in + # + # ==== Returns + # * nil + # + # ==== Side Effects + # * Creates or updates Dicente record in database + # * Creates or updates Matricula record linking student to class + # * Sends password setup email to newly created students + # * Updates import result counters + # + def import_dicente_and_matricula(dicente_data, turma) + identifier = dicente_data["usuario"] || dicente_data["matricula"] + dicente = Dicente.find_or_initialize_by(identifier: identifier) + + return unless process_dicente(dicente, dicente_data, identifier) + process_matricula(dicente, turma) + end + + def process_dicente(dicente, dicente_data, identifier) + if dicente.new_record? + create_new_dicente(dicente, dicente_data, identifier) + else + update_existing_dicente(dicente, dicente_data, identifier) + end + end + + def create_new_dicente(dicente, dicente_data, identifier) + assign_dicente_attributes(dicente, dicente_data) + setup_dicente_activation(dicente) + + if dicente.save + @result.created[:dicentes] += 1 + send_password_setup_email(dicente) + true + else + @result.errors << "Erro ao criar dicente #{identifier}: #{dicente.errors.full_messages.join(', ')}" + false + end + end + + def assign_dicente_attributes(dicente, dicente_data) + dicente.nome = dicente_data["nome"] + dicente.email = dicente_data["email"] + dicente.matricula = dicente_data["matricula"] + dicente.curso = dicente_data["curso"] + dicente.ocupacao = dicente_data["ocupacao"] || "dicente" + dicente.formacao = dicente_data["formacao"] || "graduando" + end + + def setup_dicente_activation(dicente) + dicente.pending_activation = true if dicente.respond_to?(:pending_activation) + dicente.password_reset_token = SecureRandom.urlsafe_base64(32) + dicente.password_reset_sent_at = Time.current + end + + def update_existing_dicente(dicente, dicente_data, identifier) + return true unless dicente_needs_update?(dicente, dicente_data) + + assign_dicente_attributes(dicente, dicente_data) + + if dicente.save + @result.updated[:dicentes] += 1 + true + else + @result.errors << "Erro ao atualizar dicente #{identifier}: #{dicente.errors.full_messages.join(', ')}" + false + end + end + + def dicente_needs_update?(dicente, dicente_data) + attributes_to_check = { + nome: dicente_data["nome"], + email: dicente_data["email"], + matricula: dicente_data["matricula"], + curso: dicente_data["curso"], + ocupacao: dicente_data["ocupacao"] || "dicente", + formacao: dicente_data["formacao"] || "graduando" + } + + changed = attributes_to_check.any? { |attr, new_value| dicente.send(attr) != new_value } + @result.skipped[:dicentes] += 1 unless changed + changed + end + + def process_matricula(dicente, turma) + matricula = Matricula.find_or_initialize_by(dicente: dicente, turma: turma) + + if matricula.new_record? + create_new_matricula(matricula) + else + update_matricula_if_needed(matricula) + end + end + + def create_new_matricula(matricula) + matricula.status = "ativo" + matricula.enrollment_date = Date.current + + if matricula.save + @result.created[:matriculas] += 1 + else + @result.errors << "Erro ao criar matrícula: #{matricula.errors.full_messages.join(', ')}" + end + end + + def update_matricula_if_needed(matricula) + return @result.skipped[:matriculas] += 1 if matricula.status == "ativo" + + matricula.status = "ativo" + if matricula.save + @result.updated[:matriculas] += 1 + else + @result.errors << "Erro ao atualizar matrícula: #{matricula.errors.full_messages.join(', ')}" + end + end + + def find_or_create_placeholder_docente + Docente.find_or_create_by!(identifier: "PLACEHOLDER_DOCENTE") do |d| + d.nome = "Docente Não Atribuído" + d.email = "placeholder@example.com" + d.departamento = "Não informado" + d.titulacao = "Não informado" + end + end + + def send_password_setup_email(user) + # Enviar email (síncrono em test, assíncrono em production) + if Rails.env.test? + PasswordSetupMailer.setup_instructions(user).deliver_now + else + PasswordSetupMailer.setup_instructions(user).deliver_later + end + rescue StandardError => e + # Log do erro mas não falha a importação + Rails.logger.error("Erro ao enviar email de configuração de senha para #{user.email}: #{e.message}") + @result.errors << "Email não enviado para #{user.email}: #{e.message}" + end +end diff --git a/app/views/avaliacoes/index.html.erb b/app/views/avaliacoes/index.html.erb new file mode 100644 index 0000000000..7a61cc955f --- /dev/null +++ b/app/views/avaliacoes/index.html.erb @@ -0,0 +1,116 @@ +<% content_for :title, "Avaliações" %> +<% selected_turma_ids = Array(@avaliacao_batch&.turma_ids).map(&:to_s) %> +<% selected_count = selected_turma_ids.count %> +<% selected_label = if selected_count.zero? + 'Nenhuma turma selecionada' + elsif selected_count == 1 + '1 turma selecionada' + else + "#{selected_count} turmas selecionadas" + end %> +<% due_date_value = begin + value = @avaliacao_batch&.due_date + value.present? ? value.to_date : Time.zone.today.end_of_month + rescue StandardError + Time.zone.today.end_of_month + end %> + +
+
+
+

Formulários de avaliação

+

Gere formulários para as turmas do semestre <%= @current_semester %> a partir dos templates cadastrados.

+
+ Total de formulários: <%= @avaliacoes.size %> +
+ +
+
+

Criar formulários

+ <%= form_with url: avaliacoes_path, scope: :avaliacao_batch, data: { controller: "evaluations" } do |form| %> +
+ <%= form.label :template_id, "Template", class: "field__label" %> + <%= form.collection_select :template_id, @templates, :id, :name, + { prompt: "Selecione um template" }, + { class: "field__input", data: { testid: "evaluation-template" }, required: true } %> +
+ +
+ <%= form.label :due_date, "Data limite", class: "field__label" %> + <%= form.date_field :due_date, value: due_date_value, class: "field__input", data: { testid: "evaluation-due-date" } %> +
+ +
+
+

Turmas disponíveis

+

+ <%= selected_label %> +

+
+
+ + +
+
+ + <% if @turmas.any? %> +
+ <% @turmas.each do |turma| %> + + <% end %> +
+ <% else %> +

Nenhuma turma cadastrada para o semestre atual.

+ <% end %> + +
+ <%= form.submit "Criar formulários", class: "button button--primary", data: { testid: "create-evaluations" } %> +
+ <% end %> +
+ +
+

Formulários existentes

+ <% if @avaliacoes.any? %> +
+ <% @avaliacoes.each do |avaliacao| %> +
+
+

<%= avaliacao.template&.name || "Sem template" %>

+

<%= avaliacao.title %>

+
+
+
+
Turma
+
<%= avaliacao.turma.class_code %> · <%= avaliacao.turma.semester %>
+
+
+
Docente
+
<%= avaliacao.docente.nome %>
+
+
+
Prazo
+
<%= l(avaliacao.due_date.to_date, format: :long) rescue avaliacao.due_date %>
+
+
+
Perguntas
+
<%= avaliacao.questoes.count %>
+
+
+
+ <% end %> +
+ <% else %> +

Nenhum formulário criado ainda.

+ <% end %> +
+
+
diff --git a/app/views/avaliacoes/pendentes.html.erb b/app/views/avaliacoes/pendentes.html.erb new file mode 100644 index 0000000000..a5eba438b1 --- /dev/null +++ b/app/views/avaliacoes/pendentes.html.erb @@ -0,0 +1,65 @@ +<% content_for :title, "Formulários Pendentes" %> + +
+
+
+

Formulários pendentes

+

Responda aos formulários das suas turmas antes do prazo limite.

+
+ Total: <%= @avaliacoes_pendentes.size %> +
+ + <% if @avaliacoes_pendentes.any? %> +
+ <% @avaliacoes_pendentes.each do |avaliacao| %> + <% days_until_due = (avaliacao.due_date.to_date - Date.today).to_i %> + <% is_urgent = days_until_due <= 3 && days_until_due >= 0 %> + <% is_overdue = days_until_due < 0 %> + +
+
+ <% if avaliacao.template&.name %> +

<%= avaliacao.template.name %>

+ <% end %> +

<%= avaliacao.title %>

+
+ +
+
+
Turma
+
<%= avaliacao.turma.materia.name %> — <%= avaliacao.turma.class_code %>
+
+
+
Semestre
+
<%= avaliacao.turma.semester %>
+
+
+
Professor(a)
+
<%= avaliacao.docente.nome %>
+
+
+
Prazo
+
+ <%= l(avaliacao.due_date.to_date, format: :long) rescue avaliacao.due_date %> + <% if is_overdue %> + Vencido + <% elsif is_urgent %> + + <%= days_until_due == 0 ? "Vence hoje" : "#{days_until_due} #{days_until_due == 1 ? 'dia' : 'dias'} restante#{'s' if days_until_due > 1}" %> + + <% end %> +
+
+
+ +
+ <%= link_to "Responder formulário", responder_avaliacao_path(avaliacao), class: "button button--primary", data: { testid: "answer-form" } %> +
+
+ <% end %> +
+ <% else %> +

Nenhum formulário pendente no momento.

+ <% end %> +
+ diff --git a/app/views/avaliacoes/responder.html.erb b/app/views/avaliacoes/responder.html.erb new file mode 100644 index 0000000000..a6987070b0 --- /dev/null +++ b/app/views/avaliacoes/responder.html.erb @@ -0,0 +1,100 @@ +<% content_for :title, "Responder Formulário" %> + +
+
+ + ← Voltar para formulários pendentes + +
+

<%= @avaliacao.title %>

+

+ Turma: <%= @avaliacao.turma.materia.name %> — <%= @avaliacao.turma.class_code %> · + Semestre: <%= @avaliacao.turma.semester %> · + Professor(a): <%= @avaliacao.docente.nome %> +

+

+ Prazo: <%= l(@avaliacao.due_date.to_date, format: :long) rescue @avaliacao.due_date %> +

+
+
+ + <%= form_with url: submeter_avaliacao_path(@avaliacao), method: :post, data: { testid: "answer-form" }, class: "answer-form", local: true do |form| %> + <% if @resposta.errors.any? %> +
+

Corrija os erros abaixo:

+
    + <% @resposta.errors.full_messages.each do |message| %> +
  • <%= message %>
  • + <% end %> +
+
+ <% end %> + +
+ <% @questoes.each_with_index do |questao, index| %> +
+
+

+ <%= questao.position %>. <%= questao.prompt %> + <% if questao.mandatory %> + * + <% end %> +

+
+ +
+ <% case questao.question_type %> + <% when 'likert' %> +
+ <% (questao.min_value..questao.max_value).each do |value| %> + + <% end %> +
+
+ Péssimo + Excelente +
+ + <% when 'multiple_choice' %> +
+ <% options = questao.options.present? ? JSON.parse(questao.options) : [] %> + <% options.each do |option| %> + + <% end %> +
+ + <% when 'text' %> +
+ <%= text_area_tag "respostas[#{questao.id}]", + @resposta.resposta_items.find_by(questao: questao)&.valor, + required: questao.mandatory, + rows: 4, + class: "field__input", + placeholder: "Digite sua resposta aqui...", + data: { testid: "campo-texto-pergunta" } %> +
+ <% end %> +
+
+ <% end %> +
+ +
+ <%= form.submit "Enviar", class: "button button--primary", data: { testid: "botao-enviar" } %> + <%= link_to "Cancelar", formularios_pendentes_path, class: "button button--ghost" %> +
+ <% end %> +
+ diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb new file mode 100644 index 0000000000..78f901399c --- /dev/null +++ b/app/views/layouts/application.html.erb @@ -0,0 +1,51 @@ + + + + <%= content_for(:title) || "Camaar" %> + + + + + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + + <%= yield :head %> + + <%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %> + <%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %> + + + + + + <%# Includes all stylesheet files in app/assets/stylesheets %> + <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %> + <%= javascript_importmap_tags %> + + + +
+
+ + +
+
+ +
+ <%= render "shared/flash" %> + <%= yield %> +
+ + diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb new file mode 100644 index 0000000000..3aac9002ed --- /dev/null +++ b/app/views/layouts/mailer.html.erb @@ -0,0 +1,13 @@ + + + + + + + + + <%= yield %> + + diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb new file mode 100644 index 0000000000..37f0bddbd7 --- /dev/null +++ b/app/views/layouts/mailer.text.erb @@ -0,0 +1 @@ +<%= yield %> diff --git a/app/views/password_setup_mailer/setup_instructions.html.erb b/app/views/password_setup_mailer/setup_instructions.html.erb new file mode 100644 index 0000000000..8ae5a4ebb2 --- /dev/null +++ b/app/views/password_setup_mailer/setup_instructions.html.erb @@ -0,0 +1,44 @@ + + + + + + +

Bem-vindo ao Sistema CAMAAR, <%= @user.nome %>!

+ +

+ Você foi cadastrado no Sistema CAMAAR - Coleta e Análise de Métricas de Avaliação Acadêmica em Rede. +

+ +

+ Para acessar o sistema, você precisa configurar sua senha. + Clique no link abaixo para definir sua senha: +

+ +

+ Configurar minha senha +

+ +

+ Ou copie e cole o seguinte link no seu navegador: +

+ +

+ <%= @setup_url %> +

+ +

+ Importante: Este link é válido por 24 horas. +

+ +

+ Após configurar sua senha, você poderá acessar o sistema utilizando seu email (<%= @user.email %>) + e a senha que você definiu. +

+ +

+ Atenciosamente,
+ Equipe CAMAAR +

+ + diff --git a/app/views/password_setup_mailer/setup_instructions.text.erb b/app/views/password_setup_mailer/setup_instructions.text.erb new file mode 100644 index 0000000000..b4bc2806eb --- /dev/null +++ b/app/views/password_setup_mailer/setup_instructions.text.erb @@ -0,0 +1,17 @@ +Bem-vindo ao Sistema CAMAAR, <%= @user.nome %>! +=============================================== + +Você foi cadastrado no Sistema CAMAAR - Coleta e Análise de Métricas de Avaliação Acadêmica em Rede. + +Para acessar o sistema, você precisa configurar sua senha. +Acesse o link abaixo para definir sua senha: + +<%= @setup_url %> + +IMPORTANTE: Este link é válido por 24 horas. + +Após configurar sua senha, você poderá acessar o sistema utilizando seu email (<%= @user.email %>) +e a senha que você definiu. + +Atenciosamente, +Equipe CAMAAR diff --git a/app/views/passwords/new.html.erb b/app/views/passwords/new.html.erb new file mode 100644 index 0000000000..548215b458 --- /dev/null +++ b/app/views/passwords/new.html.erb @@ -0,0 +1,170 @@ +<% content_for :title, "Defina sua Senha" %> + +
+
+

Defina sua Senha

+

Escolha uma senha segura para sua conta CAMAAR

+ + <% if flash[:alert] %> +
+ <%= flash[:alert] %> +
+ <% end %> + + <%= form_with url: password_setup_path, method: :post, data: { testid: "password-setup-form" }, class: "password-setup-form" do |form| %> + <%= hidden_field_tag :token, @token %> + +
+ <%= label_tag :password, "Nova Senha" %> + <%= password_field_tag :password, nil, + class: "form-input", + data: { testid: "password-input" }, + placeholder: "Digite sua nova senha", + required: true, + minlength: 6 %> + Mínimo 6 caracteres +
+ +
+ <%= label_tag :password_confirmation, "Confirmar Senha" %> + <%= password_field_tag :password_confirmation, nil, + class: "form-input", + data: { testid: "password-confirmation-input" }, + placeholder: "Digite sua senha novamente", + required: true %> +
+ +
+ <%= submit_tag "Definir Senha", + class: "button button--primary", + data: { testid: "set-password-button" } %> +
+ <% end %> + + +
+
+ + diff --git a/app/views/passwords/request_new.html.erb b/app/views/passwords/request_new.html.erb new file mode 100644 index 0000000000..96fe7667fe --- /dev/null +++ b/app/views/passwords/request_new.html.erb @@ -0,0 +1,86 @@ +<% content_for :title, "Solicitar Novo Link de Configuração de Senha" %> + +
+
+

Solicitar Novo Link

+

Entre em contato com seu administrador para receber um novo link de configuração de senha

+ +
+

Seu link de configuração de senha expirou ou é inválido.

+

Por favor, entre em contato com seu coordenador de curso ou administrador do sistema para solicitar um novo convite.

+
+ +
+ <%= link_to "Voltar para Login", login_path, class: "button button--secondary" %> +
+
+
+ + 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..2237a29479 --- /dev/null +++ b/app/views/resultados/index.html.erb @@ -0,0 +1,54 @@ +<% content_for :title, "Resultados" %> +
+
+
+

Resultados dos formulários

+

Consulte os formulários publicados e acompanhe as respostas enviadas pelos estudantes.

+
+ Total: <%= @avaliacoes.size %> +
+ + <%= form_with url: resultados_path, method: :get, class: "results-filter", data: { testid: "form-busca" }, local: true do |form| %> +
+ <%= form.label :q, "Buscar por título ou professor", class: "field__label" %> + <%= form.text_field :q, value: @query, placeholder: "Ex: Avaliação Docente", class: "field__input", data: { testid: "campo-busca" } %> +
+
+ <%= form.label :semester, "Semestre", class: "field__label" %> + <%= form.select :semester, options_for_select(@semester_options, @semester_filter), {}, class: "field__input", data: { testid: "filtro-semestre" } %> +
+
+ <%= form.submit "Buscar", class: "button button--primary", data: { testid: "botao-buscar" } %> +
+ <% end %> + + <% if @avaliacoes.any? %> +
+ <% @avaliacoes.each do |avaliacao| %> +
+
+

<%= avaliacao.turma.semester %>

+

<%= avaliacao.title %>

+
+
+
+
Professor(a)
+
<%= avaliacao.docente.nome %>
+
+
+
Total de respostas
+
<%= avaliacao.respostas.count %>
+
+
+ <%= link_to "Ver detalhes", resultado_path(avaliacao), class: "button button--ghost", data: { testid: "abrir-card" } %> +
+ <% end %> +
+ <% else %> + <% if @search_performed %> +

Nenhum resultado para a busca

+ <% else %> +

Nenhum formulário disponível no momento

+ <% end %> + <% end %> +
diff --git a/app/views/resultados/show.html.erb b/app/views/resultados/show.html.erb new file mode 100644 index 0000000000..253ce1bfad --- /dev/null +++ b/app/views/resultados/show.html.erb @@ -0,0 +1,56 @@ +<% content_for :title, @avaliacao.title %> +
+ <%= link_to "← Voltar para resultados", resultados_path, class: "results-back-link" %> + +
+
+

<%= @avaliacao.turma.semester %>

+

<%= @avaliacao.title %>

+

Professor(a): <%= @avaliacao.docente.nome %>

+

Prazo: <%= l(@avaliacao.due_date, format: :long) rescue @avaliacao.due_date %>

+

Total de respostas: <%= @summary[:total_responses] %>

+
+
+
+ Total de respostas + <%= @summary[:total_responses] %> +
+
+ Média geral + <%= @summary[:average_score] %> +
+
+ Taxa de conclusão + <%= @summary[:completion_rate] %>% +
+ <%= button_to "Exportar resultados", export_resultado_path(@avaliacao), method: :get, + class: "button button--primary", + data: { testid: "botao-exportar" } %> +
+
+ + <% if @summary[:total_responses].zero? %> +

Ainda não há respostas disponíveis

+ <% else %> +
+

Distribuição das respostas

+
+ <% @summary[:question_stats].each do |stat| %> +
+

<%= stat[:prompt] %>

+ <% stat[:distribution].each do |valor, total| %> + <% percentage = stat[:total].positive? ? ((total.to_f / stat[:total]) * 100).round : 0 %> +
+ <%= valor %> +
+ +
+ <%= total %> +
+ <% 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..13c9dba7b2 --- /dev/null +++ b/app/views/sessions/new.html.erb @@ -0,0 +1,131 @@ +<% content_for :title, "Login" %> + + + + diff --git a/app/views/shared/_flash.html.erb b/app/views/shared/_flash.html.erb new file mode 100644 index 0000000000..65350f3700 --- /dev/null +++ b/app/views/shared/_flash.html.erb @@ -0,0 +1,5 @@ +<% flash.each do |type, message| %> +
+ <%= message %> +
+<% end %> diff --git a/app/views/sigaa_imports/index.html.erb b/app/views/sigaa_imports/index.html.erb new file mode 100644 index 0000000000..7397c3cc09 --- /dev/null +++ b/app/views/sigaa_imports/index.html.erb @@ -0,0 +1,74 @@ +<% content_for :title, "Importação SIGAA" %> + +
+
+
+

Importação e Atualização de Dados do SIGAA

+

Sincronize a base de dados com informações do SIGAA. Importe arquivos específicos ou atualize automaticamente usando os arquivos do repositório.

+
+
+ Total: <%= Materia.count + Turma.count + Docente.count + Dicente.count + Matricula.count %> registros +
+
+ +
+ <%= link_to "Nova Importação", new_sigaa_import_path, class: "button button--primary", data: { testid: "new-import-button" } %> + + <%= form_with url: update_database_sigaa_imports_path, method: :post, local: true, class: "sigaa-imports-page__update-form" do |f| %> + <%= f.submit "🔄 Atualizar Base de Dados", + class: "button button--ghost", + data: { + testid: "update-database-button" + } %> + <% end %> +
+ +
+

+ Diferença entre Importação e Atualização:
+ Importação: Permite fazer upload de arquivos JSON específicos para importar dados.
+ Atualização: Usa automaticamente os arquivos classes.json e class_members.json do repositório para atualizar a base de dados, modificando registros existentes e adicionando novos. +

+
+ +
+

Status da Base de Dados

+ +
+
+
+

Matérias

+
+

<%= Materia.count %>

+
+ +
+
+

Turmas

+
+

<%= Turma.count %>

+
+ +
+
+

Docentes

+
+

<%= Docente.count %>

+
+ +
+
+

Dicentes

+
+

<%= Dicente.count %>

+
+ +
+
+

Matrículas

+
+

<%= Matricula.count %>

+
+
+
+
diff --git a/app/views/sigaa_imports/new.html.erb b/app/views/sigaa_imports/new.html.erb new file mode 100644 index 0000000000..e018abd747 --- /dev/null +++ b/app/views/sigaa_imports/new.html.erb @@ -0,0 +1,60 @@ +<% content_for :title, "Nova Importação SIGAA" %> + +
+
+
+

Nova Importação de Dados do SIGAA

+

Faça upload dos arquivos JSON do SIGAA para importar dados de matérias, turmas, docentes e dicentes.

+
+ <%= link_to "Voltar", sigaa_imports_path, class: "button button--ghost button--small", data: { testid: "back-button" } %> +
+ +
+

Selecionar arquivos

+ + <%= form_with url: sigaa_imports_path, multipart: true, local: true, data: { testid: "sigaa-import-form" } do |f| %> + <% if flash[:alert] %> +
+

Corrija os erros abaixo:

+
    +
  • <%= flash[:alert] %>
  • +
+
+ <% end %> + +
+ <%= f.label :classes_file, "Arquivo de Turmas e Matérias", class: "field__label" %> + <%= f.file_field :classes_file, + accept: "application/json", + class: "field__input", + data: { testid: "classes-file-input" } %> +

Formato esperado: JSON com dados de matérias e turmas (classes.json)

+
+ +
+ <%= f.label :class_members_file, "Arquivo de Participantes", class: "field__label" %> + <%= f.file_field :class_members_file, + accept: "application/json", + class: "field__input", + data: { testid: "members-file-input" } %> +

Formato esperado: JSON com dados de docentes e dicentes (class_members.json)

+
+ +
+

+ Dica: Você pode importar um ou ambos os arquivos. + O sistema evitará duplicação de registros automaticamente. +

+
+ +
+ <%= f.submit "Importar dados do SIGAA", + class: "button button--primary", + data: { testid: "submit-import" } %> + <%= link_to "Cancelar", sigaa_imports_path, + class: "button button--ghost", + data: { testid: "cancel-import" } %> +
+ <% end %> +
+
diff --git a/app/views/templates/_question_fields.html.erb b/app/views/templates/_question_fields.html.erb new file mode 100644 index 0000000000..7a3378ef84 --- /dev/null +++ b/app/views/templates/_question_fields.html.erb @@ -0,0 +1,57 @@ +<% question_type_options = [ + ["Escala 1-5 (Likert)", TemplateQuestion::QUESTION_TYPES[:likert]], + ["Múltipla escolha", TemplateQuestion::QUESTION_TYPES[:multiple_choice]], + ["Resposta aberta", TemplateQuestion::QUESTION_TYPES[:text]] +] %> + +<% question = f.object %> +
+
+ Pergunta <%= question.position || 1 %> + +
+ +
+ <%= f.label :prompt, "Enunciado", class: "field__label" %> + <%= f.text_field :prompt, class: "field__input", data: { testid: "question-prompt" } %> +
+ +
+ <%= f.label :question_type, "Tipo da questão", class: "field__label" %> + <%= f.select :question_type, question_type_options, {}, class: "field__input", data: { testid: "question-type", behavior: "question-type" } %> +
+ +
+
+ <%= f.label :min_value, "Valor mínimo", class: "field__label" %> + <%= f.number_field :min_value, min: 1, max: 5, step: 1, class: "field__input", data: { testid: "question-min" } %> +
+
+ <%= f.label :max_value, "Valor máximo", class: "field__label" %> + <%= f.number_field :max_value, min: 1, max: 5, step: 1, class: "field__input", data: { testid: "question-max" } %> +
+
+ +
+ <%= f.label :options_text, "Opções (uma por linha)", class: "field__label" %> + <%= f.text_area :options_text, rows: 3, class: "field__input", data: { testid: "question-options" } %> +
+ +
+ +
+ +
+ +
+ + <%= f.hidden_field :position, data: { testid: "question-position" } %> +
diff --git a/app/views/templates/index.html.erb b/app/views/templates/index.html.erb new file mode 100644 index 0000000000..3b9bc01642 --- /dev/null +++ b/app/views/templates/index.html.erb @@ -0,0 +1,169 @@ +<% content_for :title, "Templates" %> + +
+
+
+

Templates de formulário

+

Estruture perguntas padronizadas para reutilizar nas avaliações das turmas.

+
+
+ Total: <%= @templates.size %> +
+
+ +
+
+ <% editing = @template.persisted? %> +

<%= editing ? "Editar template" : "Novo template" %>

+ + <% if editing %> + <% cancel_path = @current_admin_id ? management_templates_path(admin_id: @current_admin_id) : templates_path %> +
+

Você está editando o template <%= @template.name %>.

+ <%= link_to "Cancelar edição", cancel_path, class: "button button--ghost button--small", data: { testid: "cancel-edit-template" } %> +
+ <% end %> + + <% if @docentes.empty? %> +
+ Cadastre pelo menos um docente para criar templates. +
+ <% else %> + <% form_url = if editing + @current_admin_id ? template_path(@template, admin_id: @current_admin_id) : template_path(@template) + else + @current_admin_id ? templates_path(admin_id: @current_admin_id) : templates_path + end %> + <% form_method = editing ? :patch : :post %> + <%= form_with model: @template, + url: form_url, + method: form_method, + data: { testid: "template-form", template_form: "container" }, + class: "template-form__form" do |form| %> + <% if @template.errors.any? || @template.template_questions.any? { |question| question.errors.any? } %> + <% error_messages = @template.errors.full_messages.dup %> + <% @template.template_questions.each do |question| %> + <% error_messages.concat(question.errors.full_messages) %> + <% end %> +
+

Corrija os erros abaixo:

+
    + <% error_messages.each do |message| %> +
  • <%= message %>
  • + <% end %> +
+
+ <% end %> + +
+ <%= form.label :name, "Nome", class: "field__label" %> + <%= form.text_field :name, class: "field__input", required: true, data: { testid: "template-name" } %> +
+ +
+ <%= form.label :description, "Descrição", class: "field__label" %> + <%= form.text_area :description, rows: 3, class: "field__input", data: { testid: "template-description" } %> +
+ +
+ <%= form.label :docente_id, "Responsável", class: "field__label" %> + <%= form.collection_select :docente_id, @docentes, :id, :nome, + { prompt: "Selecione um docente" }, + { class: "field__input", data: { testid: "template-docente" } } %> +
+ +
+
+

Perguntas

+

Adicione pelo menos uma questão para salvar o template.

+
+ +
+ +
+ <%= form.fields_for :template_questions do |question_form| %> + <%= render "question_fields", f: question_form %> + <% end %> +
+ + + +
+ <% submit_label = editing ? "Salvar template" : "Criar template" %> + <%= form.submit submit_label, class: "button button--primary", data: { testid: "submit-template" } %> +
+ <% end %> + <% end %> +
+ +
+

Templates cadastrados

+ <% if @templates.any? %> +
+ <% @templates.each do |template| %> +
+
+

<%= template.docente.nome %>

+

<%= template.name %>

+ <% if template.description.present? %> +

<%= template.description %>

+ <% end %> +
+ +
+ <% admin_scope_id = @current_admin_id || template.docente_id %> + <%= link_to "Editar", + edit_template_path(template, admin_id: admin_scope_id), + class: "button button--ghost button--small", + data: { testid: "edit-template" } %> + <%= button_to "Excluir", + template_path(template, admin_id: admin_scope_id), + method: :delete, + data: { testid: "delete-template" }, + form: { class: "template-card__delete-form" }, + class: "button button--danger button--small" %> +
+ +
+
+
Status
+
<%= template.status.humanize %>
+
+
+
Perguntas
+
<%= template.template_questions.count %>
+
+
+ +
+ Ver perguntas +
    + <% template.template_questions.sort_by(&:position).each do |question| %> +
  1. + <%= question.prompt %> + <%= question.question_type.humanize %> +
  2. + <% end %> +
+
+
+ <% end %> +
+ <% else %> +

Nenhum template cadastrado ainda

+ <% end %> +
+
+
diff --git a/bin/brakeman b/bin/brakeman new file mode 100755 index 0000000000..ace1c9ba08 --- /dev/null +++ b/bin/brakeman @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby +require "rubygems" +require "bundler/setup" + +ARGV.unshift("--ensure-latest") + +load Gem.bin_path("brakeman", "brakeman") diff --git a/bin/bundler-audit b/bin/bundler-audit new file mode 100755 index 0000000000..e2ef22690c --- /dev/null +++ b/bin/bundler-audit @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby +require_relative "../config/boot" +require "bundler/audit/cli" + +ARGV.concat %w[ --config config/bundler-audit.yml ] if ARGV.empty? || ARGV.include?("check") +Bundler::Audit::CLI.start diff --git a/bin/ci b/bin/ci new file mode 100755 index 0000000000..4137ad5bb0 --- /dev/null +++ b/bin/ci @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby +require_relative "../config/boot" +require "active_support/continuous_integration" + +CI = ActiveSupport::ContinuousIntegration +require_relative "../config/ci.rb" diff --git a/bin/cucumber b/bin/cucumber new file mode 100755 index 0000000000..eb5e962e86 --- /dev/null +++ b/bin/cucumber @@ -0,0 +1,11 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +vendored_cucumber_bin = Dir["#{File.dirname(__FILE__)}/../vendor/{gems,plugins}/cucumber*/bin/cucumber"].first +if vendored_cucumber_bin + load File.expand_path(vendored_cucumber_bin) +else + require 'rubygems' unless ENV['NO_RUBYGEMS'] + require 'cucumber' + load Cucumber::BINARY +end diff --git a/bin/dev b/bin/dev new file mode 100755 index 0000000000..5f91c20545 --- /dev/null +++ b/bin/dev @@ -0,0 +1,2 @@ +#!/usr/bin/env ruby +exec "./bin/rails", "server", *ARGV diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint new file mode 100755 index 0000000000..ed31659f40 --- /dev/null +++ b/bin/docker-entrypoint @@ -0,0 +1,8 @@ +#!/bin/bash -e + +# If running the rails server then create or migrate existing database +if [ "${@: -2:1}" == "./bin/rails" ] && [ "${@: -1:1}" == "server" ]; then + ./bin/rails db:prepare +fi + +exec "${@}" diff --git a/bin/importmap b/bin/importmap new file mode 100755 index 0000000000..36502ab16c --- /dev/null +++ b/bin/importmap @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby + +require_relative "../config/application" +require "importmap/commands" diff --git a/bin/rails b/bin/rails new file mode 100755 index 0000000000..efc0377492 --- /dev/null +++ b/bin/rails @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +APP_PATH = File.expand_path("../config/application", __dir__) +require_relative "../config/boot" +require "rails/commands" diff --git a/bin/rake b/bin/rake new file mode 100755 index 0000000000..4fbf10b960 --- /dev/null +++ b/bin/rake @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +require_relative "../config/boot" +require "rake" +Rake.application.run diff --git a/bin/rubocop b/bin/rubocop new file mode 100755 index 0000000000..5a20504716 --- /dev/null +++ b/bin/rubocop @@ -0,0 +1,8 @@ +#!/usr/bin/env ruby +require "rubygems" +require "bundler/setup" + +# Explicit RuboCop config increases performance slightly while avoiding config confusion. +ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__)) + +load Gem.bin_path("rubocop", "rubocop") diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000000..81be011e87 --- /dev/null +++ b/bin/setup @@ -0,0 +1,35 @@ +#!/usr/bin/env ruby +require "fileutils" + +APP_ROOT = File.expand_path("..", __dir__) + +def system!(*args) + system(*args, exception: true) +end + +FileUtils.chdir APP_ROOT do + # This script is a way to set up or update your development environment automatically. + # This script is idempotent, so that you can run it at any time and get an expectable outcome. + # Add necessary setup steps to this file. + + puts "== Installing dependencies ==" + system("bundle check") || system!("bundle install") + + # puts "\n== Copying sample files ==" + # unless File.exist?("config/database.yml") + # FileUtils.cp "config/database.yml.sample", "config/database.yml" + # end + + puts "\n== Preparing database ==" + system! "bin/rails db:prepare" + system! "bin/rails db:reset" if ARGV.include?("--reset") + + puts "\n== Removing old logs and tempfiles ==" + system! "bin/rails log:clear tmp:clear" + + unless ARGV.include?("--skip-server") + puts "\n== Starting development server ==" + STDOUT.flush # flush the output before exec(2) so that it displays + exec "bin/dev" + end +end diff --git a/bin/thrust b/bin/thrust new file mode 100755 index 0000000000..36bde2d832 --- /dev/null +++ b/bin/thrust @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("thruster", "thrust") diff --git a/config.ru b/config.ru new file mode 100644 index 0000000000..4a3c09a688 --- /dev/null +++ b/config.ru @@ -0,0 +1,6 @@ +# This file is used by Rack-based servers to start the application. + +require_relative "config/environment" + +run Rails.application +Rails.application.load_server diff --git a/config/application.rb b/config/application.rb new file mode 100644 index 0000000000..b4848d838b --- /dev/null +++ b/config/application.rb @@ -0,0 +1,27 @@ +require_relative "boot" + +require "rails/all" + +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(*Rails.groups) + +module Camaar + class Application < Rails::Application + # Initialize configuration defaults for originally generated Rails version. + config.load_defaults 8.1 + + # Please, add to the `ignore` list any other `lib` subdirectories that do + # not contain `.rb` files, or that should not be reloaded or eager loaded. + # Common ones are `templates`, `generators`, or `middleware`, for example. + config.autoload_lib(ignore: %w[assets tasks]) + + # Configuration for the application, engines, and railties goes here. + # + # These settings can be overridden in specific environments using the files + # in config/environments, which are processed later. + # + # config.time_zone = "Central Time (US & Canada)" + # config.eager_load_paths << Rails.root.join("extras") + end +end diff --git a/config/boot.rb b/config/boot.rb new file mode 100644 index 0000000000..988a5ddc46 --- /dev/null +++ b/config/boot.rb @@ -0,0 +1,4 @@ +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +require "bundler/setup" # Set up gems listed in the Gemfile. +require "bootsnap/setup" # Speed up boot time by caching expensive operations. diff --git a/config/bundler-audit.yml b/config/bundler-audit.yml new file mode 100644 index 0000000000..e74b3af949 --- /dev/null +++ b/config/bundler-audit.yml @@ -0,0 +1,5 @@ +# Audit all gems listed in the Gemfile for known security problems by running bin/bundler-audit. +# CVEs that are not relevant to the application can be enumerated on the ignore list below. + +ignore: + - CVE-THAT-DOES-NOT-APPLY diff --git a/config/cable.yml b/config/cable.yml new file mode 100644 index 0000000000..21ab7228f9 --- /dev/null +++ b/config/cable.yml @@ -0,0 +1,10 @@ +development: + adapter: async + +test: + adapter: test + +production: + adapter: redis + url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> + channel_prefix: camaar_production diff --git a/config/ci.rb b/config/ci.rb new file mode 100644 index 0000000000..e56a92e418 --- /dev/null +++ b/config/ci.rb @@ -0,0 +1,23 @@ +# Run using bin/ci + +CI.run do + step "Setup", "bin/setup --skip-server" + + step "Style: Ruby", "bin/rubocop" + + step "Security: Gem audit", "bin/bundler-audit" + step "Security: Importmap vulnerability audit", "bin/importmap audit" + step "Security: Brakeman code analysis", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error" + + step "Tests: Rails", "bin/rails test" + step "Tests: System", "bin/rails test:system" + step "Tests: Seeds", "env RAILS_ENV=test bin/rails db:seed:replant" + + # Optional: set a green GitHub commit status to unblock PR merge. + # Requires the `gh` CLI and `gh extension install basecamp/gh-signoff`. + # if success? + # step "Signoff: All systems go. Ready for merge and deploy.", "gh signoff" + # else + # failure "Signoff: CI failed. Do not merge or deploy.", "Fix the issues and try again." + # end +end diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc new file mode 100644 index 0000000000..d213710c2b --- /dev/null +++ b/config/credentials.yml.enc @@ -0,0 +1 @@ +3ZvB+9p9Dd6LxCqwnh/avw5JbLDQvUyey05wwNVb9Y4bSYtFM75zSqH4+59W+B5uoqYT8X+RaLULbOOqPQ+c1K4UEZR4mj16rOv4kACejkTfg/XfccVlRCKUfjPZwQmbHBVGTU7MsKeKsU2KT2qMTPaZ7UD/4kiDrkptFonxnx+1Gbsc/MYIcW0NmVI12CkCy4FKokCSxeeNy35pV2j7qMfavvwBaBq1oPSYlO8jjztIdg0u05s5OJlNWQjHodSyg4rwao4Ia5h5oweCJlL9j/vK9hmVPSh1xnfEbizic0aaNXKRvcZylkwdHiTa+Gmcl0KhBC8n36SYwK5Yq3lbXaJNPm2TvdMlemXE/zeXSbs1dyyMrS/YbmLiTD0FwMAIdl4RLghKrFRiWPOigp4GZrhjK9c3XeDuCn8bLCAdxWocRvGHYZMnSOt32uHq1X0lkwBmrtZuIkdOh6KoPlM17yYCBfCZUXRIZwdA4vU0md9cOscEHaXYLTEl--Ejtd3cyO8x/opFxr--io0jss1RikHSbSZ1xv5jdg== \ No newline at end of file diff --git a/config/cucumber.yml b/config/cucumber.yml new file mode 100644 index 0000000000..47a4663ae2 --- /dev/null +++ b/config/cucumber.yml @@ -0,0 +1,8 @@ +<% +rerun = File.file?('rerun.txt') ? IO.read('rerun.txt') : "" +rerun = rerun.strip.gsub /\s/, ' ' +rerun_opts = rerun.empty? ? "--format #{ENV['CUCUMBER_FORMAT'] || 'progress'} features" : "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} #{rerun}" +std_opts = "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} --strict --tags 'not @wip'" +%> +default: <%= std_opts %> features +rerun: <%= rerun_opts %> --format rerun --out rerun.txt --strict --tags 'not @wip' diff --git a/config/database.yml b/config/database.yml new file mode 100644 index 0000000000..693252b7c3 --- /dev/null +++ b/config/database.yml @@ -0,0 +1,41 @@ +# SQLite. Versions 3.8.0 and up are supported. +# gem install sqlite3 +# +# Ensure the SQLite 3 gem is defined in your Gemfile +# gem "sqlite3" +# +default: &default + adapter: sqlite3 + max_connections: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + timeout: 5000 + +development: + <<: *default + database: storage/development.sqlite3 + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + <<: *default + database: storage/test.sqlite3 + + +# Store production database in the storage/ directory, which by default +# is mounted as a persistent Docker volume in config/deploy.yml. +production: + primary: + <<: *default + database: storage/production.sqlite3 + cache: + <<: *default + database: storage/production_cache.sqlite3 + migrations_paths: db/cache_migrate + queue: + <<: *default + database: storage/production_queue.sqlite3 + migrations_paths: db/queue_migrate + cable: + <<: *default + database: storage/production_cable.sqlite3 + migrations_paths: db/cable_migrate diff --git a/config/environment.rb b/config/environment.rb new file mode 100644 index 0000000000..cac5315775 --- /dev/null +++ b/config/environment.rb @@ -0,0 +1,5 @@ +# Load the Rails application. +require_relative "application" + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/config/environments/development.rb b/config/environments/development.rb new file mode 100644 index 0000000000..4a6ea19c9c --- /dev/null +++ b/config/environments/development.rb @@ -0,0 +1,82 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Configure 'rails notes' to inspect Cucumber files + config.annotations.register_directories("features") + config.annotations.register_extensions("feature") { |tag| /#\s*(#{tag}):?\s*(.*)$/ } + + # Settings specified here will take precedence over those in config/application.rb. + + # Make code changes take effect immediately without server restart. + config.enable_reloading = true + + # Do not eager load code on boot. + config.eager_load = false + + # Show full error reports. + config.consider_all_requests_local = true + + # Enable server timing. + config.server_timing = true + + # Enable/disable Action Controller caching. By default Action Controller caching is disabled. + # Run rails dev:cache to toggle Action Controller caching. + if Rails.root.join("tmp/caching-dev.txt").exist? + config.action_controller.perform_caching = true + config.action_controller.enable_fragment_cache_logging = true + config.public_file_server.headers = { "cache-control" => "public, max-age=#{2.days.to_i}" } + else + config.action_controller.perform_caching = false + end + + # Change to :null_store to avoid any caching. + config.cache_store = :memory_store + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + + # Don't care if the mailer can't send. + config.action_mailer.raise_delivery_errors = false + + # Make template changes take effect immediately. + config.action_mailer.perform_caching = false + + # Set localhost to be used by links generated in mailer templates. + config.action_mailer.default_url_options = { host: "localhost", port: 3000 } + + # Print deprecation notices to the Rails logger. + config.active_support.deprecation = :log + + # Raise an error on page load if there are pending migrations. + config.active_record.migration_error = :page_load + + # Highlight code that triggered database queries in logs. + config.active_record.verbose_query_logs = true + + # Append comments with runtime information tags to SQL queries in logs. + config.active_record.query_log_tags_enabled = true + + # Highlight code that enqueued background job in logs. + config.active_job.verbose_enqueue_logs = true + + # Highlight code that triggered redirect in logs. + config.action_dispatch.verbose_redirect_logs = true + + # Suppress logger output for asset requests. + config.assets.quiet = true + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + config.action_view.annotate_rendered_view_with_filenames = true + + # Uncomment if you wish to allow Action Cable access from any origin. + # config.action_cable.disable_request_forgery_protection = true + + # Raise error when a before_action's only/except options reference missing actions. + config.action_controller.raise_on_missing_callback_actions = true + + # Apply autocorrection by RuboCop to files generated by `bin/rails generate`. + # config.generators.apply_rubocop_autocorrect_after_generate! +end diff --git a/config/environments/production.rb b/config/environments/production.rb new file mode 100644 index 0000000000..cb0241df64 --- /dev/null +++ b/config/environments/production.rb @@ -0,0 +1,89 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Code is not reloaded between requests. + config.enable_reloading = false + + # Eager load code on boot for better performance and memory savings (ignored by Rake tasks). + config.eager_load = true + + # Full error reports are disabled. + config.consider_all_requests_local = false + + # Turn on fragment caching in view templates. + config.action_controller.perform_caching = true + + # Cache assets for far-future expiry since they are all digest stamped. + config.public_file_server.headers = { "cache-control" => "public, max-age=#{1.year.to_i}" } + + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.asset_host = "http://assets.example.com" + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + + # Assume all access to the app is happening through a SSL-terminating reverse proxy. + # config.assume_ssl = true + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + # config.force_ssl = true + + # Skip http-to-https redirect for the default health check endpoint. + # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } } + + # Log to STDOUT with the current request id as a default log tag. + config.log_tags = [ :request_id ] + config.logger = ActiveSupport::TaggedLogging.logger(STDOUT) + + # Change to "debug" to log everything (including potentially personally-identifiable information!). + config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") + + # Prevent health checks from clogging up the logs. + config.silence_healthcheck_path = "/up" + + # Don't log any deprecations. + config.active_support.report_deprecations = false + + # Replace the default in-process memory cache store with a durable alternative. + # config.cache_store = :mem_cache_store + + # Replace the default in-process and non-durable queuing backend for Active Job. + # config.active_job.queue_adapter = :resque + + # Ignore bad email addresses and do not raise email delivery errors. + # Set this to true and configure the email server for immediate delivery to raise delivery errors. + # config.action_mailer.raise_delivery_errors = false + + # Set host to be used by links generated in mailer templates. + config.action_mailer.default_url_options = { host: "example.com" } + + # Specify outgoing SMTP server. Remember to add smtp/* credentials via bin/rails credentials:edit. + # config.action_mailer.smtp_settings = { + # user_name: Rails.application.credentials.dig(:smtp, :user_name), + # password: Rails.application.credentials.dig(:smtp, :password), + # address: "smtp.example.com", + # port: 587, + # authentication: :plain + # } + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true + + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false + + # Only use :id for inspections in production. + config.active_record.attributes_for_inspect = [ :id ] + + # Enable DNS rebinding protection and other `Host` header attacks. + # config.hosts = [ + # "example.com", # Allow requests from example.com + # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` + # ] + # + # Skip DNS rebinding protection for the default health check endpoint. + # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } +end diff --git a/config/environments/test.rb b/config/environments/test.rb new file mode 100644 index 0000000000..29d195b837 --- /dev/null +++ b/config/environments/test.rb @@ -0,0 +1,57 @@ +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! + +Rails.application.configure do + # Configure 'rails notes' to inspect Cucumber files + config.annotations.register_directories("features") + config.annotations.register_extensions("feature") { |tag| /#\s*(#{tag}):?\s*(.*)$/ } + + # Settings specified here will take precedence over those in config/application.rb. + + # While tests run files are not watched, reloading is not necessary. + config.enable_reloading = false + + # Eager loading loads your entire application. When running a single test locally, + # this is usually not necessary, and can slow down your test suite. However, it's + # recommended that you enable it in continuous integration systems to ensure eager + # loading is working properly before deploying your code. + config.eager_load = ENV["CI"].present? + + # Configure public file server for tests with cache-control for performance. + config.public_file_server.headers = { "cache-control" => "public, max-age=3600" } + + # Show full error reports. + config.consider_all_requests_local = true + config.cache_store = :null_store + + # Render exception templates for rescuable exceptions and raise for other exceptions. + config.action_dispatch.show_exceptions = :rescuable + + # Disable request forgery protection in test environment. + config.action_controller.allow_forgery_protection = false + + # Store uploaded files on the local file system in a temporary directory. + config.active_storage.service = :test + + # Tell Action Mailer not to deliver emails to the real world. + # The :test delivery method accumulates sent emails in the + # ActionMailer::Base.deliveries array. + config.action_mailer.delivery_method = :test + + # Set host to be used by links generated in mailer templates. + config.action_mailer.default_url_options = { host: "example.com" } + + # Print deprecation notices to the stderr. + config.active_support.deprecation = :stderr + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true + + # Raise error when a before_action's only/except options reference missing actions. + config.action_controller.raise_on_missing_callback_actions = true +end diff --git a/config/importmap.rb b/config/importmap.rb new file mode 100644 index 0000000000..0086a327b8 --- /dev/null +++ b/config/importmap.rb @@ -0,0 +1,3 @@ +# Pin npm packages by running ./bin/importmap + +pin "application" diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb new file mode 100644 index 0000000000..487324424f --- /dev/null +++ b/config/initializers/assets.rb @@ -0,0 +1,7 @@ +# Be sure to restart your server when you modify this file. + +# Version of your assets, change this if you want to expire all your assets. +Rails.application.config.assets.version = "1.0" + +# Add additional assets to the asset load path. +# Rails.application.config.assets.paths << Emoji.images_path diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb new file mode 100644 index 0000000000..d51d713979 --- /dev/null +++ b/config/initializers/content_security_policy.rb @@ -0,0 +1,29 @@ +# Be sure to restart your server when you modify this file. + +# Define an application-wide content security policy. +# See the Securing Rails Applications Guide for more information: +# https://guides.rubyonrails.org/security.html#content-security-policy-header + +# Rails.application.configure do +# config.content_security_policy do |policy| +# policy.default_src :self, :https +# policy.font_src :self, :https, :data +# policy.img_src :self, :https, :data +# policy.object_src :none +# policy.script_src :self, :https +# policy.style_src :self, :https +# # Specify URI for violation reports +# # policy.report_uri "/csp-violation-report-endpoint" +# end +# +# # Generate session nonces for permitted importmap, inline scripts, and inline styles. +# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } +# config.content_security_policy_nonce_directives = %w(script-src style-src) +# +# # Automatically add `nonce` to `javascript_tag`, `javascript_include_tag`, and `stylesheet_link_tag` +# # if the corresponding directives are specified in `content_security_policy_nonce_directives`. +# # config.content_security_policy_nonce_auto = true +# +# # Report violations without enforcing the policy. +# # config.content_security_policy_report_only = true +# end diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb new file mode 100644 index 0000000000..c0b717f7ec --- /dev/null +++ b/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. +# Use this to limit dissemination of sensitive information. +# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. +Rails.application.config.filter_parameters += [ + :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc +] diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb new file mode 100644 index 0000000000..0afce93103 --- /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.irregular "materia", "materias" + inflect.irregular "avaliacao", "avaliacoes" + inflect.irregular "questao", "questoes" + inflect.irregular "resposta", "respostas" +end + +# These inflection rules are supported but not enabled by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.acronym "RESTful" +# end diff --git a/config/locales/en.yml b/config/locales/en.yml new file mode 100644 index 0000000000..6c349ae5e3 --- /dev/null +++ b/config/locales/en.yml @@ -0,0 +1,31 @@ +# Files in the config/locales directory are used for internationalization and +# are automatically loaded by Rails. If you want to use locales other than +# English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t "hello" +# +# In views, this is aliased to just `t`: +# +# <%= t("hello") %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# To learn more about the API, please read the Rails Internationalization guide +# at https://guides.rubyonrails.org/i18n.html. +# +# Be aware that YAML interprets the following case-insensitive strings as +# booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings +# must be quoted to be interpreted as strings. For example: +# +# en: +# "yes": yup +# enabled: "ON" + +en: + hello: "Hello world" diff --git a/config/master.key b/config/master.key new file mode 100644 index 0000000000..3a2cf052d5 --- /dev/null +++ b/config/master.key @@ -0,0 +1 @@ +6eb1b7c2bf7b3c4eb1fbe8786d13ba47 \ No newline at end of file diff --git a/config/puma.rb b/config/puma.rb new file mode 100644 index 0000000000..38c4b86596 --- /dev/null +++ b/config/puma.rb @@ -0,0 +1,42 @@ +# This configuration file will be evaluated by Puma. The top-level methods that +# are invoked here are part of Puma's configuration DSL. For more information +# about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. +# +# Puma starts a configurable number of processes (workers) and each process +# serves each request in a thread from an internal thread pool. +# +# You can control the number of workers using ENV["WEB_CONCURRENCY"]. You +# should only set this value when you want to run 2 or more workers. The +# default is already 1. You can set it to `auto` to automatically start a worker +# for each available processor. +# +# The ideal number of threads per worker depends both on how much time the +# application spends waiting for IO operations and on how much you wish to +# prioritize throughput over latency. +# +# As a rule of thumb, increasing the number of threads will increase how much +# traffic a given process can handle (throughput), but due to CRuby's +# Global VM Lock (GVL) it has diminishing returns and will degrade the +# response time (latency) of the application. +# +# The default is set to 3 threads as it's deemed a decent compromise between +# throughput and latency for the average Rails application. +# +# Any libraries that use a connection pool or another resource pool should +# be configured to provide at least as many connections as the number of +# threads. This includes Active Record's `pool` parameter in `database.yml`. +threads_count = ENV.fetch("RAILS_MAX_THREADS", 3) +threads threads_count, threads_count + +# Specifies the `port` that Puma will listen on to receive requests; default is 3000. +port ENV.fetch("PORT", 3000) + +# Allow puma to be restarted by `bin/rails restart` command. +plugin :tmp_restart + +# Run the Solid Queue supervisor inside of Puma for single-server deployments. +plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"] + +# Specify the PID file. Defaults to tmp/pids/server.pid in development. +# In other environments, only set the PID file if requested. +pidfile ENV["PIDFILE"] if ENV["PIDFILE"] diff --git a/config/routes.rb b/config/routes.rb new file mode 100644 index 0000000000..45b5570583 --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,45 @@ +Rails.application.routes.draw do + # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html + + # Authentication routes + get "login", to: "sessions#new", as: :login + post "login", to: "sessions#create" + delete "logout", to: "sessions#destroy", as: :logout + + # Password setup routes + get "password/setup", to: "passwords#new", as: :password_setup + post "password/setup", to: "passwords#create" + get "password/request_new", to: "passwords#request_new", as: :request_new_password + + resources :templates, only: %i[index create edit update destroy] + get "gerenciamento/templates", to: "templates#index", as: :management_templates + resources :avaliacoes, only: %i[index create] do + member do + get :responder + post :submeter + end + end + get "formularios/pendentes", to: "avaliacoes#pendentes", as: :formularios_pendentes + resources :resultados, only: %i[index show] do + member do + get :export + end + end + + root "sessions#new" + + # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. + # Can be used by load balancers and uptime monitors to verify that the app is live. + get "up" => "rails/health#show", as: :rails_health_check + + # Render dynamic PWA files from app/views/pwa/* (remember to link manifest in application.html.erb) + # get "manifest" => "rails/pwa#manifest", as: :pwa_manifest + # get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker + + # Sigaa Import routes + resources :sigaa_imports, only: [ :new, :create, :index ] do + collection do + post :update_database + end + end +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..e8f458c362 --- /dev/null +++ b/coverage/.last_run.json @@ -0,0 +1,5 @@ +{ + "result": { + "line": 92.82 + } +} diff --git a/coverage/.resultset.json b/coverage/.resultset.json new file mode 100644 index 0000000000..b9b2446672 --- /dev/null +++ b/coverage/.resultset.json @@ -0,0 +1,3428 @@ +{ + "RSpec": { + "coverage": { + "/workspaces/CAMAAR/app/helpers/application_helper.rb": { + "lines": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null + ] + }, + "/workspaces/CAMAAR/app/mailers/password_setup_mailer.rb": { + "lines": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 71, + 71, + null, + 71, + null, + null, + null, + null, + null + ] + }, + "/workspaces/CAMAAR/app/mailers/application_mailer.rb": { + "lines": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + 1, + null + ] + }, + "/workspaces/CAMAAR/app/models/avaliacao.rb": { + "lines": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + 1, + null, + null, + null, + null, + null, + 1, + 1, + 1, + null, + 1, + 1, + null, + 1, + null, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 88, + null, + null, + null, + null, + null, + null, + null + ] + }, + "/workspaces/CAMAAR/app/models/application_record.rb": { + "lines": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null + ] + }, + "/workspaces/CAMAAR/app/models/dicente.rb": { + "lines": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + 1, + 1, + 1, + null, + 1, + null + ] + }, + "/workspaces/CAMAAR/app/models/usuario.rb": { + "lines": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + 1, + null, + 1, + 1, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + 1, + 3, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 302, + null, + null + ] + }, + "/workspaces/CAMAAR/app/models/docente.rb": { + "lines": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + 1, + 1, + null, + 1, + null + ] + }, + "/workspaces/CAMAAR/app/models/materia.rb": { + "lines": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + 1, + null, + 1, + 1, + null + ] + }, + "/workspaces/CAMAAR/app/models/matricula.rb": { + "lines": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + 1, + null, + 1, + null, + null, + null, + null, + null, + 1, + null + ] + }, + "/workspaces/CAMAAR/app/models/questao.rb": { + "lines": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + null, + 1, + null, + 1, + 1, + null, + 1, + null, + 1, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + 1, + 2, + null, + null + ] + }, + "/workspaces/CAMAAR/app/models/template_question.rb": { + "lines": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + 1, + null, + null, + null, + null, + null, + 1, + null, + 1, + null, + 1, + null, + 1, + 1, + 1, + 1, + 1, + null, + null, + null, + null, + null, + null, + 1, + 57, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + 1, + 51, + null, + null, + 1, + null, + null, + null, + null, + null, + 1, + 214, + 214, + null, + null, + null, + null, + null, + 1, + 214, + 214, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + 1, + 214, + null, + 5, + null, + 205, + null, + 4, + null, + null, + null, + null, + null, + null, + 1, + 5, + 5, + 5, + null, + null, + null, + null, + null, + 1, + 205, + 205, + 205, + null, + null, + null, + null, + null, + 1, + 4, + 4, + 4, + null, + null, + null, + null, + null, + 1, + 5, + 5, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 5, + 1, + null, + 4, + null, + null, + 5, + null, + null, + null, + null, + null, + 1, + 214, + null, + 5, + 2, + null, + null, + null, + null, + null, + null, + null, + 1, + 214, + 205, + null, + 1, + null, + null, + null, + null, + null, + 1, + 205, + null, + null, + null, + null, + null, + 1, + 205, + null, + null, + null, + null, + null, + 1, + 205, + null, + null, + null, + null, + null, + 1, + 205, + null, + null + ] + }, + "/workspaces/CAMAAR/app/models/resposta_item.rb": { + "lines": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + 1, + null, + 1, + 1, + null + ] + }, + "/workspaces/CAMAAR/app/models/resposta.rb": { + "lines": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + 1, + null, + null, + null, + null, + null, + 1, + 1, + null, + 1, + null, + 1, + null, + 1, + null + ] + }, + "/workspaces/CAMAAR/app/models/template.rb": { + "lines": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + 1, + 1, + 1, + null, + 1, + null, + 1, + null, + 1, + 1, + 1, + 1, + null, + 1, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + 1, + 196, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 196, + null, + 1, + null, + null + ] + }, + "/workspaces/CAMAAR/app/models/turma.rb": { + "lines": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + 1, + null, + 1, + 1, + 1, + null, + 1, + 1, + null + ] + }, + "/workspaces/CAMAAR/app/services/evaluation_batch_creator.rb": { + "lines": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + 1, + 6, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 7, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 7, + 7, + 7, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 7, + null, + 5, + 4, + null, + 4, + null, + 0, + null, + 1, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 3, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 5, + 4, + 4, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 4, + 4, + null, + 4, + 4, + 8, + null, + null, + null, + 4, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 8, + 1, + null, + 7, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 7, + 7, + 7, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 7, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 8, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 7, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 7, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 7, + 7, + null, + 7, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 7, + 7, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ] + }, + "/workspaces/CAMAAR/app/services/evaluation_result_aggregator.rb": { + "lines": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + 1, + 6, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 6, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + 1, + 11, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 6, + 6, + null, + 5, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 6, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 6, + 6, + 6, + null, + null, + 6, + null, + null, + null, + null, + null, + null, + null + ] + }, + "/workspaces/CAMAAR/app/services/evaluation_results_exporter.rb": { + "lines": [ + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + 1, + null, + 1, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 4, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 3, + 1, + null, + 1, + null, + 1, + null, + 3, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + 1, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 3, + 2, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + null + ] + }, + "/workspaces/CAMAAR/app/services/report_generator.rb": { + "lines": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + 1, + 3, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 3, + 4, + null, + null, + 4, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + 1, + 3, + 1, + 0, + null, + 3, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + 1, + 3, + 3, + 2, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 4, + null, + null + ] + }, + "/workspaces/CAMAAR/app/services/sigaa_importer.rb": { + "lines": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + 1, + 19, + 19, + 19, + 19, + 19, + 19, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 49, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 27, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 26, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 14, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 13, + null, + null, + 1, + null, + 1, + 13, + 13, + null, + null, + 1, + 13, + 13, + 13, + 13, + null, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 19, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 19, + 19, + 19, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 19, + 19, + null, + 18, + 18, + 18, + null, + null, + 18, + null, + 0, + 0, + 0, + null, + 0, + 0, + 0, + null, + null, + 1, + null, + 1, + 19, + 1, + 1, + null, + null, + null, + 1, + 18, + 18, + null, + 15, + 27, + null, + null, + null, + 1, + 17, + 17, + null, + 17, + 31, + null, + null, + null, + 1, + 35, + 35, + null, + 3, + 3, + 3, + null, + null, + 1, + 27, + 27, + null, + null, + 1, + 27, + null, + 27, + 26, + null, + 1, + null, + null, + 27, + null, + null, + 1, + 26, + 26, + 26, + null, + 0, + null, + null, + null, + 1, + 1, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + 1, + 27, + null, + 27, + 26, + null, + 1, + null, + null, + null, + 1, + 27, + null, + null, + null, + null, + null, + null, + 1, + 26, + 26, + null, + 26, + 26, + null, + 0, + null, + null, + null, + 1, + 1, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 31, + 31, + null, + 27, + 27, + null, + null, + null, + null, + null, + 1, + 31, + 31, + 4, + 4, + null, + null, + 27, + null, + null, + null, + null, + null, + 27, + 0, + 0, + null, + null, + 27, + null, + null, + null, + null, + null, + 1, + 27, + null, + 27, + 27, + null, + null, + null, + null, + null, + 1, + 27, + null, + 26, + 26, + null, + null, + null, + null, + null, + 1, + 27, + null, + 27, + 39, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 27, + 27, + null, + 27, + 26, + null, + 1, + null, + null, + 27, + null, + null, + 1, + 26, + 26, + null, + 26, + 26, + 26, + null, + 0, + null, + null, + null, + 1, + 26, + 26, + 26, + 26, + 26, + null, + null, + 1, + 26, + 26, + 26, + null, + null, + 1, + 1, + null, + 0, + null, + 0, + 0, + null, + 0, + null, + null, + null, + 1, + null, + 1, + null, + null, + null, + null, + null, + null, + 6, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 39, + 39, + null, + 39, + 39, + null, + null, + 1, + 39, + 36, + null, + 3, + null, + null, + null, + 1, + 36, + 36, + null, + 36, + 36, + 36, + 36, + null, + 0, + 0, + null, + null, + null, + 1, + 38, + 38, + 38, + 38, + 38, + 38, + null, + null, + 1, + 36, + 36, + 36, + null, + null, + 1, + 3, + null, + 2, + null, + 2, + 2, + 2, + null, + 0, + 0, + null, + null, + null, + 1, + null, + 3, + null, + null, + null, + null, + null, + null, + null, + 11, + 3, + 3, + null, + null, + 1, + 39, + null, + 39, + 38, + null, + 1, + null, + null, + null, + 1, + 38, + 38, + null, + 38, + 38, + null, + 0, + null, + null, + null, + 1, + 1, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + 1, + 26, + 14, + 14, + 14, + 14, + null, + null, + null, + 1, + null, + 62, + 62, + null, + 0, + null, + null, + null, + 0, + 0, + null, + null + ] + }, + "/workspaces/CAMAAR/app/controllers/application_controller.rb": { + "lines": [ + null, + null, + null, + null, + 1, + null, + 1, + null, + null, + 1, + null, + 1, + null, + 1, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 624, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 158, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 105, + 6, + 6, + null, + null, + null + ] + }, + "/workspaces/CAMAAR/app/controllers/avaliacoes_controller.rb": { + "lines": [ + 1, + null, + null, + null, + null, + null, + null, + 1, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + 1, + 88, + 85, + null, + null, + null, + null, + 3, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 20, + null, + 20, + 19, + 18, + null, + 16, + null, + null, + 1, + null, + 1, + 28, + null, + 1, + 1, + null, + null, + 1, + 27, + null, + 1, + 1, + null, + null, + 1, + 18, + 18, + null, + 2, + 2, + null, + null, + 1, + 4, + 4, + null, + null, + 1, + 16, + 16, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 8, + null, + 8, + 8, + null, + 8, + 8, + null, + null, + null, + null, + null, + 1, + 8, + null, + null, + null, + null, + null, + 1, + 8, + null, + 8, + 0, + 0, + null, + null, + 8, + 8, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 8, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 8, + 6, + null, + 2, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 6, + 6, + null, + 6, + 6, + 6, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 2, + 2, + null, + 2, + 2, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 3, + 3, + null, + 3, + null, + null, + 1, + null, + 1, + 3, + null, + null, + 1, + 3, + 2, + null, + 1, + 1, + null, + null, + null, + 1, + 6, + null, + 0, + null, + null, + 1, + 10, + 10, + 10, + 10, + null, + null, + 1, + 7, + null, + null, + 1, + 7, + null, + null, + 1, + 10, + 10, + 10, + null, + null, + 1, + 2, + 2, + 2, + null, + null, + 1, + 8, + 8, + null, + 6, + null, + null, + 1, + 8, + null, + 8, + 14, + 14, + 2, + 2, + null, + null, + null, + 6, + null, + null, + 1, + 6, + 16, + 16, + 16, + null, + 14, + null, + null, + 6, + null, + null, + 1, + 16, + null, + null, + 1, + 16, + null, + null, + 1, + 14, + 14, + null, + 14, + 0, + 0, + null, + null, + 14, + null, + null + ] + }, + "/workspaces/CAMAAR/app/controllers/passwords_controller.rb": { + "lines": [ + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 5, + 5, + null, + 5, + 2, + 2, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 9, + null, + 9, + 8, + null, + 5, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 9, + 1, + 1, + 1, + null, + 8, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 8, + 3, + 3, + 3, + 3, + null, + 5, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 5, + 5, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 5, + 5, + 5, + 5, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 5, + 5, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 5, + 5, + 5, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 12, + 12, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 5, + 5, + 0, + 0, + null, + 0, + null, + null, + null + ] + }, + "/workspaces/CAMAAR/app/controllers/resultados_controller.rb": { + "lines": [ + null, + null, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 2, + 2, + 2, + 2, + 2, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 3, + 3, + null, + null, + null, + null, + 1, + null, + 1, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 2, + 2, + 2, + null, + 2, + 2, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 5, + 5, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 2, + 2, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 2, + 2, + 2, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + 0, + null, + null + ] + }, + "/workspaces/CAMAAR/app/controllers/sessions_controller.rb": { + "lines": [ + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 54, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 51, + null, + 51, + 45, + null, + 43, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 4, + 4, + null, + null, + 1, + null, + 1, + 51, + null, + null, + 1, + 45, + null, + null, + 1, + 6, + 6, + null, + null, + 1, + 2, + 2, + null, + null, + 1, + 43, + 43, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 44, + 42, + 2, + 2, + null, + 0, + null, + null, + null + ] + }, + "/workspaces/CAMAAR/app/controllers/templates_controller.rb": { + "lines": [ + null, + null, + null, + null, + 1, + 1, + 1, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 11, + 11, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 3, + null, + 3, + 2, + 2, + null, + 1, + 1, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 2, + 2, + 2, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + 1, + 1, + null, + 0, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + 1, + 5, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 13, + 13, + 13, + 13, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 18, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 4, + 4, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 4, + null, + 4, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 5, + 5, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 13, + 13, + null, + null, + null, + null, + null, + 1, + 13, + 13, + 13, + null, + null, + null, + null, + null, + 1, + 13, + null, + null, + null, + null, + null, + 1, + 48, + null, + null, + null, + null, + null, + 1, + 35, + null, + null, + null, + null, + null, + null, + null, + null + ] + }, + "/workspaces/CAMAAR/app/controllers/sigaa_imports_controller.rb": { + "lines": [ + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 16, + null, + null, + null, + null, + null, + 16, + 13, + 13, + null, + 3, + 3, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 4, + null, + 3, + null, + null, + 1, + null, + null, + null, + null, + 1, + 4, + 1, + 1, + 1, + null, + 3, + null, + null, + null, + null, + null, + 1, + 4, + null, + null, + 1, + 7, + null, + null, + 1, + 6, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + 1, + 3, + null, + null, + null, + null, + null, + 3, + 3, + null, + null, + null, + null, + null, + 1, + 3, + 3, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 27, + 2, + 2, + null, + null, + null + ] + }, + "/workspaces/CAMAAR/app/jobs/application_job.rb": { + "lines": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + 0 + ], + "branches": {} + } + }, + "timestamp": 1765811203 + } +} 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('