diff --git a/.gitignore b/.gitignore index 2da7c79d1e..06e74f0bb7 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,4 @@ TAGS *.iml .generators .idea +.tool-versions diff --git a/.rubocop.yml b/.rubocop.yml index 8dca9e39d9..9e4acb2b23 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,5 +1,4 @@ AllCops: - RunRailsCops: true DisplayCopNames: true Exclude: - Rakefile @@ -12,6 +11,12 @@ AllCops: - tmp/**/* - app/assets/config.rb +Rails: + Enabled: true + +# we have not yet introcued ApplicationRecord as a Pattern +Rails/ApplicationRecord: + Enabled: false Metrics/AbcSize: Max: 20 @@ -21,6 +26,10 @@ Metrics/ClassLength: Max: 200 Severity: error +Metrics/ModuleLength: + Max: 200 + Severity: error + Metrics/CyclomaticComplexity: Max: 6 Severity: error @@ -28,12 +37,12 @@ Metrics/CyclomaticComplexity: Metrics/LineLength: Max: 100 Severity: warning + IgnoreCopDirectives: true Metrics/MethodLength: Max: 10 Severity: error - # Keep for now, easier with superclass definitions ClassAndModuleChildren: Enabled: false @@ -55,10 +64,6 @@ Documentation: DotPosition: Enabled: false -# Missing UTF-8 encoding statements should always be created. -Encoding: - Severity: error - # Keep single line bodys for if and unless IfUnlessModifier: Enabled: false @@ -72,23 +77,31 @@ Rails/Delegate: Enabled: false # That's no huge stopper -Style/EmptyLines: +Layout/EmptyLines: Enabled: false # We thinks that's fine for specs -Style/EmptyLinesAroundBlockBody: +Layout/EmptyLinesAroundBlockBody: Enabled: false # We thinks that's fine -Style/EmptyLinesAroundClassBody: +Layout/EmptyLinesAroundClassBody: Enabled: false # We thinks that's fine -Style/EmptyLinesAroundModuleBody: +Layout/EmptyLinesAroundModuleBody: Enabled: false # We thinks that's fine -Style/MultilineOperationIndentation: +Layout/MultilineOperationIndentation: + Enabled: false + +# We are using Ruby 2+ anyway... +Style/AsciiComments: + Enabled: false + +# For now, we keep encoding comment +Style/Encoding: Enabled: false # We thinks that's fine @@ -103,6 +116,19 @@ Style/SymbolProc: Style/GuardClause: Enabled: false +# We think that's fine +Style/PercentLiteralDelimiters: + PreferredDelimiters: + '%w': '()' + +# We think that's fine +Style/SymbolArray: + EnforcedStyle: brackets + +Style/PercentLiteralDelimiters: + PreferredDelimiters: + '%w': '()' + # We thinks that's fine Style/SingleLineBlockParams: Enabled: false diff --git a/.s2i/post_assemble b/.s2i/post_assemble new file mode 100755 index 0000000000..ae88dc70cd --- /dev/null +++ b/.s2i/post_assemble @@ -0,0 +1,27 @@ +#!/bin/bash + +set -ex + +# this script is executed after our rails images default assemble script. + +pushd /opt/app-root/src + +# load development seeds when demo instance +if [ $DEMO_INSTANCE -eq 1 ]; then + echo 'demo instance: creating symlink for loading development seeds' + for wagon in vendor/wagons/* + do + cd $wagon/db/seeds + ln -s development production + done +fi + +if [ $PULL_TRANSIFEX -eq 1 ]; then + echo 'pulling transifex translations ...' + RAILS_HOST_NAME='build.hitobito.ch' bundle exec rake tx:pull tx:wagon:pull -t +fi + +BUILD_DATE=$(date '+%Y-%m-%d %H:%M:%S') +echo "(built at: $BUILD_DATE)" > BUILD_INFO + +popd diff --git a/.s2i/post_deploy b/.s2i/post_deploy new file mode 100755 index 0000000000..d773104041 --- /dev/null +++ b/.s2i/post_deploy @@ -0,0 +1,19 @@ +#!/bin/bash + +set -ex + +# This script is executed after our rails images' default assemble script. + +pushd /opt/app-root/src + +bundle exec rake db:seed +bundle exec rake wagon:migrate +bundle exec rake wagon:seed + +for dir in vendor/wagons/*; do + if [[ -x $dir/.s2i/post_deploy ]] ; then + $dir/.s2i/post_deploy + fi +done + +popd diff --git a/.s2i/pre_assemble b/.s2i/pre_assemble new file mode 100755 index 0000000000..fbd4334873 --- /dev/null +++ b/.s2i/pre_assemble @@ -0,0 +1,62 @@ +#!/bin/bash + +set -ex + +source_dir=$(dirname $0)/.. + +# this script places the core and wagon files in the right folders and creates the Wagonfile. +# after this, we are able to use our rails images default assemble script to do the execute default +# tasks like assets precompilation + +pushd $source_dir + +# update the composition-repo to newest versions of configured .gitmodules-branch +# devel is our indicator for the integration-environment +if [[ "x${OPENSHIFT_BUILD_REFERENCE}" = "xdevel" ]]; then + git submodule update --remote +fi + +# move core +rm -r hitobito/.git +mv hitobito/* . + +# add wagon sources +mkdir vendor/wagons +for dir in hitobito_*; do + if [[ ( -d $dir ) ]]; then + rm -r $dir/.git + mv $dir vendor/wagons/ + fi +done + +# place Wagonfile +mv -f config/rpm/Wagonfile . + +# move hidden core dirs +rm -f .s2i/pre_assemble +cp -rf hitobito/.s2i . # cannot be moved since it is in use during this script's execution +mv hitobito/.tx . + +# finally remove core source directory +rm -rf hitobito +rm -r .git + +# TODO: Investigate. This seems ugly and is a hack to prevent assemble from failing with +# +# You are trying to install in deployment mode after changing +# your Gemfile. Run `bundle install` elsewhere and add the +# updated Gemfile.lock to version control. + +# You have added to the Gemfile: +# * source: source at /home/sraez/dev/hitobito_generic_composition_apply/vendor/wagons/hitobito_generic +# * hitobito_generic + +# You have deleted from the Gemfile: +# * source: source at ../hitobito_insieme +# * hitobito_insieme +# This inludes fixes from https://github.com/bundler/bundler/issues/2854#issuecomment-38991901 +bundle install --no-deployment --path vendor/bundle +# Speed up the second `bundle install` run +bundle package --all + +popd diff --git a/.travis.yml b/.travis.yml index cb607f0222..8c12fc06eb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,7 @@ language: ruby cache: bundler +addons: + firefox: 45.0 branches: only: - master @@ -9,18 +11,22 @@ env: - HEADLESS=true - RAILS_DB_ADAPTER=mysql2 rvm: - - 1.9.3 - - 2.0.0 - 2.1.7 - 2.2.3 - 2.3.1 - 2.4.0 -matrix: - allow_failures: - - rvm: 2.4.0 +before_install: + - sudo apt-get -qq update + - sudo apt-get install sphinxsearch + - echo '[mysqld]' | sudo tee /etc/mysql/conf.d/sort_buffer_size.cnf > /dev/null + - echo 'sort_buffer_size = 2M' | sudo tee -a /etc/mysql/conf.d/sort_buffer_size.cnf > /dev/null + - sudo service mysql restart install: - sed -i "s/^\(gem .mysql2.\),.*$/\1/" Gemfile - bundle install --path vendor/bundle - bundle update mysql2 script: - - bundle exec rake db:create ci --trace + - bundle exec rake db:create ci --trace skip_tasks=spec:features +matrix: + allow_failures: + - rvm: 2.4.0 diff --git a/AUTHORS b/AUTHORS index 5bfe5c3608..544d03b409 100644 --- a/AUTHORS +++ b/AUTHORS @@ -6,6 +6,7 @@ Code: Pascal Simon, Puzzle ITC Mathis Hofer, Puzzle ITC Diego Steiner + Lukas Blunschi Design & Style: Roland Studer, Puzzle ITC diff --git a/CHANGELOG.md b/CHANGELOG.md index 0813a85250..7e30c456ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,60 @@ # Hitobito Changelog +## Version 1.X + +* Alle Personenfilter sind zusammengefasst und lassen sich abspeichern. +* Personenfilter erlauben den Gültigzeitszeitraum einer Rolle einzuschränken. + + +## Version 1.17 + +* Export der Abonnenten einer Mailingliste wird im Hintergrund erstellt und per mail versendet + + +## Version 1.16 + +* Vorbedingungen von Kursarten können zusätzlich mit ODER verknüpft werden. +* Für alle Anlässe lassen sich beliebige Administrationsangaben zu den Teilnehmenden definieren. +* Anzeige der Hauptebene bei Personenexporten und Teilnehmerlisten. +* Anlässe können dupliziert werden. +* Personenfilter nach Qualifikationsdaten und mehreren Qualifikationen. +* Sichtbarkeit der Anmeldungen auf Kursliste für alle Personen ist pro Kurs konfigurierbar. +* Aktualisieren der Kontaktdaten bei der Eventanmeldung +* Festlegen von Pflichtangaben zur Person bei der Eventanmeldung +* Anmeldestand kann für alle sichtbar gemacht werden + + +## Version 1.15 + +* Neue Rolle "Helfer/-in" für Anlässe. +* Unterschriften können nun bei allen Anlässen eingefordert werden. +* Anzeige des Geburtsdatums in Anlassteilnahmelisten. +* Notizen ebenfalls auf Gruppen möglich. +* Alle Personen derselben Firma sind unter Person > Mitarbeiter/-innen ersichtlich. +* Qualifikationen werden in Kursen erst auf Knopfdruck aktualisiert. +* Anmeldedatum wird bei Anmeldeknopf auf Anlassliste angezeigt. + + ## Version 1.14 * Automatisches Ausfüllen der Kurs Beschreibung wenn ein Kurstyp gewählt wird. +* Admin kann gelöschte Personen in der Volltextsuche finden. +* Anfrageverfahren wird für gelöschte Personen ebenfalls ausgelöst. +* Gelöschte Personen können pro Ebene angezeigt werden. +* Benutzer/-innen können personalisierte Etiketten erstellen. +* Übername und ein P.P. Post Feld können den Etiketten hinzugefügt werden. +* Globale Suche nach Anlassnamen und Kursnummern. +* Excel-Export für Personen und Anlässe. +* CSV- und Excel-Exporte von Personen mit allen Angaben enthalten aktuelle Qualifikationen. +* Der Verlauf einer Person zeigt neu die Rollen so an, dass die Gruppen auch die übergeordneten Ebenen anzeigt. +* Der Verlauf einer Person wird neu nach der Gruppe inkl. übergeordneter Ebenen sortiert. + ## Version 1.13 * Personen können in Mailinglisten nach Tags gefiltert werden. + ## Version 1.12 * Zu Personen können eingeschränkt sichtbare Notizen hinterlegt werden. @@ -96,4 +143,3 @@ * Separat definierbare Qualifikationstypen für Kursleiter. * Pflichtfelder für Anlass Fragen. * Mehrfachauswahl bei Personen Filter und Abo Listen. - diff --git a/Gemfile b/Gemfile index 15947f501f..a7a53f471c 100644 --- a/Gemfile +++ b/Gemfile @@ -1,59 +1,62 @@ # encoding: utf-8 -# Copyright (c) 2012-2014, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. source 'https://rubygems.org' -gem 'rails', '4.2.7.1' +gem 'rails', '4.2.8' gem 'activerecord-session_store' gem 'acts-as-taggable-on', '~> 3.5.0' gem 'airbrake', '< 5.0' # requires newer errbit gem 'axlsx', '2.0.1' -gem 'awesome_nested_set' +gem 'awesome_nested_set', '< 3.1.0' # requires ruby 2.0 gem 'bcrypt-ruby' gem 'cancancan', '< 1.13.0' # requires ruby 2.0 -gem 'carrierwave' +gem 'carrierwave', '< 0.11.1' # uses 2.0 for testing (no explicit requirement, yet) gem 'cmess' gem 'country_select' gem 'daemons' gem 'dalli' gem 'delayed_job_active_record' -gem 'devise' +gem 'devise', '< 4.0.0' # requires ruby 2.1 gem 'draper' -gem 'faker' +gem 'faker', '< 1.6.4' # uses 2.0 for testing (no explicit requirement, yet) gem 'globalize' gem 'haml' gem 'http_accept_language' +gem 'icalendar' gem 'magiclabs-userstamp', require: 'userstamp' gem 'mime-types', '~> 2.6.2' # newer requires ruby 2.0 gem 'mini_magick' -gem 'mysql2', '0.3.15' # 0.3.16 fails sphinx specs on jenkins +gem 'mysql2', '0.4.9' gem 'nested_form' gem 'oat' gem 'paper_trail' -gem 'paranoia' +gem 'paranoia', '< 2.1.2' # uses 2.0 for testing (no explicit requirement, yet) gem 'customized_piwik_analytics', '~> 1.0.0' gem 'prawn', '< 2.0' # 2.0 requires ruby 2.0 gem 'prawn-table' gem 'protective' gem 'rack' gem 'rails_autolink' -gem 'config' +gem 'config', '< 1.1.0' # requires ruby 2 gem 'rails-i18n' +gem 'rubyzip' gem 'seed-fu' gem 'simpleidn' gem 'sqlite3' # for development, test and production when generating assets gem 'thinking-sphinx' gem 'validates_by_schema' gem 'validates_timeliness', '< 4.0' +gem 'vcard' gem 'wagons' # load after others because of active record inherited alias chain. -gem 'kaminari' +gem 'kaminari', '< 1.0.0' # requires ruby 2.0 # Gems used only for assets gem 'bootstrap-sass', '~> 2.3' @@ -71,6 +74,14 @@ gem 'therubyracer', platforms: :ruby gem 'turbolinks' gem 'uglifier' +# if these are ever in your way, you can remove these lines. +# they mostly serve as a version-restriction +group :dependencies do + gem 'nokogiri', '< 1.7.0' # requires ruby 2.1 + gem 'addressable', '< 2.5' # requires ruby 2.0 + gem 'sort_alphabetical', '< 1.1.0' # requires ruby 2.0 +end + group :development, :test do gem 'binding_of_caller' gem 'rspec-rails' @@ -90,14 +101,15 @@ end group :test do gem 'capybara' + gem 'capybara-screenshot' gem 'database_cleaner' gem 'fabrication' gem 'headless' gem 'launchy' gem 'rspec-its' gem 'rspec-collection_matchers' - gem 'selenium-webdriver' - gem 'timecop' + gem 'selenium-webdriver', '2.51.0' # 3.2.2 fails with "Unable to find Mozilla geckodriver" + gem 'pdf-inspector', require: 'pdf/inspector' end group :console do @@ -127,4 +139,4 @@ end # # To create a Wagonfile suitable for development, run 'rake wagon:file' wagonfile = File.expand_path('../Wagonfile', __FILE__) -eval(File.read(wagonfile)) if File.exist?(wagonfile) +eval(File.read(wagonfile)) if File.exist?(wagonfile) # rubocop:disable Security/Eval diff --git a/Gemfile.lock b/Gemfile.lock index 22cd1d7aff..4abe315fa1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,66 +1,78 @@ +PATH + remote: ../hitobito_pbs + specs: + hitobito_pbs (0.0.1) + hitobito_youth + +PATH + remote: ../hitobito_youth + specs: + hitobito_youth (0.0.1) + GEM remote: https://rubygems.org/ specs: - actionmailer (4.2.7.1) - actionpack (= 4.2.7.1) - actionview (= 4.2.7.1) - activejob (= 4.2.7.1) + Ascii85 (1.0.2) + actionmailer (4.2.8) + actionpack (= 4.2.8) + actionview (= 4.2.8) + activejob (= 4.2.8) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 1.0, >= 1.0.5) - actionpack (4.2.7.1) - actionview (= 4.2.7.1) - activesupport (= 4.2.7.1) + actionpack (4.2.8) + actionview (= 4.2.8) + activesupport (= 4.2.8) rack (~> 1.6) rack-test (~> 0.6.2) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (4.2.7.1) - activesupport (= 4.2.7.1) + actionview (4.2.8) + activesupport (= 4.2.8) builder (~> 3.1) erubis (~> 2.7.0) rails-dom-testing (~> 1.0, >= 1.0.5) - rails-html-sanitizer (~> 1.0, >= 1.0.2) - activejob (4.2.7.1) - activesupport (= 4.2.7.1) + rails-html-sanitizer (~> 1.0, >= 1.0.3) + activejob (4.2.8) + activesupport (= 4.2.8) globalid (>= 0.3.0) - activemodel (4.2.7.1) - activesupport (= 4.2.7.1) + activemodel (4.2.8) + activesupport (= 4.2.8) builder (~> 3.1) - activerecord (4.2.7.1) - activemodel (= 4.2.7.1) - activesupport (= 4.2.7.1) + activerecord (4.2.8) + activemodel (= 4.2.8) + activesupport (= 4.2.8) arel (~> 6.0) - activerecord-session_store (0.1.2) - actionpack (>= 4.0.0, < 5) - activerecord (>= 4.0.0, < 5) - railties (>= 4.0.0, < 5) - activesupport (4.2.7.1) + activerecord-session_store (1.0.0) + actionpack (>= 4.0, < 5.1) + activerecord (>= 4.0, < 5.1) + multi_json (~> 1.11, >= 1.11.2) + rack (>= 1.5.2, < 3) + railties (>= 4.0, < 5.1) + activesupport (4.2.8) i18n (~> 0.7) - json (~> 1.7, >= 1.7.7) minitest (~> 5.1) thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) acts-as-taggable-on (3.5.0) activerecord (>= 3.2, < 5) addressable (2.4.0) + afm (0.2.2) airbrake (4.3.4) builder multi_json - annotate (2.7.0) + annotate (2.7.1) activerecord (>= 3.2, < 6.0) - rake (~> 10.4) - arel (6.0.3) - ast (2.2.0) - astrolabe (1.3.1) - parser (~> 2.2) + rake (>= 10.4, < 12.0) + arel (6.0.4) + ast (2.3.0) awesome_nested_set (3.0.2) activerecord (>= 4.0.0, < 5) - awesome_print (1.6.1) + awesome_print (1.7.0) axlsx (2.0.1) htmlentities (~> 4.3.1) nokogiri (>= 1.4.1) rubyzip (~> 1.0.0) - bcrypt (3.1.10) + bcrypt (3.1.11) bcrypt-ruby (3.1.5) bcrypt (>= 3.1.3) binding_of_caller (0.7.2) @@ -69,63 +81,55 @@ GEM sass (~> 3.2) bootstrap-wysihtml5-rails (0.3.1.24) railties (>= 3.0) - brakeman (3.1.4) - erubis (~> 2.6) - fastercsv (~> 1.5) - haml (>= 3.0, < 5.0) - highline (>= 1.6.20, < 2.0) - multi_json (~> 1.2) - ruby2ruby (>= 2.1.1, < 2.3.0) - ruby_parser (~> 3.7.0) - safe_yaml (>= 1.0) - sass (~> 3.0) - slim (>= 1.3.6, < 4.0) - terminal-table (~> 1.4) - builder (3.2.2) - bullet (4.14.10) + brakeman (3.5.0) + builder (3.2.3) + bullet (5.5.1) activesupport (>= 3.0.0) - uniform_notifier (~> 1.9.0) + uniform_notifier (~> 1.10.0) cancancan (1.12.0) - capybara (2.5.0) + capybara (2.12.1) + addressable mime-types (>= 1.16) nokogiri (>= 1.3.3) rack (>= 1.0.0) rack-test (>= 0.5.4) xpath (~> 2.0) + capybara-screenshot (1.0.14) + capybara (>= 1.0, < 3) + launchy carrierwave (0.10.0) activemodel (>= 3.2.0) activesupport (>= 3.2.0) json (>= 1.7) mime-types (>= 1.16) - childprocess (0.5.9) + childprocess (0.6.2) ffi (~> 1.0, >= 1.0.11) choice (0.2.0) - chosen-rails (1.4.3) + chosen-rails (1.5.2) coffee-rails (>= 3.2) - compass-rails (>= 2.0.4) railties (>= 3.0) sass-rails (>= 3.2) - chunky_png (1.3.5) + chunky_png (1.3.8) ci_reporter (2.0.0) builder (>= 2.1.2) ci_reporter_rspec (1.0.0) ci_reporter (~> 2.0) rspec (>= 2.14, < 4) - cmess (0.5.0) + cmess (0.5.1) htmlentities (~> 4.3) - nuggets (~> 1.0) + nuggets (~> 1.5) safe_yaml (~> 1.0) - coderay (1.1.0) + coderay (1.1.1) codez-tarantula (0.5.5) hpricot (~> 0.8.4) htmlentities (~> 4.3.0) - coffee-rails (4.1.1) + coffee-rails (4.2.1) coffee-script (>= 2.2.0) - railties (>= 4.0.0, < 5.1.x) + railties (>= 4.0.0, < 5.2.x) coffee-script (2.4.1) coffee-script-source execjs - coffee-script-source (1.10.0) + coffee-script-source (1.12.2) columnize (0.9.0) compass (1.0.3) chunky_png (~> 1.2) @@ -139,10 +143,11 @@ GEM sass (>= 3.3.0, < 3.5) compass-import-once (1.0.5) sass (>= 3.2, < 3.5) - compass-rails (2.0.4) + compass-rails (3.0.2) compass (~> 1.0.0) - sass-rails (<= 5.0.1) - sprockets (< 2.13) + sass-rails (< 5.1) + sprockets (< 4.0) + concurrent-ruby (1.0.5) config (1.0.0) activesupport (>= 3.0) deep_merge (~> 1.0.0) @@ -157,9 +162,9 @@ GEM actionpack activesupport rails (>= 3.0.0) - daemons (1.2.3) - dalli (2.7.5) - database_cleaner (1.5.1) + daemons (1.2.4) + dalli (2.7.6) + database_cleaner (1.5.3) debug_inspector (0.0.2) debugger (1.6.8) columnize (>= 0.3.1) @@ -168,19 +173,19 @@ GEM debugger-linecache (1.2.0) debugger-ruby_core_source (1.3.8) deep_merge (1.0.1) - delayed_job (4.1.1) - activesupport (>= 3.0, < 5.0) - delayed_job_active_record (4.1.0) - activerecord (>= 3.0, < 5) + delayed_job (4.1.2) + activesupport (>= 3.0, < 5.1) + delayed_job_active_record (4.1.1) + activerecord (>= 3.0, < 5.1) delayed_job (>= 3.0, < 5) - devise (3.5.3) + devise (3.5.10) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 3.2.6, < 5) responders thread_safe (~> 0.1) warden (~> 1.2.3) - diff-lcs (1.2.5) + diff-lcs (1.3) docile (1.1.5) draper (2.1.0) actionpack (>= 3.0) @@ -188,13 +193,12 @@ GEM activesupport (>= 3.0) request_store (~> 1.0) erubis (2.7.0) - eventmachine (1.0.8) - execjs (2.6.0) - fabrication (2.14.1) - faker (1.6.1) + eventmachine (1.0.9.1) + execjs (2.7.0) + fabrication (2.16.1) + faker (1.6.3) i18n (~> 0.5) - fastercsv (1.5.5) - ffi (1.9.10) + ffi (1.9.18) globalid (0.3.7) activesupport (>= 4.1.0) globalize (5.0.1) @@ -202,43 +206,45 @@ GEM activerecord (>= 4.2.0, < 4.3) haml (4.0.7) tilt - headless (2.2.0) - highline (1.7.8) - hike (1.2.3) + hashery (2.1.2) + headless (2.3.1) hirb (0.7.3) hpricot (0.8.6) htmlentities (4.3.4) - http_accept_language (2.0.5) - i18n (0.7.0) + http_accept_language (2.1.0) + i18n (0.8.6) i18n_data (0.7.0) + icalendar (2.4.1) innertube (1.1.0) joiner (0.3.4) activerecord (>= 4.1.0) - jquery-rails (4.0.5) - rails-dom-testing (~> 1.0) + jquery-rails (4.2.2) + rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) jquery-turbolinks (2.1.0) railties (>= 3.1.0) turbolinks - jquery-ui-rails (5.0.5) + jquery-ui-rails (6.0.1) railties (>= 3.2.16) - json (1.8.3) - kaminari (0.16.3) + json (2.1.0) + kaminari (0.17.0) actionpack (>= 3.0.0) activesupport (>= 3.0.0) launchy (2.4.3) addressable (~> 2.3) - libv8 (3.16.14.13) + libv8 (3.16.14.17) loofah (2.0.3) nokogiri (>= 1.5.9) - magiclabs-userstamp (2.1.0) + magiclabs-userstamp (3.0) + actionpack (>= 4.0) + activerecord (>= 4.0) mail (2.6.4) mime-types (>= 1.16, < 4) - mailcatcher (0.6.2) - activesupport (>= 4.0.0, < 5) - eventmachine (= 1.0.8) + mailcatcher (0.6.5) + eventmachine (= 1.0.9.1) mail (~> 2.3) + rack (~> 1.5) sinatra (~> 1.2) skinny (~> 0.2.3) sqlite3 (~> 1.3) @@ -246,27 +252,35 @@ GEM method_source (0.8.2) middleware (0.1.0) mime-types (2.6.2) - mini_magick (4.3.6) + mini_magick (4.6.1) mini_portile2 (2.1.0) - minitest (5.9.1) + minitest (5.10.3) multi_json (1.12.1) - mysql2 (0.3.15) + mysql2 (0.4.9) nested_form (0.3.2) nokogiri (1.6.8.1) mini_portile2 (~> 2.1.0) - nuggets (1.4.0) - oat (0.4.6) + nuggets (1.5.0) + oat (0.5.0) activesupport orm_adapter (0.5.0) - paper_trail (4.0.1) - activerecord (>= 3.0, < 6.0) - activesupport (>= 3.0, < 6.0) + paper_trail (6.0.2) + activerecord (>= 4.0, < 5.2) request_store (~> 1.1) - paranoia (2.1.4) + parallel (1.12.0) + paranoia (2.1.1) activerecord (~> 4.0) - parser (2.2.3.0) - ast (>= 1.1, < 3.0) + parser (2.4.0.2) + ast (~> 2.3) pdf-core (0.4.0) + pdf-inspector (1.2.1) + pdf-reader (~> 1.0) + pdf-reader (1.4.1) + Ascii85 (~> 1.0.0) + afm (~> 0.2.1) + hashery (~> 2.0) + ruby-rc4 + ttfunk powerpack (0.1.1) prawn (1.3.0) pdf-core (~> 0.4.0) @@ -275,17 +289,17 @@ GEM prawn (>= 1.3.0, < 3.0.0) protective (0.1.0) activerecord - pry (0.10.3) + pry (0.10.4) coderay (~> 1.1.0) method_source (~> 0.8.1) slop (~> 3.4) pry-debugger (0.2.3) debugger (~> 1.3) pry (>= 0.9.10, < 0.11.0) - pry-doc (0.8.0) + pry-doc (0.10.0) pry (~> 0.9) - yard (~> 0.8) - pry-rails (0.3.4) + yard (~> 0.9) + pry-rails (0.3.5) pry (>= 0.9.10) pry-remote (0.1.8) pry (~> 0.9) @@ -295,118 +309,114 @@ GEM pry (>= 0.9.11) quiet_assets (1.1.0) railties (>= 3.1, < 5.0) - rack (1.6.4) + rack (1.6.5) rack-protection (1.5.3) rack rack-test (0.6.3) rack (>= 1.0) - rails (4.2.7.1) - actionmailer (= 4.2.7.1) - actionpack (= 4.2.7.1) - actionview (= 4.2.7.1) - activejob (= 4.2.7.1) - activemodel (= 4.2.7.1) - activerecord (= 4.2.7.1) - activesupport (= 4.2.7.1) + rails (4.2.8) + actionmailer (= 4.2.8) + actionpack (= 4.2.8) + actionview (= 4.2.8) + activejob (= 4.2.8) + activemodel (= 4.2.8) + activerecord (= 4.2.8) + activesupport (= 4.2.8) bundler (>= 1.3.0, < 2.0) - railties (= 4.2.7.1) + railties (= 4.2.8) sprockets-rails rails-deprecated_sanitizer (1.0.3) activesupport (>= 4.2.0.alpha) - rails-dom-testing (1.0.7) + rails-dom-testing (1.0.8) activesupport (>= 4.2.0.beta, < 5.0) - nokogiri (~> 1.6.0) + nokogiri (~> 1.6) rails-deprecated_sanitizer (>= 1.0.1) - rails-erd (1.4.4) + rails-erd (1.5.0) activerecord (>= 3.2) activesupport (>= 3.2) choice (~> 0.2.0) ruby-graphviz (~> 1.2) rails-html-sanitizer (1.0.3) loofah (~> 2.0) - rails-i18n (4.0.8) + rails-i18n (4.0.9) i18n (~> 0.7) railties (~> 4.0) rails_autolink (1.1.6) rails (> 3.1) - railties (4.2.7.1) - actionpack (= 4.2.7.1) - activesupport (= 4.2.7.1) + railties (4.2.8) + actionpack (= 4.2.8) + activesupport (= 4.2.8) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) - rainbow (2.0.0) - rake (10.5.0) - rb-fsevent (0.9.7) - rb-inotify (0.9.5) + rainbow (2.2.2) + rake + rake (11.3.0) + rb-fsevent (0.9.8) + rb-inotify (0.9.8) ffi (>= 0.5.0) - rdoc (4.2.1) - json (~> 1.4) + rdoc (4.3.0) rdoc-tags (1.3) rdoc (~> 4) - redcarpet (3.3.4) + redcarpet (3.4.0) ref (2.0.0) - remotipart (1.2.1) + remotipart (1.3.1) request_profiler (0.0.4) ruby-prof - request_store (1.2.1) - responders (2.1.1) + request_store (1.3.2) + responders (2.3.0) railties (>= 4.2.0, < 5.1) - riddle (1.5.12) - rspec (3.4.0) - rspec-core (~> 3.4.0) - rspec-expectations (~> 3.4.0) - rspec-mocks (~> 3.4.0) - rspec-collection_matchers (1.1.2) + riddle (2.2.0) + rspec (3.5.0) + rspec-core (~> 3.5.0) + rspec-expectations (~> 3.5.0) + rspec-mocks (~> 3.5.0) + rspec-collection_matchers (1.1.3) rspec-expectations (>= 2.99.0.beta1) - rspec-core (3.4.1) - rspec-support (~> 3.4.0) - rspec-expectations (3.4.0) + rspec-core (3.5.4) + rspec-support (~> 3.5.0) + rspec-expectations (3.5.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.4.0) + rspec-support (~> 3.5.0) rspec-its (1.2.0) rspec-core (>= 3.0.0) rspec-expectations (>= 3.0.0) - rspec-mocks (3.4.0) + rspec-mocks (3.5.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.4.0) - rspec-rails (3.4.0) - actionpack (>= 3.0, < 4.3) - activesupport (>= 3.0, < 4.3) - railties (>= 3.0, < 4.3) - rspec-core (~> 3.4.0) - rspec-expectations (~> 3.4.0) - rspec-mocks (~> 3.4.0) - rspec-support (~> 3.4.0) - rspec-support (3.4.1) - rubocop (0.35.1) - astrolabe (~> 1.3) - parser (>= 2.2.3.0, < 3.0) + rspec-support (~> 3.5.0) + rspec-rails (3.5.2) + actionpack (>= 3.0) + activesupport (>= 3.0) + railties (>= 3.0) + rspec-core (~> 3.5.0) + rspec-expectations (~> 3.5.0) + rspec-mocks (~> 3.5.0) + rspec-support (~> 3.5.0) + rspec-support (3.5.0) + rubocop (0.51.0) + parallel (~> 1.10) + parser (>= 2.3.3.1, < 3.0) powerpack (~> 0.1) - rainbow (>= 1.99.1, < 3.0) + rainbow (>= 2.2.2, < 3.0) ruby-progressbar (~> 1.7) - tins (<= 1.6.0) - rubocop-checkstyle_formatter (0.2.0) - rubocop (>= 0.20.1) + unicode-display_width (~> 1.0, >= 1.0.1) + rubocop-checkstyle_formatter (0.3.0) + rubocop (>= 0.30.1) ruby-graphviz (1.2.2) - ruby-prof (0.15.9) - ruby-progressbar (1.7.5) - ruby2ruby (2.2.0) - ruby_parser (~> 3.1) - sexp_processor (~> 4.0) - ruby_parser (3.7.2) - sexp_processor (~> 4.1) + ruby-prof (0.16.2) + ruby-progressbar (1.9.0) + ruby-rc4 (0.1.5) rubyzip (1.0.0) safe_yaml (1.0.4) - sass (3.4.20) - sass-rails (5.0.1) - railties (>= 4.0.0, < 5.0) + sass (3.4.23) + sass-rails (5.0.6) + railties (>= 4.0.0, < 6) sass (~> 3.1) sprockets (>= 2.8, < 4.0) sprockets-rails (>= 2.0, < 4.0) - tilt (~> 1.1) - seed-fu (2.3.5) - activerecord (>= 3.1, < 4.3) - activesupport (>= 3.1, < 4.3) + tilt (>= 1.1, < 3) + seed-fu (2.3.6) + activerecord (>= 3.1) + activesupport (>= 3.1) seed-fu-ndo (0.0.2) seed-fu (>= 2.2.0) selenium-webdriver (2.51.0) @@ -414,88 +424,81 @@ GEM multi_json (~> 1.0) rubyzip (~> 1.0) websocket (~> 1.0) - sexp_processor (4.6.0) - simplecov (0.11.1) + simplecov (0.15.1) docile (~> 1.1.0) - json (~> 1.8) + json (>= 1.8, < 3) simplecov-html (~> 0.10.0) - simplecov-html (0.10.0) + simplecov-html (0.10.2) simplecov-rcov (0.2.3) simplecov (>= 0.4.1) - simpleidn (0.0.5) - sinatra (1.4.6) - rack (~> 1.4) + simpleidn (0.0.7) + sinatra (1.4.8) + rack (~> 1.5) rack-protection (~> 1.4) tilt (>= 1.3, < 3) - skinny (0.2.3) + skinny (0.2.4) eventmachine (~> 1.0.0) - thin (~> 1.5.0) - slim (3.0.6) - temple (~> 0.7.3) - tilt (>= 1.3.3, < 2.1) + thin (>= 1.5, < 1.7) slop (3.6.0) sort_alphabetical (1.0.2) unicode_utils (>= 1.2.2) - spring (1.6.1) + spring (2.0.1) + activesupport (>= 4.2) spring-commands-rspec (1.0.4) spring (>= 0.9.1) - sprockets (2.12.4) - hike (~> 1.2) - multi_json (~> 1.0) - rack (~> 1.0) - tilt (~> 1.1, != 1.3.0) - sprockets-rails (2.3.3) - actionpack (>= 3.0) - activesupport (>= 3.0) - sprockets (>= 2.8, < 4.0) - sqlite3 (1.3.11) - temple (0.7.6) - terminal-table (1.5.2) - therubyracer (0.12.2) - libv8 (~> 3.16.14.0) + sprockets (3.7.1) + concurrent-ruby (~> 1.0) + rack (> 1, < 3) + sprockets-rails (3.2.0) + actionpack (>= 4.0) + activesupport (>= 4.0) + sprockets (>= 3.0.0) + sqlite3 (1.3.13) + therubyracer (0.12.3) + libv8 (~> 3.16.14.15) ref thin (1.5.1) daemons (>= 1.0.9) eventmachine (>= 0.12.6) rack (>= 1.0.0) - thinking-sphinx (3.1.4) + thinking-sphinx (3.4.1) activerecord (>= 3.1.0) builder (>= 2.1.2) innertube (>= 1.0.2) joiner (>= 0.2.0) middleware (>= 0.1.0) - riddle (>= 1.5.11) - thor (0.19.1) - thread_safe (0.3.5) - tilt (1.4.1) - timecop (0.8.0) - timeliness (0.3.7) - tins (1.6.0) + riddle (>= 2.0.0) + thor (0.19.4) + thread_safe (0.3.6) + tilt (2.0.6) + timeliness (0.3.8) ttfunk (1.4.0) - turbolinks (2.5.3) - coffee-rails - tzinfo (1.2.2) + turbolinks (5.0.1) + turbolinks-source (~> 5) + turbolinks-source (5.0.0) + tzinfo (1.2.3) thread_safe (~> 0.1) - uglifier (2.7.2) - execjs (>= 0.3.0) - json (>= 1.8.0) + uglifier (3.1.4) + execjs (>= 0.3.0, < 3) + unicode-display_width (1.3.0) unicode_utils (1.4.0) - uniform_notifier (1.9.0) + uniform_notifier (1.10.0) validates_by_schema (0.3.0) activerecord (>= 3.1.0) validates_timeliness (3.0.15) timeliness (~> 0.3.7) + vcard (0.2.15) wagons (0.4.8) bundler (>= 1.1) rails (>= 3.2) seed-fu-ndo (>= 0.0.2) - warden (1.2.4) + warden (1.2.7) rack (>= 1.0) - websocket (1.2.2) + websocket (1.2.4) wirble (0.1.3) xpath (2.0.0) nokogiri (~> 1.3) - yard (0.8.7.6) + yard (0.9.8) PLATFORMS ruby @@ -503,9 +506,10 @@ PLATFORMS DEPENDENCIES activerecord-session_store acts-as-taggable-on (~> 3.5.0) + addressable (< 2.5) airbrake (< 5.0) annotate - awesome_nested_set + awesome_nested_set (< 3.1.0) awesome_print axlsx (= 2.0.1) bcrypt-ruby @@ -516,7 +520,8 @@ DEPENDENCIES bullet cancancan (< 1.13.0) capybara - carrierwave + capybara-screenshot + carrierwave (< 0.11.1) chosen-rails ci_reporter_rspec cmess @@ -524,36 +529,41 @@ DEPENDENCIES coffee-rails compass compass-rails - config + config (< 1.1.0) country_select customized_piwik_analytics (~> 1.0.0) daemons dalli database_cleaner delayed_job_active_record - devise + devise (< 4.0.0) draper fabrication - faker + faker (< 1.6.4) globalize haml headless hirb + hitobito_pbs! + hitobito_youth! http_accept_language + icalendar jquery-rails jquery-turbolinks jquery-ui-rails - kaminari + kaminari (< 1.0.0) launchy magiclabs-userstamp mailcatcher mime-types (~> 2.6.2) mini_magick - mysql2 (= 0.3.15) + mysql2 (= 0.4.9) nested_form + nokogiri (< 1.7.0) oat paper_trail - paranoia + paranoia (< 2.1.2) + pdf-inspector prawn (< 2.0) prawn-table protective @@ -564,7 +574,7 @@ DEPENDENCIES pry-stack_explorer quiet_assets rack - rails (= 4.2.7.1) + rails (= 4.2.8) rails-erd rails-i18n rails_autolink @@ -578,19 +588,24 @@ DEPENDENCIES rubocop rubocop-checkstyle_formatter ruby-prof + rubyzip sass-rails seed-fu - selenium-webdriver + selenium-webdriver (= 2.51.0) simplecov-rcov simpleidn + sort_alphabetical (< 1.1.0) spring-commands-rspec sqlite3 therubyracer thinking-sphinx - timecop turbolinks uglifier validates_by_schema validates_timeliness (< 4.0) + vcard wagons wirble + +BUNDLED WITH + 1.16.0 diff --git a/Procfile b/Procfile new file mode 100644 index 0000000000..2e470afa35 --- /dev/null +++ b/Procfile @@ -0,0 +1,4 @@ +web: bundle exec rails s -b 0.0.0.0 -p $PORT +worker: bundle exec rake jobs:work +mail: mailcatcher -f + diff --git a/README.md b/README.md index 3ef875999b..1cb33aa694 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,10 @@ hitobito is an open source web application to manage complex group hierarchies w ## Development -Hitobito is a Ruby on Rails application that runs on Ruby >= 1.9.3 and Rails 4. +Hitobito is a Ruby on Rails application that runs on Ruby >= 2.1 and Rails 4. +It might run with minor tweaks on older Rubies, but is not tested against those +versions. + To get going, after you got a copy of hitobito and at least one wagon with an organization structure setup as described below, issue the following commands in the main directory: diff --git a/VERSION b/VERSION index 63738cc28d..b48f322609 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.14 +1.17 diff --git a/app/abilities/ability.rb b/app/abilities/ability.rb index 0ed71288b6..d5ede16648 100644 --- a/app/abilities/ability.rb +++ b/app/abilities/ability.rb @@ -15,13 +15,15 @@ class Ability store.register EventAbility, Event::ApplicationAbility, Event::ParticipationAbility, + Event::ParticipationContactDataAbility, Event::RoleAbility, GroupAbility, + InvoiceAbility, MailingListAbility, + NoteAbility, PeopleFilterAbility, PersonAbility, Person::AddRequestAbility, - Person::NoteAbility, QualificationAbility, RoleAbility, SubscriptionAbility, diff --git a/app/abilities/event/participation_ability.rb b/app/abilities/event/participation_ability.rb index dc6a1d0098..a8a08904f9 100644 --- a/app/abilities/event/participation_ability.rb +++ b/app/abilities/event/participation_ability.rb @@ -14,23 +14,24 @@ class Event::ParticipationAbility < AbilityDsl::Base permission(:any).may(:show).her_own_or_for_participations_read_events permission(:any).may(:show_details, :print).her_own_or_for_participations_full_events permission(:any).may(:create).her_own_if_application_possible - permission(:any).may(:update).for_participations_full_events + permission(:any).may(:show_full, :update).for_participations_full_events + permission(:any).may(:destroy).her_own_if_application_cancelable permission(:group_full). - may(:show, :show_details, :print, :create, :update, :destroy). + may(:show, :show_details, :show_full, :print, :create, :update, :destroy). in_same_group permission(:group_and_below_full). - may(:show, :show_details, :print, :create, :update, :destroy). + may(:show, :show_details, :show_full, :print, :create, :update, :destroy). in_same_group_or_below permission(:layer_full). - may(:show, :show_details, :print, :update). + may(:show, :show_details, :show_full, :print, :update). in_same_layer_or_different_prio permission(:layer_full).may(:create, :destroy).in_same_layer permission(:layer_and_below_full). - may(:show, :show_details, :print, :update). + may(:show, :show_details, :show_full, :print, :update). in_same_layer_or_below_or_different_prio permission(:layer_and_below_full).may(:create, :destroy).in_same_layer @@ -51,6 +52,12 @@ def her_own_if_application_possible her_own && event.application_possible? end + def her_own_if_application_cancelable + her_own && + event.applications_cancelable? && + (!event.application_closing_at? || event.application_closing_at >= Time.zone.today) + end + private def participation diff --git a/app/abilities/event/participation_contact_data_ability.rb b/app/abilities/event/participation_contact_data_ability.rb new file mode 100644 index 0000000000..33ab02fb4c --- /dev/null +++ b/app/abilities/event/participation_contact_data_ability.rb @@ -0,0 +1,18 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Pfadibewegung Schweiz. This file is part of +# hitobito_pbs and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito_pbs. + +class Event::ParticipationContactDataAbility < AbilityDsl::Base + + on(Event::ParticipationContactData) do + permission(:any).may(:show, :update).her_own + end + + def her_own + subject.person.id == user.id + end + +end diff --git a/app/abilities/event_ability.rb b/app/abilities/event_ability.rb index b594116dde..836711f27b 100644 --- a/app/abilities/event_ability.rb +++ b/app/abilities/event_ability.rb @@ -15,7 +15,7 @@ class EventAbility < AbilityDsl::Base permission(:any).may(:show).all permission(:any).may(:index_participations).for_participations_read_events permission(:any).may(:update).for_leaded_events - permission(:any).may(:qualify).for_qualify_event + permission(:any).may(:qualify, :qualifications_read).for_qualify_event permission(:group_full).may(:index_participations, :create, :update, :destroy).in_same_group @@ -24,15 +24,17 @@ class EventAbility < AbilityDsl::Base in_same_group_or_below permission(:layer_full). - may(:index_participations, :update, :create, :destroy, :application_market, :qualify). + may(:index_participations, :update, :create, :destroy, + :application_market, :qualify, :qualifications_read). in_same_layer permission(:layer_and_below_full). may(:index_participations, :update).in_same_layer_or_below permission(:layer_and_below_full). - may(:create, :destroy, :application_market, :qualify).in_same_layer + may(:create, :destroy, :application_market, :qualify, :qualifications_read).in_same_layer - general(:create, :destroy, :application_market, :qualify).at_least_one_group_not_deleted + general(:create, :destroy, :application_market, :qualify, :qualifications_read). + at_least_one_group_not_deleted end on(Event::Course) do diff --git a/app/abilities/group_ability.rb b/app/abilities/group_ability.rb index de2ff73ebd..611b04cd57 100644 --- a/app/abilities/group_ability.rb +++ b/app/abilities/group_ability.rb @@ -41,8 +41,9 @@ class GroupAbility < AbilityDsl::Base permission(:layer_full).may(:create).with_parent_in_same_layer permission(:layer_full).may(:destroy).in_same_layer_except_permission_giving permission(:layer_full). - may(:update, :reactivate, :index_person_add_requests, :index_person_notes, - :manage_person_tags, :activate_person_add_requests, :deactivate_person_add_requests). + may(:update, :reactivate, :index_person_add_requests, :index_notes, + :manage_person_tags, :activate_person_add_requests, :deactivate_person_add_requests, + :index_deleted_people). in_same_layer permission(:layer_and_below_read). @@ -54,14 +55,16 @@ class GroupAbility < AbilityDsl::Base permission(:layer_and_below_full).may(:create).with_parent_in_same_layer_or_below permission(:layer_and_below_full).may(:destroy).in_same_layer_or_below_except_permission_giving permission(:layer_and_below_full). - may(:update, :reactivate, :index_person_add_requests, :index_person_notes, - :manage_person_tags). + may(:update, :reactivate, :index_person_add_requests, :index_notes, + :manage_person_tags, :index_deleted_people). in_same_layer_or_below permission(:layer_and_below_full).may(:modify_superior).in_below_layers permission(:layer_and_below_full). may(:activate_person_add_requests, :deactivate_person_add_requests). in_same_layer + permission(:finance).may(:index_invoices).in_layer_group + general(:update).group_not_deleted general(:index_person_add_requests, :activate_person_add_requests, @@ -69,6 +72,10 @@ class GroupAbility < AbilityDsl::Base if_layer_group end + def in_layer_group + user.finance_groups.include?(subject) + end + def with_parent_in_same_layer parent = group.parent !group.layer? && parent && !parent.deleted? && permission_in_layer?(parent.layer_group_id) diff --git a/app/abilities/invoice_ability.rb b/app/abilities/invoice_ability.rb new file mode 100644 index 0000000000..4429ccb218 --- /dev/null +++ b/app/abilities/invoice_ability.rb @@ -0,0 +1,38 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +class InvoiceAbility < AbilityDsl::Base + + on(Invoice) do + permission(:finance).may(:create, :show, :edit, :update, :destroy).in_layer + end + + on(InvoiceArticle) do + permission(:finance).may(:new, :create, :show, :edit, :update, :destroy).in_layer + end + + on(InvoiceConfig) do + permission(:finance).may(:show, :edit, :update).in_layer + end + + on(Payment) do + permission(:finance).may(:create).in_layer + end + + on(PaymentReminder) do + permission(:finance).may(:create).in_layer + end + + def any_finance_group + user.finance_groups.present? + end + + def in_layer + user.groups_with_permission(:finance).collect(&:layer_group).include?(subject.group) + end + +end diff --git a/app/abilities/note_ability.rb b/app/abilities/note_ability.rb new file mode 100644 index 0000000000..0e349c426f --- /dev/null +++ b/app/abilities/note_ability.rb @@ -0,0 +1,42 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Dachverband Schweizer Jugendparlamente. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +class NoteAbility < AbilityDsl::Base + + on(Note) do + permission(:layer_full). + may(:create, :show, :destroy). + in_same_layer + + permission(:layer_and_below_full). + may(:create, :show, :destroy). + in_same_layer_or_below + end + + def in_same_layer + case subj + when Group then permission_in_layer?(subj.layer_group_id) + when Person then permission_in_layers?(subj.layer_group_ids) + else raise(ArgumentError, "Unknown note subject #{subj.class}") + end + end + + def in_same_layer_or_below + case subj + when Group then permission_in_layers?(subj.layer_hierarchy.collect(&:id)) + when Person then permission_in_layers?(subj.groups_hierarchy_ids) + else raise(ArgumentError, "Unknown note subject #{subj.class}") + end + end + + private + + def subj + subject.subject + end + +end diff --git a/app/abilities/people_filter_ability.rb b/app/abilities/people_filter_ability.rb index 95cae60fef..31ef6ac20d 100644 --- a/app/abilities/people_filter_ability.rb +++ b/app/abilities/people_filter_ability.rb @@ -1,6 +1,6 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. @@ -14,9 +14,9 @@ class PeopleFilterAbility < AbilityDsl::Base permission(:group_read).may(:new).in_same_group permission(:group_and_below_read).may(:new).in_same_group_or_below permission(:layer_read).may(:new).in_same_layer - permission(:layer_full).may(:create, :destroy).in_same_layer + permission(:layer_full).may(:create, :destroy, :edit, :update).in_same_layer permission(:layer_and_below_read).may(:new).in_same_layer_or_below - permission(:layer_and_below_full).may(:create, :destroy).in_same_layer + permission(:layer_and_below_full).may(:create, :destroy, :edit, :update).in_same_layer end end diff --git a/app/abilities/person/add_request_ability.rb b/app/abilities/person/add_request_ability.rb index 41f7274a2b..851c9bd496 100644 --- a/app/abilities/person/add_request_ability.rb +++ b/app/abilities/person/add_request_ability.rb @@ -13,12 +13,18 @@ class Person::AddRequestAbility < AbilityDsl::Base permission(:any).may(:approve, :reject).herself permission(:any).may(:reject).her_own - permission(:group_full).may(:approve, :reject).non_restricted_in_same_group - permission(:group_and_below_full).may(:approve, :reject).non_restricted_in_same_group_or_below - permission(:layer_full).may(:approve, :reject).non_restricted_in_same_layer + permission(:group_full). + may(:approve, :reject). + non_restricted_or_deleted_in_same_group + permission(:group_and_below_full). + may(:approve, :reject). + non_restricted_or_deleted_in_same_group_or_below + permission(:layer_full). + may(:approve, :reject). + non_restricted_or_deleted_in_same_layer permission(:layer_and_below_full). may(:approve, :reject). - non_restricted_in_same_layer_or_visible_below + non_restricted_or_deleted_in_same_layer_or_visible_below # This does not tell if people actually may be added, just if they may be added to some body, # that no request is required. Basically, this is possible if the user may already show the @@ -29,16 +35,80 @@ class Person::AddRequestAbility < AbilityDsl::Base permission(:group_and_below_read).may(:add_without_request).in_same_group_or_below permission(:layer_read).may(:add_without_request).in_same_layer permission(:layer_and_below_read).may(:add_without_request).in_same_layer_or_below + permission(:group_full). + may(:add_without_request). + active_or_deleted_in_same_group + permission(:group_and_below_full). + may(:add_without_request). + active_or_deleted_in_same_group_or_below + permission(:layer_full). + may(:add_without_request). + active_or_deleted_in_same_layer + permission(:layer_and_below_full). + may(:add_without_request). + active_or_deleted_in_same_layer_or_below end def her_own user.id == subject.requester_id end + def non_restricted_or_deleted_in_same_group + non_restricted_in_same_group || deleted_in_same_group + end + + def non_restricted_or_deleted_in_same_group_or_below + non_restricted_in_same_group || deleted_in_same_group_or_below + end + + def non_restricted_or_deleted_in_same_layer + non_restricted_in_same_layer || deleted_in_same_layer + end + + def non_restricted_or_deleted_in_same_layer_or_visible_below + non_restricted_in_same_layer_or_visible_below || deleted_in_same_layer_or_below + end + + def active_or_deleted_in_same_group + in_same_group || deleted_in_same_group + end + + def active_or_deleted_in_same_group_or_below + in_same_group_or_below || deleted_in_same_group_or_below + end + + def active_or_deleted_in_same_layer + in_same_layer || deleted_in_same_layer + end + + def active_or_deleted_in_same_layer_or_below + in_same_layer_or_below || deleted_in_same_layer_or_below + end + private def person subject.person end + def deleted_in_same_group + role = person.last_non_restricted_role + role && permission_in_group?(role.group_id) + end + + def deleted_in_same_group_or_below + role = person.last_non_restricted_role + role && permission_in_group?(role.group.local_hierarchy.collect(&:id)) + end + + def deleted_in_same_layer + role = person.last_non_restricted_role + role && permission_in_layer?(role.group.layer_group_id) + end + + def deleted_in_same_layer_or_below + role = person.last_non_restricted_role + role && permission_in_layers?(role.group.hierarchy.collect(&:id)) + end + end diff --git a/app/abilities/person/note_ability.rb b/app/abilities/person/note_ability.rb deleted file mode 100644 index 301122213d..0000000000 --- a/app/abilities/person/note_ability.rb +++ /dev/null @@ -1,28 +0,0 @@ -# encoding: utf-8 - -# Copyright (c) 2012-2016, Dachverband Schweizer Jugendparlamente. This file is part of -# hitobito_dsj and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito_dsj. - -class Person::NoteAbility < AbilityDsl::Base - - include AbilityDsl::Constraints::Person - - on(Person::Note) do - permission(:layer_full). - may(:create, :show). - in_same_layer - - permission(:layer_and_below_full). - may(:create, :show). - in_same_layer_or_below - end - - private - - def person - subject.person - end - -end diff --git a/app/abilities/person_ability.rb b/app/abilities/person_ability.rb index 148d6e02a8..ca6c020847 100644 --- a/app/abilities/person_ability.rb +++ b/app/abilities/person_ability.rb @@ -11,10 +11,14 @@ class PersonAbility < AbilityDsl::Base on(Person) do class_side(:index, :query).everybody + class_side(:index_people_without_role).if_admin - permission(:any).may(:show, :show_full, :history, :update, - :update_email, :primary_group, :log). - herself + permission(:admin).may(:destroy).not_self + + permission(:any). + may(:show, :show_details, :show_full, :history, :update, :update_email, :primary_group, :log, + :update_settings). + herself permission(:contact_data).may(:show).other_with_contact_data @@ -63,13 +67,26 @@ class PersonAbility < AbilityDsl::Base if_permissions_in_all_capable_groups_or_layer_or_above permission(:layer_and_below_full).may(:create).all # restrictions are on Roles + permission(:finance).may(:index_invoices).in_layer_group + permission(:any).may(:index_invoices).herself + + permission(:admin).may(:show).people_without_roles + general(:send_password_instructions).not_self end + def in_layer_group + contains_any?(user.finance_groups.collect(&:id), person.layer_group_ids) + end + def not_self subject.id != user.id end + def people_without_roles + subject.roles.empty? + end + def if_permissions_in_all_capable_groups !subject.root? && # true if capable roles is empty. diff --git a/app/abilities/person_fetchables.rb b/app/abilities/person_fetchables.rb index 479f07577e..3a65f9ace6 100644 --- a/app/abilities/person_fetchables.rb +++ b/app/abilities/person_fetchables.rb @@ -1,9 +1,9 @@ # encoding: utf-8 # Copyright (c) 2012-2015, Pfadibewegung Schweiz. This file is part of -# hitobito_pbs and licensed under the Affero General Public License version 3 +# hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito_pbs. +# https://github.com/hitobito/hitobito. # Commnon Base Class for fetching people. class PersonFetchables @@ -101,9 +101,9 @@ def layer_groups_with_permissions(*permissions) end def groups_with_permissions(*permissions) - permissions.collect { |p| user.groups_with_permission(p) }. - flatten. - uniq + permissions.collect { |p| user.groups_with_permission(p) } + .flatten + .uniq end end diff --git a/app/abilities/person_readables.rb b/app/abilities/person_readables.rb index 2294d9f1f4..4983b4930d 100644 --- a/app/abilities/person_readables.rb +++ b/app/abilities/person_readables.rb @@ -51,11 +51,11 @@ def accessible_people if user.root? Person.only_public_data else - Person.only_public_data. - joins(roles: :group). - where(roles: { deleted_at: nil }, groups: { deleted_at: nil }). - where(accessible_conditions.to_a). - uniq + Person.only_public_data + .joins(roles: :group) + .where(roles: { deleted_at: nil }, groups: { deleted_at: nil }) + .where(accessible_conditions.to_a) + .uniq end end diff --git a/app/abilities/various_ability.rb b/app/abilities/various_ability.rb index 770a8e51a5..8d9cc7ce0c 100644 --- a/app/abilities/various_ability.rb +++ b/app/abilities/various_ability.rb @@ -13,8 +13,10 @@ class VariousAbility < AbilityDsl::Base end on(LabelFormat) do - class_side(:index).if_admin + class_side(:index).everybody + class_side(:manage_global).if_admin permission(:admin).may(:manage).all + permission(:any).may(:create, :update, :destroy, :read).own end if Group.course_types.present? @@ -29,4 +31,7 @@ class VariousAbility < AbilityDsl::Base end end + def own + subject.person_id == user.id + end end diff --git a/app/assets/images/group.svg b/app/assets/images/group.svg new file mode 100644 index 0000000000..c97ddcb100 --- /dev/null +++ b/app/assets/images/group.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index 896ea7b3c7..59e8fb64ca 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -1,27 +1,29 @@ -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. -# This is a manifest file that'll be compiled into application.js, which will include all the files -# listed below. +# This is a manifest file that'll be compiled into application.js, which will +# include all the files listed below. # -# Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, -# or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. -# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the -# the compiled file. +# Any JavaScript/Coffee file within this directory, lib/assets/javascripts, +# vendor/assets/javascripts, or vendor/assets/javascripts of plugins, if any, +# can be referenced here using a relative path. It's not advisable to add code +# directly here, but if you do, it'll appear at the bottom of the the compiled +# file. # -# WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD -# GO AFTER THE REQUIRES BELOW. +# WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY +# BLANK LINE SHOULD GO AFTER THE REQUIRES BELOW. # #= require jquery -#= require jquery.turbolinks #= require jquery_ujs -#= require jquery-ui/datepicker +#= require jquery-ui/widgets/datepicker #= require jquery-ui-datepicker-i18n -#= require jquery-ui/effect-highlight +#= require jquery-ui/effects/effect-highlight +#= require bootstrap-transition #= require bootstrap-alert #= require bootstrap-button +#= require bootstrap-collapse #= require bootstrap-dropdown #= require bootstrap-tooltip #= require bootstrap-popover @@ -32,83 +34,9 @@ #= require jquery.remotipart #= require modernizr.custom.min #= require moment.min -#= require_self #= require_tree ./modules #= require wagon #= require turbolinks -#= require progress-bar # -# scope for global functions -app = window.App ||= {} - -# add trim function for older browsers -if !String.prototype.trim - String.prototype.trim = () -> this.replace(/^\s+|\s+$/g, '') - - -replaceContent = (e, data, status, xhr) -> - replace = $(this).data('replace') - el = if replace is true then $(this).closest('form') else $("##{replace}") - console.warn "found no element to replace" if el.size() is 0 - el.html(data) - -setDataType = (xhr) -> - $(this).data('type', 'html') - -toggleGroupContact = -> - open = !$('#group_contact_id').val() - fields = $('fieldset.info') - if !open && fields.is(':visible') - fields.slideUp() - else if open && !fields.is(':visible') - fields.slideDown() - -toggleFilterRoles = (event) -> - target = $(event.target) - - boxes = target.nextUntil('.filter-toggle').find(':checkbox') - checked = boxes.filter(':checked').length == boxes.length - - boxes.each((el) -> $(this).prop('checked', !checked)) - target.data('checked', !checked) - -app.activateChosen = (i, element) -> - element = $(element) - blank = element.find('option[value]').first().val() == '' - text = element.data('chosen-no-results') || ' ' - element.chosen({ no_results_text: text, search_contains: true, allow_single_deselect: blank, width: '100%' }) - - - -######################################################################## -# because of turbolinks.jquery, do bind ALL document events on top level - -# wire up elements with ajax replace -$(document).on('ajax:success','[data-replace]', replaceContent) -$(document).on('ajax:before','[data-replace]', setDataType) - -# show alert if ajax requests fail -$(document).on('ajax:error', (event, xhr, status, error) -> - alert('Sorry, something went wrong\n(' + error + ')')) - -# wire up disabled links -$(document).on('click', 'a.disabled', (event) -> $.rails.stopEverything(event); event.preventDefault();) - -# make clicking on typeahead item always select it (https://github.com/twitter/bootstrap/issues/4018) -$(document).on('mousedown', 'ul.typeahead', (e) -> e.preventDefault()) - -# control visibilty of group contact fields in relation to contact -$(document).on('change', '#group_contact_id', toggleGroupContact) - -$(document).on('click', '.filter-toggle', toggleFilterRoles) - -# only bind events for non-document elements in $ -> -$ -> - - # wire up tooltips - $(document).tooltip({ selector: '[rel^=tooltip]', placement: 'right' }) - - # enable chosen js - $('.chosen-select').each(app.activateChosen) diff --git a/app/assets/javascripts/modules/ajax_error_notification.js.coffee b/app/assets/javascripts/modules/ajax_error_notification.js.coffee new file mode 100644 index 0000000000..5bcd1ea9ac --- /dev/null +++ b/app/assets/javascripts/modules/ajax_error_notification.js.coffee @@ -0,0 +1,9 @@ +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +# show alert if ajax requests fail +$(document).on('ajax:error', (event, xhr, status, error) -> + alert('Sorry, something went wrong\n(' + error + ')')) + diff --git a/app/assets/javascripts/modules/ajax_replace.js.coffee b/app/assets/javascripts/modules/ajax_replace.js.coffee new file mode 100644 index 0000000000..f77648b740 --- /dev/null +++ b/app/assets/javascripts/modules/ajax_replace.js.coffee @@ -0,0 +1,17 @@ +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +replaceContent = (e, data, status, xhr) -> + replace = $(this).data('replace') + el = if replace is true then $(this).closest('form') else $("##{replace}") + console.warn "found no element to replace" if el.size() is 0 + el.html(data) + +setDataType = (xhr) -> + $(this).data('type', 'html') + +# wire up elements with ajax replace +$(document).on('ajax:success','[data-replace]', replaceContent) +$(document).on('ajax:before','[data-replace]', setDataType) diff --git a/app/assets/javascripts/modules/ajax_upload.js.coffee b/app/assets/javascripts/modules/ajax_upload.js.coffee index 8bd0c8b694..e46e5d63bd 100644 --- a/app/assets/javascripts/modules/ajax_upload.js.coffee +++ b/app/assets/javascripts/modules/ajax_upload.js.coffee @@ -1,4 +1,4 @@ -# Copyright (c) 2015 Pro Natura Schweiz. This file is part of +# Copyright (c) 2015-2017 Pro Natura Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. @@ -13,6 +13,7 @@ class app.AjaxUpload form = $(input).closest('form') new app.Spinner().show(form) form.submit() + $(input).closest('form').reset() bind: -> self = this diff --git a/app/assets/javascripts/modules/auto_submit.js.coffee b/app/assets/javascripts/modules/auto_submit.js.coffee new file mode 100644 index 0000000000..8af8d27a95 --- /dev/null +++ b/app/assets/javascripts/modules/auto_submit.js.coffee @@ -0,0 +1,9 @@ +# wire up auto submit fields + +$(document).on('change', '[data-submit]', (e) -> + form = $(this).closest('form') + if form.attr('method') == 'get' + Turbolinks.visit("#{form.attr('action')}?#{form.serialize()}", action: 'replace') + else + form.submit() +) diff --git a/app/assets/javascripts/modules/checkable.js.coffee b/app/assets/javascripts/modules/checkable.js.coffee new file mode 100644 index 0000000000..6868777b3a --- /dev/null +++ b/app/assets/javascripts/modules/checkable.js.coffee @@ -0,0 +1,23 @@ +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +$(document).on('click', 'table[data-checkable] thead :checkbox', (e) -> + checked = e.target.checked + table = $(e.target).closest('table[data-checkable]') + table.find('tbody :checkbox').prop('checked', checked) +) + +$(document).on('click', 'a[data-checkable]:not(data-method)', (e) -> + e.target.href = buildLinkWithIds(e.target.href) +) + +buildLinkWithIds = (href) -> + ids = ($(item).val() for item in $('table[data-checkable] tbody :checked')) + separator = if href.indexOf('?') != -1 then '&' else '?' + href + separator + "ids=#{ids}" + +$.rails.href = (element) -> + href = element[0].href + if $(element).is('a[data-checkable]') then buildLinkWithIds(href) else href diff --git a/app/assets/javascripts/modules/chosen.js.coffee b/app/assets/javascripts/modules/chosen.js.coffee new file mode 100644 index 0000000000..437ecb48d4 --- /dev/null +++ b/app/assets/javascripts/modules/chosen.js.coffee @@ -0,0 +1,23 @@ +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +# scope for global functions +app = window.App ||= {} + +app.activateChosen = (i, element) -> + element = $(element) + blank = element.find('option[value]').first().val() == '' + text = element.data('chosen-no-results') || ' ' + element.chosen({ + no_results_text: text, + search_contains: true, + allow_single_deselect: blank, + width: '100%' }) + +# only bind events for non-document elements in turbolinks:load +$(document).on('turbolinks:load', -> + # enable chosen js + $('.chosen-select').each(app.activateChosen) +) diff --git a/app/assets/javascripts/modules/clear_input.js.coffee b/app/assets/javascripts/modules/clear_input.js.coffee new file mode 100644 index 0000000000..967affba05 --- /dev/null +++ b/app/assets/javascripts/modules/clear_input.js.coffee @@ -0,0 +1,26 @@ +class ClearInput + + clear: (cross) -> + @_input(cross).val('').trigger('change') + + toggleHide: (input) -> + group = input.parents('.control-group') + if input.val() == '' + group.addClass('has-empty-value') + else + console.log input.val() + group.removeClass('has-empty-value') + + _input: (cross) -> + cross.parents('.control-group').find('input') + + bind: -> + self = this + $(document).on('click', '[data-clear]', () -> self.clear($(this))) + $(document).on('change', '.has-clear input', () -> self.toggleHide($(this))) + + +new ClearInput().bind() + +$(document).on 'turbolinks:load', -> + $('.has-clear input').each((i, e) -> new ClearInput().toggleHide($(e))) diff --git a/app/assets/javascripts/modules/course_description_handler.js.coffee b/app/assets/javascripts/modules/course_description_handler.js.coffee deleted file mode 100644 index 7f70e56283..0000000000 --- a/app/assets/javascripts/modules/course_description_handler.js.coffee +++ /dev/null @@ -1,59 +0,0 @@ -# Copyright (c) 2015 Pro Natura Schweiz. This file is part of -# hitobito and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito. - -app = window.App ||= {} - -app.EventDescription = { - getDescription: -> - that = app.EventDescription - id = $(this).val() - that.insertOrAsk(id) - - insertOrAsk: (id) -> - if this.descriptionEmpty() - this.fillDescription(id) - else - this.enableDefaultLink(id) - - getDescriptionForId: (id) -> - $('.default-description[data-kind=' + id + ']').text().trim() - - fillDescription: (id) -> - textarea = this.elements().textarea - oldText = textarea.val() - newText = this.getDescriptionForId(id) - - spacer = if oldText == "" then "" else " " - textarea.val(oldText + spacer + newText) - - descriptionEmpty: -> - return this.elements().textarea.val() == "" - - elements: -> - { - descriptionLink: $('.standard-description-link'), - textarea: $('textarea#event_description') - } - - enableDefaultLink: (id) -> - this.showLink() - link = this.elements().descriptionLink - - that = this - - link.off('click') - link.click (e) -> - e.preventDefault() - that.fillDescription(id) - that.hideLink() - - hideLink: -> - this.elements().descriptionLink.parents('.controls').hide(); - - showLink: -> - this.elements().descriptionLink.parents('.controls').show(); -} - -$(document).on('change', 'select#event_kind_id', app.EventDescription.getDescription) diff --git a/app/assets/javascripts/modules/disabled_links.js.coffee b/app/assets/javascripts/modules/disabled_links.js.coffee new file mode 100644 index 0000000000..fe1bd7aa08 --- /dev/null +++ b/app/assets/javascripts/modules/disabled_links.js.coffee @@ -0,0 +1,11 @@ +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +# wire up disabled links +$(document).on('click', 'a.disabled', (event) -> + $.rails.stopEverything(event) + event.preventDefault() +) + diff --git a/app/assets/javascripts/modules/element_swapper.js.coffee b/app/assets/javascripts/modules/element_swapper.js.coffee index 47b2c20b67..a7a70f7587 100644 --- a/app/assets/javascripts/modules/element_swapper.js.coffee +++ b/app/assets/javascripts/modules/element_swapper.js.coffee @@ -1,7 +1,7 @@ # Copyright (c) 2012-2016, Dachverband Schweizer Jugendparlamente. This file is part of -# hitobito_dsj and licensed under the Affero General Public License version 3 +# hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito_dsj. +# https://github.com/hitobito/hitobito. app = window.App ||= {} diff --git a/app/assets/javascripts/modules/element_toggler.js.coffee b/app/assets/javascripts/modules/element_toggler.js.coffee index d7596646d1..cb119b3cbf 100644 --- a/app/assets/javascripts/modules/element_toggler.js.coffee +++ b/app/assets/javascripts/modules/element_toggler.js.coffee @@ -35,7 +35,8 @@ $(document).on('change', 'input[data-hide]', (e) -> new app.ElementToggler(this) $(document).on('change', 'input[data-show]', (e) -> new app.ElementToggler(this).show()) $(document).on('click', 'a[data-hide]', (e) -> new app.ElementToggler(this).toggle(e)) -$ -> +$(document).on('turbolinks:load', -> # initialize visibility of checkbox controlled elements $('input[data-hide]').each((index, element) -> new app.ElementToggler(element).hide()) $('input[data-show]').each((index, element) -> new app.ElementToggler(element).show()) +) diff --git a/app/assets/javascripts/modules/event_kind_preconditions.js.coffee b/app/assets/javascripts/modules/event_kind_preconditions.js.coffee new file mode 100644 index 0000000000..c667cba3a8 --- /dev/null +++ b/app/assets/javascripts/modules/event_kind_preconditions.js.coffee @@ -0,0 +1,73 @@ +# Copyright (c) 2017, Pfadibewegung Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +app = window.App ||= {} + +app.EventKindPreconditions = { + + showFields: (e) -> + e.preventDefault() + $('#event_kind_precondition_kind_ids').val([]) + $('#add_precondition_grouping').hide() + $('#precondition_fields').slideDown() + + hideFields: (e) -> + e.preventDefault() + $('#precondition_fields').slideUp() + $('#add_precondition_grouping').show() + + removePreconditions: (e) -> + e.preventDefault() + $(this).parents('.precondition-grouping').remove() + $('.precondition-grouping:first-child .muted').remove() + + addPreconditions: (e) -> + e.preventDefault() + obj = app.EventKindPreconditions + ids = $('#event_kind_precondition_kind_ids').val() + if ids.length + grouping = $('.precondition-grouping').length + html = '
' + + ids.map((id) -> obj.buildHiddenField(grouping, id)).join(' ') + + obj.buildConjunction(grouping) + + obj.buildSentence() + + obj.buildRemoveLink() + + '
' + $('#add_precondition_grouping').before(html) + obj.hideFields(e); + + + buildHiddenField: (grouping, id) -> + '' + + buildConjunction: (grouping) -> + if grouping + '' + $('#precondition_summary').data('or') + ' ' + else + '' + + buildRemoveLink: -> + ' ' + + buildSentence: -> + labels = app.EventKindPreconditions.fetchLabels() + last = labels.pop() + if labels.length + labels.join(', ') + ' ' + $('#precondition_summary').data('and') + ' ' + last + else + last + + fetchLabels: -> + labels = [] + $('#event_kind_precondition_kind_ids option:selected').each((i, option) -> + labels.push($(option).text())) + labels +} + +$(document).on('click', '#add_precondition_grouping', app.EventKindPreconditions.showFields) +$(document).on('click', '#precondition_fields .cancel', app.EventKindPreconditions.hideFields) +$(document).on('click', '#precondition_fields button', app.EventKindPreconditions.addPreconditions) +$(document).on('click', '.remove-precondition-grouping', app.EventKindPreconditions.removePreconditions) diff --git a/app/assets/javascripts/modules/application_market.js.coffee b/app/assets/javascripts/modules/events/application_market.js.coffee similarity index 100% rename from app/assets/javascripts/modules/application_market.js.coffee rename to app/assets/javascripts/modules/events/application_market.js.coffee diff --git a/app/assets/javascripts/modules/events/course_description_handler.js.coffee b/app/assets/javascripts/modules/events/course_description_handler.js.coffee new file mode 100644 index 0000000000..e3c9faf08b --- /dev/null +++ b/app/assets/javascripts/modules/events/course_description_handler.js.coffee @@ -0,0 +1,61 @@ +# Copyright (c) 2017 Pro Natura Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +app = window.App ||= {} + +app.EventDescription = { + changeEventKind: -> + id = $(this).val() + app.EventDescription.insertOrAsk(id) + + insertOrAsk: (id) -> + if this.descriptionEmpty() + this.fillDescription(id) + else + this.enableDefaultLink(id) + + getDescriptionForId: (id) -> + $('.default-description[data-kind=' + id + ']').text().trim() + + fillDescription: (id) -> + textarea = this.textarea() + oldText = textarea.val() + newText = this.getDescriptionForId(id) + + spacer = if oldText == "" then "" else " " + textarea.val(oldText + spacer + newText) + + descriptionEmpty: -> + this.textarea().val() == "" + + insertDescription: (e) -> + e.preventDefault() + e.stopPropagation() + that = app.EventDescription + id = that.kindSelect().val() + that.fillDescription(id) + that.descriptionLink().hide() + + enableDefaultLink: (id) -> + if id && this.getDescriptionForId(id) != "" + this.descriptionLink().show() + else + this.descriptionLink().hide() + + descriptionLink: -> + $('.standard-description-link').parents('.help-block') + + textarea: -> + $('textarea#event_description') + + kindSelect: -> + $('select#event_kind_id') +} + +$(document).on('change', 'select#event_kind_id', app.EventDescription.changeEventKind) +$(document).on('click', '.standard-description-link', app.EventDescription.insertDescription) +$(document).on('turbolinks:load', -> + $('select#event_kind_id').each((i, e) -> + app.EventDescription.enableDefaultLink($(e).val()))) diff --git a/app/assets/javascripts/modules/date_period_validator.js.coffee b/app/assets/javascripts/modules/events/date_period_validator.js.coffee similarity index 100% rename from app/assets/javascripts/modules/date_period_validator.js.coffee rename to app/assets/javascripts/modules/events/date_period_validator.js.coffee diff --git a/app/assets/javascripts/modules/groups/group_contact_toggle.js.coffee b/app/assets/javascripts/modules/groups/group_contact_toggle.js.coffee new file mode 100644 index 0000000000..3a600dcd25 --- /dev/null +++ b/app/assets/javascripts/modules/groups/group_contact_toggle.js.coffee @@ -0,0 +1,16 @@ +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +# control visibilty of group contact fields in relation to contact + +toggleGroupContact = -> + open = !$('#group_contact_id').val() + fields = $('fieldset.info') + if !open && fields.is(':visible') + fields.slideUp() + else if open && !fields.is(':visible') + fields.slideDown() + +$(document).on('change', '#group_contact_id', toggleGroupContact) diff --git a/app/assets/javascripts/modules/input_enabler.js.coffee b/app/assets/javascripts/modules/input_enabler.js.coffee index fe05d47427..118fd3f02b 100644 --- a/app/assets/javascripts/modules/input_enabler.js.coffee +++ b/app/assets/javascripts/modules/input_enabler.js.coffee @@ -23,7 +23,8 @@ class app.InputEnabler $(document).on('change', 'input[data-disable]', (e) -> new app.InputEnabler(this).disable()) $(document).on('change', 'input[data-enable]', (e) -> new app.InputEnabler(this).enable()) -$ -> +$(document).on('turbolinks:load', -> # initialize disabled state of checkbox controlled elements $('input[data-disable]').each((index, element) -> new app.InputEnabler(element).disable()) $('input[data-enable]').each((index, element) -> new app.InputEnabler(element).enable()) +) diff --git a/app/assets/javascripts/modules/invoice_articles.js.coffee b/app/assets/javascripts/modules/invoice_articles.js.coffee new file mode 100644 index 0000000000..5f37fef8e0 --- /dev/null +++ b/app/assets/javascripts/modules/invoice_articles.js.coffee @@ -0,0 +1,20 @@ +app = window.App ||= {} + +app.InvoiceArticles = { + add: (e) -> + url = $('form[data-group').data('group') + articleAction = "#{url}/invoice_articles/#{e.target.value}.json" + $.ajax(url: articleAction, dataType: 'json', success: app.InvoiceArticles.updateForm) + e.target.value = undefined # reset field as preparation for next addition + + updateForm: (data, status, req) -> + $('.add_nested_fields').first().click() # add new lineitem + fields = $('#invoice_items_fields .fields').last().find('input, textarea') + fields.each (idx, elm) -> + name = elm.name.match(/\d\]\[(.*)\]$/)[1] + elm.value = data[name] if data[name] + app.Invoices.recalculate() + +} + +$(document).on('change', '#invoice_item_article', app.InvoiceArticles.add) diff --git a/app/assets/javascripts/modules/invoices.js.coffee b/app/assets/javascripts/modules/invoices.js.coffee new file mode 100644 index 0000000000..a61970f8f3 --- /dev/null +++ b/app/assets/javascripts/modules/invoices.js.coffee @@ -0,0 +1,14 @@ +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +app = window.App ||= {} + +app.Invoices = { + recalculate: (e) -> + form = $('form[data-group') + $.ajax(url: "#{form.data('group')}/invoice_list/new?#{form.serialize()}", dataType: 'script') +} + +$(document).on('input', '#invoice_items_fields :input[data-recalculate]', app.Invoices.recalculate) diff --git a/app/assets/javascripts/modules/notes.js.coffee b/app/assets/javascripts/modules/notes.js.coffee new file mode 100644 index 0000000000..2af0279d86 --- /dev/null +++ b/app/assets/javascripts/modules/notes.js.coffee @@ -0,0 +1,38 @@ +# Copyright (c) 2012-2017, Dachverband Schweizer Jugendparlamente. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +app = window.App ||= {} + +app.Notes = { + addNote: (note) -> + $('#notes-list').prepend(note) + $('#notes-list .pagination-info').text('') + + new app.ElementSwapper().swap.call($('#notes-form')) + app.Notes.resetForm() + app.Notes.hideError() + + resetForm: -> + $('#notes-form').find('form')[0].reset() + + focus: -> + setTimeout(-> $('#note_text').focus()) + + showError: (error) -> + $('#notes-error').text(error).show() + + hideError: -> + $('#notes-error').text('').hide() +} + + +$(document).on('turbolinks:load', -> + $('#notes-new-button').on('click', app.Notes.focus) + $('#notes-form .cancel').on('click', new app.ElementSwapper().swap) + $('#notes-form .cancel').on('click', -> + app.Notes.resetForm() + app.Notes.hideError() + ) +) diff --git a/app/assets/javascripts/modules/people/people_filter_role_toggle.js.coffee b/app/assets/javascripts/modules/people/people_filter_role_toggle.js.coffee new file mode 100644 index 0000000000..49b67b51ad --- /dev/null +++ b/app/assets/javascripts/modules/people/people_filter_role_toggle.js.coffee @@ -0,0 +1,41 @@ +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +toggleFilterRoles = (event) -> + target = $(event.target) + + boxes = target.nextUntil('.filter-toggle').find(':checkbox') + checked = boxes.filter(':checked').length == boxes.length + + boxes.each((el) -> $(this).prop('checked', !checked)) + target.data('checked', !checked) + +showAllGroups = (radio) -> + if radio.checked + $('.layer, .group').slideDown() + +showSameLayerGroups = (radio) -> + if radio.checked + $('.layer').hide() + $('.layer:not(.same-layer) input[type=checkbox]').prop('checked', false) + $('.same-layer').show() + $('.same-layer .group').slideDown() + +showSameGroup = (radio) -> + if radio.checked + $('.layer, .group').hide() + $('.layer:not(.same-layer) input[type=checkbox], .group:not(.same-group) input[type=checkbox]').prop('checked', false) + $('.same-layer, .same-group').show() + +$(document).on('click', '.filter-toggle', toggleFilterRoles) +$(document).on('change', 'input#range_deep', (e) -> showAllGroups(e.target)) +$(document).on('change', 'input#range_layer', (e) -> showSameLayerGroups(e.target)) +$(document).on('change', 'input#range_group', (e) -> showSameGroup(e.target)) + +$(document).on('turbolinks:load', -> + $('input#range_deep').each((i, e) -> showAllGroups(e)) + $('input#range_layer').each((i, e) -> showSameLayerGroups(e)) + $('input#range_group').each((i, e) -> showSameGroup(e)) +) diff --git a/app/assets/javascripts/modules/person_tags.js.coffee b/app/assets/javascripts/modules/people/person_tags.js.coffee similarity index 91% rename from app/assets/javascripts/modules/person_tags.js.coffee rename to app/assets/javascripts/modules/people/person_tags.js.coffee index e7944abd6b..528fb933b8 100644 --- a/app/assets/javascripts/modules/person_tags.js.coffee +++ b/app/assets/javascripts/modules/people/person_tags.js.coffee @@ -1,7 +1,7 @@ # Copyright (c) 2012-2016, Dachverband Schweizer Jugendparlamente. This file is part of -# hitobito_dsj and licensed under the Affero General Public License version 3 +# hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito_dsj. +# https://github.com/hitobito/hitobito. app = window.App ||= {} @@ -23,6 +23,7 @@ app.PersonTags = { updateTags: (tags) -> $('.person-tags').replaceWith(tags) app.PersonTags.hideForm() + $('.person-tag-add').focus(); removeTag: (event) -> event.preventDefault() @@ -44,4 +45,3 @@ $(document).on('click', '.person-tag-add', app.PersonTags.showForm) $(document).on('submit', '.person-tags-add-form', -> app.PersonTags.loading(true); return true); $(document).on('keydown', '.person-tags-add-form input#acts_as_taggable_on_tag_name', (event) -> event.keyCode == 27 && app.PersonTags.hideForm(); return true) - diff --git a/app/assets/javascripts/modules/people/toggle_condensed_labels.js.coffee b/app/assets/javascripts/modules/people/toggle_condensed_labels.js.coffee new file mode 100644 index 0000000000..2185587e17 --- /dev/null +++ b/app/assets/javascripts/modules/people/toggle_condensed_labels.js.coffee @@ -0,0 +1,16 @@ +# Copyright (c) 2015 Pfadibewegung Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +$(document).on('click', '#toggle-condense-labels', (event) -> + event.stopPropagation() + $(this).find('input[type="checkbox"]').toggle +) + +$(document).on('change', '#toggle-condense-labels input[type="checkbox"]', () -> + param = 'condense_labels=' + checked = !!this.checked + $(this).parents('.dropdown-menu').find('a.export-label-format').each -> + $(this).attr('href', $(this).attr('href').replace(param + !checked, param + checked)) +) diff --git a/app/assets/javascripts/modules/person_notes.js.coffee b/app/assets/javascripts/modules/person_notes.js.coffee deleted file mode 100644 index bcf6de0fac..0000000000 --- a/app/assets/javascripts/modules/person_notes.js.coffee +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright (c) 2012-2016, Dachverband Schweizer Jugendparlamente. This file is part of -# hitobito_dsj and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito_dsj. - -app = window.App ||= {} - -app.PersonNotes = { - addNote: (note) -> - $('#person-notes-list').prepend(note) - $('#person-notes-pagination .pagination-info').text('') - - new app.ElementSwapper().swap.call($('#person-notes-form')) - app.PersonNotes.resetForm() - app.PersonNotes.hideError() - - resetForm: -> - $('#person-notes-form').find('form')[0].reset() - - focus: -> - setTimeout(-> $('#person_note_text').focus()) - - showError: (error) -> - $('#person-notes-error').text(error).show() - - hideError: -> - $('#person-notes-error').text('').hide() -} - -$ -> - $('#person-notes-new-button').on('click', app.PersonNotes.focus) - $('#person-notes-form .cancel').on('click', new app.ElementSwapper().swap) - $('#person-notes-form .cancel').on('click', -> - app.PersonNotes.resetForm() - app.PersonNotes.hideError() - ) diff --git a/app/assets/javascripts/modules/popover_handler.js.coffee b/app/assets/javascripts/modules/popover_handler.js.coffee index 5d06ff569c..ad16a64de8 100644 --- a/app/assets/javascripts/modules/popover_handler.js.coffee +++ b/app/assets/javascripts/modules/popover_handler.js.coffee @@ -9,13 +9,14 @@ app = window.App ||= {} class app.PopoverHandler constructor: () -> - toggle: (toggler) -> + toggle: (toggler, event) -> # custom code to close other popovers when a new one is opened $('[data-toggle=popover]').not(toggler).popover('hide') $(toggler).popover() popover = $(toggler).data('popover') popover.options.html = true popover.options.placement = 'bottom' + event.preventDefault() if popover.tip().hasClass('fade') && !popover.tip().hasClass('in') $(toggler).popover('hide') else @@ -28,7 +29,7 @@ class app.PopoverHandler bind: -> self = this - $(document).on('click', '[data-toggle=popover]', (e) -> self.toggle(this)) + $(document).on('click', '[data-toggle=popover]', (e) -> self.toggle(this, e)) $(document).on('click', '.popover a.cancel', (e) -> self.close(e)) new app.PopoverHandler().bind() diff --git a/app/assets/javascripts/modules/remote_typeahead.js.coffee b/app/assets/javascripts/modules/remote_typeahead.js.coffee index ec72c3fb99..f141d8b1c1 100644 --- a/app/assets/javascripts/modules/remote_typeahead.js.coffee +++ b/app/assets/javascripts/modules/remote_typeahead.js.coffee @@ -53,8 +53,11 @@ setupRemoteTypeahead = (input, items, updater) -> queryForTypeahead = (query, process) -> return [] if query.length < 3 - $.get(this.$element.data('url'), { q: query }, (data) -> + app.request.abort() if app.request + $('#quicksearch').addClass('input-loading') + app.request = $.get(this.$element.data('url'), { q: query }, (data) -> json = $.map(data, (item) -> JSON.stringify(item)) + $('#quicksearch').removeClass('input-loading') return process(json) ) @@ -88,10 +91,14 @@ window.nestedFormEvents.insertFields = (content, assoc, link) -> .find('[data-provide=entity]').each(app.setupEntityTypeahead) -$ -> +# make clicking on typeahead item always select it (https://github.com/twitter/bootstrap/issues/4018) +$(document).on('mousedown', 'ul.typeahead', (e) -> e.preventDefault()) + +$(document).on('turbolinks:load', -> # wire up quick search app.setupQuicksearch() # wire up person auto complete $('[data-provide=entity]').each(app.setupEntityTypeahead) $('[data-provide]').each(() -> $(this).attr('autocomplete', "off")) +) diff --git a/app/assets/javascripts/modules/string_trim.js.coffee b/app/assets/javascripts/modules/string_trim.js.coffee new file mode 100644 index 0000000000..3b9313fa4d --- /dev/null +++ b/app/assets/javascripts/modules/string_trim.js.coffee @@ -0,0 +1,9 @@ +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +# add trim function for older browsers +if !String.prototype.trim + String.prototype.trim = () -> this.replace(/^\s+|\s+$/g, '') + diff --git a/app/assets/javascripts/modules/toggle_condensed_labels.js.coffee b/app/assets/javascripts/modules/toggle_condensed_labels.js.coffee deleted file mode 100644 index 32a6302c0a..0000000000 --- a/app/assets/javascripts/modules/toggle_condensed_labels.js.coffee +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright (c) 2015 Pfadibewegung Schweiz. This file is part of -# hitobito and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito. - -$ -> - button = $('#toggle-condense-labels') - checkbox = button.find('input[type="checkbox"]') - param = 'condense_labels=' - - button.click (event) -> - event.stopPropagation() - checkbox.toggle - - checkbox.change -> - checked = !!this.checked - $(this).parents('.dropdown-menu').find('a.export-label-format').each -> - $(this).attr('href', $(this).attr('href').replace(param + !checked, param + checked)) diff --git a/app/assets/javascripts/modules/tooltips.js.coffee b/app/assets/javascripts/modules/tooltips.js.coffee new file mode 100644 index 0000000000..82bc932045 --- /dev/null +++ b/app/assets/javascripts/modules/tooltips.js.coffee @@ -0,0 +1,10 @@ +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +# only bind events for non-document elements in turbolinks:load +$(document).on('turbolinks:load', -> + # wire up tooltips + $(document).tooltip({ selector: '[rel^=tooltip]', placement: 'right' }) +) diff --git a/app/assets/javascripts/progress-bar.js.coffee b/app/assets/javascripts/progress-bar.js.coffee deleted file mode 100644 index f24610d86d..0000000000 --- a/app/assets/javascripts/progress-bar.js.coffee +++ /dev/null @@ -1 +0,0 @@ -Turbolinks.enableProgressBar() diff --git a/app/assets/javascripts/wysiwyg.js.coffee b/app/assets/javascripts/wysiwyg.js.coffee index bf0781b731..522f6b808a 100644 --- a/app/assets/javascripts/wysiwyg.js.coffee +++ b/app/assets/javascripts/wysiwyg.js.coffee @@ -22,8 +22,8 @@ for lang, title of wysi_languages for num in [1..6] $.fn.wysihtml5.locale[lang].font_styles["h#{num}"] = "#{title} #{num}" -$ -> +$(document).on('turbolinks:load', -> wysilocale = do -> lang = $('html').attr('lang') lang + '-' + lang.toUpperCase() @@ -31,5 +31,6 @@ $ -> # wire up wysiwyg text areas $('textarea.wysiwyg').wysihtml5({ locale: wysilocale - }); + }) +) diff --git a/app/assets/stylesheets/bootstrap_config_manual.scss b/app/assets/stylesheets/bootstrap_config_manual.scss index dc6bcc9f76..5f3c1c3142 100644 --- a/app/assets/stylesheets/bootstrap_config_manual.scss +++ b/app/assets/stylesheets/bootstrap_config_manual.scss @@ -55,7 +55,7 @@ //@import "bootstrap/thumbnails"; @import "bootstrap/labels-badges"; //@import "bootstrap/progress-bars"; -//@import "bootstrap/accordion"; +@import "bootstrap/accordion"; //@import "bootstrap/carousel"; //@import "bootstrap/hero-unit"; diff --git a/app/assets/stylesheets/hitobito/_form.scss b/app/assets/stylesheets/hitobito/_form.scss index add400b4c8..6637a16a04 100644 --- a/app/assets/stylesheets/hitobito/_form.scss +++ b/app/assets/stylesheets/hitobito/_form.scss @@ -32,6 +32,7 @@ } } + .icon, [class^="icon-"], [class*=" icon-"] { @@ -49,6 +50,12 @@ &.btn-info, &.btn-info:hover, &.btn-inverse, &.btn-inverse:hover { color: $white; + + .icon, + [class^="icon-"], + [class*=" icon-"] { + background-image: image-url("glyphicons-halflings-white.png"); + } } } @@ -82,6 +89,9 @@ a.green { margin-bottom: 0; } +.form-inline-search .control-group { + display: inline-block; +} @include responsive(mediaPhone) { .form-horizontal .btn-toolbar { margin-left: 180px; @@ -145,6 +155,18 @@ input[type="color"], } } +.input-loading { + background-image: image-url("spinner.gif"); + background-size: 16px; + background-position:right 5px center; + background-repeat: no-repeat; +} + +select { + height: 30px; + line-height: 30px; +} + .form-inline { select, textarea, @@ -264,6 +286,9 @@ textarea { .controls > .text { line-height: 29px; + padding-bottom: 8px; + vertical-align: middle; + display: inline-block; } .remove_nested_fields { @@ -397,3 +422,72 @@ input.switcher { box-shadow: inset 0 0 0 1px $green, 0 1px 2px rgba(0, 0, 0, .2); } } + +.accordion-heading { + .header { + font-size: 1.2em; + font-weight: 600; + } + + a { + color: $black; + } + + a:hover { + text-decoration: none; + } +} + +.accordion-body { + legend { + border-bottom: 0; + font-size: 1.12em; + } + + fieldset:last-child { + margin-bottom: 0; + } +} + +#roles { + legend + .layer { + margin-top: 0; + } + + .layer { + + .layer { + margin-top: 2em; + } + + .control-group { + margin-top: 0; + } + + h4, h5 { + cursor: pointer; + } + } +} + +.has-clear { + position: relative; + input::-ms-clear { display: none; } + + [data-clear] { + cursor: pointer; + opacity: 0.3; + pointer-events: auto; + position: absolute; + right: 5px; + top: 30px; + + &:hover { + opacity: 0.5; + } + } + + &.has-empty-value { + [data-clear] { display: none; } + } +} + diff --git a/app/assets/stylesheets/hitobito/_layout.scss b/app/assets/stylesheets/hitobito/_layout.scss index 03e4e964ab..ee0d8be357 100644 --- a/app/assets/stylesheets/hitobito/_layout.scss +++ b/app/assets/stylesheets/hitobito/_layout.scss @@ -273,11 +273,18 @@ body { } } -// well .well { border: none; background: $grayLighter; + > { + h1, h2, h3, h4, h5 { + &:first-child { + margin-top: 0; + } + } + } + &.panel { margin-top: 5px; border: 2px solid $grayLighter; @@ -301,39 +308,6 @@ body { background-color: $purple !important; } -table.roles { - width: 100%; - td { - border: none; - padding: 0px; - } - td:first-child { padding-left: 10px; width: 100%; } - td { min-width: 20px; } -} - -.table-responsive { - .table { - background-color: #fff; - } - - @media screen and (max-width: $mediaDesktop) { - width: 100%; - margin-bottom: 15px; - overflow-x: scroll; - overflow-y: hidden; - -ms-overflow-style: -ms-autohiding-scrollbar; - -webkit-overflow-scrolling: touch; - - .table { - margin-bottom: 0; - } - } -} - -.table-striped th:first-child { - padding-left: 8px; -} - // Logs .log-item { margin-top: 2em; diff --git a/app/assets/stylesheets/hitobito/_main.scss b/app/assets/stylesheets/hitobito/_main.scss index a123021f95..81cc81c7c6 100644 --- a/app/assets/stylesheets/hitobito/_main.scss +++ b/app/assets/stylesheets/hitobito/_main.scss @@ -7,11 +7,16 @@ @import "hitobito/typography"; @import "hitobito/navigation"; @import "hitobito/form"; +@import "hitobito/table"; @import "hitobito/layout"; @import "hitobito/fonts"; /* Modules */ -@import "hitobito/modules/person_note"; +@import "hitobito/modules/note"; @import "hitobito/modules/person_tags"; +@import "hitobito/modules/chip"; +@import "hitobito/modules/step_wizard"; +@import "hitobito/modules/invoice"; + @import "hitobito/wagon"; diff --git a/app/assets/stylesheets/hitobito/_table.scss b/app/assets/stylesheets/hitobito/_table.scss new file mode 100644 index 0000000000..15799a5470 --- /dev/null +++ b/app/assets/stylesheets/hitobito/_table.scss @@ -0,0 +1,40 @@ +.table-responsive { + .table { + background-color: #fff; + } + + @media screen and (max-width: $mediaDesktop) { + width: 100%; + margin-bottom: 15px; + overflow-x: scroll; + overflow-y: hidden; + -ms-overflow-style: -ms-autohiding-scrollbar; + -webkit-overflow-scrolling: touch; + + .table { + margin-bottom: 0; + } + } +} + +.table-striped th:first-child { + padding-left: 8px; +} + +.table-fixed { + table-layout: fixed; +} + +td.action { + text-align: center; +} + +table.roles { + width: 100%; + td { + border: none; + padding: 0px; + } + td:first-child { padding-left: 10px; width: 100%; } + td { min-width: 20px; } +} diff --git a/app/assets/stylesheets/hitobito/modules/_chip.scss b/app/assets/stylesheets/hitobito/modules/_chip.scss new file mode 100644 index 0000000000..adbbdd99ce --- /dev/null +++ b/app/assets/stylesheets/hitobito/modules/_chip.scss @@ -0,0 +1,33 @@ +.chip { + display: inline-block; + box-sizing: border-box; + height: 28px; + margin-bottom: 8px; + padding: 4px 12px 6px 12px; + font-size: 13px; + border-radius: 14px; + background-color: $grayLight; + + i { + opacity: 0.6; + } + i:hover { + opacity: 1; + } + + &.chip-add { + border: 2px dashed $grayLight; + padding: 2px 10px 4px 10px; + font-weight: normal; + background-color: transparent; + color: $textColor; + cursor: pointer; + + i { + opacity: 0.8; + } + &:hover { + background-color: $grayLighter; + } + } +} diff --git a/app/assets/stylesheets/hitobito/modules/_invoice.scss b/app/assets/stylesheets/hitobito/modules/_invoice.scss new file mode 100644 index 0000000000..46ec55265f --- /dev/null +++ b/app/assets/stylesheets/hitobito/modules/_invoice.scss @@ -0,0 +1,73 @@ +.invoice-state { + background: $grayLighter; + margin-left: -20px; + margin-right: -20px; + padding: 10px 21px; + + .invoice-state-table { + float: left; + margin-left: 50px; + margin-bottom: 20px; + + tr th { + color: $gray; + font-weight: normal; + text-align: left; + } + tr td{ + padding-right: 30px; + } + } + + .invoice-history{ + table { + float: left; + margin-left: 50px; + + td:first-child { font-size: 6px; padding-right: 5px; } + td.red:first-child { color: $red; } + td.blue:first-child { color: $blue; } + td.green:first-child { color: $green; } + td:nth-child(2) { + padding-right: 20px; + color: $gray; + font-weight: normal; + } + } + } +} + +.invoice { + margin: 0px 50px; + + .invoice-recipient-address{ + margin: 30px 0px; + } + + + .invoice-table { + table.header { + border-bottom: 1px solid $grayLight; + width: 100%; + font-size: 16px; + margin-bottom: 10px; + } + + table tr th:last-child, + table tr td:last-child { + @extend .right; + } + + .invoice-items-total { + margin-top: -15px; + height: 60px; + + table{ + float: right; + width: 250px; + tr{ border-bottom: 1px solid $grayLight; } + tr:first-child{ border-bottom: none; } + } + } + } +} diff --git a/app/assets/stylesheets/hitobito/modules/_note.scss b/app/assets/stylesheets/hitobito/modules/_note.scss new file mode 100644 index 0000000000..2b0f16f595 --- /dev/null +++ b/app/assets/stylesheets/hitobito/modules/_note.scss @@ -0,0 +1,72 @@ +#notes-list .note:first-child { + border-top: 0; +} + +.notes-pagination { + margin: 0; +} + +#notes-index .pagination { + margin-top: 20px; + margin-bottom: 0; +} + +#notes-index .pagination:first-child { + margin-bottom: 20px; + margin-top: 0; +} + +.note { + border-top: 1px solid $grayLight; + padding: 5px 0 10px; + position: relative; + display: flex; + + .note-image { + flex: 0 0 auto; + margin-right: 20px; + height: 40px; + width: 40px; + } + + .note-body { + flex: 1 1 auto; + width: 100%; + } + + .note-subject { + font-size: 110%; + } + + .note-author { + float: right; + } + + .note-date { + display: inline-block; + padding-left: 0.2em; + } + + &.is-current-subject .note-author { + width: 100%; + } + + @include responsive(phone, $mediaTablet) { + .note-subject { + width: 100%; + clear: both; + } + .note-author { + float: left; + width: 100%; + } + } + + i { + opacity: 0.7; + } + + i:hover { + opacity: 1; + } +} diff --git a/app/assets/stylesheets/hitobito/modules/_person_note.scss b/app/assets/stylesheets/hitobito/modules/_person_note.scss deleted file mode 100644 index 28c1f57c4a..0000000000 --- a/app/assets/stylesheets/hitobito/modules/_person_note.scss +++ /dev/null @@ -1,34 +0,0 @@ -.note { - border-top: 1px solid $grayLight; - padding: 5px 0 10px; - position: relative; - - .note-image { - position: absolute; - width: 40px; - } - - .note-body { - position: relative; - left: 60px; - padding-right: 60px; - widht: 100%; - } - - .note-person { - font-size: 110%; - } - - .note-author { - float: right; - } - @include responsive(phone, $mediaTablet) { - .note-person { - width: 100%; - clear: both; - } - .note-author { - float: left; - } - } -} diff --git a/app/assets/stylesheets/hitobito/modules/_person_tags.scss b/app/assets/stylesheets/hitobito/modules/_person_tags.scss index 00f2e4c92b..115eb19a5c 100644 --- a/app/assets/stylesheets/hitobito/modules/_person_tags.scss +++ b/app/assets/stylesheets/hitobito/modules/_person_tags.scss @@ -1,5 +1,6 @@ .person-tags-category { display: table-row; + margin-top: 12px; } .person-tags-category-title, .person-tags-category-list { @@ -12,30 +13,11 @@ } .person-tags-category-title { + padding-bottom: 20px; padding-right: 20px; text-align: right; } -.person-tag { - margin-bottom: 4px; - padding: 3px 6px; - border: 2px solid $grayDark; - font-size: 14px; - line-height: 20px; -} - -.person-tag-add { - color: $textColor; - font-weight: normal; - border: 2px dashed $grayLight; - background-color: transparent; - cursor: pointer; - - &:hover { - background-color: $grayLighter; - } -} - .person-tags-add-form { display: inline-block; margin-bottom: 4px; diff --git a/app/assets/stylesheets/hitobito/modules/_step_wizard.scss b/app/assets/stylesheets/hitobito/modules/_step_wizard.scss new file mode 100644 index 0000000000..8f51549395 --- /dev/null +++ b/app/assets/stylesheets/hitobito/modules/_step_wizard.scss @@ -0,0 +1,94 @@ +.stepwizard { + $stepwizard-size: 2em; + + display: flex; + flex-direction: row; + font-size: 80%; + margin-bottom: $stepwizard-size/2; + + background-color: $grayLight; + border-radius: 2em; + + .stepwizard-step { + flex: 1 1 auto; + display: flex; + flex-direction: row; + } + + .stepwizard-link { + color: inherit; + display: flex; + overflow: hidden; + text-overflow: ellipsis; + align-items: center; + } + + .stepwizard-link:hover { + .stepwizard-number { + background-color: $blue; + text-decoration: none; + } + } + + // .stepwizard-step:not(:first-child):before, .stepwizard-step:not(:last-child):after { + // background-color: $gray; + // content: ""; + // height: 1px; + // width: 1px; + // margin-top: $stepwizard-size / 2; + // flex: 1 1 auto; + // } + + .stepwizard-number { + flex: 1 0 auto; + background-color: $gray; + color: white; + width: $stepwizard-size; + height: $stepwizard-size; + border-radius: $stepwizard-size; + display: flex; + align-items: center; + justify-content: center; + margin-right: $stepwizard-size / 4; + } + + .stepwizard-number:not(:first-child) { + margin-left: $stepwizard-size / 4; + } + + .stepwizard-title { + display: none; + align-items: center; + text-overflow: ellipsis; + white-space: nowrap; + margin-right: $stepwizard-size; + + } + + @include responsive(mediaPhone) { + font-size: 80%; + .stepwizard-title { + display: flex; + } + } + + @include responsive(mediaTablet){ + font-size: 100%; + } + + .is-current { + .stepwizard-number { + background-color: $blue; + } + .stepwizard-title { + color: $blue; + } + } + + .is-disabled { + .stepwizard-link { + pointer-events: none; + opacity: 0.7; + } + } +} diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index ba3ad6270f..34c40c548d 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -22,6 +22,7 @@ class ApplicationController < ActionController::Base hide_action :person_home_path before_action :set_no_cache + before_action :set_paper_trail_whodunnit alias_method :decorate, :__decorator_for__ @@ -52,6 +53,6 @@ def set_no_cache end def html_request? - request.format.html? || request.format == Mime::ALL + request.formats.any? { |f| f.html? || f == Mime::ALL } end end diff --git a/app/controllers/changelog_controller.rb b/app/controllers/changelog_controller.rb index 1ac1ec7237..98ca3bd035 100644 --- a/app/controllers/changelog_controller.rb +++ b/app/controllers/changelog_controller.rb @@ -6,15 +6,11 @@ # https://github.com/hitobito/hitobito. class ChangelogController < ApplicationController + + skip_before_action :authenticate_person! skip_authorization_check def index - end - private - - def devise_controller? - true # hence, no login required - end end diff --git a/app/controllers/concerns/render_people_exports.rb b/app/controllers/concerns/render_people_exports.rb index 4783851964..386faf4b66 100644 --- a/app/controllers/concerns/render_people_exports.rb +++ b/app/controllers/concerns/render_people_exports.rb @@ -1,6 +1,6 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. @@ -20,6 +20,11 @@ def render_emails(people) emails = Person.mailing_emails_for(people) render text: emails.join(',') end + + def render_vcf(people) + vcf = generate_vcf(people) + send_data vcf, type: :vcf, disposition: 'inline' + end private @@ -34,6 +39,10 @@ def condense_people(people) def generate_pdf(people) Export::Pdf::Labels.new(find_and_remember_label_format).generate(people) end + + def generate_vcf(people) + Export::Vcf::Vcards.new.generate(people) + end def find_and_remember_label_format LabelFormat.find(params[:label_format_id]).tap do |label_format| diff --git a/app/controllers/concerns/searchable.rb b/app/controllers/concerns/searchable.rb index bbe53b8ead..0d93405115 100644 --- a/app/controllers/concerns/searchable.rb +++ b/app/controllers/concerns/searchable.rb @@ -1,6 +1,6 @@ # encoding: utf-8 -# Copyright (c) 2012-2015, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. @@ -8,48 +8,56 @@ # The search functionality for the index table. # Extracted into an own module for convenience. module Searchable - def self.included(controller) + + extend ActiveSupport::Concern + + included do # Define an array of searchable columns in your subclassing controllers. - controller.class_attribute :search_columns - controller.search_columns = [] + class_attribute :search_columns + self.search_columns = [] - controller.helper_method :search_support? + helper_method :search_support? - controller.alias_method_chain :list_entries, :search + alias_method_chain :list_entries, :search end private # Enhance the list entries with an optional search criteria def list_entries_with_search - list_entries_without_search.where(search_condition(*search_columns)) + list_entries_without_search.where(search_conditions) end - # Compose the search condition with a basic SQL OR query. - def search_condition(*columns) - if columns.present? && params[:q].present? - terms = search_terms - col_condition = search_column_conditions(columns) - clause = terms.collect { |_| "(#{col_condition})" }.join(' AND ') - - ["(#{clause})"] + terms.collect { |t| [t] * columns.size }.flatten + # Concat the word clauses with AND. + def search_conditions + if search_support? && params[:q].present? + search_condition(*self.class.search_tables_and_fields) end end - def search_terms - params[:q].split(/\s+/).collect { |t| "%#{t}%" } + # Returns true if this controller has searchable columns. + def search_support? + search_columns.present? end - def search_column_conditions(columns) - columns.collect do |f| - col = f.to_s.include?('.') ? f : "#{model_class.table_name}.#{f}" - "#{col} LIKE ?" - end.join(' OR ') + def search_condition(*fields) + SearchStrategies::SqlConditionBuilder.new(params[:q], fields).search_conditions end - # Returns true if this controller has searchable columns. - def search_support? - search_columns.present? + # Class methods for Searchable. + module ClassMethods + + # All search columns divided in table and field names. + def search_tables_and_fields + @search_tables_and_fields ||= search_columns.map do |f| + if f.to_s.include?('.') + f + else + "#{model_class.table_name}.#{f}" + end + end + end + end end diff --git a/app/controllers/event/application_market_controller.rb b/app/controllers/event/application_market_controller.rb index f4db3fe703..e740cfe5be 100644 --- a/app/controllers/event/application_market_controller.rb +++ b/app/controllers/event/application_market_controller.rb @@ -52,8 +52,7 @@ def load_participants def load_applications Event::Participation. - joins(:event). - includes(:application, person: [:primary_group]). + includes(:application, :event, person: [:primary_group]). references(:application). where(filter_applications). merge(Event::Participation.pending). diff --git a/app/controllers/event/attachments_controller.rb b/app/controllers/event/attachments_controller.rb index bc430d33fd..6e19708df2 100644 --- a/app/controllers/event/attachments_controller.rb +++ b/app/controllers/event/attachments_controller.rb @@ -1,6 +1,6 @@ # encoding: utf-8 -# Copyright (c) 2015, Pro Natura Schweiz. This file is part of +# Copyright (c) 2015-2017, Pro Natura Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. @@ -22,7 +22,7 @@ def self.model_class private - alias_method :event, :parent + alias event parent def index_path group_event_path(*parents) diff --git a/app/controllers/event/kinds_controller.rb b/app/controllers/event/kinds_controller.rb index 59f2f0f12d..e5fbf3216e 100644 --- a/app/controllers/event/kinds_controller.rb +++ b/app/controllers/event/kinds_controller.rb @@ -1,6 +1,6 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. @@ -9,15 +9,16 @@ class Event::KindsController < SimpleCrudController self.permitted_attrs = [:label, :short_name, :minimum_age, :general_information, :application_conditions, + precondition_qualification_kinds: [{ qualification_kind_ids: [] }], qualification_kinds: { participant: { - precondition: { qualification_kind_ids: [] }, qualification: { qualification_kind_ids: [] }, - prolongation: { qualification_kind_ids: [] } }, + prolongation: { qualification_kind_ids: [] } + }, leader: { - precondition: { qualification_kind_ids: [] }, qualification: { qualification_kind_ids: [] }, - prolongation: { qualification_kind_ids: [] } } + prolongation: { qualification_kind_ids: [] } + } }] self.sort_mappings = { label: 'event_kind_translations.label', @@ -49,9 +50,12 @@ def unlimited_qualifications def permitted_params attrs = super kinds_attrs = attrs.delete(:qualification_kinds) || {} + precondition_attrs = attrs.delete(:precondition_qualification_kinds) || {} + existing_kinds = entry.event_kind_qualification_kinds.to_a kinds_attrs = flatten_nested_qualification_kinds(kinds_attrs, existing_kinds) + kinds_attrs += flatten_precondition_qualification_kinds(precondition_attrs, existing_kinds) mark_qualifikation_kinds_for_removal!(kinds_attrs, existing_kinds) attrs[:event_kind_qualification_kinds_attributes] = kinds_attrs @@ -63,21 +67,35 @@ def flatten_nested_qualification_kinds(kinds_attrs, existing_kinds) kinds_attrs.flat_map do |role, categories| categories.flat_map do |category, ids| ids.fetch(:qualification_kind_ids, []).collect do |id| - { id: qualification_kind_assoc_id(id, role, category, existing_kinds), + { id: find_qualification_kind_assoc_id(existing_kinds, id, role, category), role: role, category: category, qualification_kind_id: id } - end end end end - def qualification_kind_assoc_id(qualification_kind_id, role, category, existing_kinds) + def flatten_precondition_qualification_kinds(grouped_ids, existing_kinds) + grouped_ids.each_with_index.flat_map do |(_, ids), index| + ids.fetch(:qualification_kind_ids, []).map do |id| + { id: find_qualification_kind_assoc_id(existing_kinds, id, + 'participant', 'precondition', index + 1), + role: 'participant', + category: 'precondition', + qualification_kind_id: id, + grouping: index + 1 } + end + end + end + + def find_qualification_kind_assoc_id(existing_kinds, qualification_kind_id, role, + category, grouping = nil) kind = existing_kinds.find do |k| k.role == role && k.category == category && - k.qualification_kind_id == qualification_kind_id + k.qualification_kind_id == qualification_kind_id && + k.grouping == grouping end kind.try(:id) end diff --git a/app/controllers/event/lists_controller.rb b/app/controllers/event/lists_controller.rb index b7786104f8..b3f4c05a1f 100644 --- a/app/controllers/event/lists_controller.rb +++ b/app/controllers/event/lists_controller.rb @@ -11,7 +11,7 @@ class Event::ListsController < ApplicationController DEFAULT_GROUPING = ->(event) { I18n.l(event.dates.first.start_at, format: :month_year) } attr_reader :group_id - helper_method :group_id, :kind_used?, :nav_left + helper_method :group_id, :kind_used?, :nav_left, :display_any_booking_info? skip_authorize_resource only: [:events, :courses] @@ -31,7 +31,10 @@ def courses @grouped_events = sorted(grouped(limited_courses_scope, course_grouping)) end format.csv do - render_courses_csv(limited_courses_scope) + render_tabular(:csv, limited_courses_scope) + end + format.xlsx do + render_tabular(:xlsx, limited_courses_scope) end end end @@ -48,11 +51,11 @@ def sorted(courses) courses.values.each do |entries| entries.sort_by! { |e| e.dates.first.try(:start_at) || Time.zone.now } end - courses + Hash[courses.sort] end - def render_courses_csv(courses) - send_data Export::Csv::Events::List.export(courses), type: :csv + def render_tabular(format, courses) + send_data Export::Tabular::Events::List.export(format, courses), type: format end def set_group_vars @@ -65,19 +68,32 @@ def set_group_vars end def upcoming_user_events - Event.upcoming. - in_hierarchy(current_user). - includes(:dates, :groups). - where('events.type != ? OR events.type IS NULL', Event::Course.sti_name). - order('event_dates.start_at ASC') + Event. + upcoming. + in_hierarchy(current_user). + includes(:dates, :groups). + where('events.type != ? OR events.type IS NULL', Event::Course.sti_name). + order('event_dates.start_at ASC') end def default_user_course_group - Group.course_offerers. - where(id: current_user.groups_hierarchy_ids). - where('groups.id <> ?', Group.root.id). - select(:id). - first + course_group_from_primary_layer || course_group_from_hierarchy + end + + def course_group_from_primary_layer + Group. + course_offerers. + where(id: current_user.primary_group.try(:layer_group_id)). + first + end + + def course_group_from_hierarchy + Group. + course_offerers. + where(id: current_user.groups_hierarchy_ids). + where('groups.id <> ?', Group.root.id). + select(:id). + first end def limited_courses_scope(scope = course_scope) @@ -89,11 +105,11 @@ def limited_courses_scope(scope = course_scope) end def course_scope - Event::Course - .includes(:groups, additional_course_includes) - .order(course_ordering) - .in_year(year) - .list + Event::Course. + includes(:groups, additional_course_includes). + order(course_ordering). + in_year(year). + list end def course_grouping @@ -116,6 +132,10 @@ def nav_left @nav_left || params[:action] end + def display_any_booking_info? + @grouped_events.values.flatten.any? { |e| e.display_booking_info? } + end + def authorize_courses if request.format.csv? authorize!(:export_list, Event::Course) diff --git a/app/controllers/event/participation_contact_datas_controller.rb b/app/controllers/event/participation_contact_datas_controller.rb new file mode 100644 index 0000000000..c364a94355 --- /dev/null +++ b/app/controllers/event/participation_contact_datas_controller.rb @@ -0,0 +1,67 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Pfadibewegung Schweiz. This file is part of +# hitobito_pbs and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito_pbs. + +class Event::ParticipationContactDatasController < ApplicationController + + helper_method :group, :event, :entry + + authorize_resource :entry, class: Event::ParticipationContactData + + decorates :group, :event + + before_action :set_entry, :group + + def edit; end + + def update + if entry.save + redirect_to new_group_event_participation_path( + group, + event, + event_role: { type: params[:event_role][:type] } + ) + else + render :edit + end + end + + private + + def entry + @participation_contact_data + end + + def build_entry + Event::ParticipationContactData.new(event, current_user) + end + + def set_entry + @participation_contact_data = + if params[:event_participation_contact_data] + Event::ParticipationContactData.new(event, current_user, model_params) + else + build_entry + end + end + + def event + @event ||= Event.find(params[:event_id]) + end + + def group + @group ||= Group.find(params[:group_id]) + end + + def model_params + params.require('event_participation_contact_data').permit(permitted_attrs) + end + + def permitted_attrs + PeopleController.permitted_attrs + end + +end diff --git a/app/controllers/event/participations_controller.rb b/app/controllers/event/participations_controller.rb index 6aef0da4ff..189af7f4cc 100644 --- a/app/controllers/event/participations_controller.rb +++ b/app/controllers/event/participations_controller.rb @@ -1,6 +1,6 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. @@ -21,11 +21,13 @@ class Event::ParticipationsController < CrudController first_name: 'people.first_name', roles: lambda do |event| Person.order_by_name_statement.unshift( - Event::Participation.order_by_role_statement(event)) + Event::Participation.order_by_role_statement(event) + ) end, nickname: 'people.nickname', zip_code: 'people.zip_code', - town: 'people.town' } + town: 'people.town', + birthday: 'people.birthday' } decorates :group, :event, :participation, :participations, :alternatives @@ -37,11 +39,13 @@ class Event::ParticipationsController < CrudController before_action :check_preconditions, only: [:new] before_render_new :init_answers + before_render_edit :load_answers before_render_form :load_priorities before_render_show :load_answers before_render_show :load_precondition_warnings after_create :send_confirmation_email + after_destroy :send_cancel_email # new and create are only invoked by people who wish to # apply for an event themselves. A participation for somebody @@ -64,7 +68,9 @@ def index @person_add_requests = fetch_person_add_requests end format.pdf { render_pdf(entries.collect(&:person)) } - format.csv { send_data(exporter.export(entries), type: :csv) } + format.csv { render_tabular_in_background(:csv) && redirect_to(action: :index) } + format.vcf { render_vcf(entries.includes(person: :phone_numbers).collect(&:person)) } + format.xlsx { render_tabular_in_background(:xlsx) && redirect_to(action: :index) } format.email { render_emails(entries.collect(&:person)) } end end @@ -78,7 +84,16 @@ def print end def destroy - super(location: group_event_application_market_index_path(group, event)) + location = if entry.person_id == current_user.id + group_event_path(group, event) + else + group_event_application_market_index_path(group, event) + end + super(location: location) + end + + def self.model_class + Event::Participation end private @@ -103,12 +118,9 @@ def authorize_class authorize!(:index_participations, event) end - def exporter - if params[:details] && can?(:show_details, entries.first) - Export::Csv::People::ParticipationsFull - else - Export::Csv::People::ParticipationsAddress - end + def render_tabular_in_background(format) + Export::EventParticipationsExportJob.new(format, current_person.id, event.id, params).enqueue! + flash[:notice] = translate(:export_enqueued, email: current_person.email) end def check_preconditions @@ -157,7 +169,7 @@ def role_type type = event.class.find_role_type!(role_type) unless type.participant? - fail ActiveRecord::RecordNotFound, "No participant role '#{role_type}' found" + raise ActiveRecord::RecordNotFound, "No participant role '#{role_type}' found" end role_type end @@ -174,17 +186,17 @@ def assign_attributes end def init_answers - entry.init_answers + @answers = entry.init_answers entry.init_application end def load_priorities if entry.application && event.priorization && current_user - @alternatives = event.class.application_possible. - where(kind_id: event.kind_id). - in_hierarchy(current_user). - includes(:groups). - list + @alternatives = event.class.application_possible + .where(kind_id: event.kind_id) + .in_hierarchy(current_user) + .includes(:groups) + .list @priority_2s = @priority_3s = (@alternatives.to_a - [event]) end end @@ -216,6 +228,12 @@ def send_confirmation_email end end + def send_cancel_email + if entry.person_id == current_user.id + Event::CancelApplicationJob.new(entry.event, entry.person).enqueue! + end + end + def set_success_notice if action_name.to_s == 'create' notice = translate(:success, full_entry_label: full_entry_label) @@ -226,12 +244,8 @@ def set_success_notice end end - def user_course_application? - entry.person == current_user && event.supports_applications - end - def append_mailing_instructions? - user_course_application? && event.signature? + entry.person == current_user && event.signature? end def event @@ -255,7 +269,4 @@ def fetch_person_add_requests end end - def self.model_class - Event::Participation - end end diff --git a/app/controllers/event/qualifications_controller.rb b/app/controllers/event/qualifications_controller.rb index 84690149a0..941db82024 100644 --- a/app/controllers/event/qualifications_controller.rb +++ b/app/controllers/event/qualifications_controller.rb @@ -7,48 +7,39 @@ class Event::QualificationsController < ApplicationController - before_action :authorize + before_action :authorize_write, except: :index + before_action :authorize_read, only: :index - decorates :event, :leaders, :participants, :participation, :group + decorates :event, :leaders, :participants, :group - helper_method :event, :participation + helper_method :event def index entries end def update - qualifier.issue - - @nothing_changed = qualifier.nothing_changed? - - respond_to do |format| - format.html { redirect_to group_event_qualifications_path(group, event) } - format.js { render 'qualification' } + Qualification.transaction do + entries + (@leaders + @participants).uniq.each do |participation| + qualifier = Event::Qualifier.for(participation) + qualified = Array(params[:participation_ids]).include?(participation.id.to_s) + qualified ? qualifier.issue : qualifier.revoke + end end - end - - def destroy - qualifier.revoke - respond_to do |format| - format.html { redirect_to group_event_qualifications_path(group, event) } - format.js { render 'qualification' } - end + redirect_to group_event_qualifications_path(group, event), + notice: t('event.qualifications.update.flash.success') end private def entries - types = event.class.role_types + types = event.role_types @leaders ||= participations(*types.select(&:leader?)) @participants ||= participations(*types.select(&:participant?)) end - def qualifier - @qualifier ||= Event::Qualifier.for(participation) - end - def event @event ||= group.events.find(params[:event_id]) end @@ -57,16 +48,22 @@ def group @group ||= Group.find(params[:group_id]) end - def participation - @participation ||= event.participations.find(params[:id]) - end - def participations(*role_types) event.participations_for(*role_types).includes(:roles, :event) end - def authorize - not_found unless event.course_kind? && event.qualifying? + def authorize_write + event_qualifying authorize!(:qualify, event) end + + def authorize_read + event_qualifying + authorize!(:qualifications_read, event) + end + + def event_qualifying + not_found unless event.course_kind? && event.qualifying? + end + end diff --git a/app/controllers/event/register_controller.rb b/app/controllers/event/register_controller.rb index 847fd670cd..1a50e8289d 100644 --- a/app/controllers/event/register_controller.rb +++ b/app/controllers/event/register_controller.rb @@ -1,6 +1,6 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. @@ -96,8 +96,8 @@ def person @person ||= Person.new end - alias_method :entry, :person - alias_method :resource, :person + alias entry person + alias resource person def event @event ||= group.events.find(params[:id]) diff --git a/app/controllers/event/roles_controller.rb b/app/controllers/event/roles_controller.rb index 0e21f479ab..514cf250a0 100644 --- a/app/controllers/event/roles_controller.rb +++ b/app/controllers/event/roles_controller.rb @@ -49,7 +49,7 @@ def build_entry attrs.delete(:event_id) attrs.delete(:person) # assert that type is valid - event.class.find_role_type!(attrs[:type]) + event.find_role_type!(attrs[:type]) participation = event.participations.where(person_id: attrs.delete(:person_id)). first_or_initialize @@ -104,9 +104,9 @@ def permitted_params def possible_types @possible_types ||= if entry.restricted? - event.class.participant_types + event.participant_types else - event.class.role_types.reject(&:restricted?) + event.role_types.reject(&:restricted?) end end diff --git a/app/controllers/event/top_controller.rb b/app/controllers/event/top_controller.rb new file mode 100644 index 0000000000..8cca2fccfb --- /dev/null +++ b/app/controllers/event/top_controller.rb @@ -0,0 +1,36 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Pfadibewegung Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +# Handles a top-level event route (/event/:id) +class Event::TopController < ApplicationController + + before_action :authorize_action + + def show + redirect_to_group_event + end + + private + + def entry + @event ||= Event.find(params[:id]) + end + + def redirect_to_group_event + flash.keep if html_request? + redirect_to group_event_path(entry.groups.first, + entry, + format: request.format.to_sym, + user_email: params[:user_email], + user_token: params[:user_token]) + end + + def authorize_action + authorize!(:show, entry) + end + +end diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb index d020bb9a94..07995d70b1 100644 --- a/app/controllers/events_controller.rb +++ b/app/controllers/events_controller.rb @@ -1,6 +1,6 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. @@ -12,14 +12,20 @@ class EventsController < CrudController # Respective event attrs are added in corresponding instance method. self.permitted_attrs = [:signature, :signature_confirmation, :signature_confirmation_text, + :display_booking_info, group_ids: [], - dates_attributes: [:id, :label, :location, :start_at, :start_at_date, - :start_at_hour, :start_at_min, :finish_at, - :finish_at_date, :finish_at_hour, :finish_at_min, - :_destroy], - questions_attributes: [:id, :question, :choices, :multiple_choices, - :required, - :_destroy]] + dates_attributes: [ + :id, :label, :location, :start_at, :start_at_date, + :start_at_hour, :start_at_min, :finish_at, + :finish_at_date, :finish_at_hour, :finish_at_min, + :_destroy + ], + application_questions_attributes: [ + :id, :question, :choices, :multiple_choices, :required, :_destroy + ], + admin_questions_attributes: [ + :id, :question, :choices, :multiple_choices, :_destroy + ]] self.remember_params += [:year] @@ -30,14 +36,23 @@ class EventsController < CrudController # load group before authorization prepend_before_action :parent + before_render_show :load_user_participation before_render_form :load_sister_groups before_render_form :load_kinds def index respond_to do |format| - format.html { entries } - format.csv { render_csv(entries) } - format.xlsx { render_xlsx(entries) } + format.html { entries } + format.csv { render_tabular_in_background(:csv) && redirect_to(action: :index) } + format.xlsx { render_tabular_in_background(:xlsx) && redirect_to(action: :index) } + format.ics { render_ical(entries) } + end + end + + def show + respond_to do |format| + format.html { entry } + format.ics { render_ical([entry]) } end end @@ -61,11 +76,15 @@ def list_entries private def build_entry - type = model_params && model_params[:type].presence - type ||= Event.sti_name - event = Event.find_event_type!(type).new - event.groups << parent - event + if params[:source_id] + group.events.find(params[:source_id]).duplicate + else + type = model_params && model_params[:type].presence + type ||= Event.sti_name + event = Event.find_event_type!(type).new + event.groups << parent + event + end end def permitted_params @@ -97,12 +116,19 @@ def load_kinds end end - def render_csv(entries) - send_data ::Export::Csv::Events::List.export(entries), type: :csv + def load_user_participation + if current_user + @user_participation = current_user.event_participations.where(event_id: entry.id).first + end + end + + def render_tabular_in_background(format) + Export::EventsExportJob.new(format, current_person.id, params[:type], year, parent).enqueue! + flash[:notice] = translate(:export_enqueued, email: current_person.email) end - def render_xlsx(entries) - send_data ::Export::Xlsx::Events::List.export(entries), type: :xlsx + def render_ical(entries) + send_data ::Export::Ics::Events.new.generate(entries), type: :ics, disposition: :inline end def typed_group_events_path(group, event_type, options = {}) @@ -125,4 +151,25 @@ def export? format = request.format format.xlsx? || format.csv? end + + def assign_attributes + assign_contact_attrs + super + end + + def assign_contact_attrs + contact_attrs = model_params.delete(:contact_attrs) + return unless contact_attrs.present? + reset_contact_attrs + contact_attrs.each do |a, v| + entry.required_contact_attrs << a if v.to_sym == :required + entry.hidden_contact_attrs << a if v.to_sym == :hidden + end + end + + def reset_contact_attrs + entry.required_contact_attrs = [] + entry.hidden_contact_attrs = [] + end + end diff --git a/app/controllers/full_text_controller.rb b/app/controllers/full_text_controller.rb index a5d43bedb5..2634146d69 100644 --- a/app/controllers/full_text_controller.rb +++ b/app/controllers/full_text_controller.rb @@ -14,91 +14,48 @@ class FullTextController < ApplicationController respond_to :html def index - @people = if params[:q].to_s.size >= 2 - PaginatingDecorator.decorate(list_people) - else - Kaminari.paginate_array([]).page(1) - end - respond_with(@people) + @people = with_query { search_strategy.list_people } + @groups = with_query { search_strategy.query_groups } + @events = with_query { search_strategy.query_events } end def query - people = query_people.collect { |i| PersonDecorator.new(i).as_quicksearch } - groups = query_groups.collect { |i| GroupDecorator.new(i).as_quicksearch } + people = search_strategy.query_people.collect { |i| PersonDecorator.new(i).as_quicksearch } + groups = search_strategy.query_groups.collect { |i| GroupDecorator.new(i).as_quicksearch } + events = search_strategy.query_events.collect { |i| EventDecorator.new(i).as_quicksearch } - render json: result_with_separator(people, groups) + render json: results_with_separator(people, groups, events) || [] end private - def list_people - return Person.none.page(1) unless params[:q].present? - query_accessible_people do |ids| - entries = Person.search(Riddle::Query.escape(params[:q]), - page: params[:page], - order: 'last_name asc, ' \ - 'first_name asc, ' \ - "#{ThinkingSphinx::SphinxQL.weight[:select]} desc", - star: true, - with: { sphinx_internal_id: ids }) - entries = Person::PreloadGroups.for(entries) - entries = Person::PreloadPublicAccounts.for(entries) - entries + def results_with_separator(*sets) + sets.select(&:present?).inject do |memo, set| + memo + [{ label: '—' * 20 }] + set end end - def query_people - return Person.none.page(1) unless params[:q].present? - query_accessible_people do |ids| - Person.search(Riddle::Query.escape(params[:q]), - per_page: 10, - star: true, - with: { sphinx_internal_id: ids }) - end - end - - def query_accessible_people - ids = accessible_people_ids - return Person.none.page(1) if ids.blank? - yield ids + def search_strategy + @search_strategy ||= search_strategy_class.new(current_user, params[:q], params[:page]) end - def query_groups - return Person.none.page(1) unless params[:q].present? - Group.search(Riddle::Query.escape(params[:q]), - per_page: 10, - star: true, - include: :parent) - end - - def accessible_people_ids - key = "accessible_people_ids_for_#{current_user.id}" - Rails.cache.fetch(key, expires_in: 15.minutes) do - load_accessible_people_ids + def search_strategy_class + if sphinx? + SearchStrategies::Sphinx + else + SearchStrategies::Sql end end - def load_accessible_people_ids - accessible = Person.accessible_by(PersonReadables.new(current_user)) - - # This still selects all people attributes :( - # accessible.pluck('people.id') - - # rewrite query to only include id column - sql = accessible.to_sql.gsub(/SELECT (.+) FROM /, 'SELECT DISTINCT people.id FROM ') - result = Person.connection.execute(sql) - result.collect { |row| row[0] } - end - - def result_with_separator(people, groups) - if people.present? && groups.present? - people + [{ label: '—' * 20 }] + groups - else - people + groups - end + def sphinx? + Hitobito::Application.sphinx_present? end def entries @people end + + def with_query + params[:q].to_s.size >= 2 ? yield : [] + end end diff --git a/app/controllers/group/deleted_people_controller.rb b/app/controllers/group/deleted_people_controller.rb new file mode 100644 index 0000000000..8d1f0459d1 --- /dev/null +++ b/app/controllers/group/deleted_people_controller.rb @@ -0,0 +1,36 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +class Group::DeletedPeopleController < ListController + + before_action :authorize_action + + self.nesting = Group + + decorates :people, :group + + private + + def group + parent + end + + def model_class + Person + end + + def list_entries + Group::DeletedPeople.deleted_for(group). + includes(:additional_emails, :phone_numbers). + order_by_name. + page(params[:page]) + end + + def authorize_action + authorize!(:index_deleted_people, group) + end +end diff --git a/app/controllers/group/person_add_requests_controller.rb b/app/controllers/group/person_add_requests_controller.rb index 075cb68319..8252fc0441 100644 --- a/app/controllers/group/person_add_requests_controller.rb +++ b/app/controllers/group/person_add_requests_controller.rb @@ -36,8 +36,7 @@ def deactivate def load_entries Person::AddRequest. for_layer(group). - includes(:body, - :person, + includes(:person, requester: { roles: :group }). merge(Person.order_by_name) end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 50ef69290a..aa4a2d633e 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -56,7 +56,7 @@ def export_subgroups without_deleted. order(:lft). includes(:contact) - csv = Export::Csv::Groups::List.export(list) + csv = Export::Tabular::Groups::List.csv(list) send_data csv, type: :csv end @@ -105,8 +105,9 @@ def load_sub_groups(scope) @sub_groups[label] << group end end - # move this entry to the end - @sub_groups[sub_groups_label] = @sub_groups.delete(sub_groups_label) + # move entry with non-layer groups to the end + children = @sub_groups.delete(sub_groups_label) + @sub_groups[sub_groups_label] = children if children end diff --git a/app/controllers/invoice_articles_controller.rb b/app/controllers/invoice_articles_controller.rb new file mode 100644 index 0000000000..16a40cb85b --- /dev/null +++ b/app/controllers/invoice_articles_controller.rb @@ -0,0 +1,24 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +class InvoiceArticlesController < CrudController + + respond_to :json, only: [:show] + + self.nesting = Group + + self.permitted_attrs = %i[ + number name description category unit_cost vat_rate cost_center account + ] + + private + + def authorize_class + authorize!(:index_invoices, parent) + end + +end diff --git a/app/controllers/invoice_configs_controller.rb b/app/controllers/invoice_configs_controller.rb new file mode 100644 index 0000000000..b2d8c70a86 --- /dev/null +++ b/app/controllers/invoice_configs_controller.rb @@ -0,0 +1,27 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +class InvoiceConfigsController < CrudController + + self.nesting = Group + self.permitted_attrs = [:payment_information, :address, :iban, :account_number] + + private + + def build_entry + parent.invoice_config + end + + def find_entry + parent.invoice_config + end + + def path_args(_) + [parent, :invoice_config] + end + +end diff --git a/app/controllers/invoice_lists_controller.rb b/app/controllers/invoice_lists_controller.rb new file mode 100644 index 0000000000..3a13a19339 --- /dev/null +++ b/app/controllers/invoice_lists_controller.rb @@ -0,0 +1,126 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +class InvoiceListsController < CrudController + self.nesting = Group + self.permitted_attrs = [:title, + :description, + :recipient_ids, + invoice_items_attributes: [ + :name, + :description, + :unit_cost, + :vat_rate, + :count, + :_destroy + ]] + + skip_authorize_resource + before_action :authorize + before_action :prepare_flash + respond_to :js, only: [:new] + + helper_method :cancel_url + + def new + assign_attributes + session[:invoice_referer] = request.referer + end + + def create + assign_attributes + entry.recipient = parent.people.first + succeeded = entry.multi_create if entry.valid? + + if succeeded + redirect_with(count: entry.recipients.size, title: entry.title) + session.delete :invoice_referer + else + render :new + end + end + + def update + updated = invoices.includes(:recipient).map do |invoice| + alert('not_draft', invoice) && next unless invoice.draft? + alert('no_mail', invoice) && next if send_mail? && invoice.recipient_email.blank? + + update_and_send_mail(invoice) + end.compact + + redirect_with(count: updated.count) + end + + # rubocop:disable Rails/SkipsModelValidations + def destroy + count = invoices.update_all(state: :cancelled, updated_at: Time.zone.now) + redirect_with(count: count) + end + + def show + redirect_to group_invoices_path(parent) + end + + def self.model_class + Invoice + end + + private + + def send_mail? + params[:mail] == 'true' + end + + def update_and_send_mail(invoice) + invoice.update(state: send_mail? ? :sent : :issued).tap do + enqueue_sender_job(invoice) if send_mail? + end + end + + def enqueue_sender_job(invoice) + Invoice::SendNotificationJob.new(invoice, current_person).enqueue! + end + + def list_entries + super.includes(recipient: [:groups, :roles]) + end + + def invoices + parent.invoices.where(id: params[:ids].to_s.split(',')) + end + + def redirect_with(attrs) + i18n_prefix = "#{controller_name}.#{action_name}" + message = I18n.t(i18n_prefix, attrs) + key = attrs[:count] > 0 ? :notice : :alert + flash[key] << message + flash[key] << I18n.t("#{i18n_prefix}.background_send", attrs) if send_mail? + redirect_to group_invoices_path(parent) + end + + def prepare_flash + flash[:notice] = [] + flash[:alert] = [] + end + + def alert(key, invoice) + flash[:alert] << I18n.t( + "#{controller_name}.#{action_name}.error.#{key}", + number: invoice.sequence_number, + name: invoice.recipient_name + ) + end + + def authorize + authorize!(:create, parent.invoices.build) + end + + def cancel_url + session[:invoice_referer] || group_invoices_path(parent) + end + +end diff --git a/app/controllers/invoices_controller.rb b/app/controllers/invoices_controller.rb new file mode 100644 index 0000000000..8df9f0f659 --- /dev/null +++ b/app/controllers/invoices_controller.rb @@ -0,0 +1,128 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +class InvoicesController < CrudController + decorates :invoice + + self.nesting = Group + self.sort_mappings = { recipient: Person.order_by_name_statement } + self.search_columns = [:title, :sequence_number, 'people.last_name', 'people.email'] + self.permitted_attrs = [:title, :description, :state, :due_at, + :recipient_id, :recipient_email, :recipient_address, + invoice_items_attributes: [ + :id, + :name, + :description, + :unit_cost, + :vat_rate, + :count, + :_destroy + ]] + + def index + respond_to do |format| + format.html { super } + format.pdf { generate_pdf(list_entries.includes(:invoice_items)) } + format.csv { render_invoices_csv(list_entries.includes(:invoice_items)) } + end + end + + def show + @invoice_items = InvoiceItemDecorator.decorate_collection(entry.invoice_items) + respond_to do |format| + format.html { render_html } + format.pdf { generate_pdf([entry]) } + format.csv { render_invoices_csv([entry]) } + end + end + + def destroy + cancelled = run_callbacks(:destroy) { entry.update(state: :cancelled) } + set_failure_notice unless cancelled + respond_with(entry, success: cancelled, location: group_invoices_path(parent)) + end + + private + + def render_html + if entry.remindable? + @reminder = entry.payment_reminders.build(reminder_attrs) + @reminder_valid = reminder_attrs ? @reminder.valid? : true + + @payment = entry.payments.build(payment_attrs) + @payment_valid = payment_attrs ? @payment.valid? : true + end + end + + def generate_pdf(invoices) + if params[:label_format_id] + render_labels(invoices) + else + render_invoices_pdf(invoices) + end + end + + def render_invoices_csv(invoices) + csv = Export::Tabular::Invoices::List.csv(invoices) + send_data csv, type: :csv, filename: filename(:csv, invoices) + end + + def render_invoices_pdf(invoices) + pdf = Export::Pdf::Invoice.render_multiple(invoices, pdf_options) + send_data pdf, type: :pdf, disposition: 'inline', filename: filename(:pdf, invoices) + end + + def filename(extension, invoices) + if invoices.size > 1 + "#{t('activerecord.models.invoice.other').downcase}.#{extension}" + else + invoices.first.filename(extension) + end + end + + def render_labels(invoices) + recipients = Invoice.to_contactables(invoices) + pdf = Export::Pdf::Labels.new(find_and_remember_label_format).generate(recipients) + send_data pdf, type: :pdf, disposition: 'inline' + rescue Prawn::Errors::CannotFit + redirect_to :back, alert: t('people.pdf.cannot_fit') + end + + def list_entries + scope = super.includes(recipient: [:groups, :roles]).references(:recipient).list + scope = scope.page(params[:page]).per(50) + Invoice::Filter.new(params).apply(scope) + end + + def reminder_attrs + @reminder_attrs ||= flash[:payment_reminder] + end + + def payment_attrs + @payment_attrs ||= flash[:payment] || { amount: entry.amount_open } + end + + def pdf_options + { + articles: params[:articles] != 'false', + esr: params[:esr] != 'false' + } + end + + def find_and_remember_label_format + LabelFormat.find(params[:label_format_id]).tap do |label_format| + unless current_user.last_label_format_id == label_format.id + current_user.update_column(:last_label_format_id, label_format.id) + end + end + end + + def authorize_class + authorize!(:index_invoices, parent) + end + +end diff --git a/app/controllers/label_format/settings_controller.rb b/app/controllers/label_format/settings_controller.rb new file mode 100644 index 0000000000..ffa0cdc3ac --- /dev/null +++ b/app/controllers/label_format/settings_controller.rb @@ -0,0 +1,23 @@ +# encoding: utf-8 + +# Copyright (c) 2017 Pfadibewegung Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +class LabelFormat::SettingsController < ApplicationController + + before_action :authorize + + def update + current_user.update_column(:show_global_label_formats, + params[:show_global_label_formats].present?) + end + + private + + def authorize + authorize!(:update_settings, current_user) + end + +end diff --git a/app/controllers/label_formats_controller.rb b/app/controllers/label_formats_controller.rb index e1c278aca9..b470a36e1f 100644 --- a/app/controllers/label_formats_controller.rb +++ b/app/controllers/label_formats_controller.rb @@ -1,6 +1,6 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. @@ -8,15 +8,35 @@ class LabelFormatsController < SimpleCrudController self.permitted_attrs = [:name, :page_size, :landscape, :font_size, :width, :height, - :padding_top, :padding_left, :count_horizontal, :count_vertical] + :padding_top, :padding_left, :count_horizontal, :count_vertical, + :nickname, :pp_post] self.sort_mappings = { name: 'label_format_translations.name', dimensions: %w(count_horizontal count_vertical) } + before_render_index :global_entries + private + def build_entry + super.tap do |entry| + entry.person_id = current_user.id unless manage_global? + end + end + + def manage_global? + params[:global] == 'true' && can?(:manage_global, LabelFormat) + end + def list_entries - super.list + super.list.where(person_id: current_user.id) + end + + def global_entries + @global_entries = LabelFormat.list.where(person_id: nil) + if sorting? + @global_entries = @global_entries.reorder(sort_expression) + end end end diff --git a/app/controllers/notes_controller.rb b/app/controllers/notes_controller.rb new file mode 100644 index 0000000000..04ec29fdfc --- /dev/null +++ b/app/controllers/notes_controller.rb @@ -0,0 +1,77 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Dachverband Schweizer Jugendparlamente. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +class NotesController < ApplicationController + + decorates :group, :person + + def index + authorize!(:index_notes, group) + @notes = entries + end + + def create + @note = subject.notes.build(permitted_params.merge(author_id: current_user.id)) + authorize!(:create, @note) + @note.save + + respond_to do |format| + format.html { redirect_to subject_path } + format.js { group } # create.js.haml + end + end + + def destroy + @note = Note.find(params[:id]) + authorize!(:destroy, @note) + @note.destroy + + respond_to do |format| + format.html { redirect_to subject_path } + format.js # destroy.js.haml + end + end + + private + + def entries + Note + .includes(:subject, :author) + .in_or_layer_below(group) + .list + .page(params[:notes_page]) + .tap do |notes| + Person::PreloadGroups.for(notes.collect(&:subject).select { |s| s.is_a?(Person) }) + Person::PreloadGroups.for(notes.collect(&:author)) + end + end + + def group + @group ||= Group.find(params[:group_id]) + end + + def subject + if params[:person_id] + Person.find(params[:person_id]) + else + group + end + end + + def permitted_params + params.require(:note).permit(:text) + end + + def subject_path + if @note.subject_type == Group.name + group_path(id: group.id) + else + group_person_path(group_id: group.id, id: subject.id) + end + end + +end diff --git a/app/controllers/payment_reminders_controller.rb b/app/controllers/payment_reminders_controller.rb new file mode 100644 index 0000000000..064fb2f8ad --- /dev/null +++ b/app/controllers/payment_reminders_controller.rb @@ -0,0 +1,23 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +class PaymentRemindersController < CrudController + self.nesting = [Group, Invoice] + self.permitted_attrs = [:message, :due_at] + + def create + assign_attributes + + if entry.save + redirect_to(group_invoice_path(*parents), notice: flash_message(:success)) + else + flash[:payment_reminder] = permitted_params.to_h + redirect_to(group_invoice_path(*parents)) + end + end + +end diff --git a/app/controllers/payments_controller.rb b/app/controllers/payments_controller.rb new file mode 100644 index 0000000000..c478b9bb52 --- /dev/null +++ b/app/controllers/payments_controller.rb @@ -0,0 +1,30 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +class PaymentsController < CrudController + include FormatHelper + include ActionView::Helpers::NumberHelper + + self.nesting = [Group, Invoice] + self.permitted_attrs = [:amount, :received_at] + + def create + assign_attributes + + if entry.save + redirect_to(group_invoice_path(*parents), notice: flash_message) + else + flash[:payment] = permitted_params.to_h + redirect_to(group_invoice_path(*parents)) + end + end + + def flash_message + I18n.t("#{controller_name}.#{action_name}.flash.success", amount: f(entry.amount)) + end + +end diff --git a/app/controllers/people_controller.rb b/app/controllers/people_controller.rb old mode 100644 new mode 100755 index 57d9ff0db1..1212fb9c99 --- a/app/controllers/people_controller.rb +++ b/app/controllers/people_controller.rb @@ -1,6 +1,6 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. @@ -11,7 +11,7 @@ class PeopleController < CrudController self.nesting = Group - self.remember_params += [:name, :kind, :role_type_ids] + self.remember_params += [:name, :range, :filters, :filter_id] self.permitted_attrs = [:first_name, :last_name, :company_name, :nickname, :company, :gender, :birthday, :additional_information, @@ -38,11 +38,13 @@ class PeopleController < CrudController before_render_show :load_grouped_person_tags, if: -> { html_request? } before_render_index :load_people_add_requests, if: -> { html_request? } - def index + def index # rubocop:disable Metrics/AbcSize we support a lot of format, hence many code-branches respond_to do |format| format.html { @people = prepare_entries(filter_entries).page(params[:page]) } format.pdf { render_pdf(filter_entries) } - format.csv { render_entries_csv(filter_entries) } + format.csv { render_tabular_entries_in_background(:csv) && redirect_to(action: :index) } + format.xlsx { render_tabular_entries_in_background(:xlsx) && redirect_to(action: :index) } + format.vcf { render_vcf(filter_entries.includes(:phone_numbers)) } format.email { render_emails(filter_entries) } format.json { render_entries_json(filter_entries) } end @@ -52,7 +54,9 @@ def show respond_to do |format| format.html format.pdf { render_pdf([entry]) } - format.csv { render_entry_csv } + format.csv { render_tabular_entry(:csv) } + format.xlsx { render_tabular_entry(:xlsx) } + format.vcf { render_vcf([entry]) } format.json { render_entry_json } end end @@ -72,14 +76,14 @@ def send_password_instructions # PUT button, ajax def primary_group - entry.update_column :primary_group_id, params[:primary_group_id] + entry.update!(primary_group_id: params[:primary_group_id]) respond_to do |format| format.html { redirect_to group_person_path(group, entry) } format.js end end - private + private_class_method # dont use class level accessor as expression is evaluated whenever constant is # loaded which might be before wagon that defines groups / roles has been loaded @@ -88,8 +92,9 @@ def self.sort_mappings_with_indifferent_access concat(Person.order_by_name_statement) }.with_indifferent_access end + private - alias_method :group, :parent + alias group parent def find_entry if group && group.root? @@ -110,7 +115,7 @@ def assign_attributes end def load_people_add_requests - if params[:kind].blank? && can?(:create, @group.roles.new) + if params[:range].blank? && can?(:create, @group.roles.new) @person_add_requests = @group.person_add_requests.list.includes(person: :primary_group) end end @@ -143,19 +148,18 @@ def set_add_request_status_notification end def filter_entries - filter = list_filter - entries = filter.filter_entries + @person_filter = Person::Filter::List.new(@group, current_user, list_filter_args) + entries = @person_filter.entries entries = entries.reorder(sort_expression) if sorting? - @multiple_groups = filter.multiple_groups - @all_count = filter.all_count if html_request? entries end - def list_filter - if params[:filter] == 'qualification' && index_full_ability? - Person::QualificationFilter.new(@group, current_user, params) + def list_filter_args + if params[:filter_id] + filter = PeopleFilter.for_group(group).find(params[:filter_id]) + { name: filter.name, range: filter.range, filters: filter.filter_chain.to_params } else - Person::RoleFilter.new(@group, current_user, params) + params end end @@ -167,29 +171,41 @@ def prepare_entries(entries) end end - def render_entries_csv(entries) - full = params[:details].present? && index_full_ability? - render_csv(prepare_csv_entries(entries, full), full) + def render_tabular_entries_in_background(format) + email = current_person.email + if email + full = params[:details].present? && index_full_ability? + render_tabular_in_background(format, full) + flash[:notice] = translate(:export_enqueued, email: email) + else + flash[:alert] = translate(:export_email_needed) + end end - def prepare_csv_entries(entries, full) + def prepare_tabular_entries(entries, full) if full - entries.select('people.*').preload_accounts.includes(relations_to_tails: :tail) + entries + .select('people.*') + .preload_accounts + .includes(relations_to_tails: :tail, qualifications: { qualification_kind: :translations }) + .includes(:primary_group) else - entries.preload_public_accounts + entries.preload_public_accounts.includes(:primary_group) end end - def render_entry_csv - render_csv([entry], params[:details].present? && can?(:show_full, entry)) + def render_tabular_entry(format) + render_tabular(format, [entry], params[:details].present? && can?(:show_full, entry)) end - def render_csv(entries, full) - if full - send_data Export::Csv::People::PeopleFull.export(entries), type: :csv - else - send_data Export::Csv::People::PeopleAddress.export(entries), type: :csv - end + def render_tabular_in_background(format, full) + person_filter = Person::Filter::List.new(@group, current_user, list_filter_args) + Export::PeopleExportJob.new(format, full, current_person.id, person_filter).enqueue! + end + + def render_tabular(format, entries, full) + exporter = full ? Export::Tabular::People::PeopleFull : Export::Tabular::People::PeopleAddress + send_data exporter.export(format, entries), type: format end def render_entries_json(entries) @@ -197,7 +213,7 @@ def render_entries_json(entries) includes(:social_accounts). decorate, group: @group, - multiple_groups: @multiple_groups, + multiple_groups: @person_filter.multiple_groups, serializer: PeopleSerializer, controller: self) end @@ -207,7 +223,7 @@ def render_entry_json end def index_full_ability? - if params[:kind].blank? + if params[:range].blank? || params[:range] == 'group' can?(:index_full_people, @group) else can?(:index_deep_full_people, @group) diff --git a/app/controllers/people_filters_controller.rb b/app/controllers/people_filters_controller.rb index 7bd0f3d4d5..6793da8683 100644 --- a/app/controllers/people_filters_controller.rb +++ b/app/controllers/people_filters_controller.rb @@ -1,6 +1,6 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. @@ -9,13 +9,11 @@ class PeopleFiltersController < CrudController self.nesting = Group - self.permitted_attrs = [:name, :role_type_ids, role_types: [], role_type_ids: []] - decorates :group - hide_action :index, :show, :edit, :update + hide_action :index, :show - skip_authorize_resource only: [:create, :qualification] + skip_authorize_resource only: [:create] # load group before authorization prepend_before_action :parent @@ -24,12 +22,18 @@ class PeopleFiltersController < CrudController helper_method :people_list_path + def new + assign_attributes + super + end + def create if params[:button] == 'save' authorize!(:create, entry) - super(location: result_path) + super else authorize!(:new, entry) + assign_attributes redirect_to result_path end end @@ -38,14 +42,9 @@ def destroy super(location: people_list_path) end - def qualification - authorize!(:index_full_people, group) - @qualification_kinds = QualificationKind.list.without_deleted - end - private - alias_method :group, :parent + alias group parent def build_entry filter = super @@ -53,21 +52,31 @@ def build_entry filter end + def return_path + super || people_list_path(filter_id: entry.id) + end + def result_path - assign_attributes - params = {} - if entry.role_types.present? - params = { name: entry.name, role_type_ids: entry.role_type_ids_string, kind: :deep } + search_params = {} + if entry.filter_chain.present? + search_params = { + name: entry.name, + range: entry.range || 'deep', + filters: entry.filter_chain.to_params + } end - people_list_path(params) + people_list_path(search_params) end def compose_role_lists @role_types = Role::TypeList.new(group.class) + @qualification_kinds = QualificationKind.list.without_deleted end - def permitted_params - model_params ? model_params.permit(permitted_attrs) : {} + def assign_attributes + entry.name = params[:name] || (params[:people_filter] && params[:people_filter][:name]) + entry.range = params[:range] + entry.filter_chain = params[:filters] end def people_list_path(options = {}) diff --git a/app/controllers/person/colleagues_controller.rb b/app/controllers/person/colleagues_controller.rb new file mode 100644 index 0000000000..f00a4967e8 --- /dev/null +++ b/app/controllers/person/colleagues_controller.rb @@ -0,0 +1,58 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Dachverband Schweizer Jugendparlamente. This file is +# part of hitobito and licensed under the Affero General Public License +# version 3 or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +class Person::ColleaguesController < ApplicationController + + before_action :authorize_action + + decorates :group, :person, :colleagues + + respond_to :html + + def index + @colleagues = list_entries + respond_with(@colleagues) + end + + private + + def list_entries + return Person.none.page(1) unless person.company_name? + + Person. + where(company_name: person.company_name). + preload_public_accounts. + preload_groups. + joins(:roles). + order_by_name. + distinct. + page(params[:page]) + end + + def person + @person ||= group.people.find(params[:id]) + end + + def group + @group ||= Group.find(params[:group_id]) + end + + def authorize_action + authorize!(:show, person) + end + + def model_class + Person + end + + include Sortable + + self.sort_mappings = { + roles: [Person.order_by_role_statement].concat(Person.order_by_name_statement) + } + +end diff --git a/app/controllers/person/company_name_controller.rb b/app/controllers/person/company_name_controller.rb new file mode 100644 index 0000000000..e964b174ff --- /dev/null +++ b/app/controllers/person/company_name_controller.rb @@ -0,0 +1,55 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Dachverband Schweizer Jugendparlamente. This file is +# part of hitobito and licensed under the Affero General Public License +# version 3 or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +class Person::CompanyNameController < ApplicationController + + before_action :authorize_action + + delegate :model_class, to: :class + + # GET ajax, for auto complete fields, only the company_name + def index + render json: entries.map { |name| { id: name, label: name } } + end + + private + + def entries + if params.key?(:q) && params[:q].size >= 3 + list_entries. + limit(10). + pluck(:company_name). + map(&:strip) + else + [] + end + end + + def list_entries + Person. + where.not(company_name: nil). + distinct. + order(:company_name) + end + + def authorize_action + authorize!(:query, Person) + end + + include Searchable + + self.search_columns = [:company_name] + + class << self + + def model_class + Person + end + + end + +end diff --git a/app/controllers/person/csv_imports_controller.rb b/app/controllers/person/csv_imports_controller.rb index 9beea40fb4..f7182b98f4 100644 --- a/app/controllers/person/csv_imports_controller.rb +++ b/app/controllers/person/csv_imports_controller.rb @@ -133,7 +133,8 @@ def load_can_manage_tags def valid_file?(io) io.present? && io.respond_to?(:content_type) && - io.content_type =~ /text\/|excel/ # windows sends csv files as application/vnd.excel + # windows sends csv files as application/vnd.excel, windows 10 as application/octet-stream + io.content_type =~ /text\/|excel|octet-stream/ end def parse_or_redirect @@ -180,8 +181,7 @@ def map_headers_and_import end def redirect_params - filter = PeopleFilter.new(role_type_ids: [role_type.id]) - { role_type_ids: filter.role_type_ids_string, name: importer.human_role_name } + { filters: { role: { role_type_ids: [role_type.id] } }, name: importer.human_role_name } end def role_type diff --git a/app/controllers/person/history_controller.rb b/app/controllers/person/history_controller.rb index 87285e39e5..57a4080106 100644 --- a/app/controllers/person/history_controller.rb +++ b/app/controllers/person/history_controller.rb @@ -19,10 +19,10 @@ def index private def fetch_roles - Role.with_deleted. - where(person_id: entry.id). - includes(:group). - order('groups.name', 'roles.deleted_at') + Person::PreloadGroups.for([entry]).first.roles. + with_deleted. + includes(:group). + sort_by {|r| GroupDecorator.new(r.group).name_with_layer } end def fetch_participations diff --git a/app/controllers/person/invoices_controller.rb b/app/controllers/person/invoices_controller.rb new file mode 100644 index 0000000000..b256e81e26 --- /dev/null +++ b/app/controllers/person/invoices_controller.rb @@ -0,0 +1,33 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +class Person::InvoicesController < InvoicesController + + private + + def list_entries + scope = Invoice. + includes(:group, recipient: [:groups, :roles]). + joins(:recipient).where(recipient: person).list + + scope = scope.page(params[:page]).per(50) + Invoice::Filter.new(params).apply(scope) + end + + def person + @person ||= group.people.find(params[:id]) + end + + def group + @group ||= Group.find(params[:group_id]) + end + + def authorize_class + authorize!(:index_invoices, person) + end + +end diff --git a/app/controllers/person/notes_controller.rb b/app/controllers/person/notes_controller.rb deleted file mode 100644 index 3e4bc80656..0000000000 --- a/app/controllers/person/notes_controller.rb +++ /dev/null @@ -1,51 +0,0 @@ -# encoding: utf-8 - -# Copyright (c) 2012-2016, Dachverband Schweizer Jugendparlamente. This file is part of -# hitobito_dsj and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito_dsj. - -class Person::NotesController < ApplicationController - - class_attribute :permitted_attrs - - authorize_resource except: :index - - decorates :group, :person - - respond_to :html - - self.permitted_attrs = [:text] - - def index - @group = Group.find(params[:id]) - authorize!(:index_person_notes, @group) - - @notes = Person::Note. - includes(:author, person: :groups). - where(person: Person.in_layer(@group)). - where(person: Person.in_or_below(@group)). - page(params[:notes_page]). - per(100) - - respond_with(@notes) - end - - def create - @group = Group.find(params[:group_id]) - @person = Person.find(params[:person_id]) - @note = @person.notes.create(permitted_params.merge(author_id: current_user.id)) - - respond_to do |format| - format.html { redirect_to group_person_path(@group, @person) } - format.js # create.js.haml - end - end - - private - - def permitted_params - params.require(:person_note).permit(permitted_attrs) - end - -end diff --git a/app/controllers/person/query_controller.rb b/app/controllers/person/query_controller.rb index fd5fac986d..3dcc896556 100644 --- a/app/controllers/person/query_controller.rb +++ b/app/controllers/person/query_controller.rb @@ -9,6 +9,8 @@ class Person::QueryController < ApplicationController before_action :authorize_action + delegate :model_class, to: :class + # GET ajax, for auto complete fields, without @group def index people = [] @@ -26,10 +28,6 @@ def list_entries Person.only_public_data.order_by_name end - def model_class - Person - end - def authorize_action authorize!(:query, Person) end @@ -38,4 +36,12 @@ def authorize_action self.search_columns = [:first_name, :last_name, :company_name, :nickname, :town] + class << self + + def model_class + Person + end + + end + end diff --git a/app/controllers/person/tags_controller.rb b/app/controllers/person/tags_controller.rb index 47119c905a..4d641fd1ae 100644 --- a/app/controllers/person/tags_controller.rb +++ b/app/controllers/person/tags_controller.rb @@ -1,9 +1,9 @@ # encoding: utf-8 # Copyright (c) 2012-2016, Dachverband Schweizer Jugendparlamente. This file is part of -# hitobito_dsj and licensed under the Affero General Public License version 3 +# hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito_dsj. +# https://github.com/hitobito/hitobito. class Person::TagsController < ApplicationController diff --git a/app/controllers/roles_controller.rb b/app/controllers/roles_controller.rb index 16fe9806b1..783f77732c 100644 --- a/app/controllers/roles_controller.rb +++ b/app/controllers/roles_controller.rb @@ -1,6 +1,6 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. @@ -18,8 +18,12 @@ class RolesController < CrudController skip_authorize_resource only: [:details, :role_types] + define_render_callbacks :create + before_render_form :set_group_selection + before_render_create :set_group_selection + before_action :set_person_id, only: [:new] before_action :remember_primary_group, only: [:destroy] after_destroy :last_primary_group_role_deleted @@ -77,7 +81,7 @@ def create_entry_and_person created = with_callbacks(:create, :save) do (entry.person.persisted? || entry.person.save) && entry.save end - fail ActiveRecord::Rollback unless created + raise ActiveRecord::Rollback unless created end created end @@ -164,8 +168,8 @@ def build_person(role) role.person_id = person_id role.person = Person.new unless role.person else - attrs = ActionController::Parameters.new(person_attrs). - permit(*PeopleController.permitted_attrs) + attrs = ActionController::Parameters.new(person_attrs) + .permit(*PeopleController.permitted_attrs) role.person = Person.new(attrs) end end @@ -241,4 +245,8 @@ def belongs_to_persons_primary_group?(role) role.group_id == role.person.primary_group_id end + def set_person_id + @person_id = Role.with_deleted.find(params[:role_id]).person_id if params[:role_id] + end + end diff --git a/app/controllers/subscriptions_controller.rb b/app/controllers/subscriptions_controller.rb index 8926f4d0bf..efd7932eff 100644 --- a/app/controllers/subscriptions_controller.rb +++ b/app/controllers/subscriptions_controller.rb @@ -1,6 +1,6 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. @@ -15,17 +15,18 @@ class SubscriptionsController < CrudController prepend_before_action :parent - alias_method :mailing_list, :parent + alias mailing_list parent - - def index + def index # rubocop:disable Metrics/MethodLength there are a lof of formats supported respond_to do |format| format.html do @person_add_requests = fetch_person_add_requests load_grouped_subscriptions end format.pdf { render_pdf(ordered_people) } - format.csv { render_csv(ordered_people) } + format.csv { render_tabular_in_background(:csv) && redirect_to(action: :index) } + format.xlsx { render_tabular_in_background(:xlsx) && redirect_to(action: :index) } + format.vcf { render_vcf(ordered_people) } format.email { render_emails(ordered_people) } end end @@ -43,9 +44,18 @@ def ordered_people mailing_list.people.order_by_name end - def render_csv(people) - csv = Export::Csv::People::PeopleAddress.export(people) - send_data csv, type: :csv + def render_tabular_in_background(format) + Export::SubscriptionsJob.new(format, mailing_list.id, current_person.id).enqueue! + flash[:notice] = translate(:export_enqueued, email: current_person.email) + end + + def render_tabular(format, people) + data = Export::Tabular::People::PeopleAddress.export(format, prepare_tabular_entries(people)) + send_data data, type: format + end + + def prepare_tabular_entries(people) + people.preload_public_accounts.includes(roles: :group) end def group_subscriptions diff --git a/app/decorators/application_decorator.rb b/app/decorators/application_decorator.rb index dafdf954fb..6ce8ca2eae 100644 --- a/app/decorators/application_decorator.rb +++ b/app/decorators/application_decorator.rb @@ -19,7 +19,7 @@ def klass end def used_attributes(*attributes) - attributes.select { |name| klass.attr_used?(name) }.map(&:to_s) + attributes.select { |name| model.used_attributes.include?(name) }.map(&:to_s) end def used?(attribute) diff --git a/app/decorators/contactable_decorator.rb b/app/decorators/contactable_decorator.rb index 847ae6b2fc..3b4f663717 100644 --- a/app/decorators/contactable_decorator.rb +++ b/app/decorators/contactable_decorator.rb @@ -52,7 +52,9 @@ def all_additional_emails(only_public = true) end def all_phone_numbers(only_public = true) - nested_values(phone_numbers, only_public) + nested_values(phone_numbers, only_public) do |number| + h.link_to(number,"tel:#{number}") + end end def all_social_accounts(only_public = true) diff --git a/app/decorators/event/participation_decorator.rb b/app/decorators/event/participation_decorator.rb index b0efda172d..49cb18528f 100644 --- a/app/decorators/event/participation_decorator.rb +++ b/app/decorators/event/participation_decorator.rb @@ -13,15 +13,15 @@ class Event::ParticipationDecorator < ApplicationDecorator decorates_association :application delegate :to_s, :email, :primary_email, :all_emails, :all_additional_emails, - :all_phone_numbers, :all_social_accounts, :complete_address, :town, to: :person - delegate :qualified?, to: :qualifier + :all_phone_numbers, :all_social_accounts, :complete_address, :town, :layer_group_label, + :layer_group, to: :person def person_additional_information h.tag(:br) + h.muted(person.additional_name) + incomplete_label end def person_location_information - [person.town, originating_group].reject(&:blank?).join(', ') + [layer_group, town_info].reject(&:blank?).join(' ') end def incomplete_label @@ -37,46 +37,12 @@ def roles_short end end - def issue_action(group) - if qualified.nil? || !qualified? - qualify_action_link(group, :put, :ok) - else - h.icon(:ok) - end - end - - def revoke_action(group) - if qualified.nil? || qualified? - qualify_action_link(group, :delete, :remove) - else - h.icon(:remove) - end - end - - def qualify_action_link(group, method, icon) - h.link_to(h.group_event_qualification_path(group, event_id, model), - method: method, remote: true, title: tooltips[icon]) do - h.content_tag(:i, '', class: "icon icon-#{icon} disabled") - end - end - - def qualifier - Event::Qualifier.for(model) - end - def list_roles safe_join(roles, h.tag(:br)) { |role| role.to_s } end - def originating_group - person.primary_group - end - - def tooltips - @tooltips ||= { - ok: translate('tooltips.ok'), - remove: translate('tooltips.remove') - } + def town_info + "(#{h.t('.town')}: #{person.town})" if person.town end end diff --git a/app/decorators/event_decorator.rb b/app/decorators/event_decorator.rb index 5c083ff8dc..c929557828 100644 --- a/app/decorators/event_decorator.rb +++ b/app/decorators/event_decorator.rb @@ -110,6 +110,16 @@ def as_typeahead { id: id, label: "#{model} (#{groups_label})" } end + def as_quicksearch + { id: id, label: label_with_group, type: :event } + end + + def label_with_group + label = to_s + label += " (#{number})" if number? + h.safe_join([groups.first.to_s, label], ': ') + end + private def translate_issued_qualifications_info(qualis, prolongs, variables) diff --git a/app/decorators/group_decorator.rb b/app/decorators/group_decorator.rb index 291852dbb0..e233723482 100644 --- a/app/decorators/group_decorator.rb +++ b/app/decorators/group_decorator.rb @@ -44,6 +44,12 @@ def link_with_layer h.safe_join(links, ' / ') end + # compute layers and concat group names using a '/' + def name_with_layer + group_names = with_layer.map { |g| g.to_s } + group_names.join(' / ') + end + def possible_events klass.event_types end diff --git a/app/decorators/invoice_decorator.rb b/app/decorators/invoice_decorator.rb new file mode 100644 index 0000000000..982baa35e4 --- /dev/null +++ b/app/decorators/invoice_decorator.rb @@ -0,0 +1,38 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + + +class InvoiceDecorator < ApplicationDecorator + decorates :invoice + + def cost + format_currency(calculated[:cost]) + end + + def vat + format_currency(calculated[:vat]) + end + + def total + format_currency(model.total || calculated[:total]) + end + + def amount_open + format_currency(model.amount_open) + end + + def amount_paid + format_currency(model.amount_paid) + end + + private + + def format_currency(amount) + h.number_to_currency(amount, format: '%n %u') + end + +end diff --git a/app/decorators/invoice_item_decorator.rb b/app/decorators/invoice_item_decorator.rb new file mode 100644 index 0000000000..9dc3a776ba --- /dev/null +++ b/app/decorators/invoice_item_decorator.rb @@ -0,0 +1,29 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + + +class InvoiceItemDecorator < ApplicationDecorator + decorates :invoice_item + + def cost + h.number_to_currency(model.cost, format: '%n %u') + end + + def unit_cost + h.number_to_currency(model.unit_cost, format: '%n %u') + end + + def vat_rate + h.number_to_percentage(model.vat_rate || 0) + end + + def total + h.number_to_currency(model.total, format: '%n %u') + end + +end + diff --git a/app/decorators/person_decorator.rb b/app/decorators/person_decorator.rb index 7442d4ff33..a8a718463c 100644 --- a/app/decorators/person_decorator.rb +++ b/app/decorators/person_decorator.rb @@ -55,6 +55,11 @@ def picture_full_url end end + def layer_group_label + group = person.layer_group + h.link_to(group, h.group_path(group)) if group + end + # render a list of all roles # if a group is given, only render the roles of this group def roles_short(group = nil) @@ -92,6 +97,19 @@ def relations @relations ||= relations_to_tails.list.includes(tail: [:groups, :roles]) end + def last_role_new_link(group) + path = h.new_group_role_path(restored_group(group), role_id: last_role.id) + role_popover_link(path, "role_#{last_role.id}") + end + + def last_role + @last_role ||= last_non_restricted_role + end + + def restored_group(default_group) + last_role.group.deleted_at? ? default_group : last_role.group + end + private def event_queries @@ -138,12 +156,16 @@ def function_short(function, scope = nil) end def popover_edit_link(function) - content_tag(:span, style: 'padding-left: 10px') do + path = h.edit_group_role_path(function.group, function) + role_popover_link(path) + end + + def role_popover_link(path, html_id = nil) + content_tag(:span, style: 'padding-left: 10px', id: html_id) do h.link_to(h.icon(:edit), - h.edit_group_role_path(function.group, function), + path, title: h.t('global.link.edit'), remote: true) end end - end diff --git a/app/domain/event/participant_assigner.rb b/app/domain/event/participant_assigner.rb index 29aaa8fdee..a3526c3574 100644 --- a/app/domain/event/participant_assigner.rb +++ b/app/domain/event/participant_assigner.rb @@ -19,7 +19,7 @@ def initialize(event, participation, user = nil) def createable? participation.event.id == event.id || - !(participating?(event) || participating?(participation.event)) + !(applied?(event) || participating?(participation.event)) end def add_participant @@ -52,12 +52,20 @@ def remove_participant private def participating?(event) - event.participations. - active. - joins(:roles). - where(event_roles: { type: event.participant_types.map(&:sti_name) }). - where(person_id: participation.person_id). - exists? + event.participations + .active # only active/assigned participations are relevant + .joins(:roles) + .where(event_roles: { type: event.participant_types.map(&:sti_name) }) + .where(person_id: participation.person_id) + .exists? + end + + def applied?(event) + event.participations + .joins(:roles) + .where(event_roles: { type: event.participant_types.map(&:sti_name) }) + .where(person_id: participation.person_id) + .exists? end def create_participant_role diff --git a/app/domain/event/participation_filter.rb b/app/domain/event/participation_filter.rb index 40a8c145c8..8e3d541717 100644 --- a/app/domain/event/participation_filter.rb +++ b/app/domain/event/participation_filter.rb @@ -12,7 +12,9 @@ class Event::ParticipationFilter class_attribute :load_entries_includes self.load_entries_includes = [:roles, :event, answers: [:question], - person: [:additional_emails, :phone_numbers]] + person: [:additional_emails, :phone_numbers, + :primary_group] + ] attr_reader :event, :user, :params, :counts @@ -35,7 +37,7 @@ def predefined_filters private def apply_default_sort(records) - records = records.order_by_role(event.class) if Settings.people.default_sort == 'role' + records = records.order_by_role(event) if Settings.people.default_sort == 'role' records.merge(Person.order_by_name) end diff --git a/app/domain/event/precondition_checker.rb b/app/domain/event/precondition_checker.rb index d6bcb3510c..2350bc12a0 100644 --- a/app/domain/event/precondition_checker.rb +++ b/app/domain/event/precondition_checker.rb @@ -1,20 +1,22 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. -class Event::PreconditionChecker < Struct.new(:course, :person) +class Event::PreconditionChecker + extend Forwardable include Translatable def_delegator 'course.kind', :minimum_age, :course_minimum_age def_delegator 'errors', :empty?, :valid? - attr_reader :errors + attr_reader :course, :person, :errors - def initialize(*args) - super + def initialize(course, person) + @course = course + @person = person @errors = [] validate end @@ -22,11 +24,7 @@ def initialize(*args) def validate validate_minimum_age if course_minimum_age - course_preconditions.each do |qualification_kind| - unless reactivateable?(qualification_kind) - errors << qualification_kind.label - end - end + validate_qualifications end def errors_text @@ -34,6 +32,7 @@ def errors_text if errors.present? text << translate(:preconditions_not_fulfilled) text << birthday_error_text if errors.delete(:birthday) + text << some_qualifications_error_text if errors.delete(:some_qualifications) text << qualifications_error_text if errors.present? end text @@ -45,6 +44,33 @@ def validate_minimum_age errors << :birthday unless person.birthday && old_enough? end + def validate_qualifications + grouped_ids = course.kind.grouped_qualification_kind_ids('precondition', 'participant') + if grouped_ids.size == 1 + validate_simple_qualifications(grouped_ids.first) + elsif grouped_ids.size > 1 + validate_grouped_qualifications(grouped_ids) + end + end + + def validate_simple_qualifications(precondition_ids) + precondition_ids.each do |id| + errors << id unless reactivateable?(id) + end + end + + def validate_grouped_qualifications(grouped_ids) + unless any_grouped_qualifications?(grouped_ids) + errors << :some_qualifications + end + end + + def any_grouped_qualifications?(grouped_ids) + grouped_ids.any? do |ids| + ids.all? { |id| reactivateable?(id) } + end + end + def person_qualifications @person_qualifications ||= person.qualifications.where(qualification_kind_id: course_preconditions.map(&:id)) @@ -54,9 +80,9 @@ def course_preconditions course.kind.qualification_kinds('precondition', 'participant') end - def reactivateable?(qualification_kind) + def reactivateable?(qualification_kind_id) person_qualifications. - select { |q| q.qualification_kind_id == qualification_kind.id }. + select { |q| q.qualification_kind_id == qualification_kind_id }. any? { |qualification| qualification.reactivateable?(course.start_date) } end @@ -68,8 +94,13 @@ def birthday_error_text translate(:below_minimum_age, course_minimum_age: course_minimum_age) end + def some_qualifications_error_text + translate(:some_qualifications_missing) + end + def qualifications_error_text - translate(:qualifications_missing, missing: errors.join(', ')) + kinds = QualificationKind.includes(:translations).find(errors) + translate(:qualifications_missing, missing: kinds.collect(&:label).join(', ')) end end diff --git a/app/domain/event/qualifier.rb b/app/domain/event/qualifier.rb index 0f3945c3d4..a5f5c0f32b 100644 --- a/app/domain/event/qualifier.rb +++ b/app/domain/event/qualifier.rb @@ -25,7 +25,7 @@ def leader?(participation) attr_reader :created, :prolonged, :participation, :role - delegate :qualified?, :person, :event, to: :participation + delegate :person, :event, to: :participation delegate :qualification_date, to: :event def initialize(participation, role) diff --git a/app/domain/export/base.rb b/app/domain/export/base.rb deleted file mode 100644 index 5c1a62e60b..0000000000 --- a/app/domain/export/base.rb +++ /dev/null @@ -1,61 +0,0 @@ -# encoding: utf-8 - -# Copyright (c) 2012-2016, insieme Schweiz. This file is part of -# hitobito_insieme and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito_insieme. -# - -module Export - # Base class for csv/xlsx export - class Base - - attr_reader :list - - def initialize(list) - @list = list - end - - # The list of all attributes exported to the csv/xlsx. - # overridde either this or #attribute_labels - def attributes - attribute_labels.keys - end - - # A hash of all attributes mapped to their labels exported to the csv/xlsx. - # overridde either this or #attributes - def attribute_labels - @attribute_labels ||= build_attribute_labels - end - - # List of all lables. - def labels - attribute_labels.values - end - - private - - def build_attribute_labels - attributes.each_with_object({}) do |attr, labels| - labels[attr] = attribute_label(attr) - end - end - - def attribute_label(attr) - human_attribute(attr) - end - - def human_attribute(attr) - model_class.human_attribute_name(attr) - end - - def values(entry) - row = row_for(entry) - attributes.collect { |attr| row.fetch(attr) } - end - - def row_for(entry) - row_class.new(entry) - end - end -end diff --git a/app/domain/export/csv/base.rb b/app/domain/export/csv/base.rb deleted file mode 100644 index 4a70fa4c47..0000000000 --- a/app/domain/export/csv/base.rb +++ /dev/null @@ -1,29 +0,0 @@ -# encoding: utf-8 - -# Copyright (c) 2012-2014, Jungwacht Blauring Schweiz, Pfadibewegung Schweiz. -# This file is part of hitobito and licensed under the Affero General Public -# License version 3 or later. See the COPYING file at the top-level directory -# or at https://github.com/hitobito/hitobito. - - -module Export::Csv - # The base class for all the different csv export files. - class Base < ::Export::Base - - class_attribute :model_class, :row_class - self.row_class = Row - - class << self - def export(*args) - Export::Csv::Generator.new(new(*args)).csv - end - end - - def to_csv(generator) - generator << labels - list.each do |entry| - generator << values(entry) - end - end - end -end diff --git a/app/domain/export/csv/generator.rb b/app/domain/export/csv/generator.rb index 600220501f..ba02498130 100644 --- a/app/domain/export/csv/generator.rb +++ b/app/domain/export/csv/generator.rb @@ -1,30 +1,39 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. require 'csv' -module Export +module Export module Csv def self.export(exportable) - Generator.new(exportable).csv + Generator.new(exportable).call end class Generator - attr_reader :csv + attr_reader :exportable def initialize(exportable) - @csv = convert(generate(exportable)) + @exportable = exportable + end + + def call + convert(generate) end private - def generate(exportable) - CSV.generate(options) { |generator| exportable.to_csv(generator) } + def generate + CSV.generate(options) do |generator| + generator << exportable.labels + exportable.data_rows(:csv) do |row| + generator << row + end + end end # convert to 8859 for excel which is too stupid to handle utf-8 @@ -39,6 +48,7 @@ def convert(data) def options { col_sep: Settings.csv.separator.strip } end + end end end diff --git a/app/domain/export/csv/people/person_row.rb b/app/domain/export/csv/people/person_row.rb deleted file mode 100644 index 285cb04cf4..0000000000 --- a/app/domain/export/csv/people/person_row.rb +++ /dev/null @@ -1,60 +0,0 @@ -# encoding: utf-8 - -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of -# hitobito and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito. - -module Export::Csv::People - # Attributes of a person, handles associations - class PersonRow < Export::Csv::Row - - self.dynamic_attributes = { /^phone_number_/ => :phone_number_attribute, - /^social_account_/ => :social_account_attribute, - /^additional_email_/ => :additional_email_attribute, - /^people_relation_/ => :people_relation_attribute } - - def country - entry.country_label - end - - def gender - entry.gender_label - end - - def roles - entry.roles.map { |role| "#{role} #{role.group.with_layer.join(' / ')}" }.join(', ') - end - - def tags - entry.tag_list.to_s - end - - private - - def phone_number_attribute(attr) - contact_account_attribute(entry.phone_numbers, attr) - end - - def social_account_attribute(attr) - contact_account_attribute(entry.social_accounts, attr) - end - - def additional_email_attribute(attr) - contact_account_attribute(entry.additional_emails, attr) - end - - def people_relation_attribute(attr) - entry.relations_to_tails. - select { |r| :"people_relation_#{r.kind}" == attr }. - map { |r| r.tail.to_s }. - join(', ') - end - - def contact_account_attribute(accounts, attr) - account = accounts.find { |e| ContactAccounts.key(e.class, e.translated_label) == attr } - account.value if account - end - - end -end diff --git a/app/domain/export/ics/events.rb b/app/domain/export/ics/events.rb new file mode 100644 index 0000000000..2d40b4b0fb --- /dev/null +++ b/app/domain/export/ics/events.rb @@ -0,0 +1,31 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +module Export::Ics + class Events + + def generate(events) + ical = Icalendar::Calendar.new + ical_events = events.map { |event| generate_ical_events(event) }.flatten + ical_events.each { |event| ical.add_event(event) } + ical.to_ical + end + + def generate_ical_events(event) + event.dates.map do |event_date| + Icalendar::Event.new.tap do |ical_event| + ical_event.dtstart = event_date.start_at + ical_event.dtend = event_date.finish_at + ical_event.summary = "#{event.name}: #{event_date.label}" + ical_event.location = event_date.location || event.location + ical_event.description = event.description + ical_event.contact = event.contact + end + end + end + end +end diff --git a/app/domain/export/pdf/invoice.rb b/app/domain/export/pdf/invoice.rb new file mode 100644 index 0000000000..aa2d246782 --- /dev/null +++ b/app/domain/export/pdf/invoice.rb @@ -0,0 +1,56 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +module Export::Pdf + module Invoice + + class Runner + def render(invoices, options) + pdf = Prawn::Document.new(page_size: 'A4', + page_layout: :portrait, + margin: 2.cm) + customize(pdf) + invoices.each do |invoice| + invoice_page(pdf, invoice, options) + pdf.start_new_page unless invoice == invoices.last + end + pdf.render + end + + private + + def invoice_page(pdf, invoice, options) + if options[:articles] + sections.each { |section| section.new(pdf, invoice).render } + end + Esr.new(pdf, invoice).render if options[:esr] + end + + def customize(pdf) + pdf.font_size 10 + pdf.font 'Helvetica' + pdf + end + + def sections + [Header, InvoiceInformation, ReceiverAddress, Articles, InvoiceText] + end + end + + mattr_accessor :runner + + self.runner = Runner + + def self.render(invoice, options) + runner.new.render([invoice], options) + end + + def self.render_multiple(invoices, options) + runner.new.render(invoices, options) + end + end +end diff --git a/app/domain/export/pdf/invoice/articles.rb b/app/domain/export/pdf/invoice/articles.rb new file mode 100644 index 0000000000..138acb817a --- /dev/null +++ b/app/domain/export/pdf/invoice/articles.rb @@ -0,0 +1,81 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +module Export::Pdf::Invoice + class Articles < Section + + def render + bounding_box([0, 510], width: bounds.width) do + font_size(12) { text invoice.title } + pdf.move_down 10 + pdf.font_size(8) do + articles_table + end + end + total_box + end + + private + + def articles_table + table articles, header: true, column_widths: { 0 => 290, 1 => 50, 2 => 60, 3 => 80 }, + cell_style: { borders: [:bottom], + border_color: 'CCCCCC', + border_width: 0.5, + padding: [2, 0, 2, 0], + inline_format: true } do + + style(row(0), align: :center, font_style: :bold) + style(column(0), align: :left) + style(columns(1..3), align: :right) + end + end + + + def articles + [ + [I18n.t('activerecord.models.invoice_article.one'), + I18n.t('activerecord.attributes.invoice_item.count'), + I18n.t('activerecord.attributes.invoice_item.unit_cost'), + I18n.t('activerecord.attributes.invoice_item.cost')] + ] + article_data + end + + def article_data + invoice_items.collect do |it| + [ + "#{it.name}\n#{it.description}", + it.count, + helper.number_to_currency(it.unit_cost, unit: ''), + helper.number_to_currency(it.cost, unit: '') + ] + end + end + + def total_box + bounding_box([0, cursor], width: bounds.width) do + font_size(10) do + table total_data, position: :right, cell_style: { borders: [:bottom], + border_color: 'CCCCCC', + border_width: 0.5 } do + style(row(1).column(0), size: 8) + style(column(1), align: :right) + end + end + end + end + + def total_data + [ + [I18n.t('invoices.pdf.total'), + helper.number_to_currency(invoice.calculated[:total], format: '%n %u')], + [I18n.t('invoices.pdf.total_vat'), + helper.number_to_currency(invoice.calculated[:vat], format: '%n %u')] + ] + end + end +end diff --git a/app/domain/export/pdf/invoice/esr.rb b/app/domain/export/pdf/invoice/esr.rb new file mode 100644 index 0000000000..0082dd1d02 --- /dev/null +++ b/app/domain/export/pdf/invoice/esr.rb @@ -0,0 +1,73 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +module Export::Pdf::Invoice + class Esr < Section + + def render + #image "#{Prawn::DATADIR}/images/esr.png", at: [-60, 248], width: 602 + invoice_address + account_number + price + esr_number + receiver_address + end + + private + + def invoice_address + [-48, 125].each do |x| + bounding_box([x, 210], width: 150, height: 80) do + text invoice.address + end + end + end + + def account_number + [20, 193].each do |x| + bounding_box([x, 122], width: 90) do + pdf.font('Courier', size: 12) { text invoice.account_number } + end + end + end + + def price + [-50, 123].each do |x| + bounding_box([x, 96], width: 145) do + pdf.font('Courier', size: 12) do + text helper.number_to_currency(invoice.calculated[:total], + format: '%n', + separator: ' '), align: :right + end + end + end + end + + def esr_number + bounding_box([300, 146], width: 220) do + pdf.font('Courier', size: 12) do + text invoice.esr_number + end + end + end + + def receiver_address + [[300, 100], [-48, 70]].each do |width, height| + bounding_box([width, height], width: 150, height: 80) do + address_table + end + end + end + + def address_table + return unless address.present? + address_data = [address.split(/\n/)] + + table(address_data, cell_style: { borders: [], padding: [0, 0, 0, 0] }) + end + end +end diff --git a/app/domain/export/pdf/invoice/header.rb b/app/domain/export/pdf/invoice/header.rb new file mode 100644 index 0000000000..6987ee6d3f --- /dev/null +++ b/app/domain/export/pdf/invoice/header.rb @@ -0,0 +1,17 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +module Export::Pdf::Invoice + class Header < Section + + def render + bounding_box([0, cursor + 30], width: bounds.width, height: 40) do + text invoice.address + end + end + end +end diff --git a/app/domain/export/pdf/invoice/invoice_information.rb b/app/domain/export/pdf/invoice/invoice_information.rb new file mode 100644 index 0000000000..14a5fac8c4 --- /dev/null +++ b/app/domain/export/pdf/invoice/invoice_information.rb @@ -0,0 +1,30 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +module Export::Pdf::Invoice + class InvoiceInformation < Section + + def render + bounding_box([0, 640], width: bounds.width, height: 80) do + table(information, cell_style: { borders: [], padding: [1, 20, 0, 0] }) + end + end + + private + + def information + [ + [I18n.t('invoices.pdf.invoice_number') + ':', + invoice.sequence_number], + [I18n.t('invoices.pdf.invoice_date') + ':', + (I18n.l(invoice.sent_at) if invoice.sent_at)], + [I18n.t('invoices.pdf.due_at') + ':', + (I18n.l(invoice.due_at) if invoice.due_at)] + ] + end + end +end diff --git a/app/domain/export/pdf/invoice/invoice_text.rb b/app/domain/export/pdf/invoice/invoice_text.rb new file mode 100644 index 0000000000..26cfde0c92 --- /dev/null +++ b/app/domain/export/pdf/invoice/invoice_text.rb @@ -0,0 +1,17 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +module Export::Pdf::Invoice + class InvoiceText < Section + + def render + bounding_box([0, cursor - 20], width: bounds.width, height: 40) do + text invoice.description + end + end + end +end diff --git a/app/domain/export/pdf/invoice/receiver_address.rb b/app/domain/export/pdf/invoice/receiver_address.rb new file mode 100644 index 0000000000..868fb59480 --- /dev/null +++ b/app/domain/export/pdf/invoice/receiver_address.rb @@ -0,0 +1,41 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +module Export::Pdf::Invoice + class ReceiverAddress < Section + + def render + float do + bounding_box([290, 640], width: bounds.width, height: 80) do + receiver_address_table + end + end + end + + private + + def receiver_address_table + if recipient + receiver_address = receiver_address_data + else + return if recipient_address.blank? + receiver_address = [recipient_address.split(/\n/)] + end + + table(receiver_address, cell_style: { borders: [], padding: [0, 0, 0, 0] }) + end + + def receiver_address_data + [ + [recipient.full_name], + [recipient.address], + ["#{recipient.zip_code} #{recipient.town}"], + [Countries.label(recipient.country)] + ] + end + end +end diff --git a/app/domain/export/pdf/invoice/section.rb b/app/domain/export/pdf/invoice/section.rb new file mode 100644 index 0000000000..b0305ba981 --- /dev/null +++ b/app/domain/export/pdf/invoice/section.rb @@ -0,0 +1,36 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +module Export::Pdf::Invoice + class Section + + attr_reader :pdf, :invoice + + class_attribute :model_class + + delegate :bounds, :bounding_box, :table, + :text, :cursor, :font_size, :text_box, + :fill_and_stroke_rectangle, :fill_color, + :image, :group, :move_cursor_to, :float, + to: :pdf + + delegate :recipient, :invoice_items, :recipient_address, :address, to: :invoice + + def initialize(pdf, invoice) + @pdf = pdf + @invoice = invoice + end + + private + + def helper + @helper ||= Class.new do + include ActionView::Helpers::NumberHelper + end.new + end + end +end diff --git a/app/domain/export/pdf/labels.rb b/app/domain/export/pdf/labels.rb index 227f98513c..3f868b9507 100644 --- a/app/domain/export/pdf/labels.rb +++ b/app/domain/export/pdf/labels.rb @@ -34,21 +34,32 @@ def print_address_in_bounding_box(pdf, address, pos) pdf.bounding_box(pos, width: format.width.mm - min_border, height: format.height.mm - min_border) do + left = format.padding_left.mm + top = format.height.mm - format.padding_top.mm - min_border # pdf.stroke_bounds - pdf.text_box(address, at: [format.padding_left.mm, - format.height.mm - format.padding_top.mm - min_border]) + print_address_with_pp_post(pdf, address, left, top) end end # print without line wrap def print_address(pdf, address, pos) - pdf.text_box(address, at: [pos.first + format.padding_left.mm, - pos.last - format.padding_top.mm]) + left = pos.first + format.padding_left.mm + top = pos.last - format.padding_top.mm + print_address_with_pp_post(pdf, address, left, top) + end + + def print_address_with_pp_post(pdf, address, left, top) + if format.pp_post? + print_pp_post(pdf, [left, top]) + top -= 7.mm + end + pdf.text_box(address, at: [left, top]) end def address(contactable) address = '' address << contactable.company_name << "\n" if print_company?(contactable) + address << contactable.nickname << "\n" if print_nickname?(contactable) address << contactable.full_name << "\n" if contactable.full_name.present? address << contactable.address.to_s address << "\n" unless contactable.address =~ /\n\s*$/ @@ -73,6 +84,17 @@ def print_company?(contactable) contactable.respond_to?(:company) && contactable.company_name? end + def print_nickname?(contactable) + format.nickname? && contactable.respond_to?(:nickname) && contactable.nickname.present? + end + + def print_pp_post(pdf, at) + pdf.text_box("P.P. " \ + "#{format.pp_post} Post CH AG", + inline_format: true, + at: at) + end + def min_border Settings.pdf.labels.min_border.to_i.mm end diff --git a/app/domain/export/pdf/participation/confirmation.rb b/app/domain/export/pdf/participation/confirmation.rb index ad0f40083e..53175e636b 100644 --- a/app/domain/export/pdf/participation/confirmation.rb +++ b/app/domain/export/pdf/participation/confirmation.rb @@ -36,10 +36,7 @@ def render_contact_address pdf.bounding_box([10, cursor], width: bounds.width) do text(I18n.t('contactable.address_or_email', - address: [contact.to_s, - contact.address, - contact.zip_code, - contact.town].join(', '), + address: contact_address, email: contact.email)) end move_down_line @@ -72,6 +69,16 @@ def location_and_date Event::Date.model_name.human].join(' / ') end + def contact_address + [contact.company_name, + contact.full_name, + contact.address.present? && contact.address.split("\n"), + "#{contact.zip_code} #{contact.town}".strip] + .flatten + .select { |v| v.present? } + .join(', ') + end + def label_with_dots(content) text content move_down_line diff --git a/app/domain/export/pdf/participation/event_details.rb b/app/domain/export/pdf/participation/event_details.rb index b093bcee97..a1cf6ea84d 100644 --- a/app/domain/export/pdf/participation/event_details.rb +++ b/app/domain/export/pdf/participation/event_details.rb @@ -39,16 +39,27 @@ def render_requirements end if course? - boxed_attr(event_kind, :minimum_age) { translated_minimum_age } - boxed_attr(event_kind, :qualification_kinds, - human_attribute_name(:preconditions, event_kind), - %w(precondition participant)) + boxed_attr(human_attribute_name(:minimum_age, event_kind)) do + translated_minimum_age + end + boxed_attr(human_attribute_name(:preconditions, event_kind)) do + precondition_qualifications_summary + end end end end def translated_minimum_age - I18n.t('qualifications.in_years', years: event_kind.minimum_age) + I18n.t('qualifications.in_years', years: event_kind.minimum_age) if event_kind.minimum_age + end + + def precondition_qualifications_summary + kinds = event_kind.qualification_kinds('precondition', 'participant').group_by(&:id) + grouped_ids = event_kind.grouped_qualification_kind_ids('precondition', 'participant') + sentences = grouped_ids.collect do |ids| + ids.collect { |id| kinds[id].first.to_s }.sort.to_sentence + end + sentences.join(' ' + I18n.t('event.kinds.qualifications.or').upcase + ' ') end def description_title @@ -70,14 +81,10 @@ def requirements? event_kind.qualification_kinds('precondition', 'participant')].any?(&:present?) end - def boxed_attr(model, attr, title = nil, args = nil) - title ||= human_attribute_name(attr, model) - - values = Array(model.send(attr, *args)).reject(&:blank?) - values_text = block_given? ? yield : values.map(&:to_s).join("\n") - - if values.present? - render_columns(-> { text title }, -> { text values_text }) + def boxed_attr(title) + text = yield + if text.present? + render_columns(-> { text title }, -> { text text }) end end diff --git a/app/domain/export/pdf/participation/section.rb b/app/domain/export/pdf/participation/section.rb index 76bd697ce6..b0184df405 100644 --- a/app/domain/export/pdf/participation/section.rb +++ b/app/domain/export/pdf/participation/section.rb @@ -103,7 +103,7 @@ def human_attribute_name(attr, model) end def event_with_kind? - event.class.used_attributes.include?(:kind_id) + event.used_attributes.include?(:kind_id) end def i18n_event_postfix diff --git a/app/domain/export/pdf/participation/specifics.rb b/app/domain/export/pdf/participation/specifics.rb index 7121b299c8..7b8389e471 100644 --- a/app/domain/export/pdf/participation/specifics.rb +++ b/app/domain/export/pdf/participation/specifics.rb @@ -12,7 +12,7 @@ def render data = answers.map { |a| [a.question.question, a.answer] } if data.present? - with_header(I18n.t('event.participations.specific_information')) do + with_header(I18n.t('event.participations.application_answers')) do table(data, cell_style: { border_width: 0, padding: 2 }) end end @@ -25,7 +25,10 @@ def render private def answers - participation.answers + participation.answers. + joins(:question). + includes(:question). + where(event_questions: { admin: false }) end def additional_information_label diff --git a/app/domain/export/tabular/base.rb b/app/domain/export/tabular/base.rb new file mode 100644 index 0000000000..fdb87be8e0 --- /dev/null +++ b/app/domain/export/tabular/base.rb @@ -0,0 +1,102 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, insieme Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. +# + +module Export::Tabular + # Base class for csv/xlsx export + class Base + + class_attribute :model_class, :row_class, :auto_filter + self.row_class = Export::Tabular::Row + self.auto_filter = true + + attr_reader :list + + class << self + def export(format, *args) + generator(format).new(new(*args)).call + end + + def xlsx(*args) + export(:xlsx, *args) + end + + def csv(*args) + export(:csv, *args) + end + + private + + def generator(format) + case format + when :csv then Export::Csv::Generator + when :xlsx then Export::Xlsx::Generator + else raise ArgumentError, "Invalid format #{format}" + end + end + end + + def initialize(list) + @list = list + end + + # The list of all attributes exported to the csv/xlsx. + # overridde either this or #attribute_labels + def attributes + attribute_labels.keys + end + + # A hash of all attributes mapped to their labels exported to the csv/xlsx. + # overridde either this or #attributes + def attribute_labels + @attribute_labels ||= build_attribute_labels + end + + # List of all lables. + def labels + attribute_labels.values + end + + def header_rows + @header_rows ||= [] + end + + def data_rows(format = nil) + return enum_for(:data_rows) unless block_given? + + list.each do |entry| + yield values(entry, format) + end + end + + private + + def build_attribute_labels + attributes.each_with_object({}) do |attr, labels| + labels[attr] = attribute_label(attr) + end + end + + def attribute_label(attr) + human_attribute(attr) + end + + def human_attribute(attr) + model_class.human_attribute_name(attr) + end + + def values(entry, format = nil) + row = row_for(entry, format) + attributes.collect { |attr| row.fetch(attr) } + end + + def row_for(entry, format = nil) + row_class.new(entry, format) + end + + end +end diff --git a/app/domain/export/csv/events/list.rb b/app/domain/export/tabular/events/list.rb similarity index 96% rename from app/domain/export/csv/events/list.rb rename to app/domain/export/tabular/events/list.rb index 5e47137d0e..1c0d35175d 100644 --- a/app/domain/export/csv/events/list.rb +++ b/app/domain/export/tabular/events/list.rb @@ -5,13 +5,14 @@ # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. -module Export::Csv::Events - class List < Export::Csv::Base +module Export::Tabular::Events + class List < Export::Tabular::Base + include Translatable MAX_DATES = 3 - self.row_class = Export::Csv::Events::Row + self.row_class = Export::Tabular::Events::Row private diff --git a/app/domain/export/csv/events/row.rb b/app/domain/export/tabular/events/row.rb similarity index 96% rename from app/domain/export/csv/events/row.rb rename to app/domain/export/tabular/events/row.rb index 30a1e2823f..8be75e2701 100644 --- a/app/domain/export/csv/events/row.rb +++ b/app/domain/export/tabular/events/row.rb @@ -5,8 +5,8 @@ # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. -module Export::Csv::Events - class Row < Export::Csv::Row +module Export::Tabular::Events + class Row < Export::Tabular::Row self.dynamic_attributes = { /^contact_/ => :contactable_attribute, diff --git a/app/domain/export/csv/groups/list.rb b/app/domain/export/tabular/groups/list.rb similarity index 74% rename from app/domain/export/csv/groups/list.rb rename to app/domain/export/tabular/groups/list.rb index 7b66e3c059..f36a232baf 100644 --- a/app/domain/export/csv/groups/list.rb +++ b/app/domain/export/tabular/groups/list.rb @@ -5,15 +5,15 @@ # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. -module Export::Csv::Groups - class List < Export::Csv::Base +module Export::Tabular::Groups + class List < Export::Tabular::Base EXCLUDED_ATTRS = %w(lft rgt contact_id require_person_add_requests created_at updated_at deleted_at - creator_id updater_id deleter_id) + creator_id updater_id deleter_id).freeze self.model_class = Group - self.row_class = GroupRow + self.row_class = Export::Tabular::Groups::Row def attributes (model_class.column_names - EXCLUDED_ATTRS).collect(&:to_sym) diff --git a/app/domain/export/csv/groups/group_row.rb b/app/domain/export/tabular/groups/row.rb similarity index 60% rename from app/domain/export/csv/groups/group_row.rb rename to app/domain/export/tabular/groups/row.rb index 0fa6670dc1..7d4df7abaa 100644 --- a/app/domain/export/csv/groups/group_row.rb +++ b/app/domain/export/tabular/groups/row.rb @@ -1,12 +1,12 @@ # encoding: utf-8 # Copyright (c) 2012-2017, Dachverband Schweizer Jugendparlamente. This file is part of -# hitobito_dsj and licensed under the Affero General Public License version 3 +# hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito_dsj. +# https://github.com/hitobito/hitobito. -module Export::Csv::Groups - class GroupRow < Export::Csv::Row +module Export::Tabular::Groups + class Row < Export::Tabular::Row def type entry.class.label @@ -18,4 +18,3 @@ def country end end - diff --git a/app/domain/export/tabular/invoices/list.rb b/app/domain/export/tabular/invoices/list.rb new file mode 100644 index 0000000000..931391d919 --- /dev/null +++ b/app/domain/export/tabular/invoices/list.rb @@ -0,0 +1,22 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +module Export::Tabular::Invoices + class List < Export::Tabular::Base + + INCLUDED_ATTRS = %w(title sequence_number state esr_number description + recipient_email recipient_address sent_at due_at + cost vat total amount_paid) + + self.model_class = Invoice + self.row_class = Export::Tabular::Invoices::Row + + def attributes + (INCLUDED_ATTRS).collect(&:to_sym) + end + end +end diff --git a/app/domain/export/tabular/invoices/row.rb b/app/domain/export/tabular/invoices/row.rb new file mode 100644 index 0000000000..4beda808f9 --- /dev/null +++ b/app/domain/export/tabular/invoices/row.rb @@ -0,0 +1,21 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +module Export::Tabular::Invoices + class Row < Export::Tabular::Row + + def initialize(entry, format = nil) + @entry = InvoiceDecorator.decorate(entry) + @format = format + end + + def state + entry.state_label + end + + end +end diff --git a/app/domain/export/csv/people/contact_accounts.rb b/app/domain/export/tabular/people/contact_accounts.rb old mode 100644 new mode 100755 similarity index 82% rename from app/domain/export/csv/people/contact_accounts.rb rename to app/domain/export/tabular/people/contact_accounts.rb index f79e0dea84..d158029df6 --- a/app/domain/export/csv/people/contact_accounts.rb +++ b/app/domain/export/tabular/people/contact_accounts.rb @@ -1,13 +1,14 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. -module Export::Csv::People +module Export::Tabular::People module ContactAccounts class << self + def key(model, label) :"#{model.model_name.to_s.underscore}_#{label.downcase}" end @@ -15,6 +16,7 @@ def key(model, label) def human(model, label) "#{model.model_name.human} #{label}" end + end end end diff --git a/app/domain/export/csv/people/participation_row.rb b/app/domain/export/tabular/people/participation_row.rb similarity index 72% rename from app/domain/export/csv/people/participation_row.rb rename to app/domain/export/tabular/people/participation_row.rb index 30af67bcbd..7281651e91 100644 --- a/app/domain/export/csv/people/participation_row.rb +++ b/app/domain/export/tabular/people/participation_row.rb @@ -1,29 +1,30 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2017, Pfadibewegung Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. -module Export::Csv::People - class ParticipationRow < Export::Csv::People::PersonRow - dynamic_attributes[/^question_\d+$/] = :question_attribute +module Export::Tabular::People + class ParticipationRow < PersonRow attr_reader :participation - def initialize(participation) + delegate :additional_information, to: :participation, prefix: true + + dynamic_attributes[/^question_\d+$/] = :question_attribute + + def initialize(participation, format = nil) @participation = participation - super(participation.person) + super(participation.person, format) end def roles participation.roles.map { |role| role }.join(', ') end - delegate :additional_information, to: :participation, prefix: true - def created_at - I18n.l(participation.created_at.to_date) + normalize(participation.created_at.to_date) end def question_attribute(attr) @@ -31,5 +32,6 @@ def question_attribute(attr) answer = participation.answers.find { |e| e.question_id == id.to_i } answer.try(:answer) end + end end diff --git a/app/domain/export/csv/people/participations_address.rb b/app/domain/export/tabular/people/participations_address.rb similarity index 64% rename from app/domain/export/csv/people/participations_address.rb rename to app/domain/export/tabular/people/participations_address.rb index 2f896b918e..ff20c9cac2 100644 --- a/app/domain/export/csv/people/participations_address.rb +++ b/app/domain/export/tabular/people/participations_address.rb @@ -1,15 +1,14 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. -module Export::Csv::People - # handles participations +module Export::Tabular::People class ParticipationsAddress < PeopleAddress - self.row_class = Export::Csv::People::ParticipationRow + self.row_class = ParticipationRow def people list.map(&:person) diff --git a/app/domain/export/csv/people/participations_full.rb b/app/domain/export/tabular/people/participations_full.rb similarity index 84% rename from app/domain/export/csv/people/participations_full.rb rename to app/domain/export/tabular/people/participations_full.rb index 09a4f483e4..42bcff6938 100644 --- a/app/domain/export/csv/people/participations_full.rb +++ b/app/domain/export/tabular/people/participations_full.rb @@ -1,14 +1,14 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. -module Export::Csv::People +module Export::Tabular::People class ParticipationsFull < PeopleFull - self.row_class = Export::Csv::People::ParticipationRow + self.row_class = ParticipationRow def build_attribute_labels labels = super diff --git a/app/domain/export/csv/people/people_address.rb b/app/domain/export/tabular/people/people_address.rb old mode 100644 new mode 100755 similarity index 88% rename from app/domain/export/csv/people/people_address.rb rename to app/domain/export/tabular/people/people_address.rb index 9691f302f2..744d109c8b --- a/app/domain/export/csv/people/people_address.rb +++ b/app/domain/export/tabular/people/people_address.rb @@ -5,35 +5,23 @@ # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. -module Export::Csv::People - - # Attributes of people we want to include - class PeopleAddress < Export::Csv::Base +module Export::Tabular::People + class PeopleAddress < Export::Tabular::Base self.model_class = ::Person self.row_class = PersonRow - private - def build_attribute_labels - person_attribute_labels.merge(association_attributes) - end - - def person_attribute_labels - person_attributes.each_with_object({}) do |attr, hash| - hash[attr] = attribute_label(attr) - end - end - def person_attributes [:first_name, :last_name, :nickname, :company_name, :company, :email, - :address, :zip_code, :town, :country, :gender, :birthday, :roles] + :address, :zip_code, :town, :country, :gender, :birthday, :layer_group, :roles] end def association_attributes public_account_labels(:additional_emails, AdditionalEmail).merge( - public_account_labels(:phone_numbers, PhoneNumber)) + public_account_labels(:phone_numbers, PhoneNumber) + ) end def public_account_labels(accounts, klass) @@ -48,8 +36,19 @@ def account_labels(collection, model) end end + def build_attribute_labels + person_attribute_labels.merge(association_attributes) + end + + def person_attribute_labels + person_attributes.each_with_object({}) do |attr, hash| + hash[attr] = attribute_label(attr) + end + end + def people list end + end end diff --git a/app/domain/export/csv/people/people_full.rb b/app/domain/export/tabular/people/people_full.rb old mode 100644 new mode 100755 similarity index 54% rename from app/domain/export/csv/people/people_full.rb rename to app/domain/export/tabular/people/people_full.rb index be8d5d6508..f597a113e1 --- a/app/domain/export/csv/people/people_full.rb +++ b/app/domain/export/tabular/people/people_full.rb @@ -1,26 +1,30 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. -module Export::Csv::People - # adds social_accounts and company related attributes +module Export::Tabular::People class PeopleFull < PeopleAddress def person_attributes Person.column_names.collect(&:to_sym) - Person::INTERNAL_ATTRS - - [:picture, :primary_group_id] + - [:roles] + [:picture, :primary_group_id, :id] + + [:layer_group, :roles] end def association_attributes account_labels(people.map(&:additional_emails).flatten, AdditionalEmail).merge( - account_labels(people.map(&:phone_numbers).flatten, PhoneNumber)).merge( - account_labels(people.map(&:social_accounts).flatten, SocialAccount)).merge( - relation_kind_labels) + account_labels(people.map(&:phone_numbers).flatten, PhoneNumber) + ).merge( + account_labels(people.map(&:social_accounts).flatten, SocialAccount) + ).merge( + qualification_kind_labels + ).merge( + relation_kind_labels + ) end def relation_kind_labels @@ -31,5 +35,17 @@ def relation_kind_labels end end end + + def qualification_kind_labels + qualification_kinds = people.flat_map do |p| + p.qualifications.map { |q| q.qualification_kind.label } + end + qualification_kinds.uniq.sort.each_with_object({}) do |label, obj| + if label.present? + obj[ContactAccounts.key(QualificationKind, label)] = label + end + end + end + end end diff --git a/app/domain/export/tabular/people/person_row.rb b/app/domain/export/tabular/people/person_row.rb new file mode 100755 index 0000000000..eedf975ce6 --- /dev/null +++ b/app/domain/export/tabular/people/person_row.rb @@ -0,0 +1,87 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +module Export::Tabular::People + class PersonRow < Export::Tabular::Row + + self.dynamic_attributes = { /^phone_number_/ => :phone_number_attribute, + /^social_account_/ => :social_account_attribute, + /^additional_email_/ => :additional_email_attribute, + /^people_relation_/ => :people_relation_attribute, + /^qualification_kind_/ => :qualification_kind } + + def country + entry.country_label + end + + def gender + entry.gender_label + end + + def roles + if entry.try(:role_with_layer).present? + entry.roles.zip(entry.role_with_layer.split(', ')).map { |arr| arr.join(' ') }.join(', ') + else + entry.roles.map { |role| "#{role} #{role.group.with_layer.join(' / ')}" }.join(', ') + end + end + + def tags + entry.tag_list.to_s + end + + def layer_group + entry.layer_group.to_s + end + + private + + def phone_number_attribute(attr) + contact_account_attribute(entry.phone_numbers, attr) + end + + def social_account_attribute(attr) + contact_account_attribute(entry.social_accounts, attr) + end + + def additional_email_attribute(attr) + contact_account_attribute(entry.additional_emails, attr) + end + + def people_relation_attribute(attr) + entry.relations_to_tails. + select { |r| :"people_relation_#{r.kind}" == attr }. + map { |r| r.tail.to_s }. + join(', ') + end + + def qualification_kind(attr) + qualification = find_qualification(attr) + qualification.finish_at.try(:to_s) || I18n.t('global.yes') if qualification + end + + def find_qualification(label) + entry.qualifications.find do |q| + qualification_active?(q) && + ContactAccounts.key(q.qualification_kind.class, q.qualification_kind.label) == label + end + end + + def qualification_active?(q) + (q.start_at.blank? || q.start_at <= Time.zone.today) && + (q.finish_at.blank? || q.finish_at >= Time.zone.today) + end + + def contact_account_attribute(accounts, attr) + account = accounts.find do |e| + ContactAccounts.key(e.class, e.translated_label) == attr + end + account.value if account + end + + end +end diff --git a/app/domain/export/csv/row.rb b/app/domain/export/tabular/row.rb similarity index 73% rename from app/domain/export/csv/row.rb rename to app/domain/export/tabular/row.rb index a5226eca00..1953b26639 100644 --- a/app/domain/export/csv/row.rb +++ b/app/domain/export/tabular/row.rb @@ -1,11 +1,11 @@ # encoding: utf-8 -# Copyright (c) 2012-2014, Jungwacht Blauring Schweiz, Pfadibewegung Schweiz. +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz, Pfadibewegung Schweiz. # This file is part of hitobito and licensed under the Affero General Public # License version 3 or later. See the COPYING file at the top-level directory # or at https://github.com/hitobito/hitobito. -module Export::Csv +module Export::Tabular # Decorator for a row entry. # Attribute values may be accessed with fetch(attr). @@ -17,10 +17,11 @@ class Row class_attribute :dynamic_attributes self.dynamic_attributes = {} - attr_reader :entry + attr_reader :entry, :format - def initialize(entry) + def initialize(entry, format = nil) @entry = entry + @format = format end def fetch(attr) @@ -51,11 +52,17 @@ def handle_dynamic_attribute(attr) end end + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength + # rubocop:disable Metrics/PerceivedComplexity def normalize(value) if value == true I18n.t('global.yes') elsif value == false I18n.t('global.no') + elsif value.is_a?(Time) + format == :xlsx ? value.to_s : "#{I18n.l(value.to_date)} #{I18n.l(value, format: :time)}" + elsif value.is_a?(Date) + format == :xlsx ? value.to_s : I18n.l(value) else value end diff --git a/app/domain/export/vcf/vcards.rb b/app/domain/export/vcf/vcards.rb new file mode 100644 index 0000000000..c4314127d4 --- /dev/null +++ b/app/domain/export/vcf/vcards.rb @@ -0,0 +1,86 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +require 'vcard' + +module Export::Vcf + class Vcards + + def generate(people) + vcards = [] + people.each do |person| + vcards << vcard(person) + end + vcards.join + end + + + private + + def name(card, person) + card.name do |n| + n.given = person.first_name.to_s + n.family = person.last_name.to_s + end + if person.nickname.present? + card.nickname = person.nickname + end + end + + def birthday(card, person) + if person.birthday.present? + card.birthday = person.birthday + end + end + + def address_empty?(person) + !person.address.present? && !person.town.present? && + !person.zip_code.present? && !person.country.present? + end + + def address(card, person) + unless address_empty?(person) + card.add_addr do |a| + a.street = person.address.to_s + a.locality = person.town.to_s + a.postalcode = person.zip_code.to_s + a.country = person.country.to_s + end + end + end + + def emails(card, person) + if person.email.present? + card.add_email(person.email) { |e| e.preferred = true } + end + person.additional_emails.each do |email| + next unless email.public? + card.add_email(email.email) do |e| + e.location = email.label + e.preferred = false + end + end + end + + def phones(card, person) + person.phone_numbers.each do |phone| + next unless phone.public? + card.add_tel(phone.number) { |t| t.location = phone.label } + end + end + + def vcard(person) + Vcard::Vcard::Maker.make2 do |m| + name(m, person) + birthday(m, person) + address(m, person) + emails(m, person) + phones(m, person) + end + end + end +end diff --git a/app/domain/export/xlsx/base.rb b/app/domain/export/xlsx/base.rb deleted file mode 100644 index 5d7c4e0e2b..0000000000 --- a/app/domain/export/xlsx/base.rb +++ /dev/null @@ -1,52 +0,0 @@ -# encoding: utf-8 - -# Copyright (c) 2012-2016, insieme Schweiz. This file is part of -# hitobito_insieme and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito_insieme. -# - -module Export::Xlsx - # The base class for all the different xlsx export files. - class Base < ::Export::Base - - class_attribute :model_class, :row_class, :style_class - self.row_class = Row - self.style_class = Style - - delegate :column_widths, :style_definitions, :page_setup, :data_row_height, to: :style - - class << self - def export(*args) - Export::Xlsx::Generator.new(new(*args)).xlsx - end - end - - def header_rows - @header_rows ||= [] - end - - def data_rows - rows = [] - list.each.with_index do |entry, index| - rows << { values: values(entry), style: row_style(index) } - end - rows - end - - private - - def add_header_row(values = [], style = :default) - header_rows << { values: values, style: style } - end - - def row_style(index) - style.row_styles[index] || style.default_style_data_rows - end - - def style - @style ||= style_class.new - end - - end -end diff --git a/app/domain/export/xlsx/events/list.rb b/app/domain/export/xlsx/events/list.rb deleted file mode 100644 index 95f8680ea6..0000000000 --- a/app/domain/export/xlsx/events/list.rb +++ /dev/null @@ -1,102 +0,0 @@ -# encoding: utf-8 - -# Copyright (c) 2012-2014, Jungwacht Blauring Schweiz. This file is part of -# hitobito and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito. - -module Export::Xlsx::Events - class List < Export::Xlsx::Base - include Translatable - - MAX_DATES = 3 - - self.row_class = Export::Xlsx::Events::Row - self.style_class = Export::Xlsx::Events::Style - - private - - def build_attribute_labels - {}.tap do |labels| - add_main_labels(labels) - add_date_labels(labels) - add_contact_labels(labels) - add_additional_labels(labels) - add_count_labels(labels) - end - end - - def add_main_labels(labels) - add_used_attribute_label(labels, :name) - labels[:group_names] = translate(:group_names) - add_used_attribute_label(labels, :number) - labels[:kind] = Event::Kind.model_name.human if attr_used?(:kind_id) - add_used_attribute_label(labels, :description) - add_used_attribute_label(labels, :state) - add_used_attribute_label(labels, :location) - end - - def add_contact_labels(labels) - add_prefixed_contactable_labels(labels, :contact) - add_prefixed_contactable_labels(labels, :leader) - end - - def add_additional_labels(labels) - add_used_attribute_label(labels, :motto) - add_used_attribute_label(labels, :cost) - add_used_attribute_label(labels, :application_opening_at) - add_used_attribute_label(labels, :application_closing_at) - add_used_attribute_label(labels, :maximum_participants) - add_used_attribute_label(labels, :external_applications) - add_used_attribute_label(labels, :priorization) - end - - def add_count_labels(labels) - labels[:teamer_count] = human_attribute(:teamer_count) - labels[:participant_count] = human_attribute(:participant_count) - labels[:applicant_count] = human_attribute(:applicant_count) - end - - def add_date_labels(labels) - MAX_DATES.times.each do |i| - prefix = translate('date', index: i + 1) - labels[:"date_#{i}_label"] = "#{prefix} #{Event::Date.human_attribute_name(:label)}" - labels[:"date_#{i}_location"] = "#{prefix} #{Event::Date.human_attribute_name(:location)}" - labels[:"date_#{i}_duration"] = "#{prefix} #{translate('duration')}" - end - end - - def add_used_attribute_label(labels, attr) - if attr_used?(attr) - labels[attr] = human_attribute(attr) - end - end - - def attr_used?(attr) - model_class.attr_used?(attr) - end - - def add_prefixed_contactable_labels(labels, prefix) - contactable_keys.each do |key| - labels[:"#{prefix}_#{key}"] = - "#{translated_prefix(prefix)} #{Person.human_attribute_name(key)}" - end - end - - def contactable_keys - [:name, :address, :zip_code, :town, :email, :phone_numbers] - end - - def translated_prefix(prefix) - case prefix - when :leader then Event::Role::Leader.model_name.human - when :contact then human_attribute(:contact) - else prefix - end - end - - def model_class - @model_class ||= list.first ? list.first.class : ::Event::Course - end - end -end diff --git a/app/domain/export/xlsx/events/row.rb b/app/domain/export/xlsx/events/row.rb deleted file mode 100644 index 06ddbc3e62..0000000000 --- a/app/domain/export/xlsx/events/row.rb +++ /dev/null @@ -1,69 +0,0 @@ -# encoding: utf-8 - -# Copyright (c) 2012-2014, Jungwacht Blauring Schweiz. This file is part of -# hitobito and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito. - -module Export::Xlsx::Events - class Row < Export::Xlsx::Row - - self.dynamic_attributes = { - /^contact_/ => :contactable_attribute, - /^leader_/ => :contactable_attribute, - /^date_\d+_/ => :date_attribute - } - - def kind - entry.kind.try(:label) - end - - def state - if entry.possible_states.present? && entry.state - I18n.t("activerecord.attributes.event/course.states.#{entry.state}") - else - entry.state - end - end - - private - - def date_attribute(date_attr) - _, index, attr = date_attr.to_s.split('_', 3) - date = entry.dates[index.to_i] - date.try(attr).try(:to_s) - end - - # only the first leader is taken into account - def leader - leaders = entry.role_types.select(&:leader?) - @leader ||= entry.participations_for(*leaders).first.try(:person) - end - - def contact - entry.contact - end - - def contactable_attribute(contactable_attr) - subject, attr = contactable_attr.to_s.split('_', 2) - contactable = send(subject) - if contactable - contact_attr = :"contact_#{attr}" - if respond_to?(contact_attr, true) - send(contact_attr, contactable) - else - contactable.send(attr) - end - end - end - - def contact_name(contactable) - contactable.to_s - end - - def contact_phone_numbers(contactable) - contactable.phone_numbers.map(&:to_s).join(', ') - end - - end -end diff --git a/app/domain/export/xlsx/events/style.rb b/app/domain/export/xlsx/events/style.rb deleted file mode 100644 index b91834bc8e..0000000000 --- a/app/domain/export/xlsx/events/style.rb +++ /dev/null @@ -1,13 +0,0 @@ -# encoding: utf-8 - -# Copyright (c) 2012-2016, insieme Schweiz. This file is part of -# hitobito_insieme and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito_insieme. -# - -require 'axlsx' -module Export::Xlsx::Events - class Style < ::Export::Xlsx::Style - end -end diff --git a/app/domain/export/xlsx/generator.rb b/app/domain/export/xlsx/generator.rb index 9e619f36ba..56548e09d2 100644 --- a/app/domain/export/xlsx/generator.rb +++ b/app/domain/export/xlsx/generator.rb @@ -1,66 +1,79 @@ # encoding: utf-8 -# Copyright (c) 2012-2016, insieme Schweiz. This file is part of -# hitobito_insieme and licensed under the Affero General Public License version 3 +# Copyright (c) 2012-2017, insieme Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito_insieme. +# https://github.com/hitobito/hitobito. # require 'axlsx' + module Export::Xlsx def self.export(exportable) - Generator.new(exportable).xls + Generator.new(exportable).call end class Generator - attr_reader :xlsx + attr_reader :exportable, :style def initialize(exportable) - @xlsx = generate(exportable) + @exportable = exportable + @style = Style.for(exportable.class) + end + + def call + generate end private - def generate(exportable) + def generate package = Axlsx::Package.new do |p| p.workbook do |wb| - build_sheets(wb, exportable) + build_sheets(wb) end end package.to_stream.read end - def build_sheets(wb, exportable) - load_style_definitions(wb.styles, exportable) + def build_sheets(wb) + load_style_definitions(wb.styles) wb.add_worksheet do |sheet| - add_header_rows(sheet, exportable) + add_header_rows(sheet) + add_attribute_label_row(sheet) + add_data_rows(sheet) + apply_column_widths(sheet) - add_attribute_label_row(sheet, exportable) - - add_data_rows(sheet, exportable) - apply_column_widths(sheet, exportable) - sheet.page_setup.set(exportable.page_setup) + sheet.page_setup.set(style.page_setup) + add_auto_filter(sheet) end end - def add_header_rows(sheet, exportable) - exportable.header_rows.each do |r| - sheet.add_row(r[:values], row_style(r)) + def add_header_rows(sheet) + exportable.header_rows.each_with_index do |row, index| + sheet.add_row(row, row_style(style.header_style(index))) end end - def add_attribute_label_row(sheet, exportable) + def add_auto_filter(sheet) + return unless exportable.auto_filter + range = "#{sheet.rows[exportable.header_rows.size].cells.first.r}:" \ + "#{sheet.rows.last.cells.last.r}" + sheet.auto_filter = range + end + + def add_attribute_label_row(sheet) sheet.add_row(exportable.labels, style_definition(:attribute_labels)) end - def add_data_rows(sheet, exportable) - exportable.data_rows.each do |row| + def add_data_rows(sheet) + exportable.data_rows(:xlsx).each_with_index do |row, index| options = {} - options.merge!(row_style(row)) - options.merge!(data_row_height(exportable.data_row_height)) - sheet.add_row(row[:values], options) + options.merge!(row_style(style.row_style(index))) + options.merge!(data_row_height(style.data_row_height)) + sheet.add_row(row, options) end end @@ -68,12 +81,12 @@ def data_row_height(height) height.nil? ? {} : { height: height } end - def apply_column_widths(sheet, exportable) - sheet.column_widths(*exportable.column_widths) + def apply_column_widths(sheet) + sheet.column_widths(*style.column_widths) end - def load_style_definitions(workbook_styles, exportable) - definitions = exportable.style_definitions + def load_style_definitions(workbook_styles) + definitions = style.style_definitions definitions.each do |k, v| # pass each style definition through add_style # as recommended by axlsx @@ -86,8 +99,7 @@ def style_definition(key) @style_definitions[key].deep_dup end - def row_style(row) - style = row[:style] + def row_style(style) if style.is_a?(Array) cell_styles(style) else diff --git a/app/domain/export/xlsx/row.rb b/app/domain/export/xlsx/row.rb deleted file mode 100644 index 50df8a00bf..0000000000 --- a/app/domain/export/xlsx/row.rb +++ /dev/null @@ -1,66 +0,0 @@ -# encoding: utf-8 - -# Copyright (c) 2012-2016, insieme Schweiz. This file is part of -# hitobito_insieme and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito_insieme. -# - -module Export::Xlsx - - # Decorator for a row entry. - # Attribute values may be accessed with fetch(attr). - # If a method named #attr is defined on the decorator class, return its value. - # Otherwise, the attr is delegated to the entry. - class Row - - # regexp for attribute names which are handled dynamically. - class_attribute :dynamic_attributes - self.dynamic_attributes = {} - - attr_reader :entry - - def initialize(entry) - @entry = entry - end - - def fetch(attr) - normalize(value_for(attr)) - end - - private - - def value_for(attr) - if dynamic_attribute?(attr.to_s) - handle_dynamic_attribute(attr) - elsif respond_to?(attr, true) - send(attr) - else - entry.send(attr) - end - end - - def dynamic_attribute?(attr) - dynamic_attributes.any? { |regexp, _| attr =~ regexp } - end - - def handle_dynamic_attribute(attr) - dynamic_attributes.each do |regexp, handler| - if attr.to_s =~ regexp - return send(handler, attr) - end - end - end - - def normalize(value) - if value == true - I18n.t('global.yes') - elsif value == false - I18n.t('global.no') - else - value - end - end - - end -end diff --git a/app/domain/export/xlsx/style.rb b/app/domain/export/xlsx/style.rb old mode 100644 new mode 100755 index 73c5088a86..cd9211dc6d --- a/app/domain/export/xlsx/style.rb +++ b/app/domain/export/xlsx/style.rb @@ -1,16 +1,40 @@ # encoding: utf-8 # Copyright (c) 2012-2016, insieme Schweiz. This file is part of -# hitobito_insieme and licensed under the Affero General Public License version 3 +# hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito_insieme. +# https://github.com/hitobito/hitobito. # require 'axlsx' + module Export::Xlsx class Style + + class << self + + def register(style_class, *exportables) + exportables.each do |e| + registry[e] = style_class + end + end + + def for(exportable) + registry.fetch(exportable, self).new + end + + private + + def registry + @registry ||= {} + end + + end + LABEL_BACKGROUND = Settings.xlsx.label_background + class_attribute :style_definition_labels, :data_row_height + # extend in subclass and add your own definitions self.style_definition_labels = [:default, :attribute_labels, :centered] @@ -24,7 +48,20 @@ def data_row_height self.class.data_row_height end - # specify styles to apply per row or cell + def header_style(index) + header_styles[index] || :default + end + + def row_style(index) + row_styles[index] || default_style_data_rows + end + + # specify styles to apply per header row or cell + def header_styles + [] + end + + # specify styles to apply per data row or cell def row_styles [] end @@ -36,13 +73,13 @@ def column_widths # override in subclass to define page setup def page_setup - {paper_size: 9, # Default A4 - fit_to_height: 1, - orientation: :landscape } + { paper_size: 9, # Default A4 + fit_to_height: 1, + orientation: :landscape } end def default_style_data_rows - :centered + :default end private @@ -53,11 +90,17 @@ def style_definition_labels # style definitions def default_style - { style: { - font_name: Settings.xlsx.font_name, alignment: { horizontal: :left } } + { + style: { + font_name: Settings.xlsx.font_name, alignment: { horizontal: :left } + } } end + def date_style + default_style.deep_merge(style: { numFmts: NUM_FMT_YYYYMMDD }) + end + def attribute_labels_style default_style.deep_merge(style: { bg_color: LABEL_BACKGROUND }) end diff --git a/app/domain/group/deleted_people.rb b/app/domain/group/deleted_people.rb new file mode 100644 index 0000000000..e65fc3a273 --- /dev/null +++ b/app/domain/group/deleted_people.rb @@ -0,0 +1,38 @@ +# encoding: utf-8 + +# Copyright (c) 2017 Pfadibewegung Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. +# + +class Group::DeletedPeople + + class << self + + def deleted_for(layer_group) + Person. + joins('INNER JOIN roles ON roles.person_id = people.id'). + joins('INNER JOIN groups ON groups.id = roles.group_id'). + where("NOT EXISTS (#{undeleted_roles})"). + where("roles.deleted_at = (#{last_role_deleted})"). + where('groups.layer_group_id = ?', layer_group.id). + uniq + end + + private + + def undeleted_roles + 'SELECT * FROM roles ' \ + 'WHERE roles.deleted_at IS NULL ' \ + 'AND roles.person_id = people.id' + end + + def last_role_deleted + 'SELECT MAX(roles.deleted_at) FROM roles ' \ + 'WHERE roles.person_id = people.id ' + end + + end + +end diff --git a/app/domain/group/merger.rb b/app/domain/group/merger.rb index f581d7de66..d1de8198d4 100644 --- a/app/domain/group/merger.rb +++ b/app/domain/group/merger.rb @@ -1,6 +1,6 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. @@ -16,14 +16,14 @@ def initialize(group1, group2, new_group_name) end def merge! - fail('Cannot merge these Groups') unless group2_valid? + raise('Cannot merge these Groups') unless group2_valid? ::Group.transaction do if create_new_group update_events copy_roles - move_children(group1) - move_children(group2) + move_children + move_invoices_and_articles delete_old_groups end end @@ -62,14 +62,13 @@ def update_events end end - def move_children(group) + def move_children children = group1.children + group2.children children.each do |child| child.parent_id = new_group.id + child.parent(true) child.save! - child.update_attribute(:layer_group_id, child.layer_group.id) end - group.children.update_all(parent_id: new_group.id) end def copy_roles @@ -81,9 +80,24 @@ def copy_roles end end + def move_invoices_and_articles + invoices = group1.invoices + group2.invoices + invoices.each do |invoice| + invoice.group_id = new_group.id + invoice.save! + end + + invoice_articles = group1.invoice_articles + group2.invoice_articles + invoice_articles.each do |invoice_article| + invoice_article.group_id = new_group.id + invoice_article.save! + end + end + def delete_old_groups - group1.destroy - group2.destroy + [group1, group2].each do |group| + group.reload.destroy + end end end diff --git a/app/domain/invoice/filter.rb b/app/domain/invoice/filter.rb new file mode 100644 index 0000000000..48c127c201 --- /dev/null +++ b/app/domain/invoice/filter.rb @@ -0,0 +1,42 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +class Invoice::Filter + + attr_reader :params + + def initialize(params = {}) + @params = params + end + + def apply(scope) + scope = apply_scope(scope, params[:state], Invoice::STATES) + scope = apply_scope(scope, params[:due_since], Invoice::DUE_SINCE) + scope = filter_by_ids(scope) + cancelled? ? scope : scope.visible + end + + private + + def apply_scope(relation, scope, valid_scopes) + return relation unless valid_scopes.include?(scope) + relation.send(scope) + end + + def cancelled? + params[:state] == 'cancelled' + end + + def filter_by_ids(relation) + return relation if invoice_ids.blank? + relation.where(id: invoice_ids) + end + + def invoice_ids + @invoice_ids = params[:ids].to_s.split(',') + end +end diff --git a/app/domain/person/add_request/approver/event.rb b/app/domain/person/add_request/approver/event.rb index 1db16a4d06..f935f02562 100644 --- a/app/domain/person/add_request/approver/event.rb +++ b/app/domain/person/add_request/approver/event.rb @@ -19,7 +19,7 @@ def build_entity end def role_type - @role_type ||= event.class.find_role_type!(request.role_type) + @role_type ||= event.find_role_type!(request.role_type) end def event diff --git a/app/domain/person/add_request/creator/base.rb b/app/domain/person/add_request/creator/base.rb index 4835cb9182..d8501fdf9d 100644 --- a/app/domain/person/add_request/creator/base.rb +++ b/app/domain/person/add_request/creator/base.rb @@ -25,8 +25,7 @@ def handle end def required? - person_layer && - person_layer.require_person_add_requests? && + person_layer.try(:require_person_add_requests?) && ability.cannot?(:add_without_request, request) && entity.valid? end @@ -46,7 +45,7 @@ def request end def person_layer - person && person.primary_group.try(:layer_group) + person && (person.primary_group.try(:layer_group) || last_layer_group) end def request_attrs @@ -82,5 +81,10 @@ def body_class_name self.class.name.demodulize end + def last_layer_group + last_role = person.last_non_restricted_role + last_role && last_role.group.layer_group + end + end end diff --git a/app/domain/person/filter/base.rb b/app/domain/person/filter/base.rb new file mode 100644 index 0000000000..01eb351c87 --- /dev/null +++ b/app/domain/person/filter/base.rb @@ -0,0 +1,53 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +class Person::Filter::Base + + # - has not to be encoded in URLs, ',' must be and thus generate a much longer string. + ID_URL_SEPARATOR = '-'.freeze + + class_attribute :required_ability, :permitted_args + + class << self + def key + name.demodulize.underscore + end + end + + attr_reader :attr, :args + + def initialize(attr, args) + @attr = attr + @args = args.slice(*permitted_args) + end + + def apply(scope) + scope + end + + def blank? + args.blank? + end + + # Returns a serializable, persistable representation of this filter. + def to_hash + args + end + + # Returns a representation of this filter suitable for request url params. + def to_params + args + end + + private + + def id_list(key) + args[key] = args[key].to_s.split(ID_URL_SEPARATOR) unless args[key].is_a?(Array) + args[key].collect!(&:to_i) + end + +end diff --git a/app/domain/person/filter/chain.rb b/app/domain/person/filter/chain.rb new file mode 100644 index 0000000000..1f3e10044d --- /dev/null +++ b/app/domain/person/filter/chain.rb @@ -0,0 +1,92 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +class Person::Filter::Chain + + TYPES = [ # rubocop:disable Style/MutableConstant these are meant to be extended in wagons + Person::Filter::Role, + Person::Filter::Qualification + ] + + # Used for `serialize` method in ActiveRecord + class << self + def load(yaml) + new(YAML.load(yaml || '')) + end + + def dump(obj) + unless obj.is_a?(self) + raise ::ActiveRecord::SerializationTypeMismatch, + "Attribute was supposed to be a #{self}, but was a #{obj.class}. -- #{obj.inspect}" + end + + YAML.dump(obj.to_hash.deep_stringify_keys) + end + end + + attr_reader :filters + + def initialize(params) + @filters = parse(params) + end + + def filter(scope) + filters.inject(scope) do |s, filter| + filter.apply(s) + end + end + + def [](attr) + filters.find { |f| f.attr == attr.to_s } + end + + def blank? + filters.blank? + end + + def to_hash + # call #to_hash twice to get a regular hash (without indifferent access) + build_hash { |f| f.to_hash.to_hash } + end + + def to_params + build_hash { |f| f.to_params } + end + + def required_abilities + filters.map(&:required_ability).uniq + end + + private + + def build_hash + filters.each_with_object({}) { |f, h| h[f.attr] = yield f } + end + + def parse(params) + (params || {}).map { |attr, args| build_filter(attr, args) }.compact + end + + def build_filter(attr, args) + type = filter_type(attr) + if type + filter = type.new(attr, args.with_indifferent_access) + filter.present? ? filter : nil + end + end + + def filter_type(attr) + key = filter_type_key(attr) + TYPES.find { |t| t.key == key } + end + + def filter_type_key(attr) + # TODO: map filter types for regular person attrs + attr.to_s + end + +end diff --git a/app/domain/person/filter/list.rb b/app/domain/person/filter/list.rb new file mode 100644 index 0000000000..caff5ed400 --- /dev/null +++ b/app/domain/person/filter/list.rb @@ -0,0 +1,78 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +class Person::Filter::List + + attr_reader :group, :user, :chain, :range, :name, :multiple_groups + + def initialize(group, user, params = {}) + @group = group + @user = user + @chain = Person::Filter::Chain.new(params[:filters]) + @range = params[:range] + @name = params[:name] + end + + def entries + default_order(filter(accessibles).preload_groups) + end + + def all_count + @all_count ||= filter(all).count + end + + private + + def filter(scope) + if chain.present? + chain.filter(list_range(scope)).uniq + else + scope.members(group).uniq + end + end + + def accessibles + ability = accessibles_class.new(user, group_range? ? group : nil) + Person.accessible_by(ability) + end + + def accessibles_class + abilities = chain.required_abilities + if abilities.include?(:full) + PersonFullReadables + else + PersonReadables + end + end + + def all + chain.blank? || group_range? ? group.people : Person + end + + def list_range(scope) + case range + when 'deep' + @multiple_groups = true + scope.in_or_below(group) + when 'layer' + @multiple_groups = true + scope.in_layer(group) + else + scope.to_sql['INNER JOIN `roles`'] ? scope : scope.joins(:roles) + end + end + + def group_range? + !%w(deep layer).include?(range) + end + + def default_order(entries) + entries = entries.order_by_role if Settings.people.default_sort == 'role' + entries.order_by_name + end + +end diff --git a/app/domain/person/filter/qualification.rb b/app/domain/person/filter/qualification.rb new file mode 100644 index 0000000000..fc23b7f3fe --- /dev/null +++ b/app/domain/person/filter/qualification.rb @@ -0,0 +1,97 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +class Person::Filter::Qualification < Person::Filter::Base + + self.required_ability = :full + self.permitted_args = [:qualification_kind_ids, :validity, :match, + :start_at_year_from, :start_at_year_until, + :finish_at_year_from, :finish_at_year_until] + + def initialize(attr, args) + super + id_list(:qualification_kind_ids) + end + + def apply(scope) + if args[:match].to_s == 'all' + match_all_qualification_kinds(scope) + else + match_one_qualification_kind(scope) + end + end + + def blank? + args[:qualification_kind_ids].blank? + end + + def to_params + args.dup.tap do |hash| + hash[:qualification_kind_ids] = hash[:qualification_kind_ids].join(ID_URL_SEPARATOR) + end + end + + private + + def match_all_qualification_kinds(scope) + subquery = qualification_scope(scope). + select('1'). + where('qualifications.person_id = people.id AND ' \ + 'qualifications.qualification_kind_id = qk.id') + + scope.where('NOT EXISTS (' \ + ' SELECT 1 FROM qualification_kinds qk' \ + ' WHERE qk.id IN (?) ' \ + " AND NOT EXISTS (#{subquery.to_sql}) )", + args[:qualification_kind_ids]) + end + + def match_one_qualification_kind(scope) + scope. + joins(:qualifications). + where(qualifications: { qualification_kind_id: args[:qualification_kind_ids] }). + merge(qualification_scope(scope)) + end + + def qualification_scope(scope) + qualification_validity_scope(scope) + .merge(start_scope) + .merge(finish_scope) + end + + def finish_scope + qualification_date_year_scope( + :finish_at, + args[:finish_at_year_from], + args[:finish_at_year_until] + ) + end + + def start_scope + qualification_date_year_scope( + :start_at, + args[:start_at_year_from], + args[:start_at_year_until] + ) + end + + def qualification_date_year_scope(attr, from, untils) + scope = ::Qualification.all + scope = scope.where("#{attr} >= ?", Date.new(from, 1, 1)) if from.to_i > 0 + scope = scope.where("#{attr} <= ?", Date.new(untils, 12, 31)) if untils.to_i > 0 + scope + end + + def qualification_validity_scope(_scope) + case args[:validity].to_s + when 'active' then ::Qualification.active + when 'reactivateable' then ::Qualification.reactivateable + else ::Qualification.all + end + end + +end diff --git a/app/domain/person/filter/role.rb b/app/domain/person/filter/role.rb new file mode 100644 index 0000000000..29380cb3ef --- /dev/null +++ b/app/domain/person/filter/role.rb @@ -0,0 +1,100 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +class Person::Filter::Role < Person::Filter::Base + + self.permitted_args = [:role_type_ids, :role_types, :kind, :start_at, :finish_at] + + def initialize(attr, args) + super + initialize_role_types + end + + def apply(scope) + with_deleted(scope).where(type_conditions).where(duration_conditions) + end + + def blank? + args[:role_type_ids].blank? + end + + def to_hash + merge_duration_args(role_types: args[:role_types]) + end + + def to_params + merge_duration_args(role_type_ids: args[:role_type_ids].join(ID_URL_SEPARATOR)) + end + + def with_deleted? + %w(active deleted).include?(args[:kind]) + end + + def time_range + start_at = args[:start_at].presence || Time.zone.at(0).to_date.to_s + finish_at = args[:finish_at].presence || Time.zone.now.to_date.to_s + + Date.parse(start_at).beginning_of_day..Date.parse(finish_at).end_of_day + end + + private + + def merge_duration_args(hash) + hash.merge(args.slice(:kind, :start_at, :finish_at)) + end + + def initialize_role_types + classes = role_classes + args[:role_type_ids] = classes.map(&:id) + args[:role_types] = classes.map(&:sti_name) + end + + def role_classes + if args[:role_types].present? + role_classes_from_types + else + Role.types_by_ids(id_list(:role_type_ids)) + end + end + + def role_classes_from_types + map = Role.all_types.each_with_object({}) { |r, h| h[r.sti_name] = r } + args[:role_types].map { |t| map[t] }.compact + end + + def with_deleted(scope) + with_deleted? ? scope.joins(all_roles_join) : scope + end + + def role_relation + with_deleted? ? :with_deleted_roles : :roles + end + + def type_conditions + [[role_relation, { type: args[:role_types] }]].to_h + end + + def duration_conditions + case args[:kind] + when 'created' then [[role_relation, { created_at: time_range }]].to_h + when 'deleted' then [[role_relation, { deleted_at: time_range }]].to_h + when 'active' then [active_role_condition, min: time_range.min, max: time_range.max] + end + end + + def active_role_condition + <<-SQL.strip_heredoc.split.map(&:strip).join(' ') + with_deleted_roles.created_at <= :max AND + (with_deleted_roles.deleted_at >= :min OR with_deleted_roles.deleted_at IS NULL) + SQL + end + + def all_roles_join + 'INNER JOIN roles AS with_deleted_roles ON with_deleted_roles.person_id = people.id' + end + +end diff --git a/app/domain/person/list_filter.rb b/app/domain/person/list_filter.rb deleted file mode 100644 index 1dfc908a93..0000000000 --- a/app/domain/person/list_filter.rb +++ /dev/null @@ -1,58 +0,0 @@ -# encoding: utf-8 - -# Copyright (c) 2012-2015, Pfadibewegung Schweiz. This file is part of -# hitobito and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito. - -class Person::ListFilter - - class_attribute :accessibles_class - self.accessibles_class = PersonReadables - - attr_reader :group, :user, :multiple_groups - - def initialize(group, user) - @group = group - @user = user - end - - def filter_entries - entries = filtered_entries { |group| accessibles(group) }.preload_groups.uniq - entries = entries.order_by_role if Settings.people.default_sort == 'role' - entries.order_by_name - end - - def all_count - filtered_entries { |group| all(group) }.uniq.count - end - - private - - def unfiltered_entries(&block) - block.call(group).members(group) - end - - def list_scope(scope_kind, &block) - case scope_kind - when 'deep' - @multiple_groups = true - block.call.in_or_below(group) - when 'layer' - @multiple_groups = true - block.call.in_layer(group) - else - block.call(group) - end - end - - def all(group = nil) - group ? group.people : Person - end - - def accessibles(group = nil) - ability = accessibles_class.new(user, group) - Person.accessible_by(ability) - end - -end diff --git a/app/domain/person/qualification_filter.rb b/app/domain/person/qualification_filter.rb deleted file mode 100644 index c3c19fc496..0000000000 --- a/app/domain/person/qualification_filter.rb +++ /dev/null @@ -1,42 +0,0 @@ -# encoding: utf-8 - -# Copyright (c) 2012-2015, Pfadibewegung Schweiz. This file is part of -# hitobito and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito. - -class Person::QualificationFilter < Person::ListFilter - - self.accessibles_class = PersonFullReadables - - attr_reader :kind, :validity, :qualification_kind_ids - - def initialize(group, user, params) - super(group, user) - @kind = params[:kind].to_s - @validity = params[:validity].to_s - @qualification_kind_ids = Array(params[:qualification_kind_id]) - end - - private - - def filtered_entries(&block) - if qualification_kind_ids.present? - entries_with_qualifications(list_scope(kind, &block)) - else - unfiltered_entries(&block) - end - end - - def entries_with_qualifications(scope) - scope = scope.joins(:qualifications). - where(qualifications: { qualification_kind_id: qualification_kind_ids }) - - case validity - when 'active' then scope.merge(Qualification.active) - when 'reactivateable' then scope.merge(Qualification.reactivateable) - else scope - end - end - -end diff --git a/app/domain/person/role_filter.rb b/app/domain/person/role_filter.rb deleted file mode 100644 index 84e33f5eae..0000000000 --- a/app/domain/person/role_filter.rb +++ /dev/null @@ -1,28 +0,0 @@ -# encoding: utf-8 - -# Copyright (c) 2012-2014, Jungwacht Blauring Schweiz. This file is part of -# hitobito and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito. - -class Person::RoleFilter < Person::ListFilter - - attr_reader :kind, :filter - - def initialize(group, user, params) - super(group, user) - @kind = params[:kind].to_s - @filter = PeopleFilter.new(role_type_ids: params[:role_type_ids]) - end - - private - - def filtered_entries(&block) - if filter.role_types.present? - list_scope(kind, &block).where(roles: { type: filter.role_types }) - else - unfiltered_entries(&block) - end - end - -end diff --git a/app/domain/search_strategies/base.rb b/app/domain/search_strategies/base.rb new file mode 100644 index 0000000000..fc8baaf99d --- /dev/null +++ b/app/domain/search_strategies/base.rb @@ -0,0 +1,87 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Hitobito AG. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +module SearchStrategies + class Base + + QUERY_PER_PAGE = 10 + + def initialize(user, term, page) + @user = user + @term = term + @page = page + end + + def list_people + return Person.none.page(1) unless @term.present? + query_accessible_people do |ids| + entries = fetch_people(ids) + entries = Person::PreloadGroups.for(entries) + entries = Person::PreloadPublicAccounts.for(entries) + entries + end + end + + def query_people + # override + Person.none.page(1) + end + + def query_groups + # override + Group.none.page(1) + end + + def query_events + # override + Event.none.page(1) + end + + protected + + def fetch_people(_ids) + # override + Person.none.page(1) + end + + def query_accessible_people + ids = accessible_people_ids + return Person.none.page(1) if ids.blank? + yield ids + end + + def accessible_people_ids + key = "accessible_people_ids_for_#{@user.id}" + Rails.cache.fetch(key, expires_in: 15.minutes) do + ids = load_accessible_people_ids + if Ability.new(@user).can?(:index_people_without_role, Person) + ids += load_deleted_people_ids + end + ids.uniq + end + end + + def load_accessible_people_ids + accessible = Person.accessible_by(PersonReadables.new(@user)) + + # This still selects all people attributes :( + # accessible.pluck('people.id') + + # rewrite query to only include id column + sql = accessible.to_sql.gsub(/SELECT (.+) FROM /, 'SELECT DISTINCT people.id FROM ') + result = Person.connection.execute(sql) + result.collect { |row| row[0] } + end + + def load_deleted_people_ids + Person.where('NOT EXISTS (SELECT * FROM roles ' \ + 'WHERE roles.deleted_at IS NULL AND roles.person_id = people.id)') + .pluck(:id) + end + + end +end diff --git a/app/domain/search_strategies/sphinx.rb b/app/domain/search_strategies/sphinx.rb new file mode 100644 index 0000000000..bbb6487b5e --- /dev/null +++ b/app/domain/search_strategies/sphinx.rb @@ -0,0 +1,63 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Hitobito AG. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +module SearchStrategies + class Sphinx < Base + + delegate :star_supported?, to: :class + + def query_people + return Person.none.page(1) if @term.blank? + query_accessible_people do |ids| + Person.search(Riddle::Query.escape(@term), + default_search_options.merge( + with: { sphinx_internal_id: ids } + )) + end + end + + def query_groups + return Group.none.page(1) if @term.blank? + Group.search(Riddle::Query.escape(@term), + default_search_options) + end + + def query_events + return Event.none.page(1) if @term.blank? + sql = { include: [:groups, :dates] } + Event.search(Riddle::Query.escape(@term), + default_search_options.merge(sql: sql)) + end + + protected + + def default_search_options + { per_page: QUERY_PER_PAGE, + star: star_supported? } + end + + def fetch_people(ids) + Person.search(Riddle::Query.escape(@term), + page: @page, + order: 'last_name asc, ' \ + 'first_name asc, ' \ + "#{ThinkingSphinx::SphinxQL.weight[:select]} desc", + star: star_supported?, + with: { sphinx_internal_id: ids }) + end + + class << self + + def star_supported? + version = Rails.application.class.sphinx_version + version.nil? || version >= '2.1' + end + + end + + end +end diff --git a/app/domain/search_strategies/sql.rb b/app/domain/search_strategies/sql.rb new file mode 100644 index 0000000000..05ba73e4d9 --- /dev/null +++ b/app/domain/search_strategies/sql.rb @@ -0,0 +1,85 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Hitobito AG. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +module SearchStrategies + class Sql < Base + + MIN_TERM_LENGTH = 2 + + SEARCH_FIELDS = { + 'Person' => { + attrs: ['people.first_name', 'people.last_name', 'people.company_name', 'people.nickname', + 'people.company', 'people.email', 'people.address', 'people.zip_code', + 'people.town', 'people.country', 'people.birthday', 'people.additional_information', + 'phone_numbers.number', 'social_accounts.name', 'additional_emails.email'], + joins: ['LEFT JOIN phone_numbers ON phone_numbers.contactable_id = people.id AND ' \ + "phone_numbers.contactable_type = 'Person'", + 'LEFT JOIN social_accounts ON social_accounts.contactable_id = people.id AND '\ + "phone_numbers.contactable_type = 'Person'", + 'LEFT JOIN additional_emails ON additional_emails.contactable_id = people.id AND '\ + "phone_numbers.contactable_type = 'Person'"] + }, + 'Group' => { + attrs: ['groups.name', 'groups.short_name', 'groups.email', 'groups.address', + 'groups.zip_code', 'groups.town', 'groups.country', + 'parent.name', 'parent.short_name', 'phone_numbers.number', 'social_accounts.name', + 'additional_emails.email'], + joins: ['LEFT JOIN groups parent ON parent.id = groups.parent_id', + 'LEFT JOIN phone_numbers ON phone_numbers.contactable_id = groups.id AND ' \ + "phone_numbers.contactable_type = 'Group'", + 'LEFT JOIN social_accounts ON social_accounts.contactable_id = groups.id AND '\ + "phone_numbers.contactable_type = 'Group'", + 'LEFT JOIN additional_emails ON additional_emails.contactable_id = groups.id AND '\ + "phone_numbers.contactable_type = 'Group'"] + }, + 'Event' => { + attrs: ['events.name', 'events.number', 'groups.name'], + joins: [:groups] + } + } + + def list_people + return Person.none.page(1) unless term_present? + Kaminari.paginate_array(super).page(@page) + end + + def query_people + return Person.none.page(1) unless term_present? + query_accessible_people do |ids| + query_entities(Person.where(id: ids)).page(1).per(QUERY_PER_PAGE) + end + end + + def query_groups + return Group.none.page(1) unless term_present? + query_entities(Group.all).page(1).per(QUERY_PER_PAGE) + end + + def query_events + return Event.none.page(1) unless term_present? + query_entities(Event.includes(:groups, :dates).all).page(1).per(QUERY_PER_PAGE) + end + + protected + + def fetch_people(ids) + query_entities(Person.where(id: ids)) + end + + def query_entities(scope) + fields = SEARCH_FIELDS[scope.model.sti_name] + scope.joins(fields[:joins]) + .where(SqlConditionBuilder.new(@term, fields[:attrs]).search_conditions) + .uniq + end + + def term_present? + @term.present? && @term.length > MIN_TERM_LENGTH + end + + end +end diff --git a/app/domain/search_strategies/sql_condition_builder.rb b/app/domain/search_strategies/sql_condition_builder.rb new file mode 100644 index 0000000000..4d79d620b3 --- /dev/null +++ b/app/domain/search_strategies/sql_condition_builder.rb @@ -0,0 +1,47 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Hitobito AG. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +module SearchStrategies + class SqlConditionBuilder + + def initialize(search_string, search_tables_and_fields) + @search_string = search_string + @search_tables_and_fields = search_tables_and_fields + end + + # Concat the word clauses with AND. + def search_conditions + search_word_conditions.reduce do |query, condition| + query.and(condition) + end + end + + private + + # Split the search query in single words and create a list of word clauses. + def search_word_conditions + @search_string.split(/\s+/).map { |w| search_word_condition(w) } + end + + # Create a list of Arel #matches queries for each column and the given + # word. + def search_word_condition(word) + search_column_condition(word).reduce do |query, condition| + query.or(condition) + end + end + + def search_column_condition(word) + @search_tables_and_fields.map do |table_field| + table_name, field = table_field.split('.', 2) + table = Arel::Table.new(table_name) + table[field].matches(Arel::Nodes::Quoted.new("%#{word}%")) + end + end + + end +end diff --git a/app/domain/tag_category_parser.rb b/app/domain/tag_category_parser.rb index 9754d344a0..702d108cf8 100644 --- a/app/domain/tag_category_parser.rb +++ b/app/domain/tag_category_parser.rb @@ -1,9 +1,9 @@ # encoding: utf-8 # Copyright (c) 2012-2016, Dachverband Schweizer Jugendparlamente. This file is part of -# hitobito_dsj and licensed under the Affero General Public License version 3 +# hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito_dsj. +# https://github.com/hitobito/hitobito. class TagCategoryParser < ActsAsTaggableOn::DefaultParser diff --git a/app/domain/translatable.rb b/app/domain/translatable.rb index 93b88ed638..8592b56d7f 100644 --- a/app/domain/translatable.rb +++ b/app/domain/translatable.rb @@ -7,12 +7,12 @@ module Translatable - private - def translate(key, options = {}) I18n.t(full_translation_key(key), options) end + private + def full_translation_key(suffix) [translation_prefix, suffix].join('.').to_sym end diff --git a/app/helpers/action_helper.rb b/app/helpers/action_helper.rb index c9ed901cee..4c3f900bc9 100644 --- a/app/helpers/action_helper.rb +++ b/app/helpers/action_helper.rb @@ -28,8 +28,8 @@ def button_action_edit(path = nil, options = {}) # Uses the current record if none is given. def button_action_destroy(path = nil, options = {}) path ||= path_args(entry) - options[:data] = { confirm: ti(:confirm_delete), - method: :delete } + options[:data] ||= {} + options[:data].reverse_merge!(confirm: ti(:confirm_delete), method: :delete) action_button ti(:"link.delete"), path, 'trash', options end diff --git a/app/helpers/contact_attrs/control_builder.rb b/app/helpers/contact_attrs/control_builder.rb new file mode 100644 index 0000000000..053b35d5d8 --- /dev/null +++ b/app/helpers/contact_attrs/control_builder.rb @@ -0,0 +1,108 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Pfadibewegung Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +module ContactAttrs + class ControlBuilder + + include ActionView::Helpers::OutputSafetyHelper + + def initialize(form, event) + @f = form + @event = event + end + + def render + safe_join([mandatory_contact_attrs, configurable_contact_attrs, contact_associations]) + end + + private + + delegate :t, to: I18n + + attr_reader :f, :event + + def mandatory_contact_attrs + Event::ParticipationContactData.mandatory_contact_attrs.collect do |a| + f.labeled(a, attr_label(a)) do + radio_buttons(a, true, [:required]) + end + end + end + + def configurable_contact_attrs + non_mandatory_contact_attrs.collect do |a| + f.labeled(a, attr_label(a)) do + radio_buttons(a) + end + end + end + + def non_mandatory_contact_attrs + Event::ParticipationContactData.contact_attrs - + Event::ParticipationContactData.mandatory_contact_attrs + end + + def contact_associations + Event::ParticipationContactData.contact_associations.collect do |a| + f.labeled(a, attr_label(a)) do + assoc_checkbox(a) + end + end + end + + def radio_buttons(attr, disabled = false, options = [:required, :optional, :hidden]) + buttons = options.collect do |o| + checked = options.size == 1 + radio_button(attr, disabled, o, checked) + end + safe_join(buttons) + end + + def radio_button(attr, disabled, option, checked = false) + f.label("#{for_label(attr)}_#{option}", class: 'radio inline') do + checked = checked ? checked : checked?(attr, option) + options = {disabled: disabled, checked: checked} + f.radio_button(for_label(attr), option, options) + + option_label(option) + end + end + + def assoc_checkbox(assoc) + f.label(for_label(assoc), class: 'checkbox inline') do + options = {checked: assoc_hidden?(assoc)} + f.check_box(for_label(assoc), options, :hidden) + + option_label(:hidden) + end + end + + def assoc_hidden?(assoc) + event.hidden_contact_attrs.include?(assoc.to_s) + end + + def checked?(attr, option) + attr = attr.to_s + required = event.required_contact_attrs.include?(attr) + hidden = event.hidden_contact_attrs.include?(attr) + return required if option == :required + return hidden if option == :hidden + !required && !hidden + end + + def for_label(attr) + "contact_attrs[#{attr}]" + end + + def option_label(option) + t("activerecord.attributes.event/contact_attrs.#{option}") + end + + def attr_label(attr) + t("activerecord.attributes.person.#{attr}") + end + + end +end diff --git a/app/helpers/dropdown/base.rb b/app/helpers/dropdown/base.rb index 9358d98d3d..792ca86727 100644 --- a/app/helpers/dropdown/base.rb +++ b/app/helpers/dropdown/base.rb @@ -1,6 +1,7 @@ # encoding: utf-8 +# frozen_string_literal: true -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. @@ -79,7 +80,7 @@ def render_items end - class Item < Struct.new(:label, :url, :sub_items, :options) + Item = Struct.new(:label, :url, :sub_items, :options) do def initialize(label, url, options = {}) super(label, url, [], options) diff --git a/app/helpers/dropdown/event/events_export.rb b/app/helpers/dropdown/event/events_export.rb new file mode 100644 index 0000000000..c1b70c5200 --- /dev/null +++ b/app/helpers/dropdown/event/events_export.rb @@ -0,0 +1,33 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Pfadibewegung Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +module Dropdown::Event + class EventsExport < Dropdown::Base + + attr_reader :user, :params + + def initialize(template, params) + super(template, translate(:button), :download) + @params = params + + init_items + end + + private + + def init_items + tabular_links(:csv) + tabular_links(:xlsx) + end + + def tabular_links(format) + add_item(translate(format), params.merge(format: format)) + end + + end + +end diff --git a/app/helpers/dropdown/event/participant_add.rb b/app/helpers/dropdown/event/participant_add.rb index ab72a376b1..c3925ca6f9 100644 --- a/app/helpers/dropdown/event/participant_add.rb +++ b/app/helpers/dropdown/event/participant_add.rb @@ -56,11 +56,19 @@ def simple_button(url, options = {}) def init_items(url_options) event.participant_types.each do |type| opts = url_options.merge(event_role: { type: type.sti_name }) - link = template.new_group_event_participation_path(group, event, opts) + link = participate_link(opts) add_item(translate(:as, role: type.label), link) end end + def participate_link(opts) + if opts[:for_someone_else] + template.new_group_event_participation_path(group, event, opts) + else + template.contact_data_group_event_participations_path(group, event, opts) + end + end + end end end diff --git a/app/helpers/dropdown/event/role_add.rb b/app/helpers/dropdown/event/role_add.rb index 5c5f36e770..b71901f0e9 100644 --- a/app/helpers/dropdown/event/role_add.rb +++ b/app/helpers/dropdown/event/role_add.rb @@ -23,7 +23,7 @@ def initialize(template, group, event) private def init_items - event.klass.role_types.reject(&:restricted?).each do |type| + event.role_types.reject(&:restricted?).each do |type| link = template.new_group_event_role_path(group, event, event_role: { type: type.sti_name }) diff --git a/app/helpers/dropdown/invoice_sending.rb b/app/helpers/dropdown/invoice_sending.rb new file mode 100644 index 0000000000..e99230e7ed --- /dev/null +++ b/app/helpers/dropdown/invoice_sending.rb @@ -0,0 +1,38 @@ +# encoding: utf-8 +# frozen_string_literal: true + +# Copyright (c) 2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +module Dropdown + class InvoiceSending < Base + + attr_reader :params + + def initialize(template, params, path_method) + super(template, translate(:button), :envelope) + @params = params + @path_method = path_method + init_items + end + + private + + def init_items + send_links + end + + def send_links + add_item(:state, mail: false) + add_item(:mail, mail: true) + end + + def add_item(key, options = {}) + path = @template.send(@path_method, options) + super(translate(key), path, data: { method: :put, checkable: true }) + end + + end +end diff --git a/app/helpers/dropdown/invoices.rb b/app/helpers/dropdown/invoices.rb new file mode 100644 index 0000000000..4fa088d2d3 --- /dev/null +++ b/app/helpers/dropdown/invoices.rb @@ -0,0 +1,56 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +module Dropdown + class Invoices < Base + + attr_reader :params, :user + + def initialize(template, params, type) + super(template, translate(type), type) + @params = params + @user = template.current_user + end + + def print + pdf_links + self + end + + def export + label_links + csv_links + self + end + + private + + def pdf_links + add_item(translate(:full), export_path(:pdf), item_options) + add_item(translate(:articles_only), export_path(:pdf, esr: false), item_options) + add_item(translate(:esr_only), export_path(:pdf, articles: false), item_options) + end + + def label_links + if LabelFormat.exists? + Dropdown::LabelItems.new(self, item_options.merge(condense_labels: false)).add + end + end + + def csv_links + add_item(translate(:csv), export_path(:csv), item_options) + end + + def item_options + { target: :new, data: { checkable: true } } + end + + def export_path(format, options = {}) + params.merge(options).merge(format: format) + end + end +end diff --git a/app/helpers/dropdown/label_items.rb b/app/helpers/dropdown/label_items.rb new file mode 100644 index 0000000000..b22a369357 --- /dev/null +++ b/app/helpers/dropdown/label_items.rb @@ -0,0 +1,95 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +module Dropdown + class LabelItems + attr_reader :dropdown, :item_options + + delegate :add_item, :translate, :user, :params, to: :dropdown + + def initialize(dropdown, item_options = {}) + @dropdown = dropdown + @condense_labels = item_options.delete(:condense_labels) + @item_options = item_options.reverse_merge(target: :new, + class: 'export-label-format') + end + + def add + label_item = add_item(translate(:labels), main_label_link) + add_last_used_format_item(label_item) + add_label_format_items(label_item) + add_condensed_labels_option_items(label_item) if @condense_labels + end + + def main_label_link + if user.last_label_format_id + export_label_format_path(user.last_label_format_id) + else + '#' + end + end + + def add_last_used_format_item(parent) + if user.last_label_format_id? + last_format = user.last_label_format + parent.sub_items << Item.new(last_format.to_s, + export_label_format_path(last_format.id), + item_options) + parent.sub_items << Divider.new + end + end + + def add_label_format_items(parent) + LabelFormat.list.for_person(user).each do |label_format| + parent.sub_items << Item.new(label_format, + export_label_format_path(label_format.id), + item_options) + end + end + + def add_condensed_labels_option_items(parent) + parent.sub_items << Divider.new + parent.sub_items << ToggleCondensedLabelsItem.new(dropdown.template) + end + + def export_label_format_path(id) + params.merge(format: :pdf, label_format_id: id, + condense_labels: ToggleCondensedLabelsItem::DEFAULT_STATE) + end + + + class ToggleCondensedLabelsItem < Dropdown::Base + DEFAULT_STATE = false + + def initialize(template) + super(template, template.t('dropdown/people_export.condense_labels'), :plus) + end + + def render(template) + template.content_tag(:li) do + template.link_to('#', id: 'toggle-condense-labels') do + render_checkbox(template) + end + end + end + + def render_checkbox(template) + template.content_tag(:div, class: 'checkbox') do + template.content_tag(:label, for: :condense) do + template.safe_join([ + template.check_box_tag(:condense, '1', DEFAULT_STATE), + template.t('dropdown/people_export.condense_labels'), + template.content_tag(:p, template.t('dropdown/people_export.condense_labels_hint'), + class: 'help-text') + ].compact) + end + end + end + end + end +end + diff --git a/app/helpers/dropdown/people_export.rb b/app/helpers/dropdown/people_export.rb old mode 100644 new mode 100755 index 28561883b7..631335122b --- a/app/helpers/dropdown/people_export.rb +++ b/app/helpers/dropdown/people_export.rb @@ -1,6 +1,6 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. @@ -10,12 +10,13 @@ class PeopleExport < Base attr_reader :user, :params - def initialize(template, user, params, details, email_addresses) + def initialize(template, user, params, details, email_addresses, labels = true) super(template, translate(:button), :download) @user = user @params = params @details = details @email_addresses = email_addresses + @labels = labels init_items end @@ -23,23 +24,29 @@ def initialize(template, user, params, details, email_addresses) private def init_items - csv_links + tabular_links(:csv) + tabular_links(:xlsx) + vcard_link label_links email_addresses_link end - def csv_links - csv_path = params.merge(format: :csv) + def tabular_links(format) + path = params.merge(format: format) if @details - csv_item = add_item(translate(:csv), '#') - csv_item.sub_items << Item.new(translate(:addresses), csv_path) - csv_item.sub_items << Item.new(translate(:everything), csv_path.merge(details: true)) + item = add_item(translate(format), '#') + item.sub_items << Item.new(translate(:addresses), path) + item.sub_items << Item.new(translate(:everything), path.merge(details: true)) else - add_item(translate(:csv), csv_path) + add_item(translate(format), path) end end + def vcard_link + add_item(translate(:vcard), params.merge(format: :vcf), target: :new) + end + def email_addresses_link if @email_addresses add_item(translate(:emails), params.merge(format: :email), target: :new) @@ -47,77 +54,11 @@ def email_addresses_link end def label_links - if LabelFormat.all_as_hash.present? - label_item = add_item(translate(:labels), main_label_link) - add_last_used_format_item(label_item) - add_label_format_items(label_item) - add_condensed_labels_option_items(label_item) + if @labels && LabelFormat.exists? + Dropdown::LabelItems.new(self, condense_labels: true).add end end - def main_label_link - if user.last_label_format_id - export_label_format_path(user.last_label_format_id) - else - '#' - end - end - - def add_last_used_format_item(parent) - if user.last_label_format_id? - last_format = user.last_label_format - parent.sub_items << Item.new(last_format.to_s, - export_label_format_path(last_format.id), - target: :new) - parent.sub_items << Divider.new - end - end - - def add_label_format_items(parent) - LabelFormat.all_as_hash.each do |id, label| - parent.sub_items << Item.new(label, export_label_format_path(id), - target: :new, class: 'export-label-format') - end - end - - def add_condensed_labels_option_items(parent) - parent.sub_items << Divider.new - parent.sub_items << ToggleCondensedLabelsItem.new(@template) - end - - def export_label_format_path(id) - params.merge(format: :pdf, label_format_id: id, - condense_labels: ToggleCondensedLabelsItem::DEFAULT_STATE) - end - end - class ToggleCondensedLabelsItem < Base - DEFAULT_STATE = false - - def initialize(template) - super(template, template.t('dropdown/people_export.condense_labels'), :plus) - end - - def render(template) - template.content_tag(:li) do - template.link_to('#', id: 'toggle-condense-labels') do - render_checkbox(template) - end - end - end - - def render_checkbox(template) - template.content_tag(:div, class: 'checkbox') do - template.content_tag(:label, for: :condense) do - template.safe_join([ - template.check_box_tag(:condense, '1', DEFAULT_STATE), - template.t('dropdown/people_export.condense_labels'), - template.content_tag(:p, template.t('dropdown/people_export.condense_labels_hint'), - class: 'help-text') - ].compact) - end - end - end - end end diff --git a/app/helpers/event_kinds_helper.rb b/app/helpers/event_kinds_helper.rb index 411e675d0a..4855dbddfd 100644 --- a/app/helpers/event_kinds_helper.rb +++ b/app/helpers/event_kinds_helper.rb @@ -10,14 +10,28 @@ module EventKindsHelper def labeled_qualification_kinds_field(form, collection, category, role, title) selected = entry.qualification_kinds(category, role) - # Unify collection with selected, to include them even, if they are marked as deleted. + # Unify collection with selected, to include them even if they are marked as deleted. options = collection | selected form.labeled(title) do - select_tag "event_kind[qualification_kinds][#{role}][#{category}][qualification_kind_ids]", + select_tag("event_kind[qualification_kinds][#{role}][#{category}][qualification_kind_ids]", options_from_collection_for_select(options, :id, :to_s, selected.collect(&:id)), - multiple: true, class: 'span6' + multiple: true, + class: 'span6') + end + end + + def grouped_qualification_kinds_string(kind, category, role) + kinds = kind.qualification_kinds(category, role).group_by(&:id) + grouped_ids = kind.grouped_qualification_kind_ids(category, role) + or_separator = [ + ' ', + content_tag(:span, t('event.kinds.qualifications.or'), class: 'muted'), + ' ' + ] + safe_join(grouped_ids, safe_join(or_separator)) do |ids| + ids.collect { |id| kinds[id].first.to_s }.sort.to_sentence end end diff --git a/app/helpers/event_participations_helper.rb b/app/helpers/event_participations_helper.rb index 96c59fd088..87822f56f0 100644 --- a/app/helpers/event_participations_helper.rb +++ b/app/helpers/event_participations_helper.rb @@ -47,4 +47,15 @@ def show_application_priorities?(participation) participation.application.priorities? && can?(:show_priorities, participation.application) end + + def action_button_cancel_participation + action_button( + t('event.participations.cancel_application.caption'), + group_event_participation_path(parent, entry, @user_participation), + 'remove-circle', + data: { + confirm: t('event.participations.cancel_application.confirmation'), + method: :delete + }) + end end diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 1eddd5d187..38967adf3b 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -1,24 +1,50 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. module EventsHelper - def format_training_days(event) - number_with_precision(event.training_days, precision: 1) + def new_event_button + event_type = find_event_type + return unless event_type + + event = event_type.new + event.groups << @group + if can?(:new, event) + action_button(t("events.global.link.add_#{event_type.name.underscore}"), + new_group_event_path(@group, event: { type: event_type.sti_name }), + :plus) + end end - def button_action_event_apply(event, group = nil) + def export_events_button + type = params[:type].presence || 'Event' + if can?(:"export_#{type.underscore.pluralize}", @group) + Dropdown::Event::EventsExport.new(self, params).to_s + end + end + + def event_user_application_possible?(event) participation = event.participations.new participation.person = current_user - if event.application_possible? && can?(:new, participation) + event.application_possible? && can?(:new, participation) + end + + def button_action_event_apply(event, group = nil) + if event_user_application_possible?(event) group ||= event.groups.first - Dropdown::Event::ParticipantAdd.for_user(self, group, event, current_user) + button = Dropdown::Event::ParticipantAdd.for_user(self, group, event, current_user) + if event.application_closing_at.present? + button += content_tag(:div, + t('event.lists.apply_until', + date: f(event.application_closing_at))) + end + button end end @@ -31,10 +57,22 @@ def application_approve_role_exists? Role.types_with_permission(:approve_applications).present? end + def format_training_days(event) + number_with_precision(event.training_days, precision: 1) + end + def format_event_application_conditions(entry) texts = [entry.application_conditions] texts.unshift(entry.kind.application_conditions) if entry.course_kind? safe_join(texts.select(&:present?).map { |text| simple_format(text) }) end + private + + def find_event_type + @group.event_types.find do |t| + (params[:type].blank? && t == Event) || t.sti_name == params[:type] + end + end + end diff --git a/app/helpers/filter_helper.rb b/app/helpers/filter_helper.rb new file mode 100644 index 0000000000..bf60328dd3 --- /dev/null +++ b/app/helpers/filter_helper.rb @@ -0,0 +1,31 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +module FilterHelper + + # rubocop:disable Rails/OutputSafety + def direct_filter(attr, label = nil, &block) + html = ''.html_safe + label ||= model_class.human_attribute_name(attr) + html += label_tag(attr, label, class: 'control-label').html_safe if label + html += capture(&block) + content_tag(:div, html, class: 'control-group').html_safe + end + # rubocop:enable Rails/OutputSafety + + def direct_filter_select(attr, list, label = nil, options = {}) + options.reverse_merge!(prompt: t('global.all'), value_method: :first, text_method: :second) + add_css_class(options, 'control-group') + options[:data] ||= {} + options[:data][:submit] = true + select_options = options_from_collection_for_select(list, + options.delete(:value_method), + options.delete(:text_method), + params[attr]) + direct_filter(attr, label) { select_tag(attr, select_options, options) } + end +end diff --git a/app/helpers/filter_navigation/people.rb b/app/helpers/filter_navigation/people.rb index 1f47edbcdc..bdb40b387d 100644 --- a/app/helpers/filter_navigation/people.rb +++ b/app/helpers/filter_navigation/people.rb @@ -1,6 +1,6 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. @@ -8,14 +8,14 @@ module FilterNavigation class People < Base - attr_reader :group + attr_reader :group, :filter delegate :can?, to: :template - def initialize(template, group, params) + def initialize(template, group, filter) super(template) @group = group - @params = params + @filter = filter init_kind_filter_names init_labels init_kind_items @@ -23,23 +23,11 @@ def initialize(template, group, params) end def name - @params[:name] + filter.name end - def role_type_ids - @params[:role_type_ids] - end - - def qualification_kind_ids - @params[:qualification_kind_id] - end - - def deep - @params[:kind] || false - end - - def validity - @params[:validity] + def match + @params[:match] end private @@ -56,7 +44,7 @@ def init_labels @active_label = name elsif name.present? dropdown.activate(name) - elsif role_type_ids.present? || qualification_kind_ids.present? + elsif filter.chain.present? dropdown.activate(translate(:custom_filter)) else @active_label = main_filter_name @@ -66,11 +54,11 @@ def init_labels def init_kind_items @kind_filter_names.each do |kind, name| types = group.role_types.select { |t| t.kind == kind } - if visible_role_types?(types) - count = group.people.where(roles: { type: types.collect(&:sti_name) }).uniq.count - path = kind == :member ? path : fixed_types_path(name, types) - item(name, path, count) - end + next unless visible_role_types?(types) + + count = group.people.where(roles: { type: types.collect(&:sti_name) }).uniq.count + path = kind == :member ? path : fixed_types_path(name, types) + item(name, path, count) end end @@ -90,46 +78,40 @@ def init_dropdown_links else add_entire_subgroup_filter_link end - add_people_role_filter_links - add_define_people_role_filter_link - add_define_qualification_filter_link + add_people_filter_links + add_define_people_filter_link end def add_entire_layer_filter_link name = translate(:entire_layer) - link = fixed_types_path(name, sub_groups_role_types, kind: 'layer') + link = fixed_types_path(name, sub_groups_role_types, range: 'layer') dropdown.add_item(name, link) end def add_entire_subgroup_filter_link name = translate(:entire_group) - link = fixed_types_path(name, sub_groups_role_types, kind: 'deep') + link = fixed_types_path(name, sub_groups_role_types, range: 'deep') dropdown.add_item(name, link) end - def add_people_role_filter_links + def add_people_filter_links filters = PeopleFilter.for_group(group) filters.each { |filter| people_filter_link(filter) } end - def add_define_people_role_filter_link + def add_define_people_filter_link if can?(:new, group.people_filters.new) dropdown.add_divider if dropdown.items.present? - dropdown.add_item(translate(:new_role_filter), new_group_people_filter_path) - end - end - - def add_define_qualification_filter_link - if can?(:index_full_people, group) - dropdown.add_item(translate(:new_qualification_filter), - qualification_group_people_filter_path) + dropdown.add_item(translate(:new_filter), new_group_people_filter_path) end end def new_group_people_filter_path template.new_group_people_filter_path( group.id, - people_filter: { role_type_ids: role_type_ids }) + range: filter.range, + filters: filter.chain.to_params + ) end def qualification_group_people_filter_path @@ -137,35 +119,53 @@ def qualification_group_people_filter_path group.id, qualification_kind_id: qualification_kind_ids, kind: deep, - validity: validity) + validity: validity, + match: match, + start_at_year_from: @params[:start_at_year_from], + start_at_year_until: @params[:start_at_year_until], + finish_at_year_from: @params[:finish_at_year_from], + finish_at_year_until: @params[:finish_at_year_until]) end def people_filter_link(filter) - item = dropdown.add_item(filter.name, filter_path(filter, kind: 'deep')) + item = dropdown.add_item(filter.name, path(filter_id: filter.id)) if can?(:destroy, filter) + item.sub_items << edit_filter_item(filter) item.sub_items << delete_filter_item(filter) end end def delete_filter_item(filter) - ::Dropdown::Item.new(template.icon(:trash), - delete_group_people_filter_path(filter), - data: { confirm: template.ti(:confirm_delete), - method: :delete }) + ::Dropdown::Item.new( + filter_label(:trash, :delete), + delete_group_people_filter_path(filter), + data: { confirm: template.ti(:confirm_delete), method: :delete } + ) end def delete_group_people_filter_path(filter) template.group_people_filter_path(group, filter) end - def fixed_types_path(name, types, options = {}) - filter_path(PeopleFilter.new(role_type_ids: types.collect(&:id), name: name), options) + def edit_filter_item(filter) + ::Dropdown::Item.new( + filter_label(:edit, :edit), + edit_group_people_filter_path(filter) + ) + end + + def edit_group_people_filter_path(filter) + template.edit_group_people_filter_path(group, filter) + end + + def filter_label(icon, desc) + template.safe_join([template.icon(icon), ' ', template.t("global.link.#{desc}")]) end - def filter_path(filter, options = {}) - options[:role_type_ids] ||= filter.role_type_ids_string - options[:name] ||= filter.name - path(options) + def fixed_types_path(name, types, options = {}) + type_ids = types.collect(&:id).join(Person::Filter::Base::ID_URL_SEPARATOR) + path(options.merge(name: name, + filters: { role: { role_type_ids: type_ids } })) end def path(options = {}) diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 14cfff73d7..488d7137f3 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -7,16 +7,11 @@ module GroupsHelper - def new_event_button - event_type = find_event_type - return unless event_type - - event = event_type.new - event.groups << @group - if can?(:new, event) - action_button(t("events.global.link.add_#{event_type.name.underscore}"), - new_group_event_path(@group, event: { type: event_type.sti_name }), - :plus) + def export_events_ical_button + type = params[:type].presence || 'Event' + if can?(:"export_#{type.underscore.pluralize}", @group) + action_button(I18n.t('event.lists.courses.ical_export_button'), + params.merge(format: :ics), :calendar) end end @@ -35,12 +30,4 @@ def tab_person_add_request_label(group) label.html_safe end - private - - def find_event_type - @group.event_types.find do |t| - (params[:type].blank? && t == Event) || t.sti_name == params[:type] - end - end - end diff --git a/app/helpers/invoice/history.rb b/app/helpers/invoice/history.rb new file mode 100644 index 0000000000..4187c513c4 --- /dev/null +++ b/app/helpers/invoice/history.rb @@ -0,0 +1,95 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +class Invoice::History + attr_reader :template, :invoice + + delegate :content_tag, :l, :t, :concat, :number_to_currency, to: :template + + def initialize(template, invoice) + @template = template + @invoice = invoice + end + + def to_s + content_tag :table do + table_rows = [ + invoice_history_entry(invoice_issued_data, 'blue'), + invoice_history_entry(invoice_sent_data, 'blue') + ] + + table_rows << invoice_reminder_rows + table_rows << invoice_payment_rows + table_rows.compact.join.html_safe # rubocop:disable Rails/OutputSafety + end + end + + private + + def invoice_reminder_rows + if invoice.reminder_sent? + invoice.payment_reminders.collect.with_index do |reminder, count| + next unless reminder.persisted? + invoice_history_entry(reminder_sent_data(reminder, count + 1), 'red') + end + end + end + + def invoice_payment_rows + if invoice.payments.present? + invoice.payments.collect do |payment| + next unless payment.persisted? + invoice_history_entry(payment_data(payment), 'green') + end + end + end + + def invoice_history_entry(data, color) + return unless data + content_tag :tr do + data.collect do |d| + concat content_tag(:td, d, class: color) + end.to_s.html_safe # rubocop:disable Rails/OutputSafety + end + end + + def invoice_issued_data + if invoice.issued_at? + [ + '⬤', # Middle Dot + l(invoice.issued_at, format: :long), + t('invoices.issued') + ] + end + end + + def invoice_sent_data + if invoice.sent_at? + [ + '⬤', # Middle Dot + l(invoice.sent_at, format: :long), + t('invoices.sent') + ] + end + end + + def reminder_sent_data(reminder, count) + [ + '⬤', # Middle Dot + l(reminder.created_at.to_date, format: :long), + "#{count}. #{t('invoices.reminder_sent')}" + ] + end + + def payment_data(payment) + [ + '⬤', # Middle Dot + l(payment.received_at, format: :long), + "#{number_to_currency(payment.amount)} #{t('invoices.payd')}" + ] + end +end diff --git a/app/helpers/invoices_helper.rb b/app/helpers/invoices_helper.rb new file mode 100644 index 0000000000..b5203ab19a --- /dev/null +++ b/app/helpers/invoices_helper.rb @@ -0,0 +1,54 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +module InvoicesHelper + + def format_invoice_state(invoice) + type = case invoice.state + when /draft|cancelled/ then 'info' + when /sent/ then 'warning' + when /payed/ then 'success' + when /overdue|reminded/ then 'important' + end + badge(invoice.state_label, type) + end + + def invoice_due_since_options + [:one_day, :one_week, :one_month].collect do |key| + [key, I18n.t("invoices.filter.due_since_list.#{key}")] + end + end + + def invoices_export_dropdown + Dropdown::Invoices.new(self, params, :download).export + end + + def invoices_print_dropdown + Dropdown::Invoices.new(self, params, :print).print + end + + def invoice_sending_dropdown(path_meth) + Dropdown::InvoiceSending.new(self, params, path_meth) + end + + def invoice_history(invoice) + Invoice::History.new(self, invoice) + end + + def invoice_receiver_address(invoice) + return unless invoice.recipient_address + out = '' + recipient_address_lines = invoice.recipient_address.split(/\n/) + content_tag(:p) do + recipient_address_lines.collect do |l| + out << (l == recipient_address_lines.first ? "#{l}" : l) + '
' + end + out << mail_to(entry.recipient_email) + out.html_safe # rubocop:disable Rails/OutputSafety + end + end +end diff --git a/app/helpers/navigation_helper.rb b/app/helpers/navigation_helper.rb index fe273a855b..c917df0f17 100644 --- a/app/helpers/navigation_helper.rb +++ b/app/helpers/navigation_helper.rb @@ -10,7 +10,8 @@ module NavigationHelper MAIN = [ { label: :groups, url: :groups_path, - active_for: %w(groups people) }, + active_for: %w(groups people), + inactive_for: %w(invoices invoice_articles invoice_config) }, { label: :events, url: :list_events_path, @@ -25,7 +26,12 @@ module NavigationHelper { label: :admin, url: :label_formats_path, active_for: %w(label_formats custom_contents event_kinds qualification_kinds), - if: ->(_) { can?(:index, LabelFormat) } } + if: ->(_) { can?(:index, LabelFormat) } }, + + { label: :invoices, + url: :first_group_invoices_or_root_path, + if: ->(_) { current_user.finance_groups.any? }, + active_for: %w(invoices invoice_articles invoice_config) } ] @@ -34,20 +40,30 @@ def render_main_nav if !options.key?(:if) || instance_eval(&options[:if]) url = options[:url] url = send(url) if url.is_a?(Symbol) - nav(I18n.t("navigation.#{options[:label]}"), url, options[:active_for]) + nav(I18n.t("navigation.#{options[:label]}"), + url, + options[:active_for], + options[:inactive_for]) end end end + def first_group_invoices_or_root_path + return root_path if current_user.finance_groups.blank? + group_invoices_path(current_user.finance_groups.first) + end + # Create a list item for navigations. # If alternative_paths are given, and they appear in the request url, # the corresponding item is active. # If not alternative paths are given, the item is only active if the # link url equals the request url. - def nav(label, url, active_for = []) + def nav(label, url, active_for = [], inactive_for = []) options = {} if current_page?(url) || - active_for.any? { |p| request.path =~ %r{/?#{p}/?} } + Array(active_for).any? { |p| request.path =~ %r{/?#{p}/?} } && + Array(inactive_for).none? { |p| request.path =~ %r{/?#{p}/?} } + options[:class] = 'active' end content_tag(:li, link_to(label, url), options) diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb new file mode 100644 index 0000000000..362a49b822 --- /dev/null +++ b/app/helpers/notes_helper.rb @@ -0,0 +1,18 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Dachverband Schweizer Jugendparlamente. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +module NotesHelper + + def note_path(group, note) + if note.subject.is_a?(Group) + group_note_path(group_id: note.subject_id, id: note.id) + else + group_person_note_path(group_id: group.id, person_id: note.subject_id, id: note.id) + end + end + +end diff --git a/app/helpers/people_helper.rb b/app/helpers/people_helper.rb index c9a6acd9df..401d33be7c 100644 --- a/app/helpers/people_helper.rb +++ b/app/helpers/people_helper.rb @@ -11,8 +11,8 @@ def format_gender(person) person.gender_label end - def dropdown_people_export(details = false, emails = true) - Dropdown::PeopleExport.new(self, current_user, params, details, emails).to_s + def dropdown_people_export(details = false, emails = true, labels = true) + Dropdown::PeopleExport.new(self, current_user, params, details, emails, labels).to_s end def format_birthday(person) @@ -29,13 +29,15 @@ def format_tags(person) end end - def sortable_grouped_person_attr(t, sortable_attrs, grouping_attr = nil, &block) - list = sortable_attrs.map do |attr| - t.sort_header(attr.to_sym, Person.human_attribute_name(attr.to_sym)) + def sortable_grouped_person_attr(t, attrs, &block) + list = attrs.map do |attr, sortable| + if sortable + t.sort_header(attr.to_sym, Person.human_attribute_name(attr.to_sym)) + else + Person.human_attribute_name(attr.to_sym) + end end - list.unshift(Person.human_attribute_name(grouping_attr.to_sym)) if grouping_attr - header = list[0..-2].collect { |i| content_tag(:span, "#{i} |".html_safe, class: 'nowrap') } header << list.last t.col(safe_join(header, ' '), &block) @@ -50,4 +52,8 @@ def send_login_tooltip_text def person_link(person) person ? assoc_link(person) : "(#{t('global.nobody')})" end + + def format_person_layer_group(person) + person.layer_group_label + end end diff --git a/app/helpers/sheet/event.rb b/app/helpers/sheet/event.rb index 9ea442deb0..e3d844dd5d 100644 --- a/app/helpers/sheet/event.rb +++ b/app/helpers/sheet/event.rb @@ -9,6 +9,16 @@ module Sheet class Event < Base self.parent_sheet = Sheet::Group + class << self + + private + + def can_view_qualifications?(view, event) + view.can?(:qualify, event) || view.can?(:qualifications_read, event) + end + + end + tab 'global.tabs.info', :group_event_path, if: :show, @@ -31,7 +41,8 @@ class Event < Base tab 'activerecord.models.qualification.other', :group_event_qualifications_path, if: (lambda do |view, _group, event| - event.course_kind? && event.qualifying? && view.can?(:qualify, event) + event.course_kind? && event.qualifying? && + can_view_qualifications?(view, event) end) def link_url diff --git a/app/helpers/sheet/event/participation_contact_data.rb b/app/helpers/sheet/event/participation_contact_data.rb new file mode 100644 index 0000000000..7b9ef5096e --- /dev/null +++ b/app/helpers/sheet/event/participation_contact_data.rb @@ -0,0 +1,14 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Pfadibewegung Schweiz. This file is part of +# hitobito_youth and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito_youth. + +module Sheet + class Event + class ParticipationContactData < Base + self.parent_sheet = Sheet::Event + end + end +end diff --git a/app/helpers/sheet/group.rb b/app/helpers/sheet/group.rb index 051216bcc4..bc98c58ed0 100644 --- a/app/helpers/sheet/group.rb +++ b/app/helpers/sheet/group.rb @@ -45,9 +45,9 @@ class Group < Base view.can?(:index_person_add_requests, group) end) - tab 'activerecord.models.person/note.other', - :person_notes_group_path, - if: :index_person_notes + tab 'activerecord.models.note.other', + :group_notes_path, + if: :index_notes tab 'groups.tabs.deleted', :deleted_subgroups_group_path, diff --git a/app/helpers/sheet/group/deleted_people.rb b/app/helpers/sheet/group/deleted_people.rb new file mode 100644 index 0000000000..de52fc9252 --- /dev/null +++ b/app/helpers/sheet/group/deleted_people.rb @@ -0,0 +1,44 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2015, Pfadibewegung Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +module Sheet + class Group + class DeletedPeople < Group + + self.tabs = [] + + def title + I18n.t('groups.global.link.deleted_person') + end + + def active_tab + nil + end + + private + + def breadcrumbs? + true + end + + def breadcrumbs + entry.hierarchy.collect do |g| + link_to(g.to_s, group_path(g)) + end + end + + def model_name + 'group' + end + + def translation_prefix + 'sheet/group' + end + + end + end +end diff --git a/app/helpers/sheet/group/nav_left.rb b/app/helpers/sheet/group/nav_left.rb index ee9c3b1db4..adf18e6daf 100644 --- a/app/helpers/sheet/group/nav_left.rb +++ b/app/helpers/sheet/group/nav_left.rb @@ -22,7 +22,7 @@ def render render_upwards + render_header + content_tag(:ul, class: 'nav-left-list') do - render_layer_groups + render_sub_layers + render_layer_groups + render_deleted_people_link + render_sub_layers end end @@ -49,7 +49,8 @@ def render_upwards end def render_header - content_tag(:h3, class: "nav-left-title #{'active' if layer == entry}") do + active = layer == entry && view.request.path !~ /\/deleted_people$/ + content_tag(:h3, class: "nav-left-title #{'active' if active}") do link_to(layer, active_path(layer)) end end @@ -96,6 +97,16 @@ def group_link(group) active_path(group), title: group.to_s) end + def render_deleted_people_link + if view.can?(:index_deleted_people, layer) + active = view.current_page?(view.group_deleted_people_path(layer.id)) + content_tag(:li, class: "#{'active' if active}") do + link_to(view.t('groups.global.link.deleted_person'), + view.group_deleted_people_path(layer.id)) + end + end + end + def render_sub_layers safe_join(grouped_sub_layers) do |type, layers| content_tag(:li, content_tag(:span, type, class: 'divider')) + @@ -121,8 +132,8 @@ def sub_layers end def active_path(group) - renderer = sheet.active_tab.renderer(view, [group]) - if renderer.show? + renderer = sheet.active_tab.try(:renderer, view, [group]) + if renderer && renderer.show? renderer.path else view.group_path(group) diff --git a/app/helpers/sheet/invoice.rb b/app/helpers/sheet/invoice.rb new file mode 100644 index 0000000000..d41cc9b092 --- /dev/null +++ b/app/helpers/sheet/invoice.rb @@ -0,0 +1,24 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +module Sheet + class Invoice < Base + + def self.parent_sheet + nil + end + + def left_nav? + true + end + + def render_left_nav + view.render "invoices/nav_left" + end + + end +end diff --git a/app/helpers/sheet/invoice_article.rb b/app/helpers/sheet/invoice_article.rb new file mode 100644 index 0000000000..2db708b5d7 --- /dev/null +++ b/app/helpers/sheet/invoice_article.rb @@ -0,0 +1,12 @@ + +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +module Sheet + class InvoiceArticle < Sheet::Invoice + end +end diff --git a/app/helpers/sheet/invoice_config.rb b/app/helpers/sheet/invoice_config.rb new file mode 100644 index 0000000000..ac4737768d --- /dev/null +++ b/app/helpers/sheet/invoice_config.rb @@ -0,0 +1,11 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +module Sheet + class InvoiceConfig < Sheet::Invoice + end +end diff --git a/app/helpers/sheet/invoice_list.rb b/app/helpers/sheet/invoice_list.rb new file mode 100644 index 0000000000..12bb3df37d --- /dev/null +++ b/app/helpers/sheet/invoice_list.rb @@ -0,0 +1,11 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +module Sheet + class InvoiceList < Sheet::Invoice + end +end diff --git a/app/helpers/sheet/person.rb b/app/helpers/sheet/person.rb index e2070f075c..f3e248bdc2 100644 --- a/app/helpers/sheet/person.rb +++ b/app/helpers/sheet/person.rb @@ -22,6 +22,18 @@ class Person < Base :log_group_person_path, if: :log + tab 'people.tabs.colleagues', + :colleagues_group_person_path, + if: (lambda do |_view, _group, person| + person.company_name? + end) + + tab 'people.tabs.invoices', + :invoices_group_person_path, + if: (lambda do |view, group, person| + view.can?(:index_invoices, group) || view.can?(:index_invoices, person) + end) + def link_url view.group_person_path(parent_sheet.entry.id, entry.id) end diff --git a/app/helpers/sheet/person/colleague.rb b/app/helpers/sheet/person/colleague.rb new file mode 100644 index 0000000000..f6089d27af --- /dev/null +++ b/app/helpers/sheet/person/colleague.rb @@ -0,0 +1,16 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Dachverband Schweizer Jugendparlamente. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +module Sheet + class Person < Base + class Colleague < Base + + self.parent_sheet = Sheet::Person + + end + end +end diff --git a/app/helpers/sheet/person/invoice.rb b/app/helpers/sheet/person/invoice.rb new file mode 100644 index 0000000000..7b605f47af --- /dev/null +++ b/app/helpers/sheet/person/invoice.rb @@ -0,0 +1,16 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2015, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +module Sheet + class Person < Base + class Invoice < Base + + self.parent_sheet = Sheet::Person + + end + end +end diff --git a/app/helpers/standard_form_builder.rb b/app/helpers/standard_form_builder.rb index 33b4aab9be..77ba4b84f1 100644 --- a/app/helpers/standard_form_builder.rb +++ b/app/helpers/standard_form_builder.rb @@ -33,8 +33,7 @@ def labeled_input_fields(*attrs) # Render a corresponding input field for the given attribute. # The input field is chosen based on the ActiveRecord column type. # Use additional html_options for the input element. - # rubocop:disable Metrics/PerceivedComplexity - def input_field(attr, html_options = {}) + def input_field(attr, html_options = {}) # rubocop:disable Metrics/PerceivedComplexity type = column_type(@object, attr) custom_field_method = :"#{type}_field" if type == :text @@ -53,7 +52,6 @@ def input_field(attr, html_options = {}) text_field(attr, html_options) end end - # rubocop:enable Metrics/PerceivedComplexity # Render a password field def password_field(attr, html_options = {}) @@ -204,14 +202,12 @@ def belongs_to_field(attr, html_options = {}) end end - # rubocop:disable PredicateName - # Render a multi select element for a :has_many or :has_and_belongs_to_many # association defined by attr. # Use additional html_options for the select element. # To pass a custom element list, specify the list with the :list key or # define an instance variable with the pluralized name of the association. - def has_many_field(attr, html_options = {}) + def has_many_field(attr, html_options = {}) # rubocop:disable PredicateName html_options[:multiple] = true html_options[:class] ||= 'span6' add_css_class(html_options, 'multiselect') @@ -225,8 +221,6 @@ def i18n_enum_field(attr, labels, html_options = {}) html_options) end - # rubocop:enable PredicateName - def person_field(attr, _html_options = {}) attr, attr_id = assoc_and_id_attr(attr) hidden_field(attr_id) + @@ -407,6 +401,7 @@ def errors_on?(attr) # Returns true if the given attribute must be present. def required?(attr) + return true if dynamic_required?(attr) attr = attr.to_s attr, attr_id = assoc_and_id_attr(attr) validators = klass.validators_on(attr) + @@ -417,6 +412,11 @@ def required?(attr) end end + def dynamic_required?(attr) + return false unless @object.respond_to?(:required_attributes) + @object.required_attributes.include?(attr.to_s) + end + private def labeled_field_method?(name) diff --git a/app/helpers/standard_table_builder.rb b/app/helpers/standard_table_builder.rb index bc26df7ec4..cf993c4547 100644 --- a/app/helpers/standard_table_builder.rb +++ b/app/helpers/standard_table_builder.rb @@ -57,7 +57,9 @@ def attrs(*attrs) # contain the formatted attribute value for the current entry. def attr(a, header = nil) header ||= attr_header(a) - col(header, class: align_class(a)) { |e| format_attr(e, a) } + col(header, class: align_class(a)) do |e| + block_given? ? yield(e) : format_attr(e, a) + end end # Renders the table as HTML. @@ -100,7 +102,7 @@ def entry_class if entries.respond_to?(:klass) entries.klass elsif entries.respond_to?(:decorator_class) - entries.decorator_class.object_class + entries.decorator_class.try(:object_class) || entries.first.model.class else entries.first.class end diff --git a/app/helpers/version_helper.rb b/app/helpers/version_helper.rb new file mode 100644 index 0000000000..2a33caf7d6 --- /dev/null +++ b/app/helpers/version_helper.rb @@ -0,0 +1,23 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +module VersionHelper + + def app_version_changelog_link + if app_version + link_to app_version, changelog_path + end + end + + private + def app_version + app_version = Wagons.app_version.to_s + return unless app_version > '0.0' + ['Version', app_version, Hitobito::Application.build_info].compact.join(' ') + end + +end diff --git a/app/indices/event_index.rb b/app/indices/event_index.rb new file mode 100644 index 0000000000..fe53f10596 --- /dev/null +++ b/app/indices/event_index.rb @@ -0,0 +1,12 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Pfadibewegung Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +ThinkingSphinx::Index.define_partial :event do + indexes name, number, sortable: true + + indexes groups.name, as: :group_name +end diff --git a/app/jobs/event/cancel_application_job.rb b/app/jobs/event/cancel_application_job.rb new file mode 100644 index 0000000000..4555904f36 --- /dev/null +++ b/app/jobs/event/cancel_application_job.rb @@ -0,0 +1,29 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Pfadibewegung Schweiz. This file is part of +# hitobito_jubla and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito_jubla. + +class Event::CancelApplicationJob < BaseJob + + self.parameters = [:event_id, :person_id] + + def initialize(event, person) + @event_id = event.id + @person_id = person.id + end + + def perform + Event::ParticipationMailer.cancel(event, person).deliver_now + end + + def event + Event.find(@event_id) + end + + def person + Person.find(@person_id) + end + +end diff --git a/app/jobs/export/event_participations_export_job.rb b/app/jobs/export/event_participations_export_job.rb new file mode 100644 index 0000000000..40cc7fb888 --- /dev/null +++ b/app/jobs/export/event_participations_export_job.rb @@ -0,0 +1,49 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Pfadibewegung Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +class Export::EventParticipationsExportJob < Export::ExportBaseJob + + self.parameters = PARAMETERS + [:event_id, :controller_params] + + def initialize(format, user_id, event_id, controller_params) + super() + @format = format + @user_id = user_id + @tempfile_name = 'event-participations-export' + @event_id = event_id + @controller_params = controller_params + end + + private + + def send_mail(recipient, file, format) + Export::EventParticipationsExportMailer.completed(recipient, file, format).deliver_now + end + + def entries + @entries ||= Event::ParticipationFilter.new(Event.find(@event_id), + user, + @controller_params).list_entries + end + + def exporter + if full_export? + Export::Tabular::People::ParticipationsFull + else + Export::Tabular::People::ParticipationsAddress + end + end + + def full_export? + # This condition has to be in the job because it loads all entries + @controller_params[:details] && Ability.new(user).can?(:show_details, entries.first) + end + + def user + @user ||= Person.find(@user_id) + end +end diff --git a/app/jobs/export/events_export_job.rb b/app/jobs/export/events_export_job.rb new file mode 100644 index 0000000000..5f326d2091 --- /dev/null +++ b/app/jobs/export/events_export_job.rb @@ -0,0 +1,37 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Pfadibewegung Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +class Export::EventsExportJob < Export::ExportBaseJob + + self.parameters = PARAMETERS + [:event_type, :year, :parent] + + def initialize(format, user_id, event_type, year, parent) + super() + @format = format + @exporter = Export::Tabular::Events::List + @user_id = user_id + @tempfile_name = 'events-export' + @event_type = event_type + @year = year + @parent = parent + end + + private + + def send_mail(recipient, file, format) + Export::EventsExportMailer.completed(recipient, file, format).deliver_now + end + + def entries + @parent.events. + where(type: @event_type). + in_year(@year). + order_by_date. + preload_all_dates. + uniq + end +end diff --git a/app/jobs/export/export_base_job.rb b/app/jobs/export/export_base_job.rb new file mode 100644 index 0000000000..e3d1d9ffce --- /dev/null +++ b/app/jobs/export/export_base_job.rb @@ -0,0 +1,68 @@ +# encoding: utf-8 +# frozen_string_literal: true + +# Copyright (c) 2017, Pfadibewegung Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +require 'zip' + +class Export::ExportBaseJob < BaseJob + + PARAMETERS = [:format, :exporter, :user_id, :tempfile_name].freeze + + attr_reader :exporter + + def perform + set_locale + file, format = export_file_and_format + send_mail(recipient, file, format) + ensure + if file != export_file + file.close + file.unlink + end + export_file.close + export_file.unlink + end + + def send_mail + # override in sub class + end + + def entries + # override in sub class + end + + def recipient + @recipient ||= Person.find(@user_id) + end + + def export_file_and_format + return [export_file, @format] if export_file.size < 512.kilobyte + + # size reduction is by 70-80 % + zip = Tempfile.new("#{@tempfile_name}-zip", encoding: 'ascii-8bit') + Zip::OutputStream.open(zip.path) do |zos| + zos.put_next_entry "entry.#{@format}" + zos.write export_file.read + end + + [zip, :zip] + end + + def export_file + @export_file ||= begin + file = Tempfile.new("#{@tempfile}-export") + file << data + file.rewind # make subsequent read-calls start at the beginning + file + end + end + + def data + exporter.export(@format, entries) + end + +end diff --git a/app/jobs/export/people_export_job.rb b/app/jobs/export/people_export_job.rb new file mode 100644 index 0000000000..9d37d1260b --- /dev/null +++ b/app/jobs/export/people_export_job.rb @@ -0,0 +1,51 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Pfadibewegung Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +class Export::PeopleExportJob < Export::ExportBaseJob + + self.parameters = PARAMETERS + [:full, :person_filter] + + attr_reader :entries + + def initialize(format, full, user_id, person_filter) + super() + @format = format + @full = full + @exporter = exporter + @user_id = user_id + @tempfile_name = "people-#{format}-zip" + @person_filter = person_filter + end + + private + + def send_mail(recipient, file, format) + Export::PeopleExportMailer.completed(recipient, file, format).deliver_now + end + + def entries + entries = @person_filter.entries + if @full + full_entries(entries) + else + entries.preload_public_accounts.includes(:primary_group) + end + end + + def full_entries(entries) + entries + .select('people.*') + .preload_accounts + .includes(relations_to_tails: :tail, qualifications: { qualification_kind: :translations }) + .includes(:primary_group) + end + + def exporter + @full ? Export::Tabular::People::PeopleFull : Export::Tabular::People::PeopleAddress + end + +end diff --git a/app/jobs/export/subscriptions_job.rb b/app/jobs/export/subscriptions_job.rb new file mode 100644 index 0000000000..78391723ac --- /dev/null +++ b/app/jobs/export/subscriptions_job.rb @@ -0,0 +1,38 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Pfadibewegung Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +class Export::SubscriptionsJob < Export::ExportBaseJob + + self.parameters = PARAMETERS + [:mailing_list_id] + + def initialize(format, mailing_list_id, user_id) + super() + @mailing_list_id = mailing_list_id + @format = format + @exporter = Export::Tabular::People::PeopleAddress + @user_id = user_id + @tempfile_name = "subscriptions-#{mailing_list_id}-#{format}-zip" + end + + private + + def mailing_list + @mailing_list ||= MailingList.find(@mailing_list_id) + end + + def send_mail(recipient, file, format) + Export::SubscriptionsMailer.completed(recipient, mailing_list, file, format).deliver_now + end + + def entries + mailing_list.people.includes(:primary_group, :groups) + .order_by_name + .preload_public_accounts + .includes(roles: :group) + end + +end diff --git a/app/jobs/invoice/send_notification_job.rb b/app/jobs/invoice/send_notification_job.rb new file mode 100644 index 0000000000..4bf4019349 --- /dev/null +++ b/app/jobs/invoice/send_notification_job.rb @@ -0,0 +1,39 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +class Invoice::SendNotificationJob < BaseJob + + self.parameters = [:invoice_id, :sender_id, :locale] + + def initialize(invoice, sender) + super() + @invoice_id = invoice.id + @sender_id = sender.id + end + + def perform + set_locale + + pdf_options = { articles: true, esr: false } + + InvoiceMailer.notification( + invoice.recipient_name, + invoice.recipient_email, + sender, + invoice, + Export::Pdf::Invoice.render(invoice, pdf_options) + ).deliver_now + end + + def invoice + @invoice ||= Invoice.find(@invoice_id) + end + + def sender + @sender ||= Person.find(@sender_id) + end +end diff --git a/app/jobs/person/send_add_request_job.rb b/app/jobs/person/send_add_request_job.rb index 1538882e62..211c11cbf0 100644 --- a/app/jobs/person/send_add_request_job.rb +++ b/app/jobs/person/send_add_request_job.rb @@ -40,7 +40,7 @@ def ask_responsibles end def load_responsibles - Person::AddRequest::IgnoredApprover.approvers(person.primary_group.layer_group) + Person::AddRequest::IgnoredApprover.approvers(person_layer) end def clear_old_ignored_approvers @@ -55,4 +55,13 @@ def person request.person end + def person_layer + person.primary_group.try(:layer_group) || last_layer_group + end + + def last_layer_group + last_role = person.last_non_restricted_role + last_role && last_role.group.layer_group + end + end diff --git a/app/jobs/sphinx_index_job.rb b/app/jobs/sphinx_index_job.rb index 9d2d0e552a..79db647eb6 100644 --- a/app/jobs/sphinx_index_job.rb +++ b/app/jobs/sphinx_index_job.rb @@ -10,6 +10,28 @@ class SphinxIndexJob < RecurringJob run_every Settings.sphinx.index.interval.minutes def perform_internal - ThinkingSphinx::RakeInterface.new.index + if sphinx_local? + run_rebuild_task + end end + + private + + def sphinx_local? + Hitobito::Application.sphinx_local? + end + + def reschedule + sphinx_local? ? super : disable_job! + end + + def disable_job! + delayed_jobs.destroy_all + end + + def run_rebuild_task + Hitobito::Application.load_tasks + Rake::Task['ts:rebuild'].invoke + end + end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index f966ef097e..dcd8a02dca 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -1,17 +1,17 @@ # encoding: utf-8 -# Copyright (c) 2012-2014, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. class ApplicationMailer < ActionMailer::Base - HEADERS_TO_SANITIZE = [:to, :cc, :bcc, :from, :sender, :return_path, :reply_to] + HEADERS_TO_SANITIZE = [:to, :cc, :bcc, :from, :sender, :return_path, :reply_to].freeze def mail(headers = {}, &block) HEADERS_TO_SANITIZE.each do |h| - if headers.key?(h) + if headers.key?(h) && headers[h].present? headers[h] = IdnSanitizer.sanitize(headers[h]) end end @@ -20,15 +20,28 @@ def mail(headers = {}, &block) private + def compose(recipients, content_key) + values = values_for_placeholders(content_key) + custom_content_mail(recipients, content_key, values) + end + + # TODO: deprecate/remove values-parameter and call values_for_placeholders instead def custom_content_mail(recipients, content_key, values, headers = {}) content = CustomContent.get(content_key) headers[:to] = use_mailing_emails(recipients) - headers[:subject] ||= content.subject + headers[:subject] ||= content.subject_with_values(values) mail(headers) do |format| format.html { render text: content.body_with_values(values) } end end + def values_for_placeholders(content_key) + content = CustomContent.get(content_key) + content.placeholders_list.each_with_object({}) do |token, hash| + hash[token] = send(:"placeholder_#{token.underscore}") + end + end + def use_mailing_emails(recipients) if Array(recipients).first.is_a?(Person) Person.mailing_emails_for(recipients) diff --git a/app/mailers/event/participation_mailer.rb b/app/mailers/event/participation_mailer.rb index 330dbe4898..2381292a19 100644 --- a/app/mailers/event/participation_mailer.rb +++ b/app/mailers/event/participation_mailer.rb @@ -7,8 +7,9 @@ class Event::ParticipationMailer < ApplicationMailer - CONTENT_CONFIRMATION = 'event_application_confirmation' - CONTENT_APPROVAL = 'event_application_approval' + CONTENT_CONFIRMATION = 'event_application_confirmation'.freeze + CONTENT_APPROVAL = 'event_application_approval'.freeze + CONTENT_CANCEL = 'event_cancel_application'.freeze # Include all helpers that are required directly or indirectly (in decorators) helper :format, :layout, :auto_link_value @@ -21,30 +22,63 @@ def confirmation(participation) filename = Export::Pdf::Participation.filename(participation) attachments[filename] = Export::Pdf::Participation.render(participation) - compose(person, - CONTENT_CONFIRMATION, - 'recipient-name' => person.greeting_name) + compose(person, CONTENT_CONFIRMATION) end def approval(participation, recipients) @participation = participation + @recipients = recipients - compose(recipients, - CONTENT_APPROVAL, - 'participant-name' => person.to_s, - 'recipient-names' => recipients.collect(&:greeting_name).join(', ')) + compose(@recipients, CONTENT_APPROVAL) + end + + def cancel(event, person) + @event = event + @person = person + + custom_content_mail(@person, CONTENT_CANCEL, values_for_placeholders(CONTENT_CANCEL)) end private - def compose(recipients, content_key, values = {}) + def placeholder_recipient_name + person.greeting_name + end + + def placeholder_participant_name + person.to_s + end + + def placeholder_recipient_names + @recipients.collect(&:greeting_name).join(', ') + end + + def placeholder_event_details + if participation.nil? + event_without_participation + else + event_details + end + end + + def placeholder_application_url + link_to(participation_url) + end + + def compose(recipients, content_key, values = nil) # Assert the current mailer's view context is stored as Draper::ViewContext. # This is done in the #view_context method overriden by Draper. # Otherwise, decorators will not have access to all helper methods. view_context - values['event-details'] = event_details - values['application-url'] = link_to(participation_url) + values = if values + values.merge( + 'event-details' => event_details, + 'application-url' => link_to(participation_url) + ) + else + values_for_placeholders(content_key) + end custom_content_mail(recipients, content_key, values) end @@ -62,7 +96,7 @@ def event_details infos << labeled(:cost) infos << labeled(:description) { event.description.gsub("\n", '
') } infos << labeled(:location) { event.location.gsub("\n", '
') } - infos << labeled(:contact) { "#{event.contact}
#{event.contact.email}" } + infos << labeled(:contact) { "#{event.contact}
#{event.contact.email}" } infos << answers_details infos << additional_information_details infos << participation_details @@ -70,6 +104,13 @@ def event_details end # rubocop:enable MethodLength, Metrics/AbcSize + def event_without_participation + infos = [] + infos << event.name + infos << labeled(:dates) { event.dates.map(&:to_s).join('
') } + infos.compact.join('

') + end + def labeled(key) value = event.send(key).presence if value @@ -80,15 +121,23 @@ def labeled(key) end def answers_details - if participation.answers.present? + answers = load_application_answers + if answers.present? text = ["#{Event::Participation.human_attribute_name(:answers)}:"] - participation.answers.each do |a| + answers.each do |a| text << "#{a.question.question}: #{a.answer}" end text.join('
') end end + def load_application_answers + participation.answers + .joins(:question) + .includes(:question) + .where(event_questions: { admin: false }) + end + def additional_information_details if participation.additional_information? t('activerecord.attributes.event/participation.additional_information') + @@ -103,11 +152,11 @@ def participation_details end def person - participation.person + @person ||= participation.person end def event - participation.event + @event ||= participation.event end end diff --git a/app/mailers/event/register_mailer.rb b/app/mailers/event/register_mailer.rb index edd424e511..a2f8d05960 100644 --- a/app/mailers/event/register_mailer.rb +++ b/app/mailers/event/register_mailer.rb @@ -7,22 +7,34 @@ class Event::RegisterMailer < ApplicationMailer - CONTENT_REGISTER_LOGIN = 'event_register_login' + CONTENT_REGISTER_LOGIN = 'event_register_login'.freeze def register_login(recipient, group, event, token) + @recipient = recipient + @group = group + @event = event + @token = token # This email contains sensitive information and thus # is only sent to the main email address. - values = register_login_values(recipient, group, event, token) - custom_content_mail(recipient.email, CONTENT_REGISTER_LOGIN, values) + custom_content_mail( + recipient.email, + CONTENT_REGISTER_LOGIN, + values_for_placeholders(CONTENT_REGISTER_LOGIN) + ) end private - def register_login_values(recipient, group, event, token) - url = event_url(group, event, token) - { 'recipient-name' => recipient.greeting_name, - 'event-name' => event.to_s, - 'event-url' => link_to(url) } + def placeholder_recipient_name + @recipient.greeting_name + end + + def placeholder_event_name + @event.to_s + end + + def placeholder_event_url + link_to(event_url(@group, @event, @token)) end def event_url(group, event, token) diff --git a/app/mailers/export/event_participations_export_mailer.rb b/app/mailers/export/event_participations_export_mailer.rb new file mode 100644 index 0000000000..957ae23615 --- /dev/null +++ b/app/mailers/export/event_participations_export_mailer.rb @@ -0,0 +1,27 @@ +# encoding: utf-8 +# frozen_string_literal: true + +# Copyright (c) 2017, Pfadibewegung Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +class Export::EventParticipationsExportMailer < ApplicationMailer + + CONTENT_EVENT_PARTICIPATIONS_EXPORT = 'content_event_participations_export'.freeze + + def completed(recipient, export_file, export_format) + @recipient = recipient + @export_file = export_file + @export_format = export_format + + attachments["event_participations_export.#{export_format}"] = export_file.read + compose(recipient, CONTENT_EVENT_PARTICIPATIONS_EXPORT) + end + + private + + def placeholder_recipient_name + @recipient.greeting_name + end +end diff --git a/app/mailers/export/events_export_mailer.rb b/app/mailers/export/events_export_mailer.rb new file mode 100644 index 0000000000..a3399ab31d --- /dev/null +++ b/app/mailers/export/events_export_mailer.rb @@ -0,0 +1,27 @@ +# encoding: utf-8 +# frozen_string_literal: true + +# Copyright (c) 2017, Pfadibewegung Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +class Export::EventsExportMailer < ApplicationMailer + + CONTENT_EVENTS_EXPORT = 'content_events_export'.freeze + + def completed(recipient, export_file, export_format) + @recipient = recipient + @export_file = export_file + @export_format = export_format + + attachments["events_export.#{export_format}"] = export_file.read + compose(recipient, CONTENT_EVENTS_EXPORT) + end + + private + + def placeholder_recipient_name + @recipient.greeting_name + end +end diff --git a/app/mailers/export/people_export_mailer.rb b/app/mailers/export/people_export_mailer.rb new file mode 100644 index 0000000000..eb8625d663 --- /dev/null +++ b/app/mailers/export/people_export_mailer.rb @@ -0,0 +1,27 @@ +# encoding: utf-8 +# frozen_string_literal: true + +# Copyright (c) 2017, Pfadibewegung Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +class Export::PeopleExportMailer < ApplicationMailer + + CONTENT_PEOPLE_EXPORT = 'content_people_export'.freeze + + def completed(recipient, export_file, export_format) + @recipient = recipient + @export_file = export_file + @export_format = export_format + + attachments["people_export.#{export_format}"] = export_file.read + compose(recipient, CONTENT_PEOPLE_EXPORT) + end + + private + + def placeholder_recipient_name + @recipient.greeting_name + end +end diff --git a/app/mailers/export/subscriptions_mailer.rb b/app/mailers/export/subscriptions_mailer.rb new file mode 100644 index 0000000000..c9ffe3e128 --- /dev/null +++ b/app/mailers/export/subscriptions_mailer.rb @@ -0,0 +1,33 @@ +# encoding: utf-8 +# frozen_string_literal: true + +# Copyright (c) 2017, Pfadibewegung Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +class Export::SubscriptionsMailer < ApplicationMailer + + CONTENT_SUBSCRIPTIONS_EXPORT = 'content_subscriptions_export'.freeze + + def completed(recipient, mailing_list, export_file, export_format) + @recipient = recipient + @mailing_list = mailing_list + @export_file = export_file + @export_format = export_format + + attachments["subscriptions.#{export_format}"] = export_file.read + compose(recipient, CONTENT_SUBSCRIPTIONS_EXPORT) + end + + private + + def placeholder_recipient_name + @recipient.greeting_name + end + + def placeholder_mailing_list_name + @mailing_list.name + end + +end diff --git a/app/mailers/invoice_mailer.rb b/app/mailers/invoice_mailer.rb new file mode 100644 index 0000000000..b708d01b37 --- /dev/null +++ b/app/mailers/invoice_mailer.rb @@ -0,0 +1,70 @@ +# encoding: utf-8 +# frozen_string_literal: true + +# Copyright (c) 2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +class InvoiceMailer < ApplicationMailer + + CONTENT_INVOICE_NOTIFICATION = 'content_invoice_notification'.freeze + + def notification(recipient_name, recipient_mail, sender, invoice, invoice_file) + @recipient_name = recipient_name + @recipient_mail = recipient_mail + @sender = sender + @invoice = InvoiceDecorator.decorate(invoice) + + attachments[invoice.filename] = invoice_file + + custom_content_mail(@recipient_mail, CONTENT_INVOICE_NOTIFICATION, + values_for_placeholders(CONTENT_INVOICE_NOTIFICATION), + with_personal_sender(@sender)) + end + + private + + def placeholder_recipient_name + @recipient_name + end + + def placeholder_invoice_number + @invoice.sequence_number + end + + def placeholder_invoice_items + InvoiceItemDecorator.decorate_collection(@invoice.invoice_items).map do |item| + [ + item.name, + item.description, + item.total + ].join('
') + end.join('
' * 2) + end + + def placeholder_invoice_total + content_tag :table do + [:total, :vat].map do |key| + content_tag :tr do + [content_tag(:th, t("activerecord.attributes.invoice.#{key}")), + content_tag(:td, @invoice.send(key))].join + end + end.join + end + end + + def placeholder_group_name + @sender.primary_group.name + end + + def placeholder_payment_information + @invoice.invoice_config.payment_information + end + + def content_tag(name, content = nil) + content = yield if block_given? + "<#{name}>#{content}" + end + +end diff --git a/app/mailers/person/add_request_mailer.rb b/app/mailers/person/add_request_mailer.rb index ce92afb3f1..70fab51aab 100644 --- a/app/mailers/person/add_request_mailer.rb +++ b/app/mailers/person/add_request_mailer.rb @@ -7,75 +7,92 @@ class Person::AddRequestMailer < ApplicationMailer - CONTENT_ADD_REQUEST_PERSON = 'person_add_request_person' - CONTENT_ADD_REQUEST_RESPONSIBLES = 'person_add_request_responsibles' - CONTENT_ADD_REQUEST_APPROVED = 'person_add_request_approved' - CONTENT_ADD_REQUEST_REJECTED = 'person_add_request_rejected' + CONTENT_ADD_REQUEST_PERSON = 'person_add_request_person'.freeze + CONTENT_ADD_REQUEST_RESPONSIBLES = 'person_add_request_responsibles'.freeze + CONTENT_ADD_REQUEST_APPROVED = 'person_add_request_approved'.freeze + CONTENT_ADD_REQUEST_REJECTED = 'person_add_request_rejected'.freeze attr_reader :add_request delegate :body, :person, :requester, to: :add_request def ask_person_to_add(add_request) - @add_request = add_request - values = person_mail_values - compose(person, CONTENT_ADD_REQUEST_PERSON, values, requester) + @add_request = add_request + @answer_request_url = link_to_request + @recipient = person + compose(person, CONTENT_ADD_REQUEST_PERSON, requester) end def ask_responsibles(add_request, responsibles) - @add_request = add_request - recipient_names = responsibles.collect(&:greeting_name).join(', ') - values = responsible_mail_values(recipient_names) - compose(responsibles, CONTENT_ADD_REQUEST_RESPONSIBLES, values, requester) + @add_request = add_request + @answer_request_url = link_to_add_requests + @recipients = responsibles + compose(responsibles, CONTENT_ADD_REQUEST_RESPONSIBLES, requester) end def approved(person, body, requester, user) @add_request = body.person_add_requests.build(person: person, requester: requester) - values = approved_mail_values(user) - compose(requester, CONTENT_ADD_REQUEST_APPROVED, values, user) + @recipient = requester + @user = user + compose(requester, CONTENT_ADD_REQUEST_APPROVED, user) end def rejected(person, body, requester, user) @add_request = body.person_add_requests.build(person: person, requester: requester) - values = rejected_mail_values(user) - compose(requester, CONTENT_ADD_REQUEST_REJECTED, values, user) + @recipient = requester + @user = user + compose(requester, CONTENT_ADD_REQUEST_REJECTED, user) end private - def compose(recipients, content_key, values, sender) - values['request-body'] = link_to(add_request.body_label, body_url) + def compose(recipients, content_key, sender) + values = values_for_placeholders(content_key) custom_content_mail(recipients, content_key, values, with_personal_sender(sender)) end + def placeholder_request_body + link_to(add_request.body_label, body_url) + end + + def placeholder_recipient_name + @recipient.greeting_name + end + + def placeholder_requester_name + requester.full_name + end + + def placeholder_requester_roles + roles_as_string(add_request.requester_full_roles) + end + + def placeholder_answer_request_url + @answer_request_url + end + + def placeholder_recipient_names + @recipients.collect(&:greeting_name).join(', ') + end + + def placeholder_person_name + person.full_name + end - def person_mail_values - { 'recipient-name' => person.greeting_name, - 'requester-name' => requester.full_name, - 'requester-roles' => roles_as_string(add_request.requester_full_roles), - 'answer-request-url' => link_to_request } + def placeholder_approver_name + @user.full_name end - def responsible_mail_values(recipient_names) - { 'recipient-names' => recipient_names, - 'person-name' => person.full_name, - 'requester-name' => requester.full_name, - 'requester-roles' => roles_as_string(add_request.requester_full_roles), - 'answer-request-url' => link_to_add_requests } + def placeholder_rejecter_name + @user.full_name end - def approved_mail_values(user) - { 'recipient-name' => requester.greeting_name, - 'person-name' => person.full_name, - 'approver-name' => user.full_name, - 'approver-roles' => roles_as_string(layer_full_roles(user)) } + def placeholder_approver_roles + roles_as_string(layer_full_roles(@user)) end - def rejected_mail_values(user) - { 'recipient-name' => requester.greeting_name, - 'person-name' => person.full_name, - 'rejecter-name' => user.full_name, - 'rejecter-roles' => roles_as_string(layer_full_roles(user)) } + def placeholder_rejecter_roles + roles_as_string(layer_full_roles(@user)) end def roles_as_string(roles) @@ -85,7 +102,7 @@ def roles_as_string(roles) def layer_full_roles(person) person.roles.includes(:group).select do |r| r.group.layer_group_id == add_request.person_layer.try(:id) && - (r.class.permissions & [:layer_and_below_full, :layer_full]).present? + (r.class.permissions & [:layer_and_below_full, :layer_full]).present? end end @@ -107,7 +124,7 @@ def body_url when Group then group_url(id: body.id) when Event then group_event_url(group_id: body.groups.first.id, id: body.id) when MailingList then group_mailing_list_url(group_id: body.group_id, id: body.id) - else fail(ArgumentError, "Unknown body type #{body.class}") + else raise ArgumentError, "Unknown body type #{body.class}" end end diff --git a/app/mailers/person/login_mailer.rb b/app/mailers/person/login_mailer.rb index c8620b7c9d..09668d28ae 100644 --- a/app/mailers/person/login_mailer.rb +++ b/app/mailers/person/login_mailer.rb @@ -7,10 +7,14 @@ class Person::LoginMailer < ApplicationMailer - CONTENT_LOGIN = 'send_login' + CONTENT_LOGIN = 'send_login'.freeze def login(recipient, sender, token) - values = content_values(recipient, sender, token) + @recipient = recipient + @sender = sender + @token = token + + values = values_for_placeholders(CONTENT_LOGIN) # This email contains sensitive information and thus # is only sent to the main email address. @@ -19,11 +23,16 @@ def login(recipient, sender, token) private - def content_values(recipient, sender, token) - url = login_url(token) - { 'recipient-name' => recipient.greeting_name, - 'sender-name' => sender.to_s, - 'login-url' => link_to(url) } + def placeholder_recipient_name + @recipient.greeting_name + end + + def placeholder_sender_name + @sender.to_s + end + + def placeholder_login_url + link_to(login_url(@token)) end def login_url(token) diff --git a/app/models/cantons.rb b/app/models/cantons.rb index 5a573b970e..a11ed9628b 100644 --- a/app/models/cantons.rb +++ b/app/models/cantons.rb @@ -1,9 +1,9 @@ # encoding: utf-8 # Copyright (c) 2012-2014, insieme Schweiz. This file is part of -# hitobito_insieme and licensed under the Affero General Public License version 3 +# hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito_insieme. +# https://github.com/hitobito/hitobito. module Cantons diff --git a/app/models/concerns/categorized_tags.rb b/app/models/concerns/categorized_tags.rb index 59e628c274..27a4adb60a 100644 --- a/app/models/concerns/categorized_tags.rb +++ b/app/models/concerns/categorized_tags.rb @@ -1,9 +1,9 @@ # encoding: utf-8 # Copyright (c) 2012-2016, Dachverband Schweizer Jugendparlamente. This file is part of -# hitobito_dsj and licensed under the Affero General Public License version 3 +# hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito_dsj. +# https://github.com/hitobito/hitobito. module CategorizedTags diff --git a/app/models/countries.rb b/app/models/countries.rb index ee1ba3c500..01239ab365 100644 --- a/app/models/countries.rb +++ b/app/models/countries.rb @@ -1,9 +1,9 @@ # encoding: utf-8 # Copyright (c) 2012-2015, insieme Schweiz. This file is part of -# hitobito_insieme and licensed under the Affero General Public License version 3 +# hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito_insieme. +# https://github.com/hitobito/hitobito. module Countries diff --git a/app/models/custom_content.rb b/app/models/custom_content.rb index 4687b55432..df6a305163 100644 --- a/app/models/custom_content.rb +++ b/app/models/custom_content.rb @@ -1,6 +1,6 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. @@ -29,7 +29,7 @@ class CustomContent < ActiveRecord::Base class << self def get(key) - find_by_key!(key) + find_by!(key: key) end end @@ -53,10 +53,20 @@ def placeholder_token(key) "{#{key}}" end + def subject_with_values(placeholders = {}) + replace_placeholders(subject.dup, placeholders) + end + def body_with_values(placeholders = {}) + replace_placeholders(body.dup, placeholders) + end + + private + + def replace_placeholders(string, placeholders) check_placeholders_exist(placeholders) - placeholders_list.each_with_object(body.dup) do |placeholder, output| + placeholders_list.each_with_object(string) do |placeholder, output| token = placeholder_token(placeholder) if output.include?(token) output.gsub!(token, placeholders.fetch(placeholder)) @@ -64,15 +74,13 @@ def body_with_values(placeholders = {}) end end - private - def as_list(placeholders) placeholders.to_s.split(',').collect(&:strip) end def assert_required_placeholders_are_used placeholders_required_list.each do |placeholder| - unless body.to_s.include?(placeholder_token(placeholder)) + unless [subject, body].any? { |str| str.to_s.include?(placeholder_token(placeholder)) } errors.add(:body, :placeholder_missing, placeholder: placeholder_token(placeholder)) end end @@ -81,9 +89,9 @@ def assert_required_placeholders_are_used def check_placeholders_exist(placeholders) non_existing = (placeholders.keys - placeholders_list).presence if non_existing - fail(ArgumentError, - "Placeholder(s) #{non_existing.join(', ')} given, " \ - 'but not defined for this custom content') + raise(ArgumentError, + "Placeholder(s) #{non_existing.join(', ')} given, " \ + 'but not defined for this custom content') end end end diff --git a/app/models/event.rb b/app/models/event.rb index db27322c94..87b90655a9 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -1,6 +1,6 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. @@ -37,9 +37,10 @@ # signature_confirmation_text :string # creator_id :integer # updater_id :integer +# applications_cancelable :boolean default(FALSE), not null # -class Event < ActiveRecord::Base +class Event < ActiveRecord::Base # rubocop:disable Metrics/ClassLength: # This statement is required because these classes would not be loaded correctly otherwise. # The price we pay for using classes as namespace. @@ -64,12 +65,15 @@ class Event < ActiveRecord::Base self.used_attributes = [:name, :motto, :cost, :maximum_participants, :contact_id, :description, :location, :application_opening_at, :application_closing_at, :application_conditions, - :external_applications] + :external_applications, :applications_cancelable, + :signature, :signature_confirmation, :signature_confirmation_text, + :required_contact_attrs, :hidden_contact_attrs] # All participation roles that exist for this event self.role_types = [Event::Role::Leader, Event::Role::AssistantLeader, Event::Role::Cook, + Event::Role::Helper, Event::Role::Treasurer, Event::Role::Speaker, Event::Role::Participant] @@ -83,6 +87,7 @@ class Event < ActiveRecord::Base # The class used for the kind_id self.kind_class = nil + model_stamper stampable stamper_class_name: :person, deleter: false @@ -98,6 +103,9 @@ class Event < ActiveRecord::Base has_many :dates, -> { order(:start_at) }, dependent: :destroy, validate: true has_many :questions, dependent: :destroy, validate: true + has_many :application_questions, -> { where(admin: false) }, class_name: 'Event::Question' + has_many :admin_questions, -> { where(admin: true) }, class_name: 'Event::Question' + has_many :participations, dependent: :destroy has_many :people, through: :participations @@ -120,14 +128,20 @@ class Event < ActiveRecord::Base length: { allow_nil: true, maximum: 2**16 - 1 } validate :assert_type_is_allowed_for_groups validate :assert_application_closing_is_after_opening - + validate :assert_required_contact_attrs_valid + validate :assert_hidden_contact_attrs_valid ### CALLBACKS before_validation :set_self_in_nested + before_validation :set_signature, if: :signature_confirmation? + accepts_nested_attributes_for :dates, :application_questions, :admin_questions, + allow_destroy: true - accepts_nested_attributes_for :dates, :questions, allow_destroy: true + ### SERIALIZED ATTRIBUTES + serialize :required_contact_attrs, Array + serialize :hidden_contact_attrs, Array ### CLASS METHODS @@ -136,9 +150,9 @@ class << self # Default scope for event lists def list order_by_date. - order(:name). - preload_all_dates. - uniq + order(:name). + preload_all_dates. + uniq end def preload_all_dates @@ -171,15 +185,15 @@ def with_group_id(group_ids) def upcoming midnight = Time.zone.now.midnight joins(:dates). - where('event_dates.start_at >= ? OR event_dates.finish_at >= ?', midnight, midnight) + where('event_dates.start_at >= ? OR event_dates.finish_at >= ?', midnight, midnight) end # Events that are open for applications. def application_possible today = Time.zone.today where('events.application_opening_at IS NULL OR events.application_opening_at <= ?', today). - where('events.application_closing_at IS NULL OR events.application_closing_at >= ?', today). - where('events.maximum_participants IS NULL OR events.maximum_participants <= 0 OR ' \ + where('events.application_closing_at IS NULL OR events.application_closing_at >= ?', today). + where('events.maximum_participants IS NULL OR events.maximum_participants <= 0 OR ' \ 'events.participant_count < events.maximum_participants') end @@ -207,7 +221,7 @@ def all_types # Return the event type with the given sti_name or raise an exception if not found def find_event_type!(sti_name) type = all_types.detect { |t| t.sti_name == sti_name } - fail ActiveRecord::RecordNotFound, "No event type '#{sti_name}' found" if type.nil? + raise ActiveRecord::RecordNotFound, "No event type '#{sti_name}' found" if type.nil? type end @@ -218,7 +232,7 @@ def participant_types # Return the role type with the given sti_name or raise an exception if not found def find_role_type!(sti_name) type = role_types.detect { |t| t.sti_name == sti_name } - fail ActiveRecord::RecordNotFound, "No role '#{sti_name}' found" if type.nil? + raise ActiveRecord::RecordNotFound, "No role '#{sti_name}' found" if type.nil? type end end @@ -226,6 +240,8 @@ def find_role_type!(sti_name) ### INSTANCE METHODS + delegate :participant_types, :find_role_type!, to: :singleton_class + def to_s(_format = :default) name end @@ -257,16 +273,43 @@ def course_kind? kind_class == Event::Kind && kind.present? end + def duplicate # rubocop:disable Metrics/MethodLength splitting this up does not make it better + dup.tap do |event| + event.groups = groups + event.state = nil + event.application_opening_at = nil + event.application_closing_at = nil + event.participant_count = 0 + event.applicant_count = 0 + event.teamer_count = 0 + application_questions.each do |q| + event.application_questions << q.dup + end + admin_questions.each do |q| + event.admin_questions << q.dup + end + end + end + + # Overwrite to handle improper characters + def save(*args) + super + rescue ActiveRecord::StatementInvalid => e + raise e unless e.original_exception.message =~ /Incorrect string value/ + errors.add(:base, :emoji_suspected) + false + end + private def assert_type_is_allowed_for_groups - if groups.present? - master = groups.first - if groups.any? { |g| g.class != master.class } - errors.add(:group_ids, :must_have_same_type) - elsif type && !master.class.event_types.collect(&:sti_name).include?(type) - errors.add(:type, :type_not_allowed) - end + master = groups.try(:first) + return unless master + + if groups.any? { |g| g.class != master.class } + errors.add(:group_ids, :must_have_same_type) + elsif type && !master.class.event_types.collect(&:sti_name).include?(type) + errors.add(:type, :type_not_allowed) end end @@ -278,7 +321,43 @@ def assert_application_closing_is_after_opening def set_self_in_nested # don't try to set self in frozen nested attributes (-> marked for destroy) - (dates + questions).each { |e| e.event = self unless e.frozen? } + (dates + application_questions + admin_questions).each do |e| + e.event = self unless e.frozen? + end + end + + def valid_contact_attr?(attr) + (ParticipationContactData.contact_attrs + + ParticipationContactData.contact_associations). + map(&:to_s).include?(attr.to_s) + end + + def assert_required_contact_attrs_valid + required_contact_attrs.map(&:to_s).each do |a| + unless valid_contact_attr?(a) && + ParticipationContactData.contact_associations. + map(&:to_s).exclude?(a) + errors.add(:base, :contact_attr_invalid, attribute: a) + end + if hidden_contact_attrs.include?(a) + errors.add(:base, :contact_attr_hidden_required, attribute: a) + end + end + end + + def assert_hidden_contact_attrs_valid + hidden_contact_attrs.map(&:to_sym).each do |a| + unless valid_contact_attr?(a) + errors.add(:base, :contact_attr_invalid, attribute: a) + end + if ParticipationContactData.mandatory_contact_attrs.include?(a) + errors.add(:base, :contact_attr_mandatory, attribute: a) + end + end + end + + def set_signature + self.signature = true end end diff --git a/app/models/event/answer.rb b/app/models/event/answer.rb index d3655927b1..6b1bf52181 100644 --- a/app/models/event/answer.rb +++ b/app/models/event/answer.rb @@ -18,6 +18,8 @@ class Event::Answer < ActiveRecord::Base attr_writer :answer_required + delegate :admin?, to: :question + belongs_to :participation belongs_to :question @@ -25,7 +27,7 @@ class Event::Answer < ActiveRecord::Base validates_by_schema validates :question_id, uniqueness: { scope: :participation_id } validates :answer, presence: { if: lambda do - question.required? && participation.enforce_required_answers + question && question.required? && participation.enforce_required_answers end } validate :assert_answer_is_in_choice_items diff --git a/app/models/event/attachment_uploader.rb b/app/models/event/attachment_uploader.rb index 8648a519a5..41c32e16c4 100644 --- a/app/models/event/attachment_uploader.rb +++ b/app/models/event/attachment_uploader.rb @@ -5,29 +5,8 @@ # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. -class Event::AttachmentUploader < CarrierWave::Uploader::Base +class Event::AttachmentUploader < Uploader::Base - EXTENSION_WHITE_LIST = Settings.event.attachments.file_extensions.split(/\s+/) - - # Choose what kind of storage to use for this uploader: - storage :file - - class << self - def accept_extensions - EXTENSION_WHITE_LIST.collect { |e| ".#{e}" }.join(', ') - end - end - - # Override the directory where uploaded files will be stored. - # This is a sensible default for uploaders that are meant to be mounted: - def store_dir - "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" - end - - # Add a white list of extensions which are allowed to be uploaded. - # For images you might use something like this: - def extension_white_list - EXTENSION_WHITE_LIST - end + self.allowed_extensions = Settings.event.attachments.file_extensions.split(/\s+/) end diff --git a/app/models/event/course.rb b/app/models/event/course.rb index 13abd83997..1834b12e4c 100644 --- a/app/models/event/course.rb +++ b/app/models/event/course.rb @@ -37,6 +37,7 @@ # signature_confirmation_text :string # creator_id :integer # updater_id :integer +# applications_cancelable :boolean default(FALSE), not null # class Event::Course < Event @@ -44,12 +45,13 @@ class Event::Course < Event # This statement is required because this class would not be loaded otherwise. require_dependency 'event/course/role/participant' - self.used_attributes += [:number, :kind_id, :state, :priorization, :group_ids, :requires_approval, - :signature, :signature_confirmation, :signature_confirmation_text] + self.used_attributes += [:number, :kind_id, :state, :priorization, :group_ids, + :requires_approval, :display_booking_info] self.role_types = [Event::Role::Leader, Event::Role::AssistantLeader, Event::Role::Cook, + Event::Role::Helper, Event::Role::Treasurer, Event::Role::Speaker, Event::Course::Role::Participant] @@ -63,8 +65,6 @@ class Event::Course < Event validates :kind_id, presence: true, if: -> { used_attributes.include?(:kind_id) } - before_validation :set_signature, if: :signature_confirmation? - def label_detail label = used_attributes.include?(:kind_id) ? "#{kind.short_name} " : '' @@ -89,17 +89,11 @@ def start_date end def init_questions - if questions.blank? - Event::Question.global.each do |q| - questions << q.dup + if application_questions.blank? + Event::Question.application.global.each do |q| + application_questions << q.dup end end end - private - - def set_signature - self[:signature] = true - end - end diff --git a/app/models/event/kind.rb b/app/models/event/kind.rb index 960b6a2734..611da3c9cf 100644 --- a/app/models/event/kind.rb +++ b/app/models/event/kind.rb @@ -58,11 +58,16 @@ def qualifying? end def qualification_kinds(category, role) - QualificationKind.includes(:translations). - joins(:event_kind_qualification_kinds). - where(event_kind_qualification_kinds: { event_kind_id: id, - category: category, - role: role }) + QualificationKind. + includes(:translations). + joins(:event_kind_qualification_kinds). + where(event_kind_qualification_kinds: { event_kind_id: id, + category: category, + role: role }) + end + + def grouped_qualification_kind_ids(category, role) + event_kind_qualification_kinds.grouped_qualification_kind_ids(category, role) end # Soft destroy if events exist, otherwise hard destroy diff --git a/app/models/event/kind_qualification_kind.rb b/app/models/event/kind_qualification_kind.rb index de5a6cd113..079196bbdf 100644 --- a/app/models/event/kind_qualification_kind.rb +++ b/app/models/event/kind_qualification_kind.rb @@ -13,12 +13,24 @@ # qualification_kind_id :integer not null # category :string not null # role :string not null +# grouping :integer # class Event::KindQualificationKind < ActiveRecord::Base - CATEGORIES = %w(qualification precondition prolongation) - ROLES = %w(participant leader) + CATEGORIES = %w(qualification precondition prolongation).freeze + ROLES = %w(participant leader).freeze + + class << self + + def grouped_qualification_kind_ids(category, role) + where(category: category, role: role). + pluck(:grouping, :qualification_kind_id). + group_by(&:first). + map { |_, v| v.map(&:last) } + end + + end ### ASSOCIATIONS diff --git a/app/models/event/participatable.rb b/app/models/event/participatable.rb index 1fdbc3ea77..f572e92453 100644 --- a/app/models/event/participatable.rb +++ b/app/models/event/participatable.rb @@ -19,7 +19,7 @@ def participations_for(*role_types) where(event_roles: { type: role_types.map(&:sti_name) }). includes(:person). references(:person). - order_by_role(self.class). + order_by_role(self). merge(Person.order_by_name). uniq end @@ -44,10 +44,6 @@ def participation_role_labels pluck(:label) end - def participant_types - self.class.participant_types - end - private # All members of the leading team (non-participants) diff --git a/app/models/event/participation.rb b/app/models/event/participation.rb index e4f7692d00..24a60aba17 100644 --- a/app/models/event/participation.rb +++ b/app/models/event/participation.rb @@ -94,12 +94,14 @@ def upcoming ### INSTANCE METHODS def init_answers - return if answers.present? - - event.questions.each do |q| - a = q.answers.new - a.question = q # without this, only the id is set - answers << a + answers.tap do |list| + event.questions.each do |q| + unless list.find { |a| a.question_id == q.id } + a = q.answers.new + a.question = q # without this, only the id is set + list << a + end + end end end diff --git a/app/models/event/participation_contact_data.rb b/app/models/event/participation_contact_data.rb new file mode 100644 index 0000000000..1a45b7201d --- /dev/null +++ b/app/models/event/participation_contact_data.rb @@ -0,0 +1,143 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Pfadibewegung Schweiz. This file is part of +# hitobito_youth and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito_youth. + +class Event::ParticipationContactData + + attr_reader :person + + T_PERSON_ATTRS = 'activerecord.attributes.person.'.freeze + + class_attribute :mandatory_contact_attrs, + :contact_attrs, + :contact_associations + + self.mandatory_contact_attrs = [:email, :first_name, :last_name] + + self.contact_attrs = [:first_name, :last_name, :nickname, :company_name, + :email, :address, :zip_code, :town, + :country, :gender, :birthday] + + self.contact_associations = [:additional_emails, :phone_numbers, :social_accounts] + + delegate(*contact_attrs, to: :person) + delegate(*contact_associations, to: :person) + + delegate :t, to: I18n + + delegate :gender_label, :column_for_attribute, :timeliness_cache_attribute, + :has_attribute?, to: :person + + delegate :layer_group, to: :event + + include ActiveModel::Validations + + validate :assert_required_contact_attrs_valid + validate :assert_person_attrs_valid + + class << self + + delegate :reflect_on_association, :human_attribute_name, to: Person + + def base_class + self + end + + def demodulized_route_keys + nil + end + + end + + def initialize(event, person, model_params = {}) + @model_params = model_params + @event = event + @person = person + person.attributes = model_params if model_params.present? + end + + def save + valid? && person.save + end + + def parent + event + end + + def method_missing(method) + return person.send(method) if method =~ /^.*_came_from_user\?/ + return person.send(method) if method =~ /^.*_before_type_cast/ + + super(method) + end + + def show_attr?(a) + attribute_keys.include?(a) + end + + def required_attr?(a) + required_attributes.include?(a.to_s) + end + + def attribute_keys + self.class.contact_attrs - hidden_contact_attrs + end + + def hidden_contact_attrs + event.hidden_contact_attrs.collect(&:to_sym) + end + + def respond_to?(attr) + responds = super(attr) + responds ? true : person.respond_to?(attr) + end + + def new_record? + true + end + + def persisted? + false + end + + def to_model + self + end + + def to_key + nil + end + + def required_attributes + @required_attributes ||= event.required_contact_attrs + + self.class.mandatory_contact_attrs.map(&:to_s) + end + + private + + attr_reader :model_params, :event + + def assert_required_contact_attrs_valid + required_attributes.each do |a| + if model_params[a.to_s].blank? + errors.add(a, t('errors.messages.blank')) + end + end + end + + def assert_person_attrs_valid + unless person.valid? + collect_person_errors + end + end + + def collect_person_errors + person.errors.full_messages.each do |m| + errors.add(:base, m) + end + end + +end diff --git a/app/models/event/question.rb b/app/models/event/question.rb index 9c55527ed0..b4f4412b42 100644 --- a/app/models/event/question.rb +++ b/app/models/event/question.rb @@ -1,9 +1,10 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. + # == Schema Information # # Table name: event_questions @@ -12,8 +13,9 @@ # event_id :integer # question :string # choices :string -# multiple_choices :boolean default(FALSE) -# required :boolean +# multiple_choices :boolean not null, default(FALSE) +# required :boolean not null, default(FALSE) +# admin :boolean not null, default(FALSE) # class Event::Question < ActiveRecord::Base @@ -29,6 +31,9 @@ class Event::Question < ActiveRecord::Base scope :global, -> { where(event_id: nil) } + scope :application, -> { where(admin: false) } + scope :admin, -> { where(admin: true) } + def choice_items choices.to_s.split(',').collect(&:strip) diff --git a/app/models/event/role/helper.rb b/app/models/event/role/helper.rb new file mode 100644 index 0000000000..68eb7713c5 --- /dev/null +++ b/app/models/event/role/helper.rb @@ -0,0 +1,24 @@ +# encoding: utf-8 + +# Copyright (c) 2017, CVJM. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. +# == Schema Information +# +# Table name: event_roles +# +# id :integer not null, primary key +# type :string not null +# participation_id :integer not null +# label :string +# + +# Kueche +class Event::Role::Helper < Event::Role + + self.permissions = [:participations_read] + + self.kind = :helper + +end diff --git a/app/models/group.rb b/app/models/group.rb index 2914be0128..765c9b0750 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -1,9 +1,10 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. + # == Schema Information # # Table name: groups @@ -36,8 +37,8 @@ class Group < ActiveRecord::Base MINIMAL_SELECT = %w(id name type parent_id lft rgt layer_group_id deleted_at). collect { |a| "groups.#{a}" } - include Group::Types include Group::NestedSet + include Group::Types include Contactable acts_as_paranoid @@ -61,6 +62,7 @@ class Group < ActiveRecord::Base before_save :reset_contact_info + # Root group may not be destroyed protect_if :root? protect_if :children_without_deleted @@ -81,12 +83,21 @@ class Group < ActiveRecord::Base has_many :mailing_lists, dependent: :destroy has_many :subscriptions, as: :subscriber, dependent: :destroy + has_many :notes, as: :subject, dependent: :destroy + has_many :person_add_requests, foreign_key: :body_id, inverse_of: :body, class_name: 'Person::AddRequest::Group', dependent: :destroy + has_one :invoice_config, dependent: :destroy + has_many :invoices, dependent: :destroy + has_many :invoice_articles, dependent: :destroy + has_many :invoice_items, through: :invoices + + after_create :create_invoice_config, if: :layer? + ### VALIDATIONS validates_by_schema @@ -151,7 +162,7 @@ def with_layer end # create alias to call it again - alias_method :hard_destroy, :really_destroy! + alias hard_destroy really_destroy! def really_destroy! # run nested_set callback on hard destroy destroy_descendants_without_paranoia @@ -189,4 +200,8 @@ def destroy_descendants_with_paranoia end alias_method_chain :destroy_descendants, :paranoia + def create_invoice_config + create_invoice_config! + end + end diff --git a/app/models/group/types.rb b/app/models/group/types.rb index 8474c0d04b..c70fac4d54 100644 --- a/app/models/group/types.rb +++ b/app/models/group/types.rb @@ -1,6 +1,6 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. @@ -24,9 +24,8 @@ module Group::Types self.event_types = [Event] - after_create :set_layer_group_id - after_update :set_layer_group_id - after_create :create_default_children + after_save :set_layer_group_id + after_save :create_default_children validate :assert_type_is_allowed_for_parent, on: :create end @@ -34,6 +33,9 @@ module Group::Types private def create_default_children + # hack to have after_save ordering for this semantical after_create callback + return if created_at < Time.zone.now - 10.seconds + default_children.each do |group_type| child = group_type.new(name: group_type.label) child.parent = self @@ -50,7 +52,10 @@ def assert_type_is_allowed_for_parent def set_layer_group_id layer_id = self.class.layer ? id : parent.layer_group_id unless layer_id == layer_group_id - update_column(:layer_group_id, layer_id) + self_and_descendants. + where(layer_group_id: layer_group_id). + update_all(layer_group_id: layer_id) + self.layer_group_id = layer_id end end diff --git a/app/models/invoice.rb b/app/models/invoice.rb new file mode 100644 index 0000000000..6d4d2e69e1 --- /dev/null +++ b/app/models/invoice.rb @@ -0,0 +1,193 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. +# == Schema Information +# +# Table name: invoices +# +# id :integer not null, primary key +# title :string(255) not null +# sequence_number :string(255) not null +# state :string(255) default("draft"), not null +# esr_number :string(255) not null +# description :text(65535) +# recipient_email :string(255) +# recipient_address :text(65535) +# sent_at :date +# due_at :date +# group_id :integer not null +# recipient_id :integer not null +# total :decimal(12, 2) +# created_at :datetime not null +# updated_at :datetime not null +# + +class Invoice < ActiveRecord::Base + include I18nEnums + + attr_accessor :recipient_ids + + STATES = %w(draft issued sent payed overdue reminded cancelled).freeze + DUE_SINCE = %w(one_day one_week one_month).freeze + + belongs_to :group + belongs_to :recipient, class_name: 'Person' + has_many :invoice_items, dependent: :destroy + has_many :payments, dependent: :destroy + has_many :payment_reminders, dependent: :destroy + + before_validation :set_sequence_number, on: :create, if: :group + before_validation :set_esr_number, on: :create, if: :group + before_validation :set_dates, on: :update + before_validation :set_self_in_nested + before_validation :recalculate + + validates :state, inclusion: { in: STATES } + validates :due_at, timeliness: { after: :sent_at }, presence: true, if: :sent? + validate :assert_sendable?, unless: :recipient_id? + + before_create :set_recipient_fields, if: :recipient + after_create :increment_sequence_number + + + accepts_nested_attributes_for :invoice_items, allow_destroy: true + + i18n_enum :state, STATES + + validates_by_schema + + scope :list, -> { order(:sequence_number) } + scope :one_day, -> { where('due_at < ?', 1.day.ago.to_date) } + scope :one_week, -> { where('due_at < ?', 1.week.ago.to_date) } + scope :one_month, -> { where('due_at < ?', 1.month.ago.to_date) } + scope :visible, -> { where.not(state: :cancelled) } + + STATES.each do |state| + scope state.to_sym, -> { where(state: state) } + define_method "#{state}?" do + self.state == state + end + end + + def self.to_contactables(invoices) + invoices.collect do |invoice| + next if invoice.recipient_address.blank? + Person.new(address: invoice.recipient_address) + end.compact + end + + def multi_create # rubocop:disable Metrics/MethodLength + Invoice.transaction do + all_saved = recipients.all? do |recipient| + invoice = self.class.new(attributes.merge(recipient_id: recipient.id)) + invoice_items.each do |invoice_item| + invoice.invoice_items.build(invoice_item.attributes) + end + invoice.save + end + raise ActiveRecord::Rollback unless all_saved + all_saved + end + end + + def calculated + [:total, :cost, :vat].collect do |field| + [field, invoice_items.to_a.sum(&field)] + end.to_h + end + + def recalculate + self.total = invoice_items.to_a.sum(&:total) || 0 + end + + def to_s + "#{title}(#{sequence_number}): #{total}" + end + + def reminder_sent? + payment_reminders.present? + end + + def remindable? + %w(sent reminded overdue).include?(state) + end + + def recipients + Person.where(id: recipient_ids.to_s.split(',')) + end + + def recipient_name + recipient.try(:greeting_name) || recipient_address.split("\n").first + end + + def filename(extension) + format('%s-%s.%s', self.class.model_name.human, sequence_number, extension) + end + + def invoice_config + group.invoice_config + end + + def state + ActiveSupport::StringInquirer.new(self[:state]) + end + + def amount_open + total - payments.sum(:amount) + end + + def amount_paid + payments.sum(:amount) + end + + private + + def set_self_in_nested + invoice_items.each { |item| item.invoice = self } + end + + def set_sequence_number + self.sequence_number = [group_id, invoice_config.sequence_number].join('-') + end + + def set_esr_number + self.esr_number = sequence_number + end + + def set_dates + self.sent_at ||= Time.zone.today if sent? + if sent? || issued? + self.issued_at ||= Time.zone.today + self.due_at ||= issued_at + invoice_config.due_days.days + end + end + + def set_recipient_fields + self.recipient_email = recipient.email + self.recipient_address = build_recipient_address + end + + def item_invalid?(attributes) + !InvoiceItem.new(attributes.merge(invoice: self)).valid? + end + + def increment_sequence_number + invoice_config.increment!(:sequence_number) # rubocop:disable Rails/SkipsModelValidations + end + + def build_recipient_address + [recipient.full_name, + recipient.address, + [recipient.zip_code, recipient.town].compact.join(' / '), + recipient.country].compact.join("\n") + end + + def assert_sendable? + if recipient_email.blank? && recipient_address.blank? + errors.add(:base, :recipient_address_or_email_required) + end + end +end diff --git a/app/models/invoice_article.rb b/app/models/invoice_article.rb new file mode 100644 index 0000000000..9b763ae7fb --- /dev/null +++ b/app/models/invoice_article.rb @@ -0,0 +1,50 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. +# == Schema Information +# +# Table name: invoice_articles +# +# id :integer not null, primary key +# number :string(255) +# name :string(255) not null +# description :text(65535) +# category :string(255) +# unit_cost :decimal(12, 2) +# vat_rate :decimal(5, 2) +# cost_center :string(255) +# account :string(255) +# created_at :datetime not null +# updated_at :datetime not null +# group_id :integer not null +# + +class InvoiceArticle < ActiveRecord::Base + + belongs_to :group + + validates :name, presence: true, uniqueness: { scope: :group_id } + validates :number, presence: true, uniqueness: { scope: :group_id } + + validates_by_schema + + def self.categories + pluck(:category).uniq + end + + def self.cost_centers + pluck(:cost_center).uniq + end + + def self.accounts + pluck(:account).uniq + end + + def to_s + [number, name].compact.join(' - ') + end + +end diff --git a/app/models/invoice_config.rb b/app/models/invoice_config.rb new file mode 100644 index 0000000000..eebe914294 --- /dev/null +++ b/app/models/invoice_config.rb @@ -0,0 +1,33 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. +# == Schema Information +# +# Table name: invoice_configs +# +# id :integer not null, primary key +# sequence_number :integer default(1), not null +# due_days :integer default(30), not null +# group_id :integer not null +# contact_id :integer +# page_size :integer default(15) +# address :text(65535) +# payment_information :text(65535) +# + +class InvoiceConfig < ActiveRecord::Base + belongs_to :group, class_name: 'Group' + belongs_to :contact, class_name: 'Person' + + validates :group_id, uniqueness: true + + validates_by_schema + + def to_s + "#{group.name} - Invoice Config" # TODO: determine proper string representation + end + +end diff --git a/app/models/invoice_item.rb b/app/models/invoice_item.rb new file mode 100644 index 0000000000..9c11d6a99f --- /dev/null +++ b/app/models/invoice_item.rb @@ -0,0 +1,44 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. +# == Schema Information +# +# Table name: invoice_items +# +# id :integer not null, primary key +# invoice_id :integer not null +# name :string(255) not null +# description :text(65535) +# vat_rate :decimal(5, 2) +# unit_cost :decimal(12, 2) not null +# count :integer default(1), not null +# + +class InvoiceItem < ActiveRecord::Base + + belongs_to :invoice + + scope :list, -> { order(:name) } + + validates_by_schema + + def to_s + "#{name}: #{total} (#{amount} / #{vat})" + end + + def total + cost + vat + end + + def cost + unit_cost ? unit_cost * count : 0 + end + + def vat + vat_rate ? cost * (vat_rate / 100) : 0 + end + +end diff --git a/app/models/label_format.rb b/app/models/label_format.rb index 64d17d73a2..c1c767c9fe 100644 --- a/app/models/label_format.rb +++ b/app/models/label_format.rb @@ -18,6 +18,9 @@ # count_vertical :integer not null # padding_top :float not null # padding_left :float not null +# person_id :integer +# nickname :boolean default(FALSE), not null +# pp_post :string(23) # class LabelFormat < ActiveRecord::Base @@ -33,6 +36,8 @@ def available_page_sizes has_many :people, foreign_key: :last_label_format_id, dependent: :nullify + belongs_to :person + validates :name, presence: true, length: { maximum: 255, allow_nil: true } validates :page_size, inclusion: available_page_sizes @@ -47,17 +52,17 @@ def available_page_sizes validates :padding_top, numericality: { less_than: :height, if: :height } validates :padding_left, numericality: { less_than: :width, if: :width } - after_save :sweep_cache - after_destroy :sweep_cache + scope :for_user, ->(user) { where('user_id = ? OR user_id IS null', user.id) } - class << self - def all_as_hash - Rails.cache.fetch("label_formats_#{I18n.locale}") do - LabelFormat.list.each_with_object({}) { |f, result| result[f.id] = f.to_s } - end + def self.for_person(person) + if person.show_global_label_formats? + where('person_id = ? OR person_id IS NULL', person.id) + else + where(person_id: person.id) end end + def to_s(_format = :default) "#{name} (#{page_size}, #{dimensions})" end @@ -70,12 +75,4 @@ def page_layout landscape ? :landscape : :portrait end - private - - def sweep_cache - Settings.application.languages.to_hash.keys.each do |lang| - Rails.cache.delete("label_formats_#{lang}") - end - end - end diff --git a/app/models/location.rb b/app/models/location.rb index 27951a28fd..283a1b5e01 100644 --- a/app/models/location.rb +++ b/app/models/location.rb @@ -1,9 +1,9 @@ # encoding: utf-8 # Copyright (c) 2012-2015, Pfadibewegung Schweiz. This file is part of -# hitobito_pbs and licensed under the Affero General Public License version 3 +# hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito_pbs. +# https://github.com/hitobito/hitobito. # == Schema Information # diff --git a/app/models/mailing_list.rb b/app/models/mailing_list.rb index 2c87154fb0..8a3a53775e 100644 --- a/app/models/mailing_list.rb +++ b/app/models/mailing_list.rb @@ -1,6 +1,6 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. @@ -60,7 +60,7 @@ def exclude_person(person) subscriptions.where(subscriber_id: person.id, subscriber_type: Person.sti_name, excluded: false). - destroy_all + destroy_all if subscribed?(person) sub = subscriptions.new @@ -72,36 +72,38 @@ def exclude_person(person) def people(people_scope = Person.only_public_data) people_scope. - joins(people_joins). - joins(subscription_joins). - where(subscriptions: { mailing_list_id: id }). - where("people.id NOT IN (#{excluded_person_subscribers.to_sql})"). - where(suscriber_conditions). - uniq + joins(people_joins). + joins(subscription_joins). + where(subscriptions: { mailing_list_id: id }). + where("people.id NOT IN (#{excluded_person_subscribers.to_sql})"). + where(suscriber_conditions). + uniq end private def people_joins - 'LEFT JOIN roles ON people.id = roles.person_id ' \ - 'LEFT JOIN groups ON roles.group_id = groups.id ' \ - 'LEFT JOIN event_participations ON event_participations.person_id = people.id ' \ - 'LEFT JOIN taggings AS people_taggings ' \ - "ON people_taggings.taggable_type = 'Person' " \ - 'AND people_taggings.taggable_id = people.id' + <<-SQL.strip_heredoc.split.map(&:strip).join(' ') + LEFT JOIN roles ON people.id = roles.person_id + LEFT JOIN groups ON roles.group_id = groups.id + LEFT JOIN event_participations ON event_participations.person_id = people.id + LEFT JOIN taggings AS people_taggings + ON people_taggings.taggable_type = 'Person' + AND people_taggings.taggable_id = people.id + SQL end def subscription_joins - ', subscriptions ' \ - 'LEFT JOIN groups sub_groups ' \ - "ON subscriptions.subscriber_type = 'Group'" \ - 'AND subscriptions.subscriber_id = sub_groups.id ' \ - 'LEFT JOIN related_role_types ' \ - "ON related_role_types.relation_type = 'Subscription' " \ - 'AND related_role_types.relation_id = subscriptions.id ' \ - 'LEFT JOIN taggings AS subscriptions_taggings ' \ - "ON subscriptions_taggings.taggable_type = 'Subscription' " \ - 'AND subscriptions_taggings.taggable_id = subscriptions.id' + # the comma is needed because it is not a JOIN, but a second "FROM" + <<-SQL.strip_heredoc.split.map(&:strip).join(' ') + , subscriptions + LEFT JOIN groups sub_groups + ON subscriptions.subscriber_type = 'Group' AND subscriptions.subscriber_id = sub_groups.id + LEFT JOIN related_role_types + ON related_role_types.relation_type = 'Subscription' AND related_role_types.relation_id = subscriptions.id + LEFT JOIN taggings AS subscriptions_taggings + ON subscriptions_taggings.taggable_type = 'Subscription' AND subscriptions_taggings.taggable_id = subscriptions.id + SQL end def suscriber_conditions @@ -122,9 +124,9 @@ def person_subscribers(condition) def excluded_person_subscribers Subscription.select(:subscriber_id). - where(mailing_list_id: id, - excluded: true, - subscriber_type: Person.sti_name) + where(mailing_list_id: id, + excluded: true, + subscriber_type: Person.sti_name) end def group_subscribers(condition) @@ -148,7 +150,7 @@ def event_subscribers(condition) def assert_mail_name_is_not_protected if mail_name? && application_retriever_name - if mail_name.downcase == application_retriever_name.split('@', 2).first.downcase + if mail_name.casecmp(application_retriever_name.split('@', 2).first).zero? errors.add(:mail_name, :not_allowed, mail_name: mail_name) end end diff --git a/app/models/note.rb b/app/models/note.rb new file mode 100644 index 0000000000..4ae76dbd54 --- /dev/null +++ b/app/models/note.rb @@ -0,0 +1,53 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2016, Dachverband Schweizer Jugendparlamente. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +# == Schema Information +# +# Table name: notes +# +# id :integer not null, primary key +# subject_id :integer not null +# author_id :integer not null +# text :text +# created_at :datetime +# updated_at :datetime +# subject_type :string +# + +class Note < ActiveRecord::Base + + ### ASSOCIATIONS + + belongs_to :subject, polymorphic: true + belongs_to :author, class_name: 'Person' + + ### VALIDATIONS + + validates_by_schema + validates :text, presence: true + + scope :list, -> { order(created_at: :desc) } + + class << self + def in_or_layer_below(group) + joins('LEFT JOIN roles ' \ + "ON roles.person_id = notes.subject_id AND notes.subject_type = '#{Person.sti_name}'"). + joins('INNER JOIN groups ' \ + "ON (groups.id = notes.subject_id AND notes.subject_type = '#{Group.sti_name}') " \ + 'OR (groups.id = roles.group_id)'). + where(roles: { deleted_at: nil }, + groups: { deleted_at: nil, layer_group_id: group.layer_group_id }). + where('groups.lft >= :lft AND groups.rgt <= :rgt', lft: group.lft, rgt: group.rgt). + uniq + end + end + + def to_s + text.to_s.delete("\n").truncate(10) + end + +end diff --git a/app/models/payment.rb b/app/models/payment.rb new file mode 100644 index 0000000000..aff17061e6 --- /dev/null +++ b/app/models/payment.rb @@ -0,0 +1,35 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +class Payment < ActiveRecord::Base + + belongs_to :invoice + + before_validation :set_received_at + after_create :update_invoice + + scope :list, -> { order(created_at: :desc) } + + validates_by_schema + + def group + invoice.group + end + + private + + def update_invoice + if amount >= invoice.total + invoice.update(state: :payed) + end + end + + def set_received_at + self.received_at ||= Time.zone.today + end + +end diff --git a/app/models/payment_reminder.rb b/app/models/payment_reminder.rb new file mode 100644 index 0000000000..de22bcbfc9 --- /dev/null +++ b/app/models/payment_reminder.rb @@ -0,0 +1,56 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. +# == Schema Information +# +# Table name: payment_reminders +# +# id :integer not null, primary key +# invoice_id :integer not null +# message :text(65535) +# due_at :date not null +# created_at :datetime not null +# updated_at :datetime not null +# + +class PaymentReminder < ActiveRecord::Base + + belongs_to :invoice + + validate :assert_invoice_remindable + validates :due_at, uniqueness: { scope: :invoice_id }, + timeliness: { after: :invoice_due_at, allow_blank: true, type: :date }, + if: :invoice_remindable? + + after_create :update_invoice + + validates_by_schema + + delegate :due_at, :remindable?, to: :invoice, prefix: true + + scope :list, -> { order(created_at: :desc) } + + def to_s + I18n.l(due_at) + end + + def group + invoice.group + end + + private + + def update_invoice + invoice.update(state: :overdue, due_at: due_at) + end + + def assert_invoice_remindable + unless invoice_remindable? + errors.add(:invoice, :invalid) + end + end + +end diff --git a/app/models/people_filter.rb b/app/models/people_filter.rb index ec2f04840e..837394e9eb 100644 --- a/app/models/people_filter.rb +++ b/app/models/people_filter.rb @@ -1,45 +1,57 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. + # == Schema Information # # Table name: people_filters # -# id :integer not null, primary key -# name :string not null -# group_id :integer -# group_type :string +# id :integer not null, primary key +# name :string not null +# group_id :integer +# group_type :string +# filter_chain :text +# range :string +# created_at :datetime +# updated_at :datetime # class PeopleFilter < ActiveRecord::Base - include RelatedRoleType::Assigners + RANGES = %w(deep layer group).freeze + serialize :filter_chain, Person::Filter::Chain belongs_to :group - has_many :related_role_types, as: :relation, dependent: :destroy - - validates_by_schema validates :name, uniqueness: { scope: [:group_id, :group_type] } + validates :range, inclusion: { in: RANGES } - - default_scope { order(:name).includes(:related_role_types) } + scope :list, -> { order(:name) } def to_s(_format = :default) name end + def filter_chain=(value) + if value.is_a?(Hash) + super(Person::Filter::Chain.new(value)) + else + super + end + end + class << self def for_group(group) - where('group_id = ? OR group_type = ? OR ' \ - '(group_id IS NULL AND group_type IS NULL)', - group.id, - group.type) + includes(:group) + .where('group_id = ? OR group_type = ? OR ' \ + '(group_id IS NULL AND group_type IS NULL)', + group.id, + group.type) end end diff --git a/app/models/person.rb b/app/models/person.rb index c5f11d3333..c14a9f7019 100644 --- a/app/models/person.rb +++ b/app/models/person.rb @@ -8,56 +8,61 @@ # # Table name: people # -# id :integer not null, primary key -# first_name :string -# last_name :string -# company_name :string -# nickname :string -# company :boolean default(FALSE), not null -# email :string -# address :string(1024) -# zip_code :string -# town :string -# country :string -# gender :string(1) -# birthday :date -# additional_information :text -# contact_data_visible :boolean default(FALSE), not null -# created_at :datetime -# updated_at :datetime -# encrypted_password :string -# reset_password_token :string -# reset_password_sent_at :datetime -# remember_created_at :datetime -# sign_in_count :integer default(0) -# current_sign_in_at :datetime -# last_sign_in_at :datetime -# current_sign_in_ip :string -# last_sign_in_ip :string -# picture :string -# last_label_format_id :integer -# creator_id :integer -# updater_id :integer -# primary_group_id :integer -# failed_attempts :integer default(0) -# locked_at :datetime -# authentication_token :string +# id :integer not null, primary key +# first_name :string(255) +# last_name :string(255) +# company_name :string(255) +# nickname :string(255) +# company :boolean default(FALSE), not null +# email :string(255) +# address :string(1024) +# zip_code :string(255) +# town :string(255) +# country :string(255) +# gender :string(1) +# birthday :date +# additional_information :text(65535) +# contact_data_visible :boolean default(FALSE), not null +# created_at :datetime +# updated_at :datetime +# encrypted_password :string(255) +# reset_password_token :string(255) +# reset_password_sent_at :datetime +# remember_created_at :datetime +# sign_in_count :integer default(0) +# current_sign_in_at :datetime +# last_sign_in_at :datetime +# current_sign_in_ip :string(255) +# last_sign_in_ip :string(255) +# picture :string(255) +# last_label_format_id :integer +# creator_id :integer +# updater_id :integer +# primary_group_id :integer +# failed_attempts :integer default(0) +# locked_at :datetime +# authentication_token :string(255) +# show_global_label_formats :boolean default(TRUE), not null # class Person < ActiveRecord::Base - PUBLIC_ATTRS = [:id, :first_name, :last_name, :nickname, :company_name, :company, - :email, :address, :zip_code, :town, :country, :gender, :birthday, - :picture, :primary_group_id] + PUBLIC_ATTRS = [ # rubocop:disable Style/MutableConstant meant to be extended in wagons + :id, :first_name, :last_name, :nickname, :company_name, :company, + :email, :address, :zip_code, :town, :country, :gender, :birthday, + :picture, :primary_group_id + ] - INTERNAL_ATTRS = [:authentication_token, :contact_data_visible, :created_at, :creator_id, - :current_sign_in_at, :current_sign_in_ip, :encrypted_password, :id, - :last_label_format_id, :failed_attempts, :last_sign_in_at, :last_sign_in_ip, - :locked_at, :remember_created_at, :reset_password_token, - :reset_password_sent_at, :sign_in_count, :updated_at, :updater_id] - - GENDERS = %w(m w) + INTERNAL_ATTRS = [ # rubocop:disable Style/MutableConstant meant to be extended in wagons + :authentication_token, :contact_data_visible, :created_at, :creator_id, + :current_sign_in_at, :current_sign_in_ip, :encrypted_password, :id, + :last_label_format_id, :failed_attempts, :last_sign_in_at, :last_sign_in_ip, + :locked_at, :remember_created_at, :reset_password_token, + :reset_password_sent_at, :sign_in_count, :updated_at, :updater_id, + :show_global_label_formats + ] + GENDERS = %w(m w).freeze # define devise before other modules devise :database_authenticatable, @@ -114,8 +119,7 @@ class Person < ActiveRecord::Base has_many :add_requests, dependent: :destroy - has_many :notes, class_name: 'Note', - dependent: :destroy + has_many :notes, dependent: :destroy, as: :subject has_many :authored_notes, class_name: 'Note', foreign_key: 'author_id', @@ -124,6 +128,8 @@ class Person < ActiveRecord::Base belongs_to :primary_group, class_name: 'Group' belongs_to :last_label_format, class_name: 'LabelFormat' + has_many :label_formats, dependent: :destroy + accepts_nested_attributes_for :relations_to_tails, allow_destroy: true ### VALIDATIONS @@ -217,7 +223,7 @@ def default_group_id primary_group_id || groups.first.try(:id) || Group.root.id end - def years + def years # rubocop:disable Metrics/AbcSize Age calculation is complex return unless birthday? now = Time.zone.now.to_date @@ -254,6 +260,15 @@ def save(*args) false end + def layer_group + primary_group.layer_group if primary_group + end + + def finance_groups + groups_with_permission(:finance). + flat_map(&:layer_group) + end + private def override_blank_email diff --git a/app/models/person/add_request.rb b/app/models/person/add_request.rb index 928cfe40f6..f38ea914f2 100644 --- a/app/models/person/add_request.rb +++ b/app/models/person/add_request.rb @@ -35,8 +35,11 @@ class Person::AddRequest < ActiveRecord::Base class << self def for_layer(layer_group) - joins(person: :primary_group). - where(groups: { layer_group_id: layer_group.id }) + joins(:person). + joins('LEFT JOIN groups AS primary_groups ON primary_groups.id = people.primary_group_id'). + where('primary_groups.layer_group_id = ? OR people.id IN (?)', + layer_group.id, + ::Group::DeletedPeople.deleted_for(layer_group).select(:id)) end end @@ -56,7 +59,7 @@ def body_label end def person_layer - person.primary_group.try(:layer_group) + person.primary_group.try(:layer_group) || last_layer_group end def requester_full_roles @@ -66,4 +69,11 @@ def requester_full_roles end end + private + + def last_layer_group + last_role = person.last_non_restricted_role + last_role && last_role.group.layer_group + end + end diff --git a/app/models/person/add_request/event.rb b/app/models/person/add_request/event.rb index acbe20a2bd..9fc72a1629 100644 --- a/app/models/person/add_request/event.rb +++ b/app/models/person/add_request/event.rb @@ -24,10 +24,15 @@ class Person::AddRequest::Event < Person::AddRequest validates :role_type, presence: true def to_s(_format = :default) - group = body.groups.first - event_label = body_label - group_label = "#{group.model_name.human} #{group}" - self.class.human_attribute_name(:label, body: event_label, group: group_label) + if body + group = body.groups.first + event_label = body_label + group_label = "#{group.model_name.human} #{group}" + self.class.human_attribute_name(:label, body: event_label, group: group_label) + else + # event was deleted in the mean time + self.class.human_attribute_name(:deleted_event) + end end end diff --git a/app/models/person/groups.rb b/app/models/person/groups.rb index 30f7f33211..65cf3efbc5 100644 --- a/app/models/person/groups.rb +++ b/app/models/person/groups.rb @@ -28,7 +28,7 @@ def non_restricted_groups roles_with_groups.to_a.reject { |r| r.class.restricted? }.collect(&:group).uniq end - # All groups where this person has the given permission(s). + # All groups where this person has the given permission. def groups_with_permission(permission) @groups_with_permission ||= {} @groups_with_permission[permission] ||= begin @@ -55,6 +55,13 @@ def above_groups_where_visible_from groups_where_visible_from_above.collect(&:hierarchy).flatten.uniq end + def last_non_restricted_role + return nil if roles.exists? + + restricted = Role.all_types.select(&:restricted?).collect(&:sti_name) + roles.with_deleted.where.not(type: restricted).order(deleted_at: :desc).first + end + private # Helper method to access the roles association, diff --git a/app/models/person/note.rb b/app/models/person/note.rb deleted file mode 100644 index 3efc8d84f5..0000000000 --- a/app/models/person/note.rb +++ /dev/null @@ -1,36 +0,0 @@ -# encoding: utf-8 - -# Copyright (c) 2012-2016, Dachverband Schweizer Jugendparlamente. This file is part of -# hitobito_dsj and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito_dsj. - -# == Schema Information -# -# Table name: person_notes -# -# id :integer not null, primary key -# person_id :integer not null -# author_id :integer not null -# text :text -# created_at :datetime -# updated_at :datetime -# -class Person::Note < ActiveRecord::Base - - default_scope { order(created_at: :desc) } - - ### ASSOCIATIONS - - belongs_to :person - belongs_to :author, class_name: 'Person' - - ### VALIDATIONS - - validates :text, presence: true - - def to_s - text.present? && text.sub("\n", ' ')[0..9] + (text.length > 10 ? '...' : '') - end - -end diff --git a/app/models/person/picture_uploader.rb b/app/models/person/picture_uploader.rb index 4e0ee30d5c..42fbb1a9be 100644 --- a/app/models/person/picture_uploader.rb +++ b/app/models/person/picture_uploader.rb @@ -5,15 +5,13 @@ # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. -class Person::PictureUploader < CarrierWave::Uploader::Base +class Person::PictureUploader < Uploader::Base MAX_DIMENSION = 8000 - EXTENSION_WHITE_LIST = %w(jpg jpeg gif png) - include CarrierWave::MiniMagick + self.allowed_extensions = %w(jpg jpeg gif png) - # Choose what kind of storage to use for this uploader: - storage :file + include CarrierWave::MiniMagick # Process files as they are uploaded: process :validate_dimensions @@ -24,17 +22,6 @@ class Person::PictureUploader < CarrierWave::Uploader::Base process resize_to_fill: [32, 32] end - class << self - def accept_extensions - EXTENSION_WHITE_LIST.collect { |e| ".#{e}" }.join(',') - end - end - - # Override the directory where uploaded files will be stored. - # This is a sensible default for uploaders that are meant to be mounted: - def store_dir - "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" - end # Provide a default URL as a default if there hasn't been a file uploaded: def default_url @@ -45,12 +32,6 @@ def png_name ['profil', version_name].compact.join('_') + '.png' end - # Add a white list of extensions which are allowed to be uploaded. - # For images you might use something like this: - def extension_white_list - EXTENSION_WHITE_LIST - end - private # check for images that are larger than you probably want diff --git a/app/models/qualification.rb b/app/models/qualification.rb index 2f8b91fd81..b37331f845 100644 --- a/app/models/qualification.rb +++ b/app/models/qualification.rb @@ -54,7 +54,7 @@ def active(date = nil) def reactivateable(date = nil) date ||= Time.zone.today joins(:qualification_kind). - where('qualifications.start_at <= ?', date). + where('qualifications.start_at <= ?', date). where('qualifications.finish_at IS NULL OR ' \ '(qualification_kinds.reactivateable IS NULL AND ' \ ' qualifications.finish_at >= ?) OR ' \ diff --git a/app/models/role/types.rb b/app/models/role/types.rb index 33ff2589ea..90c2ac8288 100644 --- a/app/models/role/types.rb +++ b/app/models/role/types.rb @@ -14,7 +14,7 @@ module Role::Types Permissions = [:admin, :layer_and_below_full, :layer_and_below_read, :layer_full, :layer_read, :group_and_below_full, :group_and_below_read, :group_full, :group_read, - :contact_data, :approve_applications] + :contact_data, :approve_applications, :finance] # If a role contains the first permission, the second one is automatically active as well PermissionImplications = { layer_and_below_full: :layer_and_below_read, diff --git a/app/models/uploader/base.rb b/app/models/uploader/base.rb new file mode 100644 index 0000000000..f7f3e33b61 --- /dev/null +++ b/app/models/uploader/base.rb @@ -0,0 +1,37 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, hitobito AG. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +class Uploader::Base < CarrierWave::Uploader::Base + + class_attribute :allowed_extensions + + # Choose what kind of storage to use for this uploader + storage :file + + class << self + def accept_extensions + allowed_extensions.collect { |e| ".#{e}" }.join(', ') + end + end + + # Add a white list of extensions which are allowed to be uploaded. + # For images you might use something like this: + def extension_white_list + allowed_extensions + end + + def base_store_dir + 'uploads' + end + + # Override the directory where uploaded files will be stored. + # This is a sensible default for uploaders that are meant to be mounted: + def store_dir + "#{base_store_dir}/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" + end + +end diff --git a/app/serializers/person_serializer.rb b/app/serializers/person_serializer.rb index 7a5316ef2e..33eb86f487 100644 --- a/app/serializers/person_serializer.rb +++ b/app/serializers/person_serializer.rb @@ -3,40 +3,41 @@ # # Table name: people # -# id :integer not null, primary key -# first_name :string -# last_name :string -# company_name :string -# nickname :string -# company :boolean default(FALSE), not null -# email :string -# address :string(1024) -# zip_code :string -# town :string -# country :string -# gender :string(1) -# birthday :date -# additional_information :text -# contact_data_visible :boolean default(FALSE), not null -# created_at :datetime -# updated_at :datetime -# encrypted_password :string -# reset_password_token :string -# reset_password_sent_at :datetime -# remember_created_at :datetime -# sign_in_count :integer default(0) -# current_sign_in_at :datetime -# last_sign_in_at :datetime -# current_sign_in_ip :string -# last_sign_in_ip :string -# picture :string -# last_label_format_id :integer -# creator_id :integer -# updater_id :integer -# primary_group_id :integer -# failed_attempts :integer default(0) -# locked_at :datetime -# authentication_token :string +# id :integer not null, primary key +# first_name :string +# last_name :string +# company_name :string +# nickname :string +# company :boolean default(FALSE), not null +# email :string +# address :string(1024) +# zip_code :string +# town :string +# country :string +# gender :string(1) +# birthday :date +# additional_information :text +# contact_data_visible :boolean default(FALSE), not null +# created_at :datetime +# updated_at :datetime +# encrypted_password :string +# reset_password_token :string +# reset_password_sent_at :datetime +# remember_created_at :datetime +# sign_in_count :integer default(0) +# current_sign_in_at :datetime +# last_sign_in_at :datetime +# current_sign_in_ip :string +# last_sign_in_ip :string +# picture :string +# last_label_format_id :integer +# creator_id :integer +# updater_id :integer +# primary_group_id :integer +# failed_attempts :integer default(0) +# locked_at :datetime +# authentication_token :string +# show_global_label_formats :boolean default(TRUE), not null # # Copyright (c) 2014, CEVI Regionalverband ZH-SH-GL. This file is part of diff --git a/app/utils/changelog_reader.rb b/app/utils/changelog_reader.rb index cbe28c5130..4f608bc57d 100644 --- a/app/utils/changelog_reader.rb +++ b/app/utils/changelog_reader.rb @@ -22,6 +22,7 @@ def changelogs end private + def collect_changelog_data changelog_files_content = read_changelog_files(changelog_file_paths) parse_changelog_lines(changelog_files_content) @@ -49,7 +50,7 @@ def read_changelog_files(files_path) end def changelog_file_paths - file_paths = ["CHANGELOG.md"] + file_paths = ['CHANGELOG.md'] Wagons.all.each do |w| file_paths << "#{w.root}/CHANGELOG.md" end @@ -58,7 +59,7 @@ def changelog_file_paths def changelog_header_line(h) h.strip! - h[/^## [^\s]+ ((\d+\.)?(\*|\d+))$/, 1] + h[/^## [^\s]+ ((\d+\.)?(\*|x|\d+))$/i, 1] end def changelog_entry_line(e) diff --git a/app/utils/changelog_version.rb b/app/utils/changelog_version.rb index 51e9241ff7..9c68016005 100644 --- a/app/utils/changelog_version.rb +++ b/app/utils/changelog_version.rb @@ -6,24 +6,21 @@ # https://github.com/hitobito/hitobito. class ChangelogVersion - attr_accessor :major_version, :minor_version, :log_entries + attr_accessor :major_version, :minor_version, :log_entries, :version def initialize(header_line) - values = header_line.split(".") + values = header_line.split('.') + @version = header_line @major_version = values.first.to_i - @minor_version = values.second.to_i - @log_entries = [] + @minor_version = values.second.casecmp('x') == 0 ? Float::INFINITY : values.second.to_i + @log_entries = [] end def <=>(other) - [self.major_version, self.minor_version] <=> [other.major_version, other.minor_version] + [major_version, minor_version] <=> [other.major_version, other.minor_version] end def label "Version #{version}" end - - def version - "#{major_version}.#{minor_version}" - end -end \ No newline at end of file +end diff --git a/app/views/changelog/index.html.haml b/app/views/changelog/index.html.haml index b0e9726fdd..dd8e9e2733 100644 --- a/app/views/changelog/index.html.haml +++ b/app/views/changelog/index.html.haml @@ -7,6 +7,6 @@ - ChangelogReader.changelog.each do |changelog_entry| %h2 #{changelog_entry.label} - - changelog_entry.log_entries.each do |change| - %ul - %li #{change} \ No newline at end of file + %ul + - changelog_entry.log_entries.each do |change| + %li #{change} diff --git a/app/views/event/application_market/_popover_waiting_list.html.haml b/app/views/event/application_market/_popover_waiting_list.html.haml index 1d6a764af9..787a04b3ce 100644 --- a/app/views/event/application_market/_popover_waiting_list.html.haml +++ b/app/views/event/application_market/_popover_waiting_list.html.haml @@ -1,8 +1,7 @@ -- # encoding: utf-8 - # Copyright (c) 2012-2015, Pfadibewegung Schweiz. This file is part of -- # hitobito_pbs and licensed under the Affero General Public License version 3 +- # hitobito and licensed under the Affero General Public License version 3 - # or later. See the COPYING file at the top-level directory or at -- # https://github.com/hitobito/hitobito_pbs. +- # https://github.com/hitobito/hitobito. %p.muted= t('.waiting_list_info', event_kind: @event.kind) %br/ diff --git a/app/views/event/kinds/_form.html.haml b/app/views/event/kinds/_form.html.haml index 45938c1fd9..91fc057f00 100644 --- a/app/views/event/kinds/_form.html.haml +++ b/app/views/event/kinds/_form.html.haml @@ -12,8 +12,8 @@ = field_set_tag(t('.qualifications.for_participants')) do %p= t('.help_select_qualifications') - = labeled_qualification_kinds_field(f, @preconditions, :precondition, :participant, - Event::Kind.human_attribute_name(:preconditions)) + = render 'precondition_fields', f: f + = labeled_qualification_kinds_field(f, @qualification_kinds, :qualification, :participant, Event::Kind.human_attribute_name(:qualification_kinds)) = labeled_qualification_kinds_field(f, @prolongations, :prolongation, :participant, diff --git a/app/views/event/kinds/_precondition_fields.html.haml b/app/views/event/kinds/_precondition_fields.html.haml new file mode 100644 index 0000000000..77397e2a10 --- /dev/null +++ b/app/views/event/kinds/_precondition_fields.html.haml @@ -0,0 +1,28 @@ += f.labeled(Event::Kind.human_attribute_name(:preconditions)) do + - kinds = entry.qualification_kinds('precondition', 'participant').group_by(&:id) + + #precondition_summary.inline{style: 'padding-bottom: 5px;', + data: { and: t('event.kinds.qualifications.and'), + or: t('event.kinds.qualifications.or') } } + - entry.grouped_qualification_kind_ids('precondition', 'participant').each_with_index do |ids, index| + .precondition-grouping + - ids.each do |id| + = hidden_field_tag("event_kind[precondition_qualification_kinds][#{index}][qualification_kind_ids][]", id) + + - if index > 0 + %span.muted= t('event.kinds.qualifications.or') + = ids.collect { |id| kinds[id].first.to_s }.sort.to_sentence + = link_to(icon(:trash), '#', class: 'remove-precondition-grouping') + + + = link_to(t('.add_precondition_grouping'), '#', id: 'add_precondition_grouping') + + #precondition_fields.hide + = select_tag('event_kind_precondition_kind_ids', + options_from_collection_for_select(@preconditions, :id, :to_s), + multiple: true, + class: 'span6') + .help-block + .btn-group + = button_tag(t('.add_precondition'), class: 'btn btn-default') + = link_to(t('global.button.cancel'), '#', class: 'link cancel') diff --git a/app/views/event/lists/_nav_left_courses.html.haml b/app/views/event/lists/_nav_left_courses.html.haml index a14f5ffcb2..2224a4e3b3 100644 --- a/app/views/event/lists/_nav_left_courses.html.haml +++ b/app/views/event/lists/_nav_left_courses.html.haml @@ -1,7 +1,7 @@ %ul.nav-left-list - if kind_used? - @grouped_events.keys.each do |kind| - %li= link_to(kind, "##{CGI.escape(kind)}") + %li= link_to(kind, "##{CGI.escape(kind)}", data: { turbolinks: false }) - else = render 'nav_left_events' diff --git a/app/views/event/lists/_nav_left_events.html.haml b/app/views/event/lists/_nav_left_events.html.haml index baedfc7956..ad5f0e15fd 100644 --- a/app/views/event/lists/_nav_left_events.html.haml +++ b/app/views/event/lists/_nav_left_events.html.haml @@ -2,4 +2,4 @@ - @grouped_events.keys.collect(&:split).group_by(&:last).each do |year, months| %li.divider= year - months.each do |month, year| - %li= link_to(month, "##{CGI.escape("#{month} #{year}")}") + %li= link_to(month, "##{CGI.escape("#{month} #{year}")}", data: { turbolinks: false }) diff --git a/app/views/event/lists/courses.html.haml b/app/views/event/lists/courses.html.haml index ff20a21895..481f5de63b 100644 --- a/app/views/event/lists/courses.html.haml +++ b/app/views/event/lists/courses.html.haml @@ -1,4 +1,4 @@ --# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +-# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of -# hitobito and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito. @@ -6,7 +6,7 @@ - title t('.title') - manager = can?(:list_all, Event::Course) -%p= t('.explanation') +%p= t('.explanation') unless can?(:list_all, Event::Course) - content_for :toolbar do - if manager @@ -16,19 +16,20 @@ %p - if can?(:export_list, Event::Course) - = action_button(t('event.lists.courses.csv_export_button'), params.merge(format: :csv), :download) + = Dropdown::Event::EventsExport.new(self, params).to_s = render_extensions :buttons #main %section - = grouped_table(@grouped_events, manager ? 4 : 3) do |event| + = grouped_table(@grouped_events, manager || display_any_booking_info? ? 4 : 3) do |event| %td %strong = event.labeled_link %td= event.dates_full + - if manager || display_any_booking_info? + %td= manager || event.display_booking_info? ? event.booking_info : '' - if manager - %td= event.booking_info %td= event.state_translated - else - %td= button_action_event_apply(event) + %td.center= button_action_event_apply(event) diff --git a/app/views/event/lists/events.html.haml b/app/views/event/lists/events.html.haml index 85cdea0da0..1fb327bd53 100644 --- a/app/views/event/lists/events.html.haml +++ b/app/views/event/lists/events.html.haml @@ -15,4 +15,4 @@ = event.labeled_link %td= event.dates_full %td= event.description_short - %td= button_action_event_apply(event) + %td.center= button_action_event_apply(event) diff --git a/app/views/event/participation_contact_datas/_address_fields.html.haml b/app/views/event/participation_contact_datas/_address_fields.html.haml new file mode 100644 index 0000000000..8311df6a86 --- /dev/null +++ b/app/views/event/participation_contact_datas/_address_fields.html.haml @@ -0,0 +1,19 @@ +-# Copyright (c) 2012-2017, Pfadibewegung Schweiz. This file is part of +-# hitobito_pbs and licensed under the Affero General Public License version 3 +-# or later. See the COPYING file at the top-level directory or at +-# https://github.com/hitobito/hitobito_pbs. + += f.labeled_text_area(:address, rows: 2) if entry.show_attr?(:address) += f.labeled_input_field(:zip_code, class: 'span2') if entry.show_attr?(:zip_code) += f.labeled_input_field(:town, class: 'span4') if entry.show_attr?(:town) +- if entry.show_attr?(:country) + = f.labeled(:country) do + .span6{style: 'margin-left: 0px'} + = country_select(f.object.class.model_name.param_key, + 'country', + { priority_countries: Settings.countries.prioritized, + include_blank: true }, + { class: 'chosen-select', + data: { placeholder: ' ', + chosen_no_results: t('global.chosen_no_results') } }) + = entry.required_attr?(:country) ? content_tag(:span, class: 'required') { '*' } : '' diff --git a/app/views/event/participation_contact_datas/edit.html.haml b/app/views/event/participation_contact_datas/edit.html.haml new file mode 100644 index 0000000000..f2472f0a0d --- /dev/null +++ b/app/views/event/participation_contact_datas/edit.html.haml @@ -0,0 +1,43 @@ +-# Copyright (c) 2012-2017, Pfadibewegung Schweiz. This file is part of +-# hitobito_pbs and licensed under the Affero General Public License version 3 +-# or later. See the COPYING file at the top-level directory or at +-# https://github.com/hitobito/hitobito_pbs. + +- title t('.title') + += render 'event/participations/step_wizard', step: 1 + += crud_form(entry, + { cancel_url: group_event_path(group, event), + buttons_bottom: true, + submit_label: t('.save'), + url: contact_data_group_event_participations_path(group, event, + {event_role: { type: params[:event_role][:type] }})}) do |f| + + = field_set_tag do + - [:first_name, :last_name, :nickname, :company_name].each do |a| + = f.labeled_input_field(a) if entry.show_attr?(a) + + = render 'address_fields', f: f + + - if entry.show_attr?(:email) + = f.labeled_input_field :email, help_inline: t('people.email_field.used_as_login') + + - Event::ParticipationContactData.contact_associations.each do |a| + = field_set_tag do + - unless entry.hidden_contact_attrs.include?(a) + = f.labeled_inline_fields_for a, "contactable/#{a.to_s.singularize}_fields" + + = field_set_tag do + - if entry.show_attr?(:gender) + = f.labeled(:gender) do + - (Person::GENDERS + ['']).each do |key| + = f.inline_radio_button(:gender, key, entry.gender_label(key)) + + - if entry.show_attr?(:birthday) + = f.labeled_string_field(:birthday, + value: f.date_value(:birthday), + help_inline: t('people.fields.format_birthday'), + class: 'span2') + + = render_extensions :fields, locals: { f: f } diff --git a/app/views/event/participations/_actions_approval.html.haml b/app/views/event/participations/_actions_approval.html.haml index 775042349c..7f06624bf3 100644 --- a/app/views/event/participations/_actions_approval.html.haml +++ b/app/views/event/participations/_actions_approval.html.haml @@ -1,10 +1,12 @@ -- unless @application.approved? - = action_button("  #{t('.approve_button')}".html_safe, - approve_group_event_application_path(@group, @event, @application), - nil, - method: :put) -- unless @application.rejected? - = action_button("×  #{t('.reject_button')}".html_safe, - reject_group_event_application_path(@group, @event, @application), - nil, - method: :delete) +- if @event.requires_approval? && can?(:approve, @application) + + - unless @application.approved? + = action_button("  #{t('.approve_button')}".html_safe, + approve_group_event_application_path(@group, @event, @application), + nil, + method: :put) + - unless @application.rejected? + = action_button("×  #{t('.reject_button')}".html_safe, + reject_group_event_application_path(@group, @event, @application), + nil, + method: :delete) diff --git a/app/views/event/participations/_actions_index.html.haml b/app/views/event/participations/_actions_index.html.haml index d2a27f5dcf..f00129c51b 100644 --- a/app/views/event/participations/_actions_index.html.haml +++ b/app/views/event/participations/_actions_index.html.haml @@ -6,6 +6,8 @@ - if can?(:new, @event.new_role) = Dropdown::Event::RoleAdd.new(self, @group, @event) += render 'invoices/button_new', group: @group, people: entries.collect(&:person) + = dropdown_people_export(can?(:show_details, entries.first)) = action_button(t('global.button.print'), 'javascript:window.print();', :print) diff --git a/app/views/event/participations/_actions_show.html.haml b/app/views/event/participations/_actions_show.html.haml index c4d117b0fa..8697683bae 100644 --- a/app/views/event/participations/_actions_show.html.haml +++ b/app/views/event/participations/_actions_show.html.haml @@ -21,7 +21,6 @@ - if can?(:destroy, entry) = button_action_destroy - - if @event.requires_approval? && can?(:approve, @application) - = render 'actions_approval' + = render 'actions_approval' = render_extensions :actions_show diff --git a/app/views/event/participations/_answers.html.haml b/app/views/event/participations/_answers.html.haml index ccfa9d4540..e2d0eb2b8d 100644 --- a/app/views/event/participations/_answers.html.haml +++ b/app/views/event/participations/_answers.html.haml @@ -1,10 +1,11 @@ --# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +-# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of -# hitobito and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito. -= section t('event.participations.specific_information') do - %dl - - @answers.each do |answer| - %dt= answer.question.question - %dd= answer.answer || t('event.participations.no_answer_given') +- if answers.present? + = section title do + %dl + - answers.each do |answer| + %dt= answer.question.question + %dd= answer.answer || t('event.participations.no_answer_given') diff --git a/app/views/event/participations/_apply_to.html.haml b/app/views/event/participations/_apply_to.html.haml index d74ca6174b..04d7a62e2c 100644 --- a/app/views/event/participations/_apply_to.html.haml +++ b/app/views/event/participations/_apply_to.html.haml @@ -1,11 +1,11 @@ -# Copyright (c) 2012-2015, Pfadibewegung Schweiz. This file is part of --# hitobito_pbs and licensed under the Affero General Public License version 3 +-# hitobito and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at --# https://github.com/hitobito/hitobito_pbs. +-# https://github.com/hitobito/hitobito. - if entry.person == current_user && @application.present? && @application.contact.present? = section t('event.applied_to') do - contact_type = @application.contact.class.base_class.name.underscore = render "#{contact_type.pluralize}/contact_data", contact_type.to_sym => @application.contact, - only_public: true \ No newline at end of file + only_public: true diff --git a/app/views/event/participations/_attrs.html.haml b/app/views/event/participations/_attrs.html.haml index 660c97e28c..c68583c98b 100644 --- a/app/views/event/participations/_attrs.html.haml +++ b/app/views/event/participations/_attrs.html.haml @@ -7,7 +7,7 @@ %article.span6 = render 'people/contact_data', person: entry.person, only_public: cannot?(:show_details, entry) - = render_attrs(entry.person, :birthday, :gender) + = render_attrs(entry.person, :layer_group, :birthday, :gender) - if can?(:show_details, entry) = render 'application_details' @@ -16,7 +16,14 @@ folder: :people, locals: { entry: entry.person, show_full: false } - = render 'answers' if @answers.present? + = render 'answers', + answers: @answers.reject(&:admin?), + title: t('event.participations.application_answers') + + - if can?(:show_full, entry) + = render 'answers', + answers: @answers.select(&:admin?), + title: t('event.participations.admin_answers') = section t('activerecord.attributes.event/participation.additional_information') do = simple_format(entry.additional_information || '-') diff --git a/app/views/event/participations/_form.html.haml b/app/views/event/participations/_form.html.haml index 22c874d0f8..695f4a5c83 100644 --- a/app/views/event/participations/_form.html.haml +++ b/app/views/event/participations/_form.html.haml @@ -5,6 +5,8 @@ - if entry.new_record? - title t('.title_new', role: entry.roles.first.class.model_name.human) + - unless params[:for_someone_else] + = render 'event/participations/step_wizard', step: 2 - else - title t('.title_edit', person: entry.person) @@ -21,9 +23,13 @@ - if params[:for_someone_else] = f.labeled_person_field(:person) - = f.fields_for(:answers) do |fans| + = f.fields_for(:answers, @answers.reject(&:admin?)) do |fans| = render 'event/answers/fields', f: fans + - if params[:for_someone_else] || entry.persisted? + = f.fields_for(:answers, @answers.select(&:admin?)) do |fans| + = render 'event/answers/fields', f: fans + = f.labeled_text_area(:additional_information) - if entry.application && entry.new_record? diff --git a/app/views/event/participations/_list.html.haml b/app/views/event/participations/_list.html.haml index a5439114ca..d5da7f437b 100644 --- a/app/views/event/participations/_list.html.haml +++ b/app/views/event/participations/_list.html.haml @@ -1,4 +1,4 @@ --# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +-# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of -# hitobito and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito. @@ -14,7 +14,7 @@ - t.col('') do |p| .profil = image_tag(p.person.picture.thumb.url, size: '32x32') - - sortable_grouped_person_attr(t, %w(last_name first_name nickname)) do |p| + - sortable_grouped_person_attr(t, last_name: true, first_name: true, nickname: true) do |p| %strong -# Any person listed can be shown = link_to(p.to_s(:list), group_event_participation_path(@group, @event, p)) @@ -22,6 +22,9 @@ - t.col(event_participations_roles_header(t)) { |p| event_participations_roles_content(p) } - t.col(Person.human_attribute_name(:emails)) { |p| p.all_emails(cannot?(:show_details, p)) } - t.col(PhoneNumber.model_name.human(count: 2)) { |p| p.all_phone_numbers(cannot?(:show_details, p)) } - - sortable_grouped_person_attr(t, %w(zip_code town), :address) { |p| p.complete_address } + - sortable_grouped_person_attr(t, address: false, zip_code: true, town: true) { |p| p.complete_address } + - t.col(t.sort_header(:birthday, Person.human_attribute_name(:birthday))) do |p| + = format_attr(p.person, :birthday) + - t.col(Person.human_attribute_name(:layer_group)) { |p| p.layer_group_label } = render_extensions :list, locals: { table: t } diff --git a/app/views/event/participations/_precondition_warnings.html.haml b/app/views/event/participations/_precondition_warnings.html.haml index dd386a95ec..629716ff5a 100644 --- a/app/views/event/participations/_precondition_warnings.html.haml +++ b/app/views/event/participations/_precondition_warnings.html.haml @@ -1,8 +1,8 @@ -# Copyright (c) 2012-2015, Pfadibewegung Schweiz. This file is part of --# hitobito_pbs and licensed under the Affero General Public License version 3 +-# hitobito and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at --# https://github.com/hitobito/hitobito_pbs. +-# https://github.com/hitobito/hitobito. - if @precondition_warnings.present? .alert.alert-warning - %p= simple_format(@precondition_warnings.flatten.join("\n")) \ No newline at end of file + %p= simple_format(@precondition_warnings.flatten.join("\n")) diff --git a/app/views/event/participations/_step_wizard.html.haml b/app/views/event/participations/_step_wizard.html.haml new file mode 100644 index 0000000000..6dec873267 --- /dev/null +++ b/app/views/event/participations/_step_wizard.html.haml @@ -0,0 +1,18 @@ +-# Copyright (c) 2012-2017, Pfadibewegung Schweiz. This file is part of +-# hitobito_pbs and licensed under the Affero General Public License version 3 +-# or later. See the COPYING file at the top-level directory or at +-# https://github.com/hitobito/hitobito_pbs. + +.stepwizard + .stepwizard-step{class: (step == 1 ? 'is-current' : 'is-disabled')} + %a.stepwizard-link{href: '#'} + .stepwizard-number + 1 + .stepwizard-title + = t('event.participation_contact_datas.edit.title') + .stepwizard-step{class: (step == 2 ? 'is-current' : 'is-disabled')} + %a.stepwizard-link{href: '#'} + .stepwizard-number + 2 + .stepwizard-title + = t('event.participations.edit.title') diff --git a/app/views/event/qualifications/_action_link_columns.html.haml b/app/views/event/qualifications/_action_link_columns.html.haml deleted file mode 100644 index 643fcfb8c4..0000000000 --- a/app/views/event/qualifications/_action_link_columns.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -%td.issue - = p.issue_action(group) -%td.revoke - = p.revoke_action(group) diff --git a/app/views/event/qualifications/_checkbox.html.haml b/app/views/event/qualifications/_checkbox.html.haml new file mode 100644 index 0000000000..85c3f80dca --- /dev/null +++ b/app/views/event/qualifications/_checkbox.html.haml @@ -0,0 +1,6 @@ +-# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +-# hitobito and licensed under the Affero General Public License version 3 +-# or later. See the COPYING file at the top-level directory or at +-# https://github.com/hitobito/hitobito. + += check_box_tag('participation_ids[]', participation.id, participation.qualified?) diff --git a/app/views/event/qualifications/_list.html.haml b/app/views/event/qualifications/_list.html.haml index 24cf346cd2..415fa3dd52 100644 --- a/app/views/event/qualifications/_list.html.haml +++ b/app/views/event/qualifications/_list.html.haml @@ -1,4 +1,4 @@ --# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +-# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of -# hitobito and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito. @@ -7,7 +7,7 @@ %tbody - entries.each do |p| %tr{id: dom_id(p)} - = render 'action_link_columns', p: p, group: @group + %td.center= render 'checkbox', participation: p %td %strong= link_to(p.to_s(:list), group_event_participation_path(@group, @event, p)) %td= p.roles_short diff --git a/app/views/event/qualifications/_people.html.haml b/app/views/event/qualifications/_people.html.haml new file mode 100644 index 0000000000..95592c3959 --- /dev/null +++ b/app/views/event/qualifications/_people.html.haml @@ -0,0 +1,9 @@ +- if @event.kind.qualification_kinds(%w(qualification prolongation), 'leader').to_a.present? + %p= event.issued_qualifications_info_for_leaders + + = render 'list', entries: @leaders + +- if @event.kind.qualification_kinds(%w(qualification prolongation), 'participant').to_a.present? + %p= event.issued_qualifications_info_for_participants + + = render 'list', entries: @participants diff --git a/app/views/event/qualifications/index.html.haml b/app/views/event/qualifications/index.html.haml index 7ae4c540cf..6b0e26394e 100644 --- a/app/views/event/qualifications/index.html.haml +++ b/app/views/event/qualifications/index.html.haml @@ -1,18 +1,15 @@ --# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +-# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of -# hitobito and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito. --title event.to_s +- title event.to_s -= render_extensions(:index) += form_tag(nil, method: :put) do -- if @event.kind.qualification_kinds(%w(qualification prolongation), 'leader').to_a.present? - %p= event.issued_qualifications_info_for_leaders + = render 'people' - = render 'list', entries: @leaders - -- if @event.kind.qualification_kinds(%w(qualification prolongation), 'participant').to_a.present? - %p= event.issued_qualifications_info_for_participants - - = render 'list', entries: @participants + .btn-group + = button_tag(t('event.qualifications.index.save'), + class: 'btn btn-primary', + data: { disable_with: t('event.qualifications.index.save') }) diff --git a/app/views/event/qualifications/qualification.js.haml b/app/views/event/qualifications/qualification.js.haml deleted file mode 100644 index 82eccc2c8c..0000000000 --- a/app/views/event/qualifications/qualification.js.haml +++ /dev/null @@ -1,10 +0,0 @@ --# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of --# hitobito and licensed under the Affero General Public License version 3 --# or later. See the COPYING file at the top-level directory or at --# https://github.com/hitobito/hitobito. - -- if @nothing_changed - alert("#{j(t('event.no_qualifications_could_be_prolonged', person: participation.person))}"); - -$('##{dom_id(participation)} td.issue').html('#{escape_javascript(participation.issue_action(@group))}') -$('##{dom_id(participation)} td.revoke').html('#{escape_javascript(participation.revoke_action(@group))}') diff --git a/app/views/event/questions/_fields.html.haml b/app/views/event/questions/_fields.html.haml index 01eb6608f2..22bc5369a1 100644 --- a/app/views/event/questions/_fields.html.haml +++ b/app/views/event/questions/_fields.html.haml @@ -1,4 +1,4 @@ --# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +-# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of -# hitobito and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito. @@ -6,6 +6,7 @@ = f.labeled_input_field(:question, help_inline: f.link_to_remove(ta(:remove))) = f.labeled_input_field(:choices) = f.labeled_input_field(:multiple_choices) -= f.labeled_input_field(:required) += f.labeled_input_field(:required) unless admin + .controls.fields-separation %hr diff --git a/app/views/events/_actions_index.html.haml b/app/views/events/_actions_index.html.haml index 072665b4cd..d403a5e331 100644 --- a/app/views/events/_actions_index.html.haml +++ b/app/views/events/_actions_index.html.haml @@ -5,3 +5,4 @@ = new_event_button = export_events_button += export_events_ical_button diff --git a/app/views/events/_actions_show.html.haml b/app/views/events/_actions_show.html.haml index 3ba9b7ff0b..4c09fea1fb 100644 --- a/app/views/events/_actions_show.html.haml +++ b/app/views/events/_actions_show.html.haml @@ -1,10 +1,16 @@ --# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +-# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of -# hitobito and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito. -= button_action_event_apply(entry, @group) +- if !@user_participation && event_user_application_possible?(entry) + = Dropdown::Event::ParticipantAdd.for_user(self, @group, entry, current_user) = button_action_edit if can?(:update, entry) = button_action_destroy if can?(:destroy, entry) +- if can?(:new, entry) + = action_button(t('events.global.link.duplicate'), + new_group_event_path(@group, source_id: entry.id), + :book) += export_events_ical_button = render_extensions :actions_show diff --git a/app/views/events/_additional_fields.html.haml b/app/views/events/_additional_fields.html.haml deleted file mode 100644 index 30ff208c1d..0000000000 --- a/app/views/events/_additional_fields.html.haml +++ /dev/null @@ -1,10 +0,0 @@ --# Copyright (c) 2015, insieme Schweiz. This file is part of --# hitobito_insieme and licensed under the Affero General Public License version 3 --# or later. See the COPYING file at the top-level directory or at --# https://github.com/hitobito/hitobito_insieme. - -- if entry.role_types.present? - = field_set_tag t('events.form.additional_information') do - %p= t('events.form.explain_application_questions') - - = f.nested_fields_for :questions, 'event/questions/fields' diff --git a/app/views/events/_admin_questions_fields.haml b/app/views/events/_admin_questions_fields.haml new file mode 100644 index 0000000000..7dec3e1325 --- /dev/null +++ b/app/views/events/_admin_questions_fields.haml @@ -0,0 +1,11 @@ +-# Copyright (c) 2015 - 2017, Pfadibewegung Schweiz. This file is part of +-# hitobito and licensed under the Affero General Public License version 3 +-# or later. See the COPYING file at the top-level directory or at +-# https://github.com/hitobito/hitobito. + + +%p= t('events.form.explain_admin_questions') + += field_set_tag nil do + = f.nested_fields_for :admin_questions do |fields| + = render 'event/questions/fields', f: fields, admin: true diff --git a/app/views/events/_application_fields.html.haml b/app/views/events/_application_fields.html.haml index cc477ab6bb..17c8a9ac61 100644 --- a/app/views/events/_application_fields.html.haml +++ b/app/views/events/_application_fields.html.haml @@ -1,26 +1,34 @@ -# Copyright (c) 2015, insieme Schweiz. This file is part of --# hitobito_insieme and licensed under the Affero General Public License version 3 +-# hitobito and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at --# https://github.com/hitobito/hitobito_insieme. +-# https://github.com/hitobito/hitobito. -- if entry.participant_types.present? - = field_set_tag do - = f.labeled_input_fields(*entry.used_attributes(:application_opening_at, :application_closing_at, :application_conditions, :maximum_participants)) - - entry.used?(:external_applications) do - = f.labeled_boolean_field(:external_applications, caption: t('events.form.caption_external_applications')) += field_set_tag do + = f.labeled_input_fields(*entry.used_attributes(:application_opening_at, :application_closing_at, :application_conditions, :maximum_participants)) - - entry.used?(:priorization) do - = f.labeled_boolean_field(:priorization, caption: t('events.form.caption_prioritization')) + - entry.used?(:external_applications) do + = f.labeled_boolean_field(:external_applications, caption: t('events.form.caption_external_applications')) - - application_approve_role_exists? && entry.used?(:requires_approval) do - = f.labeled_boolean_field(:requires_approval, caption: t('events.form.caption_requires_approval')) + - entry.used?(:priorization) do + = f.labeled_boolean_field(:priorization, caption: t('events.form.caption_prioritization')) - - entry.used?(:signature) do - = f.labeled_boolean_field(:signature, caption: t('.caption_signature')) - - entry.used?(:signature_confirmation) do - = f.labeled_boolean_field(:signature_confirmation, caption: t('.caption_signature_confirmation')) - - entry.used?(:signature_confirmation_text) do - = f.labeled_input_field(:signature_confirmation_text, value: entry.signature_confirmation_text || t('.signature_confirmation_text_default')) + - application_approve_role_exists? && entry.used?(:requires_approval) do + = f.labeled_boolean_field(:requires_approval, caption: t('events.form.caption_requires_approval')) - = render_extensions 'application_fields', locals: { f: f } + - entry.used?(:signature) do + = f.labeled_boolean_field(:signature, caption: t('.caption_signature')) + - entry.used?(:signature_confirmation) do + = f.labeled_boolean_field(:signature_confirmation, caption: t('.caption_signature_confirmation')) + - entry.used?(:signature_confirmation_text) do + = f.labeled_input_field(:signature_confirmation_text, value: entry.signature_confirmation_text || t('.signature_confirmation_text_default')) + + - entry.used?(:applications_cancelable) do + = f.labeled_boolean_field(:applications_cancelable, + caption: t('events.form.caption_applications_cancelable')) + + - entry.used?(:display_booking_info) do + = f.labeled_boolean_field(:display_booking_info, + caption: t('events.form.caption_display_booking_info')) + + = render_extensions 'application_fields', locals: { f: f } diff --git a/app/views/events/_application_questions_fields.haml b/app/views/events/_application_questions_fields.haml new file mode 100644 index 0000000000..1fd2191ad6 --- /dev/null +++ b/app/views/events/_application_questions_fields.haml @@ -0,0 +1,11 @@ +-# Copyright (c) 2015, insieme Schweiz. This file is part of +-# hitobito and licensed under the Affero General Public License version 3 +-# or later. See the COPYING file at the top-level directory or at +-# https://github.com/hitobito/hitobito. + + +%p= t('events.form.explain_application_questions') + += field_set_tag nil do + = f.nested_fields_for :application_questions do |fields| + = render 'event/questions/fields', f: fields, admin: false diff --git a/app/views/events/_attrs.html.haml b/app/views/events/_attrs.html.haml index dbd36bbe6f..c752a9cd20 100644 --- a/app/views/events/_attrs.html.haml +++ b/app/views/events/_attrs.html.haml @@ -3,6 +3,9 @@ -# or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito. +- if @user_participation + = render 'banner_participating' + #main.row-fluid %article.span7 = render 'attrs_primary' diff --git a/app/views/events/_attrs_application.html.haml b/app/views/events/_attrs_application.html.haml index fca97f2c0c..0f3ace941f 100644 --- a/app/views/events/_attrs_application.html.haml +++ b/app/views/events/_attrs_application.html.haml @@ -14,15 +14,15 @@ - if entry.course_kind? = labeled(Event::Kind.human_attribute_name(:minimum_age), - entry.kind.minimum_age? ? t('events.minimum_age_with_years', minimum_age: entry.kind.minimum_age) : '') - = labeled(t('events.preconditions'), entry.kind.qualification_kinds('precondition', 'participant').join(', ')) + entry.kind.minimum_age? ? t('events.minimum_age_with_years', minimum_age: entry.kind.minimum_age) : '') + = labeled(t('events.preconditions'), grouped_qualification_kinds_string(entry.kind, 'precondition', 'participant')) - if entry.course_kind? %dl.dl-horizontal = labeled(Event::Kind.human_attribute_name(:qualification_kinds), - entry.kind.qualification_kinds('qualification', 'participant').join(', ')) + entry.kind.qualification_kinds('qualification', 'participant').join(', ')) = labeled(Event::Kind.human_attribute_name(:prolongations), - entry.kind.qualification_kinds('prolongation', 'participant').join(', ')) + entry.kind.qualification_kinds('prolongation', 'participant').join(', ')) = render_attrs(entry, *entry.used_attributes(:signature, :signature_confirmation)) diff --git a/app/views/events/_banner_participating.html.haml b/app/views/events/_banner_participating.html.haml new file mode 100644 index 0000000000..d13eade80a --- /dev/null +++ b/app/views/events/_banner_participating.html.haml @@ -0,0 +1,10 @@ +-# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +-# hitobito and licensed under the Affero General Public License version 3 +-# or later. See the COPYING file at the top-level directory or at +-# https://github.com/hitobito/hitobito. + +.alert.alert-success + = t("event.participations.cancel_application.explanation") + - if can?(:destroy, @user_participation) +   + = action_button_cancel_participation diff --git a/app/views/events/_contact_attr_fields.html.haml b/app/views/events/_contact_attr_fields.html.haml new file mode 100644 index 0000000000..1b5eac807d --- /dev/null +++ b/app/views/events/_contact_attr_fields.html.haml @@ -0,0 +1,9 @@ +-# Copyright (c) 2017, Pfadibewegung Schweiz. This file is part of +-# hitobito and licensed under the Affero General Public License version 3 +-# or later. See the COPYING file at the top-level directory or at +-# https://github.com/hitobito/hitobito. + +%p= t('events.form.explain_contact_attrs') + += field_set_tag nil do + = ContactAttrs::ControlBuilder.new(f, entry).render diff --git a/app/views/events/_default_description_link.html.haml b/app/views/events/_default_description_link.html.haml index bb399e7ebd..17440359d2 100644 --- a/app/views/events/_default_description_link.html.haml +++ b/app/views/events/_default_description_link.html.haml @@ -1,8 +1,5 @@ -.controls{style: 'display: none' } - %span.help-inline - %a.standard-description-link - = t('activerecord.attributes.event/kind.general_information') +%a.standard-description-link{ href: '#' }= t('.insert_general_information') - @kinds.each do |event_kind| - %p.hide.default-description{data: {kind: event_kind.id} } + %span.hide.default-description{ data: { kind: event_kind.id } } = event_kind.general_information diff --git a/app/views/events/_form.html.haml b/app/views/events/_form.html.haml index e1bcec6307..3239667fb0 100644 --- a/app/views/events/_form.html.haml +++ b/app/views/events/_form.html.haml @@ -1,4 +1,4 @@ --# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +-# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of -# hitobito and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito. @@ -18,10 +18,21 @@ #dates.tab-pane = render 'date_fields', f: f - #application.tab-pane - = render 'application_fields', f: f - = render 'additional_fields', f: f + - if entry.participant_types.present? + #application.tab-pane + = render 'application_fields', f: f = render_extensions 'form_tab_pane', locals: { f: f } + - if entry.role_types.present? + #application_questions.tab-pane + = render 'application_questions_fields', f: f + + #admin_questions.tab-pane + = render 'admin_questions_fields', f: f + + - if entry.participant_types.present? + #contact_attrs.tab-pane + = render 'contact_attr_fields', f: f + = render_extensions 'form_actions_caption', locals: { f: f } diff --git a/app/views/events/_form_tabs.html.haml b/app/views/events/_form_tabs.html.haml index de82c15605..77afcf81d2 100644 --- a/app/views/events/_form_tabs.html.haml +++ b/app/views/events/_form_tabs.html.haml @@ -1,6 +1,20 @@ +-# Copyright (c) 2015 - 2017, insieme Schweiz. This file is part of +-# hitobito and licensed under the Affero General Public License version 3 +-# or later. See the COPYING file at the top-level directory or at +-# https://github.com/hitobito/hitobito. + %ul.nav.nav-tabs %li.active= link_to(t('events.form_tabs.general'), '#general', data: { toggle: 'tab' }) %li= link_to(Event.human_attribute_name(:dates), '#dates', data: { toggle: 'tab' }) - %li= link_to(Event::Application.model_name.human, '#application', data: { toggle: 'tab' }) + - if entry.participant_types.present? + %li= link_to(Event::Application.model_name.human, '#application', data: { toggle: 'tab' }) = render_extensions :form_tabs + + - if entry.role_types.present? + %li= link_to(t('events.form_tabs.application_questions'), '#application_questions', data: { toggle: 'tab' }) + %li= link_to(t('events.form_tabs.admin_questions'), '#admin_questions', data: { toggle: 'tab' }) + + - if entry.participant_types.present? + %li= link_to(t('events.form_tabs.contact_attrs'), '#contact_attrs', data: { toggle: 'tab' }) + diff --git a/app/views/events/_general_fields.html.haml b/app/views/events/_general_fields.html.haml index 6cc46f5590..601c94601b 100644 --- a/app/views/events/_general_fields.html.haml +++ b/app/views/events/_general_fields.html.haml @@ -1,7 +1,7 @@ -# Copyright (c) 2015, insieme Schweiz. This file is part of --# hitobito_insieme and licensed under the Affero General Public License version 3 +-# hitobito and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at --# https://github.com/hitobito/hitobito_insieme. +-# https://github.com/hitobito/hitobito. = field_set_tag do = f.labeled_input_field(:name) @@ -21,8 +21,8 @@ = f.labeled_input_field(:number) - entry.used?(:description) do - = render partial: 'default_description_link' if entry.kind_class == Event::Kind - = f.labeled_input_field(:description) + - link = entry.kind_class == Event::Kind ? render('default_description_link') : nil + = f.labeled_input_field(:description, help: link) = f.labeled_input_fields(*entry.used_attributes(:motto, :cost)) diff --git a/app/views/events/_group_fields.html.haml b/app/views/events/_group_fields.html.haml index d8f58dda3b..c49d9b8940 100644 --- a/app/views/events/_group_fields.html.haml +++ b/app/views/events/_group_fields.html.haml @@ -1,7 +1,7 @@ -# Copyright (c) 2015, insieme Schweiz. This file is part of --# hitobito_insieme and licensed under the Affero General Public License version 3 +-# hitobito and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at --# https://github.com/hitobito/hitobito_insieme. +-# https://github.com/hitobito/hitobito. - entry.used?(:group_ids) do - if @groups.present? diff --git a/app/views/events/_list.html.haml b/app/views/events/_list.html.haml index 32e9f075f6..c098df00fd 100644 --- a/app/views/events/_list.html.haml +++ b/app/views/events/_list.html.haml @@ -8,7 +8,10 @@ = render 'shared/page_per_year' = crud_table do |t| - -t.col(Event.human_attribute_name(:name)) do |e| + - t.col(t.attr_header(:name)) do |e| %strong= link_to e.name, group_event_path(@group, e) - -t.attrs(:dates_full, :booking_info) + - t.attr(:dates_full) + - t.attr(:description_short, t.attr_header(:description)) + - t.attr(:booking_info) + - t.col(nil, class: 'center') { |e| button_action_event_apply(e, @group) } diff --git a/app/views/full_text/_events.html.haml b/app/views/full_text/_events.html.haml new file mode 100644 index 0000000000..24630fd581 --- /dev/null +++ b/app/views/full_text/_events.html.haml @@ -0,0 +1,5 @@ += table(@events, class: 'table table-striped table-hover') do |t| + - t.col(Event.human_attribute_name(:name)) do |e| + - content_tag(:strong, link_to(e, e)) + - t.col(Event.human_attribute_name(:dates)) { |e| format_attr(e, :dates) } + - t.col(Group.model_name.human) { |e| format_attr(e, :groups) } diff --git a/app/views/full_text/_groups.html.haml b/app/views/full_text/_groups.html.haml new file mode 100644 index 0000000000..94f9eab9ec --- /dev/null +++ b/app/views/full_text/_groups.html.haml @@ -0,0 +1,5 @@ += table(@groups, class: 'table table-striped table-hover') do |t| + - t.col(Group.human_attribute_name(:name)) do |g| + - content_tag(:strong, link_to(g, g)) + - t.col(Group.human_attribute_name(:layer_group)) do |g| + - link_to(format_attr(g, :layer_group), g.layer_group) diff --git a/app/views/full_text/_people.html.haml b/app/views/full_text/_people.html.haml new file mode 100644 index 0000000000..95a89990c4 --- /dev/null +++ b/app/views/full_text/_people.html.haml @@ -0,0 +1,11 @@ += crud_table do |t| + - t.col('') do |p| + .profil= image_tag(p.picture.thumb.url, size: '32x32') + - t.col(Person.human_attribute_name(:name)) do |p| + %strong + -# Any person listed can be shown + = link_to(p.to_s(:list), group_person_path(p.default_group_id, p)) + - t.col(Role.model_name.human(count: 2)) { |p| p.decorate.roles_short(nil) } + - t.col(Person.human_attribute_name(:emails)) { |p| p.decorate.all_emails(true) } + - t.col(PhoneNumber.model_name.human(count: 2)) { |p| p.decorate.all_phone_numbers(true) } + - t.col(Person.human_attribute_name(:address)) { |p| p.decorate.complete_address } diff --git a/app/views/full_text/index.html.haml b/app/views/full_text/index.html.haml index c8551178af..172c762f00 100644 --- a/app/views/full_text/index.html.haml +++ b/app/views/full_text/index.html.haml @@ -5,22 +5,17 @@ - title t('.title') -= paginate(entries) - #main - = crud_table do |t| - - t.col('') do |p| - .profil= image_tag(p.picture.thumb.url, size: '32x32') - - t.col(Person.human_attribute_name(:name)) do |p| - %strong - -# Any person listed can be shown - = link_to(p.to_s(:list), group_person_path(p.default_group_id, p)) - - t.col(Role.model_name.human(count: 2)) { |p| p.roles_short(nil) } - - t.col(Person.human_attribute_name(:emails)) { |p| p.all_emails(true) } - - t.col(PhoneNumber.model_name.human(count: 2)) { |p| p.all_phone_numbers(true) } - - t.col(Person.human_attribute_name(:address)) { |p| p.complete_address } + - if params[:q].to_s.size < 2 + %p= t('.incomplete_search_request') -- if params[:q].to_s.size < 2 - %p= t('.incomplete_search_request') + - else + %ul.nav.nav-tabs + %li.active= link_to(Person.model_name.human(count: 2), '#people', data: { toggle: 'tab' }) + %li= link_to(Group.model_name.human(count: 2), '#groups', data: { toggle: 'tab' }) + %li= link_to(Event.model_name.human(count: 2), '#events', data: { toggle: 'tab' }) -= paginate(entries) + .tab-content + #people.tab-pane.active= render 'people' + #groups.tab-pane= render 'groups' + #events.tab-pane= render 'events' diff --git a/app/views/group/deleted_people/_list.html.haml b/app/views/group/deleted_people/_list.html.haml new file mode 100644 index 0000000000..c4652a3cdf --- /dev/null +++ b/app/views/group/deleted_people/_list.html.haml @@ -0,0 +1,36 @@ +-# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +-# hitobito and licensed under the Affero General Public License version 3 +-# or later. See the COPYING file at the top-level directory or at +-# https://github.com/hitobito/hitobito. + +- title @group.to_s + +.pagination-bar + = paginate @people + + .pagination-info + - if @people.total_count > 0 + = t('people.list.number_of_people_shown', count: @people.total_count) + - else + = ti(:no_list_entries) + +- if @people.total_count > 0 + = crud_table do |t| + - t.col('') do |p| + .profil= image_tag(p.picture.thumb.url, size: '32x32') + - sortable_grouped_person_attr(t, last_name: false, first_name: false, nickname: false) do |p| + %strong + = p.to_s(:list) + %br/ + = muted p.additional_name + - t.col(Role.model_name.human(count: 2)) do |p| + - if can?(:create, Role.new(group_id: p.restored_group(@group).id)) + - p.last_role_new_link(@group) + - t.col(Person.human_attribute_name(:emails)) do |p| + - p.all_emails(true) + - t.col(PhoneNumber.model_name.human(count: 2)) do |p| + - p.all_phone_numbers(true) + - sortable_grouped_person_attr(t, address: false, zip_code: false, town: false) do |p| + - p.complete_address + += paginate @people diff --git a/app/views/group/deleted_people/index.html.haml b/app/views/group/deleted_people/index.html.haml new file mode 100644 index 0000000000..02411fc1d6 --- /dev/null +++ b/app/views/group/deleted_people/index.html.haml @@ -0,0 +1,8 @@ +-# Copyright (c) 2012-2015, Jungwacht Blauring Schweiz. This file is part of +-# hitobito and licensed under the Affero General Public License version 3 +-# or later. See the COPYING file at the top-level directory or at +-# https://github.com/hitobito/hitobito. + +- @sheet = Sheet::Group::DeletedPeople.new(self) + += render 'list' diff --git a/app/views/groups/_attrs.html.haml b/app/views/groups/_attrs.html.haml index de1f2264f0..b659f39cae 100644 --- a/app/views/groups/_attrs.html.haml +++ b/app/views/groups/_attrs.html.haml @@ -8,11 +8,13 @@ %h2= t('.contact_details') = render 'contact_data', group: entry, only_public: cannot?(:show_details, @group) - %h2= t('.additional_information') = render_attrs(entry, :type_name) = render_extensions :attrs = render_present_attrs(entry, :created_info, :updated_info, :deleted_info) if can?(:show_details, @group) + - if can?(:index_notes, entry) + = render 'notes/section', create_path: group_notes_path(@group) + %aside.span5.offset1 = render 'child_groups' diff --git a/app/views/groups/deleted_subgroups.html.haml b/app/views/groups/deleted_subgroups.html.haml index df7a041f9a..1e89658aaf 100644 --- a/app/views/groups/deleted_subgroups.html.haml +++ b/app/views/groups/deleted_subgroups.html.haml @@ -1,7 +1,9 @@ --# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +-# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of -# hitobito and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito. -= render 'child_groups' - +- if @sub_groups.present? + = render 'child_groups' +- else + = t('.no_deleted_sub_groups') diff --git a/app/views/invoice_articles/_form.html.haml b/app/views/invoice_articles/_form.html.haml new file mode 100644 index 0000000000..3ea61e2b56 --- /dev/null +++ b/app/views/invoice_articles/_form.html.haml @@ -0,0 +1,12 @@ +-# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +-# hitobito and licensed under the Affero General Public License version 3 +-# or later. See the COPYING file at the top-level directory or at +-# https://github.com/hitobito/hitobito. + += entry_form do |f| + = f.labeled_input_fields :number, :name, :description + = f.labeled_input_field(:category, data: { provide: :typeahead, source: InvoiceArticle.categories }) + + = f.labeled_input_fields :unit_cost, :vat_rate + = f.labeled_input_field(:cost_center, data: { provide: :typeahead, source: InvoiceArticle.cost_centers }) + = f.labeled_input_field(:account, data: { provide: :typeahead, source: InvoiceArticle.accounts }) diff --git a/app/views/invoice_articles/_list.html.haml b/app/views/invoice_articles/_list.html.haml new file mode 100644 index 0000000000..f2546bb3b4 --- /dev/null +++ b/app/views/invoice_articles/_list.html.haml @@ -0,0 +1,11 @@ +-# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +-# hitobito and licensed under the Affero General Public License version 3 +-# or later. See the COPYING file at the top-level directory or at +-# https://github.com/hitobito/hitobito. + += crud_table do |t| + - t.col(t.sort_header(:name)) do |article| + %strong= link_to article.name, group_invoice_article_path(parent, article) + - t.sortable_attrs(:number, :description, :category) + - t.col(t.sort_header(:unit_cost)) { |a| number_to_currency(a.unit_cost) } + - t.sortable_attrs(:vat_rate, :cost_center, :account) diff --git a/app/views/invoice_configs/_actions_show.html.haml b/app/views/invoice_configs/_actions_show.html.haml new file mode 100644 index 0000000000..721eb1c77b --- /dev/null +++ b/app/views/invoice_configs/_actions_show.html.haml @@ -0,0 +1,6 @@ +-# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +-# hitobito and licensed under the Affero General Public License version 3 +-# or later. See the COPYING file at the top-level directory or at +-# https://github.com/hitobito/hitobito. + += button_action_edit if can?(:edit, entry) diff --git a/app/views/invoice_configs/_form.html.haml b/app/views/invoice_configs/_form.html.haml new file mode 100644 index 0000000000..ac40bd0416 --- /dev/null +++ b/app/views/invoice_configs/_form.html.haml @@ -0,0 +1,9 @@ +-# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +-# hitobito and licensed under the Affero General Public License version 3 +-# or later. See the COPYING file at the top-level directory or at +-# https://github.com/hitobito/hitobito. + += standard_form(entry, url: [parent, :invoice_config]) do |f| + = f.error_messages + = f.labeled_input_field :payment_information + = save_form_buttons(f, nil, group_invoice_config_path(parent)) diff --git a/app/views/invoice_lists/_calculated.html.haml b/app/views/invoice_lists/_calculated.html.haml new file mode 100644 index 0000000000..0a1b02a81c --- /dev/null +++ b/app/views/invoice_lists/_calculated.html.haml @@ -0,0 +1,14 @@ +-# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +-# hitobito and licensed under the Affero General Public License version 3 +-# or later. See the COPYING file at the top-level directory or at +-# https://github.com/hitobito/hitobito. + +#calculated.control-group + .controls + %dl.dl-horizontal + %dt.muted= Invoice.human_attribute_name(:cost) + %dd= invoice.cost + %dt.muted= Invoice.human_attribute_name(:vat) + %dd= invoice.vat + %dt.muted= Invoice.human_attribute_name(:total) + %dd= invoice.total diff --git a/app/views/invoice_lists/_form.html.haml b/app/views/invoice_lists/_form.html.haml new file mode 100644 index 0000000000..c2bf3092cc --- /dev/null +++ b/app/views/invoice_lists/_form.html.haml @@ -0,0 +1,25 @@ +-# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +-# hitobito and licensed under the Affero General Public License version 3 +-# or later. See the COPYING file at the top-level directory or at +-# https://github.com/hitobito/hitobito. + += entry_form(url: group_invoice_list_path(parent), + cancel_url: cancel_url, + data: { group: group_path(parent) }) do |f| + + = f.hidden_field :recipient_ids + = f.labeled_input_field :title, help: t('.recipient_info', count: entry.recipients.size) + + = f.labeled(:invoice_item_article) do + = select("temp", "invoice_article_id", + InvoiceArticle.all.pluck(:number, :id), + { include_blank: true }, + { id: "invoice_item_article" }) + + = field_set_tag do + = f.labeled_inline_fields_for :invoice_items, 'invoice_items' + + + = f.labeled_input_field :description + + = render "calculated", invoice: entry.decorate diff --git a/app/views/invoice_lists/_invoice_items.html.haml b/app/views/invoice_lists/_invoice_items.html.haml new file mode 100644 index 0000000000..781c0bf56e --- /dev/null +++ b/app/views/invoice_lists/_invoice_items.html.haml @@ -0,0 +1,10 @@ +-# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +-# hitobito and licensed under the Affero General Public License version 3 +-# or later. See the COPYING file at the top-level directory or at +-# https://github.com/hitobito/hitobito. + += f.input_field(:name, class: 'span2', placeholder: InvoiceItem.human_attribute_name(:name)) += f.input_field(:description, class: 'span2', rows: 1, placeholder: InvoiceItem.human_attribute_name(:description)) += f.input_field(:vat_rate, class: 'span1', data: { recalculate: true }, placeholder: InvoiceItem.human_attribute_name(:vat_rate)) += f.input_field(:unit_cost, class: 'span1', data: { recalculate: true }, placeholder: InvoiceItem.human_attribute_name(:unit_cost)) += f.input_field(:count, class: 'span1', data: { recalculate: true }, placeholder: InvoiceItem.human_attribute_name(:count)) diff --git a/app/views/invoice_lists/new.js.haml b/app/views/invoice_lists/new.js.haml new file mode 100644 index 0000000000..03757b99a2 --- /dev/null +++ b/app/views/invoice_lists/new.js.haml @@ -0,0 +1,6 @@ +-# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +-# hitobito and licensed under the Affero General Public License version 3 +-# or later. See the COPYING file at the top-level directory or at +-# https://github.com/hitobito/hitobito. + +$('#calculated').html('#{escape_javascript(render('calculated', invoice: entry.decorate))}') diff --git a/app/views/invoices/_actions_index.html.haml b/app/views/invoices/_actions_index.html.haml new file mode 100644 index 0000000000..9ff2f1cfb5 --- /dev/null +++ b/app/views/invoices/_actions_index.html.haml @@ -0,0 +1,14 @@ +-# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +-# hitobito and licensed under the Affero General Public License version 3 +-# or later. See the COPYING file at the top-level directory or at +-# https://github.com/hitobito/hitobito. + +- if parent.invoices.draft.exists? + = invoice_sending_dropdown(:group_invoice_list_path) + +- if entries.present? + = action_button ti('link.delete'), group_invoice_list_path, :trash, method: :delete, data: { checkable: true } + = invoices_export_dropdown + = invoices_print_dropdown + += action_button(t('invoices.add'), new_group_invoice_path(parent), :plus) diff --git a/app/views/invoices/_actions_show.html.haml b/app/views/invoices/_actions_show.html.haml new file mode 100644 index 0000000000..a5fe13c4be --- /dev/null +++ b/app/views/invoices/_actions_show.html.haml @@ -0,0 +1,16 @@ +-# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +-# hitobito and licensed under the Affero General Public License version 3 +-# or later. See the COPYING file at the top-level directory or at +-# https://github.com/hitobito/hitobito. + += button_action_edit if can?(:edit, entry) += invoices_export_dropdown += invoices_print_dropdown += button_action_destroy if can?(:destroy, entry) + +- if entry.remindable? && can?(:update, entry) + = action_button(t('crud.new.title', model: PaymentReminder.model_name.human), + '#', :plus, { data: { turbolinks: false, toggle: 'collapse', target: '#payment_reminder' } }) + + = action_button(t('crud.new.title', model: Payment.model_name.human), + '#', :plus, { data: { turbolinks: false, toggle: 'collapse', target: '#payment' } }) diff --git a/app/views/invoices/_attrs.html.haml b/app/views/invoices/_attrs.html.haml new file mode 100644 index 0000000000..e99199610f --- /dev/null +++ b/app/views/invoices/_attrs.html.haml @@ -0,0 +1,10 @@ +-# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +-# hitobito and licensed under the Affero General Public License version 3 +-# or later. See the COPYING file at the top-level directory or at +-# https://github.com/hitobito/hitobito. + += render 'payment_reminder_form' if @reminder += render 'payment_form' if @payment + += render 'summary' += render 'details' diff --git a/app/views/invoices/_button_new.html.haml b/app/views/invoices/_button_new.html.haml new file mode 100644 index 0000000000..5356b6d1ff --- /dev/null +++ b/app/views/invoices/_button_new.html.haml @@ -0,0 +1,5 @@ +- if can?(:create, group.layer_group.invoices.new) + = action_button(t('crud.new.title', model: Invoice.model_name.human), + new_group_invoice_list_path(group.layer_group, invoice: { recipient_ids: people.collect(&:id).join(',') }), + :plus) + diff --git a/app/views/invoices/_details.html.haml b/app/views/invoices/_details.html.haml new file mode 100644 index 0000000000..88144a55c1 --- /dev/null +++ b/app/views/invoices/_details.html.haml @@ -0,0 +1,48 @@ +-# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +-# hitobito and licensed under the Affero General Public License version 3 +-# or later. See the COPYING file at the top-level directory or at +-# https://github.com/hitobito/hitobito. + +.invoice + .invoice-recipient-address + .contactable + .address + = invoice_receiver_address(entry) + + .invoice-table + %h2= entry.title + + %table.header + %tr + %td.left + = "#{t('activerecord.models.invoice.one')}: ##{entry.sequence_number}" + %td.right + = l(entry.sent_at, format: :long) if entry.sent_at? + + = table(entry.invoice_items.list, class: 'table table-striped') do |t| + - t.attr(:name) + - t.attr(:description) + - t.attr(:count) + - t.attr(:vat_rate) { |i| i.decorate.vat_rate } + - t.attr(:unit_cost) { |i| i.decorate.unit_cost } + - t.attr(:total) { |i| i.decorate.cost } + + .invoice-items-total + %table + %tr + %td.left + = captionize(:cost, Invoice) + %td.right + = entry.cost + %tr + %td.left + = captionize(:vat_rate, InvoiceItem) + %td.right + = entry.vat + %tr + %td.left + %b= captionize(:total_inkl_vat, Invoice) + %td.right + %b= entry.total + + %p= entry.description diff --git a/app/views/invoices/_filter.html.haml b/app/views/invoices/_filter.html.haml new file mode 100644 index 0000000000..ed036009fe --- /dev/null +++ b/app/views/invoices/_filter.html.haml @@ -0,0 +1,15 @@ += content_for(:filter) do + = form_tag(nil, { method: :get, class: 'form-inline-search', role: 'search', remote: true, data: { spin: true } }) do + = hidden_field_tag :returning, true + = hidden_field_tag :page, 1 + + .control-group.has-feedback.has-clear + = label_tag(:q, t('global.button.search'), class: 'control-label') + = search_field_tag :q, params[:q], class: 'form-control', placeholder: t('global.button.search'), data: { submit: true } + %span.form-control-feedback{data: { clear: true }} + = icon(:remove) + + = direct_filter_select(:state, Invoice.state_labels.to_a, nil) + + - if params[:state].blank? || %w(overdue reminded).include?(params[:state]) + = direct_filter_select(:due_since, invoice_due_since_options, t('.due_since')) diff --git a/app/views/invoices/_form.html.haml b/app/views/invoices/_form.html.haml new file mode 100644 index 0000000000..71ef7a7815 --- /dev/null +++ b/app/views/invoices/_form.html.haml @@ -0,0 +1,25 @@ += entry_form(data: { group: group_path(parent) }) do |f| + = field_set_tag do + = f.labeled_input_fields :title, :description + = f.labeled(:state) do + = f.collection_select(:state, Invoice.state_labels.to_a, :first, :second) + = f.labeled_input_field :issued_at + = f.labeled_input_field :due_at + + = field_set_tag do + = f.labeled_input_field :recipient_email + = f.labeled_input_field :recipient_address + + + = field_set_tag do + - if parent.invoice_articles.exists? + = f.labeled(:invoice_item_article) do + = select("temp", "invoice_article_id", + InvoiceArticle.all.pluck(:number, :id), + { include_blank: true }, + { id: "invoice_item_article" }) + + = f.labeled_inline_fields_for :invoice_items, 'invoice_lists/invoice_items' + + + = render "invoice_lists/calculated", invoice: entry diff --git a/app/views/invoices/_list.html.haml b/app/views/invoices/_list.html.haml new file mode 100644 index 0000000000..1884f7b773 --- /dev/null +++ b/app/views/invoices/_list.html.haml @@ -0,0 +1,7 @@ +-# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +-# hitobito and licensed under the Affero General Public License version 3 +-# or later. See the COPYING file at the top-level directory or at +-# https://github.com/hitobito/hitobito. + += render "filter" += render "table" diff --git a/app/views/invoices/_nav_left.html.haml b/app/views/invoices/_nav_left.html.haml new file mode 100644 index 0000000000..438f505c0e --- /dev/null +++ b/app/views/invoices/_nav_left.html.haml @@ -0,0 +1,15 @@ +-# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +-# hitobito and licensed under the Affero General Public License version 3 +-# or later. See the COPYING file at the top-level directory or at +-# https://github.com/hitobito/hitobito. + +%nav.nav-left-list + - current_user.finance_groups.each do |group| + - if group == parent + %h3.nav-left-title= link_to parent, parent + %ul.nav-left-list + = nav Invoice.model_name.human(count: 2), group_invoices_path(parent), %w(invoices) + = nav InvoiceArticle.model_name.human(count: 2), group_invoice_articles_path(parent), %w(invoice_articles) + = nav t('navigation.admin'), group_invoice_config_path(parent), %w(invoice_config) + - else + = nav(group, group_invoices_path(group)) diff --git a/app/views/invoices/_payment_form.html.haml b/app/views/invoices/_payment_form.html.haml new file mode 100644 index 0000000000..c2b249969c --- /dev/null +++ b/app/views/invoices/_payment_form.html.haml @@ -0,0 +1,7 @@ +- state = 'in' unless @payment_valid +#payment{class: %W(collapse #{state}).compact.join(' ')} + = standard_form(@payment, url: group_invoice_payments_path(parent, entry)) do |f| + = f.error_messages + = f.labeled_input_fields :amount, :received_at + = save_form_buttons(f, nil, cancel_url: group_invoice_path(parent, entry)) + diff --git a/app/views/invoices/_payment_reminder_form.html.haml b/app/views/invoices/_payment_reminder_form.html.haml new file mode 100644 index 0000000000..4f1332b6ff --- /dev/null +++ b/app/views/invoices/_payment_reminder_form.html.haml @@ -0,0 +1,7 @@ +- state = 'in' unless @reminder_valid + +#payment_reminder{class: %W(collapse #{state}).compact.join(' ')} + = standard_form(@reminder, url: group_invoice_payment_reminders_path(parent, entry)) do |f| + = f.error_messages + = f.labeled_input_fields :due_at, :message + = save_form_buttons(f, nil, cancel_url: group_invoice_path(parent, entry)) diff --git a/app/views/invoices/_summary.html.haml b/app/views/invoices/_summary.html.haml new file mode 100644 index 0000000000..b1f97de967 --- /dev/null +++ b/app/views/invoices/_summary.html.haml @@ -0,0 +1,23 @@ +-# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +-# hitobito and licensed under the Affero General Public License version 3 +-# or later. See the COPYING file at the top-level directory or at +-# https://github.com/hitobito/hitobito. +.invoice-state.clearfix + %table.invoice-state-table + %tr + %th.left + = captionize(:state, Invoice) + %th.left + = captionize(:total, Invoice) + %th.left + = captionize(:due_at, Invoice) if entry.due_at + %tr + %td + = format_attr(entry, :state) + %td + = entry.cost + %td + = format_attr(entry, :due_at) + + .invoice-history + = invoice_history(entry) diff --git a/app/views/invoices/_table.html.haml b/app/views/invoices/_table.html.haml new file mode 100644 index 0000000000..bebd7000b4 --- /dev/null +++ b/app/views/invoices/_table.html.haml @@ -0,0 +1,12 @@ +.pagination-bar + = paginate @invoices + += crud_table(data: { checkable: true }) do |t| + - t.col(check_box_tag(:all, 0, false, { data: :multiselect })) do |i| + - check_box_tag('ids[]', i.id, false, data: { multiselect: true }) + - t.col(t.sort_header(:title)) do |invoice| + %strong= link_to invoice.title, group_invoice_path(parent, invoice) + - t.sortable_attrs(:sequence_number, :state, :recipient, :issued_at, :sent_at, :due_at) + - t.col(t.sort_header(:total)) { |i| i.decorate.total } + += paginate @invoices diff --git a/app/views/label_format/settings/update.js.haml b/app/views/label_format/settings/update.js.haml new file mode 100644 index 0000000000..702601d507 --- /dev/null +++ b/app/views/label_format/settings/update.js.haml @@ -0,0 +1,5 @@ +- if current_user.show_global_label_formats + $('.global-formats').slideDown(); +- else + $('.global-formats').slideUp(); + diff --git a/app/views/label_formats/_form.html.haml b/app/views/label_formats/_form.html.haml index 7736246fb0..e1b932eb63 100644 --- a/app/views/label_formats/_form.html.haml +++ b/app/views/label_formats/_form.html.haml @@ -4,6 +4,7 @@ -# https://github.com/hitobito/hitobito. = entry_form(buttons_bottom: false) do |f| + = hidden_field_tag :global, params[:global] #main.row-fluid %article.span6 @@ -17,5 +18,13 @@ = f.labeled_input_field :height, help_inline: 'mm' = f.labeled_input_field :padding_top, help_inline: 'mm' = f.labeled_input_field :padding_left, help_inline: 'mm' + = f.labeled_input_field :nickname + = f.labeled(:pp_post) do + .input-prepend.input-append + %span.add-on + = 'P.P.' + = f.input_field :pp_post, :placeholder => 'CH-3030 Bern' + %span.add-on + = 'Post CH AG' %aside.span6 = image_tag("label_formats.png", size: '426x323') diff --git a/app/views/label_formats/_list.html.haml b/app/views/label_formats/_list.html.haml index aaf1e66532..8248b3b970 100644 --- a/app/views/label_formats/_list.html.haml +++ b/app/views/label_formats/_list.html.haml @@ -3,4 +3,14 @@ -# or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito. -= crud_table :name, :page_size, :landscape, :dimensions, :font_size, :width, :height +- global ||= false + +-if !global || can?(:manage_global, LabelFormat) + .btn-toolbar + = button_action_add(new_label_format_path(global: global)) + += table(entries, class: 'table table-striped table-hover') do |t| + - t.sortable_attr(:name) do |f| + - link_to_if(!global || can?(:manage_global, LabelFormat), f.name, label_format_path(f.id)) + - t.sortable_attrs(:page_size, :landscape, :dimensions, :font_size, :width, :height) + - add_table_actions(t) diff --git a/app/views/label_formats/index.html.haml b/app/views/label_formats/index.html.haml new file mode 100644 index 0000000000..e9319ef45c --- /dev/null +++ b/app/views/label_formats/index.html.haml @@ -0,0 +1,28 @@ +-# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +-# hitobito and licensed under the Affero General Public License version 3 +-# or later. See the COPYING file at the top-level directory or at +-# https://github.com/hitobito/hitobito. + +- title ti(:title, :models => models_label) + +#main + %h2= I18n.t('label_formats.own_labels') + = render 'list' + + %h2 + = I18n.t('label_formats.global_labels') + - if can?(:update_settings, current_user) +   + = check_box_tag(:show_global_label_formats, + true, + current_user.show_global_label_formats, + id: 'show_global_label_formats', + class: 'switcher', + data: { remote: true, + url: label_format_settings_path, + method: :put }) + %label{for: 'show_global_label_formats'} + + .global-formats{ style: element_visible(current_user.show_global_label_formats) } + = render 'list', entries: @global_entries, global: true + diff --git a/app/views/layouts/_quicksearch.html.haml b/app/views/layouts/_quicksearch.html.haml index a7c8ccb82e..9f03a5a20f 100644 --- a/app/views/layouts/_quicksearch.html.haml +++ b/app/views/layouts/_quicksearch.html.haml @@ -11,5 +11,6 @@ placeholder: t('global.search.placeholder'), data: { url: query_path, person_url: person_path(1).gsub(/\/1$/, ''), - group_url: groups_path } - = button_tag icon(:search), class: 'btn' \ No newline at end of file + group_url: groups_path, + event_url: event_path(1).gsub(/\/1$/, '') } + = button_tag icon(:search), class: 'btn' diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 6d9ca67c9f..819f02326e 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -10,6 +10,7 @@ %meta{charset: 'utf-8'} %title= "#{Settings.application.name} - #{title}" %meta{name: 'viewport', content: 'width=device-width, initial-scale=1.0'} + %meta{name: 'turbolinks-cache-control', content: 'no-cache'} = csrf_meta_tag = favicon_link_tag 'favicon.ico' @@ -77,10 +78,8 @@ = link_to label, url, target: '_blank' %br/ %p - - if Wagons.app_version.to_s > '0.0' - = link_to "Version #{Wagons.app_version}", changelog_path - - %br/ + = app_version_changelog_link + %br/ = link_to t('.source_code'), 'https://github.com/hitobito', target: '_blank' = t('.available_under_license') diff --git a/app/views/notes/_list.html.haml b/app/views/notes/_list.html.haml new file mode 100644 index 0000000000..5d72b3a377 --- /dev/null +++ b/app/views/notes/_list.html.haml @@ -0,0 +1,17 @@ +-# Copyright (c) 2012-2016, Dachverband Schweizer Jugendparlamente. This file is part of +-# hitobito and licensed under the Affero General Public License version 3 +-# or later. See the COPYING file at the top-level directory or at +-# https://github.com/hitobito/hitobito. + +.pagination-bar + = render 'notes/paginator', notes: notes + +#notes-list + - if notes.total_count == 0 + .pagination-info + = ti(:no_list_entries) + + - notes.each do |note| + = render note, show_subject: show_subject + += render 'notes/paginator', notes: notes diff --git a/app/views/notes/_note.html.haml b/app/views/notes/_note.html.haml new file mode 100644 index 0000000000..c324c0e440 --- /dev/null +++ b/app/views/notes/_note.html.haml @@ -0,0 +1,30 @@ +-# Copyright (c) 2012-2016, Dachverband Schweizer Jugendparlamente. This file is part of +-# hitobito and licensed under the Affero General Public License version 3 +-# or later. See the COPYING file at the top-level directory or at +-# https://github.com/hitobito/hitobito. + +.row-fluid.note{id: dom_id(note), class: ('is-current-subject' unless show_subject)} + - if show_subject + .note-image + - case note.subject + - when Group + = image_tag('group.svg') + - when Person + %img{src: note.subject.picture} + + .note-body + - if show_subject + = assoc_link(note.subject) + + %small.muted.note-author + = person_link(note.author) + .note-date + = t('.created', time_ago: time_ago_in_words(note.created_at)) + - if can?(:destroy, note) + = link_to icon(:trash), + note_path(@group, note), + method: :delete, + remote: true, + data: { confirm: ti(:confirm_delete) } + + = auto_link(simple_format(note.text)) diff --git a/app/views/notes/_paginator.html.haml b/app/views/notes/_paginator.html.haml new file mode 100644 index 0000000000..6e85ed9926 --- /dev/null +++ b/app/views/notes/_paginator.html.haml @@ -0,0 +1,22 @@ +-# Copyright (c) 2012-2017, Dachverband Schweizer Jugendparlamente. This file is part of +-# hitobito and licensed under the Affero General Public License version 3 +-# or later. See the COPYING file at the top-level directory or at +-# https://github.com/hitobito/hitobito. + +- if notes.total_pages > 1 + .pagination.notes-pagination + %ul + %li{class: notes.first_page? && :disabled} + = link_to_previous_page notes, + t('views.pagination.previous'), + param_name: :notes_page, + params: { anchor: 'notes' } + - if notes.first_page? + %a= t('views.pagination.previous') + %li{class: notes.last_page? && :disabled} + = link_to_next_page notes, + t('views.pagination.next'), + param_name: :notes_page, + params: { anchor: 'notes' } + - if notes.last_page? + %a= t('views.pagination.next') diff --git a/app/views/notes/_section.html.haml b/app/views/notes/_section.html.haml new file mode 100644 index 0000000000..85407cd545 --- /dev/null +++ b/app/views/notes/_section.html.haml @@ -0,0 +1,28 @@ +-# Copyright (c) 2012-2017, Dachverband Schweizer Jugendparlamente. This file is part of +-# hitobito and licensed under the Affero General Public License version 3 +-# or later. See the COPYING file at the top-level directory or at +-# https://github.com/hitobito/hitobito. + + +%section + %h2 + = Note.model_name.human(count: 2) + - if can?(:create, entry.notes.new) + %span.pull-right + = action_button(t('notes.new_note'), + '#', + 'plus', + id: 'notes-new-button', + class: 'btn-small notes-swap', + data: { swap: 'notes-swap' }) + + #notes-form.notes-swap{ style: 'display: none;', data: { swap: 'notes-swap' } } + #notes-error.alert.alert-error{ style: 'display: none;' } + + = form_for(Note.new, url: create_path, remote: true) do |f| + = f.text_area(:text, rows: 5, class: 'input-block-level') + = save_form_buttons(f, nil, '') + + = render 'notes/list', + notes: entry.notes.includes(:author, :subject).list.page(params[:notes_page]).per(10), + show_subject: false diff --git a/app/views/notes/create.js.haml b/app/views/notes/create.js.haml new file mode 100644 index 0000000000..5bfd72394c --- /dev/null +++ b/app/views/notes/create.js.haml @@ -0,0 +1,9 @@ +-# Copyright (c) 2012-2017, Dachverband Schweizer Jugendparlamente. This file is part of +-# hitobito and licensed under the Affero General Public License version 3 +-# or later. See the COPYING file at the top-level directory or at +-# https://github.com/hitobito/hitobito. + +- if @note.errors.blank? + App.Notes.addNote('#{escape_javascript(render @note, show_subject: false)}'); +- else + App.Notes.showError('#{@note.errors.full_messages.join("\n")}'); diff --git a/app/views/notes/destroy.js.haml b/app/views/notes/destroy.js.haml new file mode 100644 index 0000000000..68a5a29f87 --- /dev/null +++ b/app/views/notes/destroy.js.haml @@ -0,0 +1,9 @@ +-# Copyright (c) 2012-2016, hitobito AG. This file is part of +-# hitobito and licensed under the Affero General Public License version 3 +-# or later. See the COPYING file at the top-level directory or at +-# https://github.com/hitobito/hitobito. + +- if @note.destroyed? + $('#notes-list ##{dom_id(@note)}').remove() +- else + alert('#{t('.failure', errors: @note.errors.full_messages.join(', '))}'); diff --git a/app/views/person/notes/index.html.haml b/app/views/notes/index.html.haml similarity index 50% rename from app/views/person/notes/index.html.haml rename to app/views/notes/index.html.haml index beae54f426..1d0c8139ee 100644 --- a/app/views/person/notes/index.html.haml +++ b/app/views/notes/index.html.haml @@ -1,8 +1,9 @@ -# Copyright (c) 2012-2016, Dachverband Schweizer Jugendparlamente. This file is part of --# hitobito_dsj and licensed under the Affero General Public License version 3 +-# hitobito and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at --# https://github.com/hitobito/hitobito_dsj. +-# https://github.com/hitobito/hitobito. - @sheet = Sheet::Group.new(self) -= render 'person/notes/list', notes: @notes, show_person: true +#notes-index + = render 'list', notes: @notes, show_subject: true diff --git a/app/views/people/_actions_index.html.haml b/app/views/people/_actions_index.html.haml index fab696ca50..8207ed45de 100644 --- a/app/views/people/_actions_index.html.haml +++ b/app/views/people/_actions_index.html.haml @@ -5,6 +5,8 @@ - if can?(:new, @group.roles.new) = action_button(t('.add_person'), new_group_role_path(@group), :plus) += render 'invoices/button_new', group: @group, people: @people + - if can?(:new, @group.roles.new) = action_button(t('.import_list'), new_group_csv_imports_path, :upload) diff --git a/app/views/people/_actions_show.html.haml b/app/views/people/_actions_show.html.haml index 003b736916..231fe80ebb 100644 --- a/app/views/people/_actions_show.html.haml +++ b/app/views/people/_actions_show.html.haml @@ -6,6 +6,11 @@ - if can?(:edit, entry) = button_action_edit +- if can?(:destroy, entry) + = button_action_destroy(nil, { class: "btn-danger", data: { confirm: t('person.confirm_delete', + person: entry.person) } }) + += render 'invoices/button_new', group: parent, people: [entry] = dropdown_people_export(can?(:show_full, entry), false) - if entry.email? && can?(:send_password_instructions, entry) diff --git a/app/views/people/_attrs.html.haml b/app/views/people/_attrs.html.haml index 3efc323449..02f18336e0 100644 --- a/app/views/people/_attrs.html.haml +++ b/app/views/people/_attrs.html.haml @@ -20,12 +20,12 @@ - if entry.additional_information? %h2= Person.human_attribute_name(:additional_information) - = simple_format(entry.additional_information) + = auto_link(simple_format(entry.additional_information)) = render_extensions :show_left - if can?(:index_notes, entry) - = render 'notes' + = render 'notes/section', create_path: group_person_notes_path(@group, entry) - if can?(:show_full, entry) %aside.span6.offset1 @@ -34,6 +34,9 @@ = render 'add_requests' = render 'event_aside', title: Event::Application.model_name.human(count: 2), collection: entry.pending_applications = render 'event_aside', title: t('.events'), collection: entry.upcoming_events + + = render_extensions :show_event + = render 'qualifications', show_buttons: true = render_extensions :show_right diff --git a/app/views/people/_fields.html.haml b/app/views/people/_fields.html.haml index fe04ee0ef6..46aea64ed0 100644 --- a/app/views/people/_fields.html.haml +++ b/app/views/people/_fields.html.haml @@ -11,7 +11,12 @@ - without_relations ||= false = field_set_tag do - = f.labeled_input_fields :first_name, :last_name, :nickname, :company_name, :company + = f.labeled_input_fields :first_name, :last_name, :nickname + = f.labeled_string_field(:company_name, + placeholder: I18n.t('global.search.placeholder_company_name'), + data: { provide: 'entity', + url: query_company_name_path }) + = f.labeled_input_fields :company = render_extensions :name_fields, locals: { f: f } diff --git a/app/views/people/_list.html.haml b/app/views/people/_list.html.haml index 4402f89b47..af453f8a6f 100644 --- a/app/views/people/_list.html.haml +++ b/app/views/people/_list.html.haml @@ -1,20 +1,20 @@ --# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +-# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of -# hitobito and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito. - title @group.to_s -- content_for(:filter, FilterNavigation::People.new(self, @group, params).to_s) +- content_for(:filter, FilterNavigation::People.new(self, @group, @person_filter).to_s) .pagination-bar = paginate @people .pagination-info - - if @all_count > 0 + - if @person_filter.all_count > 0 = t('.number_of_people_shown', count: @people.total_count) - - unless @all_count == @people.total_count - = muted(t('.number_of_people_hidden', count: @all_count - @people.total_count)) + - unless @person_filter.all_count == @people.total_count + = muted(t('.number_of_people_hidden', count: @person_filter.all_count - @people.total_count)) - else = ti(:no_list_entries) @@ -25,15 +25,20 @@ = crud_table do |t| - t.col('') do |p| .profil= image_tag(p.picture.thumb.url, size: '32x32') - - sortable_grouped_person_attr(t, %w(last_name first_name nickname)) do |p| + - sortable_grouped_person_attr(t, last_name: true, first_name: true, nickname: true) do |p| %strong -# Any person listed can be shown - = link_to(p.to_s(:list), @multiple_groups ? group_person_path(p.default_group_id, p) : group_person_path(@group, p)) + = link_to(p.to_s(:list), + @person_filter.multiple_groups ? group_person_path(p.default_group_id, p) : group_person_path(@group, p)) %br/ = muted p.additional_name - - t.col(t.sort_header(:roles, Role.model_name.human(count: 2))) { |p| p.roles_short(@multiple_groups ? nil : @group) } - - t.col(Person.human_attribute_name(:emails)) { |p| p.all_emails(!index_full_ability?) } - - t.col(PhoneNumber.model_name.human(count: 2)) { |p| p.all_phone_numbers(!index_full_ability?) } - - sortable_grouped_person_attr(t, %w(zip_code town), :address) { |p| p.complete_address } + - t.col(t.sort_header(:roles, Role.model_name.human(count: 2))) do |p| + = p.roles_short(@person_filter.multiple_groups ? nil : @group) + - t.col(Person.human_attribute_name(:emails)) do |p| + = p.all_emails(!index_full_ability?) + - t.col(PhoneNumber.model_name.human(count: 2)) do |p| + = p.all_phone_numbers(!index_full_ability?) + - sortable_grouped_person_attr(t, address: false, zip_code: true, town: true) do |p| + = p.complete_address = paginate @people diff --git a/app/views/people/_notes.html.haml b/app/views/people/_notes.html.haml deleted file mode 100644 index 6acccb4982..0000000000 --- a/app/views/people/_notes.html.haml +++ /dev/null @@ -1,22 +0,0 @@ --# Copyright (c) 2012-2016, Dachverband Schweizer Jugendparlamente. This file is part of --# hitobito_dsj and licensed under the Affero General Public License version 3 --# or later. See the COPYING file at the top-level directory or at --# https://github.com/hitobito/hitobito_dsj. - -%h2#person-notes= Person::Note.model_name.human(count: 2) - -- if can?(:create, Person::Note) - %button#person-notes-new-button.btn.person-notes-swap{ data: { swap: 'person-notes-swap' } }= t('.new_note') - - #person-notes-form.person-notes-swap{ style: 'display: none;', data: { swap: 'person-notes-swap' } } - %h3= t('.new_note') - - #person-notes-error.alert.alert-error{ style: 'display: none;' } - - = form_for(Person::Note.new, url: group_person_notes_path(@group, entry), remote: true) do |f| - = f.text_area(:text, rows: 7, width: '100%') - = save_form_buttons(f, nil, '') - -= render 'person/notes/list', - notes: entry.notes.includes(:author).page(params[:notes_page]).per(10), - show_person: false diff --git a/app/views/people/_tags.html.haml b/app/views/people/_tags.html.haml index 26ca354390..70ec686410 100644 --- a/app/views/people/_tags.html.haml +++ b/app/views/people/_tags.html.haml @@ -1,7 +1,7 @@ -# Copyright (c) 2012-2016, Dachverband Schweizer Jugendparlamente. This file is part of --# hitobito_dsj and licensed under the Affero General Public License version 3 +-# hitobito and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at --# https://github.com/hitobito/hitobito_dsj. +-# https://github.com/hitobito/hitobito. - if can?(:index_tags, entry) || can?(:manage_tags, entry) %section.tags @@ -9,7 +9,7 @@ = render 'person/tags/list', tags: @tags - if can?(:manage_tags, entry) - %span.label.person-tag.person-tag-add + %button.chip.chip-add.person-tag-add = t('.add_tag') = icon(:plus) diff --git a/app/views/people_filters/_filter.html.haml b/app/views/people_filters/_filter.html.haml new file mode 100644 index 0000000000..f0a2311770 --- /dev/null +++ b/app/views/people_filters/_filter.html.haml @@ -0,0 +1,8 @@ +.accordion-group + .accordion-heading + %a.accordion-toggle.collapsed.header{ href: "##{id}", data: { toggle: :collapse } } + = caption + + .accordion-body.collapse{ id: id } + .accordion-inner + = yield diff --git a/app/views/people_filters/_form.html.haml b/app/views/people_filters/_form.html.haml index 0daaf3c3a2..537c6777af 100644 --- a/app/views/people_filters/_form.html.haml +++ b/app/views/people_filters/_form.html.haml @@ -1,31 +1,27 @@ --# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +-# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of -# hitobito and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito. -= standard_form(path_args(entry), noindent: true) do |f| += standard_form(path_args(entry), noindent: true, stacked: true) do |f| = render 'search_or_save_buttons', f: f = render_extensions :form, locals: { f: f } = f.error_messages - .label-columns - = field_set_tag(t('.prompt_role_selection')) do - - @role_types.each do |layer, groups| - %h4.filter-toggle= layer - - groups.each do |group, role_types| - .control-group - = label_tag(nil, group, class: 'filter-toggle control-label') - .controls - - role_types.each do |role_type| - = f.inline_check_box(:role_type_ids, - role_type.id, - role_type.label, - checked: entry.role_types.include?(role_type.sti_name)) + = render 'range', f: f + + .accordion + = render(layout: 'filter', locals: { caption: t('people_filters.role.title'), id: 'roles' }) do + = render 'role', f: f + + - if @qualification_kinds.present? + = render(layout: 'filter', locals: { caption: t('people_filters.qualification.title'), id: 'qualifications' }) do + = render 'qualification', f: f + - if can?(:create, entry) - = field_set_tag(t('.save_filter')) do - = f.labeled_input_field :name, placeholder: t('.save_filter_placeholder') + = f.labeled_input_field :name, placeholder: t('.save_filter_placeholder') = render 'search_or_save_buttons', f: f diff --git a/app/views/people_filters/_qualification.html.haml b/app/views/people_filters/_qualification.html.haml new file mode 100644 index 0000000000..835cd5625c --- /dev/null +++ b/app/views/people_filters/_qualification.html.haml @@ -0,0 +1,35 @@ +-# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +-# hitobito and licensed under the Affero General Public License version 3 +-# or later. See the COPYING file at the top-level directory or at +-# https://github.com/hitobito/hitobito. + +- filter = entry.filter_chain[:qualification] + +.label-columns + = field_set_tag(t('.prompt_qualification_selection')) do + - unless can?(:index_full_people, @group) + .alert.alert-warning= t('.not_enough_permissions') + + .controls + - @qualification_kinds.each do |kind| + - dom_id = "qualification_kind_id_#{kind.id}" + = label_tag(dom_id, class: 'checkbox inline') do + = check_box_tag("filters[qualification][qualification_kind_ids][]", + kind.id, + filter && filter.args[:qualification_kind_ids].include?(kind.id), + id: dom_id) + = kind.to_s + + = field_set_tag(t('.prompt_validity')) do + = render 'simple_radio', + attr: "filters[qualification][validity]", + value: 'active', + checked: true # first item is checked per default + = render 'simple_radio', + attr: "filters[qualification][validity]", + value: 'reactivateable', + checked: filter && filter.args[:validity] == 'reactivateable' + = render 'simple_radio', + attr: "filters[qualification][validity]", + value: 'all', + checked: filter && filter.args[:validity] == 'all' diff --git a/app/views/people_filters/_range.html.haml b/app/views/people_filters/_range.html.haml new file mode 100644 index 0000000000..b06cf9de5d --- /dev/null +++ b/app/views/people_filters/_range.html.haml @@ -0,0 +1,15 @@ +-# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +-# hitobito and licensed under the Affero General Public License version 3 +-# or later. See the COPYING file at the top-level directory or at +-# https://github.com/hitobito/hitobito. + += render 'simple_radio', + attr: 'range', + value: 'deep', + checked: true, + caption: "people_filters.form.range.#{@group.layer? ? 'deep' : 'group_deep'}" +- if @group.layer? + = render 'simple_radio', attr: 'range', value: 'layer', checked: entry.range == 'layer' += render 'simple_radio', attr: 'range', value: 'group', checked: entry.range == 'group' + +%br/ diff --git a/app/views/people_filters/_role.html.haml b/app/views/people_filters/_role.html.haml new file mode 100644 index 0000000000..fac2fec618 --- /dev/null +++ b/app/views/people_filters/_role.html.haml @@ -0,0 +1,44 @@ +-# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +-# hitobito and licensed under the Affero General Public License version 3 +-# or later. See the COPYING file at the top-level directory or at +-# https://github.com/hitobito/hitobito. + +- filter = entry.filter_chain[:role] + +.label-columns + = field_set_tag(t('.prompt_role_selection')) do + - @role_types.each do |layer, groups| + .layer{ class: [@group.klass.label, @group.layer_group.class.label].include?(layer) && 'same-layer' } + %h4.filter-toggle= layer + - groups.each do |group, role_types| + .group.control-group{ class: group == @group.klass.label && 'same-group' } + %h5.filter-toggle= group + .controls + - role_types.each do |role_type| + - id = "filters_role_role_type_ids_#{role_type.id}" + = label_tag(nil, id, class: 'checkbox inline') do + = check_box_tag("filters[role][role_type_ids][]", + role_type.id, + filter && filter.to_hash[:role_types].include?(role_type.to_s), + id: id) + = role_type.label + + = field_set_tag do + = label_tag(:duration) do + %label=t('.prompt_role_duration') + .input-prepend + %span{class: 'add-on'} + = icon(:calendar) + = text_field_tag("filters[role][start_at]", filter && filter.args[:start_at], class: 'date span2') + %div{style: 'vertical-align: middle; margin: 0px 5px 15px 5px; display: inline-block;'} + + =t('.prompt_role_duration_until') + .input-prepend + %span{class: 'add-on'} + = icon(:calendar) + = text_field_tag("filters[role][finish_at]", filter && filter.args[:finish_at], class: 'date span2') + - %w(active created deleted).each do |key| + = render 'simple_radio', + attr: "filters[role][kind]", + value: key, + checked: filter && filter.args[:kind] == key diff --git a/app/views/people_filters/_search_or_save_buttons.html.haml b/app/views/people_filters/_search_or_save_buttons.html.haml index 161bfc96d0..6e7a994226 100644 --- a/app/views/people_filters/_search_or_save_buttons.html.haml +++ b/app/views/people_filters/_search_or_save_buttons.html.haml @@ -1,7 +1,13 @@ +-# Copyright (c) 2015-2017, Hitobito AG. This file is part of +-# hitobito and licensed under the Affero General Public License version 3 +-# or later. See the COPYING file at the top-level directory or at +-# https://github.com/hitobito/hitobito. + .btn-toolbar - .btn-group - = f.button(t('global.button.search'), class: 'btn btn-primary', value: 'search') + - unless entry.persisted? + .btn-group + = f.button(t('global.button.search'), class: 'btn btn-primary', value: 'search') - if can?(:create, entry) .btn-group = f.button(t('people_filters.form.save_search'), class: 'btn btn-primary', value: 'save') - = link_to(t('global.button.cancel'), people_list_path, class: 'link') \ No newline at end of file + = link_to(t('global.button.cancel'), people_list_path, class: 'link') diff --git a/app/views/people_filters/_simple_radio.html.haml b/app/views/people_filters/_simple_radio.html.haml index c780cfc15e..c35cc95a50 100644 --- a/app/views/people_filters/_simple_radio.html.haml +++ b/app/views/people_filters/_simple_radio.html.haml @@ -1,7 +1,6 @@ -- caption ||= ".#{attr}.#{value}" +- key = attr.gsub(/\W+/, '_').gsub(/_+$/, '') +- caption ||= "people_filters.form.#{key}.#{value}" -= label_tag("#{attr}_#{value}", class: 'radio') do - = radio_button_tag(attr, - value, - params[attr] == value || defined?(first)) - = t(caption) \ No newline at end of file += label_tag("#{key}_#{value}", class: 'radio') do + = radio_button_tag(attr, value, checked) + = t(caption) diff --git a/app/views/people_filters/qualification.html.haml b/app/views/people_filters/qualification.html.haml deleted file mode 100644 index 4d76c34b90..0000000000 --- a/app/views/people_filters/qualification.html.haml +++ /dev/null @@ -1,36 +0,0 @@ --# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of --# hitobito and licensed under the Affero General Public License version 3 --# or later. See the COPYING file at the top-level directory or at --# https://github.com/hitobito/hitobito. - -- title(t('.title')) - -= form_tag(people_list_path, method: :get, class: 'form-noindent') do |f| - = hidden_field_tag 'filter', 'qualification' - - = render 'search_button' - - .label-columns - = field_set_tag(t('.prompt_qualification_selection')) do - .controls - - @qualification_kinds.each do |kind| - - dom_id = "qualification_kind_id_#{kind.id}" - = label_tag(dom_id, class: 'checkbox inline') do - = check_box_tag('qualification_kind_id[]', - kind.id, - Array(params[:qualification_kind_id]).include?(kind.id.to_s), - id: dom_id) - = kind.to_s - - = field_set_tag(t('.prompt_validity')) do - = render 'simple_radio', attr: 'validity', value: 'active', first: true - = render 'simple_radio', attr: 'validity', value: 'reactivateable' - = render 'simple_radio', attr: 'validity', value: 'all' - - = field_set_tag(t('.prompt_kind')) do - = render 'simple_radio', attr: 'kind', value: 'deep', first: true, - caption: ".kind.#{@group.layer? ? 'deep' : 'group_deep'}" - = render 'simple_radio', attr: 'kind', value: 'layer' - = render 'simple_radio', attr: 'kind', value: 'group' - - = render 'search_button' diff --git a/app/views/person/colleagues/index.html.haml b/app/views/person/colleagues/index.html.haml new file mode 100644 index 0000000000..1e8bd90db1 --- /dev/null +++ b/app/views/person/colleagues/index.html.haml @@ -0,0 +1,33 @@ +-# Copyright (c) 2017, Dachverband Schweizer Jugendparlamente. This file is part of +-# hitobito and licensed under the Affero General Public License version 3 +-# or later. See the COPYING file at the top-level directory or at +-# https://github.com/hitobito/hitobito. + + += paginate @colleagues + += table(@colleagues, class: 'table table-striped table-hover') do |t| + - t.col('') do |p| + .profil= image_tag(p.picture.thumb.url, size: '32x32') + - sortable_grouped_person_attr(t, last_name: true, first_name: true, nickname: true) do |p| + - @showable = can?(:show, p) + %strong + = link_to_if(@showable, + p.to_s(:list), + group_person_path(p.default_group_id, p)) + %br/ + = muted p.additional_name + - t.col(t.sort_header(:roles, Role.model_name.human(count: 2))) do |p| + - if @showable + = p.roles_short + - t.col(Person.human_attribute_name(:emails)) do |p| + - if @showable + = p.all_emails(true) + - t.col(PhoneNumber.model_name.human(count: 2)) do |p| + - if @showable + = p.all_phone_numbers(true) + - sortable_grouped_person_attr(t, address: false, zip_code: true, town: true) do |p| + - if @showable + = p.complete_address + += paginate @colleagues diff --git a/app/views/person/history/index.html.haml b/app/views/person/history/index.html.haml index edfd58437a..1f56767224 100644 --- a/app/views/person/history/index.html.haml +++ b/app/views/person/history/index.html.haml @@ -6,7 +6,8 @@ %h2= Role.model_name.human(count: 2) = table(@roles, class: 'table table-striped') do |t| - - t.attr(:group_id, Group.model_name.human) + - t.col(Group.model_name.human) do |r| + = GroupDecorator.new(r.group).link_with_layer - t.col(Role.model_name.human) do |r| = r.to_s - t.attr(:created_at, t('global.from')) diff --git a/app/views/person/invoices/_actions_index.html.haml b/app/views/person/invoices/_actions_index.html.haml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/views/person/invoices/_table.html.haml b/app/views/person/invoices/_table.html.haml new file mode 100644 index 0000000000..f321d49f66 --- /dev/null +++ b/app/views/person/invoices/_table.html.haml @@ -0,0 +1,12 @@ +- title @person.to_s + +.pagination-bar + = paginate @invoices + += crud_table(data: { checkable: true }) do |t| + - t.col(t.sort_header(:title)) do |invoice| + %strong= link_to invoice.title, group_invoice_path(invoice.group, invoice) + - t.sortable_attrs(:sequence_number, :state, :issued_at, :sent_at, :due_at) + - t.col(t.sort_header(:total)) { |i| i.decorate.total } + += paginate @invoices diff --git a/app/views/person/notes/_list.html.haml b/app/views/person/notes/_list.html.haml deleted file mode 100644 index eabda32be7..0000000000 --- a/app/views/person/notes/_list.html.haml +++ /dev/null @@ -1,17 +0,0 @@ --# Copyright (c) 2012-2016, Dachverband Schweizer Jugendparlamente. This file is part of --# hitobito_dsj and licensed under the Affero General Public License version 3 --# or later. See the COPYING file at the top-level directory or at --# https://github.com/hitobito/hitobito_dsj. - -#person-notes-pagination.pagination-bar - = render 'person/notes/paginator', notes: notes - - .pagination-info - - if notes.total_count == 0 - = ti(:no_list_entries) - -#person-notes-list - - notes.each do |note| - = render note, show_person: show_person - -= render 'person/notes/paginator', notes: notes diff --git a/app/views/person/notes/_note.html.haml b/app/views/person/notes/_note.html.haml deleted file mode 100644 index e97c19be1f..0000000000 --- a/app/views/person/notes/_note.html.haml +++ /dev/null @@ -1,27 +0,0 @@ --# Copyright (c) 2012-2016, Dachverband Schweizer Jugendparlamente. This file is part of --# hitobito_dsj and licensed under the Affero General Public License version 3 --# or later. See the COPYING file at the top-level directory or at --# https://github.com/hitobito/hitobito_dsj. - - -- if show_person - .row-fluid.note - %img.note-image{:src => note.person.picture} - .note-body - %small.muted.note-author - = !show_person ? person_link(note.author) : note.author - = t('.created', time_ago: time_ago_in_words(note.created_at)) - .note-person - = person_link(note.person) - = auto_link(simple_format(note.text)) -- else - .row-fluid.note - .span12 - %small.muted - = !show_person ? person_link(note.author) : note.author - - if show_person - = t('.wrote_about') - = person_link(note.person) - .pull-right - = t('.created', time_ago: time_ago_in_words(note.created_at)) - = auto_link(simple_format(note.text)) diff --git a/app/views/person/notes/_paginator.html.haml b/app/views/person/notes/_paginator.html.haml deleted file mode 100644 index 4982c704c7..0000000000 --- a/app/views/person/notes/_paginator.html.haml +++ /dev/null @@ -1,20 +0,0 @@ --# Copyright (c) 2012-2016, Dachverband Schweizer Jugendparlamente. This file is part of --# hitobito_dsj and licensed under the Affero General Public License version 3 --# or later. See the COPYING file at the top-level directory or at --# https://github.com/hitobito/hitobito_dsj. - -- if notes.total_count > 0 - .pagination - %ul - %li{class: notes.first_page? && :disabled} - = link_to_previous_page notes, "« #{t('.newer_notes')}", - param_name: :notes_page, - params: { anchor: 'person-notes' } - - if notes.first_page? - %a= "« #{t('.newer_notes')}" - %li{class: notes.last_page? && :disabled} - = link_to_next_page notes, "#{t('.older_notes')} »", - param_name: :notes_page, - params: { anchor: 'person-notes' } - - if notes.last_page? - %a= "#{t('.older_notes')} »" diff --git a/app/views/person/notes/create.js.haml b/app/views/person/notes/create.js.haml deleted file mode 100644 index cd4f10ffc6..0000000000 --- a/app/views/person/notes/create.js.haml +++ /dev/null @@ -1,4 +0,0 @@ -- if @note.errors.blank? - App.PersonNotes.addNote('#{escape_javascript(render @note, show_person: false)}'); -- else - App.PersonNotes.showError('#{@note.errors.full_messages.join("\n")}'); diff --git a/app/views/person/tags/_tag.html.haml b/app/views/person/tags/_tag.html.haml index 2edeae7525..079eee7347 100644 --- a/app/views/person/tags/_tag.html.haml +++ b/app/views/person/tags/_tag.html.haml @@ -1,8 +1,8 @@ -%span.label.label-inverse.person-tag{data: {tag_id: tag.id}} +%span.chip.person-tag{data: {tag_id: tag.id}} = tag.name_without_category - if can?(:manage_tags, person) = link_to icon(:remove), - group_person_tag_path(person_id: person.id, name: tag.name), + group_person_tags_path(person_id: person.id, name: tag.name), method: :delete, data: { tag_id: tag.id }, remote: true, diff --git a/app/views/roles/_popover.html.haml b/app/views/roles/_popover.html.haml index a4275c6e7d..690458e693 100644 --- a/app/views/roles/_popover.html.haml +++ b/app/views/roles/_popover.html.haml @@ -1,4 +1,5 @@ -= standard_form entry, url: group_role_path(group_id: entry.group.id, id: entry.id), remote: true do |f| +- action = entry.persisted? ? :update : :create += standard_form [group, entry], controller: :roles, action: action, remote: true do |f| = f.error_messages - if @group_selection.present? && @group_selection.size > 1 = f.labeled(:group_id) do @@ -32,5 +33,8 @@ class: 'span4', help: t('roles.fields.help_optional_label'), data: { provide: :typeahead, source: entry.klass.available_labels }) + + - if @person_id + = f.hidden_field :person_id, value: @person_id = save_form_buttons(f, ti(:"button.save"), '#') diff --git a/app/views/roles/_popover.js.haml b/app/views/roles/_popover.js.haml new file mode 100644 index 0000000000..a679b9c044 --- /dev/null +++ b/app/views/roles/_popover.js.haml @@ -0,0 +1,12 @@ +- id ||= nil +:plain + var el = #{id ? "'##{id}'" : "$('body').data('popover')"}; + $($('body').data('popover')).popover('destroy'); + $(el).popover({content: '#{j(render('popover.html', group: entry.group))}', + title: "#{t(title_key, person: entry.person)}", + container: 'body', + placement: 'bottom', + html: true }).popover('show'); + + $('body').data('popover', el); + $('body .chosen-select').each(App.activateChosen); diff --git a/app/views/roles/create.js.haml b/app/views/roles/create.js.haml new file mode 100644 index 0000000000..c727ce2732 --- /dev/null +++ b/app/views/roles/create.js.haml @@ -0,0 +1,7 @@ +- if entry.persisted? + $($('body').data('popover')).parent().hide() + $($('body').data('popover')).popover('destroy').replaceWith('#{j(PersonDecorator.new(entry.person).roles_short)}') + $('##{dom_id(entry)}').parent().toggle('highlight', {duration: 1000}) +- else + - @person_id = entry.person_id + = render('popover', title_key: 'roles.form.new_role_for_person' ) diff --git a/app/views/roles/edit.js.haml b/app/views/roles/edit.js.haml index 02539a8854..61082d510c 100644 --- a/app/views/roles/edit.js.haml +++ b/app/views/roles/edit.js.haml @@ -1,10 +1 @@ -:plain - $($('body').data('popover')).popover('destroy') - $('##{dom_id(entry)}').popover({content: '#{j(render('popover'))}', - title: "#{t('roles.form.edit_role_for_person', person: entry.person)}", - container: 'body', - placement: 'bottom', - html: true }).popover('show') - - $('body').data('popover', '##{dom_id(entry)}') - $('body .chosen-select').each(App.activateChosen) += render('popover', title_key: 'roles.form.edit_role_for_person', id: dom_id(entry) ) diff --git a/app/views/roles/new.js.haml b/app/views/roles/new.js.haml new file mode 100644 index 0000000000..75a8d96132 --- /dev/null +++ b/app/views/roles/new.js.haml @@ -0,0 +1 @@ += render('popover', title_key: 'roles.form.new_role_for_person', id: "role_#{params[:role_id]}" ) diff --git a/app/views/roles/update.js.haml b/app/views/roles/update.js.haml index b7390f00d8..36dabcf54f 100644 --- a/app/views/roles/update.js.haml +++ b/app/views/roles/update.js.haml @@ -1,3 +1,4 @@ +- group ||= defined?(@group) ? group : nil $('##{dom_id(@old_role || @role)}').parent().hide() -$('##{dom_id(@old_role || @role)}').popover('destroy').replaceWith('#{j(PersonDecorator.new(entry.person).roles_short)}') +$('##{dom_id(@old_role || @role)}').popover('destroy').replaceWith('#{j(PersonDecorator.new(entry.person).roles_short(group))}') $('##{dom_id(entry)}').parent().toggle('highlight', {duration: 1000}) diff --git a/app/views/subscriber/group/_tags.html.haml b/app/views/subscriber/group/_tags.html.haml index bc348bf0b4..7175e37cc1 100644 --- a/app/views/subscriber/group/_tags.html.haml +++ b/app/views/subscriber/group/_tags.html.haml @@ -1,7 +1,7 @@ -# Copyright (c) 2012-2016, Dachverband Schweizer Jugendparlamente. This file is part of --# hitobito_dsj and licensed under the Affero General Public License version 3 +-# hitobito and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at --# https://github.com/hitobito/hitobito_dsj. +-# https://github.com/hitobito/hitobito. %p= t('.only_add_persons_with_tags') diff --git a/app/views/subscriptions/_subscription.html.haml b/app/views/subscriptions/_subscription.html.haml index 53dd15aee6..0c26d7b59b 100644 --- a/app/views/subscriptions/_subscription.html.haml +++ b/app/views/subscriptions/_subscription.html.haml @@ -1,7 +1,7 @@ -# Copyright (c) 2012-2016, Dachverband Schweizer Jugendparlamente. This file is part of --# hitobito_dsj and licensed under the Affero General Public License version 3 +-# hitobito and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at --# https://github.com/hitobito/hitobito_dsj. +-# https://github.com/hitobito/hitobito. - if subscription.subscriber.is_a?(Group) && subscription.related_role_types.present? %h4= subscription.subscriber.with_layer.join(' / ') diff --git a/bin/ci/wagon_setup.sh b/bin/ci/wagon_setup.sh index cd72ec27a0..41f745fbde 100755 --- a/bin/ci/wagon_setup.sh +++ b/bin/ci/wagon_setup.sh @@ -14,6 +14,11 @@ bundle install --path vendor/bundle for d in ../hitobito_*; do cp Gemfile.lock $d + mkdir -p $d/.bundle + bundle_config=$d/.bundle/config + echo "---" > $bundle_config + echo "BUNDLE_PATH: ../hitobito/vendor/bundle" >> $bundle_config + echo "BUNDLE_DISABLE_SHARED_GEMS: '1'" >> $bundle_config done rm -rf tmp/tarantula diff --git a/bin/rake b/bin/rake new file mode 100755 index 0000000000..615dea9089 --- /dev/null +++ b/bin/rake @@ -0,0 +1,8 @@ +#!/usr/bin/env ruby +begin + load File.expand_path('../spring', __FILE__) +rescue LoadError => e + raise unless e.message.include?('spring') +end +require 'bundler/setup' +load Gem.bin_path('rake', 'rake') diff --git a/bin/rspec b/bin/rspec new file mode 100755 index 0000000000..6e6709219a --- /dev/null +++ b/bin/rspec @@ -0,0 +1,8 @@ +#!/usr/bin/env ruby +begin + load File.expand_path('../spring', __FILE__) +rescue LoadError => e + raise unless e.message.include?('spring') +end +require 'bundler/setup' +load Gem.bin_path('rspec-core', 'rspec') diff --git a/bin/spring b/bin/spring new file mode 100755 index 0000000000..fb2ec2ebb4 --- /dev/null +++ b/bin/spring @@ -0,0 +1,17 @@ +#!/usr/bin/env ruby + +# This file loads spring without using Bundler, in order to be fast. +# It gets overwritten when you run the `spring binstub` command. + +unless defined?(Spring) + require 'rubygems' + require 'bundler' + + lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read) + spring = lockfile.specs.detect { |spec| spec.name == "spring" } + if spring + Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path + gem 'spring', spring.version + require 'spring/binstub' + end +end diff --git a/config/application.rb b/config/application.rb index a292e6d8fd..62e6cc0918 100644 --- a/config/application.rb +++ b/config/application.rb @@ -26,6 +26,7 @@ def with_benchmark(tag, &block) module Hitobito class Application < Rails::Application + # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. @@ -89,7 +90,7 @@ class Application < Rails::Application config.assets.version = '1.0' config.assets.precompile += %w(print.css ie.css ie7.css wysiwyg.css wysiwyg.js - *.png *.gif *.jpg) + *.png *.gif *.jpg favicon.ico) config.generators do |g| g.test_framework :rspec, fixture: true @@ -101,7 +102,7 @@ class Application < Rails::Application # Assert the mail relay job is scheduled on every restart. if Delayed::Job.table_exists? MailRelayJob.new.schedule if Settings.email.retriever.config.present? - SphinxIndexJob.new.schedule + SphinxIndexJob.new.schedule if Application.sphinx_present? && Application.sphinx_local? end end @@ -111,6 +112,26 @@ class Application < Rails::Application ThinkingSphinx::Index.define_partial_indizes! end end + + def self.sphinx_version + @sphinx_version ||= ThinkingSphinx::Configuration.instance.controller.sphinx_version.presence || + ENV['RAILS_SPHINX_VERSION'] + end + + def self.sphinx_present? + port = ENV['RAILS_SPHINX_PORT'] + port.present? || ThinkingSphinx::Configuration.instance.controller.running? + end + + def self.sphinx_local? + host = ENV['RAILS_SPHINX_HOST'] + host.blank? || host == '127.0.0.1' || host == 'localhost' + end + + def self.build_info + @build_info ||= File.read("#{Rails.root}/BUILD_INFO").strip rescue '' + end + end end diff --git a/config/initializers/acts_as_taggable_on.rb b/config/initializers/acts_as_taggable_on.rb index bb3aa99d45..ad183a0089 100644 --- a/config/initializers/acts_as_taggable_on.rb +++ b/config/initializers/acts_as_taggable_on.rb @@ -1,9 +1,9 @@ # encoding: utf-8 # Copyright (c) 2012-2016, Dachverband Schweizer Jugendparlamente. This file is part of -# hitobito_dsj and licensed under the Affero General Public License version 3 +# hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito_dsj. +# https://github.com/hitobito/hitobito. ActsAsTaggableOn.remove_unused_tags = true ActsAsTaggableOn.default_parser = TagCategoryParser diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index fd086a5010..a386be7ef8 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -8,6 +8,7 @@ # Configure the class responsible to send e-mails. # config.mailer = "Devise::Mailer" + config.parent_mailer = 'ApplicationMailer' # ==> ORM configuration # Load and configure the ORM. Supports :active_record (default) and diff --git a/config/initializers/paper_trail.rb b/config/initializers/paper_trail.rb new file mode 100644 index 0000000000..39a6679176 --- /dev/null +++ b/config/initializers/paper_trail.rb @@ -0,0 +1 @@ +PaperTrail.config.track_associations = false diff --git a/config/initializers/secret_token.rb b/config/initializers/secret_token.rb index 45baac6dd7..1854071fc1 100644 --- a/config/initializers/secret_token.rb +++ b/config/initializers/secret_token.rb @@ -17,8 +17,8 @@ def initialize_secret hash = Digest::SHA512.hexdigest(seed) hash[0, 128] else - ENV['RAILS_SECRET_TOKEN'] || - '026a97227d5e4cdf52470310b0f2511b259f4743606a45be8cbbd42ee48a004ee0d71de819138ba36b6526c58cd7811f5ca58f2f1e006835f57c551d6192f974' + ENV['SECRET_KEY_BASE'] || ENV['RAILS_SECRET_TOKEN'] || + '026a97227d5e4cdf52470310b0f2511b259f4743606a45be8cbbd42ee48a004ee0d71de819138ba36b6526c58cd7811f5ca58f2f1e006835f57c551d6192f974' end end diff --git a/config/initializers/sphinx_20.rb b/config/initializers/sphinx_20.rb index bdac65b06f..d80f387624 100644 --- a/config/initializers/sphinx_20.rb +++ b/config/initializers/sphinx_20.rb @@ -1,6 +1,5 @@ # Config for Sphinx < 2.1 -version = ThinkingSphinx::Configuration.instance.controller.sphinx_version.presence || - ENV['RAILS_SPHINX_VERSION'] +version = Rails.application.class.sphinx_version if version.nil? || version < '2.1' ThinkingSphinx::SphinxQL.variables! diff --git a/config/locales/models.de.yml b/config/locales/models.de.yml index f23f326903..3ee15b267d 100644 --- a/config/locales/models.de.yml +++ b/config/locales/models.de.yml @@ -1,4 +1,4 @@ -# Copyright (c) 2012-2015, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. @@ -47,6 +47,12 @@ de: attributes: body: placeholder_missing: 'muss den Platzhalter %{placeholder} enthalten' + event: + attributes: + base: + contact_attr_mandatory: "'%{attribute}' ist ein Pflichtfeld und kann nicht als optional oder 'nicht anzeigen' gesetzt werden" + contact_attr_invalid: "'%{attribute}' ist kein gültiges Personen-Attribut" + contact_attr_hidden_required: "'%{attribute}' kann nicht als obligatorisch und 'nicht anzeigen' gesetzt werden" event/date: attributes: finish_at: @@ -102,6 +108,9 @@ de: name: must_be_unique: 'existiert bereits' + invoice: + recipient_address_or_email_required: 'Empfänger Addresse oder E-Mail muss ausgefüllt werden' + models: acts_as_taggable_on/tag: one: Tag @@ -139,6 +148,9 @@ de: event/role/cook: one: Küche other: Küche + event/role/helper: + one: Helfer/-in + other: Helfer/-innen event/role/participant: one: Teilnehmer/-in other: Teilnehmer/-innen @@ -154,21 +166,36 @@ de: group: one: Gruppe other: Gruppen + invoice: + one: Rechnung + other: Rechnungen + invoice_article: + one: Rechnungsartikel + other: Rechnungsartikel + invoice_item: + one: Rechnungsposten + other: Rechnungsposten label_format: one: Etikettenformat other: Etikettenformate mailing_list: one: Abo other: Abos + note: + one: Notiz + other: Notizen + payment: + one: Zahlung + other: Zahlungen + payment_reminder: + one: Mahnung + other: Mahnungen person: one: Person other: Personen person/add_request: one: Zugriffsanfrage other: Anfragen - person/note: - one: Notiz - other: Notizen people_filter: one: Filter other: Filter @@ -235,6 +262,7 @@ de: nickname: Übername email: Haupt-E-Mail emails: E-Mails + layer_group: Hauptebene password: Passwort password_confirmation: Passwort Bestätigung current_password: Altes Passwort @@ -269,13 +297,11 @@ de: person/add_request/event: label: "%{body} in %{group}" + deleted_event: Gelöschter Anlass person/add_request/mailing_list: label: "%{body} in %{group}" - person/note: - text: Text - group: name: Name short_name: Kurzname @@ -289,6 +315,7 @@ de: phone_numbers: Telefonnummern social_accounts: Social Media additional_emails: Weitere E-Mails + layer_group: Ebene parent_id: Elterngruppe layer_group_id: Ebene type: Gruppentyp @@ -320,6 +347,13 @@ de: teamer_count: Anzahl Leitungsteam participant_count: Anzahl Teilnehmende applicant_count: Anzahl Anmeldungen + applications_cancelable: Abmeldung möglich + display_booking_info: Anzeige Anmeldestand + + event/contact_attrs: + required: Obligatorisch + optional: Optional + hidden: Nicht anzeigen event/answer: answer: Antwort @@ -382,7 +416,13 @@ de: multiple_choices: Mehrfachauswahl required: Antwort obligatorisch - questions: + admin_questions: + question: Frage + choices: Mögliche Antworten + multiple_choices: Mehrfachauswahl + required: Antwort obligatorisch + + application_questions: question: Frage choices: Mögliche Antworten multiple_choices: Mehrfachauswahl @@ -394,6 +434,9 @@ de: type: Rolle participation: Person + note: + text: Text + qualification: qualification_kind: Qualifikation qualification_kind_id: Qualifikation @@ -455,6 +498,7 @@ de: contact_data: Lesen der Kontaktdaten aller anderen Personen mit Kontaktdatenberechtigung. qualify: Erstellen von Qualifikationen für Personen auf dieser Ebene und allen darunter liegenden Ebenen. approve_applications: Bestätigen der Kursanmeldungen für Personen dieser Ebene. + finance: Erstellen und Verwalten von Rechnungen. kind: member: one: Mitglied @@ -537,6 +581,64 @@ de: padding_top: Rand oben padding_left: Rand links dimensions: Anzahl + nickname: Übername auf Etikett + pp_post: PP-Zeile + + invoice: + title: Titel + description: Beschreibung + invoice_items: Rechnungsposten + invoice_item_article: Rechnungsartikel + state: Status + sequence_number: Nummer + esr_number: Referenz Nummer + amount_paid: Total bezahlt + states: + draft: Entwurf + sent: Gestellt + payed: Bezahlt + overdue: Überfällig + reminded: Gemahnt + cancelled: Storniert + recipient: Empfänger + recipient_email: Empfänger E-Mail + recipient_address: Empfänger Adresse + due_at: Fällig am + issued_at: Gestellt am + sent_at: Verschickt am + cost: Betrag + total: Total + total_inkl_vat: Total inkl. MWSt. + total: Total inkl. MWSt. + vat: MWSt. + + invoice_article: + number: Artikelnummer + name: Bezeichnung + description: Beschreibung + category: Kategorie + unit_cost: Preis + vat_rate: MWSt. + cost_center: Kostenstelle + account: Konto + + invoice_item: + name: Name + description: Beschreibung + vat_rate: MWSt. + unit_cost: Preis + count: Anzahl + cost: Betrag + + payment: + invoice: Rechnung + amount: Betrag + received_at: Empfangen am + + payment_reminder: + invoice: Rechnung + message: Nachricht + due_at: Fällig am errors: messages: diff --git a/config/locales/models.en.yml b/config/locales/models.en.yml index e3e0bc9aab..f02221754c 100644 --- a/config/locales/models.en.yml +++ b/config/locales/models.en.yml @@ -37,6 +37,12 @@ en: attributes: body: placeholder_missing: 'must contain the placeholder %{placeholder} ' + event: + attributes: + base: + contact_attr_mandatory: "'%{attribute}' is a mandatory field and cannot be set to optional or 'do not display'." + contact_attr_invalid: "'%{attribute}' is no valid person attribute." + contact_attr_hidden_required: "'%{attribute}' cannot be set as mandatory or as 'do-not-display'" event/date: attributes: finish_at: @@ -46,6 +52,7 @@ en: choices: requires_more_than_one_choice: 'needs at least two picks' event/participation: + emoji_suspected: 'Please don''t use any special characters (especially emojis).' attributes: person_id: taken: is already registered @@ -55,6 +62,7 @@ en: not_allowed: "'%{mail_name}' is not allowed" person: name_missing: 'Please enter a name' + emoji_suspected: 'Please don''t use any special characters (especially emojis).' attributes: email: taken: > @@ -124,6 +132,9 @@ en: event/role/cook: one: Kitchen other: Kitchen + event/role/helper: + one: Helper + other: Helpers event/role/participant: one: Participant other: Participants @@ -145,15 +156,15 @@ en: mailing_list: one: Subscription other: Subscriptions + note: + one: Note + other: Notes person: one: Person other: Persons person/add_request: one: request other: requests - person/note: - one: note - other: notes people_filter: one: Filter other: Filters @@ -217,6 +228,7 @@ en: nickname: Nickname email: Main e-mail emails: e-mails + layer_group: Main layer password: Password password_confirmation: Password confirmation current_password: Old password @@ -239,6 +251,7 @@ en: picture: Upload new picture remove_picture: Remove current image roles: Roles + tags: Tags created_at: Created updated_at: Changed person/add_request: @@ -248,10 +261,9 @@ en: created_at: date person/add_request/event: label: "%{body} in %{group}" + deleted_event: Deleted event person/add_request/mailing_list: label: "%{body} in %{group}" - person/note: - text: text group: name: Name short_name: Nickname @@ -295,6 +307,12 @@ en: teamer_count: Leaders count participant_count: Participants count applicant_count: Registrations count + applications_cancelable: Deregistration possible + display_booking_info: Display number of registrations + event/contact_attrs: + required: Mandatory + optional: Optional + hidden: Do not display event/answer: answer: Reply answers: @@ -329,7 +347,7 @@ en: preconditions: Preconditions prolongations: Extended qualification_kinds: Qualifies for - general_information: General information + general_information: Standard description application_conditions: Application conditions created_at: Created updated_at: Changed @@ -346,7 +364,12 @@ en: choices: Possible answers multiple_choices: Multiple choice required: Reply mandatory - questions: + admin_questions: + question: Question + choices: Possible answers + multiple_choices: Multiple choice + required: Reply mandatory + application_questions: question: Question choices: Possible answers multiple_choices: Multiple choice @@ -356,6 +379,8 @@ en: person: Person type: Role participation: Person + note: + text: Text qualification: qualification_kind: Qualification qualification_kind_id: Qualification @@ -479,6 +504,8 @@ en: padding_top: Margin top padding_left: Margin left dimensions: Number + nickname: Nickname on label + pp_post: PP-Line errors: messages: invalid_date: "is not a valid date" diff --git a/config/locales/models.fr.yml b/config/locales/models.fr.yml index 745bd45ab2..0f817cf4d6 100644 --- a/config/locales/models.fr.yml +++ b/config/locales/models.fr.yml @@ -37,6 +37,12 @@ fr: attributes: body: placeholder_missing: 'doit contenir la variable %{placeholder}' + event: + attributes: + base: + contact_attr_mandatory: "'%{attribute}' est un champ obligatoire et ne peut pas être défini comme optionnel ou invisible" + contact_attr_invalid: "'%{attribute}' n'est pas un attribut personnel valable" + contact_attr_hidden_required: "'%{attribute}' ne peut pas être défini comme optionnel ou invisible" event/date: attributes: finish_at: @@ -46,6 +52,7 @@ fr: choices: requires_more_than_one_choice: 'doit au moins contenir deux réponses' event/participation: + emoji_suspected: 'Prière de ne pas utiliser de caractères spéciaux (en particulier, des emoji)' attributes: person_id: taken: est déjà annoncé @@ -55,6 +62,7 @@ fr: not_allowed: "'%{mail_name}' ne peut pas être utilisé" person: name_missing: 'Veuillez entrer votre nom' + emoji_suspected: 'Prière de ne pas utiliser de caractères spéciaux (en particulier, des emoji)' attributes: email: taken: > @@ -126,6 +134,9 @@ fr: event/role/cook: one: Cuisine other: Cuisine + event/role/helper: + one: Assistant/e + other: Assistant(e)s event/role/participant: one: Participant/-e other: Participant/-es @@ -147,15 +158,15 @@ fr: mailing_list: one: abonnement other: Abonnements + note: + one: Note + other: Notes person: one: Personne other: Personnes person/add_request: one: Question other: Questions - person/note: - one: Note - other: Notes people_filter: one: Filtre other: Filtres @@ -219,6 +230,7 @@ fr: nickname: Surnom email: Adresse e-mail principale emails: E-mails + layer_group: Niveau password: Mot de passe password_confirmation: Confirmation du mot de passe current_password: Ancien mot de passe @@ -251,10 +263,9 @@ fr: created_at: Date person/add_request/event: label: "%{body} dans %{group}" + deleted_event: Évènement supprimé person/add_request/mailing_list: label: "%{body} dans %{group}" - person/note: - text: Texte group: name: Nom short_name: Abréviation @@ -298,6 +309,12 @@ fr: teamer_count: Nombre d'équipe de direction participant_count: Nombre de participant-e-s applicant_count: Nombre d'inscriptions + applications_cancelable: Désinscription possible + display_booking_info: Voir l'état des inscriptions + event/contact_attrs: + required: Obligatoire + optional: Optionnel + hidden: Ne pas afficher event/answer: answer: Réponse answers: @@ -349,7 +366,12 @@ fr: choices: Réponses possibles multiple_choices: Sélection multiple required: réponse obligatoire - questions: + admin_questions: + question: Question + choices: Réponses possibles + multiple_choices: Sélection multiple + required: réponse obligatoire + application_questions: question: Question choices: Réponses possibles multiple_choices: Sélection multiple @@ -359,6 +381,8 @@ fr: person: Personne type: Rôle participation: Personne + note: + text: Texte qualification: qualification_kind: Qualification qualification_kind_id: Qualification @@ -482,6 +506,8 @@ fr: padding_top: Marge (en haut) padding_left: Marge (à gauche) dimensions: Nombre + nickname: Surnom sur l'étiquette + pp_post: Espace PP errors: messages: invalid_date: "n'est pas une date valable" diff --git a/config/locales/models.it.yml b/config/locales/models.it.yml index bd3e9edaf5..e94eb9ff0a 100644 --- a/config/locales/models.it.yml +++ b/config/locales/models.it.yml @@ -37,6 +37,12 @@ it: attributes: body: placeholder_missing: 'deve contenere il carattere %{placeholder}' + event: + attributes: + base: + contact_attr_mandatory: "'%{attribute}' è un campo obbligatorio e non può essere impostato come facoltativo o 'non visualizzare'" + contact_attr_invalid: "'%{attribute}' non è un attributo valido in merito alla persona" + contact_attr_hidden_required: "'%{attribute}' non può essere impostato come obbligatorio e 'non visualizzare'" event/date: attributes: finish_at: @@ -46,6 +52,7 @@ it: choices: requires_more_than_one_choice: 'deve contenere almeno due risposte' event/participation: + emoji_suspected: 'Non utilizzare simboli speciali (in particolare Emoji)' attributes: person_id: taken: è già iscritto @@ -55,6 +62,7 @@ it: not_allowed: "'%{mail_name}' non può essere utilizzato" person: name_missing: 'Inserire un nome' + emoji_suspected: 'Non utilizzare simboli speciali (in particolare Emoji)' attributes: email: taken: > @@ -89,6 +97,9 @@ it: name: must_be_unique: 'già existe' models: + acts_as_taggable_on/tag: + one: Tag + other: Tags additional_email: one: altro email other: Altre email @@ -122,6 +133,9 @@ it: event/role/cook: one: Cucina other: Cucina + event/role/helper: + one: Aiutante + other: Aiutanti event/role/participant: one: Partecipante other: Partecipante @@ -143,15 +157,15 @@ it: mailing_list: one: abbonamento other: Abbonamenti + note: + one: Nota + other: Note person: one: persona other: Persone person/add_request: one: Richiesta di accesso other: Richieste - person/note: - one: Nota - other: Note people_filter: one: filtro other: Filtri @@ -209,12 +223,13 @@ it: person: first_name: Nome last_name: Cognome - name: Cognome + name: Nome company_name: Nome della ditta company: Ditta nickname: Soprannome email: Email principale emails: Email + layer_group: Livello principale password: Password password_confirmation: Confermare la password current_password: Vecchia password @@ -237,6 +252,7 @@ it: picture: Carica una nuova foto remove_picture: Elimina la foto attuale roles: Ruoli + tags: Tags created_at: Salvato updated_at: Modificato person/add_request: @@ -246,12 +262,11 @@ it: created_at: Data person/add_request/event: label: "%{body} in %{group}" + deleted_event: Evento cancellato person/add_request/mailing_list: label: "%{body} in %{group}" - person/note: - text: Testo group: - name: Cognome + name: Nome short_name: Abbreviazione email: Email principale address: Indirizzo @@ -269,7 +284,7 @@ it: type_name: Tipo di gruppi event: group_ids: Gruppi - name: Cognome + name: Nome number: Numero motto: Motto cost: Costi @@ -293,6 +308,12 @@ it: teamer_count: Numero di organizzatori participant_count: Numero di partecipanti applicant_count: Numero di iscrizioni + applications_cancelable: Disiscrizione possibile + display_booking_info: Visualizza stato iscrizioni + event/contact_attrs: + required: Obbligatorio + optional: Facoltativo + hidden: Non visualizzare event/answer: answer: Risposta answers: @@ -344,7 +365,12 @@ it: choices: Possibili risposte multiple_choices: Scelta multipla required: Risposta obbligatoria - questions: + admin_questions: + question: Domanda + choices: Possibili risposte + multiple_choices: Scelta multipla + required: Risposta obbligatoria + application_questions: question: Domanda choices: Possibili risposte multiple_choices: Scelta multipla @@ -354,6 +380,8 @@ it: person: Persona type: Ruolo participation: Persona + note: + text: Testo qualification: qualification_kind: Qualifiche qualification_kind_id: Qualifica @@ -460,7 +488,7 @@ it: subscription: related_role_types: Ruoli people_filter: - name: Cognome + name: Nome custom_content: label: Testo subject: Oggetto @@ -477,6 +505,8 @@ it: padding_top: Margine superiore padding_left: Margine a sinistra dimensions: Numero + nickname: Soprannome sull'etichetta + pp_post: Riga PP errors: messages: invalid_date: "non è una data valida" diff --git a/config/locales/views.de.yml b/config/locales/views.de.yml old mode 100644 new mode 100755 index 3629bc646c..2663dbbf7f --- a/config/locales/views.de.yml +++ b/config/locales/views.de.yml @@ -1,4 +1,4 @@ -# Copyright (c) 2012-2015, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. @@ -12,6 +12,7 @@ de: "no": "nein" unknown: unbekannt nobody: niemand + all: Alle from: Von until: Bis @@ -64,6 +65,7 @@ de: placeholder_person: 'Person suchen...' placeholder_group: 'Gruppe suchen...' placeholder_event: 'Anlass suchen...' + placeholder_company_name: 'Firmennamen suchen...' list: index: @@ -113,6 +115,11 @@ de: dropdown/event/group_filter: all_groups: 'Alle Gruppen' + dropdown/event/events_export: + button: Export + csv: CSV + xlsx: Excel + dropdown/event/role_add: add: 'Person hinzufügen' @@ -126,6 +133,8 @@ de: dropdown/people_export: button: 'Export' csv: 'CSV' + xlsx: 'Excel' + vcard: 'vCard' labels: 'Etiketten' emails: 'E-Mail Adressen' addresses: 'Adressliste' @@ -133,6 +142,21 @@ de: condense_labels: 'Mehrfachsendungen vermeiden' condense_labels_hint: 'Nur einen Eintrag ausgeben, wenn die gesamte Adresse und der Nachname übereinstimmen.' + dropdown/invoices: + download: 'Export' + print: 'Drucken' + labels: 'Etiketten' + full: 'Rechnung inkl. Einzahlungsschein' + esr_only: 'Einzahlungsschein separat' + articles_only: 'Rechnung separat' + csv: 'CSV' + pdf: 'PDF' + + dropdown/invoice_sending: + button: 'Rechnung stellen' + state: 'Status setzen' + mail: 'Status setzen und per E-Mail verschicken' + devise: failure: already_authenticated: 'Du bist bereits angemeldet.' @@ -212,7 +236,7 @@ de: explanation: 'Wir arbeiten gerade an dieser Seite, deshalb ist sie nicht verfügbar. Das sollte nicht all zu lange dauern.' instruction: 'Bitte versuchen Sie es später nochmals.' - export/csv/events/list: + export/tabular/events/list: group_names: 'Organisatoren' duration: Zeitraum date: "Datum %{index}" @@ -222,20 +246,21 @@ de: applied_to: Anmeldung an alternative_dates: Ausweichdaten priorities: 'Anmeldeprioritäten' - no_qualifications_could_be_prolonged: "%{person} hat keine Qualifikationen, die verlängert werden können." lists: courses: title: 'Verfügbare Kurse' explanation: 'Hier werden Kurse deiner Gruppe, sowie deren Übergruppen angezeigt. Andere Kurse findest du bei der organisierenden Gruppe.' csv_export_button: 'CSV Export' + ical_export_button: 'Kalender Export' events: title: 'Demnächst stattfindende Anlässe' explanation: 'Hier werden Anlässe von Gruppen, bei denen du Mitglied bist, sowie deren Übergruppen angezeigt. Andere Anlässe findest du bei der organisierenden Gruppe.' - + apply_until: 'bis %{date}' application_market: - already_assigned: 'Diese Person ist bereits anderweitig zugeteilt.' + already_assigned: 'Diese Person ist bereits anderweitig zugeteilt oder bei diesem Anlass angemeldet.' participation: registered_at: 'Anmeldung am %{date}' + town: Wohnort prio_buttons: national_waitinglist: 'nationale Warteliste' index: @@ -261,11 +286,25 @@ de: qualifications: for_participants: 'Qualifikationen für Teilnehmende' for_leaders: 'Qualifikationen für Leitende' - + precondition_fields: + add_precondition_grouping: '+ Vorbedingungen hinzufügen' + add_precondition: 'Hinzufügen' + qualifications: + or: oder + and: und + + participation_contact_datas: + edit: + title: Kontaktangaben + save: Weiter participations: + edit: + title: Anmeldung approvals: title: Freigabe specific_information: 'Spezifische Angaben' + application_answers: Anmeldeangaben + admin_answers: Administrationsangaben no_answer_given: '(nicht beantwortet)' actions_show: change_contact_data_button: 'Kontaktdaten ändern' @@ -294,6 +333,17 @@ de: on_waiting_list: Auf Warteliste list: incomplete: Pflichtangaben fehlen + cancel_application: + explanation: Du bist für diesen Anlass angemeldet. + caption: Abmelden + confirmation: "Bist Du sicher, dass Du dich von diesem Anlass abmelden möchtest?" + + qualifications: + index: + save: Qualifikationen aktualisieren + update: + flash: + success: Die Qualifikationen wurden erfolgreich aktualisiert. register: register: @@ -313,21 +363,32 @@ de: signature_confirmation_text_default: Erziehungsberechtigte Person (bei Minderjährigen) attachments: add: "hinzufügen" + default_description_link: + insert_general_information: Standardbeschreibung einsetzen form: caption_external_applications: 'Externe können sich für diesen Anlass anmelden' caption_prioritization: 'Teilnehmende können bei der Anmeldung zwei weitere Kurse als Alternativen angeben' caption_requires_approval: 'Der/Die Gruppenleiter/-in wird informiert und muss die Anmeldung bestätigen' - additional_information: 'Angaben für Anmeldung' times_are_optional: Uhrzeiten sind optional explain_application_questions: Hier kannst du weitere Angaben für die Anmeldung verlangen. Gib mögliche Antworten mit Komma getrennt ein oder lass das Feld leer, um beliebige Antworten zu ermöglichen. + explain_admin_questions: Hier kannst du weitere Angaben pro Teilnehmer/-in definieren, + welche nur für die Kursadministration verwendet werden. + explain_contact_attrs: Hier kannst du wählen, welche Kontaktangaben bei der Anmeldung + abgefragt werden sollen. + caption_applications_cancelable: Teilnehmende können sich selbst abmelden + caption_display_booking_info: Die Anzahl Anmeldungen/Plätze ist auf der Kursliste für alle sichtbar form_tabs: general: Allgemein + application_questions: Anmeldeangaben + admin_questions: Administrationsangaben + contact_attrs: Kontaktangaben global: link: add_event: Anlass erstellen add_event/course: Kurs erstellen + duplicate: Duplizieren minimum_age_with_years: '%{minimum_age} Jahre (Jahrgang)' new: title_event: Anlass erstellen @@ -335,6 +396,7 @@ de: preconditions: 'Erforderliche Qualifikationen' tabs: participants: 'Teilnehmende' + export_enqueued: 'Export wird im Hintergrund gestartet und nach Fertigstellung an %{email} versendet.' event/applications: approved: 'Die Anmeldung wurde freigegeben.' @@ -356,7 +418,8 @@ de: event/participations: full_entry_label: '%{model_label} von %{person} in %{event}' success: '%{full_entry_label} wurde erfolgreich erstellt. Bitte überprüfe die Kontaktdaten und passe diese gegebenenfalls an.' - instructions: 'Für die definitive Anmeldung musst du diese Seite über Drucken ausdrucken, unterzeichnen und per Post an die entsprechende Adresse schicken.' + instructions: 'Für die definitive Anmeldung musst du die Teilnahmebestätigung über Drucken ausdrucken, unterzeichnen und per Post an die entsprechende Adresse schicken.' + export_enqueued: 'Export wird im Hintergrund gestartet und nach Fertigstellung an %{email} versendet.' show: link: @@ -369,8 +432,9 @@ de: event/precondition_checker: preconditions_not_fulfilled: 'Vorbedingungen für Anmeldung sind nicht erfüllt.' - below_minimum_age: "Altersgrenze von %{course_minimum_age} unterschritten." + below_minimum_age: "Altersgrenze von %{course_minimum_age} Jahren ist unterschritten." qualifications_missing: "Folgende Qualifikationen fehlen: %{missing}" + some_qualifications_missing: "Erforderliche Qualifikationen fehlen." event/register: not_logged_in: "Du musst dich einloggen um dich für den Anlass '%{event}' anzumelden." @@ -397,11 +461,11 @@ de: active_participants_info: one: '%{count} Anmeldung zugeteilt' other: '%{count} Anmeldungen zugeteilt' - issue_only: 'Vergibt die %{model} %{issued} auf den %{until} (letztes Kursdatum).' + issue_only: 'Vergibt die %{model} %{issued} unmittelbar per %{until} (letztes Kursdatum).' prolong_only: - one: 'Verlängert existierende Qualifikation %{prolonged} auf den %{until} (letztes Kursdatum).' - other: 'Verlängert existierende Qualifikationen %{prolonged} auf den %{until} (letztes Kursdatum).' - issue_and_prolong: 'Vergibt die %{model} %{issued} und verlängert existierende Qualifikationen %{prolonged} auf den %{until} (letztes Kursdatum).' + one: 'Verlängert existierende Qualifikation %{prolonged} unmittelbar per %{until} (letztes Kursdatum).' + other: 'Verlängert existierende Qualifikationen %{prolonged} unmittelbar per %{until} (letztes Kursdatum).' + issue_and_prolong: 'Vergibt die %{model} %{issued} und verlängert existierende Qualifikationen %{prolonged} unmittelbar per %{until} (letztes Kursdatum).' filter_navigation/dropdown: @@ -411,8 +475,7 @@ de: custom_filter: 'Eigener Filter' entire_layer: 'Gesamte Ebene' entire_group: 'Gesamte Gruppe' - new_role_filter: 'Neuer Rollen Filter...' - new_qualification_filter: 'Neuer Qualifikationen Filter...' + new_filter: 'Neuer Filter...' filter_navigation/event/participations: predefined_filters: @@ -422,7 +485,7 @@ de: full_text: index: - title: 'Gefundene Personen' + title: 'Ergebnisse' incomplete_search_request: 'Bitte geben Sie mindestens zwei Zeichen ein.' group: @@ -471,11 +534,14 @@ de: attrs: contact_details: 'Kontaktangaben' additional_information: 'Weitere Angaben' + deleted_subgroups: + no_deleted_sub_groups: 'Keine gelöschten Gruppen vorhanden.' form: help_contact: 'Adresse und öffentliche Telefonnummern dieser Person verwenden.' global: link: add: Gruppe erstellen + deleted_person: Ohne Rollen new: title: '%{model} erstellen' reactivated: "Gruppe %{group} wurde erfolgreich reaktiviert." @@ -515,7 +581,61 @@ de: import/person_doublette_finder: duplicates: "%{count} Treffer in Duplikatserkennung." + invoices: + filter: + due_since: Fällig seit + due_since_list: + one_day: Gestern + one_week: Einer Woche + one_month: Einem Monat + add: Externe Rechnung erstellen + issued: Rechnung gestellt + sent: Rechnung versendet + reminder_sent: Mahnung versendet + payd: bezahlt + destroy: + flash: + success: Rechnung wurde storniert. + pdf: + total: Gesamtbetrag + total_vat: MWSt. gesamt + invoice_number: Rechnungsnummer + invoice_date: Rechnungsdataum + due_at: Fällig bis + + invoice_lists: + form: + recipient_info: + one: "Rechnung wird für eine Person erstellt." + other: "Rechnung wird für %{count} Personen erstellt." + create: + one: "Rechnung %{title} wurde erstellt." + other: "Rechnung %{title} wurde für %{count} Empfänger erstellt." + update: + zero: 'Es muss mindestens eine Rechnung im Status "Entwurf" ausgewählt werden.' + one: "Rechnung wurde gestellt." + other: "%{count} Rechnungen wurden gestellt." + background_send: + zero: "Es werden keine Rechnungen per E-Mail verschickt." + one: "Rechnung wird im Hintergrund per E-Mail verschickt." + other: "%{count} Rechnungen werden im Hintergrund per E-Mail verschickt." + error: + no_mail: "Rechnung %{number} an %{name} kann nicht verschickt werden, da keine E-Mail hinterlegt ist." + not_draft: "Rechnung %{number} wurde bereits verschickt." + destroy: + zero: "Zuerst muss eine Rechnung ausgewählt werden." + one: "Rechnung wurde storniert." + other: "%{count} Rechnungen wurden storniert." + + payments: + create: + flash: + success: Zahlung über %{amount} wurde erfasst. + label_formats: + global_labels: "Globale Etikettenformate" + own_labels: Meine Etikettenformate + see_global_labels: "Globale Etikettenformate anzeigen" form: portrait: 'Hochformat' landscape: 'Querformat' @@ -561,7 +681,16 @@ de: groups: 'Gruppen' events: 'Anlässe' courses: 'Kurse' - admin: 'Admin' + admin: 'Einstellungen' + invoices: 'Rechnungen' + + notes: + new_note: Neue Notiz + note: + created: vor %{time_ago} + paginator: + newer_notes: Neuere Beiträge + older_notes: Altere Beiträge qualifications: in_years: '%{years} Jahre' @@ -583,6 +712,8 @@ de: person: + confirm_delete: "Sämtliche Daten zu %{person} werden endgültig gelöscht, inkl. Teilnahmen und Qualifikationen. Lösche diese Person nur, wenn niemand mehr diese Daten benötigt." + add_requests: body_list: @@ -685,13 +816,6 @@ de: ask_responsibles: request_link: Anfrage beantworten - notes: - note: - created: vor %{time_ago} - paginator: - newer_notes: Neuere Beiträge - older_notes: Altere Beiträge - tags: list: category_other: Andere @@ -738,6 +862,8 @@ de: tabs: history: Verlauf log: Log + colleagues: Mitarbeiter/-innen + invoices: Rechnungen actions_show: send_login: Login schicken @@ -792,9 +918,8 @@ de: tags: add_tag: Tag hinzufügen… no_entry: (keine) - - notes: - new_note: Neue Notiz + export_enqueued: 'Export wird im Hintergrund gestartet und nach Fertigstellung an %{email} versendet.' + export_email_needed: 'Für den export wird eine Email Adresse benötigt.' participations: destroy: @@ -806,18 +931,51 @@ de: save_search: Suche speichern save_filter: Filter speichern save_filter_placeholder: Geben Sie einen Namen an, um diesen Filter zu speichern - prompt_role_selection: Welche Rollen sollen angezeigt werden? + range: + deep: In der aktuellen Ebene und allen darunter liegenden Ebenen und Gruppen + layer: In der aktuellen Ebene und allen ihren Gruppen + group: Nur in der aktuellen Gruppe + group_deep: In der aktuellen Gruppe und allen darunter liegenden Gruppen + filters_qualification_validity: + active: Nur aktuell gültige Qualifikationen + reactivateable: Gültige und reaktivierbare Qualifikationen + all: Alle jemals erteilten Qualifikationen + filters_role_kind: + active: war die Role aktiv. + created: wurde die Role erstellt. + deleted: wurde die Role gelöscht. new: - title: Personen nach Rollen filtern + title: Personen filtern + + range: + prompt_range: In welchem Bereich soll gesucht werden? + + role: + title: Rollen + prompt_role_selection: Welche Rollen sollen angezeigt werden? + prompt_role_duration: Im Zeitraum (optional) + prompt_role_duration_selection: Welche Zeitraum sollen berücksichtigt werden? + prompt_role_duration_until: bis qualification: - title: Personen nach Qualifikationen filtern + title: Qualifikationen prompt_qualification_selection: Welche Qualifikationen sollen angezeigt werden? prompt_validity: Welche Gültigkeit sollen die angezeigten Qualifikationen haben? + prompt_year: Qualifikationsjahr einschränken prompt_kind: In welchem Bereich soll gesucht werden? + start_at_year_label: Erhalten zwischen + start_at_year_infix: und + start_at_year_suffix: '' + finish_at_year_label: Abgelaufen zwischen + finish_at_year_infix: und + finish_at_year_suffix: '' + not_enough_permissions: Du verfügst nicht über ausreichende Berechtigungen, um Personen mit diesem Filter zu suchen. simple_radio: + match: + one: Person hat mindestens EINE dieser Qualifikationen + all: Person hat ALLE diese Qualifikationen validity: active: Nur aktuell gültige Qualifikationen reactivateable: Gültige und reaktivierbare Qualifikationen @@ -838,6 +996,7 @@ de: add_person: Person hinzufügen add_role_for_person: Rolle für %{person} erstellen edit_role_for_person: Rolle für %{person} bearbeiten + new_role_for_person: Neue Rolle erstellen fields: help_optional_label: Optionale Bezeichnung der Rolle dieser Person @@ -900,6 +1059,7 @@ de: export_not_allowed: 'Du darfst die Abonnenten dieses Abos nicht exportieren, da diese auch Rollen und/oder Anlässe ausserhalb deines Berechtigungsbereiches enthalten.' subscription: only_persons_with: 'Nur Personen mit:' + export_enqueued: 'Export wird im Hintergrund gestartet und nach Fertigstellung an %{email} versendet.' version: attribute_change: diff --git a/config/locales/views.en.yml b/config/locales/views.en.yml index 0bbabcabcf..1965453fd3 100644 --- a/config/locales/views.en.yml +++ b/config/locales/views.en.yml @@ -83,6 +83,10 @@ en: available_placeholders_empty: 'No placeholders available' dropdown/event/group_filter: all_groups: 'All groups' + dropdown/event/events_export: + button: Export + csv: CSV + xlsx: Excel dropdown/event/role_add: add: 'Add person' dropdown/event/participant_add: @@ -93,10 +97,14 @@ en: dropdown/people_export: button: 'Export' csv: 'CSV' + xlsx: 'Excel' + vcard: 'vCard' labels: 'Labels' emails: 'E-Mail addresses' addresses: 'Address list' everything: 'All information' + condense_labels: 'Avoid multiple entries for household' + condense_labels_hint: 'Only give one entry for multiple people with the same adress and family name. ' devise: failure: already_authenticated: 'You are already registered.' @@ -122,7 +130,7 @@ en: updated: 'Your password has been changed. You''re now logged in.' updated_not_active: 'Your password has been changed.' send_paranoid_instructions: "If your e-mail address exist in our database, you will receive in a few minutes an e-mail with instructions on how to reset your password." - no_token: 'You can only access this page by clicking the link provided in a password reset email. If you have copied the link from a password reset email, please make sure you copied the entire URL provided.' + no_token: 'You can only access this page by clicking the link provided in a password reset e-mail. If you have copied the link from a password reset email, please make sure you copied the entire URL provided.' new: reset_password_button: 'Reset password' confirmations: @@ -164,7 +172,7 @@ en: title: 'Sorry, page is currently not available (503)' explanation: 'We are working on this page, so it is not available. This should not take too long.' instruction: 'Please try again later.' - export/csv/events/list: + export/tabular/events/list: group_names: 'Organizers' duration: Duration date: "Date %{index}" @@ -173,7 +181,6 @@ en: applied_to: Registration to alternative_dates: Alternative dates priorities: 'Application priorities' - no_qualifications_could_be_prolonged: "%{person} has no qualifications that can be extended." lists: courses: title: 'Available courses' @@ -183,7 +190,7 @@ en: title: 'Next events' explanation: 'Events of your groups and their top groups are shown here. Other events you''ll find in the organizing group.' application_market: - already_assigned: 'This person is already allocated elsewhere.' + already_assigned: 'This person was already assigned or registered for a different event.' participation: registered_at: 'Registration on %{date}' prio_buttons: @@ -197,6 +204,11 @@ en: label: 'Waiting list' popover_waiting_list: waiting_list_info: Put this person on the waiting list for courses of of %{event_kind}. + attachments: + create: + failure: "Upload failed: %{errors}." + destroy: + failure: "Failed to delete: %{errors}." kinds: form: help_minimum_age: 'Years (vintage on the first course date is decisive)' @@ -226,6 +238,7 @@ en: signature: Signature signature_confirmation: First name, last name, signature heading_event/course: Course application %{year} + heading_event: Registration for event read_and_agreed_for_event: I meet the requirements for the event, have read the event description and agree to them. read_and_agreed_for_event_course: I meet the course prerequisites, have read the course description and course regulations and agree to them. requirements_for_event: Requirements for the event @@ -235,6 +248,10 @@ en: on_waiting_list: On waiting list list: incomplete: Required data is missing + cancel_application: + explanation: You are registered for this event. + caption: Cancel registration + confirmation: "Are you sure, that you want to cancel your registration?" register: register: title: Collect contact data @@ -250,13 +267,17 @@ en: caption_signature: Participants must sign the application caption_signature_confirmation: Application must be confirmed with a second signature signature_confirmation_text_default: Legal guardian (for minors) + attachments: + add: "add" + default_description_link: + insert_general_information: Insert standard description form: caption_external_applications: 'Externals can sign up for this event' caption_prioritization: 'Participants may set two additional courses as alternatives' caption_requires_approval: 'The group leader will be informed and has to confirm your registration' - additional_information: 'Information for registration' times_are_optional: Times are optional explain_application_questions: Here you can request further information for the application. Give possible answers separated by a comma or leave the field blank to allow any answers. + caption_applications_cancelable: Participants can cancel their registration form_tabs: general: General global: @@ -287,7 +308,6 @@ en: event/participations: full_entry_label: '%{model_label} from %{person} in %{event}' success: '%{full_entry_label} has successfully been created. Please check the contact data, and update them if necessary.' - instructions: 'For the final registration you have to print the page via Print, sign and mail it to the appropriate address.' show: link: delete: 'Delete application' @@ -297,7 +317,6 @@ en: remove: 'Markes course as not passed and removes qualifications.' event/precondition_checker: preconditions_not_fulfilled: 'Prerequisites for registration are not met.' - below_minimum_age: "Age limit falls below %{course_minimum_age}" qualifications_missing: "The following qualifications are missing: %{missing}" event/register: not_logged_in: "You must log in for the registration of the event '%{event}'. " @@ -313,11 +332,15 @@ en: apply: 'Register' applied: 'Registered' not_possible: 'not possible' - issue_only: 'Assigns the %{model} %{issued} on the %{until} (last course date).' - prolong_only: - one: 'Extends existing qualification %{prolonged} until %{until} (last course date).' - other: 'Extends existing qualifications %{prolonged} until %{until} (last course date).' - issue_and_prolong: 'Assigns the %{model} %{issued} and extends existing qualifications %{prolonged} until %{until} (last course date).' + participants_info: + one: '%{count} registered' + other: '%{count} registered' + participants_info_with_limit: + one: '%{count} registered for %{limit} plesac' + other: '%{count} registered for %{limit} places' + active_participants_info: + one: '%{count} assigned registration' + other: '%{count} assigned registrations' filter_navigation/dropdown: additional_views: 'Further views' filter_navigation/people: @@ -328,13 +351,43 @@ en: new_qualification_filter: 'New qualification filter...' filter_navigation/event/participations: predefined_filters: - all: All Persons + all: All people teamers: Leaders participants: Participants full_text: index: title: 'people found' incomplete_search_request: 'Please enter at least two characters.' + group: + merge: + select: + title: 'Merge %{group} ' + explanation: When merging two groups all the subgroups, events and people including their roles will be merged. Subscriptions will not be merged. All former groups will still be visible under 'Deleted'. + merge_is_irreversible: This process cannot be undone! + new_group_name: 'Name of the merged group' + merge_group_with: 'Merge %{group} with' + merge_button: 'Merge group' + move: + select: + title: "Move group" + select_group: "Please pick the target group that will contain the current group." + person_add_requests: + actions_index: + activate: Activate manual approval + deactivate: Deactivate + activate_title: Activate manual approval + deactivate_title: Deactivate manual approval + index: + activated: Manual approval is active + deactivated: Manual approval is not active + explanation: > + When active people from this layer will only be added to other groups, + events or subscriptions after an authorized person has given his/her approval. + list: + approve: Accept + reject: Deny + approvers: + title: 'Notifications to' groups: actions_show: export_subgroups: 'CSV subgroups' @@ -342,6 +395,8 @@ en: attrs: contact_details: 'Contact data' additional_information: 'More information' + deleted_subgroups: + no_deleted_sub_groups: 'No deleted groups found.' form: help_contact: 'Use public address and telephone number of this person.' global: @@ -353,6 +408,11 @@ en: subgroups: 'Subgroups' tabs: deleted: 'Deleted' + group/person_add_requests: + deactivated: Manual approval deactivated + activated: Manual approval activated + global: + no_list_entries: No pending approvals. group/merge: success: 'The selected groups were merged to form the new group %{new_group_name}.' failure: 'The selected groups can''t be merged.' @@ -375,6 +435,9 @@ en: import/person_doublette_finder: duplicates: "%{count} results in duplicate detection." label_formats: + global_labels: "Global label formats" + own_labels: My label formats + see_global_labels: "Show global labels" form: portrait: 'Portrait' landscape: 'Landscape' @@ -414,7 +477,7 @@ en: groups: 'Groups' events: 'Events' courses: 'Courses' - admin: 'Admin' + admin: 'Settings' qualifications: in_years: '%{years} years' valid_until: 'to %{date}' @@ -430,6 +493,126 @@ en: title: Create qualification kind paper_trail/version_decorator: by: 'from %{author}' + person: + confirm_delete: "All Data for %{person} will be completely deleted, including any participations and qualifications. Delete this person only, if no one needs this data anymore." + add_requests: + body_list: + open_requests: + one: 1 pending approval. + other: '%{count} pending approvals.' + request_to: Request to + cancel: Cancel request for access + creator: + group: + success: "We sent an approval request for %{person}. The role will be created as soon as the request was granted. Any additional details for the role have to entered again after the approval." + failure: "The role could not be created, because for %{person} you need be granted access by approval. %{errors}." + event: + success: "We sent an approval request for %{person}. The registration will be created as soon as the request was granted. Any additional details for the registration have to entered again after the approval." + failure: "The registration could not be created, because for %{person} you need be granted access by approval. %{errors}." + mailing_list: + success: "We sent an approval request for %{person}. The subscription will be created as soon as the request was granted." + failure: "%{person} could not be added, because you need be granted access using an approval request. %{errors}." + approve: + success_notice: "The request for access for %{person} was granted." + failure_notice: "The request for approval für %{person} could not be granted: %{errors}." + reject: + success_notice: "The request for access for %{person} has been denied." + cancel: + success_notice: "The request for access for %{person} has been retracted." + status: + group: + approved: "The request for access for %{person} has already been granted." + rejected: "The request for access for %{person} has already been denied." + event: + approved: "The request for access for %{person} has already been granted." + rejected: "The request for access for %{person} has already been denied." + mailing_list: + approved: "The request for access for %{person} has already been granted." + rejected: "The request for access for %{person} has already been denied." + csv_imports: + description: + header: CSV files + explain_csv_format: 'A CSV file contains the details of a person on each line. The first line contains the field names. The fields are separated with a comma. If a field value itself contains commas, it must be complemented with quotation marks ("). Example:' + example: | + First name,Last name,e-Mail,Place,gender + Hans,Muster,hans@beispiel.ch,Bern,m + Vreni,Bischofsberger,vreni@beispiel.net,"Zürich, New York",f + explain_field_names: The fields in the CSV file can be assigned to the fields in the application in the next step. + explain_role: All people in a file will be assigned to the same role, which can be selected in the next step. + special_fields: Special field values + explain_special_fields_html: The following fields must contain one of the italic values. + preview_table: + icon_tooltip_invalid: 'Invalid values' + icon_tooltip_updated: 'Updating' + icon_tooltip_created: 'Recording' + icon_tooltip_request: 'is being requested' + define_mapping: + choose_role: 'Select role' + choose_role_help: 'This role will be assigned to all imported people' + assign_columns_to_fields: 'Assign columns to fields' + column_from_csv: 'Column from CSV' + field_in_database: 'Field in database' + preview: 'Preview' + update_behaviour: Update behavior + keep_behaviour: > + Only update empty values in the database with the values from the CSV + file, existing values are preserved. + keep_behaviour_tags: Tags will be added. + override_behaviour: > + Overwrite existing values in the database with the values from the CSV + file. Empty values in the CSV file will delete existing values in the + database. + override_behaviour_tags: Tags will be removed or replaced. + update_behaviour_explanation: > + This behavior only applies to persons already existing in the database. + New persons are created with the data from the CSV file. + new: + upload_button: 'Upload' + csv_file: 'CSV file' + preview: + import_now_button: Import people now + import_details_html: > + The following persons will be importet with the role %{role} + into the group %{group}. + add_request_mailer: + ask_person_to_add: + request_link: Answer request + ask_responsibles: + request_link: Answer request + tags: + list: + category_other: Other + person/csv_imports: + invalid_file: Please select a valid CSV file. + duplicate_keys: + one: '%{list} was assigned more than once.' + other: '%{list} were assigned more than once.' + preview: + new: + one: '%{count} %{role} will be imported.' + other: '%{count} %{role} will be imported.' + updated: + one: '%{count} %{role} will be updated.' + other: '%{count} %{role} will be updated.' + failed: + one: '%{count} %{role} won''t be imported.' + other: '%{count} %{role} won''t be imported.' + requests: + one: '%{count} %{role} will receive a request for their data. Your data will not be imported.' + other: '%{count} %{role} will receive a request for their data. Your data will not be imported.' + create: + new: + one: '%{count} %{role} has been imported succesfully.' + other: '%{count} %{role} have been imported succesfully.' + updated: + one: '%{count} %{role} has been updated succesfully.' + other: '%{count} %{role} have been updated succesfully.' + failed: + one: '%{count} %{role} has not been imported.' + other: '%{count} %{role} have not been imported.' + requests: + one: '%{count} %{role} will receive a request for their data. Any changes from this import have to be repeated after having been granted access.' + other: '%{count} %{role} will receive a request for their data. Any changes from this import have to be repeated after having been granted access.' people: send_password_instructions: Login information has been sent. years_old: (%{years} years old) @@ -445,6 +628,9 @@ en: actions_index: add_person: Add Person import_list: Import list + add_requests: + approve_title: 'Approve request' + reject_title: 'Deny request' attrs: additional_data: More information events: My next events @@ -458,10 +644,10 @@ en: list: number_of_people_shown: one: "%{count} person displayed." - other: "%{count} persons displayed." + other: "%{count} people displayed." number_of_people_hidden: one: "%{count} more person is not visible for you." - other: "%{count} more persons are not visible for you." + other: "%{count} more people are not visible for you." log: no_changes: So far, no changes were recorded. pdf: @@ -469,7 +655,11 @@ en: roles: title: Active roles roles_aside: + add_role: Add role set_main_group: Set main group + tags: + add_tag: Add tag... + no_entry: (none) participations: destroy: flash: @@ -506,6 +696,7 @@ en: add_person: Add person add_role_for_person: Create role for %{person} edit_role_for_person: Edit role for %{person} + new_role_for_person: Create role fields: help_optional_label: Optional name of the role of this person person_fields: @@ -516,8 +707,8 @@ en: role_has_permissions: "The role %{role} in the group %{group} has the following permissions:" role_only_public: > The role %{role} in the group %{group} can only view public data (groups, - events and subscriptions; no other persons). - only_visible_from_above: This role is visible only to persons in this level, not by people from higher layers. + events and subscriptions; no other people). + only_visible_from_above: This role is visible only to people in this level, not by people from higher layers. subscriber: group: form: @@ -525,6 +716,8 @@ en: subgroups_and_siblings_selectable: 'Only groups within the subscription group or within the adjacent groups from tye %{type} can be selected' roles: please_choose_group: Please select a group + tags: + only_add_persons_with_tags: 'Only add people with one of the following tags:' exclude_person: form: exclude_subscriber: 'Exclude person' @@ -538,6 +731,8 @@ en: subscriber/exclude_person: success: 'Subscriber %{subscriber} has been successfully excluded' failure: '%{subscriber} is not a subscriber' + sheet/person/csv_import: + title: 'Import person via CSV' sheet/group: belongs_to: 'belongs to' deleted: '(deleted)' @@ -549,7 +744,10 @@ en: add_event: Add event exclude_person: 'Exclude person' list: - excluded_people: 'Excluded persons' + excluded_people: 'Excluded people' + export_not_allowed: 'You are not allowed to export the subscribers, because it contains people outside of your authorization.' + subscription: + only_persons_with: 'Only people with:' version: attribute_change: from_to: "%{attr} has been changed from %{from} to %{to}." @@ -559,6 +757,10 @@ en: create: "%{model} %{label} has been added." update: "%{model} %{label} has been updated: %{changeset}" destroy: "%{model} %{label} has been deleted." + person/add_request: + create: "Access for %{label} requested." + update: "Update of request for %{label}: %{changeset}" + destroy: "Request for %{label} has been answered." views: pagination: next: '>>' diff --git a/config/locales/views.fr.yml b/config/locales/views.fr.yml index 8c525b653c..281b7ebdc3 100644 --- a/config/locales/views.fr.yml +++ b/config/locales/views.fr.yml @@ -45,6 +45,7 @@ fr: placeholder_person: 'Chercher une personne …' placeholder_group: 'Chercher un groupe ….' placeholder_event: 'Chercher un événement ….' + placeholder_company_name: 'Chercher un nom d''entreprise…' list: index: title: '%{models}' @@ -83,6 +84,10 @@ fr: available_placeholders_empty: 'Pas d''éléments de substitution disponibles' dropdown/event/group_filter: all_groups: 'Tous les groupes' + dropdown/event/events_export: + button: Exporter + csv: CSV + xlsx: Excel dropdown/event/role_add: add: 'Ajouter une personne' dropdown/event/participant_add: @@ -93,6 +98,8 @@ fr: dropdown/people_export: button: 'Exporter' csv: 'CSV' + xlsx: 'Excel' + vcard: 'vCard' labels: 'Etiquettes' emails: 'Adresses électroniques' addresses: 'Liste d''adresses' @@ -166,7 +173,7 @@ fr: title: 'Nous présentons nos excuses, cette page n''est pas disponible pour le moment (503)' explanation: 'Nous travaillons sur cette page, c''est pourquoi elle n''est pas disponible. Ceci ne devrait pas durer très longtemps.' instruction: 'Prière de réessayer plus tard.' - export/csv/events/list: + export/tabular/events/list: group_names: 'organisateurs' duration: Délai date: "Date %{index}" @@ -175,7 +182,6 @@ fr: applied_to: Inscription auprès de alternative_dates: Dates alternatives priorities: 'Priorité d''inscription' - no_qualifications_could_be_prolonged: "%{person} n'a pas de qualifications susceptibles d'êtres prolongées." lists: courses: title: 'Cours disponibles' @@ -184,10 +190,12 @@ fr: events: title: 'Evénements prévus prochainement' explanation: 'Les événements de ton groupe ainsi que des groupes hiérarchiquement supérieurs sont indiqués ici. Tu trouveras d''autres événements auprès des groupes qui les organisent.' + apply_until: 'jusqu''au %{date}' application_market: already_assigned: 'Cette personne est déjà attribuée à un autre endroit.' participation: registered_at: 'Inscription le %{date}' + town: Domicile prio_buttons: national_waitinglist: 'liste d''attente nationale' index: @@ -211,10 +219,24 @@ fr: qualifications: for_participants: 'Qualifications des participants' for_leaders: 'Qualifications des responsables' + precondition_fields: + add_precondition_grouping: '+ Ajouter une condition préalable' + add_precondition: 'Ajouter' + qualifications: + or: ou + and: et + participation_contact_datas: + edit: + title: Coordonnées + save: Continuer participations: + edit: + title: Inscription approvals: title: Autorisation specific_information: 'Informations spécifiques' + application_answers: Données pour l'inscription + admin_answers: Données administratives no_answer_given: '(pas de réponse)' actions_show: change_contact_data_button: 'Modifier les données de contact' @@ -243,6 +265,16 @@ fr: on_waiting_list: Sur la liste d'attente list: incomplete: des indications obligatoires manquent + cancel_application: + explanation: Tu es déjà inscrit(e) à cet évènement. + caption: Désinscrire + confirmation: "Es-tu sûr/sure de vouloir annuler ton inscription à cet évènement?" + qualifications: + index: + save: Mettre à jour les qualifications + update: + flash: + success: Les qualifications ont été mises à jour. register: register: title: Saisir les données de contact @@ -260,19 +292,28 @@ fr: signature_confirmation_text_default: Autorité parentale (pour les mineurs) attachments: add: "ajouter" + default_description_link: + insert_general_information: Placer une description standard form: caption_external_applications: 'Les personnes externes peuvent s''inscrire à cet événement.' caption_prioritization: 'Les participants peuvent indiquer deux autres cours alternatifs lors de l''inscription.' caption_requires_approval: 'Le responsable de troupe est informé et doit confirmer l''inscription.' - additional_information: 'Données pour l''inscription' times_are_optional: L'heure est optionnelle. explain_application_questions: Tu peux demander des données supplémentaires pour l'inscription. Saisis les réponses possibles en les séparant avec une virgule, ou laisse le champ vierge pour permettre des réponses libres. + explain_admin_questions: Tu peux définir des données supplémentaires pour chaque participant(e); elles ne seront utilisées que pour l'administration du cours. + explain_contact_attrs: Tu peux choisir les coordonnées requises pour l'inscription. + caption_applications_cancelable: Les participants peuvent se désinscrire eux-mêmes. + caption_display_booking_info: Le nombre d'inscrits et de places libres est visible pour tous sur la liste de cours. form_tabs: general: Général + application_questions: Données pour l'inscription + admin_questions: Données administratives + contact_attrs: Coordonnées global: link: add_event: Créer un événement add_event/course: Créer un cours + duplicate: Dupliquer minimum_age_with_years: 'au moins %{minimum_age} ans (année de naissance)' new: title_event: Créer un événement @@ -296,7 +337,7 @@ fr: title: Créer une catégorie de cours event/participations: full_entry_label: '%{model_label} de %{person} dans %{event}' - success: '%{full_entry_label} a été ajouté avec succès. Prière de contrôler les informations de contact et de les adapter le cas échéant.' + success: '%{full_entry_label} a été ajouté avec succès.' instructions: 'Pour l''inscription définitive, tu dois imprimer cette page en utilisant l''option imprimer, puis la signer et l''envoyer par poste à l''adresse indiquée.' show: link: @@ -309,6 +350,7 @@ fr: preconditions_not_fulfilled: 'Des conditions d''inscription ne sont pas remplies.' below_minimum_age: "L'âge limite de %{course_minimum_age} n'est pas atteint." qualifications_missing: "Les qualifications suivantes manquent : %{missing}" + some_qualifications_missing: "Des qualifications requises manquent." event/register: not_logged_in: "Tu dois t'authentifier afin de pouvoir t'inscrire à l'événement '%{event}'." person_found: 'Nous t''avons trouvé(e) dans notre base de données.' @@ -393,11 +435,14 @@ fr: attrs: contact_details: 'Contacts' additional_information: 'Informations supplémentaires' + deleted_subgroups: + no_deleted_sub_groups: 'Pas de groupes effacés.' form: help_contact: 'Utiliser l''adresse et le numéro de téléphone public de cette personne' global: link: add: Créer un groupe + deleted_person: Personnes effacées new: title: 'Créer un %{model}' reactivated: "Le groupe %{group} a été réactivé avec succès." @@ -431,6 +476,9 @@ fr: import/person_doublette_finder: duplicates: "%{count} emplacements trouvés lors de la recherche de doublons." label_formats: + global_labels: "Formats globaux d'étiquettes" + own_labels: Mes formats d'étiquettes + see_global_labels: "Afficher les formats globaux d'étiquettes" form: portrait: 'Format portrait' landscape: 'Format paysage' @@ -471,6 +519,13 @@ fr: events: 'Activités' courses: 'Cours' admin: 'Administrateur' + notes: + new_note: Ajouter une note + note: + created: il y a %{time_ago} + paginator: + newer_notes: Nouvelles contributions + older_notes: Anciennes contributions qualifications: in_years: '%{years} ans' valid_until: 'à %{date}' @@ -487,6 +542,7 @@ fr: paper_trail/version_decorator: by: 'de %{author}' person: + confirm_delete: "Toutes les données sur %{person} vont être définitivement effacées, y compris les participations et les qualifications. N'efface cette personne que si plus personne n'a besoin de ces données. " add_requests: body_list: open_requests: @@ -570,12 +626,6 @@ fr: request_link: Répondre à la requête ask_responsibles: request_link: 'Répondre à la requête ' - notes: - note: - created: il y a %{time_ago} - paginator: - newer_notes: Plus récent - older_notes: Plus ancien tags: list: category_other: Autre @@ -616,6 +666,7 @@ fr: tabs: history: Historique log: Journal + colleagues: Collaborateurs/collaboratrices actions_show: send_login: Envoyer les données d'identification send_login_tooltip: @@ -658,8 +709,6 @@ fr: tags: add_tag: Ajouter un tag… no_entry: (aucun) - notes: - new_note: Ajouter une note participations: destroy: flash: @@ -676,8 +725,16 @@ fr: title: Filtrer les personnes par qualification prompt_qualification_selection: Quelles qualifications doivent apparaître? prompt_validity: Quelles validités doivent avoir les qualifications montrées? + prompt_year: Restreindre l'année de qualification prompt_kind: Dans quel secteur faut-il chercher ? + start_at_year_label: 'Reçu entre ' + start_at_year_infix: et + finish_at_year_label: Périmé entre + finish_at_year_infix: et simple_radio: + match: + one: La personne a au moins UNE de ces qualifications + all: La personne a TOUTES ces qualifications validity: active: Uniquement les qualifications valides reactivateable: Les qualifications valides et renouvelables @@ -696,6 +753,7 @@ fr: add_person: Ajouter une personne add_role_for_person: Créer un rôle pour %{person} edit_role_for_person: Modifier un rôle pour %{person} + new_role_for_person: Créer un nouveau rôle fields: help_optional_label: désignation facultative du rôle de cette personne person_fields: @@ -747,6 +805,7 @@ fr: export_not_allowed: 'Tu n''as pas le droit d''exporté les abonnés de cet abonnement, car ils contiennent des rôles ou des activités qui ne font pas partie de ton domaine d''autorisation.' subscription: only_persons_with: 'Seulement personnes avec:' + export_enqueued: 'L''exportation a commencé. Il sera envoyé à %{email} une fois terminé.' version: attribute_change: from_to: "%{attr} a été changé de %{from} à %{to}." diff --git a/config/locales/views.it.yml b/config/locales/views.it.yml index fc45cdf20b..f75112fc1c 100644 --- a/config/locales/views.it.yml +++ b/config/locales/views.it.yml @@ -45,6 +45,7 @@ it: placeholder_person: 'Ricerca della persona....' placeholder_group: 'Ricerca del gruppo...' placeholder_event: 'Ricerca dell''evento...' + placeholder_company_name: 'Ricerca nome della ditta' list: index: title: '%{models}' @@ -83,6 +84,10 @@ it: available_placeholders_empty: 'nessun carattere a disposizione' dropdown/event/group_filter: all_groups: 'tutti i gruppi' + dropdown/event/events_export: + button: esportare + csv: CSV + xlsx: Excel dropdown/event/role_add: add: 'aggiungere una persona' dropdown/event/participant_add: @@ -93,6 +98,8 @@ it: dropdown/people_export: button: 'esportare' csv: 'CSV' + xlsx: 'Excel' + vcard: 'vCard' labels: 'etichette' emails: 'indirizzi email' addresses: 'lista di indirizzi' @@ -166,7 +173,7 @@ it: title: 'siamo spiacenti, questa pagina non è disponibile al momento (503)' explanation: 'al momento stiamo lavorando a questa pagine e per questo motivo non è disponibile. Non dovrebbe durare a lungo.' instruction: 'vi preghiamo di riprovare nuovamente più tardi.' - export/csv/events/list: + export/tabular/events/list: group_names: 'organizzatori' duration: Periodo di tempo date: "Data %{index}" @@ -175,7 +182,6 @@ it: applied_to: iscrizioni a alternative_dates: dati alternativi priorities: 'Priorità per l''iscrizione' - no_qualifications_could_be_prolonged: "%{person} non ha nessuna qualifica che può essere prolungata." lists: courses: title: 'corsi disponibili' @@ -184,10 +190,12 @@ it: events: title: 'eventi che si terranno prossimamente' explanation: 'Qui vengono mostrati gli eventi dei gruppi così come nei gruppi superiori nei quali sei membro. Puoi trovare altri eventi tra i gruppi che li organizzano.' + apply_until: 'fino a %{date}' application_market: already_assigned: 'Questa persona è già assegnata da un''altra parte.' participation: registered_at: 'iscrizione il %{date}' + town: Domicilio prio_buttons: national_waitinglist: 'lista d''attesa nazionale' index: @@ -211,10 +219,24 @@ it: qualifications: for_participants: 'Qualifiche per i partecipanti' for_leaders: 'Qualifiche per gli animatori' + precondition_fields: + add_precondition_grouping: 'Aggiungere requisiti per la partecipazione' + add_precondition: 'aggiungere' + qualifications: + or: oppure + and: e + participation_contact_datas: + edit: + title: Dati di contatto + save: avanti participations: + edit: + title: Iscrizione approvals: title: Attivazione specific_information: 'dati specifici' + application_answers: Dati per l'iscrizione + admin_answers: Dati amministrativi no_answer_given: '(non risponde)' actions_show: change_contact_data_button: 'modificare i dati di contatto' @@ -243,6 +265,16 @@ it: on_waiting_list: In lista d'attesa list: incomplete: mancano dei campi obbligatori + cancel_application: + explanation: Sei iscritto a questo evento. + caption: Disiscriversi + confirmation: "Sei sicuro di volerti disiscrivere da questo evento?" + qualifications: + index: + save: Aggiornare le qualifiche + update: + flash: + success: Le qualifiche sono state aggiornate con successo. register: register: title: registrare i dati di contatto @@ -260,19 +292,28 @@ it: signature_confirmation_text_default: Rappresentante legale (per i minorenni) attachments: add: "aggiungere" + default_description_link: + insert_general_information: Inserire informazioni generali form: caption_external_applications: 'esterni possono iscriversi a questo evento' caption_prioritization: 'i partecipanti possono dare durante l''iscrizione due ulteriori corsi come alternative.' caption_requires_approval: 'il responsabile verrà informato e dovrà confermare l''iscrizione' - additional_information: 'dati per l''iscrizione' times_are_optional: Gli orari sono opzionali explain_application_questions: qui puoi richiedere altri dati per l'iscrizione. Digita le risposte dividendole con una virgola oppure lascia il campo vuoto per rendere possibile qualunque risposta. + explain_admin_questions: Qui per ogni partecipante puoi definire altri dati che vengono utilizzati solo per l'amministratzione del corso. + explain_contact_attrs: Qui puoi scegliere quali dati di contatto devono essere chiesti al momento dell'iscrizione. + caption_applications_cancelable: I partecipanti si possono disiscrivere personalmente + caption_display_booking_info: Il numero di iscrizioni/posti è visibile a tutti sulla lista del corso form_tabs: general: In generale + application_questions: Dati per l'iscrizione + admin_questions: Dati amministrativi + contact_attrs: Dati di contatto global: link: add_event: Creare un evento add_event/course: Creare un corso + duplicate: Raddoppiare minimum_age_with_years: '%{minimum_age} anni (anno di nascita)' new: title_event: Creare un evento @@ -296,7 +337,7 @@ it: title: Creare il tipo di corso event/participations: full_entry_label: '%{model_label} da %{person} a %{event}' - success: '%{full_entry_label} è stato creato con successo. Verifica i dati di contatto e eventualmente aggiornali' + success: '%{full_entry_label} è stato creato con successo.' instructions: 'per l''iscrizione definitiva devi stampare questa pagina tramita stampare, firmarla e spedirla per posta all''indirizzo corrispondente' show: link: @@ -309,6 +350,7 @@ it: preconditions_not_fulfilled: 'le condizioni per iscriversi non sono soddisfatte.' below_minimum_age: "il limite di età di %{course_minimum_age} non è raggiunto" qualifications_missing: "mancano le qualifiche seguenti: %{missing}" + some_qualifications_missing: "Le qualifiche necessarie mancano." event/register: not_logged_in: "devi effettuare l'accesso per poterti iscrivere all'evento '%{event}'." person_found: 'ti abbiamo trovato all''interno della nostra banca dati.' @@ -392,11 +434,14 @@ it: attrs: contact_details: 'Dati di contatto' additional_information: 'Altri dati' + deleted_subgroups: + no_deleted_sub_groups: 'Non ci sono gruppi cancellati' form: help_contact: 'Utilizzare l''indirizzo e il numero di telefono pubblico di questa persona' global: link: add: Creare un gruppo + deleted_person: Persone cancellate new: title: 'Creare %{model} ' reactivated: "Il gruppo %{group} è stato riattivato con successo." @@ -430,6 +475,9 @@ it: import/person_doublette_finder: duplicates: "%{count} risultati nel riconoscimento di duplicati." label_formats: + global_labels: "Formati globali delle etichette" + own_labels: I miei formati delle etichette + see_global_labels: "Indicare formati globali delle etichette" form: portrait: 'formato verticale' landscape: 'formato orizzontale' @@ -470,6 +518,13 @@ it: events: 'eventi' courses: 'corsi' admin: 'admin' + notes: + new_note: Nuova nota + note: + created: fa %{time_ago} + paginator: + newer_notes: Contributi più recenti + older_notes: Contributi più vecchi qualifications: in_years: '%{years} anni' valid_until: 'fino a %{date}' @@ -486,6 +541,7 @@ it: paper_trail/version_decorator: by: 'da %{author}' person: + confirm_delete: "Tutti i dati relativi a %{person} vengono cancellati definitivamente, incl. partecipazioni e qualifiche. Cancella questa persona solo se nessuno ha ancora bisogno di queste informazioni." add_requests: body_list: open_requests: @@ -549,10 +605,12 @@ it: keep_behaviour: > Aggiornare solo i campi vuoti nella banca dati con i dati del CSV; i valori già esistenti vengono mantenuti. + keep_behaviour_tags: I tags vengono completati. override_behaviour: > Sovrascrivere i valori già esistenti nella banca dati con i dati del CSV. I valori vuoti nel CSV cancellano in questo caso i valori esistenti nella banca dati. + override_behaviour_tags: I tags vengono cancellati o completati. update_behaviour_explanation: > Questa funzione si applica unicamente alle persone già esistenti nella banca dati. Nuove persone vengono sempre create in totale corrispondenza @@ -570,12 +628,6 @@ it: request_link: Rispondere alla richiesta ask_responsibles: request_link: Rispondere alla richiesta - notes: - note: - created: fa %{time_ago} - paginator: - newer_notes: Più nuovo - older_notes: Più vecchio tags: list: category_other: Altro @@ -616,6 +668,7 @@ it: tabs: history: Azioni log: Log + colleagues: Collaboratore actions_show: send_login: Inviare login send_login_tooltip: @@ -657,8 +710,6 @@ it: tags: add_tag: Aggiungere un tag no_entry: (nessuno) - notes: - new_note: Aggiungere una nota participations: destroy: flash: @@ -675,8 +726,16 @@ it: title: Filtrare le persone secondo le qualifiche prompt_qualification_selection: Quali qualifiche devono essere visualizzate? prompt_validity: Che validità devono avere le qualifiche mostrate? + prompt_year: Limitare l'anno delle qualifiche prompt_kind: In quale ambito deve essere ricercato? + start_at_year_label: Ricevuto tra + start_at_year_infix: e + finish_at_year_label: Scaduto tra + finish_at_year_infix: e simple_radio: + match: + one: La persona ha almeno UNA di queste qualifiche + all: La persona ha TUTTE queste qualifiche validity: active: Solo qualifiche attuali e valide reactivateable: Qualifiche valide e riattivabili @@ -695,6 +754,7 @@ it: add_person: Aggiungere una persona add_role_for_person: 'Inserire un ruolo per %{person} ' edit_role_for_person: 'Modificare il ruolo per %{person} ' + new_role_for_person: Creare un nuovo ruolo fields: help_optional_label: Designazione opzionale del ruolo di questa persona person_fields: @@ -714,6 +774,8 @@ it: subgroups_and_siblings_selectable: 'Possono essere selezionati unicamente i gruppi all''interno del gruppo abbonato oppure i gruppi vicini di tipo %{type}' roles: please_choose_group: prego selezionare un gruppo + tags: + only_add_persons_with_tags: 'Aggiungere solo una persona con i tags sottostanti' exclude_person: form: exclude_subscriber: 'Escludere abbonato/a' @@ -742,6 +804,9 @@ it: list: excluded_people: 'persone escluse' export_not_allowed: 'Non è possibiie esportare gli abbonati di questo abbonamento perché contengono anche ruoli e/o eventi del tuo ambito di autorizzazioni.' + subscription: + only_persons_with: 'Solo persone con' + export_enqueued: 'L''esportazione viene effettuata e verrà inviata a %{email}.' version: attribute_change: from_to: "%{attr} è stato modificato da %{from} in %{to}." diff --git a/config/routes.rb b/config/routes.rb index c41e8c1d71..2c711f102f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,6 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. @@ -18,10 +18,11 @@ get '/503', to: 'errors#503' get '/people/query' => 'person/query#index', as: :query_people + get '/people/company_name' => 'person/company_name#index', as: :query_company_name get '/people/:id' => 'person/top#show', as: :person + get '/events/:id' => 'event/top#show', as: :event resources :groups do - member do get :deleted_subgroups get :export_subgroups @@ -33,24 +34,37 @@ get 'move' => 'group/move#select' post 'move' => 'group/move#perform' - get 'person_notes' => 'person/notes#index' end + resource :invoice_list, except: [:edit] + resource :invoice_config, only: [:edit, :show, :update] + resources :invoices do + resources :payment_reminders, only: :create + resources :payments, only: :create + end + resources :invoice_articles + + resources :notes, only: [:index, :create, :destroy] + resources :people, except: [:new, :create] do + member do post :send_password_instructions put :primary_group get 'history' => 'person/history#index' get 'log' => 'person/log#index' + get 'colleagues' => 'person/colleagues#index' + get 'invoices' => 'person/invoices#index' end + resources :notes, only: [:create, :destroy] resources :qualifications, only: [:new, :create, :destroy] get 'qualifications' => 'qualifications#new' # route required for language switch scope module: 'person' do - resources :notes, only: [:create] - resources :tags, param: :name, only: [:create, :destroy] + post 'tags' => 'tags#create' + delete 'tags' => 'tags#destroy' get 'tags/query' => 'tags#query' end end @@ -64,14 +78,12 @@ get 'roles' => 'roles#new' # route required for language switch get 'roles/:id' => 'roles#edit' # route required for language switch - resources :people_filters, only: [:new, :create, :destroy] do - collection do - get 'qualification' - end - end + resources :people_filters, only: [:new, :create, :edit, :update, :destroy] get 'people_filters' => 'people_filters#new' # route required for language switch + get 'deleted_people' => 'group/deleted_people#index' + get 'person_add_requests' => 'group/person_add_requests#index', as: :person_add_requests post 'person_add_requests' => 'group/person_add_requests#activate' delete 'person_add_requests' => 'group/person_add_requests#deactivate' @@ -113,15 +125,21 @@ resources :attachments, only: [:create, :destroy] resources :participations do - get 'print', on: :member + collection do + get 'contact_data', controller: 'participation_contact_datas', action: 'edit' + post 'contact_data', controller: 'participation_contact_datas', action: 'update' + end + member do + get 'print' + end end resources :roles, except: [:index, :show] get 'roles' => 'roles#new' # route required for language switch get 'roles/:id' => 'roles#edit' # route required for language switch - resources :qualifications, only: [:index, :update, :destroy] - + get 'qualifications' => 'qualifications#index' + put 'qualifications' => 'qualifications#update' end end @@ -176,7 +194,11 @@ resources :qualification_kinds - resources :label_formats + resources :label_formats do + collection do + resource :settings, controller: 'label_format/settings', as: 'label_format_settings' + end + end resources :custom_contents, only: [:index, :edit, :update] get 'custom_contents/:id' => 'custom_contents#edit' diff --git a/config/rpm/rails-app.spec b/config/rpm/rails-app.spec index 716c7f2f20..3205cb5040 100644 --- a/config/rpm/rails-app.spec +++ b/config/rpm/rails-app.spec @@ -3,7 +3,7 @@ %define app_name RPM_NAME -%define app_version 1.14 +%define app_version 1.15 %define ruby_version 1.9.3 ### optional libs diff --git a/config/thinking_sphinx.yml b/config/thinking_sphinx.yml index 6cbee37b3e..9d34f5e93d 100644 --- a/config/thinking_sphinx.yml +++ b/config/thinking_sphinx.yml @@ -10,11 +10,13 @@ base: &generic max_filter_values: 1048576 charset_type: utf-8 utf8: false - # maps accented characters to their 'base' character (e.g. é => e) # furthermore, the following punctuation characters are indexed: _.: - charset_table: "0..9, a..z, _, U+002E->., U+003A, A..Z->a..z, U+00C0->a, U+00C1->a, U+00C2->a, U+00C3->a, U+00C4->a, U+00C5->a, U+00C7->c, U+00C8->e, U+00C9->e, U+00CA->e, U+00CB->e, U+00CC->i, U+00CD->i, U+00CE->i, U+00CF->i, U+00D1->n, U+00D2->o, U+00D3->o, U+00D4->o, U+00D5->o, U+00D6->o, U+00D9->u, U+00DA->u, U+00DB->u, U+00DC->u, U+00DD->y, U+00E0->a, U+00E1->a, U+00E2->a, U+00E3->a, U+00E4->a, U+00E5->a, U+00E7->c, U+00E8->e, U+00E9->e, U+00EA->e, U+00EB->e, U+00EC->i, U+00ED->i, U+00EE->i, U+00EF->i, U+00F1->n, U+00F2->o, U+00F3->o, U+00F4->o, U+00F5->o, U+00F6->o, U+00F9->u, U+00FA->u, U+00FB->u, U+00FC->u, U+00FD->y, U+00FF->y, U+0100->a, U+0101->a, U+0102->a, U+0103->a, U+0104->a, U+0105->a, U+0106->c, U+0107->c, U+0108->c, U+0109->c, U+010A->c, U+010B->c, U+010C->c, U+010D->c, U+010E->d, U+010F->d, U+0112->e, U+0113->e, U+0114->e, U+0115->e, U+0116->e, U+0117->e, U+0118->e, U+0119->e, U+011A->e, U+011B->e, U+011C->g, U+011D->g, U+011E->g, U+011F->g, U+0120->g, U+0121->g, U+0122->g, U+0123->g, U+0124->h, U+0125->h, U+0128->i, U+0129->i, U+012A->i, U+012B->i, U+012C->i, U+012D->i, U+012E->i, U+012F->i, U+0130->i, U+0134->j, U+0135->j, U+0136->k, U+0137->k, U+0139->l, U+013A->l, U+013B->l, U+013C->l, U+013D->l, U+013E->l, U+0142->l, U+0143->n, U+0144->n, U+0145->n, U+0146->n, U+0147->n, U+0148->n, U+014C->o, U+014D->o, U+014E->o, U+014F->o, U+0150->o, U+0151->o, U+0154->r, U+0155->r, U+0156->r, U+0157->r, U+0158->r, U+0159->r, U+015A->s, U+015B->s, U+015C->s, U+015D->s, U+015E->s, U+015F->s, U+0160->s, U+0161->s, U+0162->t, U+0163->t, U+0164->t, U+0165->t, U+0168->u, U+0169->u, U+016A->u, U+016B->u, U+016C->u, U+016D->u, U+016E->u, U+016F->u, U+0170->u, U+0171->u, U+0172->u, U+0173->u, U+0174->w, U+0175->w, U+0176->y, U+0177->y, U+0178->y, U+0179->z, U+017A->z, U+017B->z, U+017C->z, U+017D->z, U+017E->z, U+01A0->o, U+01A1->o, U+01AF->u, U+01B0->u, U+01CD->a, U+01CE->a, U+01CF->i, U+01D0->i, U+01D1->o, U+01D2->o, U+01D3->u, U+01D4->u, U+01D5->u, U+01D6->u, U+01D7->u, U+01D8->u, U+01D9->u, U+01DA->u, U+01DB->u, U+01DC->u, U+01DE->a, U+01DF->a, U+01E0->a, U+01E1->a, U+01E6->g, U+01E7->g, U+01E8->k, U+01E9->k, U+01EA->o, U+01EB->o, U+01EC->o, U+01ED->o, U+01F0->j, U+01F4->g, U+01F5->g, U+01F8->n, U+01F9->n, U+01FA->a, U+01FB->a, U+0200->a, U+0201->a, U+0202->a, U+0203->a, U+0204->e, U+0205->e, U+0206->e, U+0207->e, U+0208->i, U+0209->i, U+020A->i, U+020B->i, U+020C->o, U+020D->o, U+020E->o, U+020F->o, U+0210->r, U+0211->r, U+0212->r, U+0213->r, U+0214->u, U+0215->u, U+0216->u, U+0217->u, U+0218->s, U+0219->s, U+021A->t, U+021B->t, U+021E->h, U+021F->h, U+0226->a, U+0227->a, U+0228->e, U+0229->e, U+022A->o, U+022B->o, U+022C->o, U+022D->o, U+022E->o, U+022F->o, U+0230->o, U+0231->o, U+0232->y, U+0233->y, U+1E00->a, U+1E01->a, U+1E02->b, U+1E03->b, U+1E04->b, U+1E05->b, U+1E06->b, U+1E07->b, U+1E08->c, U+1E09->c, U+1E0A->d, U+1E0B->d, U+1E0C->d, U+1E0D->d, U+1E0E->d, U+1E0F->d, U+1E10->d, U+1E11->d, U+1E12->d, U+1E13->d, U+1E14->e, U+1E15->e, U+1E16->e, U+1E17->e, U+1E18->e, U+1E19->e, U+1E1A->e, U+1E1B->e, U+1E1C->e, U+1E1D->e, U+1E1E->f, U+1E1F->f, U+1E20->g, U+1E21->g, U+1E22->h, U+1E23->h, U+1E24->h, U+1E25->h, U+1E26->h, U+1E27->h, U+1E28->h, U+1E29->h, U+1E2A->h, U+1E2B->h, U+1E2C->i, U+1E2D->i, U+1E2E->i, U+1E2F->i, U+1E30->k, U+1E31->k, U+1E32->k, U+1E33->k, U+1E34->k, U+1E35->k, U+1E36->l, U+1E37->l, U+1E38->l, U+1E39->l, U+1E3A->l, U+1E3B->l, U+1E3C->l, U+1E3D->l, U+1E3E->m, U+1E3F->m, U+1E40->m, U+1E41->m, U+1E42->m, U+1E43->m, U+1E44->n, U+1E45->n, U+1E46->n, U+1E47->n, U+1E48->n, U+1E49->n, U+1E4A->n, U+1E4B->n, U+1E4C->o, U+1E4D->o, U+1E4E->o, U+1E4F->o, U+1E50->o, U+1E51->o, U+1E52->o, U+1E53->o, U+1E54->p, U+1E55->p, U+1E56->p, U+1E57->p, U+1E58->r, U+1E59->r, U+1E5A->r, U+1E5B->r, U+1E5C->r, U+1E5D->r, U+1E5E->r, U+1E5F->r, U+1E60->s, U+1E61->s, U+1E62->s, U+1E63->s, U+1E64->s, U+1E65->s, U+1E66->s, U+1E67->s, U+1E68->s, U+1E69->s, U+1E6A->t, U+1E6B->t, U+1E6C->t, U+1E6D->t, U+1E6E->t, U+1E6F->t, U+1E70->t, U+1E71->t, U+1E72->u, U+1E73->u, U+1E74->u, U+1E75->u, U+1E76->u, U+1E77->u, U+1E78->u, U+1E79->u, U+1E7A->u, U+1E7B->u, U+1E7C->v, U+1E7D->v, U+1E7E->v, U+1E7F->v, U+1E80->w, U+1E81->w, U+1E82->w, U+1E83->w, U+1E84->w, U+1E85->w, U+1E86->w, U+1E87->w, U+1E88->w, U+1E89->w, U+1E8A->x, U+1E8B->x, U+1E8C->x, U+1E8D->x, U+1E8E->y, U+1E8F->y, U+1E96->h, U+1E97->t, U+1E98->w, U+1E99->y, U+1EA0->a, U+1EA1->a, U+1EA2->a, U+1EA3->a, U+1EA4->a, U+1EA5->a, U+1EA6->a, U+1EA7->a, U+1EA8->a, U+1EA9->a, U+1EAA->a, U+1EAB->a, U+1EAC->a, U+1EAD->a, U+1EAE->a, U+1EAF->a, U+1EB0->a, U+1EB1->a, U+1EB2->a, U+1EB3->a, U+1EB4->a, U+1EB5->a, U+1EB6->a, U+1EB7->a, U+1EB8->e, U+1EB9->e, U+1EBA->e, U+1EBB->e, U+1EBC->e, U+1EBD->e, U+1EBE->e, U+1EBF->e, U+1EC0->e, U+1EC1->e, U+1EC2->e, U+1EC3->e, U+1EC4->e, U+1EC5->e, U+1EC6->e, U+1EC7->e, U+1EC8->i, U+1EC9->i, U+1ECA->i, U+1ECB->i, U+1ECC->o, U+1ECD->o, U+1ECE->o, U+1ECF->o, U+1ED0->o, U+1ED1->o, U+1ED2->o, U+1ED3->o, U+1ED4->o, U+1ED5->o, U+1ED6->o, U+1ED7->o, U+1ED8->o, U+1ED9->o, U+1EDA->o, U+1EDB->o, U+1EDC->o, U+1EDD->o, U+1EDE->o, U+1EDF->o, U+1EE0->o, U+1EE1->o, U+1EE2->o, U+1EE3->o, U+1EE4->u, U+1EE5->u, U+1EE6->u, U+1EE7->u, U+1EE8->u, U+1EE9->u, U+1EEA->u, U+1EEB->u, U+1EEC->u, U+1EED->u, U+1EEE->u, U+1EEF->u, U+1EF0->u, U+1EF1->u, U+1EF2->y, U+1EF3->y, U+1EF4->y, U+1EF5->y, U+1EF6->y, U+1EF7->y, U+1EF8->y, U+1EF9->y" # specify all indexed models to decrease loading time in development mode # and avoid obscurly missing models in production mode. + # maps accented characters to their 'base' character (e.g. é => e) + # furthermore, the following punctuation characters are indexed: _-.: + charset_table: "0..9, a..z, _, -, U+002E->., U+003A, A..Z->a..z, U+00C0->a, U+00C1->a, U+00C2->a, U+00C3->a, U+00C4->a, U+00C5->a, U+00C7->c, U+00C8->e, U+00C9->e, U+00CA->e, U+00CB->e, U+00CC->i, U+00CD->i, U+00CE->i, U+00CF->i, U+00D1->n, U+00D2->o, U+00D3->o, U+00D4->o, U+00D5->o, U+00D6->o, U+00D9->u, U+00DA->u, U+00DB->u, U+00DC->u, U+00DD->y, U+00E0->a, U+00E1->a, U+00E2->a, U+00E3->a, U+00E4->a, U+00E5->a, U+00E7->c, U+00E8->e, U+00E9->e, U+00EA->e, U+00EB->e, U+00EC->i, U+00ED->i, U+00EE->i, U+00EF->i, U+00F1->n, U+00F2->o, U+00F3->o, U+00F4->o, U+00F5->o, U+00F6->o, U+00F9->u, U+00FA->u, U+00FB->u, U+00FC->u, U+00FD->y, U+00FF->y, U+0100->a, U+0101->a, U+0102->a, U+0103->a, U+0104->a, U+0105->a, U+0106->c, U+0107->c, U+0108->c, U+0109->c, U+010A->c, U+010B->c, U+010C->c, U+010D->c, U+010E->d, U+010F->d, U+0112->e, U+0113->e, U+0114->e, U+0115->e, U+0116->e, U+0117->e, U+0118->e, U+0119->e, U+011A->e, U+011B->e, U+011C->g, U+011D->g, U+011E->g, U+011F->g, U+0120->g, U+0121->g, U+0122->g, U+0123->g, U+0124->h, U+0125->h, U+0128->i, U+0129->i, U+012A->i, U+012B->i, U+012C->i, U+012D->i, U+012E->i, U+012F->i, U+0130->i, U+0134->j, U+0135->j, U+0136->k, U+0137->k, U+0139->l, U+013A->l, U+013B->l, U+013C->l, U+013D->l, U+013E->l, U+0142->l, U+0143->n, U+0144->n, U+0145->n, U+0146->n, U+0147->n, U+0148->n, U+014C->o, U+014D->o, U+014E->o, U+014F->o, U+0150->o, U+0151->o, U+0154->r, U+0155->r, U+0156->r, U+0157->r, U+0158->r, U+0159->r, U+015A->s, U+015B->s, U+015C->s, U+015D->s, U+015E->s, U+015F->s, U+0160->s, U+0161->s, U+0162->t, U+0163->t, U+0164->t, U+0165->t, U+0168->u, U+0169->u, U+016A->u, U+016B->u, U+016C->u, U+016D->u, U+016E->u, U+016F->u, U+0170->u, U+0171->u, U+0172->u, U+0173->u, U+0174->w, U+0175->w, U+0176->y, U+0177->y, U+0178->y, U+0179->z, U+017A->z, U+017B->z, U+017C->z, U+017D->z, U+017E->z, U+01A0->o, U+01A1->o, U+01AF->u, U+01B0->u, U+01CD->a, U+01CE->a, U+01CF->i, U+01D0->i, U+01D1->o, U+01D2->o, U+01D3->u, U+01D4->u, U+01D5->u, U+01D6->u, U+01D7->u, U+01D8->u, U+01D9->u, U+01DA->u, U+01DB->u, U+01DC->u, U+01DE->a, U+01DF->a, U+01E0->a, U+01E1->a, U+01E6->g, U+01E7->g, U+01E8->k, U+01E9->k, U+01EA->o, U+01EB->o, U+01EC->o, U+01ED->o, U+01F0->j, U+01F4->g, U+01F5->g, U+01F8->n, U+01F9->n, U+01FA->a, U+01FB->a, U+0200->a, U+0201->a, U+0202->a, U+0203->a, U+0204->e, U+0205->e, U+0206->e, U+0207->e, U+0208->i, U+0209->i, U+020A->i, U+020B->i, U+020C->o, U+020D->o, U+020E->o, U+020F->o, U+0210->r, U+0211->r, U+0212->r, U+0213->r, U+0214->u, U+0215->u, U+0216->u, U+0217->u, U+0218->s, U+0219->s, U+021A->t, U+021B->t, U+021E->h, U+021F->h, U+0226->a, U+0227->a, U+0228->e, U+0229->e, U+022A->o, U+022B->o, U+022C->o, U+022D->o, U+022E->o, U+022F->o, U+0230->o, U+0231->o, U+0232->y, U+0233->y, U+1E00->a, U+1E01->a, U+1E02->b, U+1E03->b, U+1E04->b, U+1E05->b, U+1E06->b, U+1E07->b, U+1E08->c, U+1E09->c, U+1E0A->d, U+1E0B->d, U+1E0C->d, U+1E0D->d, U+1E0E->d, U+1E0F->d, U+1E10->d, U+1E11->d, U+1E12->d, U+1E13->d, U+1E14->e, U+1E15->e, U+1E16->e, U+1E17->e, U+1E18->e, U+1E19->e, U+1E1A->e, U+1E1B->e, U+1E1C->e, U+1E1D->e, U+1E1E->f, U+1E1F->f, U+1E20->g, U+1E21->g, U+1E22->h, U+1E23->h, U+1E24->h, U+1E25->h, U+1E26->h, U+1E27->h, U+1E28->h, U+1E29->h, U+1E2A->h, U+1E2B->h, U+1E2C->i, U+1E2D->i, U+1E2E->i, U+1E2F->i, U+1E30->k, U+1E31->k, U+1E32->k, U+1E33->k, U+1E34->k, U+1E35->k, U+1E36->l, U+1E37->l, U+1E38->l, U+1E39->l, U+1E3A->l, U+1E3B->l, U+1E3C->l, U+1E3D->l, U+1E3E->m, U+1E3F->m, U+1E40->m, U+1E41->m, U+1E42->m, U+1E43->m, U+1E44->n, U+1E45->n, U+1E46->n, U+1E47->n, U+1E48->n, U+1E49->n, U+1E4A->n, U+1E4B->n, U+1E4C->o, U+1E4D->o, U+1E4E->o, U+1E4F->o, U+1E50->o, U+1E51->o, U+1E52->o, U+1E53->o, U+1E54->p, U+1E55->p, U+1E56->p, U+1E57->p, U+1E58->r, U+1E59->r, U+1E5A->r, U+1E5B->r, U+1E5C->r, U+1E5D->r, U+1E5E->r, U+1E5F->r, U+1E60->s, U+1E61->s, U+1E62->s, U+1E63->s, U+1E64->s, U+1E65->s, U+1E66->s, U+1E67->s, U+1E68->s, U+1E69->s, U+1E6A->t, U+1E6B->t, U+1E6C->t, U+1E6D->t, U+1E6E->t, U+1E6F->t, U+1E70->t, U+1E71->t, U+1E72->u, U+1E73->u, U+1E74->u, U+1E75->u, U+1E76->u, U+1E77->u, U+1E78->u, U+1E79->u, U+1E7A->u, U+1E7B->u, U+1E7C->v, U+1E7D->v, U+1E7E->v, U+1E7F->v, U+1E80->w, U+1E81->w, U+1E82->w, U+1E83->w, U+1E84->w, U+1E85->w, U+1E86->w, U+1E87->w, U+1E88->w, U+1E89->w, U+1E8A->x, U+1E8B->x, U+1E8C->x, U+1E8D->x, U+1E8E->y, U+1E8F->y, U+1E96->h, U+1E97->t, U+1E98->w, U+1E99->y, U+1EA0->a, U+1EA1->a, U+1EA2->a, U+1EA3->a, U+1EA4->a, U+1EA5->a, U+1EA6->a, U+1EA7->a, U+1EA8->a, U+1EA9->a, U+1EAA->a, U+1EAB->a, U+1EAC->a, U+1EAD->a, U+1EAE->a, U+1EAF->a, U+1EB0->a, U+1EB1->a, U+1EB2->a, U+1EB3->a, U+1EB4->a, U+1EB5->a, U+1EB6->a, U+1EB7->a, U+1EB8->e, U+1EB9->e, U+1EBA->e, U+1EBB->e, U+1EBC->e, U+1EBD->e, U+1EBE->e, U+1EBF->e, U+1EC0->e, U+1EC1->e, U+1EC2->e, U+1EC3->e, U+1EC4->e, U+1EC5->e, U+1EC6->e, U+1EC7->e, U+1EC8->i, U+1EC9->i, U+1ECA->i, U+1ECB->i, U+1ECC->o, U+1ECD->o, U+1ECE->o, U+1ECF->o, U+1ED0->o, U+1ED1->o, U+1ED2->o, U+1ED3->o, U+1ED4->o, U+1ED5->o, U+1ED6->o, U+1ED7->o, U+1ED8->o, U+1ED9->o, U+1EDA->o, U+1EDB->o, U+1EDC->o, U+1EDD->o, U+1EDE->o, U+1EDF->o, U+1EE0->o, U+1EE1->o, U+1EE2->o, U+1EE3->o, U+1EE4->u, U+1EE5->u, U+1EE6->u, U+1EE7->u, U+1EE8->u, U+1EE9->u, U+1EEA->u, U+1EEB->u, U+1EEC->u, U+1EED->u, U+1EEE->u, U+1EEF->u, U+1EF0->u, U+1EF1->u, U+1EF2->y, U+1EF3->y, U+1EF4->y, U+1EF5->y, U+1EF6->y, U+1EF7->y, U+1EF8->y, U+1EF9->y" # specify all indexed models to decrease loading time in development mode # and avoid obscurly missing models in production mode. indexed_models: - - Person + - Event - Group + - Person development: <<: *generic diff --git a/db/migrate/20130625071410_add_multiple_choices_field_to_questions.rb b/db/migrate/20130625071410_add_multiple_choices_field_to_questions.rb index 2e7571bfdc..bb196519e4 100644 --- a/db/migrate/20130625071410_add_multiple_choices_field_to_questions.rb +++ b/db/migrate/20130625071410_add_multiple_choices_field_to_questions.rb @@ -7,6 +7,6 @@ class AddMultipleChoicesFieldToQuestions < ActiveRecord::Migration def change - add_column(:event_questions, :multiple_choices, :boolean, default: false) + add_column(:event_questions, :multiple_choices, :boolean, default: false, null: false) end end diff --git a/db/migrate/20140128145128_create_translation_tables.rb b/db/migrate/20140128145128_create_translation_tables.rb index d7f24766e7..08f93b8a55 100644 --- a/db/migrate/20140128145128_create_translation_tables.rb +++ b/db/migrate/20140128145128_create_translation_tables.rb @@ -1,3 +1,10 @@ +# encoding: utf-8 + +# Copyright (c) 2014, hitobito AG. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + class CreateTranslationTables < ActiveRecord::Migration def up CustomContent.create_translation_table!( diff --git a/db/migrate/20140129150113_add_qualified_to_participation.rb b/db/migrate/20140129150113_add_qualified_to_participation.rb index 70ca136529..f309974c21 100644 --- a/db/migrate/20140129150113_add_qualified_to_participation.rb +++ b/db/migrate/20140129150113_add_qualified_to_participation.rb @@ -1,3 +1,10 @@ +# encoding: utf-8 + +# Copyright (c) 2014, hitobito AG. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + class AddQualifiedToParticipation < ActiveRecord::Migration def up diff --git a/db/migrate/20140211092343_create_versions.rb b/db/migrate/20140211092343_create_versions.rb index 6fc2be1659..8869011dc6 100644 --- a/db/migrate/20140211092343_create_versions.rb +++ b/db/migrate/20140211092343_create_versions.rb @@ -1,3 +1,10 @@ +# encoding: utf-8 + +# Copyright (c) 2014, hitobito AG. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + class CreateVersions < ActiveRecord::Migration def self.up create_table :versions do |t| diff --git a/db/migrate/20140303120546_add_lockable_fields.rb b/db/migrate/20140303120546_add_lockable_fields.rb index a5e0d0c580..047b2cfcd9 100644 --- a/db/migrate/20140303120546_add_lockable_fields.rb +++ b/db/migrate/20140303120546_add_lockable_fields.rb @@ -1,3 +1,10 @@ +# encoding: utf-8 + +# Copyright (c) 2014, hitobito AG. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + class AddLockableFields < ActiveRecord::Migration def change add_column :people, :failed_attempts, :integer, default: 0 diff --git a/db/migrate/20140326103939_create_additional_emails.rb b/db/migrate/20140326103939_create_additional_emails.rb index 45cd931142..2416f54a3a 100644 --- a/db/migrate/20140326103939_create_additional_emails.rb +++ b/db/migrate/20140326103939_create_additional_emails.rb @@ -1,3 +1,10 @@ +# encoding: utf-8 + +# Copyright (c) 2014, hitobito AG. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + class CreateAdditionalEmails < ActiveRecord::Migration def change create_table :additional_emails do |t| diff --git a/db/migrate/20140702083911_add_authentication_token_to_people.rb b/db/migrate/20140702083911_add_authentication_token_to_people.rb index 71f0bca7dd..198f19c8bb 100644 --- a/db/migrate/20140702083911_add_authentication_token_to_people.rb +++ b/db/migrate/20140702083911_add_authentication_token_to_people.rb @@ -1,3 +1,10 @@ +# encoding: utf-8 + +# Copyright (c) 2014, hitobito AG. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + class AddAuthenticationTokenToPeople < ActiveRecord::Migration def change add_column :people, :authentication_token, :string diff --git a/db/migrate/20140715093651_add_required_to_event_questions.rb b/db/migrate/20140715093651_add_required_to_event_questions.rb index ce1b4ecc6e..5d2f9312ed 100644 --- a/db/migrate/20140715093651_add_required_to_event_questions.rb +++ b/db/migrate/20140715093651_add_required_to_event_questions.rb @@ -1,5 +1,12 @@ +# encoding: utf-8 + +# Copyright (c) 2014, hitobito AG. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + class AddRequiredToEventQuestions < ActiveRecord::Migration def change - add_column(:event_questions, :required, :boolean) + add_column(:event_questions, :required, :boolean, null: false, default: false) end end diff --git a/db/migrate/20140717080643_add_representative_event_participant_count.rb b/db/migrate/20140717080643_add_representative_event_participant_count.rb index 9286bb15fe..2d56dc3f37 100644 --- a/db/migrate/20140717080643_add_representative_event_participant_count.rb +++ b/db/migrate/20140717080643_add_representative_event_participant_count.rb @@ -1,3 +1,10 @@ +# encoding: utf-8 + +# Copyright (c) 2014, hitobito AG. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + class AddRepresentativeEventParticipantCount < ActiveRecord::Migration def change add_column :events, :representative_participant_count, :integer, default: 0 diff --git a/db/migrate/20140731181038_add_indexes_to_event_dates.rb b/db/migrate/20140731181038_add_indexes_to_event_dates.rb index 1dde498c94..32818734dd 100644 --- a/db/migrate/20140731181038_add_indexes_to_event_dates.rb +++ b/db/migrate/20140731181038_add_indexes_to_event_dates.rb @@ -1,3 +1,10 @@ +# encoding: utf-8 + +# Copyright (c) 2014, hitobito AG. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + class AddIndexesToEventDates < ActiveRecord::Migration def change add_index(:event_dates, [:event_id, :start_at]) diff --git a/db/migrate/20140813074515_people_relations.rb b/db/migrate/20140813074515_people_relations.rb index ad7f2c6f02..e1f3f5a597 100644 --- a/db/migrate/20140813074515_people_relations.rb +++ b/db/migrate/20140813074515_people_relations.rb @@ -1,3 +1,10 @@ +# encoding: utf-8 + +# Copyright (c) 2014, hitobito AG. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + class PeopleRelations < ActiveRecord::Migration def change create_table :people_relations do |t| diff --git a/db/migrate/20141218140203_modify_event_counts.rb b/db/migrate/20141218140203_modify_event_counts.rb index f9743c125e..120b1e70b8 100644 --- a/db/migrate/20141218140203_modify_event_counts.rb +++ b/db/migrate/20141218140203_modify_event_counts.rb @@ -1,9 +1,9 @@ # encoding: utf-8 # Copyright (c) 2012-2014, insieme Schweiz. This file is part of -# hitobito_insieme and licensed under the Affero General Public License version 3 +# hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito_insieme. +# https://github.com/hitobito/hitobito. class ModifyEventCounts < ActiveRecord::Migration def change diff --git a/db/migrate/20150123100928_remove_other_than_descendants_or_self_group_subscriptions.rb b/db/migrate/20150123100928_remove_other_than_descendants_or_self_group_subscriptions.rb index fa3281ed3d..a622b860f6 100644 --- a/db/migrate/20150123100928_remove_other_than_descendants_or_self_group_subscriptions.rb +++ b/db/migrate/20150123100928_remove_other_than_descendants_or_self_group_subscriptions.rb @@ -1,3 +1,10 @@ +# encoding: utf-8 + +# Copyright (c) 2015, hitobito AG. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + class RemoveOtherThanDescendantsOrSelfGroupSubscriptions < ActiveRecord::Migration def up diff --git a/db/migrate/20150210084002_convert_person_zip_to_string.rb b/db/migrate/20150210084002_convert_person_zip_to_string.rb index 920d536b06..e8041f0dc7 100644 --- a/db/migrate/20150210084002_convert_person_zip_to_string.rb +++ b/db/migrate/20150210084002_convert_person_zip_to_string.rb @@ -1,9 +1,9 @@ # encoding: utf-8 # Copyright (c) 2012-2014, Pfadibewegung Schweiz. This file is part of -# hitobito_pbs and licensed under the Affero General Public License version 3 +# hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito_pbs. +# https://github.com/hitobito/hitobito. class ConvertPersonZipToString < ActiveRecord::Migration def change diff --git a/db/migrate/20150212103138_add_anyone_may_post_to_mailing_list.rb b/db/migrate/20150212103138_add_anyone_may_post_to_mailing_list.rb index 89ed7ff5cf..0d048b2453 100644 --- a/db/migrate/20150212103138_add_anyone_may_post_to_mailing_list.rb +++ b/db/migrate/20150212103138_add_anyone_may_post_to_mailing_list.rb @@ -1,9 +1,9 @@ # encoding: utf-8 # Copyright (c) 2012-2014, Pfadibewegung Schweiz. This file is part of -# hitobito_pbs and licensed under the Affero General Public License version 3 +# hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito_pbs. +# https://github.com/hitobito/hitobito. class AddAnyoneMayPostToMailingList < ActiveRecord::Migration def change diff --git a/db/migrate/20150701123049_add_information_and_conditions_to_event_kinds.rb b/db/migrate/20150701123049_add_information_and_conditions_to_event_kinds.rb index 8890aa0c5a..516e2367e8 100644 --- a/db/migrate/20150701123049_add_information_and_conditions_to_event_kinds.rb +++ b/db/migrate/20150701123049_add_information_and_conditions_to_event_kinds.rb @@ -1,3 +1,10 @@ +# encoding: utf-8 + +# Copyright (c) 2015, hitobito AG. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + class AddInformationAndConditionsToEventKinds < ActiveRecord::Migration def change add_column(:event_kinds, :general_information, :text) diff --git a/db/migrate/20150706123121_add_signature_fields_to_events.rb b/db/migrate/20150706123121_add_signature_fields_to_events.rb index 1b0c31b91e..78c411a2f5 100644 --- a/db/migrate/20150706123121_add_signature_fields_to_events.rb +++ b/db/migrate/20150706123121_add_signature_fields_to_events.rb @@ -1,3 +1,10 @@ +# encoding: utf-8 + +# Copyright (c) 2015, hitobito AG. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + class AddSignatureFieldsToEvents < ActiveRecord::Migration def up diff --git a/db/migrate/20150707053351_add_locations.rb b/db/migrate/20150707053351_add_locations.rb index 39d0a21290..e9aa33bcbb 100644 --- a/db/migrate/20150707053351_add_locations.rb +++ b/db/migrate/20150707053351_add_locations.rb @@ -1,9 +1,9 @@ # encoding: utf-8 # Copyright (c) 2012-2015, Pfadibewegung Schweiz. This file is part of -# hitobito_pbs and licensed under the Affero General Public License version 3 +# hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito_pbs. +# https://github.com/hitobito/hitobito. class AddLocations < ActiveRecord::Migration def change diff --git a/db/migrate/20150722094419_add_event_application_waiting_list_comment.rb b/db/migrate/20150722094419_add_event_application_waiting_list_comment.rb index 7096408336..c37aa907c1 100644 --- a/db/migrate/20150722094419_add_event_application_waiting_list_comment.rb +++ b/db/migrate/20150722094419_add_event_application_waiting_list_comment.rb @@ -1,3 +1,10 @@ +# encoding: utf-8 + +# Copyright (c) 2015, hitobito AG. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + class AddEventApplicationWaitingListComment < ActiveRecord::Migration def change add_column :event_applications, :waiting_list_comment, :text diff --git a/db/migrate/20151007090030_fix_event_kind_globalized_fields.rb b/db/migrate/20151007090030_fix_event_kind_globalized_fields.rb index 8aa6c6db4b..448a462edd 100644 --- a/db/migrate/20151007090030_fix_event_kind_globalized_fields.rb +++ b/db/migrate/20151007090030_fix_event_kind_globalized_fields.rb @@ -1,3 +1,10 @@ +# encoding: utf-8 + +# Copyright (c) 2015, hitobito AG. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + class FixEventKindGlobalizedFields < ActiveRecord::Migration def up Event::Kind.add_translation_fields!({ general_information: :text }, migrate_data: true) diff --git a/db/migrate/20151007121335_add_event_userstamps.rb b/db/migrate/20151007121335_add_event_userstamps.rb index 92c76768bd..d1020ef32e 100644 --- a/db/migrate/20151007121335_add_event_userstamps.rb +++ b/db/migrate/20151007121335_add_event_userstamps.rb @@ -1,3 +1,10 @@ +# encoding: utf-8 + +# Copyright (c) 2015, hitobito AG. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + class AddEventUserstamps < ActiveRecord::Migration def change change_table :events do |t| diff --git a/db/migrate/20151103115637_update_peoples_primary_group.rb b/db/migrate/20151103115637_update_peoples_primary_group.rb index 1518e67b8d..d4cda21f2c 100644 --- a/db/migrate/20151103115637_update_peoples_primary_group.rb +++ b/db/migrate/20151103115637_update_peoples_primary_group.rb @@ -1,3 +1,10 @@ +# encoding: utf-8 + +# Copyright (c) 2015, hitobito AG. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + class UpdatePeoplesPrimaryGroup < ActiveRecord::Migration def up # people with no primary group and only one active role diff --git a/db/migrate/20151125132550_regenerate_event_participant_counts.rb b/db/migrate/20151125132550_regenerate_event_participant_counts.rb index 44203f9731..6c71b83294 100644 --- a/db/migrate/20151125132550_regenerate_event_participant_counts.rb +++ b/db/migrate/20151125132550_regenerate_event_participant_counts.rb @@ -1,3 +1,10 @@ +# encoding: utf-8 + +# Copyright (c) 2015, hitobito AG. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + class RegenerateEventParticipantCounts < ActiveRecord::Migration def up # Recalculate the counts of all events as teamers got omitted in certain cases diff --git a/db/migrate/20151229124153_create_person_add_request_ignored_approvers.rb b/db/migrate/20151229124153_create_person_add_request_ignored_approvers.rb index c455665161..3c05b3e3bd 100644 --- a/db/migrate/20151229124153_create_person_add_request_ignored_approvers.rb +++ b/db/migrate/20151229124153_create_person_add_request_ignored_approvers.rb @@ -1,3 +1,10 @@ +# encoding: utf-8 + +# Copyright (c) 2017, hitobito AG. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + class CreatePersonAddRequestIgnoredApprovers < ActiveRecord::Migration def change create_table :person_add_request_ignored_approvers do |t| diff --git a/db/migrate/20151230101630_create_event_attachments.rb b/db/migrate/20151230101630_create_event_attachments.rb index 002f6b5020..3772fdfa32 100644 --- a/db/migrate/20151230101630_create_event_attachments.rb +++ b/db/migrate/20151230101630_create_event_attachments.rb @@ -1,3 +1,10 @@ +# encoding: utf-8 + +# Copyright (c) 2015, hitobito AG. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + class CreateEventAttachments < ActiveRecord::Migration def change create_table :event_attachments do |t| diff --git a/db/migrate/20160314124800_create_person_note.rb b/db/migrate/20160314124800_create_person_note.rb index e9ef0aa18c..2a04afb032 100644 --- a/db/migrate/20160314124800_create_person_note.rb +++ b/db/migrate/20160314124800_create_person_note.rb @@ -1,9 +1,9 @@ # encoding: utf-8 # Copyright (c) 2012-2016, Dachverband Schweizer Jugendparlamente. This file is part of -# hitobito_dsj and licensed under the Affero General Public License version 3 +# hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito_dsj. +# https://github.com/hitobito/hitobito. class CreatePersonNote < ActiveRecord::Migration def change diff --git a/db/migrate/20160324134656_create_tag.rb b/db/migrate/20160324134656_create_tag.rb index 006385abb9..f35baaf2a0 100644 --- a/db/migrate/20160324134656_create_tag.rb +++ b/db/migrate/20160324134656_create_tag.rb @@ -1,9 +1,9 @@ # encoding: utf-8 # Copyright (c) 2012-2016, Dachverband Schweizer Jugendparlamente. This file is part of -# hitobito_dsj and licensed under the Affero General Public License version 3 +# hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito_dsj. +# https://github.com/hitobito/hitobito. class CreateTag < ActiveRecord::Migration def change diff --git a/db/migrate/20161019081518_rename_tags.rb b/db/migrate/20161019081518_rename_tags.rb index 84ccf5190d..1fa9cb457f 100644 --- a/db/migrate/20161019081518_rename_tags.rb +++ b/db/migrate/20161019081518_rename_tags.rb @@ -1,3 +1,10 @@ +# encoding: utf-8 + +# Copyright (c) 2016, hitobito AG. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + class RenameTags < ActiveRecord::Migration def change rename_table :tags, :old_tags diff --git a/db/migrate/20161019081524_acts_as_taggable_on_migration.acts_as_taggable_on_engine.rb b/db/migrate/20161019081524_acts_as_taggable_on_migration.acts_as_taggable_on_engine.rb index 6bbd5594ea..8116864785 100644 --- a/db/migrate/20161019081524_acts_as_taggable_on_migration.acts_as_taggable_on_engine.rb +++ b/db/migrate/20161019081524_acts_as_taggable_on_migration.acts_as_taggable_on_engine.rb @@ -1,3 +1,10 @@ +# encoding: utf-8 + +# Copyright (c) 2016, hitobito AG. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + # This migration comes from acts_as_taggable_on_engine (originally 1) class ActsAsTaggableOnMigration < ActiveRecord::Migration def self.up diff --git a/db/migrate/20161019081525_add_missing_unique_indices.acts_as_taggable_on_engine.rb b/db/migrate/20161019081525_add_missing_unique_indices.acts_as_taggable_on_engine.rb index 4bbb042c51..19084e9801 100644 --- a/db/migrate/20161019081525_add_missing_unique_indices.acts_as_taggable_on_engine.rb +++ b/db/migrate/20161019081525_add_missing_unique_indices.acts_as_taggable_on_engine.rb @@ -1,3 +1,10 @@ +# encoding: utf-8 + +# Copyright (c) 2016, hitobito AG. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + # This migration comes from acts_as_taggable_on_engine (originally 2) class AddMissingUniqueIndices < ActiveRecord::Migration def self.up diff --git a/db/migrate/20161019081526_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb b/db/migrate/20161019081526_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb index 8edb508078..5b938a9d9a 100644 --- a/db/migrate/20161019081526_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb +++ b/db/migrate/20161019081526_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb @@ -1,3 +1,10 @@ +# encoding: utf-8 + +# Copyright (c) 2016, hitobito AG. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + # This migration comes from acts_as_taggable_on_engine (originally 3) class AddTaggingsCounterCacheToTags < ActiveRecord::Migration def self.up diff --git a/db/migrate/20161019081527_add_missing_taggable_index.acts_as_taggable_on_engine.rb b/db/migrate/20161019081527_add_missing_taggable_index.acts_as_taggable_on_engine.rb index 71f2d7f433..44cd4b71a9 100644 --- a/db/migrate/20161019081527_add_missing_taggable_index.acts_as_taggable_on_engine.rb +++ b/db/migrate/20161019081527_add_missing_taggable_index.acts_as_taggable_on_engine.rb @@ -1,3 +1,10 @@ +# encoding: utf-8 + +# Copyright (c) 2016, hitobito AG. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + # This migration comes from acts_as_taggable_on_engine (originally 4) class AddMissingTaggableIndex < ActiveRecord::Migration def self.up diff --git a/db/migrate/20161019081528_change_collation_for_tag_names.acts_as_taggable_on_engine.rb b/db/migrate/20161019081528_change_collation_for_tag_names.acts_as_taggable_on_engine.rb index bfb06bc7cd..78013b3d77 100644 --- a/db/migrate/20161019081528_change_collation_for_tag_names.acts_as_taggable_on_engine.rb +++ b/db/migrate/20161019081528_change_collation_for_tag_names.acts_as_taggable_on_engine.rb @@ -1,3 +1,10 @@ +# encoding: utf-8 + +# Copyright (c) 2016, hitobito AG. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + # This migration comes from acts_as_taggable_on_engine (originally 5) # This migration is added to circumvent issue #623 and have special characters # work properly diff --git a/db/migrate/20161019081705_migrate_old_tags.rb b/db/migrate/20161019081705_migrate_old_tags.rb index 2184f8bd3d..3705b61182 100644 --- a/db/migrate/20161019081705_migrate_old_tags.rb +++ b/db/migrate/20161019081705_migrate_old_tags.rb @@ -1,3 +1,10 @@ +# encoding: utf-8 + +# Copyright (c) 2016, hitobito AG. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + class MigrateOldTags < ActiveRecord::Migration def change migrate_tags diff --git a/db/migrate/20161122144630_add_cancel_participation_enabled_to_event.rb b/db/migrate/20161122144630_add_cancel_participation_enabled_to_event.rb new file mode 100644 index 0000000000..4025def19d --- /dev/null +++ b/db/migrate/20161122144630_add_cancel_participation_enabled_to_event.rb @@ -0,0 +1,12 @@ +# encoding: utf-8 + +# Copyright (c) 2016, hitobito AG. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +class AddCancelParticipationEnabledToEvent < ActiveRecord::Migration + def change + add_column :events, :applications_cancelable, :boolean, default: false, null: false + end +end diff --git a/db/migrate/20161128120439_add_user_id_to_label_format.rb b/db/migrate/20161128120439_add_user_id_to_label_format.rb new file mode 100644 index 0000000000..c07a86d1ee --- /dev/null +++ b/db/migrate/20161128120439_add_user_id_to_label_format.rb @@ -0,0 +1,13 @@ +# encoding: utf-8 + +# Copyright (c) 2016, hitobito AG. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +class AddUserIdToLabelFormat < ActiveRecord::Migration + def change + add_column :label_formats, :person_id, :integer + add_column :people, :show_global_label_formats, :boolean, default: true, null: false + end +end diff --git a/db/migrate/20161129134942_add_nickname_to_label_format.rb b/db/migrate/20161129134942_add_nickname_to_label_format.rb new file mode 100644 index 0000000000..6cf641ccd0 --- /dev/null +++ b/db/migrate/20161129134942_add_nickname_to_label_format.rb @@ -0,0 +1,13 @@ +# encoding: utf-8 + +# Copyright (c) 2016, hitobito AG. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +class AddNicknameToLabelFormat < ActiveRecord::Migration + def change + add_column :label_formats, :nickname, :boolean, null: false, default: false + add_column :label_formats, :pp_post, :string, limit: 23 # PLZ + {18} + end +end diff --git a/db/migrate/20170103142035_change_locations_zip_code_to_strings.rb b/db/migrate/20170103142035_change_locations_zip_code_to_strings.rb index cef88b5090..d5edbf43bc 100644 --- a/db/migrate/20170103142035_change_locations_zip_code_to_strings.rb +++ b/db/migrate/20170103142035_change_locations_zip_code_to_strings.rb @@ -1,3 +1,10 @@ +# encoding: utf-8 + +# Copyright (c) 2017, hitobito AG. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + class ChangeLocationsZipCodeToStrings < ActiveRecord::Migration def change change_column :locations, :zip_code, :string, null: false diff --git a/db/migrate/20170314130759_add_event_kind_qualification_kind_grouping.rb b/db/migrate/20170314130759_add_event_kind_qualification_kind_grouping.rb new file mode 100644 index 0000000000..0d2c73dd9c --- /dev/null +++ b/db/migrate/20170314130759_add_event_kind_qualification_kind_grouping.rb @@ -0,0 +1,5 @@ +class AddEventKindQualificationKindGrouping < ActiveRecord::Migration + def change + add_column :event_kind_qualification_kinds, :grouping, :integer + end +end diff --git a/db/migrate/20170322144544_add_contact_attrs_to_event.rb b/db/migrate/20170322144544_add_contact_attrs_to_event.rb new file mode 100644 index 0000000000..1964181868 --- /dev/null +++ b/db/migrate/20170322144544_add_contact_attrs_to_event.rb @@ -0,0 +1,8 @@ +class AddContactAttrsToEvent < ActiveRecord::Migration + + def change + add_column :events, :required_contact_attrs, :string + add_column :events, :hidden_contact_attrs, :string + end + +end diff --git a/db/migrate/20170404103510_change_event_contact_attrs.rb b/db/migrate/20170404103510_change_event_contact_attrs.rb new file mode 100644 index 0000000000..e3f6617a80 --- /dev/null +++ b/db/migrate/20170404103510_change_event_contact_attrs.rb @@ -0,0 +1,8 @@ +class ChangeEventContactAttrs < ActiveRecord::Migration + + def change + change_column :events, :required_contact_attrs, :text + change_column :events, :hidden_contact_attrs, :text + end + +end diff --git a/db/migrate/20170404113313_add_event_questions_admin.rb b/db/migrate/20170404113313_add_event_questions_admin.rb new file mode 100644 index 0000000000..8569123a14 --- /dev/null +++ b/db/migrate/20170404113313_add_event_questions_admin.rb @@ -0,0 +1,5 @@ +class AddEventQuestionsAdmin < ActiveRecord::Migration + def change + add_column :event_questions, :admin, :boolean, null: false, default: false + end +end diff --git a/db/migrate/20170410094209_add_event_display_booking_info_flag.rb b/db/migrate/20170410094209_add_event_display_booking_info_flag.rb new file mode 100644 index 0000000000..8557c37f71 --- /dev/null +++ b/db/migrate/20170410094209_add_event_display_booking_info_flag.rb @@ -0,0 +1,5 @@ +class AddEventDisplayBookingInfoFlag < ActiveRecord::Migration + def change + add_column :events, :display_booking_info, :boolean, null: false, default: true + end +end diff --git a/db/migrate/20170529134942_create_notes.rb b/db/migrate/20170529134942_create_notes.rb new file mode 100644 index 0000000000..83850d725d --- /dev/null +++ b/db/migrate/20170529134942_create_notes.rb @@ -0,0 +1,16 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Dachverband Schweizer Jugendparlamente. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +class CreateNotes < ActiveRecord::Migration + def change + rename_table :person_notes, :notes + add_column :notes, :subject_type, :string + rename_column :notes, :person_id, :subject_id + + Note.update_all(subject_type: Person.name) + end +end diff --git a/db/migrate/20170607111148_add_people_filter_chain.rb b/db/migrate/20170607111148_add_people_filter_chain.rb new file mode 100644 index 0000000000..43e9403559 --- /dev/null +++ b/db/migrate/20170607111148_add_people_filter_chain.rb @@ -0,0 +1,23 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +class AddPeopleFilterChain < ActiveRecord::Migration + def change + add_column :people_filters, :filter_chain, :text + add_column :people_filters, :range, :string, default: 'deep' + + add_column :people_filters, :created_at, :timestamp + add_column :people_filters, :updated_at, :timestamp + + PeopleFilter.reset_column_information + + PeopleFilter.find_each do |filter| + types = RelatedRoleType.where(relation: filter).pluck(:role_type) + filter.update!(range: 'deep', filter_chain: { role: { role_types: types } }) + end + end +end diff --git a/db/migrate/20170911123243_add_invoice_models.rb b/db/migrate/20170911123243_add_invoice_models.rb new file mode 100644 index 0000000000..28e3021ea4 --- /dev/null +++ b/db/migrate/20170911123243_add_invoice_models.rb @@ -0,0 +1,70 @@ +class AddInvoiceModels < ActiveRecord::Migration + + def change + create_table :invoice_configs do |t| + t.integer :sequence_number, null: false, default: 1 + t.integer :due_days, null: false, default: 30 + t.belongs_to :group, index: true, null: false + t.integer :contact_id, index: true + t.integer :page_size, default: 15 + t.text :address + t.text :payment_information + end + + create_table :invoices do |t| + t.string :title, null: false + + t.string :sequence_number, null: false, index: :unique + t.string :state, null: false, default: :draft + t.string :esr_number, null: false, index: :unique + + t.text :description + + t.string :recipient_email + t.text :recipient_address + + t.date :sent_at + t.date :due_at + + t.belongs_to :group, index: true, null: false + t.belongs_to :recipient, index: true, null: false + + t.decimal :total, precision: 12, scale: 2 + + t.timestamps null: false + end + + create_table :invoice_items do |t| + t.belongs_to :invoice, index: true, null: false + + t.string :name, null: false + t.text :description + + t.decimal :vat_rate, precision: 5, scale: 2 + t.decimal :unit_cost, precision: 12, scale: 2, null: false + t.integer :count, default: 1, null: false + end + + create_table :payments do |t| + t.belongs_to :invoice, index: true, null: false + t.decimal :amount, precision: 12, scale: 2, null: false + t.date :received_at, null: :false + end + + create_table :payment_reminders do |t| + t.belongs_to :invoice, index: true, null: false + t.text :message + t.date :due_at, null: false + t.timestamps null: false + end + + reversible do |dir| + dir.up do + ids = select_values("SELECT id FROM groups WHERE id = layer_group_id") + values = ids.collect { |id| "(#{id})" }.join(', ') + execute("INSERT INTO invoice_configs(group_id) VALUES #{values}") if ids.present? + end + end + end + +end diff --git a/db/migrate/20171120181851_create_invoice_articles.rb b/db/migrate/20171120181851_create_invoice_articles.rb new file mode 100644 index 0000000000..6ae54d524e --- /dev/null +++ b/db/migrate/20171120181851_create_invoice_articles.rb @@ -0,0 +1,25 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +class CreateInvoiceArticles < ActiveRecord::Migration + def change + create_table :invoice_articles do |t| + t.string :number + t.string :name, null: false + t.string :description + t.string :category + t.decimal :net_price, precision: 12, scale: 2 + t.decimal :vat_rate, precision: 5, scale: 2 + t.string :cost_center + t.string :account + + t.timestamps null: false + + t.index :number, unique: true + end + end +end diff --git a/db/migrate/20171123102231_change_invoice_articles.rb b/db/migrate/20171123102231_change_invoice_articles.rb new file mode 100644 index 0000000000..31109592bb --- /dev/null +++ b/db/migrate/20171123102231_change_invoice_articles.rb @@ -0,0 +1,19 @@ +class ChangeInvoiceArticles < ActiveRecord::Migration + def change + # Workaround because SQLite cannot add new column with null false + add_column(:invoice_articles, :group_id, :integer, index: true) + change_column(:invoice_articles, :group_id, :integer, null: false, index: true) + + rename_column(:invoice_articles, :net_price, :unit_cost) + + reversible do |dir| + dir.up do + change_column(:invoice_articles, :description, :text) + end + + dir.down do + change_column(:invoice_articles, :description, :string) + end + end + end +end diff --git a/db/migrate/20171129105145_add_attributes_to_invoice.rb b/db/migrate/20171129105145_add_attributes_to_invoice.rb new file mode 100644 index 0000000000..bcf7e95162 --- /dev/null +++ b/db/migrate/20171129105145_add_attributes_to_invoice.rb @@ -0,0 +1,8 @@ +class AddAttributesToInvoice < ActiveRecord::Migration + def change + add_column :invoice_configs, :account_number, :string + + add_column :invoices, :account_number, :string + add_column :invoices, :address, :text + end +end diff --git a/db/migrate/20171201094234_allow_null_recipient_in_invoice.rb b/db/migrate/20171201094234_allow_null_recipient_in_invoice.rb new file mode 100644 index 0000000000..a7bd4cb6dd --- /dev/null +++ b/db/migrate/20171201094234_allow_null_recipient_in_invoice.rb @@ -0,0 +1,12 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +class AllowNullRecipientInInvoice < ActiveRecord::Migration + def change + change_column_null :invoices, :recipient_id, true + end +end diff --git a/db/migrate/20171205085115_change_payments_received_at_to_not_null.rb b/db/migrate/20171205085115_change_payments_received_at_to_not_null.rb new file mode 100644 index 0000000000..fb635d4899 --- /dev/null +++ b/db/migrate/20171205085115_change_payments_received_at_to_not_null.rb @@ -0,0 +1,13 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +class ChangePaymentsReceivedAtToNotNull < ActiveRecord::Migration + def change + Payment.delete_all + change_column_null :payments, :received_at, false + end +end diff --git a/db/migrate/20171205122949_add_issued_at_to_invoices.rb b/db/migrate/20171205122949_add_issued_at_to_invoices.rb new file mode 100644 index 0000000000..d3fa13c57e --- /dev/null +++ b/db/migrate/20171205122949_add_issued_at_to_invoices.rb @@ -0,0 +1,5 @@ +class AddIssuedAtToInvoices < ActiveRecord::Migration + def change + add_column :invoices, :issued_at, :date + end +end diff --git a/db/schema.rb b/db/schema.rb index 8704507716..35b8cd0694 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170103142035) do +ActiveRecord::Schema.define(version: 20171205122949) do create_table "additional_emails", force: :cascade do |t| t.integer "contactable_id", limit: 4, null: false @@ -22,7 +22,7 @@ t.boolean "mailings", default: true, null: false end - add_index "additional_emails", ["contactable_id", "contactable_type"], name: "index_additional_emails_on_contactable_id_and_contactable_type", using: :btree + add_index "additional_emails", ["contactable_id", "contactable_type"], name: "index_additional_emails_on_contactable_id_and_contactable_type" create_table "custom_content_translations", force: :cascade do |t| t.integer "custom_content_id", limit: 4, null: false @@ -34,8 +34,8 @@ t.text "body", limit: 65535 end - add_index "custom_content_translations", ["custom_content_id"], name: "index_custom_content_translations_on_custom_content_id", using: :btree - add_index "custom_content_translations", ["locale"], name: "index_custom_content_translations_on_locale", using: :btree + add_index "custom_content_translations", ["custom_content_id"], name: "index_custom_content_translations_on_custom_content_id" + add_index "custom_content_translations", ["locale"], name: "index_custom_content_translations_on_locale" create_table "custom_contents", force: :cascade do |t| t.string "key", limit: 255, null: false @@ -57,7 +57,7 @@ t.datetime "updated_at" end - add_index "delayed_jobs", ["priority", "run_at"], name: "delayed_jobs_priority", using: :btree + add_index "delayed_jobs", ["priority", "run_at"], name: "delayed_jobs_priority" create_table "event_answers", force: :cascade do |t| t.integer "participation_id", limit: 4, null: false @@ -65,7 +65,7 @@ t.string "answer", limit: 255 end - add_index "event_answers", ["participation_id", "question_id"], name: "index_event_answers_on_participation_id_and_question_id", unique: true, using: :btree + add_index "event_answers", ["participation_id", "question_id"], name: "index_event_answers_on_participation_id_and_question_id", unique: true create_table "event_applications", force: :cascade do |t| t.integer "priority_1_id", limit: 4, null: false @@ -82,7 +82,7 @@ t.string "file", limit: 255, null: false end - add_index "event_attachments", ["event_id"], name: "index_event_attachments_on_event_id", using: :btree + add_index "event_attachments", ["event_id"], name: "index_event_attachments_on_event_id" create_table "event_dates", force: :cascade do |t| t.integer "event_id", limit: 4, null: false @@ -92,18 +92,19 @@ t.string "location", limit: 255 end - add_index "event_dates", ["event_id", "start_at"], name: "index_event_dates_on_event_id_and_start_at", using: :btree - add_index "event_dates", ["event_id"], name: "index_event_dates_on_event_id", using: :btree + add_index "event_dates", ["event_id", "start_at"], name: "index_event_dates_on_event_id_and_start_at" + add_index "event_dates", ["event_id"], name: "index_event_dates_on_event_id" create_table "event_kind_qualification_kinds", force: :cascade do |t| t.integer "event_kind_id", limit: 4, null: false t.integer "qualification_kind_id", limit: 4, null: false t.string "category", limit: 255, null: false t.string "role", limit: 255, null: false + t.integer "grouping" end - add_index "event_kind_qualification_kinds", ["category"], name: "index_event_kind_qualification_kinds_on_category", using: :btree - add_index "event_kind_qualification_kinds", ["role"], name: "index_event_kind_qualification_kinds_on_role", using: :btree + add_index "event_kind_qualification_kinds", ["category"], name: "index_event_kind_qualification_kinds_on_category" + add_index "event_kind_qualification_kinds", ["role"], name: "index_event_kind_qualification_kinds_on_role" create_table "event_kind_translations", force: :cascade do |t| t.integer "event_kind_id", limit: 4, null: false @@ -116,8 +117,8 @@ t.text "application_conditions", limit: 65535 end - add_index "event_kind_translations", ["event_kind_id"], name: "index_event_kind_translations_on_event_kind_id", using: :btree - add_index "event_kind_translations", ["locale"], name: "index_event_kind_translations_on_locale", using: :btree + add_index "event_kind_translations", ["event_kind_id"], name: "index_event_kind_translations_on_event_kind_id" + add_index "event_kind_translations", ["locale"], name: "index_event_kind_translations_on_locale" create_table "event_kinds", force: :cascade do |t| t.datetime "created_at" @@ -137,9 +138,9 @@ t.boolean "qualified" end - add_index "event_participations", ["event_id", "person_id"], name: "index_event_participations_on_event_id_and_person_id", unique: true, using: :btree - add_index "event_participations", ["event_id"], name: "index_event_participations_on_event_id", using: :btree - add_index "event_participations", ["person_id"], name: "index_event_participations_on_person_id", using: :btree + add_index "event_participations", ["event_id", "person_id"], name: "index_event_participations_on_event_id_and_person_id", unique: true + add_index "event_participations", ["event_id"], name: "index_event_participations_on_event_id" + add_index "event_participations", ["person_id"], name: "index_event_participations_on_person_id" create_table "event_questions", force: :cascade do |t| t.integer "event_id", limit: 4 @@ -147,9 +148,10 @@ t.string "choices", limit: 255 t.boolean "multiple_choices", default: false t.boolean "required" + t.boolean "admin", default: false, null: false end - add_index "event_questions", ["event_id"], name: "index_event_questions_on_event_id", using: :btree + add_index "event_questions", ["event_id"], name: "index_event_questions_on_event_id" create_table "event_roles", force: :cascade do |t| t.string "type", limit: 255, null: false @@ -157,8 +159,8 @@ t.string "label", limit: 255 end - add_index "event_roles", ["participation_id"], name: "index_event_roles_on_participation_id", using: :btree - add_index "event_roles", ["type"], name: "index_event_roles_on_type", using: :btree + add_index "event_roles", ["participation_id"], name: "index_event_roles_on_participation_id" + add_index "event_roles", ["type"], name: "index_event_roles_on_type" create_table "events", force: :cascade do |t| t.string "type", limit: 255 @@ -189,16 +191,20 @@ t.string "signature_confirmation_text", limit: 255 t.integer "creator_id", limit: 4 t.integer "updater_id", limit: 4 + t.boolean "applications_cancelable", default: false, null: false + t.text "required_contact_attrs" + t.text "hidden_contact_attrs" + t.boolean "display_booking_info", default: true, null: false end - add_index "events", ["kind_id"], name: "index_events_on_kind_id", using: :btree + add_index "events", ["kind_id"], name: "index_events_on_kind_id" create_table "events_groups", id: false, force: :cascade do |t| t.integer "event_id", limit: 4 t.integer "group_id", limit: 4 end - add_index "events_groups", ["event_id", "group_id"], name: "index_events_groups_on_event_id_and_group_id", unique: true, using: :btree + add_index "events_groups", ["event_id", "group_id"], name: "index_events_groups_on_event_id_and_group_id", unique: true create_table "groups", force: :cascade do |t| t.integer "parent_id", limit: 4 @@ -223,9 +229,75 @@ t.boolean "require_person_add_requests", default: false, null: false end - add_index "groups", ["layer_group_id"], name: "index_groups_on_layer_group_id", using: :btree - add_index "groups", ["lft", "rgt"], name: "index_groups_on_lft_and_rgt", using: :btree - add_index "groups", ["parent_id"], name: "index_groups_on_parent_id", using: :btree + add_index "groups", ["layer_group_id"], name: "index_groups_on_layer_group_id" + add_index "groups", ["lft", "rgt"], name: "index_groups_on_lft_and_rgt" + add_index "groups", ["parent_id"], name: "index_groups_on_parent_id" + + create_table "invoice_articles", force: :cascade do |t| + t.string "number", limit: 255 + t.string "name", limit: 255, null: false + t.text "description", limit: 65535 + t.string "category", limit: 255 + t.decimal "unit_cost", precision: 12, scale: 2 + t.decimal "vat_rate", precision: 5, scale: 2 + t.string "cost_center", limit: 255 + t.string "account", limit: 255 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "group_id", null: false + end + + add_index "invoice_articles", ["number"], name: "index_invoice_articles_on_number", unique: true + + create_table "invoice_configs", force: :cascade do |t| + t.integer "sequence_number", default: 1, null: false + t.integer "due_days", default: 30, null: false + t.integer "group_id", null: false + t.integer "contact_id" + t.integer "page_size", default: 15 + t.text "address" + t.text "payment_information" + t.string "account_number" + end + + add_index "invoice_configs", ["contact_id"], name: "index_invoice_configs_on_contact_id" + add_index "invoice_configs", ["group_id"], name: "index_invoice_configs_on_group_id" + + create_table "invoice_items", force: :cascade do |t| + t.integer "invoice_id", null: false + t.string "name", null: false + t.text "description" + t.decimal "vat_rate", precision: 5, scale: 2 + t.decimal "unit_cost", precision: 12, scale: 2, null: false + t.integer "count", default: 1, null: false + end + + add_index "invoice_items", ["invoice_id"], name: "index_invoice_items_on_invoice_id" + + create_table "invoices", force: :cascade do |t| + t.string "title", null: false + t.string "sequence_number", null: false + t.string "state", default: "draft", null: false + t.string "esr_number", null: false + t.text "description" + t.string "recipient_email" + t.text "recipient_address" + t.date "sent_at" + t.date "due_at" + t.integer "group_id", null: false + t.integer "recipient_id" + t.decimal "total", precision: 12, scale: 2 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "account_number" + t.text "address" + t.date "issued_at" + end + + add_index "invoices", ["esr_number"], name: "index_invoices_on_esr_number" + add_index "invoices", ["group_id"], name: "index_invoices_on_group_id" + add_index "invoices", ["recipient_id"], name: "index_invoices_on_recipient_id" + add_index "invoices", ["sequence_number"], name: "index_invoices_on_sequence_number" create_table "label_format_translations", force: :cascade do |t| t.integer "label_format_id", limit: 4, null: false @@ -235,8 +307,8 @@ t.string "name", limit: 255, null: false end - add_index "label_format_translations", ["label_format_id"], name: "index_label_format_translations_on_label_format_id", using: :btree - add_index "label_format_translations", ["locale"], name: "index_label_format_translations_on_locale", using: :btree + add_index "label_format_translations", ["label_format_id"], name: "index_label_format_translations_on_label_format_id" + add_index "label_format_translations", ["locale"], name: "index_label_format_translations_on_locale" create_table "label_formats", force: :cascade do |t| t.string "page_size", limit: 255, default: "A4", null: false @@ -248,6 +320,9 @@ t.integer "count_vertical", limit: 4, null: false t.float "padding_top", limit: 24, null: false t.float "padding_left", limit: 24, null: false + t.integer "person_id", limit: 4 + t.boolean "nickname", default: false, null: false + t.string "pp_post", limit: 23 end create_table "locations", force: :cascade do |t| @@ -256,7 +331,7 @@ t.string "zip_code", limit: 255, null: false end - add_index "locations", ["zip_code", "canton", "name"], name: "index_locations_on_zip_code_and_canton_and_name", unique: true, using: :btree + add_index "locations", ["zip_code", "canton", "name"], name: "index_locations_on_zip_code_and_canton_and_name", unique: true create_table "mailing_lists", force: :cascade do |t| t.string "name", limit: 255, null: false @@ -270,55 +345,89 @@ t.boolean "anyone_may_post", default: false, null: false end - add_index "mailing_lists", ["group_id"], name: "index_mailing_lists_on_group_id", using: :btree + add_index "mailing_lists", ["group_id"], name: "index_mailing_lists_on_group_id" + + create_table "notes", force: :cascade do |t| + t.integer "subject_id", limit: 4, null: false + t.integer "author_id", limit: 4, null: false + t.text "text", limit: 65535 + t.datetime "created_at" + t.datetime "updated_at" + t.string "subject_type", limit: 255 + end + + add_index "notes", ["subject_id"], name: "index_notes_on_subject_id" + + create_table "payment_reminders", force: :cascade do |t| + t.integer "invoice_id", null: false + t.text "message" + t.date "due_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "payment_reminders", ["invoice_id"], name: "index_payment_reminders_on_invoice_id" + + create_table "payments", force: :cascade do |t| + t.integer "invoice_id", null: false + t.decimal "amount", precision: 12, scale: 2, null: false + t.date "received_at", null: false + end + + add_index "payments", ["invoice_id"], name: "index_payments_on_invoice_id" create_table "people", force: :cascade do |t| - t.string "first_name", limit: 255 - t.string "last_name", limit: 255 - t.string "company_name", limit: 255 - t.string "nickname", limit: 255 - t.boolean "company", default: false, null: false - t.string "email", limit: 255 - t.string "address", limit: 1024 - t.string "zip_code", limit: 255 - t.string "town", limit: 255 - t.string "country", limit: 255 - t.string "gender", limit: 1 + t.string "first_name", limit: 255 + t.string "last_name", limit: 255 + t.string "company_name", limit: 255 + t.string "nickname", limit: 255 + t.boolean "company", default: false, null: false + t.string "email", limit: 255 + t.string "address", limit: 1024 + t.string "zip_code", limit: 255 + t.string "town", limit: 255 + t.string "country", limit: 255 + t.string "gender", limit: 1 t.date "birthday" - t.text "additional_information", limit: 65535 - t.boolean "contact_data_visible", default: false, null: false + t.text "additional_information", limit: 65535 + t.boolean "contact_data_visible", default: false, null: false t.datetime "created_at" t.datetime "updated_at" - t.string "encrypted_password", limit: 255 - t.string "reset_password_token", limit: 255 + t.string "encrypted_password", limit: 255 + t.string "reset_password_token", limit: 255 t.datetime "reset_password_sent_at" t.datetime "remember_created_at" - t.integer "sign_in_count", limit: 4, default: 0 + t.integer "sign_in_count", limit: 4, default: 0 t.datetime "current_sign_in_at" t.datetime "last_sign_in_at" - t.string "current_sign_in_ip", limit: 255 - t.string "last_sign_in_ip", limit: 255 - t.string "picture", limit: 255 - t.integer "last_label_format_id", limit: 4 - t.integer "creator_id", limit: 4 - t.integer "updater_id", limit: 4 - t.integer "primary_group_id", limit: 4 - t.integer "failed_attempts", limit: 4, default: 0 + t.string "current_sign_in_ip", limit: 255 + t.string "last_sign_in_ip", limit: 255 + t.string "picture", limit: 255 + t.integer "last_label_format_id", limit: 4 + t.integer "creator_id", limit: 4 + t.integer "updater_id", limit: 4 + t.integer "primary_group_id", limit: 4 + t.integer "failed_attempts", limit: 4, default: 0 t.datetime "locked_at" - t.string "authentication_token", limit: 255 + t.string "authentication_token", limit: 255 + t.boolean "show_global_label_formats", default: true, null: false end - add_index "people", ["authentication_token"], name: "index_people_on_authentication_token", using: :btree - add_index "people", ["email"], name: "index_people_on_email", unique: true, using: :btree - add_index "people", ["reset_password_token"], name: "index_people_on_reset_password_token", unique: true, using: :btree + add_index "people", ["authentication_token"], name: "index_people_on_authentication_token" + add_index "people", ["email"], name: "index_people_on_email", unique: true + add_index "people", ["reset_password_token"], name: "index_people_on_reset_password_token", unique: true create_table "people_filters", force: :cascade do |t| - t.string "name", limit: 255, null: false - t.integer "group_id", limit: 4 - t.string "group_type", limit: 255 + t.string "name", limit: 255, null: false + t.integer "group_id", limit: 4 + t.string "group_type", limit: 255 + t.text "filter_chain", limit: 65535 + t.string "range", limit: 255, default: "deep" + t.datetime "created_at" + t.datetime "updated_at" end - add_index "people_filters", ["group_id", "group_type"], name: "index_people_filters_on_group_id_and_group_type", using: :btree + add_index "people_filters", ["group_id", "group_type"], name: "index_people_filters_on_group_id_and_group_type" create_table "people_relations", force: :cascade do |t| t.integer "head_id", limit: 4, null: false @@ -326,15 +435,15 @@ t.string "kind", limit: 255, null: false end - add_index "people_relations", ["head_id"], name: "index_people_relations_on_head_id", using: :btree - add_index "people_relations", ["tail_id"], name: "index_people_relations_on_tail_id", using: :btree + add_index "people_relations", ["head_id"], name: "index_people_relations_on_head_id" + add_index "people_relations", ["tail_id"], name: "index_people_relations_on_tail_id" create_table "person_add_request_ignored_approvers", force: :cascade do |t| t.integer "group_id", limit: 4, null: false t.integer "person_id", limit: 4, null: false end - add_index "person_add_request_ignored_approvers", ["group_id", "person_id"], name: "person_add_request_ignored_approvers_index", unique: true, using: :btree + add_index "person_add_request_ignored_approvers", ["group_id", "person_id"], name: "person_add_request_ignored_approvers_index", unique: true create_table "person_add_requests", force: :cascade do |t| t.integer "person_id", limit: 4, null: false @@ -345,18 +454,8 @@ t.datetime "created_at", null: false end - add_index "person_add_requests", ["person_id"], name: "index_person_add_requests_on_person_id", using: :btree - add_index "person_add_requests", ["type", "body_id"], name: "index_person_add_requests_on_type_and_body_id", using: :btree - - create_table "person_notes", force: :cascade do |t| - t.integer "person_id", limit: 4, null: false - t.integer "author_id", limit: 4, null: false - t.text "text", limit: 65535 - t.datetime "created_at" - t.datetime "updated_at" - end - - add_index "person_notes", ["person_id"], name: "index_person_notes_on_person_id", using: :btree + add_index "person_add_requests", ["person_id"], name: "index_person_add_requests_on_person_id" + add_index "person_add_requests", ["type", "body_id"], name: "index_person_add_requests_on_type_and_body_id" create_table "phone_numbers", force: :cascade do |t| t.integer "contactable_id", limit: 4, null: false @@ -366,7 +465,7 @@ t.boolean "public", default: true, null: false end - add_index "phone_numbers", ["contactable_id", "contactable_type"], name: "index_phone_numbers_on_contactable_id_and_contactable_type", using: :btree + add_index "phone_numbers", ["contactable_id", "contactable_type"], name: "index_phone_numbers_on_contactable_id_and_contactable_type" create_table "qualification_kind_translations", force: :cascade do |t| t.integer "qualification_kind_id", limit: 4, null: false @@ -377,8 +476,8 @@ t.string "description", limit: 1023 end - add_index "qualification_kind_translations", ["locale"], name: "index_qualification_kind_translations_on_locale", using: :btree - add_index "qualification_kind_translations", ["qualification_kind_id"], name: "index_qualification_kind_translations_on_qualification_kind_id", using: :btree + add_index "qualification_kind_translations", ["locale"], name: "index_qualification_kind_translations_on_locale" + add_index "qualification_kind_translations", ["qualification_kind_id"], name: "index_qualification_kind_translations_on_qualification_kind_id" create_table "qualification_kinds", force: :cascade do |t| t.integer "validity", limit: 4 @@ -396,8 +495,8 @@ t.string "origin", limit: 255 end - add_index "qualifications", ["person_id"], name: "index_qualifications_on_person_id", using: :btree - add_index "qualifications", ["qualification_kind_id"], name: "index_qualifications_on_qualification_kind_id", using: :btree + add_index "qualifications", ["person_id"], name: "index_qualifications_on_person_id" + add_index "qualifications", ["qualification_kind_id"], name: "index_qualifications_on_qualification_kind_id" create_table "related_role_types", force: :cascade do |t| t.integer "relation_id", limit: 4 @@ -405,8 +504,8 @@ t.string "relation_type", limit: 255 end - add_index "related_role_types", ["relation_id", "relation_type"], name: "index_related_role_types_on_relation_id_and_relation_type", using: :btree - add_index "related_role_types", ["role_type"], name: "index_related_role_types_on_role_type", using: :btree + add_index "related_role_types", ["relation_id", "relation_type"], name: "index_related_role_types_on_relation_id_and_relation_type" + add_index "related_role_types", ["role_type"], name: "index_related_role_types_on_role_type" create_table "roles", force: :cascade do |t| t.integer "person_id", limit: 4, null: false @@ -418,8 +517,8 @@ t.datetime "deleted_at" end - add_index "roles", ["person_id", "group_id"], name: "index_roles_on_person_id_and_group_id", using: :btree - add_index "roles", ["type"], name: "index_roles_on_type", using: :btree + add_index "roles", ["person_id", "group_id"], name: "index_roles_on_person_id_and_group_id" + add_index "roles", ["type"], name: "index_roles_on_type" create_table "sessions", force: :cascade do |t| t.string "session_id", limit: 255, null: false @@ -428,8 +527,8 @@ t.datetime "updated_at" end - add_index "sessions", ["session_id"], name: "index_sessions_on_session_id", using: :btree - add_index "sessions", ["updated_at"], name: "index_sessions_on_updated_at", using: :btree + add_index "sessions", ["session_id"], name: "index_sessions_on_session_id" + add_index "sessions", ["updated_at"], name: "index_sessions_on_updated_at" create_table "social_accounts", force: :cascade do |t| t.integer "contactable_id", limit: 4, null: false @@ -439,7 +538,7 @@ t.boolean "public", default: true, null: false end - add_index "social_accounts", ["contactable_id", "contactable_type"], name: "index_social_accounts_on_contactable_id_and_contactable_type", using: :btree + add_index "social_accounts", ["contactable_id", "contactable_type"], name: "index_social_accounts_on_contactable_id_and_contactable_type" create_table "subscriptions", force: :cascade do |t| t.integer "mailing_list_id", limit: 4, null: false @@ -448,8 +547,8 @@ t.boolean "excluded", default: false, null: false end - add_index "subscriptions", ["mailing_list_id"], name: "index_subscriptions_on_mailing_list_id", using: :btree - add_index "subscriptions", ["subscriber_id", "subscriber_type"], name: "index_subscriptions_on_subscriber_id_and_subscriber_type", using: :btree + add_index "subscriptions", ["mailing_list_id"], name: "index_subscriptions_on_mailing_list_id" + add_index "subscriptions", ["subscriber_id", "subscriber_type"], name: "index_subscriptions_on_subscriber_id_and_subscriber_type" create_table "taggings", force: :cascade do |t| t.integer "tag_id", limit: 4 @@ -461,15 +560,15 @@ t.datetime "created_at" end - add_index "taggings", ["tag_id", "taggable_id", "taggable_type", "context", "tagger_id", "tagger_type"], name: "taggings_idx", unique: true, using: :btree - add_index "taggings", ["taggable_id", "taggable_type", "context"], name: "index_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree + add_index "taggings", ["tag_id", "taggable_id", "taggable_type", "context", "tagger_id", "tagger_type"], name: "taggings_idx", unique: true + add_index "taggings", ["taggable_id", "taggable_type", "context"], name: "index_taggings_on_taggable_id_and_taggable_type_and_context" create_table "tags", force: :cascade do |t| t.string "name", limit: 255 t.integer "taggings_count", limit: 4, default: 0 end - add_index "tags", ["name"], name: "index_tags_on_name", unique: true, using: :btree + add_index "tags", ["name"], name: "index_tags_on_name", unique: true create_table "versions", force: :cascade do |t| t.string "item_type", limit: 255, null: false @@ -483,7 +582,7 @@ t.datetime "created_at" end - add_index "versions", ["item_type", "item_id"], name: "index_versions_on_item_type_and_item_id", using: :btree - add_index "versions", ["main_id", "main_type"], name: "index_versions_on_main_id_and_main_type", using: :btree + add_index "versions", ["item_type", "item_id"], name: "index_versions_on_item_type_and_item_id" + add_index "versions", ["main_id", "main_type"], name: "index_versions_on_main_id_and_main_type" end diff --git a/db/seeds/custom_contents.rb b/db/seeds/custom_contents.rb index fb79492a54..11e5fc2102 100644 --- a/db/seeds/custom_contents.rb +++ b/db/seeds/custom_contents.rb @@ -1,6 +1,6 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. @@ -18,6 +18,10 @@ placeholders_required: 'participant-name, event-details, application-url', placeholders_optional: 'recipient-names'}, + {key: Event::ParticipationMailer::CONTENT_CANCEL, + placeholders_required: 'event-details', + placeholders_optional: 'recipient-name'}, + {key: Event::RegisterMailer::CONTENT_REGISTER_LOGIN, placeholders_required: 'event-url', placeholders_optional: 'recipient-name, event-name'}, @@ -41,17 +45,43 @@ { key: Person::AddRequestMailer::CONTENT_ADD_REQUEST_REJECTED, placeholders_required: 'person-name, request-body', placeholders_optional: 'recipient-name, rejecter-name, rejecter-roles' }, + + { key: Export::SubscriptionsMailer::CONTENT_SUBSCRIPTIONS_EXPORT, + placeholders_required: nil, + placeholders_optional: 'recipient-name, mailing-list-name' }, + + { key: Export::PeopleExportMailer::CONTENT_PEOPLE_EXPORT, + placeholders_required: nil, + placeholders_optional: 'recipient-name' }, + + { key: Export::EventsExportMailer::CONTENT_EVENTS_EXPORT, + placeholders_required: nil, + placeholders_optional: 'recipient-name' }, + + { key: Export::EventParticipationsExportMailer::CONTENT_EVENT_PARTICIPATIONS_EXPORT, + placeholders_required: nil, + placeholders_optional: 'recipient-name' }, + + { key: InvoiceMailer::CONTENT_INVOICE_NOTIFICATION, + placeholders_required: 'invoice-items, invoice-total, payment-information', + placeholders_optional: 'recipient-name, group-name, invoice-number' }, ) send_login_id = CustomContent.get(Person::LoginMailer::CONTENT_LOGIN).id participation_confirmation_id = CustomContent.get(Event::ParticipationMailer::CONTENT_CONFIRMATION).id participation_approval_id = CustomContent.get(Event::ParticipationMailer::CONTENT_APPROVAL).id +cancel_application_id = CustomContent.get(Event::ParticipationMailer::CONTENT_CANCEL).id temp_login_id = CustomContent.get(Event::RegisterMailer::CONTENT_REGISTER_LOGIN).id login_form_id = CustomContent.get('views/devise/sessions/info').id add_request_person_id = CustomContent.get(Person::AddRequestMailer::CONTENT_ADD_REQUEST_PERSON).id add_request_responsibles_id = CustomContent.get(Person::AddRequestMailer::CONTENT_ADD_REQUEST_RESPONSIBLES).id add_request_approved_id = CustomContent.get(Person::AddRequestMailer::CONTENT_ADD_REQUEST_APPROVED).id add_request_rejected_id = CustomContent.get(Person::AddRequestMailer::CONTENT_ADD_REQUEST_REJECTED).id +subscriptions_export_id = CustomContent.get(Export::SubscriptionsMailer::CONTENT_SUBSCRIPTIONS_EXPORT).id +people_export_id = CustomContent.get(Export::PeopleExportMailer::CONTENT_PEOPLE_EXPORT).id +events_export_id = CustomContent.get(Export::EventsExportMailer::CONTENT_EVENTS_EXPORT).id +event_participations_export_id = CustomContent.get(Export::EventParticipationsExportMailer::CONTENT_EVENT_PARTICIPATIONS_EXPORT).id +invoice_notification_id = CustomContent.get(InvoiceMailer::CONTENT_INVOICE_NOTIFICATION).id CustomContent::Translation.seed_once(:custom_content_id, :locale, @@ -102,6 +132,26 @@ locale: 'it', label: "Evento: E-mail per l'affermazione della inscrizione"}, + {custom_content_id: cancel_application_id, + locale: 'de', + label: 'Anlass: E-Mail Abmeldebestätigung', + subject: 'Bestätigung der Abmeldung', + body: "Hallo {recipient-name}

" \ + "Du hast dich von folgendem Anlass abgemeldet:

" \ + "{event-details}

"}, + + {custom_content_id: cancel_application_id, + locale: 'fr', + label: "Événement: E-Mail de confirmation de la désinscription"}, + + {custom_content_id: cancel_application_id, + locale: 'en', + label: 'Event: Deregistration confirmation email'}, + + {custom_content_id: cancel_application_id, + locale: 'it', + label: "Evento: E-mail per l'affermazione della disinscrizione"}, + {custom_content_id: participation_approval_id, locale: 'de', label: 'Anlass: E-Mail Freigabe der Anmeldung', @@ -253,4 +303,104 @@ locale: 'it', label: "Richiesta dei dati personali: Email abilitazione rifiutata"}, + {custom_content_id: subscriptions_export_id, + locale: 'de', + label: 'Export der Abonnenten', + subject: 'Export der Abonnenten', + body: "Hallo {recipient-name}

" \ + "Der Export der Abonnenten von {mailing-list-name} ist fertig und an dieser Mail angehängt.

" }, + + {custom_content_id: subscriptions_export_id, + locale: 'en', + label: 'Export of Mailinglist' }, + + {custom_content_id: subscriptions_export_id, + locale: 'fr', + label: 'Export der Abonnenten' }, + + {custom_content_id: subscriptions_export_id, + locale: 'it', + label: 'Export der Abonnenten' }, + + {custom_content_id: people_export_id, + locale: 'de', + label: 'Export der Personen', + subject: 'Export der Personen', + body: "Hallo {recipient-name}

" \ + "Der Export der Personen ist fertig und an dieser Mail angehängt.

" }, + + {custom_content_id: people_export_id, + locale: 'en', + label: 'Export of People' }, + + {custom_content_id: people_export_id, + locale: 'fr', + label: 'Export der Personen' }, + + {custom_content_id: people_export_id, + locale: 'it', + label: 'Export der Personen' }, + + {custom_content_id: events_export_id, + locale: 'de', + label: 'Export der Anlässe', + subject: 'Export der Anlässe', + body: "Hallo {recipient-name}

" \ + "Der Export der Anlässe ist fertig und an dieser Mail angehängt.

" }, + + {custom_content_id: events_export_id, + locale: 'en', + label: 'Export of Events' }, + + {custom_content_id: events_export_id, + locale: 'fr', + label: 'Export der Anlässe' }, + + {custom_content_id: events_export_id, + locale: 'it', + label: 'Export der Anlässe' }, + + {custom_content_id: event_participations_export_id, + locale: 'de', + label: 'Export der Event-Teilnehmer', + subject: 'Export der Event-Teilnehmer', + body: "Hallo {recipient-name}

" \ + "Der Export der Event-Teilnehmer ist fertig und an dieser Mail angehängt.

" }, + + {custom_content_id: event_participations_export_id, + locale: 'en', + label: 'Export of Event Participants' }, + + {custom_content_id: event_participations_export_id, + locale: 'fr', + label: 'Export der Event-Teilnehmer' }, + + {custom_content_id: event_participations_export_id, + locale: 'it', + label: 'Export der Event-Teilnehmer' }, + + {custom_content_id: invoice_notification_id, + locale: 'de', + label: 'Rechnung', + subject: 'Rechnung {invoice-number} von {group-name}', + body: "

Hallo {recipient-name}

" \ + "

Rechnung von:

" \ + "

Absender: Verband, Verbandstrasse 23, 3000 Verbandort

" \ + "

" \ + "{invoice-items}

" \ + "{invoice-total}

" \ + "{payment-information}

" }, + + {custom_content_id: invoice_notification_id, + locale: 'en', + label: 'Rechnung' }, + + {custom_content_id: invoice_notification_id, + locale: 'fr', + label: 'Rechnung' }, + + {custom_content_id: invoice_notification_id, + locale: 'it', + label: 'Rechnung' }, + ) diff --git a/db/seeds/support/event_seeder.rb b/db/seeds/support/event_seeder.rb index 7a986a3d01..650c659e6d 100644 --- a/db/seeds/support/event_seeder.rb +++ b/db/seeds/support/event_seeder.rb @@ -39,7 +39,7 @@ def seed_base_event(values) seed_questions(event) if true? seed_leaders(event) 3.times do - event.class.participant_types.each do |type| + event.participant_types.each do |type| seed_event_role(event, type) end end @@ -104,7 +104,7 @@ def seed_leaders(event) def seed_participants(event) 3.times do - event.class.participant_types.each do |type| + event.participant_types.each do |type| p = seed_event_role(event, type) seed_application(p) end diff --git a/db/seeds/support/group_seeder.rb b/db/seeds/support/group_seeder.rb index 81245e2d76..61c986743e 100644 --- a/db/seeds/support/group_seeder.rb +++ b/db/seeds/support/group_seeder.rb @@ -4,9 +4,9 @@ class GroupSeeder def group_attributes - { short_name: ('A'..'Z').to_a.sample(2).join, + { address: Faker::Address.street_address, - zip_code: Faker::Address.zip, + zip_code: Faker::Address.zip_code[0..3], town: Faker::Address.city, email: Faker::Internet.safe_email } @@ -37,4 +37,4 @@ def seed_social_accounts(group) public: true } ) end -end \ No newline at end of file +end diff --git a/db/seeds/support/location_seeder.rb b/db/seeds/support/location_seeder.rb index f851f881d5..f4f920559f 100644 --- a/db/seeds/support/location_seeder.rb +++ b/db/seeds/support/location_seeder.rb @@ -1,9 +1,9 @@ # encoding: utf-8 # Copyright (c) 2012-2015, Pfadibewegung Schweiz. This file is part of -# hitobito_pbs and licensed under the Affero General Public License version 3 +# hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito_pbs. +# https://github.com/hitobito/hitobito. require 'csv' diff --git a/doc/README.md b/doc/README.md index c87e3027b2..635310f1e9 100644 --- a/doc/README.md +++ b/doc/README.md @@ -4,6 +4,7 @@ Folgende Dokumentationen sind hier zu finden: * [Architektur](architecture/README.md) * [Entwicklung](development/README.md) +* [E-Mail](e-mail/README.md) Um die Code Dokumentation zu generieren, kann `rake doc:app` ausgeführt werden. diff --git a/doc/architecture/08_konzepte.md b/doc/architecture/08_konzepte.md index 8b058a5c4c..d0fa155607 100644 --- a/doc/architecture/08_konzepte.md +++ b/doc/architecture/08_konzepte.md @@ -33,7 +33,6 @@ Qualifikationseigenschaften. haben und dadurch ebenfalls als E-Mail Liste verwendet werden können. Einzelne Personen, jedoch auch bestimmte Rollen einer Gruppe oder Teilnehmende eines Events können Abonnenten sein. - ### Wagons Die Applikation ist aufgeteilt in Core (generischer Teil) und Wagons (Verbandsspezifische @@ -44,7 +43,6 @@ werden soll, können einfach weitere Wagons erstellt werden. In einem Wagon können Tabellen um weitere Attribute ergänzt werden, Funktionalitäten, Berechtigungen und Darstellungen angepasst und hinzugefügt werden. - ### Gruppen- und Rollentypen Hitobito verfügt über ein mächtiges Metamodell um Gruppenstrukturen zu beschreiben. Gruppen sind @@ -238,26 +236,7 @@ oder nicht. Sonst ist sie immer aktiv. ### Mailing Listen / Abos -Hitobito stellt eine simple Implementation von Mailing Listen zur Verfügung. Diese können in der -Applikation beliebig erstellt und verwaltet werden. Dies geschieht in den Modellen `MailingList` -und `Subscription`. - -Alle E-Mails an die Applikationsdomain (z.B `news@db.jubla.ch`) werden über einen Catch-All Mail -Account gesammelt. Dabei muss der Mailserver den zusätzlichen E-Mail Header `X-Envelope-To` setzen, -welcher den ursprünglichen Empfänger enthält (z.B. `news`). Von der Applikation wird dieser Account -in einem Background Job über POP3 regelmässig gepollt. Die eingetroffenen E-Mails werden danach wie -folgt verarbeitet: - -1. Verwerfe das Email, falls der Empfänger keine definierte Mailing Liste ist. -1. Sende eine Rückweisungsemail, falls der Absender nicht berechtigt ist. -1. Leite das Email weiter an alle Empfänger der Mailing Liste. - -Die Berechtigung, um auf eine Mailing Liste zu schreiben, kann konfiguriert werden. Der Absender -wird über seine Haupt- oder zusätzlichen E-Mail Adressen identifiziert. Standardmässig können alle -Personen, welche die Liste Bearbeiten können, sowie die Gruppe, welcher das Abo gehört, E-Mails -schreiben. Optional können zusätzlich spezifische E-Mail Adressen, alle Abonnenten der Gruppe oder -beliebige Absender (auch nicht in hitobito erfasste) berechtigt werden. - +siehe [Mailing Listen / Abos](https://github.com/hitobito/hitobito/tree/master/doc/e-mail) ### Single Table Inheritance diff --git a/doc/development/01_setup.md b/doc/development/01_setup.md index c4671df9f2..939cccff69 100644 --- a/doc/development/01_setup.md +++ b/doc/development/01_setup.md @@ -38,7 +38,7 @@ Dazu muss Git installiert sein. cp hitobito/Gemfile.lock hitobito_[wagon]/ -Siehe [Wagon erstellen](#wagon-erstellen), wenn du frisch startest und einen Wagon für eine neue +Siehe [Wagon erstellen](04_wagons.md#wagon-erstellen), wenn du frisch startest und einen Wagon für eine neue Organisation erstellen willst. @@ -59,7 +59,11 @@ Initialisieren der Datenbank, laden der Seeds und Wagons: Starten des Entwicklungsservers: rails server - + +oder gleich aller wichtigen Prozesse: + + gem install foreman + foreman start ### Tests @@ -129,6 +133,11 @@ Achtung: Der Index wird grundsätzlich nur über diesen Aufruf aktualisiert! Än werden für die Volltextsuche also erst sichtbar, wenn wieder neu indexiert wurde. Auf der Produktion läuft dazu alle 10 Minuten ein Delayed Job. +Hinweis: Falls beim Indexieren der Fehler ``ERROR: index 'group_core': sql_fetch_row: Out of sort memory, consider increasing server sort buffer size.`` auftritt, muss in der MySql-Konfiguration (je nach Distro im File ``/etc/mysql/mysql.conf.d/mysqld.cnf`` oder ``/etc/mysql/my.cnf``) folgende Buffergrösse erhöht werden: + + [mysqld] + sort_buffer_size = 2M + ### Delayed Job @@ -165,64 +174,3 @@ und dann mittles Browser auf `http://localhost:1080` E-Mails liest. | `rake ci:nightly` | Führt die Tasks für einen Nightly Build aus. | | `rake ci:wagon` | Führt die Tasks für die Wagon Commit Builds aus. | | `rake ci:wagon:nightly` | Führt die Tasks für die Wagon Nightly Builds aus. | - - -### Wagon erstellen - -Um für hitobito eine Gruppenstruktur zu defnieren, die Grundfunktionaliäten zu erweitern oder -gewisse Features für mehrere Organisationen gemeinsam verfügbar zu machen, können Wagons verwendet -werden. Siehe dazu auch die Wagon Guidelines. Die Grundstruktur eines neuen Wagons kann sehr -einfach im Hauptprojekt generiert werden (Die Templates dazu befinden sich in `lib/templates/wagon`): - - rails generate wagon [name] - -Danach müssen noch folgende spezifischen Anpassungen gemacht werden: - -* Dateien von `hitobito/vendor/wagons/[name]` nach `hitobito_[name]` verschieben. -* Eigenes Git Repo für den Wagon erzeugen. -* `Gemfile.lock` vom Core in den Wagon kopieren. -* Organisation im Lizenz Generator (`lib/tasks/license.rake`) anpassen und überall Lizenzen - hinzufügen: `rake app:license:insert`. -* Organisation in `COPYING` ergänzen. -* `AUTHORS` ergänzen. -* In `hitobito_[name].gemspec` authors, email, summary und description anpassen. - -Falls der Wagon für eine neue Organisation ist, können noch diese Punkte angepasst werden: - -* In den Seeddaten Entwickler- und Kundenaccount hinzufügen: `db/seed/development/1_people.rb` unter `devs`. -* Die gewünschte E-Mail des Root Users in `config/settings.yml` eintragen. -* Falls die Applikation mehrsprachig sein soll: Transifex Projekt erstellen und vorbereiten. - Siehe dazu auch die Mehrsprachigkeits Guidelines. - -Falls der Wagon nicht für eine spezifische Organisation ist und keine Gruppenstruktur definiert, -sollten folgende generierten Dateien gelöscht werden: - -* Gruppen Models: `rm -rf app/models/group/root.rb app/models/[name]/group.rb` -* Übersetzungen der Models in `config/locales/models.[name].de.yml` -* Seeddaten: `rm -rf db/seeds` - -Damit entsprechende Testdaten für Tests sowie Tarantula vorhanden sind, müssen die Fixtures im Wagon entsprechend der generierten Organisationsstruktur angepasst werden. -* Anpassen der Fixtures für people, groups, roles, events, usw. (`spec/fixtures`) -* Anpassen der Tarantula Tests im Wagon (`test/tarantula/tarantula_test.rb`) - -### Gruppenstruktur erstellen - -Nachdem für eine Organisation ein neuer Wagon erstellt worden ist, muss oft auch eine -Gruppenstruktur definiert werden. Wie die entsprechenden Modelle aufgebaut sind, ist in der -Architekturdokumentation beschrieben. Hier die einzelnen Schritte, welche für das Aufsetzen der -Entwicklungsumgebung noch vorgenommen werden müssen: - -* Am Anfang steht die alleroberste Gruppe. Die Klasse in `app/models/group/root.rb` entsprechend - umbenennen (z.B. nach "Dachverband") und erste Rollen definieren. -* `app/models/[name]/group.rb#root_types` entsprechend anpassen. -* In `config/locales/models.[name].de.yml` Übersetzungen für Gruppe und Rollen hinzufügen. -* In `db/seed/development/1_people.rb` die Admin Rolle für die Entwickler anpassen. -* In `db/seed/groups.rb` den Seed der Root Gruppe anpassen. -* In `spec/fixtures/groups.yml` den Typ der Root Gruppe anpassen. -* In `spec/fixtures/roles.yml` die Rollentypen anpassen. -* Tests ausführen -* Weitere Gruppen und Rollen inklusive Übersetzungen definieren. -* In `db/seed/development/0_groups.rb` Seed Daten für die definierten Gruppentypen definieren. -* In `spec/fixtures/groups.yml` Fixtures für die definierten Gruppentypen definieren. Es empfielt - sich, die selben Gruppen wie in den Development Seeds zu verwenden. -* `README.md` mit Output von `rake app:hitobito:roles` ergänzen. diff --git a/doc/development/03_guidelines.md b/doc/development/03_guidelines.md index 27fb686640..a7059f0f38 100644 --- a/doc/development/03_guidelines.md +++ b/doc/development/03_guidelines.md @@ -13,55 +13,6 @@ Violations sind unmittelbar zu korrigieren. Das selbe gilt für Warnungen, welche im Jenkins auftreten (Brakeman, ...). -### Wagons - -Die Applikation ist aufgeteilt in Core (generischer Teil) und Wagon (Verbandsspezifische -Erweiterungen). Im Development und Production Mode sind jeweils beide Teile geladen, in den Tests -nur der Core bzw. in den Wagon Tests der Core und der spezifische Wagon. Dies wird über das Gemfile -gesteuert. Zur Funktionsweise von Wagons allgemein siehe auch -[wagons](http://github.com/codez/wagons). - -Einige grundlegende Dinge, welche in Zusammenhang mit Wagons zu beachten sind: - -* Der hitobito Core und alle Wagon Verzeichnisse müssen im gleichen Haupverzeichnis sein. -* Zu Entwicklung kann die Datei `Wagonfile.ci` nach `Wagonfile` kopiert werden, um alle Wagons in -benachbarten Verzeichnissen zu laden. Falls nur bestimmte Wagons aktiviert werden sollen, kann dies -ebenfalls im `Wagonfile` konfiguriert werden. -* Wagons verwenden die gleiche Datenbank wie der Core. Wenn im Core Migrationen erstellt werden, -müssen alle Wagon Migrationen daraus entfernt werden, bevor das `schema.rb` generiert werden kann. -Dies geht am einfachsten, indem die development Datenbank komplett gelöscht und wiederhergestellt -wird. -* Wenn neue Gems zum Core hinzugefügt werden, müssen alle `Gemfile.lock` Dateien in den Wagons -aktualisert werden. Dies geschieht am einfachsten mit `rake wagon:bundle:update`, oder manuell mit -`cp Gemfile.lock ../hitobito_[wagon]/`. Dasselbe gilt, wenn Gems beim Umstellen einer Wagon Version -nicht mehr passen. Das `Gemfile.lock` eines Wagons wird NIE ins Git eingecheckt. -* Ein neuer Wagon kann mit `rails g wagon [name]` erstellt werden. Danach sollte dieser von -`vendor/wagons` in ein benachbartes Verzeichnis des Cores verschoben werden und die Datei -`app_root.rb` des Wagons entsprechend angepasst werden. - - -#### Entwickeln für mehrere Verbände/Instanzen - -Es kann immer nur ein 'Haupt'-Wagon aktiv sein, welcher die Verbandsstruktur definiert. Um zwischen -verschiedenen aktiven Verbänden zu wechseln, empfiehlt sich das Speichern der einzelnen Development -Datenbanken, damit die jeweiligen Seed Daten nicht immer neu geladen werden müssen (Diese Files -nicht ins Git einchecken!). Danach erfolgt die Umstellung von einer Konfiguration auf die andere: - -1. Alle aktiven Prozesse (Server, Console, ...) stoppen. -1. Im `Wagonfile` den [new wagon] aktivieren, andere auskommentieren. -1. `cp db/development-[new_wagon].sqlite3 db/development.sqlite3` -1. `rm -rf tmp/cache` (Falls customized CSS vorhanden). -1. Prozesse (Server, ...) wieder starten. - -Falls `spring` im Einsatz ist, muss vor dem Wechsel `spring stop` ausgeführt werden. - -#### Stylesheets in allen Wagons überprüfen - -Wenn an den Core Stylesheets Anpassungen vorgenommen werden, müssen diese bei allen Wagons, -insbesondere denjenigen mit customized Styles (z.B. Jubla) überprüft werden, damit die auch dort -funktionieren. - - ### Spezifische Guidelines Allgemeine Konventionen und Erklärungen für spezifische Bereiche. @@ -96,7 +47,14 @@ Action? * Sind in jedem Fall die richtigen Menu Items als aktiv markiert? * Sind in allen Texten Gendergerechte Bezeichnungen, falls nötig in der Form "/-in" verwendet? -#### Checkliste für neue Attribute +#### Stylesheets in allen Wagons überprüfen + +Wenn an den Core Stylesheets Anpassungen vorgenommen werden, müssen diese bei allen Wagons, +insbesondere denjenigen mit customized Styles (z.B. Jubla) überprüft werden, damit die auch dort +funktionieren. + + +### Checkliste für neue Attribute Folgende Punkte sind zu berücksichtigen, wenn neue Attribute zu Hitobito Modellen hinzugefügt werden. Da hitobito über diverse Schnittstellen verfügt, gehen beim Definieren von Attributen rasch @@ -116,13 +74,15 @@ sowie im JSON API die selben Regeln. Ist also z.B. ein Attribut öffentlich, wir und in der JSON Personen Liste angezeigt, wenn nicht, nur im Full CSV und im Einzelperson JSON, falls die Berechtigung dafür vorhanden ist. -##### Personenattribute +Ein beispielhafte Anleitung, wie in einem Wagon Attribute hinzugefügt werden können, findest du im Kapitel [Wagons](04_wagons.md#attribute-hinzuf-gen). + +#### Personenattribute * CSV Import * CSV Export (Adressexport? Voller Export?) * Log (Papertrail) -##### Rollen umbennen / entfernen +#### Rollen umbennen / entfernen * Migration aller betroffenen `Role` Instanzen (`with_deleted`!). * Migration aller betroffenen `RelatedRoleType` Instanzen. diff --git a/doc/development/04_wagons.md b/doc/development/04_wagons.md new file mode 100644 index 0000000000..6b52b6c71b --- /dev/null +++ b/doc/development/04_wagons.md @@ -0,0 +1,321 @@ +## Wagons + +Hitobito ist aufgeteilt in Core (generischer Teil) und Wagon(s) (Verbandsspezifische Erweiterungen). Um eine Gruppenstruktur zu definieren, das Verhalten der Applikation auf benutzerspezifische Bedürfnisse anzupassen oder gewisse Features für mehrere Organisationen gemeinsam verfügbar zu machen, können Wagons verwendet werden. Damit Hitobito lauffähig ist, wird mindestens ein Wagon mit einer Gruppenstruktur benötigt. Jeder Wagon wird in einem eigenen Git Repo verwaltet. + +### Grundlegendes + +Im Development und Production Mode sind jeweils beide Teile geladen, in den Tests +nur der Core bzw. in den Wagon Tests der Core und der spezifische Wagon. Dies wird über das Gemfile +gesteuert. Zur Funktionsweise von Wagons allgemein siehe auch +[wagons](http://github.com/codez/wagons). + + +Einige grundlegende Dinge, welche in Zusammenhang mit Wagons zu beachten sind: + +* Der hitobito Core und alle Wagon Verzeichnisse müssen im gleichen Haupverzeichnis sein. +* Zu Entwicklung kann die Datei `Wagonfile.ci` nach `Wagonfile` kopiert werden, um alle Wagons in +benachbarten Verzeichnissen zu laden. Falls nur bestimmte Wagons aktiviert werden sollen, kann dies +ebenfalls im `Wagonfile` konfiguriert werden. +* Wagons verwenden die gleiche Datenbank wie der Core. Wenn im Core Migrationen erstellt werden, +müssen alle Wagon Migrationen daraus entfernt werden, bevor das `schema.rb` generiert werden kann. +Dies geht am einfachsten, indem die development Datenbank komplett gelöscht und wiederhergestellt +wird. +* Wenn neue Gems zum Core hinzugefügt werden, müssen alle `Gemfile.lock` Dateien in den Wagons +aktualisert werden. Dies geschieht am einfachsten mit `rake wagon:bundle:update`, oder manuell mit +`cp Gemfile.lock ../hitobito_[wagon]/`. Dasselbe gilt, wenn Gems beim Umstellen einer Wagon Version +nicht mehr passen. Das `Gemfile.lock` eines Wagons wird NIE ins Git eingecheckt. +* Ein neuer Wagon kann mit `rails g wagon [name]` erstellt werden. Danach sollte dieser von +`vendor/wagons` in ein benachbartes Verzeichnis des Cores verschoben werden und die Datei +`app_root.rb` des Wagons entsprechend angepasst werden. + + +### Entwickeln für mehrere Verbände/Instanzen + +Es kann immer nur ein 'Haupt'-Wagon aktiv sein, welcher die Verbandsstruktur definiert. Um zwischen +verschiedenen aktiven Verbänden zu wechseln, empfiehlt sich das Speichern der einzelnen Development +Datenbanken, damit die jeweiligen Seed Daten nicht immer neu geladen werden müssen (Diese Files +nicht ins Git einchecken!). Danach erfolgt die Umstellung von einer Konfiguration auf die andere: + +1. Alle aktiven Prozesse (Server, Console, ...) stoppen. +1. Im `Wagonfile` den [new wagon] aktivieren, andere auskommentieren. +1. `cp db/development-[new_wagon].sqlite3 db/development.sqlite3` +1. `rm -rf tmp/cache` (Falls customized CSS vorhanden). +1. Prozesse (Server, ...) wieder starten. + +Falls `spring` im Einsatz ist, muss vor dem Wechsel `spring stop` ausgeführt werden. + + +### Anleitung: Wagon erstellen + +Die Grundstruktur eines neuen Wagons kann sehr +einfach im Hauptprojekt generiert werden (Die Templates dazu befinden sich in `lib/templates/wagon`): + + rails generate wagon [name] + +Danach müssen noch folgende spezifischen Anpassungen gemacht werden: + +* Dateien von `hitobito/vendor/wagons/[name]` nach `hitobito_[name]` verschieben. +* Eigenes Git Repo für den Wagon erzeugen. +* `Gemfile.lock` vom Core in den Wagon kopieren. +* Organisation im Lizenz Generator (`lib/tasks/license.rake`) anpassen und überall Lizenzen + hinzufügen: `rake app:license:insert`. +* Organisation in `COPYING` ergänzen. +* `AUTHORS` ergänzen. +* In `hitobito_[name].gemspec` authors, email, summary und description anpassen. + +Falls der Wagon für eine neue Organisation ist, können noch diese Punkte angepasst werden: + +* In den Seeddaten Entwickler- und Kundenaccount hinzufügen: `db/seed/development/1_people.rb` unter `devs`. +* Die gewünschte E-Mail des Root Users in `config/settings.yml` eintragen. +* Falls die Applikation mehrsprachig sein soll: Transifex Projekt erstellen und vorbereiten. + Siehe dazu auch die Mehrsprachigkeits Guidelines. + +Falls der Wagon nicht für eine spezifische Organisation ist und keine Gruppenstruktur definiert, +sollten folgende generierten Dateien gelöscht werden: + +* Gruppen Models: `rm -rf app/models/group/root.rb app/models/[name]/group.rb` +* Übersetzungen der Models in `config/locales/models.[name].de.yml` +* Seeddaten: `rm -rf db/seeds` + +Damit entsprechende Testdaten für Tests sowie Tarantula vorhanden sind, müssen die Fixtures im Wagon entsprechend der generierten Organisationsstruktur angepasst werden. +* Anpassen der Fixtures für people, groups, roles, events, usw. (`spec/fixtures`) +* Anpassen der Tarantula Tests im Wagon (`test/tarantula/tarantula_test.rb`) + +### Anleitung: Gruppenstruktur definieren + +Nachdem für eine Organisation ein neuer Wagon erstellt worden ist, muss oft auch eine +Gruppenstruktur definiert werden. Wie die entsprechenden Modelle aufgebaut sind, ist in der +Architekturdokumentation beschrieben. Hier die einzelnen Schritte, welche für das Aufsetzen der +Entwicklungsumgebung noch vorgenommen werden müssen: + +* Am Anfang steht die alleroberste Gruppe. Die Klasse in `app/models/group/root.rb` entsprechend + umbenennen (z.B. nach "Dachverband") und erste Rollen definieren. +* `app/models/[name]/group.rb#root_types` entsprechend anpassen. +* In `config/locales/models.[name].de.yml` Übersetzungen für Gruppe und Rollen hinzufügen. +* In `db/seed/development/1_people.rb` die Admin Rolle für die Entwickler anpassen. +* In `db/seed/groups.rb` den Seed der Root Gruppe anpassen. +* In `spec/fixtures/groups.yml` den Typ der Root Gruppe anpassen. +* In `spec/fixtures/roles.yml` die Rollentypen anpassen. +* Tests ausführen +* Weitere Gruppen und Rollen inklusive Übersetzungen definieren. +* In `db/seed/development/0_groups.rb` Seed Daten für die definierten Gruppentypen definieren. +* In `spec/fixtures/groups.yml` Fixtures für die definierten Gruppentypen definieren. Es empfielt + sich, die selben Gruppen wie in den Development Seeds zu verwenden. +* `README.md` mit Output von `rake app:hitobito:roles` ergänzen. + + +### Anleitung: Einzelne Methode anpassen + +Im PBS Wagon wurde die Methode `full_name` auf dem `Person` Model angepasst. + +Die Implementation im Core sieht dabei folgendermassen aus: (`hitobito/app/models/person.rb`) + + def full_name(format = :default) + case format + when :list then "#{last_name} #{first_name}".strip + else "#{first_name} #{last_name}".strip + end + end + +Im PBS Wagon gibt es ein entsprechendes Modul mit dem benutzerspezifischen Code für die Person Model Klasse: (`hitobito_pbs/app/models/pbs/person.rb`) + + module Pbs::Person + ... + extend ActiveSupport::Concern + + included do + ... + alias_method_chain :full_name, :title + ... + end + ... + + def full_name_with_title(format = :default) + case format + when :list then full_name_without_title(format) + else "#{title} #{full_name_without_title(format)}".strip + end + end + +Mit `alias_method_chain` wird beim Aufruf von `#full_name` die Methode `#full_name_with_title` aufgerufen. Diese Methode wird ebenfalls in diesem Modul definiert. Die Implementation aus dem Core steht unter `#full_name_without_title` zur Verfügung. + +Damit der Code in diesem Module entsprechend für das Person Model übernommen wird, wird dies in der `wagon.rb` entsprechend included: (`hitobito_pbs/lib/hitobito_pbs/wagon.rb`) + + module HitobitoPbs + class Wagon < Rails::Engine + include Wagons::Wagon + ... + Person.send :include, Pbs::Person + ... + + +### Anleitung: Attribute hinzufügen + +The following documentation describes how new attributes can be added to a model in an own wagon. For reasons of simplification, this documentation follows an example where the generic wagon is going to be adapted and the `Person` model gets two new attributes called `title` and `salutation`. + +All mentioned files are created/adjusted in a dedicated wagon, not in the core application. + +#### Add new attributes to the database + +In order to adapt the database structure and add the desired new attributes to the model, a new migration must be created by the following command, which is executed in the root directory of the wagon: + + $ bin/rails generate migration AddPeopleAttrs + +This command will create a new migration file in the path `db/migrate/YYYYMMDDHHMMSS_add_people_attrs.rb` which in the end should look as follows: + + class AddPeopleAttrs < ActiveRecord::Migration + def change + add_column :people, :title, :string + add_column :people, :salutation, :string + end + end + +In this example, the data types of the attributes are set to strings. + +#### Permit attributes for editing + +The new attributes must be included in the application logic. To do so, a new controller has to be created in `app/controllers//people_controller.rb` which permits the two attributes to be updated: + + module + module PeopleController + extend ActiveSupport::Concern + included do + self.permitted_attrs += [:title, :salutation] + end + end + end + +#### Show and edit attributes in the view + +There are two views which have to be adapted regarding the `Person` model: On one side the show view of the person and on the other side the edit view of the person. + +Create a new file in `app/views/people/_details_.html.haml` with the following content: + + = render_attrs(entry, :title, :salutation) + +Create a new file in `app/views/people/_fields_.html.haml` with the following content: + + = f.labeled_input_fields :title, :salutation + +It is important that these files start with `_details` respectively `_fields`. The core-application automatically includes/renders all files starting with `_details` and `_fields`. The subsequent characters (`_`) can be chosen arbitrarily. + +#### Translate the attribute names + +In order to display the attribute names properly in each language, the language files of all used languages must be adapted by simply adding the following lines to the `config/locales/models...yml`-files: + + attributes: + person: + title: + salutation: + +#### Include attributes in the CSV/Excel Exports + +If wished, the attributes can be included in the CSV-File that is generated when performing a contact export. For this inclusion, a new file in `app/domain//export/tabular/people/people_address.rb` with the following content must be created: + + module + module Export + module Tabular + module People + module PeopleAddress + extend ActiveSupport::Concern + + included do + alias_method_chain :person_attributes, :title + end + + def person_attributes_with_title + person_attributes_without_title + [:title, :salutation] + end + end + end + end + end + end + +#### Make attributes searchable + +The new attributes must be indexed in `app/indices/person_index.rb` where all indexes for Sphinx (the search tool that is used by hitobito) are defined. + + ThinkingSphinx::Index.define_partial :person do + indexes title + end + +#### Output attributes in the API + +In order to provide the additional attributes in the API (the JSON-file of the object), the serializer for the people must be extended in `app/serializers//person_serializer.rb`: + + module ::PersonSerializer + extend ActiveSupport::Concern + included do + extension(:details) do |_| + map_properties :title, :salutation + end + end + end + +#### Wire up above extensions + +The newly created or updated `PeopleController`, the CSV export file and the serializer file for the API must also be defined in the wagon configuration file which is located in `lib//wagon.rb`. + + config.to_prepare do + ... + PeopleController.send :include, ::PeopleController + Export::Tabular::People::PeopleAddress.send :include, ::Export::Tabular::People::PeopleAddress + PersonSerializer.send :include, ::PersonSerializer + end + +#### Write tests for attributes + +Arbitrary tests cases can be defined in the `spec/` directory of the wagon. As an example, the following file (`spec/domain/export/tabular/people/people_address_spec.rb`) proposes a test case that checks whether the attributes are exported properly into the CSV-file: + + require 'spec_helper' + require 'csv' + + describe Export::Tabular::People::PeopleAddress do + + let(:person) { people(:admin) } + let(:simple_headers) do + %w(Vorname Nachname Übername Firmenname Firma Haupt-E-Mail Adresse PLZ Ort Land + Geschlecht Geburtstag Rollen Titel Anrede) + end + let(:list) { Person.where(id: person) } + let(:data) { Export::Tabular::People::PeopleAddress.csv(list) } + let(:csv) { CSV.parse(data, headers: true, col_sep: Settings.csv.separator) } + + subject { csv } + + before do + person.update!(title: 'Dr.', salutation: 'Herr', town: 'Bern') + end + + context 'export' do + its(:headers) { should == simple_headers } + + context 'first row' do + subject { csv[0] } + + its(['Vorname']) { should eq person.first_name } + its(['Nachname']) { should eq person.last_name } + its(['Haupt-E-Mail']) { should eq person.email } + its(['Ort']) { should eq person.town } + its(['Geschlecht']) { should eq person.gender_label } + its(['Rollen']) { should eq 'Administrator Verband' } + its(['Titel']) { should eq 'Dr.' } + its(['Anrede']) { should eq 'Herr' } + end + end + + context 'export_full' do + its(:headers) { should include('Titel') } + its(:headers) { should include('Anrede') } + + let(:data) { Export::Tabular::People::PeopleFull.csv(list) } + + context 'first row' do + subject { csv[0] } + + its(['Titel']) { should eq 'Dr.' } + its(['Anrede']) { should eq 'Herr' } + end + end + end diff --git a/doc/development/04_rest_api.md b/doc/development/05_rest_api.md similarity index 73% rename from doc/development/04_rest_api.md rename to doc/development/05_rest_api.md index 30869037a4..e6d815081a 100644 --- a/doc/development/04_rest_api.md +++ b/doc/development/05_rest_api.md @@ -4,10 +4,15 @@ Das JSON Format folgt den Konventionen von [json:api](http://jsonapi.org). ### Authentisierung -Die folgenden Methoden dienen zur Authentisierung und Verwaltung des Authentisierungstokens. -Als Parameter müssen immer `person[email]` und `person[password]` übergeben werden. In der Antwort -ist der Wert des `authentication_token` enthalten, welches für die folgenden Requests jeweils -mitgegeben werden muss. +Für die Verwendung der API ist ein Authentisierungstoken nötig. Jeder Benutzeraccount +kann ein solches Token erstellen. Dieses Token ist danach mit dem Benutzeraccount +verknüpft und hat die selben Zugriffsrechte wie der/die Benutzer/in. + +Ein Token ist für unbeschränkte Zeit gültig, es kann also z.B. in eine Webseite +eingebunden werden, ohne dass das Passwort für den Benutzeraccount +verwendet werden muss. + +Für die Verwaltung des Tokens dienen die folgenden HTTP Endpunkte: | Methode | Pfad | Funktion | | --- | --- | --- | @@ -15,8 +20,15 @@ mitgegeben werden muss. | POST | /users/token.json | Token neu generieren | | DELETE | /users/token.json | Token löschen | -Sobald das Authentisierungstoken bekannt ist, können verschiedene Endpunkte abgefragt werden. -Dazu bestehen zwei Möglichkeiten: +Als Parameter müssen immer `person[email]` und `person[password]` übergeben werden. + +Mit `curl` geht das so: + + curl -d "person[email]=mitglied@hitobito.ch" \ + -d "person[password]=demo" \ + http://demo.hitobito.ch/users/sign_in.json + +Um das Token bei der restlichen API zu verwenden, bestehen zwei Möglichkeiten: * **Parameter**: `user_email` und `user_token` werden als Pfadparameter angegeben, der Pfad muss mit `.json` enden (Bsp: `/groups/1.json?user_email=zumkehr@puzzle.ch&user_token=abcdef`). diff --git a/doc/development/05_jenkins_setup.md b/doc/development/06_jenkins_setup.md similarity index 92% rename from doc/development/05_jenkins_setup.md rename to doc/development/06_jenkins_setup.md index f42812cf52..c926d64f3f 100644 --- a/doc/development/05_jenkins_setup.md +++ b/doc/development/06_jenkins_setup.md @@ -1,4 +1,4 @@ -#Jenkins Setup +## Jenkins Setup Um mit den verschiedenen Hitobito Projekten nicht zuviel Overhead zu erzeugen, werden die Jenkins Jobs wie folgt aufgeteilt. Damit werden die jeweiligen Tests nur einmal ausgeführt. @@ -18,7 +18,7 @@ Als Home Verzeichnis für alle Tasks und Reports muss danach hitobito gesetzt we Damit die Änderungen am Core Repo bei Wagon Jobs keinen Commit Build triggert, müssen dort für das Core Repo alle Dateien ausgeschlossen werden (Erweitert > Included Regions: 'none'). Änderungen am Core, welche bei Wagons Probleme verursachen könnten, werden somit spätestens in den Nightly Builds erkannt (Core Repo Daten nicht ausgeschlossen). Dies ist ein Trade-off zwischen ständig laufenden Jobs, auf welche gewartet werden muss, und unmittelbarem Feedback. -##Umgebungsvariablen +### Umgebungsvariablen Für alle Jobs muss eine ensprechende Test DB erstellt und konfiguriert werden. Jeder Job muss eine eigene DB erstellen, damit sich diese nicht in die Quere kommen. Dabei ist der Prefix 'jenkins_hitobito_' zu wählen. Die Jobs müssen ebenfalls einen eindeutigen RAILS_SPHINX_PORT sowie einen CAPYBARA_SERVER_PORT definieren. @@ -36,7 +36,7 @@ Set environment variable names: RAILS_DB_PASSWORD=.. -##hitobito-core: Commit Build für den Core +### hitobito-core: Commit Build für den Core Läuft nach jedem Commit auf dem Core Repo (pitc_hit/hitobito.git), Master und Stable Branch. * Läuft die Rubocop Hard Conventions @@ -46,7 +46,7 @@ Rake Task: ci -##hitobito-core-nightly_master/stable: Nightly Build für den Core +### hitobito-core-nightly_master/stable: Nightly Build für den Core Läuft jede Nacht bei Changes auf dem Core Repo Master und Stable Branch. Die gesamte Build History wird archiviert. @@ -58,7 +58,7 @@ Läuft jede Nacht bei Changes auf dem Core Repo Master und Stable Branch. Die ge Rake Task: bundle exec rake ci:nightly tx:auth tx:push -t -##hitobito-[wagon]: Commit Build für einen Wagon +### hitobito-[wagon]: Commit Build für einen Wagon Läuft nach jedem Commit auf dem Wagon Repo, je nach dem auf dem Master oder Stable Branch. @@ -71,7 +71,7 @@ Rake Task: Script: bin/ci/wagon_commit.sh -##hitobito-[wagon]-nightly_master: Nightly Build für einen Wagon, Master Branch +### hitobito-[wagon]-nightly_master: Nightly Build für einen Wagon, Master Branch Läuft jede Nacht bei Changes auf dem Wagon Repo, Master Branch, falls der hitobito-core-nightly Job erfolgreich war. Die gesamte Build History wird archiviert. @@ -91,7 +91,7 @@ Script: bin/ci/wagon_nightly_master.sh -##Abhängigkeiten +#### Abhängigkeiten Zum überprüfen, ob der hitobito-core-nightly Job erfolgreich war, wurde folgendes Script als erster Build Step verwendet. Dieses funktioniert leider nicht mehr, da die Jobs auf unterschiedlichen Build Nodes ablaufen können und daher keinen Zugriff aufeinander haben. D.h., dass momentan ein Wagon Nightly Job erfolgreich sein kann, obwohl der Core Nightly failed. Damit können fehlerhafte Cores deployt werden. @@ -109,7 +109,7 @@ Zum überprüfen, ob der hitobito-core-nightly Job erfolgreich war, wurde folgen Dadurch failt der Job direkt, falls hitobito-core-nightly nicht successfull ist. -##hitobito-[wagon]-nightly_stable: Nightly Build für einen Wagon, Stable Branch +### hitobito-[wagon]-nightly_stable: Nightly Build für einen Wagon, Stable Branch Läuft jede Nacht bei Changes auf dem Wagon Repo, Stable Branch, falls der hitobito-core-nightly Job erfolgreich war. @@ -124,7 +124,7 @@ Script: bin/ci/wagon_nightly_stable.sh -##hitobito-[wagon]-rpm: RPM Build für einen Wagon +### hitobito-[wagon]-rpm: RPM Build für einen Wagon Läuft nach dem entsprechenden hitobito-[wagon]-nightly Job und erstellt ein RPM. Master oder Stable Branch. diff --git a/doc/development/README.md b/doc/development/README.md index 83a752084b..997bac1519 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -7,6 +7,8 @@ Diese Dokumente beschreiben verschiedene Aspekte, welche bei der Entwicklung zu * [Entwicklungsumgebung](01_setup.md) * [Deployment](02_deployment.md) * [Guidelines](03_guidelines.md) -* [REST API](04_rest_api.md) +* [Wagons](04_wagons.md) +* [REST API](05_rest_api.md) +* [Jenkins Setup](06_jenkins_setup.md) Alle Diagramme werden mit [Draw.io](http://draw.io) erstellt und jeweils als Original .xml sowie als .svg abgespeichert. diff --git a/doc/e-mail/README.md b/doc/e-mail/README.md new file mode 100644 index 0000000000..28994611e5 --- /dev/null +++ b/doc/e-mail/README.md @@ -0,0 +1,47 @@ +## E-Mail + +Hier findest du eine Übersicht sowie die Dokumentation der E-Mail Features von Hitobito + +### Mailing Listen / Abos + +Hitobito stellt eine simple Implementation von Mailing Listen zur Verfügung. Diese können in der +Applikation beliebig erstellt und verwaltet werden. Dies geschieht in den Modellen `MailingList` +und `Subscription`. + +Alle E-Mails an die Applikationsdomain (z.B `news@db.jubla.ch`) werden über einen Catch-All Mail +Account gesammelt. Dabei muss der Mailserver den zusätzlichen E-Mail Header `X-Envelope-To` setzen, +welcher den ursprünglichen Empfänger enthält (z.B. `news`). Von der Applikation wird dieser Account +in einem Background Job über POP3 regelmässig gepollt. Die eingetroffenen E-Mails werden danach wie +folgt verarbeitet: + +1. Verwerfe das Email, falls der Empfänger keine definierte Mailing Liste ist. +1. Sende eine Rückweisungsemail, falls der Absender nicht berechtigt ist. +1. Leite das Email weiter an alle Empfänger der Mailing Liste. + +DiDa man aus diversen Gründen (BCC, Mail Aliase) den eigentlichen Empfänger nicht aus dem To: Header lesen kann, muss ein zusätzlicher Header mit der Empfängeradresse vom Mailserver gesetzt werden. Als quasi Standard hat sich für solche Zwecke hier der X-Envelope-to Header etabliert.e Berechtigung, um auf eine Mailing Liste zu schreiben, kann konfiguriert werden. Der Absender +wird über seine Haupt- oder zusätzlichen E-Mail Adressen identifiziert. Standardmässig können alle +Personen, welche die Liste Bearbeiten können, sowie die Gruppe, welcher das Abo gehört, E-Mails +schreiben. Optional können zusätzlich spezifische E-Mail Adressen, alle Abonnenten der Gruppe oder +beliebige Absender (auch nicht in hitobito erfasste) berechtigt werden. + +Jede Gruppe kann beliebig viele Abos haben, welche optional eine E-Mail Adresse +haben und dadurch ebenfalls als E-Mail Liste verwendet werden können. Einzelne Personen, jedoch auch +bestimmte Rollen einer Gruppe oder Teilnehmende eines Events können Abonnenten sein. + +#### X-Envelope-To Header + +Da man aus diversen Gründen (BCC, Mail Aliase) den eigentlichen Empfänger nicht aus dem To: Header lesen kann, muss ein zusätzlicher Header mit der Empfängeradresse vom Mailserver gesetzt werden. Als quasi Standard hat sich für solche Zwecke hier der X-Envelope-to Header etabliert. + +### Mailversand + +Bei folgenden Aktionen werden Mails versendet: (Wagon Features sind hier nicht berücksichtigt) + +| Aktion | Mailer Class | DelayedJob | Attachment ? | +| --- | --- | --- | --- | +| Passwort vergessen | via Devise gem | - | nein | +| Passwort Reset / Login erstellen (durch Fremdperson) | Person::LoginMailer | Person::SendLoginJob | nein | +| Zugriffsanfrage Person | Person::AddRequestMailer | Person::SendAddRequestJob | nein | +| Event Bestätigung Teilnahme | Event::ParticipationMailer | Event::ParticipationConfirmationJob | ja | +| Event Abmeldung Teilnahme | Event::ParticipationMailer | Event::CancelApplicationJob | nein | +| Event Einladung Registrierung | Event::RegisterMailer | Event::SendRegisterLoginJob | nein | +| Export der Abonnenten einer Mailingliste| Export::SubscriptionsMailer | Export::SubscriptionsJob | ja | diff --git a/doc/template/skeleton.html b/doc/template/skeleton.html index 3b03435cf5..1a894f2535 100644 --- a/doc/template/skeleton.html +++ b/doc/template/skeleton.html @@ -10,57 +10,30 @@ - - + - + diff --git a/doc/template/style.css b/doc/template/style.css index d201c42962..b51264ed5b 100644 --- a/doc/template/style.css +++ b/doc/template/style.css @@ -1,17 +1,122 @@ -@import url(//fonts.googleapis.com/css?family=Varela+Round);@import url(//fonts.googleapis.com/css?family=Quicksand:400,700);ul.wysihtml5-toolbar{margin:0;padding:0;display:block}ul.wysihtml5-toolbar::after{clear:both;display:table;content:""}ul.wysihtml5-toolbar>li{float:left;display:list-item;list-style:none;margin:0 5px 10px 0}ul.wysihtml5-toolbar a[data-wysihtml5-command=bold]{font-weight:bold}ul.wysihtml5-toolbar a[data-wysihtml5-command=italic]{font-style:italic}ul.wysihtml5-toolbar a[data-wysihtml5-command=underline]{text-decoration:underline}ul.wysihtml5-toolbar a.btn.wysihtml5-command-active{background-image:none;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);background-color:#E6E6E6;background-color:#D9D9D9;outline:0}ul.wysihtml5-commands-disabled .dropdown-menu{display:none !important}ul.wysihtml5-toolbar div.wysihtml5-colors{display:block;width:50px;height:20px;margin-top:2px;margin-left:5px;position:absolute;pointer-events:none}ul.wysihtml5-toolbar a.wysihtml5-colors-title{padding-left:70px}ul.wysihtml5-toolbar div[data-wysihtml5-command-value="black"]{background:black !important}ul.wysihtml5-toolbar div[data-wysihtml5-command-value="silver"]{background:silver !important}ul.wysihtml5-toolbar div[data-wysihtml5-command-value="gray"]{background:gray !important}ul.wysihtml5-toolbar div[data-wysihtml5-command-value="maroon"]{background:maroon !important}ul.wysihtml5-toolbar div[data-wysihtml5-command-value="red"]{background:red !important}ul.wysihtml5-toolbar div[data-wysihtml5-command-value="purple"]{background:purple !important}ul.wysihtml5-toolbar div[data-wysihtml5-command-value="green"]{background:green !important}ul.wysihtml5-toolbar div[data-wysihtml5-command-value="olive"]{background:olive !important}ul.wysihtml5-toolbar div[data-wysihtml5-command-value="navy"]{background:navy !important}ul.wysihtml5-toolbar div[data-wysihtml5-command-value="blue"]{background:blue !important}ul.wysihtml5-toolbar div[data-wysihtml5-command-value="orange"]{background:orange !important}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}audio:not([controls]){display:none}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}a:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}a:hover,a:active{outline:0}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{max-width:100%;width:auto\9;height:auto;vertical-align:middle;border:0;-ms-interpolation-mode:bicubic}#map_canvas img,.google-maps img{max-width:none}button,input,select,textarea{margin:0;font-size:100%;vertical-align:middle}button,input{*overflow:visible;line-height:normal}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}label,select,button,input[type="button"],input[type="reset"],input[type="submit"],input[type="radio"],input[type="checkbox"]{cursor:pointer}input[type="search"]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none}textarea{overflow:auto;vertical-align:top}@media print{*{text-shadow:none !important;color:#000 !important;background:transparent !important;box-shadow:none !important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}@page{margin:0.5cm}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}body{margin:0;font-family:"Varela Round", Helvetica, Arial, sans-serif;font-size:14px;line-height:20px;color:#474e5d;background-color:#329897}a{color:#329897;text-decoration:none}a:hover,a:focus{color:#6a396f;text-decoration:underline}.img-rounded{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.img-polaroid{padding:4px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);-webkit-box-shadow:0 1px 3px rgba(0,0,0,0.1);-moz-box-shadow:0 1px 3px rgba(0,0,0,0.1);box-shadow:0 1px 3px rgba(0,0,0,0.1)}.img-circle{-webkit-border-radius:500px;-moz-border-radius:500px;border-radius:500px}.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;content:"";line-height:0}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px}.span1{width:60px}.span2{width:140px}.span3{width:220px}.span4{width:300px}.span5{width:380px}.span6{width:460px}.span7{width:540px}.span8{width:620px}.span9{width:700px}.span10{width:780px}.span11{width:860px}.span12{width:940px}.offset1{margin-left:100px}.offset2{margin-left:180px}.offset3{margin-left:260px}.offset4{margin-left:340px}.offset5{margin-left:420px}.offset6{margin-left:500px}.offset7{margin-left:580px}.offset8{margin-left:660px}.offset9{margin-left:740px}.offset10{margin-left:820px}.offset11{margin-left:900px}.offset12{margin-left:980px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;content:"";line-height:0}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;float:left;margin-left:2.12766%;*margin-left:2.07447%}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.12766%}.row-fluid .span1{width:6.38298%;*width:6.32979%}.row-fluid .span2{width:14.89362%;*width:14.84043%}.row-fluid .span3{width:23.40426%;*width:23.35106%}.row-fluid .span4{width:31.91489%;*width:31.8617%}.row-fluid .span5{width:40.42553%;*width:40.37234%}.row-fluid .span6{width:48.93617%;*width:48.88298%}.row-fluid .span7{width:57.44681%;*width:57.39362%}.row-fluid .span8{width:65.95745%;*width:65.90426%}.row-fluid .span9{width:74.46809%;*width:74.41489%}.row-fluid .span10{width:82.97872%;*width:82.92553%}.row-fluid .span11{width:91.48936%;*width:91.43617%}.row-fluid .span12{width:100.0%;*width:99.94681%}.row-fluid .offset1{margin-left:10.6383%;*margin-left:10.53191%}.row-fluid .offset1:first-child{margin-left:8.51064%;*margin-left:8.40426%}.row-fluid .offset2{margin-left:19.14894%;*margin-left:19.04255%}.row-fluid .offset2:first-child{margin-left:17.02128%;*margin-left:16.91489%}.row-fluid .offset3{margin-left:27.65957%;*margin-left:27.55319%}.row-fluid .offset3:first-child{margin-left:25.53191%;*margin-left:25.42553%}.row-fluid .offset4{margin-left:36.17021%;*margin-left:36.06383%}.row-fluid .offset4:first-child{margin-left:34.04255%;*margin-left:33.93617%}.row-fluid .offset5{margin-left:44.68085%;*margin-left:44.57447%}.row-fluid .offset5:first-child{margin-left:42.55319%;*margin-left:42.44681%}.row-fluid .offset6{margin-left:53.19149%;*margin-left:53.08511%}.row-fluid .offset6:first-child{margin-left:51.06383%;*margin-left:50.95745%}.row-fluid .offset7{margin-left:61.70213%;*margin-left:61.59574%}.row-fluid .offset7:first-child{margin-left:59.57447%;*margin-left:59.46809%}.row-fluid .offset8{margin-left:70.21277%;*margin-left:70.10638%}.row-fluid .offset8:first-child{margin-left:68.08511%;*margin-left:67.97872%}.row-fluid .offset9{margin-left:78.7234%;*margin-left:78.61702%}.row-fluid .offset9:first-child{margin-left:76.59574%;*margin-left:76.48936%}.row-fluid .offset10{margin-left:87.23404%;*margin-left:87.12766%}.row-fluid .offset10:first-child{margin-left:85.10638%;*margin-left:85.0%}.row-fluid .offset11{margin-left:95.74468%;*margin-left:95.6383%}.row-fluid .offset11:first-child{margin-left:93.61702%;*margin-left:93.51064%}.row-fluid .offset12{margin-left:104.25532%;*margin-left:104.14894%}.row-fluid .offset12:first-child{margin-left:102.12766%;*margin-left:102.02128%}[class*="span"].hide,.row-fluid [class*="span"].hide{display:none}[class*="span"].pull-right,.row-fluid [class*="span"].pull-right{float:right}.container{margin-right:auto;margin-left:auto;*zoom:1}.container:before,.container:after{display:table;content:"";line-height:0}.container:after{clear:both}.container-fluid{padding-right:20px;padding-left:20px;*zoom:1}.container-fluid:before,.container-fluid:after{display:table;content:"";line-height:0}.container-fluid:after{clear:both}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:21px;font-weight:200;line-height:30px}small{font-size:85%}strong{font-weight:bold}em{font-style:italic}cite{font-style:normal}.muted{color:#e8ebf0}a.muted:hover,a.muted:focus{color:#c9d0dc}.text-warning{color:#c09853}a.text-warning:hover,a.text-warning:focus{color:#a47e3c}.text-error{color:#b94a48}a.text-error:hover,a.text-error:focus{color:#953b39}.text-info{color:#3a87ad}a.text-info:hover,a.text-info:focus{color:#2d6987}.text-success{color:#468847}a.text-success:hover,a.text-success:focus{color:#356635}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}h1,h2,h3,h4,h5,h6{margin:10px 0;font-family:inherit;font-weight:normal;line-height:20px;color:inherit;text-rendering:optimizelegibility}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-weight:normal;line-height:1;color:#e8ebf0}h1,h2,h3{line-height:40px}h1{font-size:38.5px}h2{font-size:31.5px}h3{font-size:24.5px}h4{font-size:17.5px}h5{font-size:14px}h6{font-size:11.9px}h1 small{font-size:24.5px}h2 small{font-size:17.5px}h3 small{font-size:14px}h4 small{font-size:14px}.page-header{padding-bottom:9px;margin:20px 0 30px;border-bottom:1px solid #f8f9fa}ul,ol{padding:0;margin:0 0 10px 25px}ul ul,ul ol,ol ol,ol ul{margin-bottom:0}li{line-height:20px}ul.unstyled,ol.unstyled{margin-left:0;list-style:none}ul.inline,ol.inline{margin-left:0;list-style:none}ul.inline>li,ol.inline>li{display:inline-block;*display:inline;*zoom:1;padding-left:5px;padding-right:5px}dl{margin-bottom:20px}dt,dd{line-height:20px}dt{font-weight:bold}dd{margin-left:10px}.dl-horizontal{*zoom:1}.dl-horizontal:before,.dl-horizontal:after{display:table;content:"";line-height:0}.dl-horizontal:after{clear:both}.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}hr{margin:20px 0;border:0;border-top:1px solid #f8f9fa;border-bottom:1px solid white}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #e8ebf0}abbr.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:0 0 0 15px;margin:0 0 20px;border-left:5px solid #f8f9fa}blockquote p{margin-bottom:0;font-size:17.5px;font-weight:300;line-height:1.25}blockquote small{display:block;line-height:20px;color:#e8ebf0}blockquote small:before{content:'\2014 \00A0'}blockquote.pull-right{float:right;padding-right:15px;padding-left:0;border-right:5px solid #f8f9fa;border-left:0}blockquote.pull-right p,blockquote.pull-right small{text-align:right}blockquote.pull-right small:before{content:''}blockquote.pull-right small:after{content:'\00A0 \2014'}q:before,q:after,blockquote:before,blockquote:after{content:""}address{display:block;margin-bottom:20px;font-style:normal;line-height:20px}form{margin:0 0 20px}fieldset{padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:40px;color:#8b8b8b;border:0;border-bottom:1px solid #e5e5e5}legend small{font-size:15px;color:#e8ebf0}label,input,button,select,textarea{font-size:14px;font-weight:normal;line-height:20px}input,button,select,textarea{font-family:"Varela Round", Helvetica, Arial, sans-serif}label{display:block;margin-bottom:5px}select,textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{display:inline-block;height:20px;padding:4px 6px;margin-bottom:10px;font-size:14px;line-height:20px;color:#979ca6;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;vertical-align:middle}input,textarea,.uneditable-input{width:206px}textarea{height:auto}textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{background-color:white;border:1px solid #cccccc;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border linear 0.2s, box-shadow linear 0.2s;-moz-transition:border linear 0.2s, box-shadow linear 0.2s;-o-transition:border linear 0.2s, box-shadow linear 0.2s;transition:border linear 0.2s, box-shadow linear 0.2s}textarea:focus,input[type="text"]:focus,input[type="password"]:focus,input[type="datetime"]:focus,input[type="datetime-local"]:focus,input[type="date"]:focus,input[type="month"]:focus,input[type="time"]:focus,input[type="week"]:focus,input[type="number"]:focus,input[type="email"]:focus,input[type="url"]:focus,input[type="search"]:focus,input[type="tel"]:focus,input[type="color"]:focus,.uneditable-input:focus{border-color:rgba(82,168,236,0.8);outline:0;outline:thin dotted \9;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6)}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;*margin-top:0;margin-top:1px \9;line-height:normal}input[type="file"],input[type="image"],input[type="submit"],input[type="reset"],input[type="button"],input[type="radio"],input[type="checkbox"]{width:auto}select,input[type="file"]{height:30px;*margin-top:4px;line-height:30px}select{width:220px;border:1px solid #cccccc;background-color:white}select[multiple],select[size]{height:auto}select:focus,input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.uneditable-input,.uneditable-textarea{color:#e8ebf0;background-color:#fcfcfc;border-color:#cccccc;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.025);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.025);box-shadow:inset 0 1px 2px rgba(0,0,0,0.025);cursor:not-allowed}.uneditable-input{overflow:hidden;white-space:nowrap}.uneditable-textarea{width:auto;height:auto}input:-moz-placeholder,textarea:-moz-placeholder{color:#e8ebf0}input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:#e8ebf0}input::-webkit-input-placeholder,textarea::-webkit-input-placeholder{color:#e8ebf0}.radio,.checkbox{min-height:20px;padding-left:20px}.radio input[type="radio"],.checkbox input[type="checkbox"]{float:left;margin-left:-20px}.controls>.radio:first-child,.controls>.checkbox:first-child{padding-top:5px}.radio.inline,.checkbox.inline{display:inline-block;padding-top:5px;margin-bottom:0;vertical-align:middle}.radio.inline+.radio.inline,.checkbox.inline+.checkbox.inline{margin-left:10px}.input-mini{width:60px}.input-small{width:90px}.input-medium{width:150px}.input-large{width:210px}.input-xlarge{width:270px}.input-xxlarge{width:530px}input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input[class*="span"],.row-fluid input[class*="span"],.row-fluid select[class*="span"],.row-fluid textarea[class*="span"],.row-fluid .uneditable-input[class*="span"]{float:none;margin-left:0}.input-append input[class*="span"],.input-append .uneditable-input[class*="span"],.input-prepend input[class*="span"],.input-prepend .uneditable-input[class*="span"],.row-fluid input[class*="span"],.row-fluid select[class*="span"],.row-fluid textarea[class*="span"],.row-fluid .uneditable-input[class*="span"],.row-fluid .input-prepend [class*="span"],.row-fluid .input-append [class*="span"]{display:inline-block}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span1,textarea.span1,.uneditable-input.span1{width:46px}input.span2,textarea.span2,.uneditable-input.span2{width:126px}input.span3,textarea.span3,.uneditable-input.span3{width:206px}input.span4,textarea.span4,.uneditable-input.span4{width:286px}input.span5,textarea.span5,.uneditable-input.span5{width:366px}input.span6,textarea.span6,.uneditable-input.span6{width:446px}input.span7,textarea.span7,.uneditable-input.span7{width:526px}input.span8,textarea.span8,.uneditable-input.span8{width:606px}input.span9,textarea.span9,.uneditable-input.span9{width:686px}input.span10,textarea.span10,.uneditable-input.span10{width:766px}input.span11,textarea.span11,.uneditable-input.span11{width:846px}input.span12,textarea.span12,.uneditable-input.span12{width:926px}.controls-row{*zoom:1}.controls-row:before,.controls-row:after{display:table;content:"";line-height:0}.controls-row:after{clear:both}.controls-row [class*="span"],.row-fluid .controls-row [class*="span"]{float:left}.controls-row .checkbox[class*="span"],.controls-row .radio[class*="span"]{padding-top:5px}input[disabled],select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{cursor:not-allowed;background-color:#f8f9fa}input[type="radio"][disabled],input[type="checkbox"][disabled],input[type="radio"][readonly],input[type="checkbox"][readonly]{background-color:transparent}.control-group.warning .control-label,.control-group.warning .help-block,.control-group.warning .help-inline{color:#c09853}.control-group.warning .checkbox,.control-group.warning .radio,.control-group.warning input,.control-group.warning select,.control-group.warning textarea{color:#c09853}.control-group.warning input,.control-group.warning select,.control-group.warning textarea{border-color:#c09853;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.warning input:focus,.control-group.warning select:focus,.control-group.warning textarea:focus{border-color:#a47e3c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e}.control-group.warning .input-prepend .add-on,.control-group.warning .input-append .add-on{color:#c09853;background-color:#fcf8e3;border-color:#c09853}.control-group.error .control-label,.control-group.error .help-block,.control-group.error .help-inline{color:#b94a48}.control-group.error .checkbox,.control-group.error .radio,.control-group.error input,.control-group.error select,.control-group.error textarea{color:#b94a48}.control-group.error input,.control-group.error select,.control-group.error textarea{border-color:#b94a48;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.error input:focus,.control-group.error select:focus,.control-group.error textarea:focus{border-color:#953b39;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392}.control-group.error .input-prepend .add-on,.control-group.error .input-append .add-on{color:#b94a48;background-color:#f2dede;border-color:#b94a48}.control-group.success .control-label,.control-group.success .help-block,.control-group.success .help-inline{color:#468847}.control-group.success .checkbox,.control-group.success .radio,.control-group.success input,.control-group.success select,.control-group.success textarea{color:#468847}.control-group.success input,.control-group.success select,.control-group.success textarea{border-color:#468847;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.success input:focus,.control-group.success select:focus,.control-group.success textarea:focus{border-color:#356635;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b}.control-group.success .input-prepend .add-on,.control-group.success .input-append .add-on{color:#468847;background-color:#dff0d8;border-color:#468847}.control-group.info .control-label,.control-group.info .help-block,.control-group.info .help-inline{color:#3a87ad}.control-group.info .checkbox,.control-group.info .radio,.control-group.info input,.control-group.info select,.control-group.info textarea{color:#3a87ad}.control-group.info input,.control-group.info select,.control-group.info textarea{border-color:#3a87ad;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.info input:focus,.control-group.info select:focus,.control-group.info textarea:focus{border-color:#2d6987;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3}.control-group.info .input-prepend .add-on,.control-group.info .input-append .add-on{color:#3a87ad;background-color:#d9edf7;border-color:#3a87ad}input:focus:invalid,textarea:focus:invalid,select:focus:invalid{color:#b94a48;border-color:#ee5f5b}input:focus:invalid:focus,textarea:focus:invalid:focus,select:focus:invalid:focus{border-color:#e9322d;-webkit-box-shadow:0 0 6px #f8b9b7;-moz-box-shadow:0 0 6px #f8b9b7;box-shadow:0 0 6px #f8b9b7}.form-actions{padding:19px 20px 20px;margin-top:20px;margin-bottom:20px;background-color:whitesmoke;border-top:1px solid #e5e5e5;*zoom:1}.form-actions:before,.form-actions:after{display:table;content:"";line-height:0}.form-actions:after{clear:both}.help-block,.help-inline{color:#687288}.help-block{display:block;margin-bottom:10px}.help-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle;padding-left:5px}.input-append,.input-prepend{display:inline-block;margin-bottom:10px;vertical-align:middle;font-size:0;white-space:nowrap}.input-append input,.input-append select,.input-append .uneditable-input,.input-append .dropdown-menu,.input-append .popover,.input-prepend input,.input-prepend select,.input-prepend .uneditable-input,.input-prepend .dropdown-menu,.input-prepend .popover{font-size:14px}.input-append input,.input-append select,.input-append .uneditable-input,.input-prepend input,.input-prepend select,.input-prepend .uneditable-input{position:relative;margin-bottom:0;*margin-left:0;vertical-align:top;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-append input:focus,.input-append select:focus,.input-append .uneditable-input:focus,.input-prepend input:focus,.input-prepend select:focus,.input-prepend .uneditable-input:focus{z-index:2}.input-append .add-on,.input-prepend .add-on{display:inline-block;width:auto;height:20px;min-width:16px;padding:4px 5px;font-size:14px;font-weight:normal;line-height:20px;text-align:center;text-shadow:0 1px 0 white;background-color:#f8f9fa;border:1px solid #ccc}.input-append .add-on,.input-append .btn,.input-append .btn-group>.dropdown-toggle,.input-prepend .add-on,.input-prepend .btn,.input-prepend .btn-group>.dropdown-toggle{vertical-align:top;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.input-append .active,.input-prepend .active{background-color:#d4e4bd;border-color:#8db850}.input-prepend .add-on,.input-prepend .btn{margin-right:-1px}.input-prepend .add-on:first-child,.input-prepend .btn:first-child{-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.input-append input,.input-append select,.input-append .uneditable-input{-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.input-append input+.btn-group .btn:last-child,.input-append select+.btn-group .btn:last-child,.input-append .uneditable-input+.btn-group .btn:last-child{-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-append .add-on,.input-append .btn,.input-append .btn-group{margin-left:-1px}.input-append .add-on:last-child,.input-append .btn:last-child,.input-append .btn-group:last-child>.dropdown-toggle{-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-prepend.input-append input,.input-prepend.input-append select,.input-prepend.input-append .uneditable-input{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.input-prepend.input-append input+.btn-group .btn,.input-prepend.input-append select+.btn-group .btn,.input-prepend.input-append .uneditable-input+.btn-group .btn{-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-prepend.input-append .add-on:first-child,.input-prepend.input-append .btn:first-child{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.input-prepend.input-append .add-on:last-child,.input-prepend.input-append .btn:last-child{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-prepend.input-append .btn-group:first-child{margin-left:0}input.search-query{padding-right:14px;padding-right:4px \9;padding-left:14px;padding-left:4px \9;margin-bottom:0;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.form-search .input-append .search-query,.form-search .input-prepend .search-query{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.form-search .input-append .search-query{-webkit-border-radius:14px 0 0 14px;-moz-border-radius:14px 0 0 14px;border-radius:14px 0 0 14px}.form-search .input-append .btn{-webkit-border-radius:0 14px 14px 0;-moz-border-radius:0 14px 14px 0;border-radius:0 14px 14px 0}.form-search .input-prepend .search-query{-webkit-border-radius:0 14px 14px 0;-moz-border-radius:0 14px 14px 0;border-radius:0 14px 14px 0}.form-search .input-prepend .btn{-webkit-border-radius:14px 0 0 14px;-moz-border-radius:14px 0 0 14px;border-radius:14px 0 0 14px}.form-search input,.form-search textarea,.form-search select,.form-search .help-inline,.form-search .uneditable-input,.form-search .input-prepend,.form-search .input-append,.form-inline input,.form-inline textarea,.form-inline select,.form-inline .help-inline,.form-inline .uneditable-input,.form-inline .input-prepend,.form-inline .input-append,.form-horizontal input,.form-horizontal textarea,.form-horizontal select,.form-horizontal .help-inline,.form-horizontal .uneditable-input,.form-horizontal .input-prepend,.form-horizontal .input-append{display:inline-block;*display:inline;*zoom:1;margin-bottom:0;vertical-align:middle}.form-search .hide,.form-inline .hide,.form-horizontal .hide{display:none}.form-search label,.form-inline label,.form-search .btn-group,.form-inline .btn-group{display:inline-block}.form-search .input-append,.form-inline .input-append,.form-search .input-prepend,.form-inline .input-prepend{margin-bottom:0}.form-search .radio,.form-search .checkbox,.form-inline .radio,.form-inline .checkbox{padding-left:0;margin-bottom:0;vertical-align:middle}.form-search .radio input[type="radio"],.form-search .checkbox input[type="checkbox"],.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{float:left;margin-right:3px;margin-left:0}.control-group{margin-bottom:10px}legend+.control-group{margin-top:20px;-webkit-margin-top-collapse:separate}.form-horizontal .control-group{margin-bottom:20px;*zoom:1}.form-horizontal .control-group:before,.form-horizontal .control-group:after{display:table;content:"";line-height:0}.form-horizontal .control-group:after{clear:both}.form-horizontal .control-label{float:left;width:160px;padding-top:5px;text-align:right}.form-horizontal .controls{*display:inline-block;*padding-left:20px;margin-left:180px;*margin-left:0}.form-horizontal .controls:first-child{*padding-left:180px}.form-horizontal .help-block{margin-bottom:0}.form-horizontal input+.help-block,.form-horizontal select+.help-block,.form-horizontal textarea+.help-block,.form-horizontal .uneditable-input+.help-block,.form-horizontal .input-prepend+.help-block,.form-horizontal .input-append+.help-block{margin-top:10px}.form-horizontal .form-actions{padding-left:180px}table{max-width:100%;background-color:transparent;border-collapse:collapse;border-spacing:0}.table{width:100%;margin-bottom:20px}.table th,.table td{padding:8px;line-height:20px;text-align:left;vertical-align:top;border-top:1px solid #dddddd}.table th{font-weight:bold}.table thead th{vertical-align:bottom}.table caption+thead tr:first-child th,.table caption+thead tr:first-child td,.table colgroup+thead tr:first-child th,.table colgroup+thead tr:first-child td,.table thead:first-child tr:first-child th,.table thead:first-child tr:first-child td{border-top:0}.table tbody+tbody{border-top:2px solid #dddddd}.table .table{background-color:#329897}.table-condensed th,.table-condensed td{padding:4px 5px}.table-bordered{border:1px solid #dddddd;border-collapse:separate;*border-collapse:collapse;border-left:0;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.table-bordered th,.table-bordered td{border-left:1px solid #dddddd}.table-bordered caption+thead tr:first-child th,.table-bordered caption+tbody tr:first-child th,.table-bordered caption+tbody tr:first-child td,.table-bordered colgroup+thead tr:first-child th,.table-bordered colgroup+tbody tr:first-child th,.table-bordered colgroup+tbody tr:first-child td,.table-bordered thead:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child td{border-top:0}.table-bordered thead:first-child tr:first-child>th:first-child,.table-bordered tbody:first-child tr:first-child>td:first-child,.table-bordered tbody:first-child tr:first-child>th:first-child{-webkit-border-top-left-radius:4px;-moz-border-radius-topleft:4px;border-top-left-radius:4px}.table-bordered thead:first-child tr:first-child>th:last-child,.table-bordered tbody:first-child tr:first-child>td:last-child,.table-bordered tbody:first-child tr:first-child>th:last-child{-webkit-border-top-right-radius:4px;-moz-border-radius-topright:4px;border-top-right-radius:4px}.table-bordered thead:last-child tr:last-child>th:first-child,.table-bordered tbody:last-child tr:last-child>td:first-child,.table-bordered tbody:last-child tr:last-child>th:first-child,.table-bordered tfoot:last-child tr:last-child>td:first-child,.table-bordered tfoot:last-child tr:last-child>th:first-child{-webkit-border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px;border-bottom-left-radius:4px}.table-bordered thead:last-child tr:last-child>th:last-child,.table-bordered tbody:last-child tr:last-child>td:last-child,.table-bordered tbody:last-child tr:last-child>th:last-child,.table-bordered tfoot:last-child tr:last-child>td:last-child,.table-bordered tfoot:last-child tr:last-child>th:last-child{-webkit-border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px;border-bottom-right-radius:4px}.table-bordered tfoot+tbody:last-child tr:last-child td:first-child{-webkit-border-bottom-left-radius:0;-moz-border-radius-bottomleft:0;border-bottom-left-radius:0}.table-bordered tfoot+tbody:last-child tr:last-child td:last-child{-webkit-border-bottom-right-radius:0;-moz-border-radius-bottomright:0;border-bottom-right-radius:0}.table-bordered caption+thead tr:first-child th:first-child,.table-bordered caption+tbody tr:first-child td:first-child,.table-bordered colgroup+thead tr:first-child th:first-child,.table-bordered colgroup+tbody tr:first-child td:first-child{-webkit-border-top-left-radius:4px;-moz-border-radius-topleft:4px;border-top-left-radius:4px}.table-bordered caption+thead tr:first-child th:last-child,.table-bordered caption+tbody tr:first-child td:last-child,.table-bordered colgroup+thead tr:first-child th:last-child,.table-bordered colgroup+tbody tr:first-child td:last-child{-webkit-border-top-right-radius:4px;-moz-border-radius-topright:4px;border-top-right-radius:4px}.table-striped tbody>tr:nth-child(odd)>td,.table-striped tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}.table-hover tbody tr:hover>td,.table-hover tbody tr:hover>th{background-color:whitesmoke}table td[class*="span"],table th[class*="span"],.row-fluid table td[class*="span"],.row-fluid table th[class*="span"]{display:table-cell;float:none;margin-left:0}.table td.span1,.table th.span1{float:none;width:44px;margin-left:0}.table td.span2,.table th.span2{float:none;width:124px;margin-left:0}.table td.span3,.table th.span3{float:none;width:204px;margin-left:0}.table td.span4,.table th.span4{float:none;width:284px;margin-left:0}.table td.span5,.table th.span5{float:none;width:364px;margin-left:0}.table td.span6,.table th.span6{float:none;width:444px;margin-left:0}.table td.span7,.table th.span7{float:none;width:524px;margin-left:0}.table td.span8,.table th.span8{float:none;width:604px;margin-left:0}.table td.span9,.table th.span9{float:none;width:684px;margin-left:0}.table td.span10,.table th.span10{float:none;width:764px;margin-left:0}.table td.span11,.table th.span11{float:none;width:844px;margin-left:0}.table td.span12,.table th.span12{float:none;width:924px;margin-left:0}.table tbody tr.success>td{background-color:#dff0d8}.table tbody tr.error>td{background-color:#f2dede}.table tbody tr.warning>td{background-color:#fcf8e3}.table tbody tr.info>td{background-color:#d9edf7}.table-hover tbody tr.success:hover>td{background-color:#d0e9c6}.table-hover tbody tr.error:hover>td{background-color:#ebcccc}.table-hover tbody tr.warning:hover>td{background-color:#faf2cc}.table-hover tbody tr.info:hover>td{background-color:#c4e3f3}[class^="icon-"],[class*=" icon-"]{display:inline-block;width:14px;height:14px;*margin-right:.3em;line-height:14px;vertical-align:text-top;background-image:url("/assets/glyphicons-halflings-33fd5142f0d8989dd05bb0b3e07047a3.png");background-position:14px 14px;background-repeat:no-repeat;margin-top:1px}.icon-white,.nav-pills>.active>a>[class^="icon-"],.nav-pills>.active>a>[class*=" icon-"],.nav-list>.active>a>[class^="icon-"],.nav-list>.active>a>[class*=" icon-"],.navbar-inverse .nav>.active>a>[class^="icon-"],.navbar-inverse .nav>.active>a>[class*=" icon-"],.dropdown-menu>li>a:hover>[class^="icon-"],.dropdown-menu>li>a:focus>[class^="icon-"],.dropdown-menu>li>a:hover>[class*=" icon-"],.dropdown-menu>li>a:focus>[class*=" icon-"],.dropdown-menu>.active>a>[class^="icon-"],.dropdown-menu>.active>a>[class*=" icon-"],.dropdown-submenu:hover>a>[class^="icon-"],.dropdown-submenu:focus>a>[class^="icon-"],.dropdown-submenu:hover>a>[class*=" icon-"],.dropdown-submenu:focus>a>[class*=" icon-"]{background-image:url("/assets/glyphicons-halflings-white-b24ef881bcd5c1b763bd67c3bfea6620.png")}.icon-glass{background-position:0 0}.icon-music{background-position:-24px 0}.icon-search{background-position:-48px 0}.icon-envelope{background-position:-72px 0}.icon-heart{background-position:-96px 0}.icon-star{background-position:-120px 0}.icon-star-empty{background-position:-144px 0}.icon-user{background-position:-168px 0}.icon-film{background-position:-192px 0}.icon-th-large{background-position:-216px 0}.icon-th{background-position:-240px 0}.icon-th-list{background-position:-264px 0}.icon-ok{background-position:-288px 0}.icon-remove{background-position:-312px 0}.icon-zoom-in{background-position:-336px 0}.icon-zoom-out{background-position:-360px 0}.icon-off{background-position:-384px 0}.icon-signal{background-position:-408px 0}.icon-cog{background-position:-432px 0}.icon-trash{background-position:-456px 0}.icon-home{background-position:0 -24px}.icon-file{background-position:-24px -24px}.icon-time{background-position:-48px -24px}.icon-road{background-position:-72px -24px}.icon-download-alt{background-position:-96px -24px}.icon-download{background-position:-120px -24px}.icon-upload{background-position:-144px -24px}.icon-inbox{background-position:-168px -24px}.icon-play-circle{background-position:-192px -24px}.icon-repeat{background-position:-216px -24px}.icon-refresh{background-position:-240px -24px}.icon-list-alt{background-position:-264px -24px}.icon-lock{background-position:-287px -24px}.icon-flag{background-position:-312px -24px}.icon-headphones{background-position:-336px -24px}.icon-volume-off{background-position:-360px -24px}.icon-volume-down{background-position:-384px -24px}.icon-volume-up{background-position:-408px -24px}.icon-qrcode{background-position:-432px -24px}.icon-barcode{background-position:-456px -24px}.icon-tag{background-position:0 -48px}.icon-tags{background-position:-25px -48px}.icon-book{background-position:-48px -48px}.icon-bookmark{background-position:-72px -48px}.icon-print{background-position:-96px -48px}.icon-camera{background-position:-120px -48px}.icon-font{background-position:-144px -48px}.icon-bold{background-position:-167px -48px}.icon-italic{background-position:-192px -48px}.icon-text-height{background-position:-216px -48px}.icon-text-width{background-position:-240px -48px}.icon-align-left{background-position:-264px -48px}.icon-align-center{background-position:-288px -48px}.icon-align-right{background-position:-312px -48px}.icon-align-justify{background-position:-336px -48px}.icon-list{background-position:-360px -48px}.icon-indent-left{background-position:-384px -48px}.icon-indent-right{background-position:-408px -48px}.icon-facetime-video{background-position:-432px -48px}.icon-picture{background-position:-456px -48px}.icon-pencil{background-position:0 -72px}.icon-map-marker{background-position:-24px -72px}.icon-adjust{background-position:-48px -72px}.icon-tint{background-position:-72px -72px}.icon-edit{background-position:-96px -72px}.icon-share{background-position:-120px -72px}.icon-check{background-position:-144px -72px}.icon-move{background-position:-168px -72px}.icon-step-backward{background-position:-192px -72px}.icon-fast-backward{background-position:-216px -72px}.icon-backward{background-position:-240px -72px}.icon-play{background-position:-264px -72px}.icon-pause{background-position:-288px -72px}.icon-stop{background-position:-312px -72px}.icon-forward{background-position:-336px -72px}.icon-fast-forward{background-position:-360px -72px}.icon-step-forward{background-position:-384px -72px}.icon-eject{background-position:-408px -72px}.icon-chevron-left{background-position:-432px -72px}.icon-chevron-right{background-position:-456px -72px}.icon-plus-sign{background-position:0 -96px}.icon-minus-sign{background-position:-24px -96px}.icon-remove-sign{background-position:-48px -96px}.icon-ok-sign{background-position:-72px -96px}.icon-question-sign{background-position:-96px -96px}.icon-info-sign{background-position:-120px -96px}.icon-screenshot{background-position:-144px -96px}.icon-remove-circle{background-position:-168px -96px}.icon-ok-circle{background-position:-192px -96px}.icon-ban-circle{background-position:-216px -96px}.icon-arrow-left{background-position:-240px -96px}.icon-arrow-right{background-position:-264px -96px}.icon-arrow-up{background-position:-289px -96px}.icon-arrow-down{background-position:-312px -96px}.icon-share-alt{background-position:-336px -96px}.icon-resize-full{background-position:-360px -96px}.icon-resize-small{background-position:-384px -96px}.icon-plus{background-position:-408px -96px}.icon-minus{background-position:-433px -96px}.icon-asterisk{background-position:-456px -96px}.icon-exclamation-sign{background-position:0 -120px}.icon-gift{background-position:-24px -120px}.icon-leaf{background-position:-48px -120px}.icon-fire{background-position:-72px -120px}.icon-eye-open{background-position:-96px -120px}.icon-eye-close{background-position:-120px -120px}.icon-warning-sign{background-position:-144px -120px}.icon-plane{background-position:-168px -120px}.icon-calendar{background-position:-192px -120px}.icon-random{background-position:-216px -120px;width:16px}.icon-comment{background-position:-240px -120px}.icon-magnet{background-position:-264px -120px}.icon-chevron-up{background-position:-288px -120px}.icon-chevron-down{background-position:-313px -119px}.icon-retweet{background-position:-336px -120px}.icon-shopping-cart{background-position:-360px -120px}.icon-folder-close{background-position:-384px -120px;width:16px}.icon-folder-open{background-position:-408px -120px;width:16px}.icon-resize-vertical{background-position:-432px -119px}.icon-resize-horizontal{background-position:-456px -118px}.icon-hdd{background-position:0 -144px}.icon-bullhorn{background-position:-24px -144px}.icon-bell{background-position:-48px -144px}.icon-certificate{background-position:-72px -144px}.icon-thumbs-up{background-position:-96px -144px}.icon-thumbs-down{background-position:-120px -144px}.icon-hand-right{background-position:-144px -144px}.icon-hand-left{background-position:-168px -144px}.icon-hand-up{background-position:-192px -144px}.icon-hand-down{background-position:-216px -144px}.icon-circle-arrow-right{background-position:-240px -144px}.icon-circle-arrow-left{background-position:-264px -144px}.icon-circle-arrow-up{background-position:-288px -144px}.icon-circle-arrow-down{background-position:-312px -144px}.icon-globe{background-position:-336px -144px}.icon-wrench{background-position:-360px -144px}.icon-tasks{background-position:-384px -144px}.icon-filter{background-position:-408px -144px}.icon-briefcase{background-position:-432px -144px}.icon-fullscreen{background-position:-456px -144px}.dropup,.dropdown{position:relative}.dropdown-toggle{*margin-bottom:-3px}.dropdown-toggle:active,.open .dropdown-toggle{outline:0}.caret{display:inline-block;width:0;height:0;vertical-align:top;border-top:4px solid #474e5d;border-right:4px solid transparent;border-left:4px solid transparent;content:""}.dropdown .caret{margin-top:8px;margin-left:2px}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;background-color:white;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);*border-right-width:2px;*border-bottom-width:2px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{*width:100%;height:1px;margin:9px 1px;*margin:-5px 0 5px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid white}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:20px;color:#8b8b8b;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus,.dropdown-submenu:hover>a,.dropdown-submenu:focus>a{text-decoration:none;color:white;background-color:#2f908f;background-image:-moz-linear-gradient(top, #329897, #2c8584);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#329897), to(#2c8584));background-image:-webkit-linear-gradient(top, #329897, #2c8584);background-image:-o-linear-gradient(top, #329897, #2c8584);background-image:linear-gradient(to bottom, #329897,#2c8584);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FF329897', endColorstr='#FF2C8584', GradientType=0)}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:white;text-decoration:none;outline:0;background-color:#2f908f;background-image:-moz-linear-gradient(top, #329897, #2c8584);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#329897), to(#2c8584));background-image:-webkit-linear-gradient(top, #329897, #2c8584);background-image:-o-linear-gradient(top, #329897, #2c8584);background-image:linear-gradient(to bottom, #329897,#2c8584);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FF329897', endColorstr='#FF2C8584', GradientType=0)}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#e8ebf0}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);cursor:default}.open{*z-index:1000}.open>.dropdown-menu{display:block}.dropdown-backdrop{position:fixed;left:0;right:0;bottom:0;top:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid #474e5d;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}.dropdown-submenu{position:relative}.dropdown-submenu>.dropdown-menu{top:0;left:100%;margin-top:-6px;margin-left:-1px;-webkit-border-radius:0 6px 6px 6px;-moz-border-radius:0 6px 6px 6px;border-radius:0 6px 6px 6px}.dropdown-submenu:hover>.dropdown-menu{display:block}.dropup .dropdown-submenu>.dropdown-menu{top:auto;bottom:0;margin-top:0;margin-bottom:-2px;-webkit-border-radius:5px 5px 5px 0;-moz-border-radius:5px 5px 5px 0;border-radius:5px 5px 5px 0}.dropdown-submenu>a:after{display:block;content:" ";float:right;width:0;height:0;border-color:transparent;border-style:solid;border-width:5px 0 5px 5px;border-left-color:#cccccc;margin-top:5px;margin-right:-10px}.dropdown-submenu:hover>a:after{border-left-color:white}.dropdown-submenu.pull-left{float:none}.dropdown-submenu.pull-left>.dropdown-menu{left:-100%;margin-left:10px;-webkit-border-radius:6px 0 6px 6px;-moz-border-radius:6px 0 6px 6px;border-radius:6px 0 6px 6px}.dropdown .dropdown-menu .nav-header{padding-left:20px;padding-right:20px}.typeahead{z-index:1051;margin-top:2px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:whitesmoke;border:1px solid #e3e3e3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-large{padding:24px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.well-small{padding:9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.fade{opacity:0;-webkit-transition:opacity 0.15s linear;-moz-transition:opacity 0.15s linear;-o-transition:opacity 0.15s linear;transition:opacity 0.15s linear}.fade.in{opacity:1}.collapse{position:relative;height:0;overflow:hidden;-webkit-transition:height 0.35s ease;-moz-transition:height 0.35s ease;-o-transition:height 0.35s ease;transition:height 0.35s ease}.collapse.in{height:auto}.close{float:right;font-size:20px;font-weight:bold;line-height:20px;color:#474e5d;text-shadow:0 1px 0 white;opacity:0.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#474e5d;text-decoration:none;cursor:pointer;opacity:0.4;filter:alpha(opacity=40)}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.btn{display:inline-block;*display:inline;*zoom:1;padding:4px 12px;margin-bottom:0;font-size:14px;line-height:20px;text-align:center;vertical-align:middle;cursor:pointer;color:#8b8b8b;text-shadow:0 1px 1px rgba(255,255,255,0.75);background-color:whitesmoke;background-image:-moz-linear-gradient(top, #fff, #e6e6e6);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#fff), to(#e6e6e6));background-image:-webkit-linear-gradient(top, #fff, #e6e6e6);background-image:-o-linear-gradient(top, #fff, #e6e6e6);background-image:linear-gradient(to bottom, #ffffff,#e6e6e6);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFE6E6E6', GradientType=0);border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#e6e6e6;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);border:1px solid #cccccc;*border:0;border-bottom-color:#b3b3b3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;*margin-left:.3em;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05)}.btn:hover,.btn:focus,.btn:active,.btn.active,.btn.disabled,.btn[disabled]{color:#8b8b8b;background-color:#e6e6e6;*background-color:#d9d9d9}.btn:active,.btn.active{background-color:#cccccc \9}.btn:first-child{*margin-left:0}.btn:hover,.btn:focus{color:#8b8b8b;text-decoration:none;background-position:0 -15px;-webkit-transition:background-position 0.1s linear;-moz-transition:background-position 0.1s linear;-o-transition:background-position 0.1s linear;transition:background-position 0.1s linear}.btn:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05)}.btn.disabled,.btn[disabled]{cursor:default;background-image:none;opacity:0.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn-large{padding:11px 19px;font-size:17.5px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.btn-large [class^="icon-"],.btn-large [class*=" icon-"]{margin-top:4px}.btn-small{padding:2px 10px;font-size:11.9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.btn-small [class^="icon-"],.btn-small [class*=" icon-"]{margin-top:0}.btn-mini [class^="icon-"],.btn-mini [class*=" icon-"]{margin-top:-1px}.btn-mini{padding:0px 6px;font-size:10.5px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.btn-block{display:block;width:100%;padding-left:0;padding-right:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.btn-primary.active,.btn-warning.active,.btn-danger.active,.btn-success.active,.btn-info.active,.btn-inverse.active{color:rgba(255,255,255,0.75)}.btn-primary{color:white;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#328a97;background-image:-moz-linear-gradient(top, #329897, #327798);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#329897), to(#327798));background-image:-webkit-linear-gradient(top, #329897, #327798);background-image:-o-linear-gradient(top, #329897, #327798);background-image:linear-gradient(to bottom, #329897,#327798);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FF329897', endColorstr='#FF327798', GradientType=0);border-color:#327798 #327798 #1f4a5e;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#327798;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.btn-primary.disabled,.btn-primary[disabled]{color:white;background-color:#327798;*background-color:#2c6885}.btn-primary:active,.btn-primary.active{background-color:#255972 \9}.btn-warning{color:white;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#e39880;background-image:-moz-linear-gradient(top, #e9ad9a, #dc795b);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#e9ad9a), to(#dc795b));background-image:-webkit-linear-gradient(top, #e9ad9a, #dc795b);background-image:-o-linear-gradient(top, #e9ad9a, #dc795b);background-image:linear-gradient(to bottom, #e9ad9a,#dc795b);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFE9AD9A', endColorstr='#FFDC795B', GradientType=0);border-color:#dc795b #dc795b #c14d29;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#dc795b;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.btn-warning.disabled,.btn-warning[disabled]{color:white;background-color:#dc795b;*background-color:#d86846}.btn-warning:active,.btn-warning.active{background-color:#d35731 \9}.btn-danger{color:white;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#da4e49;background-image:-moz-linear-gradient(top, #ee5f5b, #bd362f);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f));background-image:-webkit-linear-gradient(top, #ee5f5b, #bd362f);background-image:-o-linear-gradient(top, #ee5f5b, #bd362f);background-image:linear-gradient(to bottom, #ee5f5b,#bd362f);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEE5F5B', endColorstr='#FFBD362F', GradientType=0);border-color:#bd362f #bd362f #802420;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#bd362f;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.btn-danger.disabled,.btn-danger[disabled]{color:white;background-color:#bd362f;*background-color:#a9302a}.btn-danger:active,.btn-danger.active{background-color:#942a25 \9}.btn-success{color:white;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#5bb65b;background-image:-moz-linear-gradient(top, #62c462, #51a351);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351));background-image:-webkit-linear-gradient(top, #62c462, #51a351);background-image:-o-linear-gradient(top, #62c462, #51a351);background-image:linear-gradient(to bottom, #62c462,#51a351);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FF62C462', endColorstr='#FF51A351', GradientType=0);border-color:#51a351 #51a351 #387038;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#51a351;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.btn-success.disabled,.btn-success[disabled]{color:white;background-color:#51a351;*background-color:#499249}.btn-success:active,.btn-success.active{background-color:#408140 \9}.btn-info{color:white;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#49afcd;background-image:-moz-linear-gradient(top, #5bc0de, #2f96b4);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f96b4));background-image:-webkit-linear-gradient(top, #5bc0de, #2f96b4);background-image:-o-linear-gradient(top, #5bc0de, #2f96b4);background-image:linear-gradient(to bottom, #5bc0de,#2f96b4);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FF5BC0DE', endColorstr='#FF2F96B4', GradientType=0);border-color:#2f96b4 #2f96b4 #1f6377;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#2f96b4;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.btn-info.disabled,.btn-info[disabled]{color:white;background-color:#2f96b4;*background-color:#2a85a0}.btn-info:active,.btn-info.active{background-color:#24748c \9}.btn-inverse{color:white;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#595b5f;background-image:-moz-linear-gradient(top, #444, #797e89);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#444), to(#797e89));background-image:-webkit-linear-gradient(top, #444, #797e89);background-image:-o-linear-gradient(top, #444, #797e89);background-image:linear-gradient(to bottom, #444444,#797e89);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FF444444', endColorstr='#FF797E89', GradientType=0);border-color:#797e89 #797e89 #555961;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#797e89;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-inverse:hover,.btn-inverse:focus,.btn-inverse:active,.btn-inverse.active,.btn-inverse.disabled,.btn-inverse[disabled]{color:white;background-color:#797e89;*background-color:#6d717c}.btn-inverse:active,.btn-inverse.active{background-color:#61656e \9}button.btn,input[type="submit"].btn{*padding-top:3px;*padding-bottom:3px}button.btn::-moz-focus-inner,input[type="submit"].btn::-moz-focus-inner{padding:0;border:0}button.btn.btn-large,input[type="submit"].btn.btn-large{*padding-top:7px;*padding-bottom:7px}button.btn.btn-small,input[type="submit"].btn.btn-small{*padding-top:3px;*padding-bottom:3px}button.btn.btn-mini,input[type="submit"].btn.btn-mini{*padding-top:1px;*padding-bottom:1px}.btn-link,.btn-link:active,.btn-link[disabled]{background-color:transparent;background-image:none;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn-link{border-color:transparent;cursor:pointer;color:#329897;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-link:hover,.btn-link:focus{color:#6a396f;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,.btn-link[disabled]:focus{color:#8b8b8b;text-decoration:none}.btn-group{position:relative;display:inline-block;*display:inline;*zoom:1;font-size:0;vertical-align:middle;white-space:nowrap;*margin-left:.3em}.btn-group:first-child{*margin-left:0}.btn-group+.btn-group{margin-left:5px}.btn-toolbar{font-size:0;margin-top:10px;margin-bottom:10px}.btn-toolbar>.btn+.btn,.btn-toolbar>.btn-group+.btn,.btn-toolbar>.btn+.btn-group{margin-left:5px}.btn-group>.btn{position:relative;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-group>.btn+.btn{margin-left:-1px}.btn-group>.btn,.btn-group>.dropdown-menu,.btn-group>.popover{font-size:14px}.btn-group>.btn-mini{font-size:10.5px}.btn-group>.btn-small{font-size:11.9px}.btn-group>.btn-large{font-size:17.5px}.btn-group>.btn:first-child{margin-left:0;-webkit-border-top-left-radius:4px;-moz-border-radius-topleft:4px;border-top-left-radius:4px;-webkit-border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px;border-bottom-left-radius:4px}.btn-group>.btn:last-child,.btn-group>.dropdown-toggle{-webkit-border-top-right-radius:4px;-moz-border-radius-topright:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px;border-bottom-right-radius:4px}.btn-group>.btn.large:first-child{margin-left:0;-webkit-border-top-left-radius:6px;-moz-border-radius-topleft:6px;border-top-left-radius:6px;-webkit-border-bottom-left-radius:6px;-moz-border-radius-bottomleft:6px;border-bottom-left-radius:6px}.btn-group>.btn.large:last-child,.btn-group>.large.dropdown-toggle{-webkit-border-top-right-radius:6px;-moz-border-radius-topright:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;-moz-border-radius-bottomright:6px;border-bottom-right-radius:6px}.btn-group>.btn:hover,.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active{z-index:2}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-left:8px;padding-right:8px;-webkit-box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);*padding-top:5px;*padding-bottom:5px}.btn-group>.btn-mini+.dropdown-toggle{padding-left:5px;padding-right:5px;*padding-top:2px;*padding-bottom:2px}.btn-group>.btn-small+.dropdown-toggle{*padding-top:5px;*padding-bottom:4px}.btn-group>.btn-large+.dropdown-toggle{padding-left:12px;padding-right:12px;*padding-top:7px;*padding-bottom:7px}.btn-group.open .dropdown-toggle{background-image:none;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05)}.btn-group.open .btn.dropdown-toggle{background-color:#e6e6e6}.btn-group.open .btn-primary.dropdown-toggle{background-color:#327798}.btn-group.open .btn-warning.dropdown-toggle{background-color:#dc795b}.btn-group.open .btn-danger.dropdown-toggle{background-color:#bd362f}.btn-group.open .btn-success.dropdown-toggle{background-color:#51a351}.btn-group.open .btn-info.dropdown-toggle{background-color:#2f96b4}.btn-group.open .btn-inverse.dropdown-toggle{background-color:#797e89}.btn .caret{margin-top:8px;margin-left:0}.btn-large .caret{margin-top:6px}.btn-large .caret{border-left-width:5px;border-right-width:5px;border-top-width:5px}.btn-mini .caret,.btn-small .caret{margin-top:8px}.dropup .btn-large .caret{border-bottom-width:5px}.btn-primary .caret,.btn-warning .caret,.btn-danger .caret,.btn-info .caret,.btn-success .caret,.btn-inverse .caret{border-top-color:white;border-bottom-color:white}.btn-group-vertical{display:inline-block;*display:inline;*zoom:1}.btn-group-vertical>.btn{display:block;float:none;max-width:100%;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-group-vertical>.btn+.btn{margin-left:0;margin-top:-1px}.btn-group-vertical>.btn:first-child{-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0}.btn-group-vertical>.btn:last-child{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px}.btn-group-vertical>.btn-large:first-child{-webkit-border-radius:6px 6px 0 0;-moz-border-radius:6px 6px 0 0;border-radius:6px 6px 0 0}.btn-group-vertical>.btn-large:last-child{-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px}.alert{padding:8px 35px 8px 14px;margin-bottom:20px;text-shadow:0 1px 0 rgba(255,255,255,0.5);background-color:#fcf8e3;border:1px solid #fbeed5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.alert,.alert h4{color:#c09853}.alert h4{margin:0}.alert .close{position:relative;top:-2px;right:-21px;line-height:20px}.alert-success{background-color:#dff0d8;border-color:#d6e9c6;color:#468847}.alert-success h4{color:#468847}.alert-danger,.alert-error{background-color:#f2dede;border-color:#eed3d7;color:#b94a48}.alert-danger h4,.alert-error h4{color:#b94a48}.alert-info{background-color:#d9edf7;border-color:#bce8f1;color:#3a87ad}.alert-info h4{color:#3a87ad}.alert-block{padding-top:14px;padding-bottom:14px}.alert-block>p,.alert-block>ul{margin-bottom:0}.alert-block p+p{margin-top:5px}.nav{margin-left:0;margin-bottom:20px;list-style:none}.nav>li>a{display:block}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#f8f9fa}.nav>li>a>img{max-width:none}.nav>.pull-right{float:right}.nav-header{display:block;padding:3px 15px;font-size:11px;font-weight:bold;line-height:20px;color:#e8ebf0;text-shadow:0 1px 0 rgba(255,255,255,0.5);text-transform:uppercase}.nav li+.nav-header{margin-top:9px}.nav-list{padding-left:15px;padding-right:15px;margin-bottom:0}.nav-list>li>a,.nav-list .nav-header{margin-left:-15px;margin-right:-15px;text-shadow:0 1px 0 rgba(255,255,255,0.5)}.nav-list>li>a{padding:3px 15px}.nav-list>.active>a,.nav-list>.active>a:hover,.nav-list>.active>a:focus{color:white;text-shadow:0 -1px 0 rgba(0,0,0,0.2);background-color:#329897}.nav-list [class^="icon-"],.nav-list [class*=" icon-"]{margin-right:2px}.nav-list .divider{*width:100%;height:1px;margin:9px 1px;*margin:-5px 0 5px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid white}.nav-tabs,.nav-pills{*zoom:1}.nav-tabs:before,.nav-tabs:after,.nav-pills:before,.nav-pills:after{display:table;content:"";line-height:0}.nav-tabs:after,.nav-pills:after{clear:both}.nav-tabs>li,.nav-pills>li{float:left}.nav-tabs>li>a,.nav-pills>li>a{padding-right:12px;padding-left:12px;margin-right:2px;line-height:14px}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{margin-bottom:-1px}.nav-tabs>li>a{padding-top:8px;padding-bottom:8px;line-height:20px;border:1px solid transparent;-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover,.nav-tabs>li>a:focus{border-color:#f8f9fa #f8f9fa #dddddd}.nav-tabs>.active>a,.nav-tabs>.active>a:hover,.nav-tabs>.active>a:focus{color:#979ca6;background-color:#329897;border:1px solid #ddd;border-bottom-color:transparent;cursor:default}.nav-pills>li>a{padding-top:8px;padding-bottom:8px;margin-top:2px;margin-bottom:2px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.nav-pills>.active>a,.nav-pills>.active>a:hover,.nav-pills>.active>a:focus{color:white;background-color:#329897}.nav-stacked>li{float:none}.nav-stacked>li>a{margin-right:0}.nav-tabs.nav-stacked{border-bottom:0}.nav-tabs.nav-stacked>li>a{border:1px solid #ddd;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.nav-tabs.nav-stacked>li:first-child>a{-webkit-border-top-right-radius:4px;-moz-border-radius-topright:4px;border-top-right-radius:4px;-webkit-border-top-left-radius:4px;-moz-border-radius-topleft:4px;border-top-left-radius:4px}.nav-tabs.nav-stacked>li:last-child>a{-webkit-border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px;border-bottom-right-radius:4px;-webkit-border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px;border-bottom-left-radius:4px}.nav-tabs.nav-stacked>li>a:hover,.nav-tabs.nav-stacked>li>a:focus{border-color:#ddd;z-index:2}.nav-pills.nav-stacked>li>a{margin-bottom:3px}.nav-pills.nav-stacked>li:last-child>a{margin-bottom:1px}.nav-tabs .dropdown-menu{-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px}.nav-pills .dropdown-menu{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.nav .dropdown-toggle .caret{border-top-color:#329897;border-bottom-color:#329897;margin-top:6px}.nav .dropdown-toggle:hover .caret,.nav .dropdown-toggle:focus .caret{border-top-color:#6a396f;border-bottom-color:#6a396f}.nav-tabs .dropdown-toggle .caret{margin-top:8px}.nav .active .dropdown-toggle .caret{border-top-color:#fff;border-bottom-color:#fff}.nav-tabs .active .dropdown-toggle .caret{border-top-color:#979ca6;border-bottom-color:#979ca6}.nav>.dropdown.active>a:hover,.nav>.dropdown.active>a:focus{cursor:pointer}.nav-tabs .open .dropdown-toggle,.nav-pills .open .dropdown-toggle,.nav>li.dropdown.open.active>a:hover,.nav>li.dropdown.open.active>a:focus{color:white;background-color:#e8ebf0;border-color:#e8ebf0}.nav li.dropdown.open .caret,.nav li.dropdown.open.active .caret,.nav li.dropdown.open a:hover .caret,.nav li.dropdown.open a:focus .caret{border-top-color:white;border-bottom-color:white;opacity:1;filter:alpha(opacity=100)}.tabs-stacked .open>a:hover,.tabs-stacked .open>a:focus{border-color:#e8ebf0}.tabbable{*zoom:1}.tabbable:before,.tabbable:after{display:table;content:"";line-height:0}.tabbable:after{clear:both}.tab-content{overflow:auto}.tabs-below>.nav-tabs,.tabs-right>.nav-tabs,.tabs-left>.nav-tabs{border-bottom:0}.tab-content>.tab-pane,.pill-content>.pill-pane{display:none}.tab-content>.active,.pill-content>.active{display:block}.tabs-below>.nav-tabs{border-top:1px solid #ddd}.tabs-below>.nav-tabs>li{margin-top:-1px;margin-bottom:0}.tabs-below>.nav-tabs>li>a{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px}.tabs-below>.nav-tabs>li>a:hover,.tabs-below>.nav-tabs>li>a:focus{border-bottom-color:transparent;border-top-color:#ddd}.tabs-below>.nav-tabs>.active>a,.tabs-below>.nav-tabs>.active>a:hover,.tabs-below>.nav-tabs>.active>a:focus{border-color:transparent #ddd #ddd #ddd}.tabs-left>.nav-tabs>li,.tabs-right>.nav-tabs>li{float:none}.tabs-left>.nav-tabs>li>a,.tabs-right>.nav-tabs>li>a{min-width:74px;margin-right:0;margin-bottom:3px}.tabs-left>.nav-tabs{float:left;margin-right:19px;border-right:1px solid #ddd}.tabs-left>.nav-tabs>li>a{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.tabs-left>.nav-tabs>li>a:hover,.tabs-left>.nav-tabs>li>a:focus{border-color:#f8f9fa #dddddd #f8f9fa #f8f9fa}.tabs-left>.nav-tabs .active>a,.tabs-left>.nav-tabs .active>a:hover,.tabs-left>.nav-tabs .active>a:focus{border-color:#ddd transparent #ddd #ddd;*border-right-color:white}.tabs-right>.nav-tabs{float:right;margin-left:19px;border-left:1px solid #ddd}.tabs-right>.nav-tabs>li>a{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.tabs-right>.nav-tabs>li>a:hover,.tabs-right>.nav-tabs>li>a:focus{border-color:#f8f9fa #f8f9fa #f8f9fa #dddddd}.tabs-right>.nav-tabs .active>a,.tabs-right>.nav-tabs .active>a:hover,.tabs-right>.nav-tabs .active>a:focus{border-color:#ddd #ddd #ddd transparent;*border-left-color:white}.nav>.disabled>a{color:#e8ebf0}.nav>.disabled>a:hover,.nav>.disabled>a:focus{text-decoration:none;background-color:transparent;cursor:default}.navbar{overflow:visible;margin-bottom:20px;*position:relative;*z-index:2}.navbar-inner{min-height:40px;padding-left:20px;padding-right:20px;background-color:#f9f9f9;background-image:-moz-linear-gradient(top, #fff, #f2f2f2);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#fff), to(#f2f2f2));background-image:-webkit-linear-gradient(top, #fff, #f2f2f2);background-image:-o-linear-gradient(top, #fff, #f2f2f2);background-image:linear-gradient(to bottom, #ffffff,#f2f2f2);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFF2F2F2', GradientType=0);border:1px solid #d4d4d4;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 4px rgba(0,0,0,0.065);-moz-box-shadow:0 1px 4px rgba(0,0,0,0.065);box-shadow:0 1px 4px rgba(0,0,0,0.065);*zoom:1}.navbar-inner:before,.navbar-inner:after{display:table;content:"";line-height:0}.navbar-inner:after{clear:both}.navbar .container{width:auto}.nav-collapse.collapse{height:auto;overflow:visible}.navbar .brand{float:left;display:block;padding:10px 20px 10px;margin-left:-20px;font-size:20px;font-weight:200;color:#777777;text-shadow:0 1px 0 white}.navbar .brand:hover,.navbar .brand:focus{text-decoration:none}.navbar-text{margin-bottom:0;line-height:40px;color:#777777}.navbar-link{color:#777777}.navbar-link:hover,.navbar-link:focus{color:#8b8b8b}.navbar .divider-vertical{height:40px;margin:0 9px;border-left:1px solid #f2f2f2;border-right:1px solid white}.navbar .btn,.navbar .btn-group{margin-top:5px}.navbar .btn-group .btn,.navbar .input-prepend .btn,.navbar .input-append .btn,.navbar .input-prepend .btn-group,.navbar .input-append .btn-group{margin-top:0}.navbar-form{margin-bottom:0;*zoom:1}.navbar-form:before,.navbar-form:after{display:table;content:"";line-height:0}.navbar-form:after{clear:both}.navbar-form input,.navbar-form select,.navbar-form .radio,.navbar-form .checkbox{margin-top:5px}.navbar-form input,.navbar-form select,.navbar-form .btn{display:inline-block;margin-bottom:0}.navbar-form input[type="image"],.navbar-form input[type="checkbox"],.navbar-form input[type="radio"]{margin-top:3px}.navbar-form .input-append,.navbar-form .input-prepend{margin-top:5px;white-space:nowrap}.navbar-form .input-append input,.navbar-form .input-prepend input{margin-top:0}.navbar-search{position:relative;float:left;margin-top:5px;margin-bottom:0}.navbar-search .search-query{margin-bottom:0;padding:4px 14px;font-family:Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:1;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.navbar-static-top{position:static;margin-bottom:0}.navbar-static-top .navbar-inner{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030;margin-bottom:0}.navbar-fixed-top .navbar-inner,.navbar-static-top .navbar-inner{border-width:0 0 1px}.navbar-fixed-bottom .navbar-inner{border-width:1px 0 0}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding-left:0;padding-right:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px}.navbar-fixed-top{top:0}.navbar-fixed-top .navbar-inner,.navbar-static-top .navbar-inner{-webkit-box-shadow:0 1px 10px rgba(0,0,0,0.1);-moz-box-shadow:0 1px 10px rgba(0,0,0,0.1);box-shadow:0 1px 10px rgba(0,0,0,0.1)}.navbar-fixed-bottom{bottom:0}.navbar-fixed-bottom .navbar-inner{-webkit-box-shadow:0 -1px 10px rgba(0,0,0,0.1);-moz-box-shadow:0 -1px 10px rgba(0,0,0,0.1);box-shadow:0 -1px 10px rgba(0,0,0,0.1)}.navbar .nav{position:relative;left:0;display:block;float:left;margin:0 10px 0 0}.navbar .nav.pull-right{float:right;margin-right:0}.navbar .nav>li{float:left}.navbar .nav>li>a{float:none;padding:10px 15px 10px;color:#777777;text-decoration:none;text-shadow:0 1px 0 white}.navbar .nav .dropdown-toggle .caret{margin-top:8px}.navbar .nav>li>a:focus,.navbar .nav>li>a:hover{background-color:transparent;color:#8b8b8b;text-decoration:none}.navbar .nav>.active>a,.navbar .nav>.active>a:hover,.navbar .nav>.active>a:focus{color:#979ca6;text-decoration:none;background-color:#e6e6e6;-webkit-box-shadow:inset 0 3px 8px rgba(0,0,0,0.125);-moz-box-shadow:inset 0 3px 8px rgba(0,0,0,0.125);box-shadow:inset 0 3px 8px rgba(0,0,0,0.125)}.navbar .btn-navbar{display:none;float:right;padding:7px 10px;margin-left:5px;margin-right:5px;color:white;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#ededed;background-image:-moz-linear-gradient(top, #f2f2f2, #e6e6e6);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#f2f2f2), to(#e6e6e6));background-image:-webkit-linear-gradient(top, #f2f2f2, #e6e6e6);background-image:-o-linear-gradient(top, #f2f2f2, #e6e6e6);background-image:linear-gradient(to bottom, #f2f2f2,#e6e6e6);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFF2F2F2', endColorstr='#FFE6E6E6', GradientType=0);border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#e6e6e6;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075)}.navbar .btn-navbar:hover,.navbar .btn-navbar:focus,.navbar .btn-navbar:active,.navbar .btn-navbar.active,.navbar .btn-navbar.disabled,.navbar .btn-navbar[disabled]{color:white;background-color:#e6e6e6;*background-color:#d9d9d9}.navbar .btn-navbar:active,.navbar .btn-navbar.active{background-color:#cccccc \9}.navbar .btn-navbar .icon-bar{display:block;width:18px;height:2px;background-color:#f5f5f5;-webkit-border-radius:1px;-moz-border-radius:1px;border-radius:1px;-webkit-box-shadow:0 1px 0 rgba(0,0,0,0.25);-moz-box-shadow:0 1px 0 rgba(0,0,0,0.25);box-shadow:0 1px 0 rgba(0,0,0,0.25)}.btn-navbar .icon-bar+.icon-bar{margin-top:3px}.navbar .nav>li>.dropdown-menu:before{content:'';display:inline-block;border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-bottom-color:rgba(0,0,0,0.2);position:absolute;top:-7px;left:9px}.navbar .nav>li>.dropdown-menu:after{content:'';display:inline-block;border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid white;position:absolute;top:-6px;left:10px}.navbar-fixed-bottom .nav>li>.dropdown-menu:before{border-top:7px solid #ccc;border-top-color:rgba(0,0,0,0.2);border-bottom:0;bottom:-7px;top:auto}.navbar-fixed-bottom .nav>li>.dropdown-menu:after{border-top:6px solid white;border-bottom:0;bottom:-6px;top:auto}.navbar .nav li.dropdown>a:hover .caret,.navbar .nav li.dropdown>a:focus .caret{border-top-color:#979ca6;border-bottom-color:#979ca6}.navbar .nav li.dropdown.open>.dropdown-toggle,.navbar .nav li.dropdown.active>.dropdown-toggle,.navbar .nav li.dropdown.open.active>.dropdown-toggle{background-color:#e6e6e6;color:#979ca6}.navbar .nav li.dropdown>.dropdown-toggle .caret{border-top-color:#777777;border-bottom-color:#777777}.navbar .nav li.dropdown.open>.dropdown-toggle .caret,.navbar .nav li.dropdown.active>.dropdown-toggle .caret,.navbar .nav li.dropdown.open.active>.dropdown-toggle .caret{border-top-color:#979ca6;border-bottom-color:#979ca6}.navbar .pull-right>li>.dropdown-menu,.navbar .nav>li>.dropdown-menu.pull-right{left:auto;right:0}.navbar .pull-right>li>.dropdown-menu:before,.navbar .nav>li>.dropdown-menu.pull-right:before{left:auto;right:12px}.navbar .pull-right>li>.dropdown-menu:after,.navbar .nav>li>.dropdown-menu.pull-right:after{left:auto;right:13px}.navbar .pull-right>li>.dropdown-menu .dropdown-menu,.navbar .nav>li>.dropdown-menu.pull-right .dropdown-menu{left:auto;right:100%;margin-left:0;margin-right:-1px;-webkit-border-radius:6px 0 6px 6px;-moz-border-radius:6px 0 6px 6px;border-radius:6px 0 6px 6px}.navbar-inverse .navbar-inner{background-color:#1b1b1b;background-image:-moz-linear-gradient(top, #222, #111);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#222), to(#111));background-image:-webkit-linear-gradient(top, #222, #111);background-image:-o-linear-gradient(top, #222, #111);background-image:linear-gradient(to bottom, #222222,#111111);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FF222222', endColorstr='#FF111111', GradientType=0);border-color:#252525}.navbar-inverse .brand,.navbar-inverse .nav>li>a{color:#e8ebf0;text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.navbar-inverse .brand:hover,.navbar-inverse .brand:focus,.navbar-inverse .nav>li>a:hover,.navbar-inverse .nav>li>a:focus{color:white}.navbar-inverse .brand{color:#e8ebf0}.navbar-inverse .navbar-text{color:#e8ebf0}.navbar-inverse .nav>li>a:focus,.navbar-inverse .nav>li>a:hover{background-color:transparent;color:white}.navbar-inverse .nav .active>a,.navbar-inverse .nav .active>a:hover,.navbar-inverse .nav .active>a:focus{color:white;background-color:#111111}.navbar-inverse .navbar-link{color:#e8ebf0}.navbar-inverse .navbar-link:hover,.navbar-inverse .navbar-link:focus{color:white}.navbar-inverse .divider-vertical{border-left-color:#111111;border-right-color:#222222}.navbar-inverse .nav li.dropdown.open>.dropdown-toggle,.navbar-inverse .nav li.dropdown.active>.dropdown-toggle,.navbar-inverse .nav li.dropdown.open.active>.dropdown-toggle{background-color:#111111;color:white}.navbar-inverse .nav li.dropdown>a:hover .caret,.navbar-inverse .nav li.dropdown>a:focus .caret{border-top-color:white;color:white}.navbar-inverse .nav li.dropdown>.dropdown-toggle .caret{border-top-color:#e8ebf0;border-bottom-color:#e8ebf0}.navbar-inverse .nav li.dropdown.open>.dropdown-toggle .caret,.navbar-inverse .nav li.dropdown.active>.dropdown-toggle .caret,.navbar-inverse .nav li.dropdown.open.active>.dropdown-toggle .caret{border-top-color:white;border-bottom-color:white}.navbar-inverse .navbar-search .search-query{color:white;background-color:#515151;border-color:#111111;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);-webkit-transition:none;-moz-transition:none;-o-transition:none;transition:none}.navbar-inverse .navbar-search .search-query:-moz-placeholder{color:#cccccc}.navbar-inverse .navbar-search .search-query:-ms-input-placeholder{color:#cccccc}.navbar-inverse .navbar-search .search-query::-webkit-input-placeholder{color:#cccccc}.navbar-inverse .navbar-search .search-query:focus,.navbar-inverse .navbar-search .search-query.focused{padding:5px 15px;color:#8b8b8b;text-shadow:0 1px 0 white;background-color:white;border:0;-webkit-box-shadow:0 0 3px rgba(0,0,0,0.15);-moz-box-shadow:0 0 3px rgba(0,0,0,0.15);box-shadow:0 0 3px rgba(0,0,0,0.15);outline:0}.navbar-inverse .btn-navbar{color:white;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#0e0e0e;background-image:-moz-linear-gradient(top, #151515, #040404);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#151515), to(#040404));background-image:-webkit-linear-gradient(top, #151515, #040404);background-image:-o-linear-gradient(top, #151515, #040404);background-image:linear-gradient(to bottom, #151515,#040404);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FF151515', endColorstr='#FF040404', GradientType=0);border-color:#040404 #040404 black;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#040404;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.navbar-inverse .btn-navbar:hover,.navbar-inverse .btn-navbar:focus,.navbar-inverse .btn-navbar:active,.navbar-inverse .btn-navbar.active,.navbar-inverse .btn-navbar.disabled,.navbar-inverse .btn-navbar[disabled]{color:white;background-color:#040404;*background-color:black}.navbar-inverse .btn-navbar:active,.navbar-inverse .btn-navbar.active{background-color:black \9}.breadcrumb{padding:8px 15px;margin:0 0 20px;list-style:none;background-color:#f5f5f5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.breadcrumb>li{display:inline-block;*display:inline;*zoom:1;text-shadow:0 1px 0 white}.breadcrumb>li>.divider{padding:0 5px;color:#ccc}.breadcrumb .active{color:#e8ebf0}.pagination{margin:20px 0}.pagination ul{display:inline-block;*display:inline;*zoom:1;margin-left:0;margin-bottom:0;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:0 1px 2px rgba(0,0,0,0.05);box-shadow:0 1px 2px rgba(0,0,0,0.05)}.pagination ul>li{display:inline}.pagination ul>li>a,.pagination ul>li>span{float:left;padding:4px 12px;line-height:20px;text-decoration:none;background-color:white;border:1px solid #dddddd;border-left-width:0}.pagination ul>li>a:hover,.pagination ul>li>a:focus,.pagination ul>.active>a,.pagination ul>.active>span{background-color:whitesmoke}.pagination ul>.active>a,.pagination ul>.active>span{color:#e8ebf0;cursor:default}.pagination ul>.disabled>span,.pagination ul>.disabled>a,.pagination ul>.disabled>a:hover,.pagination ul>.disabled>a:focus{color:#e8ebf0;background-color:transparent;cursor:default}.pagination ul>li:first-child>a,.pagination ul>li:first-child>span{border-left-width:1px;-webkit-border-top-left-radius:4px;-moz-border-radius-topleft:4px;border-top-left-radius:4px;-webkit-border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px;border-bottom-left-radius:4px}.pagination ul>li:last-child>a,.pagination ul>li:last-child>span{-webkit-border-top-right-radius:4px;-moz-border-radius-topright:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px;border-bottom-right-radius:4px}.pagination-centered{text-align:center}.pagination-right{text-align:right}.pagination-large ul>li>a,.pagination-large ul>li>span{padding:11px 19px;font-size:17.5px}.pagination-large ul>li:first-child>a,.pagination-large ul>li:first-child>span{-webkit-border-top-left-radius:6px;-moz-border-radius-topleft:6px;border-top-left-radius:6px;-webkit-border-bottom-left-radius:6px;-moz-border-radius-bottomleft:6px;border-bottom-left-radius:6px}.pagination-large ul>li:last-child>a,.pagination-large ul>li:last-child>span{-webkit-border-top-right-radius:6px;-moz-border-radius-topright:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;-moz-border-radius-bottomright:6px;border-bottom-right-radius:6px}.pagination-mini ul>li:first-child>a,.pagination-mini ul>li:first-child>span,.pagination-small ul>li:first-child>a,.pagination-small ul>li:first-child>span{-webkit-border-top-left-radius:3px;-moz-border-radius-topleft:3px;border-top-left-radius:3px;-webkit-border-bottom-left-radius:3px;-moz-border-radius-bottomleft:3px;border-bottom-left-radius:3px}.pagination-mini ul>li:last-child>a,.pagination-mini ul>li:last-child>span,.pagination-small ul>li:last-child>a,.pagination-small ul>li:last-child>span{-webkit-border-top-right-radius:3px;-moz-border-radius-topright:3px;border-top-right-radius:3px;-webkit-border-bottom-right-radius:3px;-moz-border-radius-bottomright:3px;border-bottom-right-radius:3px}.pagination-small ul>li>a,.pagination-small ul>li>span{padding:2px 10px;font-size:11.9px}.pagination-mini ul>li>a,.pagination-mini ul>li>span{padding:0px 6px;font-size:10.5px}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#474e5d}.modal-backdrop.fade{opacity:0}.modal-backdrop,.modal-backdrop.fade.in{opacity:0.8;filter:alpha(opacity=80)}.modal{position:fixed;top:10%;left:50%;z-index:1050;width:560px;margin-left:-280px;background-color:white;border:1px solid #999;border:1px solid rgba(0,0,0,0.3);*border:1px solid #999;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 3px 7px rgba(0,0,0,0.3);-moz-box-shadow:0 3px 7px rgba(0,0,0,0.3);box-shadow:0 3px 7px rgba(0,0,0,0.3);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box;outline:none}.modal.fade{-webkit-transition:opacity 0.3s linear, top 0.3s ease-out;-moz-transition:opacity 0.3s linear, top 0.3s ease-out;-o-transition:opacity 0.3s linear, top 0.3s ease-out;transition:opacity 0.3s linear, top 0.3s ease-out;top:-25%}.modal.fade.in{top:10%}.modal-header{padding:9px 15px;border-bottom:1px solid #eee}.modal-header .close{margin-top:2px}.modal-header h3{margin:0;line-height:30px}.modal-body{position:relative;overflow-y:auto;max-height:400px;padding:15px}.modal-form{margin-bottom:0}.modal-footer{padding:14px 15px 15px;margin-bottom:0;text-align:right;background-color:#f5f5f5;border-top:1px solid #ddd;-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;-webkit-box-shadow:inset 0 1px 0 white;-moz-box-shadow:inset 0 1px 0 white;box-shadow:inset 0 1px 0 white;*zoom:1}.modal-footer:before,.modal-footer:after{display:table;content:"";line-height:0}.modal-footer:after{clear:both}.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.tooltip{position:absolute;z-index:1030;display:block;visibility:visible;font-size:11px;line-height:1.4;opacity:0;filter:alpha(opacity=0)}.tooltip.in{opacity:0.8;filter:alpha(opacity=80)}.tooltip.top{margin-top:-3px;padding:5px 0}.tooltip.right{margin-left:3px;padding:0 5px}.tooltip.bottom{margin-top:3px;padding:5px 0}.tooltip.left{margin-left:-3px;padding:0 5px}.tooltip-inner{max-width:200px;padding:8px;color:white;text-align:center;text-decoration:none;background-color:black;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:black}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:black}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:black}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:black}.popover{position:absolute;top:0;left:0;z-index:1010;display:none;max-width:276px;padding:1px;text-align:left;background-color:white;-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);white-space:normal}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{margin:0;padding:8px 14px;font-size:14px;font-weight:normal;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0}.popover-title:empty{display:none}.popover-content{padding:9px 14px}.popover .arrow,.popover .arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover .arrow{border-width:11px}.popover .arrow:after{border-width:10px;content:""}.popover.top .arrow{left:50%;margin-left:-11px;border-bottom-width:0;border-top-color:#999;border-top-color:rgba(0,0,0,0.25);bottom:-11px}.popover.top .arrow:after{bottom:1px;margin-left:-10px;border-bottom-width:0;border-top-color:white}.popover.right .arrow{top:50%;left:-11px;margin-top:-11px;border-left-width:0;border-right-color:#999;border-right-color:rgba(0,0,0,0.25)}.popover.right .arrow:after{left:1px;bottom:-10px;border-left-width:0;border-right-color:white}.popover.bottom .arrow{left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,0.25);top:-11px}.popover.bottom .arrow:after{top:1px;margin-left:-10px;border-top-width:0;border-bottom-color:white}.popover.left .arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,0.25)}.popover.left .arrow:after{right:1px;border-right-width:0;border-left-color:white;bottom:-10px}.label,.badge{display:inline-block;padding:2px 4px;font-size:11.844px;font-weight:bold;line-height:14px;color:white;vertical-align:baseline;white-space:nowrap;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#e8ebf0}.label{-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.badge{padding-left:9px;padding-right:9px;-webkit-border-radius:9px;-moz-border-radius:9px;border-radius:9px}.label:empty,.badge:empty{display:none}a.label:hover,a.label:focus,a.badge:hover,a.badge:focus{color:white;text-decoration:none;cursor:pointer}.label-important{background-color:#b94a48}.label-important[href]{background-color:#953b39}.label-warning{background-color:#dc795b}.label-warning[href]{background-color:#d35731}.label-success{background-color:#468847}.label-success[href]{background-color:#356635}.label-info{background-color:#3a87ad}.label-info[href]{background-color:#2d6987}.label-inverse{background-color:#8b8b8b}.label-inverse[href]{background-color:#727272}.badge-important{background-color:#b94a48}.badge-important[href]{background-color:#953b39}.badge-warning{background-color:#dc795b}.badge-warning[href]{background-color:#d35731}.badge-success{background-color:#468847}.badge-success[href]{background-color:#356635}.badge-info{background-color:#3a87ad}.badge-info[href]{background-color:#2d6987}.badge-inverse{background-color:#8b8b8b}.badge-inverse[href]{background-color:#727272}.btn .label,.btn .badge{position:relative;top:-1px}.btn-mini .label,.btn-mini .badge{top:0}.pull-right{float:right}.pull-left{float:left}.hide{display:none}.show{display:block}.invisible{visibility:hidden}.affix{position:fixed}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:"";line-height:0}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}/*! - * Bootstrap Responsive v2.3.2 - * - * Copyright 2012 Twitter, Inc - * Licensed under the Apache License v2.0 - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Designed and built with all the love in the world @twitter by @mdo and @fat. - */@-ms-viewport{width:device-width}.hidden{display:none;visibility:hidden}.visible-phone{display:none !important}.visible-tablet{display:none !important}.hidden-desktop{display:none !important}.visible-desktop{display:inherit !important}@media (min-width: 768px) and (max-width: 979px){.hidden-desktop{display:inherit !important}.visible-desktop{display:none !important}.visible-tablet{display:inherit !important}.hidden-tablet{display:none !important}}@media (max-width: 767px){.hidden-desktop{display:inherit !important}.visible-desktop{display:none !important}.visible-phone{display:inherit !important}.hidden-phone{display:none !important}}.visible-print{display:none !important}@media print{.visible-print{display:inherit !important}.hidden-print{display:none !important}}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:"";line-height:0}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}@media (min-width: 1200px){.row{margin-left:-30px;*zoom:1}.row:before,.row:after{display:table;content:"";line-height:0}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:30px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px}.span1{width:70px}.span2{width:170px}.span3{width:270px}.span4{width:370px}.span5{width:470px}.span6{width:570px}.span7{width:670px}.span8{width:770px}.span9{width:870px}.span10{width:970px}.span11{width:1070px}.span12{width:1170px}.offset1{margin-left:130px}.offset2{margin-left:230px}.offset3{margin-left:330px}.offset4{margin-left:430px}.offset5{margin-left:530px}.offset6{margin-left:630px}.offset7{margin-left:730px}.offset8{margin-left:830px}.offset9{margin-left:930px}.offset10{margin-left:1030px}.offset11{margin-left:1130px}.offset12{margin-left:1230px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;content:"";line-height:0}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;float:left;margin-left:2.5641%;*margin-left:2.51091%}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.5641%}.row-fluid .span1{width:5.98291%;*width:5.92971%}.row-fluid .span2{width:14.52991%;*width:14.47672%}.row-fluid .span3{width:23.07692%;*width:23.02373%}.row-fluid .span4{width:31.62393%;*width:31.57074%}.row-fluid .span5{width:40.17094%;*width:40.11775%}.row-fluid .span6{width:48.71795%;*width:48.66476%}.row-fluid .span7{width:57.26496%;*width:57.21177%}.row-fluid .span8{width:65.81197%;*width:65.75877%}.row-fluid .span9{width:74.35897%;*width:74.30578%}.row-fluid .span10{width:82.90598%;*width:82.85279%}.row-fluid .span11{width:91.45299%;*width:91.3998%}.row-fluid .span12{width:100%;*width:99.94681%}.row-fluid .offset1{margin-left:11.11111%;*margin-left:11.00473%}.row-fluid .offset1:first-child{margin-left:8.54701%;*margin-left:8.44063%}.row-fluid .offset2{margin-left:19.65812%;*margin-left:19.55174%}.row-fluid .offset2:first-child{margin-left:17.09402%;*margin-left:16.98763%}.row-fluid .offset3{margin-left:28.20513%;*margin-left:28.09875%}.row-fluid .offset3:first-child{margin-left:25.64103%;*margin-left:25.53464%}.row-fluid .offset4{margin-left:36.75214%;*margin-left:36.64575%}.row-fluid .offset4:first-child{margin-left:34.18803%;*margin-left:34.08165%}.row-fluid .offset5{margin-left:45.29915%;*margin-left:45.19276%}.row-fluid .offset5:first-child{margin-left:42.73504%;*margin-left:42.62866%}.row-fluid .offset6{margin-left:53.84615%;*margin-left:53.73977%}.row-fluid .offset6:first-child{margin-left:51.28205%;*margin-left:51.17567%}.row-fluid .offset7{margin-left:62.39316%;*margin-left:62.28678%}.row-fluid .offset7:first-child{margin-left:59.82906%;*margin-left:59.72268%}.row-fluid .offset8{margin-left:70.94017%;*margin-left:70.83379%}.row-fluid .offset8:first-child{margin-left:68.37607%;*margin-left:68.26969%}.row-fluid .offset9{margin-left:79.48718%;*margin-left:79.3808%}.row-fluid .offset9:first-child{margin-left:76.92308%;*margin-left:76.81669%}.row-fluid .offset10{margin-left:88.03419%;*margin-left:87.92781%}.row-fluid .offset10:first-child{margin-left:85.47009%;*margin-left:85.3637%}.row-fluid .offset11{margin-left:96.5812%;*margin-left:96.47481%}.row-fluid .offset11:first-child{margin-left:94.01709%;*margin-left:93.91071%}.row-fluid .offset12{margin-left:105.12821%;*margin-left:105.02182%}.row-fluid .offset12:first-child{margin-left:102.5641%;*margin-left:102.45772%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:30px}input.span1,textarea.span1,.uneditable-input.span1{width:56px}input.span2,textarea.span2,.uneditable-input.span2{width:156px}input.span3,textarea.span3,.uneditable-input.span3{width:256px}input.span4,textarea.span4,.uneditable-input.span4{width:356px}input.span5,textarea.span5,.uneditable-input.span5{width:456px}input.span6,textarea.span6,.uneditable-input.span6{width:556px}input.span7,textarea.span7,.uneditable-input.span7{width:656px}input.span8,textarea.span8,.uneditable-input.span8{width:756px}input.span9,textarea.span9,.uneditable-input.span9{width:856px}input.span10,textarea.span10,.uneditable-input.span10{width:956px}input.span11,textarea.span11,.uneditable-input.span11{width:1056px}input.span12,textarea.span12,.uneditable-input.span12{width:1156px}.thumbnails{margin-left:-30px}.thumbnails>li{margin-left:30px}.row-fluid .thumbnails{margin-left:0}}@media (min-width: 768px) and (max-width: 979px){.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;content:"";line-height:0}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px}.span1{width:42px}.span2{width:104px}.span3{width:166px}.span4{width:228px}.span5{width:290px}.span6{width:352px}.span7{width:414px}.span8{width:476px}.span9{width:538px}.span10{width:600px}.span11{width:662px}.span12{width:724px}.offset1{margin-left:82px}.offset2{margin-left:144px}.offset3{margin-left:206px}.offset4{margin-left:268px}.offset5{margin-left:330px}.offset6{margin-left:392px}.offset7{margin-left:454px}.offset8{margin-left:516px}.offset9{margin-left:578px}.offset10{margin-left:640px}.offset11{margin-left:702px}.offset12{margin-left:764px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;content:"";line-height:0}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;float:left;margin-left:2.76243%;*margin-left:2.70924%}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.76243%}.row-fluid .span1{width:5.8011%;*width:5.74791%}.row-fluid .span2{width:14.36464%;*width:14.31145%}.row-fluid .span3{width:22.92818%;*width:22.87499%}.row-fluid .span4{width:31.49171%;*width:31.43852%}.row-fluid .span5{width:40.05525%;*width:40.00206%}.row-fluid .span6{width:48.61878%;*width:48.56559%}.row-fluid .span7{width:57.18232%;*width:57.12913%}.row-fluid .span8{width:65.74586%;*width:65.69266%}.row-fluid .span9{width:74.30939%;*width:74.2562%}.row-fluid .span10{width:82.87293%;*width:82.81974%}.row-fluid .span11{width:91.43646%;*width:91.38327%}.row-fluid .span12{width:100%;*width:99.94681%}.row-fluid .offset1{margin-left:11.32597%;*margin-left:11.21958%}.row-fluid .offset1:first-child{margin-left:8.56354%;*margin-left:8.45715%}.row-fluid .offset2{margin-left:19.8895%;*margin-left:19.78312%}.row-fluid .offset2:first-child{margin-left:17.12707%;*margin-left:17.02069%}.row-fluid .offset3{margin-left:28.45304%;*margin-left:28.34666%}.row-fluid .offset3:first-child{margin-left:25.69061%;*margin-left:25.58422%}.row-fluid .offset4{margin-left:37.01657%;*margin-left:36.91019%}.row-fluid .offset4:first-child{margin-left:34.25414%;*margin-left:34.14776%}.row-fluid .offset5{margin-left:45.58011%;*margin-left:45.47373%}.row-fluid .offset5:first-child{margin-left:42.81768%;*margin-left:42.7113%}.row-fluid .offset6{margin-left:54.14365%;*margin-left:54.03726%}.row-fluid .offset6:first-child{margin-left:51.38122%;*margin-left:51.27483%}.row-fluid .offset7{margin-left:62.70718%;*margin-left:62.6008%}.row-fluid .offset7:first-child{margin-left:59.94475%;*margin-left:59.83837%}.row-fluid .offset8{margin-left:71.27072%;*margin-left:71.16434%}.row-fluid .offset8:first-child{margin-left:68.50829%;*margin-left:68.4019%}.row-fluid .offset9{margin-left:79.83425%;*margin-left:79.72787%}.row-fluid .offset9:first-child{margin-left:77.07182%;*margin-left:76.96544%}.row-fluid .offset10{margin-left:88.39779%;*margin-left:88.29141%}.row-fluid .offset10:first-child{margin-left:85.63536%;*margin-left:85.52898%}.row-fluid .offset11{margin-left:96.96133%;*margin-left:96.85494%}.row-fluid .offset11:first-child{margin-left:94.1989%;*margin-left:94.09251%}.row-fluid .offset12{margin-left:105.52486%;*margin-left:105.41848%}.row-fluid .offset12:first-child{margin-left:102.76243%;*margin-left:102.65605%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span1,textarea.span1,.uneditable-input.span1{width:28px}input.span2,textarea.span2,.uneditable-input.span2{width:90px}input.span3,textarea.span3,.uneditable-input.span3{width:152px}input.span4,textarea.span4,.uneditable-input.span4{width:214px}input.span5,textarea.span5,.uneditable-input.span5{width:276px}input.span6,textarea.span6,.uneditable-input.span6{width:338px}input.span7,textarea.span7,.uneditable-input.span7{width:400px}input.span8,textarea.span8,.uneditable-input.span8{width:462px}input.span9,textarea.span9,.uneditable-input.span9{width:524px}input.span10,textarea.span10,.uneditable-input.span10{width:586px}input.span11,textarea.span11,.uneditable-input.span11{width:648px}input.span12,textarea.span12,.uneditable-input.span12{width:710px}}@media (max-width: 767px){body{padding-left:20px;padding-right:20px}.navbar-fixed-top,.navbar-fixed-bottom,.navbar-static-top{margin-left:-20px;margin-right:-20px}.container-fluid{padding:0}.dl-horizontal dt{float:none;clear:none;width:auto;text-align:left}.dl-horizontal dd{margin-left:0}.container{width:auto}.row-fluid{width:100%}.row,.thumbnails{margin-left:0}.thumbnails>li{float:none;margin-left:0}[class*="span"],.uneditable-input[class*="span"],.row-fluid [class*="span"]{float:none;display:block;width:100%;margin-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.span12,.row-fluid .span12{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="offset"]:first-child{margin-left:0}.input-large,.input-xlarge,.input-xxlarge,input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.input-prepend input,.input-append input,.input-prepend input[class*="span"],.input-append input[class*="span"]{display:inline-block;width:auto}.controls-row [class*="span"]+[class*="span"]{margin-left:0}.modal{position:fixed;top:20px;left:20px;right:20px;width:auto;margin:0}.modal.fade{top:-100px}.modal.fade.in{top:20px}}@media (max-width: 480px){.nav-collapse{-webkit-transform:translate3d(0, 0, 0)}.page-header h1 small{display:block;line-height:20px}input[type="checkbox"],input[type="radio"]{border:1px solid #ccc}.form-horizontal .control-label{float:none;width:auto;padding-top:0;text-align:left}.form-horizontal .controls{margin-left:0}.form-horizontal .control-list{padding-top:0}.form-horizontal .form-actions{padding-left:10px;padding-right:10px}.media .pull-left,.media .pull-right{float:none;display:block;margin-bottom:10px}.media-object{margin-right:0;margin-left:0}.modal{top:10px;left:10px;right:10px}.modal-header .close{padding:10px;margin:-10px}.carousel-caption{position:static}}@media (max-width: 979px){body{padding-top:0}.navbar-fixed-top,.navbar-fixed-bottom{position:static}.navbar-fixed-top{margin-bottom:20px}.navbar-fixed-bottom{margin-top:20px}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding:5px}.navbar .container{width:auto;padding:0}.navbar .brand{padding-left:10px;padding-right:10px;margin:0 0 0 -5px}.nav-collapse{clear:both}.nav-collapse .nav{float:none;margin:0 0 10px}.nav-collapse .nav>li{float:none}.nav-collapse .nav>li>a{margin-bottom:2px}.nav-collapse .nav>.divider-vertical{display:none}.nav-collapse .nav .nav-header{color:#777777;text-shadow:none}.nav-collapse .nav>li>a,.nav-collapse .dropdown-menu a{padding:9px 15px;font-weight:bold;color:#777777;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.nav-collapse .btn{padding:4px 10px 4px;font-weight:normal;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.nav-collapse .dropdown-menu li+li a{margin-bottom:2px}.nav-collapse .nav>li>a:hover,.nav-collapse .nav>li>a:focus,.nav-collapse .dropdown-menu a:hover,.nav-collapse .dropdown-menu a:focus{background-color:#f2f2f2}.navbar-inverse .nav-collapse .nav>li>a,.navbar-inverse .nav-collapse .dropdown-menu a{color:#e8ebf0}.navbar-inverse .nav-collapse .nav>li>a:hover,.navbar-inverse .nav-collapse .nav>li>a:focus,.navbar-inverse .nav-collapse .dropdown-menu a:hover,.navbar-inverse .nav-collapse .dropdown-menu a:focus{background-color:#111111}.nav-collapse.in .btn-group{margin-top:5px;padding:0}.nav-collapse .dropdown-menu{position:static;top:auto;left:auto;float:none;display:none;max-width:none;margin:0 15px;padding:0;background-color:transparent;border:none;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.nav-collapse .open>.dropdown-menu{display:block}.nav-collapse .dropdown-menu:before,.nav-collapse .dropdown-menu:after{display:none}.nav-collapse .dropdown-menu .divider{display:none}.nav-collapse .nav>li>.dropdown-menu:before,.nav-collapse .nav>li>.dropdown-menu:after{display:none}.nav-collapse .navbar-form,.nav-collapse .navbar-search{float:none;padding:10px 15px;margin:10px 0;border-top:1px solid #f2f2f2;border-bottom:1px solid #f2f2f2;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1)}.navbar-inverse .nav-collapse .navbar-form,.navbar-inverse .nav-collapse .navbar-search{border-top-color:#111111;border-bottom-color:#111111}.navbar .nav-collapse .nav.pull-right{float:none;margin-left:0}.nav-collapse,.nav-collapse.collapse{overflow:hidden;height:0}.navbar .btn-navbar{display:block}.navbar-static .navbar-inner{padding-left:10px;padding-right:10px}}@media (min-width: 980px){.nav-collapse.collapse{height:auto !important;overflow:visible !important}}/*! - * jQuery UI Bootstrap (0.22) - * http://addyosmani.github.com/jquery-ui-bootstrap - * - * Copyright 2012, Addy Osmani - * Dual licensed under the MIT or GPL Version 2 licenses. - * - * Portions copyright jQuery UI & Twitter Bootstrap - */.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{position:absolute !important;clip:rect(1px 1px 1px 1px);clip:rect(1px, 1px, 1px, 1px)}.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:"Helvetica Neue", Helvetica, Arial, sans-serif;font-size:13px}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:"Helvetica Neue", Helvetica, Arial, sans-serif;font-size:1em}.ui-widget-content{border:1px solid #aaaaaa;background:#fff url(/assets/ui-bg_glass_75_ffffff_1x400-38c3e5f87ed29ad61032671541538a30.png) 50% 50% repeat-x;color:#404040}.ui-widget-content a{color:#404040}.ui-widget-header{font-weight:bold;border-color:#0064cd #0064cd #003f81;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);border:1px solid #666}.ui-widget-header a{color:#222222}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default{background-color:#e6e6e6;background-repeat:no-repeat;background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#fff), color-stop(25%, #fff), to(#e6e6e6));background-image:-webkit-linear-gradient(#fff, #fff 25%, #e6e6e6);background-image:-moz-linear-gradient(top, #fff, #fff 25%, #e6e6e6);background-image:-ms-linear-gradient(#fff, #fff 25%, #e6e6e6);background-image:-o-linear-gradient(#fff, #fff 25%, #e6e6e6);background-image:linear-gradient(#ffffff,#ffffff 25%,#e6e6e6);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0);text-shadow:0 1px 1px rgba(255,255,255,0.75);color:#333;font-size:13px;line-height:normal;border:1px solid #ccc;border-bottom-color:#bbb;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);-webkit-transition:0.1s linear background-image;-moz-transition:0.1s linear background-image;-ms-transition:0.1s linear background-image;-o-transition:0.1s linear background-image;transition:0.1s linear background-image;overflow:visible}.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{background-position:0 -15px;color:#333;text-decoration:none}.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;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 p,.ui-state-error p,.ui-state-default p{font-size:13px;font-weight:normal;line-height:18px;margin:7px 15px}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{position:auto;margin-bottom:18px;color:#404040;background-color:#eedc94;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#fceec1), to(#eedc94));background-image:-moz-linear-gradient(top, #fceec1, #eedc94);background-image:-ms-linear-gradient(top, #fceec1, #eedc94);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #fceec1), color-stop(100%, #eedc94));background-image:-webkit-linear-gradient(top, #fceec1, #eedc94);background-image:-o-linear-gradient(top, #fceec1, #eedc94);background-image:linear-gradient(top, #fceec1,#eedc94);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fceec1', endColorstr='#eedc94', GradientType=0);text-shadow:0 -1px 0 rgba(0,0,0,0.25);border-color:#eedc94 #eedc94 #e4c652;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);text-shadow:0 1px 0 rgba(255,255,255,0.5);border-width:1px;border-style:solid;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.25);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.25);box-shadow:inset 0 1px 0 rgba(255,255,255,0.25)}.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{position:relative;margin-bottom:18px;color:#ffffff;border-width:1px;border-style:solid;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.25);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.25);box-shadow:inset 0 1px 0 rgba(255,255,255,0.25);background-color:#c43c35;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#ee5f5b), to(#c43c35));background-image:-moz-linear-gradient(top, #ee5f5b, #c43c35);background-image:-ms-linear-gradient(top, #ee5f5b, #c43c35);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #ee5f5b), color-stop(100%, #c43c35));background-image:-webkit-linear-gradient(top, #ee5f5b, #c43c35);background-image:-o-linear-gradient(top, #ee5f5b, #c43c35);background-image:linear-gradient(top, #ee5f5b,#c43c35);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#c43c35', GradientType=0);text-shadow:0 -1px 0 rgba(0,0,0,0.25);border-color:#c43c35 #c43c35 #882a25;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25)}.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(/assets/ui-icons_222222_256x240-5e0a27a37253be796f7e20232dda7a14.png)}.ui-widget-content .ui-icon{background-image:url(/assets/ui-icons_222222_256x240-5e0a27a37253be796f7e20232dda7a14.png)}.ui-widget-header .ui-icon{background-image:url(/assets/ui-icons_222222_256x240-5e0a27a37253be796f7e20232dda7a14.png)}.ui-state-default .ui-icon{background-image:url(/assets/ui-icons_888888_256x240-535ccced4c425c87c31d31705aaa778f.png)}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon{background-image:url(/assets/ui-icons_454545_256x240-e93232d8b260642dae24b04d6dda5d13.png)}.ui-state-active .ui-icon{background-image:url(/assets/ui-icons_454545_256x240-e93232d8b260642dae24b04d6dda5d13.png)}.ui-state-highlight .ui-icon{background-image:url(/assets/ui-icons_2e83ff_256x240-c8cd519bf6794d9f605b51f911171e2d.png)}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url(/assets/ui-icons_f6cf3b_256x240-181ea146dfb201a203da18e7c1dc5027.png)}.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-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{-moz-border-radius-topleft:4px;-webkit-border-top-left-radius:4px;-khtml-border-top-left-radius:4px;border-top-left-radius:4px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{-moz-border-radius-topright:4px;-webkit-border-top-right-radius:4px;-khtml-border-top-right-radius:4px;border-top-right-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{-moz-border-radius-bottomleft:4px;-webkit-border-bottom-left-radius:4px;-khtml-border-bottom-left-radius:4px;border-bottom-left-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{-moz-border-radius-bottomright:4px;-webkit-border-bottom-right-radius:4px;-khtml-border-bottom-right-radius:4px;border-bottom-right-radius:4px}.ui-widget-overlay{background:#aaa url(/assets/ui-bg_flat_0_aaaaaa_40x100-e38c4e18477af70cfd3fb949f1c6aae8.png) 50% 50% repeat-x;opacity:.30;filter:Alpha(Opacity=30)}.ui-widget-shadow{margin:-8px 0 0 -8px;padding:8px;background:#aaa url(/assets/ui-bg_flat_0_aaaaaa_40x100-e38c4e18477af70cfd3fb949f1c6aae8.png) 50% 50% repeat-x;opacity:.30;filter:Alpha(Opacity=30);-moz-border-radius:8px;-khtml-border-radius:8px;-webkit-border-radius:8px;border-radius:8px}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;z-index:99999;display:block}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-selectable-helper{position:absolute;z-index:100;border:1px dotted black}.ui-accordion{width:100%}.ui-accordion .ui-accordion-header{cursor:pointer;position:relative;margin-top:1px;zoom:1;font-weight:bold}.ui-accordion .ui-accordion-li-fix{display:inline}.ui-accordion .ui-accordion-header-active{border-bottom:0 !important}.ui-accordion .ui-accordion-header a{display:block;font-size:1em;padding:.5em .5em .5em .7em}.ui-accordion-icons .ui-accordion-header a{padding-left:2.2em}.ui-accordion .ui-accordion-header .ui-icon{position:absolute;left:.5em;top:50%;margin-top:-8px}.ui-accordion .ui-accordion-content{padding:1em 2.2em;border-top:0;margin-top:-2px;position:relative;top:1px;margin-bottom:2px;overflow:auto;display:none;zoom:1}.ui-accordion .ui-accordion-content-active{display:block}.ui-autocomplete{position:absolute;cursor:default}* html .ui-autocomplete{width:1px}.ui-menu{list-style:none;padding:2px;margin:0;display:block;float:left}.ui-menu .ui-menu{margin-top:-3px}.ui-menu .ui-menu-item{margin:0;padding:0;zoom:1;float:left;clear:left;width:100%}.ui-menu .ui-menu-item a{text-decoration:none;display:block;padding:.2em .4em;line-height:1.5;zoom:1}.ui-menu .ui-menu-item a.ui-state-hover,.ui-menu .ui-menu-item a.ui-state-active{font-weight:normal;background:#0064CD;color:white}.ui-button{cursor:pointer;display:inline-block;background-color:#e6e6e6;background-repeat:no-repeat;background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#fff), color-stop(25%, #fff), to(#e6e6e6));background-image:-webkit-linear-gradient(#fff, #fff 25%, #e6e6e6);background-image:-moz-linear-gradient(top, #fff, #fff 25%, #e6e6e6);background-image:-ms-linear-gradient(#fff, #fff 25%, #e6e6e6);background-image:-o-linear-gradient(#fff, #fff 25%, #e6e6e6);background-image:linear-gradient(#ffffff,#ffffff 25%,#e6e6e6);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0);padding:5px 14px 6px;margin:0;text-shadow:0 1px 1px rgba(255,255,255,0.75);color:#333;font-size:13px;line-height:normal;border:1px solid #ccc;border-bottom-color:#bbb;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);-webkit-transition:0.1s linear background-image;-moz-transition:0.1s linear background-image;-ms-transition:0.1s linear background-image;-o-transition:0.1s linear background-image;transition:0.1s linear background-image;overflow:visible}.ui-dialog-buttonset .ui-button:focus{outline:none}.ui-button-primary{color:#ffffff;background-color:#0064cd;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#049cdb), to(#0064cd));background-image:-moz-linear-gradient(top, #049cdb, #0064cd);background-image:-ms-linear-gradient(top, #049cdb, #0064cd);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #049cdb), color-stop(100%, #0064cd));background-image:-webkit-linear-gradient(top, #049cdb, #0064cd);background-image:-o-linear-gradient(top, #049cdb, #0064cd);background-image:linear-gradient(top, #049cdb,#0064cd);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#049cdb', endColorstr='#0064cd', GradientType=0);text-shadow:0 -1px 0 rgba(0,0,0,0.25);border-color:#0064cd #0064cd #003f81;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25)}.ui-button-success{color:#ffffff;background-color:#57a957;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#62c462), to(#57a957));background-image:-moz-linear-gradient(top, #62c462, #57a957);background-image:-ms-linear-gradient(top, #62c462, #57a957);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #62c462), color-stop(100%, #57a957));background-image:-webkit-linear-gradient(top, #62c462, #57a957);background-image:-o-linear-gradient(top, #62c462, #57a957);background-image:linear-gradient(top, #62c462,#57a957);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#57a957', GradientType=0);text-shadow:0 -1px 0 rgba(0,0,0,0.25);border-color:#57a957 #57a957 #3d773d;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25)}.ui-button-error{color:#ffffff;background-color:#c43c35;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#ee5f5b), to(#c43c35));background-image:-moz-linear-gradient(top, #ee5f5b, #c43c35);background-image:-ms-linear-gradient(top, #ee5f5b, #c43c35);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #ee5f5b), color-stop(100%, #c43c35));background-image:-webkit-linear-gradient(top, #ee5f5b, #c43c35);background-image:-o-linear-gradient(top, #ee5f5b, #c43c35);background-image:linear-gradient(top, #ee5f5b,#c43c35);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#c43c35', GradientType=0);text-shadow:0 -1px 0 rgba(0,0,0,0.25);border-color:#c43c35 #c43c35 #882a25;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25)}.ui-button-icon-only{width:2.2em}.ui-button-icons-only{width:3.4em}button.ui-button-icons-only{width:3.7em}.ui-button .ui-button-text{display:block}.ui-button-icon-only .ui-button-text,.ui-button-icons-only .ui-button-text{padding:.4em;text-indent:-9999999px;display:none}.ui-button-text-icon-primary .ui-button-text,.ui-button-text-icons .ui-button-text{padding:.4em 1em .4em 2.1em}.ui-button-text-icon-secondary .ui-button-text,.ui-button-text-icons .ui-button-text{padding:.4em 2.1em .4em 1em}.ui-button-text-icons .ui-button-text{padding-left:2.1em;padding-right:2.1em}.ui-button-icon-only .ui-icon,.ui-button-text-icon-primary .ui-icon,.ui-button-text-icon-secondary .ui-icon,.ui-button-text-icons .ui-icon,.ui-button-icons-only .ui-icon{top:50%;margin-top:-3px;margin-bottom:3px}.ui-button-icon-only .ui-icon{left:50%;margin-left:-8px}.ui-button-text-icon-primary .ui-button-icon-primary,.ui-button-text-icons .ui-button-icon-primary,.ui-button-icons-only .ui-button-icon-primary{left:.5em}.ui-button-text-icon-secondary .ui-button-icon-secondary,.ui-button-text-icons .ui-button-icon-secondary,.ui-button-icons-only .ui-button-icon-secondary{right:.5em}.ui-button-text-icons .ui-button-icon-secondary,.ui-button-icons-only .ui-button-icon-secondary{right:.5em}.ui-buttonset{margin-right:7px}.ui-buttonset .ui-state-active{color:#ffffff;background-color:#0064cd;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#049cdb), to(#0064cd));background-image:-moz-linear-gradient(top, #049cdb, #0064cd);background-image:-ms-linear-gradient(top, #049cdb, #0064cd);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #049cdb), color-stop(100%, #0064cd));background-image:-webkit-linear-gradient(top, #049cdb, #0064cd);background-image:-o-linear-gradient(top, #049cdb, #0064cd);background-image:linear-gradient(top, #049cdb,#0064cd);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#049cdb', endColorstr='#0064cd', GradientType=0);text-shadow:0 -1px 0 rgba(0,0,0,0.25);border-color:#0064cd #0064cd #003f81;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25)}.ui-buttonset .ui-button{margin-left:0;margin-right:-.4em}button.ui-button::-moz-focus-inner{border:0;padding:0}.ui-dialog{position:absolute;padding:.2em;width:300px;overflow:hidden}.ui-dialog .ui-dialog-titlebar{position:relative;padding:5px 15px;border:0px 0px 0px 1px solid;border-color:white;padding:5px 15px;font-size:18px;text-decoration:none;background:none;-moz-border-radius-bottomright:0px;-webkit-border-bottom-right-radius:0px;-khtml-border-bottom-right-radius:0px;-moz-border-radius-bottomleft:0px;-webkit-border-bottom-left-radius:0px;-khtml-border-bottom-left-radius:0px;border-bottom-left-radius:0px;border-bottom:1px solid #ccc}.ui-dialog .ui-dialog-title{float:left;color:#404040;font-weight:bold;margin-top:5px;margin-bottom:5px;padding:5px}.ui-dialog .ui-dialog-titlebar-close{position:absolute;right:.3em;top:50%;width:19px;margin:-10px 0 0 0;padding:1px;height:18px;font-size:20px;font-weight:bold;line-height:13.5px;text-shadow:0 1px 0 #ffffff;filter:alpha(opacity=25);-khtml-opacity:0.25;-moz-opacity:0.25;opacity:0.25}.ui-dialog .ui-dialog-titlebar-close span{display:block;margin:1px;text-indent:9999px}.ui-dialog .ui-dialog-titlebar-close:hover,.ui-dialog .ui-dialog-titlebar-close:focus{filter:alpha(opacity=90);-khtml-opacity:0.90;-moz-opacity:0.90;opacity:0.90}.ui-dialog .ui-dialog-content{position:relative;border:0;padding:.5em 1em;background:none;overflow:auto;zoom:1}.ui-dialog .ui-dialog-buttonpane{text-align:left;border-width:1px 0 0 0;background-image:none;margin:.5em 0 0 0;background-color:#f5f5f5;padding:5px 15px 5px;border-top:1px solid #ddd;-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;-webkit-box-shadow:inset 0 1px 0 #ffffff;-moz-box-shadow:inset 0 1px 0 #ffffff;box-shadow:inset 0 1px 0 #ffffff;zoom:1;margin-bottom:0}.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset{float:right}.ui-dialog .ui-dialog-buttonpane button{margin:.5em .4em .5em 0;cursor:pointer}.ui-dialog .ui-resizable-se{width:14px;height:14px;right:3px;bottom:3px}.ui-draggable .ui-dialog-titlebar{cursor:move}.ui-dialog-buttonpane .ui-dialog-buttonset .ui-button{color:#ffffff;background-color:#0064cd;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#049cdb), to(#0064cd));background-image:-moz-linear-gradient(top, #049cdb, #0064cd);background-image:-ms-linear-gradient(top, #049cdb, #0064cd);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #049cdb), color-stop(100%, #0064cd));background-image:-webkit-linear-gradient(top, #049cdb, #0064cd);background-image:-o-linear-gradient(top, #049cdb, #0064cd);background-image:linear-gradient(top, #049cdb,#0064cd);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#049cdb', endColorstr='#0064cd', GradientType=0);text-shadow:0 -1px 0 rgba(0,0,0,0.25);border-color:#0064cd #0064cd #003f81;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25)}.ui-slider{position:relative;text-align:left}.ui-slider .ui-slider-handle{position:absolute;z-index:2;width:1.2em;height:1.2em;cursor:default}.ui-slider .ui-slider-range{position:absolute;z-index:1;font-size:.7em;display:block;border:0;background-position:0 0;color:#ffffff;background-color:#0064cd;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#049cdb), to(#0064cd));background-image:-moz-linear-gradient(top, #049cdb, #0064cd);background-image:-ms-linear-gradient(top, #049cdb, #0064cd);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #049cdb), color-stop(100%, #0064cd));background-image:-webkit-linear-gradient(top, #049cdb, #0064cd);background-image:-o-linear-gradient(top, #049cdb, #0064cd);background-image:linear-gradient(top, #049cdb,#0064cd);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#049cdb', endColorstr='#0064cd', GradientType=0);text-shadow:0 -1px 0 rgba(0,0,0,0.25);border-color:#0064cd #0064cd #003f81;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25)}.ui-slider-horizontal{height:.8em}.ui-slider-horizontal .ui-slider-handle{top:-.3em;margin-left:-.6em}.ui-slider-horizontal .ui-slider-range{top:0;height:100%}.ui-slider-horizontal .ui-slider-range-min{left:0}.ui-slider-horizontal .ui-slider-range-max{right:0}.ui-slider-vertical{width:.8em;height:100px}.ui-slider-vertical .ui-slider-handle{left:-.3em;margin-left:0;margin-bottom:-.6em}.ui-slider-vertical .ui-slider-range{left:0;width:100%}.ui-slider-vertical .ui-slider-range-min{bottom:0}.ui-slider-vertical .ui-slider-range-max{top:0}.ui-tabs .ui-tabs-nav{background:none;border-color:#ddd;border-style:solid;border-width:0 0 1px}.ui-tabs{position:relative;padding:.2em;zoom:1;border:0px}.ui-tabs .ui-tabs-nav li:hover,.ui-tabs .ui-tabs-nav li a:hover{background:whiteSmoke;border-bottom:1px solid #ddd;padding-bottom:0px;color:#00438A}.ui-tabs .ui-tabs-nav{margin:0;padding:.2em .2em 0;border-bottom:1px solid #DDD}.ui-tabs .ui-tabs-nav li{text-decoration:none;list-style:none;float:left;position:relative;top:1px;padding:0px 0px 1px 0px;white-space:nowrap;background:none;border:0px}.ui-tabs-nav .ui-state-default{-webkit-box-shadow:0px 0px 0px #ffffff;-moz-box-shadow:0px 0px 0px #ffffff;box-shadow:0px 0px 0px #ffffff}.ui-tabs .ui-tabs-nav li a{float:left;text-decoration:none;cursor:text;padding:0 15px;margin-right:2px;line-height:34px;border:1px solid transparent;-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0}.ui-tabs .ui-tabs-nav li.ui-tabs-selected,.ui-tabs .ui-tabs-nav li.ui-tabs-active{margin-bottom:0;padding-bottom:0px;outline:none}.ui-tabs .ui-tabs-nav li.ui-tabs-selected a,.ui-tabs .ui-tabs-nav li.ui-state-disabled a,.ui-tabs .ui-tabs-nav li.ui-state-processing a,.ui-tabs .ui-tabs-nav li.ui-tabs-active a{background-color:#ffffff;border:1px solid #ddd;border-bottom-color:#ffffff;cursor:default;color:gray;outline:none}.ui-tabs .ui-tabs-nav li.ui-tabs-selected:hover,.ui-tabs .ui-tabs-nav li.ui-tabs-active:hover{background:#ffffff;outline:none}.ui-tabs .ui-tabs-nav li a,.ui-tabs.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-selected a{cursor:pointer;color:#0069D6;background:none;font-weight:normal;margin-bottom:-1px}.ui-tabs .ui-tabs-panel{display:block;border-width:0;padding:1em 1.4em;background:none}.ui-tabs-panel .ui-button{text-decoration:none}.ui-tabs .ui-tabs-hide{display:none !important}.ui-tabs .ui-tabs-nav li{filter:none}.ui-datepicker{width:17em;padding:.2em .2em 0;display:none}.ui-datepicker .ui-datepicker-header{position:relative;padding:.2em 0;border:0px;font-weight:bold;width:100%;padding:4px 0;background-color:#f5f5f5;color:#808080}.ui-datepicker .ui-datepicker-prev,.ui-datepicker .ui-datepicker-next{position:absolute;top:2px;width:1.8em;height:1.8em}.ui-datepicker .ui-datepicker-prev{left:2px}.ui-datepicker .ui-datepicker-next{right:2px}.ui-datepicker .ui-datepicker-prev span,.ui-datepicker .ui-datepicker-next span{display:block;position:absolute;left:50%;margin-left:-8px;top:50%;margin-top:-8px}.ui-datepicker .ui-datepicker-title{margin:0 2.3em;line-height:1.8em;text-align:center}.ui-datepicker .ui-datepicker-title select{font-size:1em;margin:1px 0}.ui-datepicker select.ui-datepicker-month-year{width:100%}.ui-datepicker select.ui-datepicker-month,.ui-datepicker select.ui-datepicker-year{width:49%}.ui-datepicker table{width:100%;font-size:.9em;border-collapse:collapse;margin:0 0 .4em}.ui-datepicker th{padding:.7em .3em;text-align:center;font-weight:bold;border:0}.ui-datepicker td{border:0;padding:1px}.ui-datepicker td span,.ui-datepicker td a{display:block;padding:.2em;text-align:right;text-decoration:none}.ui-datepicker .ui-datepicker-buttonpane{background-image:none;margin:.7em 0 0 0;padding:0 .2em;border-left:0;border-right:0;border-bottom:0}.ui-datepicker .ui-datepicker-buttonpane button{float:right;margin:.5em .2em .4em;cursor:pointer;padding:.2em .6em .3em .6em;width:auto;overflow:visible}.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current{float:left}.ui-datepicker.ui-datepicker-multi{width:auto}.ui-datepicker-multi .ui-datepicker-group{float:left}.ui-datepicker-multi .ui-datepicker-group table{width:95%;margin:0 auto .4em}.ui-datepicker-multi-2 .ui-datepicker-group{width:50%}.ui-datepicker-multi-3 .ui-datepicker-group{width:33.3%}.ui-datepicker-multi-4 .ui-datepicker-group{width:25%}.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header{border-left-width:0}.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header{border-left-width:0}.ui-datepicker-multi .ui-datepicker-buttonpane{clear:left}.ui-datepicker-row-break{clear:both;width:100%;font-size:0em}.ui-datepicker-rtl{direction:rtl}.ui-datepicker-rtl .ui-datepicker-prev{right:2px;left:auto}.ui-datepicker-rtl .ui-datepicker-next{left:2px;right:auto}.ui-datepicker-rtl .ui-datepicker-prev:hover{right:1px;left:auto}.ui-datepicker-rtl .ui-datepicker-next:hover{left:1px;right:auto}.ui-datepicker-rtl .ui-datepicker-buttonpane{clear:right}.ui-datepicker-rtl .ui-datepicker-buttonpane button{float:left}.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current{float:right}.ui-datepicker-rtl .ui-datepicker-group{float:right}.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header{border-right-width:0;border-left-width:1px}.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header{border-right-width:0;border-left-width:1px}.ui-datepicker-cover{display:none;display/**/:block;position:absolute;z-index:-1;filter:mask();top:-4px;left:-4px;width:200px;height:200px}.ui-datepicker th{font-weight:bold;color:gray}.ui-datepicker-today a:hover{background-color:#808080;color:#ffffff}.ui-datepicker-today a{background-color:#BFBFBF;cursor:pointer;padding:0 4px;margin-bottom:0px}.ui-datepicker td a{margin-bottom:0px;border:0px}.ui-datepicker td:hover{color:white}.ui-datepicker td .ui-state-default{border:0px;background:none;margin-bottom:0px;padding:5px;color:gray;text-align:center;filter:none}.ui-datepicker td .ui-state-active{background:#BFBFBF;margin-bottom:0px;font-size:normal;text-shadow:0px;color:white;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.ui-datepicker td .ui-state-default:hover{background:#0064cd;color:white;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.ui-progressbar{height:2em;text-align:left}.ui-progressbar .ui-progressbar-value{margin:-1px;height:100%;color:#ffffff;background-color:#0064cd;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#049cdb), to(#0064cd));background-image:-moz-linear-gradient(top, #049cdb, #0064cd);background-image:-ms-linear-gradient(top, #049cdb, #0064cd);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #049cdb), color-stop(100%, #0064cd));background-image:-webkit-linear-gradient(top, #049cdb, #0064cd);background-image:-o-linear-gradient(top, #049cdb, #0064cd);background-image:linear-gradient(top, #049cdb,#0064cd);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#049cdb', endColorstr='#0064cd', GradientType=0);text-shadow:0 -1px 0 rgba(0,0,0,0.25);border-color:#0064cd #0064cd #003f81;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25)}.ui-toolbar{padding:7px 14px;margin:0 0 18px;background-color:#f5f5f5;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#fff), to(#f5f5f5));background-image:-moz-linear-gradient(top, #fff, #f5f5f5);background-image:-ms-linear-gradient(top, #fff, #f5f5f5);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #fff), color-stop(100%, #f5f5f5));background-image:-webkit-linear-gradient(top, #fff, #f5f5f5);background-image:-o-linear-gradient(top, #fff, #f5f5f5);background-image:linear-gradient(top, #ffffff,#f5f5f5);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#f5f5f5', GradientType=0);border:1px solid #ddd;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;-webkit-box-shadow:inset 0 1px 0 #ffffff;-moz-box-shadow:inset 0 1px 0 #ffffff;box-shadow:inset 0 1px 0 #ffffff}.ui-dialog-buttonset .ui-button:nth-child(2){cursor:pointer;display:inline-block;background-color:#e6e6e6;background-repeat:no-repeat;background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#fff), color-stop(25%, #fff), to(#e6e6e6));background-image:-webkit-linear-gradient(#fff, #fff 25%, #e6e6e6);background-image:-moz-linear-gradient(top, #fff, #fff 25%, #e6e6e6);background-image:-ms-linear-gradient(#fff, #fff 25%, #e6e6e6);background-image:-o-linear-gradient(#fff, #fff 25%, #e6e6e6);background-image:linear-gradient(#ffffff,#ffffff 25%,#e6e6e6);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0);padding:5px 14px 6px;text-shadow:0 1px 1px rgba(255,255,255,0.75);color:#333;font-size:13px;line-height:normal;border:1px solid #ccc;border-bottom-color:#bbb;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);-webkit-transition:0.1s linear all;-moz-transition:0.1s linear all;-ms-transition:0.1s linear all;-o-transition:0.1s linear all;transition:0.1s linear all;overflow:visible}div.wijmo-wijmenu{padding:0 20px;background-color:#222;background-color:#222222;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#333), to(#222));background-image:-moz-linear-gradient(top, #333, #222);background-image:-ms-linear-gradient(top, #333, #222);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #333), color-stop(100%, #222));background-image:-webkit-linear-gradient(top, #333, #222);background-image:-o-linear-gradient(top, #333, #222);background-image:linear-gradient(top, #333333,#222222);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0);-webkit-box-shadow:0 1px 3px rgba(0,0,0,0.25),inset 0 -1px 0 rgba(0,0,0,0.1);-moz-box-shadow:0 1px 3px rgba(0,0,0,0.25),inset 0 -1px 0 rgba(0,0,0,0.1);box-shadow:0 1px 3px rgba(0,0,0,0.25),inset 0 -1px 0 rgba(0,0,0,0.1)}.wijmo-wijmenu .ui-state-default{box-shadow:none;color:#BFBFBF}.wijmo-wijmenu .ui-state-default .wijmo-wijmenu-text{color:#BFBFBF}.wijmo-wijmenu .ui-state-hover{background:#444;background:rgba(255,255,255,0.05)}.wijmo-wijmenu .ui-state-hover .wijmo-wijmenu-text{color:#ffffff}div.wijmo-wijmenu .ui-widget-header h3{position:relative;margin-top:1px;padding:0}.wijmo-wijmenu h3 a{color:#FFFFFF;display:block;float:left;font-size:20px;font-weight:200;line-height:1;margin-left:-20px;margin-top:1px;padding:8px 20px 12px}.wijmo-wijmenu h3 a:hover{background-color:rgba(255,255,255,0.05);color:#FFFFFF;text-decoration:none}.wijmo-wijmenu .ui-widget-header{border:0px}.wijmo-wijmenu .wijmo-wijmenu-parent .wijmo-wijmenu-child{padding:0.3em 0}div.wijmo-wijmenu .wijmo-wijmenu-item .wijmo-wijmenu-child{background:#333;border:0;margin:0;padding:6px 0;width:160px;-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;-webkit-box-shadow:0 2px 4px rgba(0,0,0,0.2);-moz-box-shadow:0 2px 4px rgba(0,0,0,0.2);box-shadow:0 2px 4px rgba(0,0,0,0.2)}div.wijmo-wijmenu .wijmo-wijmenu-item{margin:0;border:0}.wijmo-wijmenu a.wijmo-wijmenu-link{margin:0;line-height:19px;padding:10px 10px 11px;border:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}div.wijmo-wijmenu .wijmo-wijmenu-child .wijmo-wijmenu-link{display:block;float:none;padding:4px 15px;width:auto}div.wijmo-wijmenu .wijmo-wijmenu-child .wijmo-wijmenu-text{float:none}.wijmo-wijmenu .wijmo-wijmenu-item .wijmo-wijmenu-child .ui-state-hover{background:#191919}.wijmo-wijmenu .wijmo-wijmenu-item .wijmo-wijmenu-separator{padding:5px 0;background-image:none;background-color:#222;border-top:1px solid #444;border-bottom:0;border-left:0;border-right:0}.wijmo-wijmenu-horizontal .wijmo-wijmenu-child li.wijmo-wijmenu-separator{width:96%;margin:1px 2%}.wijmo-wijmenu .wijmo-wijmenu-item input{-moz-transition:none 0s ease 0s;background-color:rgba(255,255,255,0.3);border:1px solid #111111;border-radius:4px 4px 4px 4px;box-shadow:0 1px 2px rgba(0,0,0,0.1) inset,0 1px 0 rgba(255,255,255,0.25);color:rgba(255,255,255,0.75);font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;line-height:1;margin:5px 10px 0 10px;padding:4px 9px;width:100px}.wijmo-wijmenu .wijmo-wijmenu-item input:hover{background-color:rgba(255,255,255,0.5);color:#FFFFFF}.wijmo-wijmenu .wijmo-wijmenu-item input:focus{background-color:#FFFFFF;border:0 none;box-shadow:0 0 3px rgba(0,0,0,0.15);color:#404040;outline:0 none;padding:5px 10px;text-shadow:0 1px 0 #FFFFFF}.wijmo-wijmenu .ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default{text-shadow:none}.wijmo-wijmenu .ui-state-default{box-shadow:none;color:#BFBFBF;filter:none}.fc{direction:ltr;text-align:left}.fc table{border-collapse:collapse;border-spacing:0}.fc .btn{line-height:1.2em}html .fc{font-size:1em}.fc table{font-size:1em}.fc td,.fc th{padding:1px;vertical-align:top}.fc-header td{white-space:nowrap}.fc-header-left{width:25%;text-align:left}.fc-header-center{text-align:center}.fc-header-right{width:25%;text-align:right}.fc-header-title{display:inline-block;vertical-align:top}.fc-header-title h2{margin-top:0;white-space:nowrap}.fc .fc-header-space{padding-left:10px}.fc-header .fc-button{margin-bottom:1em;vertical-align:top;margin-right:-1px}.fc-header .fc-corner-right{margin-right:1px}.fc-header .ui-corner-right{margin-right:0}.fc-header .fc-state-hover,.fc-header .ui-state-hover{z-index:2}.fc-header .fc-state-down{z-index:3}.fc-header .fc-state-active,.fc-header .ui-state-active,.fc-header .ui-state-down{z-index:4;background-color:#e6e6e6;background-color:#d9d9d9;background-image:none;outline:0;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05)}.fc-content{clear:both}.fc-view{width:100%}.fc .ui-widget-header{border-color:#dddddd;padding:4px 0}thead th.fc-first{border-top-left-radius:5px;-moz-border-top-left-radius:5px;-webkit-border-top-left-radius:5px}thead th.fc-last{border-top-right-radius:5px;-moz-border-top-right-radius:5px;-webkit-border-top-right-radius:5px}.ui-state-highlight.fc-today{background:#f4f4f4;margin:2px !important;border:0;border-left:1px solid #dddddd;border-top:1px solid #dddddd;border-radius:0;-moz-border-radius:0;-webkit-border-radius:0}.fc-button{position:relative;display:inline-block;cursor:pointer}.fc-button-inner{position:relative;float:left;overflow:hidden}.fc-button-content{position:relative;float:left;height:1.9em;line-height:1.9em;padding:0 0.6em;white-space:nowrap}.fc-button-content .fc-icon-wrap{position:relative;float:left;top:50%}.fc-button-content .ui-icon{position:relative;float:left;margin-top:-50%;*margin-top:0;*top:-50%}.fc-state-default{border-style:solid;border-color:#cccccc #bbbbbb #aaaaaa;background:#f3f3f3;color:black}.fc-state-default .fc-button-effect{position:absolute;top:50%;left:0}.fc-state-default .fc-button-effect span{position:absolute;top:-100px;left:0;width:500px;height:100px;border-width:100px 0 0 1px;border-style:solid;border-color:white;background:#444444;opacity:0.09;filter:alpha(opacity=9)}.fc-state-default .fc-button-inner{border-style:solid;border-color:#cccccc #bbbbbb #aaaaaa;background:#f3f3f3;color:black}.fc-state-hover{border-color:#999999}.fc-state-hover .fc-button-inner{border-color:#999999}.fc-state-down{border-color:#555555;background:#777777}.fc-state-down .fc-button-inner{border-color:#555555;background:#777777}.fc-state-active{border-color:#555555;background:#777777;color:white}.fc-state-active .fc-button-inner{border-color:#555555;background:#777777;color:white}.fc-state-disabled{color:#999999;border-color:#dddddd;cursor:default}.fc-state-disabled .fc-button-inner{color:#999999;border-color:#dddddd}.fc-state-disabled .fc-button-effect{display:none}.fc-event{border-style:solid;border-width:0;font-size:0.85em;cursor:default}a.fc-event,.fc-event-draggable{cursor:pointer}a.fc-event{text-decoration:none}.fc-rtl .fc-event{text-align:right}.fc-event-skin{border:0}.fc-event-inner{position:relative;width:100%;height:100%;border-style:solid;border-width:0;overflow:hidden}.fc-event-time,.fc-event-title{padding:0 1px}.fc-event-hori{border-width:1px 0;margin-bottom:1px}.fc-event-hori .ui-resizable-e{top:0 !important}table.fc-border-separate{border-collapse:separate}.fc-border-separate th,.fc-border-separate td{border-width:1px 0 0 1px}.fc-border-separate th.fc-last,.fc-border-separate td.fc-last{border-right-width:1px}.fc-border-separate tr.fc-last th,.fc-border-separate tr.fc-last td{border-bottom-width:1px}.fc-border-separate tbody tr.fc-first td,.fc-border-separate tbody tr.fc-first th{border-top-width:0}.fc-grid th{text-align:center}.fc-grid .fc-day-number{float:right;padding:0 2px}.fc-grid .fc-other-month .fc-day-number{opacity:0.3;filter:alpha(opacity=30)}.fc-rtl .fc-grid .fc-day-number{float:left}.fc-rtl .fc-grid .fc-event-time{float:right}.fc-agenda table{border-collapse:separate}.fc-agenda-days th{text-align:center}.fc-agenda .fc-agenda-axis{width:50px;padding:0 4px;vertical-align:middle;text-align:right;white-space:nowrap;font-weight:normal}.fc-agenda .fc-day-content{padding:2px 2px 1px}.fc-agenda-days .fc-agenda-axis{border-right-width:1px}.fc-agenda-days .fc-col0{border-left-width:0}.fc-agenda-allday th{border-width:0 1px}.fc-agenda-allday .fc-day-content{min-height:34px}.fc-agenda-divider-inner{height:2px;overflow:hidden}.fc-widget-header .fc-agenda-divider-inner{background:#eeeeee}.fc-agenda-slots th{border-width:1px 1px 0}.fc-agenda-slots td{border-width:1px 0 0;background:none}.fc-agenda-slots td div{height:20px}.fc-agenda-slots tr.fc-slot0 th,.fc-agenda-slots tr.fc-slot0 td{border-top-width:0}.fc-agenda-slots tr.fc-minor th,.fc-agenda-slots tr.fc-minor td{border-top-style:dotted}.fc-agenda-slots tr.fc-minor th.ui-widget-header{*border-top-style:solid}.fc-event-vert{border-width:0 1px}.fc-event-vert .fc-event-head,.fc-event-vert .fc-event-content{position:relative;z-index:2;width:100%;overflow:hidden}.fc-event-vert .fc-event-time{white-space:nowrap;font-size:10px}.fc .ui-draggable-dragging .fc-event-bg,.fc-select-helper .fc-event-bg{display:none}.fc-event-vert .ui-resizable-s{bottom:0 !important}.chosen-select{width:100%}.chosen-select-deselect{width:100%}.chosen-container{display:inline-block;font-size:14px;position:relative;vertical-align:middle;margin-bottom:9px !important}.chosen-container .chosen-drop{background:white;border:1px solid #925d97;border-top:0px;-webkit-border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px;border-bottom-right-radius:4px;-webkit-border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px;border-bottom-left-radius:4px;-webkit-box-shadow:0 8px 8px rgba(0,0,0,0.25);-moz-box-shadow:0 8px 8px rgba(0,0,0,0.25);box-shadow:0 8px 8px rgba(0,0,0,0.25);margin-top:-1px;position:absolute;top:100%;left:-9000px;z-index:1060}.chosen-container.chosen-with-drop .chosen-drop{left:0;right:0}.chosen-container .chosen-results{color:#979ca6;margin:0 4px 4px 0;max-height:240px;padding:0 0 0 4px;position:relative;overflow-x:hidden;overflow-y:auto;-webkit-overflow-scrolling:touch}.chosen-container .chosen-results li{display:none;list-style:none;margin:0;padding:2px 6px}.chosen-container .chosen-results li em{background:#feffde;font-style:normal}.chosen-container .chosen-results li.group-result{display:list-item;cursor:default;color:#999;font-weight:bold}.chosen-container .chosen-results li.group-option{padding-left:15px}.chosen-container .chosen-results li.active-result{cursor:pointer;display:list-item}.chosen-container .chosen-results li.highlighted{background-color:#329897;color:white}.chosen-container .chosen-results li.highlighted em{background:transparent}.chosen-container .chosen-results li.disabled-result{display:list-item;color:#e8ebf0}.chosen-container .chosen-results .no-results{background:#f8f9fa;display:list-item}.chosen-container .chosen-results-scroll{background:white;margin:0 4px;position:absolute;text-align:center;width:321px;z-index:1}.chosen-container .chosen-results-scroll span{display:inline-block;height:20px;text-indent:-5000px;width:9px}.chosen-container .chosen-results-scroll-down{bottom:0}.chosen-container .chosen-results-scroll-down span{background:url("chosen-sprite.png") no-repeat -4px -3px}.chosen-container .chosen-results-scroll-up span{background:url("chosen-sprite.png") no-repeat -22px -3px}.chosen-container-single .chosen-single{background:white;border:1px solid rgba(0,0,0,0.2);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;box-shadow:none;color:#797e89;display:block;height:28px;overflow:hidden;line-height:30px;padding:0 0 0 10px;position:relative;text-decoration:none;white-space:nowrap;overflow:hidden}.chosen-container-single .chosen-single span{display:block;margin-right:26px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.chosen-container-single .chosen-single abbr{background:url("chosen-sprite.png") right top no-repeat;display:block;font-size:1px;height:10px;position:absolute;right:26px;top:10px;width:12px}.chosen-container-single .chosen-single abbr:hover{background-position:right -11px}.chosen-container-single .chosen-single.chosen-disabled .chosen-single abbr:hover{background-position:right 2px}.chosen-container-single .chosen-single div{display:block;height:100%;position:absolute;top:0;right:0;width:18px}.chosen-container-single .chosen-single div b{background:url("chosen-sprite.png") no-repeat 0 5px;display:block;height:100%;width:100%}.chosen-container-single .chosen-default{color:#e8ebf0}.chosen-container-single .chosen-search{margin:0;padding:3px 4px;position:relative;white-space:nowrap;z-index:1000}.chosen-container-single .chosen-search input{background:url("chosen-sprite.png") no-repeat 100% -20px,#fff;border:1px solid rgba(0,0,0,0.2);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-border-top-right-radius:4px;-moz-border-radius-topright:4px;border-top-right-radius:4px;-webkit-border-top-left-radius:4px;-moz-border-radius-topleft:4px;border-top-left-radius:4px;-webkit-border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px;border-bottom-right-radius:4px;-webkit-border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px;border-bottom-left-radius:4px;margin:1px 0 0 0;padding:4px 20px 4px 4px;width:100%;height:auto;line-height:normal;font-size:1em}.chosen-container-single .chosen-search input:focus{box-shadow:none;-webkit-box-shadow:none;-moz-box-shadow:none}.chosen-container-single .chosen-drop{margin-top:-1px;-webkit-border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px;border-bottom-right-radius:4px;-webkit-border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px;border-bottom-left-radius:4px;-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.chosen-container-single-nosearch .chosen-search input{position:absolute;left:-9000px}.chosen-container-multi .chosen-choices{background-color:white;border:1px solid rgba(0,0,0,0.2);-webkit-border-top-right-radius:4px;-moz-border-radius-topright:4px;border-top-right-radius:4px;-webkit-border-top-left-radius:4px;-moz-border-radius-topleft:4px;border-top-left-radius:4px;-webkit-border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px;border-bottom-right-radius:4px;-webkit-border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px;border-bottom-left-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);cursor:text;height:auto !important;height:1%;margin:0;overflow:hidden;padding:0;position:relative}.chosen-container-multi .chosen-choices li{float:left;list-style:none}.chosen-container-multi .chosen-choices .search-field{margin:0;padding:0;white-space:nowrap}.chosen-container-multi .chosen-choices .search-field input{background:transparent !important;border:0 !important;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;color:#979ca6;height:22px;margin:0;padding:4px;outline:0}.chosen-container-multi .chosen-choices .search-field .default{color:#999}.chosen-container-multi .chosen-choices .search-choice{-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box;background-color:#f8f9fa;border:1px solid rgba(0,0,0,0.2);-webkit-border-top-right-radius:4px;-moz-border-radius-topright:4px;border-top-right-radius:4px;-webkit-border-top-left-radius:4px;-moz-border-radius-topleft:4px;border-top-left-radius:4px;-webkit-border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px;border-bottom-right-radius:4px;-webkit-border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px;border-bottom-left-radius:4px;background-color:#fcfcfd;background-image:-moz-linear-gradient(top, #fff, #f8f9fa);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#fff), to(#f8f9fa));background-image:-webkit-linear-gradient(top, #fff, #f8f9fa);background-image:-o-linear-gradient(top, #fff, #f8f9fa);background-image:linear-gradient(to bottom, #ffffff,#f8f9fa);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFF8F9FA', GradientType=0);-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);color:#8b8b8b;cursor:default;line-height:13px;margin:6px 0 3px 5px;padding:3px 20px 3px 5px;position:relative}.chosen-container-multi .chosen-choices .search-choice .search-choice-close{background:url("chosen-sprite.png") right top no-repeat;display:block;font-size:1px;height:10px;position:absolute;right:4px;top:5px;width:12px}.chosen-container-multi .chosen-choices .search-choice .search-choice-close:hover{background-position:right -11px}.chosen-container-multi .chosen-choices .search-choice-focus{background:#d4d4d4}.chosen-container-multi .chosen-choices .search-choice-focus .search-choice-close{background-position:right -11px}.chosen-container-multi .chosen-results{margin:0 0 0 0;padding:0}.chosen-container-multi .chosen-drop .result-selected{display:none}.chosen-container-active .chosen-single{border:1px solid #925d97;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px #925d97;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px #925d97;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px #925d97;-webkit-transition:border linear 0.2s, box-shadow linear 0.2s;-moz-transition:border linear 0.2s, box-shadow linear 0.2s;-o-transition:border linear 0.2s, box-shadow linear 0.2s;transition:border linear 0.2s, box-shadow linear 0.2s}.chosen-container-active.chosen-with-drop .chosen-single{background:white;border:1px solid #925d97;-moz-border-radius-bottomright:0;border-bottom-right-radius:0;-moz-border-radius-bottomleft:0;border-bottom-left-radius:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px #925d97;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px #925d97;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px #925d97;-webkit-transition:border linear 0.2s, box-shadow linear 0.2s;-moz-transition:border linear 0.2s, box-shadow linear 0.2s;-o-transition:border linear 0.2s, box-shadow linear 0.2s;transition:border linear 0.2s, box-shadow linear 0.2s}.chosen-container-active.chosen-with-drop .chosen-single div{background:transparent;border-left:none}.chosen-container-active.chosen-with-drop .chosen-single div b{background-position:-18px 5px}.chosen-container-active .chosen-choices{border:1px solid #925d97;-webkit-border-bottom-right-radius:0;-moz-border-radius-bottomright:0;border-bottom-right-radius:0;-webkit-border-bottom-left-radius:0;-moz-border-radius-bottomleft:0;border-bottom-left-radius:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px #925d97;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px #925d97;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px #925d97;-webkit-transition:border linear 0.2s, box-shadow linear 0.2s;-moz-transition:border linear 0.2s, box-shadow linear 0.2s;-o-transition:border linear 0.2s, box-shadow linear 0.2s;transition:border linear 0.2s, box-shadow linear 0.2s}.chosen-container-active .chosen-choices .search-field input{color:#111 !important}.chosen-disabled{cursor:default;opacity:0.5 !important}.chosen-disabled .chosen-single{cursor:default}.chosen-disabled .chosen-choices .search-choice .search-choice-close{cursor:default}.chosen-rtl{text-align:right}.chosen-rtl .chosen-single{padding:0 8px 0 0;overflow:visible}.chosen-rtl .chosen-single span{margin-left:26px;margin-right:0;direction:rtl}.chosen-rtl .chosen-single div{left:7px;right:auto}.chosen-rtl .chosen-single abbr{left:26px;right:auto}.chosen-rtl .chosen-choices .search-field input{direction:rtl}.chosen-rtl .chosen-choices li{float:right}.chosen-rtl .chosen-choices .search-choice{margin:6px 5px 3px 0;padding:3px 5px 3px 19px}.chosen-rtl .chosen-choices .search-choice .search-choice-close{background-position:right top;left:4px;right:auto}.chosen-rtl.chosen-container-single .chosen-results{margin:0 0 4px 4px;padding:0 4px 0 0}.chosen-rtl .chosen-results .group-option{padding-left:0;padding-right:15px}.chosen-rtl.chosen-container-active.chosen-with-drop .chosen-single div{border-right:none}.chosen-rtl .chosen-search input{background:url("chosen-sprite.png") no-repeat -28px -20px,#fff;direction:rtl;padding:4px 5px 4px 20px}.control-group.error .chosen-container-single .chosen-single{border-color:#b94a48;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.error .chosen-container-active .chosen-single{border:1px solid #b94a48;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392}.control-group.error .chosen-container-active.chosen-with-drop .chosen-single{border:1px solid #b94a48;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392}h1,h2{font-family:"Quicksand", Helvetica, Arial, sans-serif;margin-top:0}h1{font-size:1.5em !important;font-weight:700;line-height:1.25em}h2{font-size:1.4em !important;font-weight:700}h3{font-size:1.25em !important}h4{font-size:1.2em !important}h5{font-size:1.1em !important;margin:10px 0 0 0}.content-header{margin-bottom:24px}.content-header h1{margin-bottom:0px}p:last-child{margin-bottom:0}.muted{color:#979ca6}td p,address{margin:0}.alert ul{margin:0 0 0 25px}.btn-toolbar .dropdown,.btn-toolbar .link{font-size:14px;vertical-align:middle}.btn-group+.link{margin-left:24px}h1 .btn,h2 .btn,h3 .btn,h4 .btn{font-weight:normal;font-family:"Varela Round", Helvetica, Arial, sans-serif}dl{margin-top:0}.dl-horizontal{padding:12px 0;border-top:1px solid #e8ebf0;margin:0}.dl-horizontal dd{margin-bottom:12px}.dl-horizontal dd:last-child{margin-bottom:0}@media screen and (min-width: 768px){.dl-horizontal dt{width:180px}.dl-horizontal dd{margin-left:200px;margin-bottom:0}}.dl-horizontal:last-child{border-bottom:1px solid #e8ebf0;margin-bottom:24px}table.table{border-bottom:1px solid #e8ebf0}div.table{margin-top:30px}.table td,.table th,.table-basic td,.table-basic th{padding:4px;padding-left:0;border-color:#e8ebf0}.table td:last-child,.table th:last-child,.table-basic td:last-child,.table-basic th:last-child{padding-right:0}.table-striped td:first-child{padding-left:8px}.table-hover tbody tr:hover>td,.table-hover tbody tr:hover>th{background-color:#fbf8fb}.table-grouped thead:first-child th{padding-top:0px}.table-grouped th{padding-top:30px}.table-grouped th h2{margin-bottom:0}td.center,th.center{text-align:center}td.right,th.right{text-align:right}select[class*="span"]{min-height:30px;height:30px;*margin-top:4px;line-height:30px}legend{font-size:1.2em;line-height:180%}legend small{font-size:85%;margin-left:10px}.nowrap{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}a.anchor{color:inherit;text-decoration:none}#page-navigation{margin-top:13px;float:left}@media screen and (min-width: 0) and (max-width: 767px){#page-navigation{clear:both;width:100%}}#page-navigation .nav{margin:0;padding:0;border:0;*zoom:1;font-family:"Varela Round", Helvetica, Arial, sans-serif}#page-navigation .nav:before,#page-navigation .nav:after{display:table;content:"";line-height:0}#page-navigation .nav:after{clear:both}#page-navigation .nav li{list-style-image:none;list-style-type:none;margin-left:0;white-space:nowrap;display:inline;float:left;padding-left:15px;padding-right:15px}#page-navigation .nav li:first-child,#page-navigation .nav li.first{padding-left:0}#page-navigation .nav li:last-child{padding-right:0}#page-navigation .nav li.last{padding-right:0}@media screen and (min-width: 0) and (max-width: 767px){#page-navigation .nav li{float:none;display:block;padding:0;margin:0}}#page-navigation .nav li.active a{color:white;background-color:#329897}#page-navigation .nav li a{-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;font-size:1.077em;text-transform:uppercase;color:white;padding:2px 5px}#page-navigation .nav li a:hover{text-shadow:0 -1px 0 #6a396f;background-color:#925d97;color:white}.nav-sub{*zoom:1;border-bottom:1px solid #e8ebf0;clear:both;font-size:0.938em}.nav-sub:before,.nav-sub:after{display:table;content:"";line-height:0}.nav-sub:after{clear:both}.nav-sub>li{float:left}.nav-sub>li a{-webkit-transition:border-color 300ms;-moz-transition:border-color 300ms;-o-transition:border-color 300ms;transition:border-color 300ms;padding:8px 0;margin-top:2px;margin-right:30px;line-height:14px;border-bottom:2px solid white}.nav-sub>li a:hover{background:none;border-color:#e8ebf0}.nav-sub>.active>a{border-color:#329897}.nav-sub>.active>a:hover{border-color:#925d97}.level{display:block;font-family:"Quicksand", Helvetica, Arial, sans-serif;font-size:1.5em !important;font-weight:bold;line-height:1.25em;padding:16px 20px 0 20px}.level+.nav-sub{padding:0 20px 0 20px;margin-bottom:0;border-bottom:none}.level+.nav-sub a{color:#979ca6;border:none}.level+.nav-sub .active a{color:#8b8b8b}.breadcrumb{float:right;margin:15px 0 0;padding:5px 15px;list-style:none;background:none;border:none;font-family:"Quicksand", Helvetica, Arial, sans-serif}.breadcrumb ul{margin:0}.breadcrumb ul li{color:#979ca6;display:inline-block;text-shadow:0 1px 0 white}.breadcrumb ul li.divider{padding:0 5px;color:#979ca6}.pagination{*zoom:1;margin-top:0}.pagination:before,.pagination:after{display:table;content:"";line-height:0}.pagination:after{clear:both}.pagination ul>.active>a,.pagination ul>.active>span{color:#8b8b8b}.pagination-bar .pagination{margin-right:20px;float:left}.pagination-bar .pagination-info{line-height:20px;padding:5px 0px}.nav-pills>li>a{padding-top:7px;padding-bottom:7px;border:1px solid #e8ebf0;margin-top:0;margin-bottom:0;margin-right:0}.nav-pills>.active>a{border-color:#329897}.nav-pills>.active>a:hover,.nav-pills>.active>a:focus{background-color:#925d97;border-color:#925d97 !important}.nav-pills .open .dropdown-toggle,.nav.nav-pills>li.dropdown.open.active>.dropdown-toggle{background-color:#925d97;border-color:#925d97}.toolbar-pills{float:left;margin-right:15px}.toolbar-pills .nav-pills{float:left}.toolbar-pills li>a{margin-right:0px}.toolbar-pills+.btn-toolbar{float:left}.group-pills>li>a{border-left:none;-webkit-border-radius:0px;-moz-border-radius:0px;border-radius:0px;margin-right:0}.group-pills>li:first-child>a{border-left:1px solid #e8ebf0;-webkit-border-top-left-radius:6px;-moz-border-radius-topleft:6px;border-top-left-radius:6px;-webkit-border-bottom-left-radius:6px;-moz-border-radius-bottomleft:6px;border-bottom-left-radius:6px}.group-pills>li:last-child>a{-webkit-border-top-right-radius:6px;-moz-border-radius-topright:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;-moz-border-radius-bottomright:6px;border-bottom-right-radius:6px;margin-right:5px}.dropdown-menu .dropdown-menu{min-width:0}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus,.dropdown-submenu:hover>a,.dropdown-submenu:focus>a{background:#925d97}.pull-right>.btn-group>.dropdown-menu,.pull-right>.dropdown>.dropdown-menu{right:0;left:auto}.nav-left .inner{padding:18px 18px 18px 20px;border-right:2px solid #e8ebf0;-webkit-border-top-left-radius:6px;-moz-border-radius-topleft:6px;border-top-left-radius:6px;-webkit-border-bottom-left-radius:6px;-moz-border-radius-bottomleft:6px;border-bottom-left-radius:6px;background:#f8f9fa}@media screen and (min-width: 0) and (max-width: 767px){.nav-left{display:none !important}}#page.with-left-nav{display:table;table-layout:fixed;width:100%}#page.with-left-nav,#page.with-left-nav:before,#page.with-left-nav:after{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}#page.with-left-nav>.nav-left{display:table-cell;width:196px;vertical-align:top}#page.with-left-nav>.container-fluid{display:table-cell;vertical-align:top}#page.with-left-nav>.nav-left+.container-fluid{padding-left:0}@media screen and (min-width: 768px){#page.with-left-nav .container-fluid>.sheet,#page.with-left-nav .container-fluid>.sheet>.container-shadow>#content{-webkit-border-top-left-radius:0;-moz-border-radius-topleft:0;border-top-left-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}}.nav-left-list,.nav-left-list ul{list-style:none;margin:0;width:100%}.nav-left-list li{line-height:1.2em}.nav-left-list li>a{display:block;padding:4px 20px;margin:2px -18px 0 -20px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.nav-left-list ul li>a{padding-left:32px}.nav-left-list ul ul li>a{padding-left:44px}.nav-left-list .active>a{background:#e8ebf0}.nav-left-back{font-size:0.85em}.nav-left-title{line-height:20px;display:block;padding:6px 20px;margin:0px -18px 0 -20px}.nav-left-title.active{background:#e8ebf0}.nav-left .divider{display:block;text-transform:uppercase;font-size:0.85em;color:#979ca6;margin:20px 0 8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.btn-toolbar{margin-top:0;margin-bottom:24px}.btn{color:#257271}.btn:hover,.btn:focus,.btn:active{color:white;background:#925d97;text-shadow:0 -1px 1px #6a396f;border-color:#6a396f}.btn:hover .icon,.btn:hover [class^="icon-"],.btn:hover [class*=" icon-"],.btn:focus .icon,.btn:focus [class^="icon-"],.btn:focus [class*=" icon-"],.btn:active .icon,.btn:active [class^="icon-"],.btn:active [class*=" icon-"]{background-image:url(/assets/glyphicons-halflings-white-b24ef881bcd5c1b763bd67c3bfea6620.png)}.btn:hover .caret,.btn:focus .caret,.btn:active .caret{border-top-color:white}.btn .icon,.btn [class^="icon-"],.btn [class*=" icon-"]{background-image:url(/assets/glyphicons-halflings-green-69bfe940a857558b1ce2f352218017c0.png)}.btn .caret{border-top-color:#257271}.btn.btn-primary,.btn.btn-primary:hover,.btn.btn-warning,.btn.btn-warning:hover,.btn.btn-danger,.btn.btn-danger:hover,.btn.btn-success,.btn.btn-success:hover,.btn.btn-info,.btn.btn-info:hover,.btn.btn-inverse,.btn.btn-inverse:hover{color:white}.btn-primary{color:white;background:#329897;border-color:#329897;text-shadow:0 -1px 1px #257271}.btn-group.open .btn.dropdown-toggle{background:#6a396f;color:white;border-color:#6a396f}a.btn.disabled,a.btn[disabled="disabled"]{pointer-events:none}.form-horizontal .control-group{margin-bottom:0}@media screen and (min-width: 480px){.form-horizontal .btn-toolbar{margin-left:180px}.form-horizontal.form-noindent .btn-toolbar{margin-left:0}}fieldset{margin-bottom:18px}legend{margin-bottom:10px}legend+*{-webkit-margin-top-collapse:separate;margin-top:10px}td select,td input[type="text"],td input[type="number"]{margin-bottom:0 !important}select,textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{display:inline-block;height:20px;padding:4px 6px;margin-bottom:9px !important;font-size:14px;line-height:20px;color:#797e89}select:focus,textarea:focus,input[type="text"]:focus,input[type="password"]:focus,input[type="datetime"]:focus,input[type="datetime-local"]:focus,input[type="date"]:focus,input[type="month"]:focus,input[type="time"]:focus,input[type="week"]:focus,input[type="number"]:focus,input[type="email"]:focus,input[type="url"]:focus,input[type="search"]:focus,input[type="tel"]:focus,input[type="color"]:focus,.uneditable-input:focus{border-color:#925d97;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px #925d97;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px #925d97;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px #925d97}select.time{width:55px;height:30px}select[class*="span"][multiple]{height:120px}label.checkbox,label.radio{height:20px;font-size:14px;line-height:20px;margin-right:10px}.label-columns .controls label{overflow:hidden}.label-columns .controls label:hover{background:#f8f9fa}.label-columns .control-group+.control-group{margin-top:2em}.no-csscolumns .label-columns .controls{*zoom:1}.no-csscolumns .label-columns .controls:before,.no-csscolumns .label-columns .controls:after{display:table;content:"";line-height:0}.no-csscolumns .label-columns .controls:after{clear:both}.no-csscolumns .label-columns .controls label{float:left;width:200px;margin-left:0 !important}.csscolumns .label-columns .controls{-moz-columns:200px 4;-webkit-columns:200px 4;columns:200px 4}.csscolumns .label-columns .controls label{width:80%;white-space:nowrap;text-overflow:ellipsis;margin-left:0 !important}.controls div.inline,.controls p.inline{padding-top:5px;display:inline-block}.controls .help-inline{margin-left:10px;padding-bottom:8px}.controls .inline+.help-inline{padding-bottom:0}textarea{height:auto}.fields-separation hr{margin:7px 0 15px;border-top-color:#e8ebf0}.control-group .fields .controls-row{border-bottom:1px solid #e8ebf0;margin-bottom:10px;padding-top:5px;padding-bottom:5px}.control-group .fields .help-inline{float:right}.form-horizontal .controls .help-block{margin:0 0 10px 0}.controls>.text{line-height:29px}.remove_nested_fields{color:#dc795b;margin:0;padding:0}.remove_nested_fields i{margin:5px}.remove_nested_fields:hover{color:#d35731}.hp{position:absolute;left:-9999px}td.issue,td.revoke{width:25px}.disabled{opacity:0.3;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=30)";filter:alpha(opacity=30)}.popover{max-width:650px;width:650px}.filter_toggle{cursor:pointer;cursor:hand}body{padding:0}#page-header{*zoom:1;margin:0;padding:10px 10px 0;background:#99bf62}#page-header:before,#page-header:after{display:table;content:"";line-height:0}#page-header:after{clear:both}@media screen and (min-width: 768px){#page-header{padding:10px 20px 0}}#page-header .options>ul>li>a{color:#ecf3e1}#page-header .options>ul>li>a:hover{color:white}.brand{display:block;float:left;width:230px;height:30px;padding:10px 0;margin-right:40px;text-indent:-9999px;white-space:nowrap;font-family:"Quicksand", Helvetica, Arial, sans-serif;font-weight:bold;font-size:2.462em;line-height:1.250em;text-shadow:0 1px 0 white;background:url(logo.png) transparent no-repeat 0px 10px}@media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min-device-pixel-ratio: 2){.brand{background:url(logo.png) transparent no-repeat 0px 10px;background-size:230px 30px}}.options{float:right;margin-top:10px}.options>ul{margin:0;padding:0;border:0;overflow:hidden}.options>ul>li{list-style-image:none;list-style-type:none;margin-left:0;white-space:nowrap;display:inline;float:left;padding-left:10px;padding-right:10px}.options .lang .active{text-decoration:none}.options a{line-height:30px}.options form,.options input[type="search"]{margin-bottom:0 !important}.options a,a.link{text-decoration:underline}.options a:hover,a.link:hover{background:none}.dropdown-menu a{text-decoration:none}.dropdown-menu .active a,.dropdown-menu .active a:hover{color:white}#page{padding:20px 10px 90px;margin:0;background:#99bf62}@media screen and (min-width: 768px){#page{margin:0;padding:20px 20px 90px}}#page>.sheet{margin:0 20px}.sheet,#content{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.sheet.parent{padding-bottom:1px;background:#f1f3f6;-webkit-box-shadow:0 2px 8px #80aa45;-moz-box-shadow:0 2px 8px #80aa45;box-shadow:0 2px 8px #80aa45}.sheet.parent .sheet{margin:0 -10px -30px 10px}.sheet.parent .sheet.parent{background:#fbfbfc}.sheet.parent .level{color:#979ca6}.sheet.current{background:url(/assets/shadow_left-2d2b96eea4c19f4c329a470c26ea9bcb.png) transparent no-repeat bottom left;padding-bottom:28px;margin-bottom:-58px !important}.sheet.current .container-shadow{background:url(/assets/shadow_right-84778bd40e6ff5d76f79d9e546fce381.png) transparent no-repeat bottom right;padding-bottom:28px;margin-bottom:-28px}#content{-webkit-box-shadow:0 2px 8px #80aa45;-moz-box-shadow:0 2px 8px #80aa45;box-shadow:0 2px 8px #80aa45;background:white;padding:16px 20px 20px}#content aside{margin-bottom:24px}@media screen and (min-width: 0) and (max-width: 979px){#content aside{margin-top:24px}}#content section{*zoom:1;clear:both;margin-top:24px}#content section:before,#content section:after{display:table;content:"";line-height:0}#content section:after{clear:both}#content section:first-child{margin-top:0}#content .breadcrumb{margin:0;padding:5px 0 5px 15px}.contactable{*zoom:1}.contactable:before,.contactable:after{display:table;content:"";line-height:0}.contactable:after{clear:both}.contactable .profil-big{margin:0 0 0 10px}#main{clear:both}#page-footer{padding-top:0;clear:both;color:white}#page-footer .footer_content{padding-top:20px;padding-bottom:20px}@media screen and (min-width: 0) and (max-width: 767px){#page-footer .footer_content{padding:20px}}#page-footer .footer_content a{color:white}#page-footer .footer_content a:hover{color:white}#page-footer>.container-fluid{-webkit-box-shadow:inset 0 12px 8px -8px #257271;-moz-box-shadow:inset 0 12px 8px -8px #257271;box-shadow:inset 0 12px 8px -8px #257271;background:#329897}.highlight{background-color:#FFD}#flash .alert{clear:both}.tooltip-inner{text-align:left}.profil,.profil-big{background:white;border:1px solid #e8ebf0;overflow:hidden;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.profil{width:32px;height:32px;padding:1px}.table-striped .profil{margin-left:-8px}.profil-big{width:72px;height:72px;padding:2px}.icon-calendar{cursor:pointer}#content section.roles{overflow:visible}.icon-muted{opacity:0.7}.contactable.well .contact address{margin-left:1em}.contactable.well .contact .social{margin-left:1em}.profiler-results{opacity:0.2}.profiler-results:hover{opacity:1}.well{border:none;background:#f8f9fa}.ui-datepicker td .ui-state-default:hover{background-color:#925d97 !important}table.roles{width:100%}table.roles td{border:none;padding:0px}table.roles td:first-child{padding-left:10px;width:100%}table.roles td{min-width:20px}.table-responsive .table{background-color:#fff}@media screen and (max-width: 980px){.table-responsive{width:100%;margin-bottom:15px;overflow-x:scroll;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;-webkit-overflow-scrolling:touch}.table-responsive .table{margin-bottom:0}}.log-item{margin-top:2em}.log-item:first-child{margin-top:0}.log-infos{padding-top:11px} +@import url(//fonts.googleapis.com/css?family=Varela+Round); + + +/* Layout */ +body { + font-family: "Varela Round", sans-serif; +} + +nav, main { + overflow-x: hidden; +} + +h1 {font-size: 2em;} +h2 {font-size: 2em;} +h3 {font-size: 1.5em;} +h4 {font-size: 1.2em;} + + + + + +/* NAV LISTEN */ + +nav { + background-color: #329897; + color: white; +} + +nav li li li { + display: none; +} + + +nav li { + line-height: 150%; + list-style: none; +} + +nav li > a { + text-decoration: none; +} + +nav li > a:hover { + text-decoration: underline; +} + +nav ul { + padding-left: 0; +} + +nav ul > li > ul { + padding-left: 2em; +} + +nav li > ul > li { + font-size: 90%; +} + +nav a { + color: inherit; +} + + +/* Code */ + + +pre { + padding: 1em; + background-color: #eee; + color: #6f4073; + border-radius: 4px; + overflow-x: scroll; +} + +/* Table */ + +table { + border-collapse: collapse; +} + +table, th, td { + border: 1px solid #999; +} + +/* Desktop */ + +@media screen and (min-width: 50em) { + body { + display: flex; + flex-direction: row; + flex-wrap: wrap; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + margin: 0; + padding: 0; + } + + nav, main { + height: 100%; + overflow-y: auto; + padding: 1em; + } + + nav { + flex: 1 1 auto; + width: 20em; + } + + main { + flex: 3 1 auto; + width: 30em; + } + + nav li li li { + display: list-item; + } + +} + diff --git a/lib/tarantula/tarantula_config.rb b/lib/tarantula/tarantula_config.rb index 8bd4ffca93..2a67522b9b 100644 --- a/lib/tarantula/tarantula_config.rb +++ b/lib/tarantula/tarantula_config.rb @@ -56,16 +56,21 @@ def configure_urls(t, person) # The parent entry may already have been deleted, thus producing 404s. t.allow_404_for(/groups$/) + t.allow_404_for(/groups\/\d+\/notes\/\d+$/) t.allow_404_for(/groups\/\d+\/roles$/) t.allow_404_for(/groups\/\d+\/roles\/\d+$/) t.allow_404_for(/groups\/\d+\/people$/) t.allow_404_for(/groups\/\d+\/people\/\d+$/) t.allow_404_for(/groups\/\d+\/people\/\d+\/edit/) t.allow_404_for(/groups\/\d+\/people\/\d+\/qualifications\/\d+$/) + t.allow_404_for(/groups\/\d+\/people\/\d+\/colleagues$/) t.allow_404_for(/groups\/\d+\/people\/\d+\/log$/) t.allow_404_for(/groups\/\d+\/people\/\d+\/history$/) - t.allow_404_for(/groups\/\d+\/people\/\d+\/tags\/-?\d+$/) - t.allow_500_for(/groups\/\d+\/people\/\d+\/tags\/-?\d+$/) + t.allow_404_for(/groups\/\d+\/people\/\d+\/invoices$/) + t.allow_404_for(/groups\/\d+\/people\/\d+\/tags\?name=-?\d+$/) + t.allow_500_for(/groups\/\d+\/people\/\d+\/tags\?name=-?\d+$/) + t.allow_404_for(/groups\/\d+\/people\/\d+\/notes\/\d+$/) + t.allow_500_for(/groups\/\d+\/people\/\d+\/notes\/\d+$/) t.allow_404_for(/groups\/\d+\/merge$/) t.allow_404_for(/groups\/\d+\/move$/) t.allow_404_for(/groups\/\d+\/events$/) diff --git a/lib/tasks/ci.rake b/lib/tasks/ci.rake index 257d6f98d0..60ec3b63ee 100644 --- a/lib/tasks/ci.rake +++ b/lib/tasks/ci.rake @@ -6,12 +6,18 @@ # https://github.com/hitobito/hitobito. desc "Runs the tasks for a commit build" -task :ci => ['log:clear', - 'rubocop', - 'db:migrate', - 'ci:setup:rspec', - 'spec:features', # run feature specs first to get coverage from spec - 'spec'] +task :ci do + tasks_to_skip = ENV['skip_tasks'].present? ? ENV['skip_tasks'].split(',') : [] + tasks = ['log:clear', + 'rubocop', + 'db:migrate', + 'ci:setup:env', + 'ci:setup:rspec', + 'spec:features', # run feature specs first to get coverage from spec + 'spec'].delete_if { |task| tasks_to_skip.include?(task) } + + tasks.each { |task| Rake::Task[task].invoke } +end namespace :ci do desc "Runs the tasks for a nightly build" @@ -19,6 +25,7 @@ namespace :ci do 'db:migrate', 'erd', 'doc:all', + 'ci:setup:env', 'ci:setup:rspec', 'spec:features', # run feature specs first to get coverage from spec 'spec', @@ -32,12 +39,19 @@ namespace :ci do wagon_exec('bundle exec rake app:rubocop app:ci:setup:rspec spec:all') end + namespace :setup do + task :env do + ENV['CI'] = "true" + end + end + namespace :wagon do desc "Run the tasks for a wagon nightly build" task :nightly do Rake::Task['log:clear'].invoke - wagon_exec('bundle exec rake app:ci:setup:rspec spec:all app:rubocop:report app:brakeman') + wagon_exec('bundle exec rake app:ci:setup:env ' \ + 'app:ci:setup:rspec spec:all app:rubocop:report app:brakeman') Rake::Task['erd'].invoke end diff --git a/lib/tasks/model.rake b/lib/tasks/model.rake index 8f198a102e..17fd82805d 100644 --- a/lib/tasks/model.rake +++ b/lib/tasks/model.rake @@ -8,7 +8,7 @@ desc "Add column annotations to active records" task :annotate do - sh 'annotate -p before' + sh 'annotate -p before -e tests' end namespace :erd do diff --git a/lib/tasks/reseed.rake b/lib/tasks/reseed.rake index c57979d81b..2b321b91ac 100644 --- a/lib/tasks/reseed.rake +++ b/lib/tasks/reseed.rake @@ -30,4 +30,4 @@ namespace :db do end end end -end \ No newline at end of file +end diff --git a/rubocop-must.yml b/rubocop-must.yml index 0cead1c0da..b1637e11f1 100644 --- a/rubocop-must.yml +++ b/rubocop-must.yml @@ -2,6 +2,7 @@ AllCops: DisplayCopNames: true + DisabledByDefault: true Exclude: - db/**/* - config/**/* @@ -32,353 +33,3 @@ Metrics/ParameterLists: Metrics/AbcSize: Max: 36 # Try to reduce this value - -Style/Encoding: - Enabled: true - -# Disable all other cops - -AccessorMethodName: - Enabled: false - -AccessModifierIndentation: - Enabled: false - -Alias: - Enabled: false - -AlignArray: - Enabled: false - -AlignHash: - Enabled: false - -AlignParameters: - Enabled: false - -AmbiguousOperator: - Enabled: false - -AsciiComments: - Enabled: false - -AssignmentInCondition: - Enabled: false - -Attr: - Enabled: false - -BlockAlignment: - Enabled: false - -BlockComments: - Enabled: false - -BracesAroundHashParameters: - Enabled: false - -ClassAndModuleChildren: - Enabled: false - -CaseIndentation: - Enabled: false - -ClassVars: - Enabled: false - -CollectionMethods: - Enabled: false - -CommentAnnotation: - Enabled: false - -ConstantName: - Enabled: false - -DefaultScope: - Enabled: false - -Documentation: - Enabled: false - -DotPosition: - Enabled: false - -EmptyLineBetweenDefs: - Enabled: false - -EmptyLines: - Enabled: false - -EmptyLinesAroundAccessModifier: - Enabled: false - -EmptyLiteral: - Enabled: false - -EndAlignment: - Enabled: false - -EvenOdd: - Enabled: false - -FormatString: - Enabled: false - -GlobalVars: - Enabled: false - -HandleExceptions: - Enabled: false - -HasAndBelongsToMany: - Enabled: false - -DeprecatedHashMethods: - Enabled: false - -HashSyntax: - Enabled: false - -IfUnlessModifier: - Enabled: false - -IndentationConsistency: - Enabled: false - -IndentationWidth: - Enabled: false - -IndentHash: - Enabled: false - -Lambda: - Enabled: false - -LineEndConcatenation: - Enabled: false - -Lint/Eval: - Enabled: false - -Lint/DefEndAlignment: - Enabled: false - -Lint/UnusedBlockArgument: - Enabled: false - -Lint/UnusedMethodArgument: - Enabled: false - -Lint/UnneededDisable: - Enabled: false - -Lint/UselessAccessModifier: - Enabled: false - -Loop: - Enabled: false - -MultilineTernaryOperator: - Enabled: false - -NilComparison: - Enabled: false - -NumericLiterals: - Enabled: false - -Output: - Enabled: false - -ParenthesesAroundCondition: - Enabled: false - -ParenthesesAsGroupedExpression: - Enabled: false - -PerlBackrefs: - Enabled: false - -PredicateName: - Enabled: false - -Proc: - Enabled: false - -RedundantBegin: - Enabled: false - -RedundantSelf: - Enabled: false - -RegexpLiteral: - Enabled: false - -RescueException: - Enabled: false - -RescueModifier: - Enabled: false - -Semicolon: - Enabled: false - -ShadowingOuterLocalVariable: - Enabled: false - -SignalException: - Enabled: false - -SingleSpaceBeforeFirstArg: - Enabled: false - -SpaceAfterComma: - Enabled: false - -SpaceAroundEqualsInParameterDefault: - Enabled: false - -SpaceAroundOperators: - Enabled: false - -SpaceBeforeComma: - Enabled: false - -SpaceInsideBrackets: - Enabled: false - -SpaceInsideHashLiteralBraces: - Enabled: false - -SpaceInsideParens: - Enabled: false - -SpecialGlobalVars: - Enabled: false - -StringConversionInInterpolation: - Enabled: false - -StringLiterals: - Enabled: false - -Style/ClassCheck: - Enabled: false - -Style/CommentIndentation: - Enabled: false - -Style/EachWithObject: - Enabled: false - -Style/ExtraSpacing: - Enabled: false - -Style/GuardClause: - Enabled: false - -Style/IndentArray: - Enabled: false - -Style/LeadingCommentSpace: - Enabled: false - -Style/MethodCallParentheses: - Enabled: false - -Style/MultilineBlockChain: - Enabled: false - -Style/Next: - Enabled: false - -Style/ParallelAssignment: - Enabled: false - -Style/SingleLineBlockParams: - Enabled: false - -Style/SpaceBeforeComment: - Enabled: false - -Style/SpaceInsideBlockBraces: - Enabled: false - -Style/StringLiteralsInInterpolation: - Enabled: false - -Style/UnneededCapitalW: - Enabled: false - -Style/VariableName: - Enabled: false - -Tab: - Enabled: false - -TrailingBlankLines: - Enabled: false - -TrailingComma: - Enabled: false - -TrailingWhitespace: - Enabled: false - -TrivialAccessors: - Enabled: false - -UnlessElse: - Enabled: false - -UselessAssignment: - Enabled: false - -UselessSetterCall: - Enabled: false - -Void: - Enabled: false - -WordArray: - Enabled: false - -Performance/Detect: - Enabled: false - -Performance/ReverseEach: - Enabled: false - -Performance/Sample: - Enabled: false - -Style/BlockDelimiters: - Enabled: false - -Style/ClosingParenthesisIndentation: - Enabled: false - -Style/EmptyLinesAroundBlockBody: - Enabled: false - -Style/EmptyLinesAroundClassBody: - Enabled: false - -Style/EmptyLinesAroundMethodBody: - Enabled: false - -Style/EmptyLinesAroundModuleBody: - Enabled: false - -Style/FirstParameterIndentation: - Enabled: false - -Style/MultilineOperationIndentation: - Enabled: false - -Style/StructInheritance: - Enabled: false - -Style/SymbolProc: - Enabled: false diff --git a/spec/abilities/ability_dsl/user_context_spec.rb b/spec/abilities/ability_dsl/user_context_spec.rb index fd891a3383..0fa1f7892c 100644 --- a/spec/abilities/ability_dsl/user_context_spec.rb +++ b/spec/abilities/ability_dsl/user_context_spec.rb @@ -21,7 +21,7 @@ it { expect(subject.permission_layer_ids(:layer_and_below_full)).to eq [groups(:top_layer).id] } it { expect(subject.permission_layer_ids(:layer_and_below_read)).to eq [groups(:top_layer).id] } its(:admin) { should be_truthy } - its(:all_permissions) { is_expected.to contain_exactly(:admin, :layer_and_below_full, :layer_and_below_read, :contact_data) } + its(:all_permissions) { is_expected.to contain_exactly(:admin, :finance, :layer_and_below_full, :layer_and_below_read, :contact_data) } it 'has no events with permission full' do expect(subject.events_with_permission(:event_full)).to be_blank @@ -38,7 +38,7 @@ it { expect(subject.permission_layer_ids(:layer_and_below_full)).to eq [] } it { expect(subject.permission_layer_ids(:layer_and_below_read)).to eq [groups(:bottom_layer_one).id] } its(:admin) { should be_falsey } - its(:all_permissions) { is_expected.to eq [:layer_and_below_read] } + its(:all_permissions) { is_expected.to eq [:layer_and_below_read, :finance] } it 'has events with permission full' do expect(subject.events_with_permission(:event_full)).to match_array([events(:top_course).id]) diff --git a/spec/abilities/event_ability_spec.rb b/spec/abilities/event_ability_spec.rb index fb6623b642..1724085e2b 100644 --- a/spec/abilities/event_ability_spec.rb +++ b/spec/abilities/event_ability_spec.rb @@ -635,6 +635,28 @@ Fabricate(Event::Course::Role::Participant.name.to_sym, participation: other) is_expected.not_to be_able_to(:update, other) end + + it 'may destroy his participation if applications_cancelable' do + event.update!(applications_cancelable: true, application_closing_at: Time.zone.today) + is_expected.to be_able_to(:destroy, participation) + end + + it 'may not destroy his participation if applications cancelable and applications closed' do + event.update!(applications_cancelable: true, application_closing_at: Time.zone.today - 1.day) + is_expected.not_to be_able_to(:destroy, participation) + end + + it 'may not destroy his participation if applications not cancelable' do + event.update!(applications_cancelable: false, application_closing_at: Time.zone.today + 10.days) + is_expected.not_to be_able_to(:destroy, participation) + end + + it 'may not destroy other participation if applications cancelable' do + event.update!(applications_cancelable: true, application_closing_at: Time.zone.today) + other = Fabricate(:event_participation, event: event) + Fabricate(Event::Course::Role::Participant.name.to_sym, participation: other) + is_expected.not_to be_able_to(:destroy, other) + end end end diff --git a/spec/abilities/group_ability_spec.rb b/spec/abilities/group_ability_spec.rb index 789674822e..fef5a85663 100644 --- a/spec/abilities/group_ability_spec.rb +++ b/spec/abilities/group_ability_spec.rb @@ -49,7 +49,7 @@ end it 'may show person notes' do - is_expected.to be_able_to(:index_person_notes, group) + is_expected.to be_able_to(:index_notes, group) end it 'may manage person tags' do @@ -83,7 +83,7 @@ end it 'may show person notes' do - is_expected.to be_able_to(:index_person_notes, group) + is_expected.to be_able_to(:index_notes, group) end it 'may manage person tags' do @@ -107,7 +107,7 @@ end it 'may show person notes' do - is_expected.to be_able_to(:index_person_notes, group) + is_expected.to be_able_to(:index_notes, group) end it 'may manage person tags' do @@ -119,7 +119,7 @@ let(:group) { groups(:top_group) } it 'may not show person notes' do - is_expected.not_to be_able_to(:index_person_notes, group) + is_expected.not_to be_able_to(:index_notes, group) end it 'may not manage person tags' do @@ -161,7 +161,7 @@ end it 'may show person notes' do - is_expected.to be_able_to(:index_person_notes, group) + is_expected.to be_able_to(:index_notes, group) end it 'may manage person tags' do @@ -185,7 +185,7 @@ end it 'may show person notes' do - is_expected.to be_able_to(:index_person_notes, group) + is_expected.to be_able_to(:index_notes, group) end it 'may manage person tags' do @@ -209,7 +209,7 @@ end it 'may not show person notes' do - is_expected.not_to be_able_to(:index_person_notes, group) + is_expected.not_to be_able_to(:index_notes, group) end it 'may not manage person tags' do @@ -260,7 +260,7 @@ end it 'may not show person notes' do - is_expected.not_to be_able_to(:index_person_notes, group) + is_expected.not_to be_able_to(:index_notes, group) end it 'may not manage person tags' do @@ -311,7 +311,7 @@ end it 'may not show person notes' do - is_expected.not_to be_able_to(:index_person_notes, group) + is_expected.not_to be_able_to(:index_notes, group) end it 'may not manage person tags' do @@ -340,6 +340,26 @@ end end + context 'finance' do + let(:role) { Fabricate(Group::TopGroup::Leader.name.to_sym, group: groups(:top_group)) } + + it 'may not index invoices on random group' do + is_expected.not_to be_able_to(:index_invoices, Group.new) + end + + it 'may not index in own group' do + is_expected.not_to be_able_to(:index_invoices, groups(:top_group)) + end + + it 'may not index in bottom layer group' do + is_expected.not_to be_able_to(:index_invoices, groups(:bottom_layer_one)) + end + + it 'may index in top layer layer group' do + is_expected.to be_able_to(:index_invoices, groups(:top_layer)) + end + end + context 'deleted group' do let(:group) { groups(:bottom_layer_two) } let(:role) { Fabricate(Group::BottomLayer::Leader.name.to_sym, group: group) } diff --git a/spec/abilities/invoice_ability_spec.rb b/spec/abilities/invoice_ability_spec.rb new file mode 100644 index 0000000000..9073f65bb8 --- /dev/null +++ b/spec/abilities/invoice_ability_spec.rb @@ -0,0 +1,93 @@ + +# encoding: utf-8 + +# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +require 'spec_helper' + +describe InvoiceAbility do + + subject { ability } + + let(:ability) { Ability.new(role.person.reload) } + + [ + %w(bottom_member bottom_layer_one top_layer), + %w(top_leader top_layer bottom_layer_one) + ].each do |role, own_group, other_group| + context role do + let(:role) { roles(role)} + let(:invoice) { Invoice.new(group: group) } + let(:article) { InvoiceArticle.new(group: group) } + let(:reminder) { invoice.payment_reminders.build } + let(:payment) { invoice.payments.build } + + it 'may not index' do + is_expected.not_to be_able_to(:index, Invoice) + end + + it 'may not manage' do + is_expected.not_to be_able_to(:manage, Invoice) + end + + context 'in own group' do + let(:group) { groups(own_group) } + + %w(create edit show update destroy).each do |action| + it "may #{action} invoices in #{own_group}" do + is_expected.to be_able_to(action.to_sym, invoice) + end + end + + %w(create edit show update destroy).each do |action| + it "may #{action} articles in #{own_group}" do + is_expected.to be_able_to(action.to_sym, article) + end + end + + [:reminder, :payment].each do |obj| + it "may create #{obj} in #{own_group}" do + is_expected.to be_able_to(:create, send(obj)) + end + end + + %w(edit show update).each do |action| + it "may #{action} invoice_config in #{own_group}" do + is_expected.to be_able_to(action.to_sym, group.invoice_config) + end + end + end + + context 'in other group' do + let(:group) { groups(other_group) } + + %w(create edit show update destroy).each do |action| + it "may not #{action} invoices in #{other_group}" do + is_expected.not_to be_able_to(action.to_sym, invoice) + end + end + + %w(create edit show update destroy).each do |action| + it "may not #{action} articles in #{other_group}" do + is_expected.not_to be_able_to(action.to_sym, article) + end + end + + [:reminder, :payment].each do |obj| + it "may not create #{obj} in #{own_group}" do + is_expected.not_to be_able_to(:create, send(obj)) + end + end + + %w(edit show update destroy).each do |action| + it "may not #{action} invoice_config in #{other_group}" do + is_expected.not_to be_able_to(action.to_sym, group.invoice_config) + end + end + end + end + end +end diff --git a/spec/abilities/person/note_ability_spec.rb b/spec/abilities/note_ability_spec.rb similarity index 71% rename from spec/abilities/person/note_ability_spec.rb rename to spec/abilities/note_ability_spec.rb index 8017889ae2..54cb3f96ea 100644 --- a/spec/abilities/person/note_ability_spec.rb +++ b/spec/abilities/note_ability_spec.rb @@ -1,13 +1,13 @@ # encoding: utf-8 # Copyright (c) 2012-2016, Dachverband Schweizer Jugendparlamente. This file is part of -# hitobito_dsj and licensed under the Affero General Public License version 3 +# hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito_dsj. +# https://github.com/hitobito/hitobito. require 'spec_helper' -describe Person::NoteAbility do +describe NoteAbility do subject { ability } let(:ability) { Ability.new(role.person.reload) } @@ -15,87 +15,98 @@ context :layer_and_below_full do let(:role) { Fabricate(Group::TopGroup::Leader.name.to_sym, group: groups(:top_group)) } - it 'may create note in his layer' do + it 'may create and destroy note in his layer' do other = Fabricate(Group::TopGroup::Member.name, group: groups(:top_group)).person note = create_note(role.person, other) is_expected.to be_able_to(:create, note) + is_expected.to be_able_to(:destroy, note) end - it 'may create note in bottom layer' do + it 'may create and destroy note in bottom layer' do other = Fabricate(Group::BottomLayer::Member.name.to_sym, group: groups(:bottom_layer_one)).person note = create_note(role.person, other) is_expected.to be_able_to(:create, note) + is_expected.to be_able_to(:destroy, note) end + end context 'layer_and_below_full in bottom layer' do let(:role) { Fabricate(Group::BottomLayer::Leader.name.to_sym, group: groups(:bottom_layer_one)) } - it 'may create note in his layer' do + it 'may create and destroy note in his layer' do other = Fabricate(Group::BottomLayer::Member.name.to_sym, group: groups(:bottom_layer_one)).person note = create_note(role.person, other) is_expected.to be_able_to(:create, note) + is_expected.to be_able_to(:destroy, note) end - it 'may not create note in top layer' do + it 'may not create and destroy note in top layer' do other = Fabricate(Group::TopGroup::Member.name, group: groups(:top_group)).person note = create_note(role.person, other) is_expected.not_to be_able_to(:create, note) + is_expected.not_to be_able_to(:destroy, note) end end context :layer_full do let(:role) { Fabricate(Group::TopGroup::LocalGuide.name.to_sym, group: groups(:top_group)) } - it 'may create note in his layer' do + it 'may create and destroy note in his layer' do other = Fabricate(Group::TopGroup::Member.name, group: groups(:top_group)).person note = create_note(role.person, other) is_expected.to be_able_to(:create, note) + is_expected.to be_able_to(:destroy, note) end - it 'may not create note in bottom layer' do + it 'may not create or delete note in bottom layer' do other = Fabricate(Group::BottomLayer::Member.name.to_sym, group: groups(:bottom_layer_one)).person note = create_note(role.person, other) is_expected.not_to be_able_to(:create, note) + is_expected.not_to be_able_to(:destroy, note) end end context 'layer_full in bottom layer' do let(:role) { Fabricate(Group::BottomLayer::LocalGuide.name.to_sym, group: groups(:bottom_layer_one)) } - it 'may create note in his layer' do + it 'may create or delete note in his layer' do other = Fabricate(Group::BottomLayer::Member.name.to_sym, group: groups(:bottom_layer_one)).person note = create_note(role.person, other) is_expected.to be_able_to(:create, note) + is_expected.to be_able_to(:destroy, note) end - it 'may not create note in upper layer' do + it 'may not create or delete note in upper layer' do other = Fabricate(Group::TopGroup::Member.name, group: groups(:top_group)).person note = create_note(role.person, other) is_expected.not_to be_able_to(:create, note) + is_expected.not_to be_able_to(:destroy, note) end end context :group_and_below_read do let(:role) { Fabricate(Group::TopGroup::Member.name.to_sym, group: groups(:top_group)) } - it 'may not create note in his layer' do + it 'may not create or delete note in his layer' do other = Fabricate(Group::TopGroup::Member.name, group: groups(:top_group)).person note = create_note(role.person, other) is_expected.not_to be_able_to(:create, note) + is_expected.not_to be_able_to(:destroy, note) end - it 'may not create note in bottom layer' do + it 'may not create or delete note in bottom layer' do other = Fabricate(Group::BottomLayer::Member.name.to_sym, group: groups(:bottom_layer_one)).person note = create_note(role.person, other) is_expected.not_to be_able_to(:create, note) + is_expected.not_to be_able_to(:destroy, note) end end def create_note(author, person) - Person::Note.create!( + Note.create!( author: author, - person: person, + subject: person, text: 'Lorem ipsum' ) end diff --git a/spec/abilities/person/add_request_ability_spec.rb b/spec/abilities/person/add_request_ability_spec.rb index 25d4ba567c..7662ff8110 100644 --- a/spec/abilities/person/add_request_ability_spec.rb +++ b/spec/abilities/person/add_request_ability_spec.rb @@ -44,6 +44,17 @@ is_expected.to be_able_to(:add_without_request, request) end + it 'allowed with person deleted in below layer' do + other = Fabricate(Group::BottomLayer::Member.name, group: groups(:bottom_layer_one), created_at: 1.year.ago, deleted_at: 1.month.ago).person + request = create_request(other) + + is_expected.to be_able_to(:approve, request) + is_expected.to be_able_to(:reject, request) + is_expected.to be_able_to(:add_without_request, request) + is_expected.to be_able_to(:index_person_add_requests, groups(:bottom_layer_one)) + end + + context 'in below layer' do let(:role) { Fabricate(Group::BottomLayer::Leader.name, group: groups(:bottom_layer_one)) } @@ -57,6 +68,16 @@ is_expected.to be_able_to(:index_person_add_requests, groups(:bottom_layer_one)) end + it 'allowed with person deleted in same layer' do + other = Fabricate(Group::BottomGroup::Member.name, group: groups(:bottom_group_one_one)).person + other.roles.first.update!(created_at: 1.year.ago, deleted_at: 1.month.ago) + request = create_request(other) + + is_expected.to be_able_to(:approve, request) + is_expected.to be_able_to(:reject, request) + is_expected.to be_able_to(:add_without_request, request) + end + it 'not allowed with person in neighbour layer' do other = Fabricate(Group::BottomLayer::Member.name, group: groups(:bottom_layer_two)).person request = create_request(other) @@ -67,6 +88,21 @@ is_expected.not_to be_able_to(:index_person_add_requests, groups(:bottom_layer_two)) end + it 'not allowed with person in neighbour layer and deleted role in same layer' do + other = Fabricate(Group::BottomLayer::Member.name, group: groups(:bottom_layer_two)).person + Fabricate(Group::BottomGroup::Member.name, + group: groups(:bottom_group_one_one), + person: other, + created_at: 1.year.ago, + deleted_at: 1.month.ago) + request = create_request(other) + + is_expected.not_to be_able_to(:approve, request) + is_expected.not_to be_able_to(:reject, request) + is_expected.not_to be_able_to(:add_without_request, request) + is_expected.not_to be_able_to(:index_person_add_requests, groups(:bottom_layer_two)) + end + it 'allowed with person in neighbour layer with contact data' do other = Fabricate(Group::BottomLayer::Leader.name, group: groups(:bottom_layer_two)).person request = create_request(other) @@ -87,6 +123,18 @@ is_expected.not_to be_able_to(:index_person_add_requests, groups(:bottom_layer_two)) end + it 'not allowed with deleted person in neighbour layer where user has a simple role' do + Fabricate(Group::BottomLayer::Member.name, group: groups(:bottom_layer_two), person: role.person) + other = Fabricate(Group::BottomLayer::Leader.name, group: groups(:bottom_layer_two), created_at: 1.year.ago).person + other.roles.first.destroy! + + request = create_request(other) + + is_expected.not_to be_able_to(:approve, request) + is_expected.not_to be_able_to(:reject, request) + is_expected.not_to be_able_to(:add_without_request, request) + end + end end @@ -114,6 +162,15 @@ is_expected.not_to be_able_to(:index_person_add_requests, groups(:bottom_layer_one)) end + it 'allowed with person deleted in same layer' do + other = Fabricate(Group::TopLayer::TopAdmin.name, group: groups(:top_layer), created_at: 1.year.ago, deleted_at: 1.month.ago).person + request = create_request(other) + + is_expected.to be_able_to(:approve, request) + is_expected.to be_able_to(:reject, request) + is_expected.to be_able_to(:add_without_request, request) + is_expected.to be_able_to(:index_person_add_requests, groups(:top_layer)) + end end context :group_full do @@ -131,6 +188,16 @@ is_expected.not_to be_able_to(:index_person_add_requests, groups(:top_group)) end + it 'allowed with person deleted in same group' do + other = Fabricate(Group::TopGroup::Member.name, group: groups(:top_group), created_at: 1.year.ago, deleted_at: 1.month.ago).person + request = create_request(other) + + is_expected.to be_able_to(:approve, request) + is_expected.to be_able_to(:reject, request) + is_expected.to be_able_to(:add_without_request, request) + is_expected.not_to be_able_to(:index_person_add_requests, groups(:top_group)) + end + it 'not allowed with person in same layer' do other = Fabricate(Group::TopLayer::TopAdmin.name, group: groups(:top_layer)).person request = create_request(other) diff --git a/spec/abilities/person_ability_spec.rb b/spec/abilities/person_ability_spec.rb index afc5431ca3..0a708f342c 100644 --- a/spec/abilities/person_ability_spec.rb +++ b/spec/abilities/person_ability_spec.rb @@ -1111,6 +1111,20 @@ end end + context 'finance' do + let(:role) { Fabricate(Group::TopGroup::Leader.name.to_sym, group: groups(:top_group)) } + + it 'may not index in bottom layer group' do + other = Fabricate(Group::BottomLayer::Member.name.to_sym, group: groups(:bottom_layer_one)) + is_expected.not_to be_able_to(:index_invoices, other) + end + + it 'may index in top group' do + other = Fabricate(Group::TopGroup::Member.name.to_sym, group: groups(:top_group)) + is_expected.not_to be_able_to(:index_invoices, other) + end + end + describe 'no permissions' do let(:role) { Fabricate(Role::External.name.to_sym, group: groups(:top_group)) } @@ -1118,6 +1132,10 @@ is_expected.to be_able_to(:show_full, role.person.reload) end + it 'may view invoices of himself' do + is_expected.to be_able_to(:index_invoices, role.person.reload) + end + it 'may modify himself' do is_expected.to be_able_to(:update, role.person.reload) is_expected.to be_able_to(:update_email, role.person) diff --git a/spec/abilities/person_readables_spec.rb b/spec/abilities/person_readables_spec.rb index 4390564ac5..294c07e577 100644 --- a/spec/abilities/person_readables_spec.rb +++ b/spec/abilities/person_readables_spec.rb @@ -68,6 +68,7 @@ is_expected.not_to include(other.person) end end + end diff --git a/spec/controllers/event/kinds_controller_spec.rb b/spec/controllers/event/kinds_controller_spec.rb index c9252ad8e2..e710b788af 100644 --- a/spec/controllers/event/kinds_controller_spec.rb +++ b/spec/controllers/event/kinds_controller_spec.rb @@ -51,9 +51,12 @@ it 'adds associations to new event kind' do post :create, event_kind: { label: 'Foo', + precondition_qualification_kinds: { + '0' => { qualification_kind_ids: [sl.id, gl.id] }, + '2' => { qualification_kind_ids: [sl.id, ql.id] } + }, qualification_kinds: { participant: { - precondition: { qualification_kind_ids: [sl.id, gl.id] }, qualification: { qualification_kind_ids: [sl.id, gl.id] }, prolongation: { qualification_kind_ids: [sl.id] } }, @@ -67,8 +70,9 @@ expect(assigns(:kind).errors.full_messages).to eq [] assocs = assigns(:kind).event_kind_qualification_kinds - expect(assocs.count).to eq 9 - expect(assocs.where(role: :participant, category: :precondition).count).to eq 2 + expect(assocs.count).to eq 11 + expect(assocs.where(role: :participant, category: :precondition, grouping: 1).count).to eq 2 + expect(assocs.where(role: :participant, category: :precondition, grouping: 2).count).to eq 2 expect(assocs.where(role: :participant, category: :qualification).count).to eq 2 expect(assocs.where(role: :participant, category: :prolongation).count).to eq 1 expect(assocs.where(role: :leader, category: :qualification).count).to eq 2 @@ -90,19 +94,29 @@ end it 'removes association from existing event kind' do - expect(kind.event_kind_qualification_kinds.count).to eq 4 + kind.event_kind_qualification_kinds.create!( + category: 'precondition', role: 'participant', grouping: 1, qualification_kind_id: gl.id) + kind.event_kind_qualification_kinds.create!( + category: 'precondition', role: 'participant', grouping: 2, qualification_kind_id: sl.id) + expect(kind.event_kind_qualification_kinds.count).to eq 6 put :update, id: kind.id, event_kind: { label: kind.label, + precondition_qualification_kinds: { + '0' => { qualification_kind_ids: [ql.id] }, + '1' => { qualification_kind_ids: [gl.id] }, + }, qualification_kinds: { participant: { prolongation: { qualification_kind_ids: [gl.id] } } } } assocs = assigns(:kind).event_kind_qualification_kinds - expect(assocs.count).to eq 1 - expect(assocs.pluck(:qualification_kind_id)).to match_array([gl.id]) + expect(assocs.count).to eq 3 + expect(assocs.pluck(:qualification_kind_id)).to match_array([gl.id, gl.id, ql.id]) end it 'removes all associations from existing event kind' do - expect(kind.event_kind_qualification_kinds.count).to eq 4 + kind.event_kind_qualification_kinds.create!( + category: 'precondition', role: 'participant', grouping: 1, qualification_kind_id: gl.id) + expect(kind.event_kind_qualification_kinds.count).to eq 5 put :update, id: kind.id, event_kind: { label: kind.label, qualification_kinds: { participant: { prolongation: { diff --git a/spec/controllers/event/lists_controller_spec.rb b/spec/controllers/event/lists_controller_spec.rb index 0a10338911..b5644fcae4 100644 --- a/spec/controllers/event/lists_controller_spec.rb +++ b/spec/controllers/event/lists_controller_spec.rb @@ -8,6 +8,7 @@ require 'spec_helper' describe Event::ListsController do + render_views before { sign_in(person) } let(:person) { people(:bottom_member) } @@ -66,9 +67,9 @@ context 'filter per group' do before { sign_in(people(:top_leader)) } - it 'defaults to toplevel group with courses in hiearchy' do + it 'defaults to layer of primary group' do get :courses - expect(assigns(:group_id)).to eq groups(:top_group).id + expect(assigns(:group_id)).to eq groups(:top_group).layer_group_id end it 'can be set via param, only if year is present' do @@ -77,9 +78,25 @@ end end + context 'finds course offerer' do + it 'via primary group' do + sign_in(people(:top_leader)) + get :courses + expect(controller.send(:course_group_from_primary_layer)).to eq groups(:top_layer) + end + + it 'via hierarchy' do + group = groups(:bottom_group_one_one) + user = Fabricate(Group::BottomGroup::Leader.name.to_s, label: 'foo', group: group).person + sign_in(user) + get :courses + expect(controller.send(:course_group_from_hierarchy).id).to eq groups(:bottom_layer_one).id + end + end + context 'exports to csv' do let(:rows) { response.body.split("\n") } - let(:course) { Fabricate(:course) } + let(:course) { Fabricate(:course) } before { Fabricate(:event_date, event: course) } it 'renders csv headers' do @@ -102,6 +119,34 @@ after { Event::Course.used_attributes += [:kind_id] } end + context 'booking info' do + before do + course = Fabricate(:course, display_booking_info: false) + Fabricate(:event_date, event: course, start_at: Date.new(2012, 1, 23)) + end + + it 'is visible for manager' do + sign_in(people(:top_leader)) + get :courses, year: 2012 + expect(response.body).to have_selector('tbody tr', count: 2) + expect(response.body).to have_selector('tbody tr:nth-child(1) td:nth-child(3)', + text: '0 Anmeldungen') + expect(response.body).to have_selector('tbody tr:nth-child(2) td:nth-child(3)', + text: '0 Anmeldungen') + end + + it 'is only visible for member where allowed by course' do + sign_in(people(:bottom_member)) + get :courses, year: 2012 + expect(response.body).to have_selector('tbody tr', count: 2) + expect(response.body).not_to have_selector('tbody tr:nth-child(1) td:nth-child(3)', + text: '0 Anmeldungen') + expect(response.body).to have_selector('tbody tr:nth-child(2) td:nth-child(3)', + text: '0 Anmeldungen') + end + + end + end def create_event(group, hash = {}) diff --git a/spec/controllers/event/participations_controller_spec.rb b/spec/controllers/event/participations_controller_spec.rb index 0e21807f3b..77e9335848 100644 --- a/spec/controllers/event/participations_controller_spec.rb +++ b/spec/controllers/event/participations_controller_spec.rb @@ -51,9 +51,9 @@ @leader, @participant = *create(Event::Role::Leader, course.participant_types.first) update_person(@participant, first_name: 'Al', last_name: 'Barns', nickname: 'al', - town: 'Eye', address: 'Spring Road', zip_code: '3000') + town: 'Eye', address: 'Spring Road', zip_code: '3000', birthday: '21.10.1978') update_person(@leader, first_name: 'Joe', last_name: 'Smith', nickname: 'js', - town: 'Stoke', address: 'Howard Street', zip_code: '8000') + town: 'Stoke', address: 'Howard Street', zip_code: '8000', birthday: '1.3.1992') end it 'lists participant and leader group by default' do @@ -87,13 +87,17 @@ end it 'exports csv files' do - get :index, group_id: group, event_id: course.id, format: :csv + expect do + get :index, group_id: group, event_id: course.id, format: :csv + expect(flash[:notice]).to match(/Export wird im Hintergrund gestartet und nach Fertigstellung an \S+@\S+ versendet./) + end.to change(Delayed::Job, :count).by(1) + end - expect(@response.content_type).to eq('text/csv') - expect(@response.body).to match(/^Vorname;Nachname/) - expect(@response.body). - to match(/^#{@participant.person.first_name};#{@participant.person.last_name}/) - expect(@response.body).to match(/^#{@leader.person.first_name};#{@leader.person.last_name}/) + it 'exports xlsx files' do + expect do + get :index, group_id: group, event_id: course.id, format: :xlsx + expect(flash[:notice]).to match(/Export wird im Hintergrund gestartet und nach Fertigstellung an \S+@\S+ versendet./) + end.to change(Delayed::Job, :count).by(1) end it 'renders email addresses with additional ones' do @@ -118,7 +122,7 @@ context 'sorting' do - %w(first_name last_name nickname zip_code town).each do |attr| + %w(first_name last_name nickname zip_code town birthday).each do |attr| it "sorts based on #{attr}" do get :index, group_id: group, event_id: course.id, sort: attr, sort_dir: :asc expect(assigns(:participations)).to eq([@participant, @leader]) @@ -351,8 +355,6 @@ def update_person(participation, attrs) expect(flash[:notice]). to include 'Teilnahme von Top Leader in Eventus wurde erfolgreich erstellt.' - expect(flash[:notice]). - to include 'Bitte überprüfe die Kontaktdaten und passe diese gegebenenfalls an.' end it 'creates non-active participant role for course events' do @@ -375,8 +377,6 @@ def update_person(participation, attrs) expect(flash[:notice]). to include 'Teilnahme von Top Leader in Eventus wurde erfolgreich erstellt.' - expect(flash[:notice]). - to include 'Bitte überprüfe die Kontaktdaten und passe diese gegebenenfalls an.' end it 'creates specific non-active participant role for course events' do @@ -395,8 +395,6 @@ class TestParticipant < Event::Course::Role::Participant; end expect(role).to be_kind_of(TestParticipant) expect(flash[:notice]). to include 'Teilnahme von Top Leader in Eventus wurde erfolgreich erstellt.' - expect(flash[:notice]). - to include 'Bitte überprüfe die Kontaktdaten und passe diese gegebenenfalls an.' expect(role.participation).to eq participation.model end @@ -421,8 +419,20 @@ class TestParticipant < Event::Course::Role::Participant; end expect(flash[:notice]). to include 'Teilnahme von Top Leader in Eventus wurde erfolgreich erstellt.' - expect(flash[:notice]). - to include 'Bitte überprüfe die Kontaktdaten und passe diese gegebenenfalls an.' + end + + it 'creates new participation with all answers' do + post :create, + group_id: group.id, + event_id: course.id, + event_participation: { + answers: { + 1 => { question_id: course.questions.first.id, answer: 'Bla' } + } + } + + participation = assigns(:participation) + expect(participation.answers.size).to eq(2) end it 'fails for invalid event role' do @@ -505,6 +515,27 @@ class TestParticipant < Event::Course::Role::Participant; end end end + + context 'DELETE destroy' do + + it 'redirects to application market' do + delete :destroy, group_id: group.id, event_id: course.id, id: participation.id + + is_expected.to redirect_to group_event_application_market_index_path(group, course) + expect(flash[:notice]).to match(/Anmeldung/) + expect(Delayed::Job.where("handler LIKE ?", '%CancelApplicationJob%')).not_to exist + end + + it 'redirects to event show if own participation' do + participation.update_column(:person_id, user.id) + delete :destroy, group_id: group.id, event_id: course.id, id: participation.id + + is_expected.to redirect_to group_event_path(group, course) + expect(Delayed::Job.where("handler LIKE ?", '%CancelApplicationJob%')).to exist + end + + end + context 'preconditions' do before { user.qualifications.first.destroy } @@ -535,6 +566,10 @@ class TestParticipant < Event::Course::Role::Participant; end context 'GET new' do before { get :new, group_id: group.id, event_id: course.id } + it 'sets answers instance variable' do + expect(assigns(:answers)).to have(2).item + end + it 'allows the user to apply' do is_expected.to_not redirect_to group_event_path(group, course) end diff --git a/spec/controllers/event/qualifications_controller_spec.rb b/spec/controllers/event/qualifications_controller_spec.rb index dd4ef97fbe..30774bcd4a 100644 --- a/spec/controllers/event/qualifications_controller_spec.rb +++ b/spec/controllers/event/qualifications_controller_spec.rb @@ -1,6 +1,6 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. @@ -29,6 +29,12 @@ def create_participation(role) before { sign_in(people(:top_leader)) } + before do + participant_1 + participant_2 + leader_1 + end + it 'event kind has one qualification kind' do expect(event.kind.qualification_kinds('qualification', 'participant')).to eq [qualification_kinds(:sl)] end @@ -38,10 +44,6 @@ def create_participation(role) context 'entries' do before do - participant_1 - participant_2 - leader_1 - get :index, group_id: group.id, event_id: event.id end @@ -63,87 +65,82 @@ def create_participation(role) describe 'PUT update' do subject { obtained_qualifications } - context 'with one existing qualifications' do - before do - qualification_kind_id = event.kind.qualification_kinds('qualification', 'participant').first.id - participant_1.person.qualifications.create!(qualification_kind_id: qualification_kind_id, - start_at: start_at) - end + context 'adding' do + context 'with one existing qualifications' do + before do + qualification_kind_id = event.kind.qualification_kinds('qualification', 'participant').first.id + participant_1.person.qualifications.create!(qualification_kind_id: qualification_kind_id, + start_at: start_at) + end - context 'issued before qualification date' do - let(:start_at) { event.qualification_date - 1.day } + context 'issued before qualification date' do + let(:start_at) { event.qualification_date - 1.day } - it 'issues qualification' do - expect do - put :update, group_id: group.id, event_id: event.id, id: participant_1.id, format: :js - end.to change { Qualification.count }.by(1) - expect(subject.size).to eq(1) - is_expected.to render_template('qualification') + it 'issues qualification' do + expect do + put :update, group_id: group.id, event_id: event.id, participation_ids: [participant_1.id.to_s] + end.to change { Qualification.count }.by(1) + expect(subject.size).to eq(1) + end end - end - context 'issued on qualification date' do - let(:start_at) { event.qualification_date } + context 'issued on qualification date' do + let(:start_at) { event.qualification_date } - it 'keeps existing qualification' do - expect do - put :update, group_id: group.id, event_id: event.id, id: participant_1.id, format: :js - end.not_to change { Qualification.count } - expect(subject.size).to eq(1) - is_expected.to render_template('qualification') + it 'keeps existing qualification' do + expect do + put :update, group_id: group.id, event_id: event.id, participation_ids: [participant_1.id] + end.not_to change { Qualification.count } + expect(subject.size).to eq(1) + end end - end - end + end - context 'without existing qualifications for participant' do - before { put :update, group_id: group.id, event_id: event.id, id: participant_1.id, format: :js } + context 'without existing qualifications for participant' do + before { put :update, group_id: group.id, event_id: event.id, participation_ids: [participant_1.id] } - it 'has 1 item' do - expect(subject.size).to eq(1) + it 'has 1 item' do + expect(subject.size).to eq(1) + end end - it { is_expected.to render_template('qualification') } - end - context 'without existing qualifications for leader' do - before { put :update, group_id: group.id, event_id: event.id, id: leader_1.id, format: :js } + context 'without existing qualifications for leader' do + before { put :update, group_id: group.id, event_id: event.id, participation_ids: [leader_1.id] } - it 'should obtain a qualification' do - obtained = obtained_qualifications(leader_1) - expect(obtained.size).to eq(1) - expect(obtained).to render_template('qualification') + it 'should obtain a qualification' do + obtained = obtained_qualifications(leader_1) + expect(obtained.size).to eq(1) + end end - end - end + end - describe 'DELETE destroy' do + context 'removing' do - subject { obtained_qualifications } + context 'without existing qualifications' do + before do + put :update, group_id: group.id, event_id: event.id + end - context 'without existing qualifications' do - before do - delete :destroy, group_id: group.id, event_id: event.id, id: participant_1.id, format: :js + it 'has no items' do + expect(subject.size).to eq(0) + end end - it 'has no items' do - expect(subject.size).to eq(0) - end - it { is_expected.to render_template('qualification') } - end + context 'with one existing qualification' do + before do + qualification_kind_id = event.kind.qualification_kinds('qualification', 'participant').first.id + participant_1.person.qualifications.create!(qualification_kind_id: qualification_kind_id, + start_at: event.qualification_date) + put :update, group_id: group.id, event_id: event.id, participation_ids: [] + end - context 'with one existing qualification' do - before do - qualification_kind_id = event.kind.qualification_kinds('qualification', 'participant').first.id - participant_1.person.qualifications.create!(qualification_kind_id: qualification_kind_id, - start_at: event.qualification_date) - delete :destroy, group_id: group.id, event_id: event.id, id: participant_1.id, format: :js + it 'has no items' do + expect(subject.size).to eq(0) + end end - it 'has no items' do - expect(subject.size).to eq(0) - end - it { is_expected.to render_template('qualification') } end end @@ -151,4 +148,5 @@ def obtained_qualifications(person = participant_1) q = Event::Qualifier.for(person) q.send(:obtained, q.send(:qualification_kinds)) end + end diff --git a/spec/controllers/events_controller_spec.rb b/spec/controllers/events_controller_spec.rb index 478746dc23..da34f26ca6 100644 --- a/spec/controllers/events_controller_spec.rb +++ b/spec/controllers/events_controller_spec.rb @@ -1,6 +1,6 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. @@ -17,6 +17,27 @@ before { group2 } + context 'GET show' do + + it 'sets empty @user_participation' do + sign_in(people(:top_leader)) + + get :show, group_id: groups(:top_layer).id, id: events(:top_event) + + expect(assigns(:user_participation)).to be_nil + end + + it 'sets @user_participation' do + p = Fabricate(:event_participation, event: events(:top_event), person: people(:top_leader)) + sign_in(people(:top_leader)) + + get :show, group_id: groups(:top_layer).id, id: events(:top_event) + + expect(assigns(:user_participation)).to eq(p) + end + + end + context 'GET new' do it 'loads sister groups' do sign_in(people(:top_leader)) @@ -33,6 +54,29 @@ get :new, group_id: group.id, event: { type: 'Event::Course' } expect(assigns(:kinds)).not_to include event_kinds(:old) end + + it 'duplicates other course' do + sign_in(people(:top_leader)) + source = events(:top_course) + + get :new, group_id: source.groups.first.id, source_id: source.id + + event = assigns(:event) + expect(event.state).to be_nil + expect(event.name).to eq(source.name) + expect(event.kind_id).to eq(source.kind_id) + expect(event.application_questions.map(&:question)).to match_array( + source.application_questions.map(&:question)) + expect(event.application_questions.map(&:id).uniq).to eq([nil]) + end + + it 'raises not found if event is in other group' do + sign_in(people(:top_leader)) + + expect do + get :new, group_id: group.id, source_id: events(:top_course).id + end.to raise_error(ActiveRecord::RecordNotFound) + end end context 'POST create' do @@ -46,7 +90,7 @@ name: 'foo', kind_id: event_kinds(:slk).id, dates_attributes: [date], - questions_attributes: [question], + application_questions_attributes: [question], contact_id: people(:top_leader).id, type: 'Event::Course' }, group_id: group.id @@ -117,28 +161,37 @@ it 'creates, updates and destroys questions' do q1 = event.questions.create!(question: 'Who?') q2 = event.questions.create!(question: 'What?') + q3 = event.questions.create!(question: 'Payed?', admin: true) expect do put :update, group_id: group.id, id: event.id, event: { name: 'testevent', - questions_attributes: { - q1.id.to_s => { id: q1.id, - question: 'Whoo?' }, - q2.id.to_s => { id: q2.id, _destroy: true }, - '999' => { question: 'How much?', - choices: '1,2,3' } } } + application_questions_attributes: { + q1.id.to_s => { id: q1.id, + question: 'Whoo?' }, + q2.id.to_s => { id: q2.id, _destroy: true }, + '999' => { question: 'How much?', + choices: '1,2,3' } }, + admin_questions_attributes: { + q3.id.to_s => { id: q3.id, _destroy: true }, + '999' => { question: 'Powned?', + choices: 'ja, nein' } } } expect(assigns(:event)).to be_valid end.not_to change { Event::Question.count } expect(event.reload.name).to eq 'testevent' questions = event.questions.order(:question) - expect(questions.size).to eq(2) + expect(questions.size).to eq(3) first = questions.first expect(first.question).to eq 'How much?' expect(first.choices).to eq '1,2,3' second = questions.second - expect(second.question).to eq 'Whoo?' + expect(second.question).to eq 'Powned?' + expect(second.admin).to eq true + third = questions.third + expect(third.question).to eq 'Whoo?' + expect(third.admin).to eq false end end @@ -182,5 +235,37 @@ end end + context 'contact attributes' do + + let(:event) { events(:top_event) } + let(:group) { groups(:top_layer) } + + before { sign_in(people(:top_leader)) } + + it 'assigns required and hidden contact attributes' do + + put :update, group_id: group.id, id: event.id, + event: { contact_attrs: { nickname: :required, address: :hidden, social_accounts: :hidden } } + + expect(event.reload.required_contact_attrs).to include('nickname') + expect(event.reload.hidden_contact_attrs).to include('address') + expect(event.reload.hidden_contact_attrs).to include('social_accounts') + + end + + it 'removes contact attributes' do + + event.update!({hidden_contact_attrs: ['social_accounts', 'address', 'nickname']}) + + put :update, group_id: group.id, id: event.id, + event: { contact_attrs: { nickname: :hidden } } + + expect(event.reload.hidden_contact_attrs).to include('nickname') + expect(event.hidden_contact_attrs).not_to include('address') + expect(event.hidden_contact_attrs).not_to include('social_accounts') + + end + end + end diff --git a/spec/controllers/full_text_controller_spec.rb b/spec/controllers/full_text_controller_spec.rb new file mode 100644 index 0000000000..bbbaa1df92 --- /dev/null +++ b/spec/controllers/full_text_controller_spec.rb @@ -0,0 +1,87 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +require 'spec_helper' + +describe FullTextController, type: :controller do + + before { sign_in(people(:top_leader)) } + + [SearchStrategies::Sphinx, SearchStrategies::Sql].each do |strategy| + + context strategy.name.demodulize.downcase do + before do + [[:list_people, Person.where(id: people(:bottom_member).id)], + [:query_people, Person.where(id: people(:bottom_member).id)], + [:query_groups, Group.where(id: groups(:bottom_layer_one).id)], + [:query_events, Event.where(id: events(:top_course).id)]].each do |stub, value| + allow_any_instance_of(strategy).to receive(stub).and_return(value) + end + + allow(Hitobito::Application).to receive(:sphinx_present?) + .and_return(strategy == SearchStrategies::Sphinx) + end + + describe 'GET index' do + + before do + sign_in(people(:top_leader)) + end + + it 'uses correct search strategy' do + get :index, q: 'Bottom' + expect(assigns(:search_strategy).class).to eq(strategy) + end + + it 'finds person' do + get :index, q: 'Bottom' + + expect(assigns(:people)).to include(people(:bottom_member)) + end + + context 'without any params' do + it 'returns nothing' do + get :index + + expect(@response).to be_ok + expect(assigns(:people)).to eq([]) + end + end + + end + + describe 'GET query' do + + it 'uses correct search strategy' do + get :query, q: 'Bottom' + expect(assigns(:search_strategy).class).to eq(strategy) + end + + it 'finds person' do + get :query, q: 'Bottom' + + expect(@response.body).to include(people(:bottom_member).full_name) + end + + it 'finds groups' do + get :query, q: groups(:bottom_layer_one).to_s[1..5] + + expect(@response.body).to include(groups(:bottom_layer_one).to_s) + end + + it 'finds events' do + get :query, q: events(:top_course).to_s[1..5] + + expect(@response.body).to include(events(:top_course).to_s) + end + + end + end + + end + +end diff --git a/spec/controllers/group/deleted_people_controller_spec.rb b/spec/controllers/group/deleted_people_controller_spec.rb new file mode 100644 index 0000000000..ffa30af837 --- /dev/null +++ b/spec/controllers/group/deleted_people_controller_spec.rb @@ -0,0 +1,35 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +require 'spec_helper' + +describe Group::DeletedPeopleController do + + let(:group) { groups(:top_group) } + let(:person1) { people(:top_leader) } + let(:person2) { people(:bottom_member) } + + context 'authenticated and permitted' do + + before { sign_in(person1) } + + it 'renders index view if permitted' do + get :index, group_id: group.id + is_expected.to render_template('group/deleted_people/index') + end + end + + context 'authenticated and not permitted' do + before { sign_in(person2) } + + it 'fails if not permitted' do + expect do + get :index, group_id: group.id + end.to raise_error(CanCan::AccessDenied) + end + end +end diff --git a/spec/controllers/group/person_add_requests_controller_spec.rb b/spec/controllers/group/person_add_requests_controller_spec.rb index f4f0460826..9a442bec6a 100644 --- a/spec/controllers/group/person_add_requests_controller_spec.rb +++ b/spec/controllers/group/person_add_requests_controller_spec.rb @@ -81,6 +81,7 @@ expect(flash[:notice]).to be_blank expect(flash[:alert]).to be_blank + expect(assigns(:add_requests)).to eq([request]) expect(assigns(:current_add_request)).to eq(request) end end @@ -123,5 +124,5 @@ end end - + end diff --git a/spec/controllers/invoice_articles_controller_spec.rb b/spec/controllers/invoice_articles_controller_spec.rb new file mode 100644 index 0000000000..0310879471 --- /dev/null +++ b/spec/controllers/invoice_articles_controller_spec.rb @@ -0,0 +1,43 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +require 'spec_helper' + +describe InvoiceArticlesController do + + let(:group) { groups(:bottom_layer_one) } + let(:person) { people(:bottom_member) } + + context 'authorization' do + before { sign_in(person) } + + it "may index when person has finance permission on layer group" do + get :index, group_id: group.id + expect(response).to be_success + end + + it "may edit when person has finance permission on layer group" do + invoice = InvoiceArticle.create!(group: group, number: 1, name: 'test') + get :edit, group_id: group.id, id: invoice.id + expect(response).to be_success + end + + it "may not index when person has no finance permission on layer group" do + expect do + get :index, group_id: groups(:top_layer).id + end.to raise_error(CanCan::AccessDenied) + end + + it "may not edit when person has no finance permission on layer group" do + invoice = InvoiceArticle.create!(group: groups(:top_layer), number: 1, name: 'test') + expect do + get :edit, group_id: groups(:top_layer).id, id: invoice.id + end.to raise_error(CanCan::AccessDenied) + end + end + +end diff --git a/spec/controllers/invoice_configs_controller_spec.rb b/spec/controllers/invoice_configs_controller_spec.rb new file mode 100644 index 0000000000..5d8ce28efa --- /dev/null +++ b/spec/controllers/invoice_configs_controller_spec.rb @@ -0,0 +1,42 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +require 'spec_helper' + +describe InvoiceConfigsController do + + let(:group) { groups(:bottom_layer_one) } + let(:person) { people(:bottom_member) } + let(:entry) { invoice_configs(:bottom_layer_one) } + + context 'authorization' do + before { sign_in(person) } + + it "may show when person has finance permission on layer group" do + get :show, group_id: group.id, id: entry.id + expect(response).to be_success + end + + it "may edit when person has finance permission on layer group" do + get :edit, group_id: group.id, id: entry.id + expect(response).to be_success + end + + it "may not show when person has finance permission on layer group" do + expect do + get :show, group_id: groups(:top_layer).id, id: invoice_configs(:top_layer).id + end.to raise_error(CanCan::AccessDenied) + end + + it "may not edit when person has finance permission on layer group" do + expect do + get :edit, group_id: groups(:top_layer).id, id: invoice_configs(:top_layer).id + end.to raise_error(CanCan::AccessDenied) + end + end +end + diff --git a/spec/controllers/invoice_lists_controller_spec.rb b/spec/controllers/invoice_lists_controller_spec.rb new file mode 100644 index 0000000000..fece56df96 --- /dev/null +++ b/spec/controllers/invoice_lists_controller_spec.rb @@ -0,0 +1,161 @@ +# frozen_string_literal: true + +require 'spec_helper' + +# Copyright (c) 2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +describe InvoiceListsController do + let(:group) { groups(:bottom_layer_one) } + let(:person) { people(:bottom_member) } + + context 'authorization' do + before { sign_in(person) } + + it "may index when person has finance permission on layer group" do + get :new, group_id: group.id, invoice: { recipient_ids: [] } + expect(response).to be_success + end + + it "may update when person has finance permission on layer group" do + put :update, group_id: group.id, invoice: { recipient_ids: [] } + expect(response).to redirect_to group_invoices_path(group) + end + + it "may not index when person has finance permission on layer group" do + expect do + get :new, group_id: groups(:top_layer).id, invoice: { recipient_ids: [] } + end.to raise_error(CanCan::AccessDenied) + end + + it "may not edit when person has finance permission on layer group" do + expect do + put :update, group_id: groups(:top_layer).id, invoice: { recipient_ids: [] } + end.to raise_error(CanCan::AccessDenied) + end + end + + context 'authorized' do + include ActiveSupport::Testing::TimeHelpers + + before { sign_in(person) } + + it 'GET#new assigns_attributes and renders crud/new template' do + get :new, group_id: group.id, invoice: { recipient_ids: person.id } + expect(response).to render_template('crud/new') + expect(assigns(:invoice).recipients).to eq [person] + end + + it 'GET#new via xhr assigns invoice items and total' do + xhr :get, :new, { group_id: group.id, invoice: invoice_attrs } + expect(assigns(:invoice).invoice_items).to have(2).items + expect(assigns(:invoice).calculated[:total]).to eq 3 + expect(response).to render_template('invoice_lists/new') + end + + it 'POST#create creates an invoice for single member' do + expect do + post :create, { group_id: group.id, invoice: invoice_attrs.merge(recipient_ids: person.id) } + end.to change { group.invoices.count }.by(1) + + expect(response).to redirect_to group_invoices_path(group) + expect(flash[:notice]).to include 'Rechnung Title wurde erstellt.' + end + + it 'POST#create creates an invoice for each member of group' do + Fabricate(Group::BottomLayer::Leader.name.to_sym, group: group, person: Fabricate(:person)) + + expect do + post :create, { group_id: group.id, invoice: invoice_attrs } + end.to change { group.invoices.count }.by(2) + + expect(response).to redirect_to group_invoices_path(group) + expect(flash[:notice]).to include 'Rechnung Title wurde für 2 Empfänger erstellt.' + end + + it 'PUT#update informs if not invoice has been selected' do + post :update, { group_id: group.id } + expect(response).to redirect_to group_invoices_path(group) + expect(flash[:alert]).to include 'Es muss mindestens eine Rechnung im Status "Entwurf" ausgewählt werden.' + end + + it 'PUT#update moves invoice to sent state' do + invoice = Invoice.create!(group: group, title: 'test', recipient: person) + travel(1.day) do + expect do + expect do + post :update, { group_id: group.id, ids: invoice.id } + end.to change { invoice.reload.updated_at } + end.not_to change { Delayed::Job.count } + end + expect(response).to redirect_to group_invoices_path(group) + expect(flash[:notice]).to include 'Rechnung wurde gestellt.' + expect(invoice.reload.state).to eq 'issued' + expect(invoice.due_at).to be_present + expect(invoice.issued_at).to be_present + end + + it 'PUT#update can move multiple invoices at once' do + invoice = Invoice.create!(group: group, title: 'test', recipient: person) + other = Invoice.create!(group: group, title: 'test', recipient: person) + travel(1.day) do + expect do + post :update, { group_id: group.id, ids: [invoice.id, other.id].join(',') } + end.to change { other.reload.updated_at } + end + expect(response).to redirect_to group_invoices_path(group) + expect(flash[:notice]).to include '2 Rechnungen wurden gestellt.' + end + + it 'PUT#update enqueues job' do + invoice = Invoice.create!(group: group, title: 'test', recipient: person) + expect do + post :update, { group_id: group.id, ids: [invoice.id].join(','), mail: 'true' } + end.to change { Delayed::Job.count }.by(1) + + expect(response).to redirect_to group_invoices_path(group) + expect(flash[:notice]).to include 'Rechnung wurde gestellt.' + expect(flash[:notice]).to include 'Rechnung wird im Hintergrund per E-Mail verschickt.' + end + + it 'DELETE#destroy informs if no invoice has been selected' do + delete :destroy, { group_id: group.id } + expect(response).to redirect_to group_invoices_path(group) + expect(flash[:alert]).to include 'Zuerst muss eine Rechnung ausgewählt werden.' + end + + it 'DELETE#destroy moves invoice to cancelled state' do + invoice = Invoice.create!(group: group, title: 'test', recipient: person) + expect do + travel(1.day) { delete :destroy, { group_id: group.id, ids: invoice.id } } + end.to change { invoice.reload.updated_at } + expect(response).to redirect_to group_invoices_path(group) + expect(flash[:notice]).to include 'Rechnung wurde storniert.' + expect(invoice.reload.state).to eq 'cancelled' + end + + it 'DELETE#destroy may move multiple invoices to cancelled state' do + invoice = Invoice.create!(group: group, title: 'test', recipient: person) + other = Invoice.create!(group: group, title: 'test', recipient: person) + expect do + travel 1.day do + delete :destroy, { group_id: group.id, ids: [invoice.id, other.id].join(',') } + end + end.to change { other.reload.updated_at } + expect(response).to redirect_to group_invoices_path(group) + expect(flash[:notice]).to include '2 Rechnungen wurden storniert.' + expect(other.reload.state).to eq 'cancelled' + end + end + + def invoice_attrs + { + title: 'Title', + recipient_ids: group.people.limit(2).collect(&:id).join(','), + invoice_items_attributes: { '1' => { name: 'item1', unit_cost: 1, count: 1}, + '2' => { name: 'item2', unit_cost: 2, count: 1 } } + } + end +end diff --git a/spec/controllers/invoices_controller_spec.rb b/spec/controllers/invoices_controller_spec.rb new file mode 100644 index 0000000000..69425a714b --- /dev/null +++ b/spec/controllers/invoices_controller_spec.rb @@ -0,0 +1,171 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +require 'spec_helper' + +describe InvoicesController do + + let(:group) { groups(:bottom_layer_one) } + let(:person) { people(:bottom_member) } + + context 'authorization' do + before { sign_in(person) } + + it "may index when person has finance permission on layer group" do + get :index, group_id: group.id + expect(response).to be_success + end + + it "may edit when person has finance permission on layer group" do + invoice = Invoice.create!(group: group, title: 'test', recipient: person) + get :edit, group_id: group.id, id: invoice.id + expect(response).to be_success + end + + it "may not index when person has no finance permission on layer group" do + expect do + get :index, group_id: groups(:top_layer).id + end.to raise_error(CanCan::AccessDenied) + end + + it "may not edit when person has no finance permission on layer group" do + invoice = Invoice.create!(group: groups(:top_layer), title: 'test', recipient: person) + expect do + get :edit, group_id: groups(:top_layer).id, id: invoice.id + end.to raise_error(CanCan::AccessDenied) + end + end + + context 'searching' do + let(:invoice) { invoices(:invoice) } + before { sign_in(person) } + + it 'GET#index finds invoices by title' do + get :index, group_id: group.id, q: 'Invoice' + expect(assigns(:invoices)).to have(1).item + end + + it 'GET#index finds invoices by sequence_number' do + get :index, group_id: group.id, q: invoices(:invoice).sequence_number + expect(assigns(:invoices)).to have(1).item + end + + it 'GET#index finds invoices by recipient.last_name' do + get :index, group_id: group.id, q: people(:top_leader).last_name + expect(assigns(:invoices)).to have(2).item + end + + it 'GET#index finds nothing for dummy' do + get :index, group_id: group.id, q: 'dummy' + expect(assigns(:invoices)).to be_empty + end + + it 'filters invoices by state' do + get :index, group_id: group.id, state: :draft + expect(assigns(:invoices)).to have(1).item + end + + it 'filters invoices by due_since' do + invoice.update(due_at: 2.weeks.ago) + get :index, group_id: group.id, due_since: :one_week + expect(assigns(:invoices)).to have(1).item + end + end + + context 'show' do + let(:invoice) { invoices(:invoice) } + before { sign_in(person) } + + it 'GET#show assigns reminder if invoice has been sent' do + invoice.update(state: :sent) + get :show, group_id: group.id, id: invoice.id + expect(assigns(:reminder)).to be_present + expect(assigns(:reminder_valid)).to eq true + end + + it 'GET#show assigns payment if invoice has been sent' do + invoice.update(state: :sent) + get :show, group_id: group.id, id: invoice.id + expect(assigns(:payment)).to be_present + expect(assigns(:payment_valid)).to eq true + expect(assigns(:payment).amount).to eq 5 + end + + it 'GET#show assigns payment with amount_open' do + invoice.update(state: :sent) + invoice.payments.create!(amount: 0.5) + get :show, group_id: group.id, id: invoice.id + expect(assigns(:payment)).to be_present + expect(assigns(:payment_valid)).to eq true + expect(assigns(:payment).amount).to eq 4.5 + end + + it 'GET#show assigns reminder with flash parameters' do + invoice.update(state: :sent) + allow(subject).to receive(:flash).and_return(payment_reminder: { due_at: invoice.due_at }) + get :show, group_id: group.id, id: invoice.id + expect(assigns(:reminder)).to be_present + expect(assigns(:reminder_valid)).to eq false + end + + it 'GET#show assigns payment with flash parameters' do + invoice.update(state: :sent) + allow(subject).to receive(:flash).and_return(payment: {}) + get :show, group_id: group.id, id: invoice.id + expect(assigns(:payment)).to be_present + expect(assigns(:payment_valid)).to eq false + end + + it 'exports pdf' do + get :show, group_id: group.id, id: invoice.id, format: :pdf + + expect(response.header['Content-Disposition']).to match(/Rechnung-#{invoice.sequence_number}.pdf/) + expect(response.content_type).to eq('application/pdf') + end + + it 'exports csv' do + get :show, group_id: group.id, id: invoice.id, format: :csv + + expect(response.header['Content-Disposition']).to match(/Rechnung-#{invoice.sequence_number}.csv/) + expect(response.content_type).to eq('text/csv') + end + end + + it 'DELETE#destroy moves invoice to cancelled state' do + sign_in(person) + + invoice = Invoice.create!(group: group, title: 'test', recipient: person) + expect do + delete :destroy, group_id: group.id, id: invoice.id + end.not_to change { group.invoices.count } + expect(invoice.reload.state).to eq 'cancelled' + expect(response).to redirect_to group_invoices_path(group) + expect(flash[:notice]).to eq 'Rechnung wurde storniert.' + end + + context '#index' do + before { sign_in(person) } + + it 'exports pdf' do + get :index, group_id: group.id, format: :pdf + expect(response.header['Content-Disposition']).to match(/rechnungen.pdf/) + expect(response.content_type).to eq('application/pdf') + end + + it 'exports labels pdf' do + get :index, group_id: group.id, label_format_id: label_formats(:standard).id, format: :pdf + expect(response.content_type).to eq('application/pdf') + end + + it 'exports pdf' do + get :index, group_id: group.id, format: :csv + expect(response.header['Content-Disposition']).to match(/rechnungen.csv/) + expect(response.content_type).to eq('text/csv') + end + end + +end diff --git a/spec/controllers/label_formats/settings_controller_spec.rb b/spec/controllers/label_formats/settings_controller_spec.rb new file mode 100644 index 0000000000..5a12a7faba --- /dev/null +++ b/spec/controllers/label_formats/settings_controller_spec.rb @@ -0,0 +1,42 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +require 'spec_helper' + +describe LabelFormat::SettingsController do + + let(:user) { people(:top_leader) } + + context 'PUT :update' do + render_views + + it 'renders update.js' do + sign_in(user) + + put :update, show_global_label_formats: '', format: :js + + is_expected.to render_template('update') + end + + it 'sets flag to false if param empty' do + sign_in(user) + + put :update, show_global_label_formats: '', format: :js + user.reload + expect(user.show_global_label_formats).to be_falsey + end + + it 'sets flag to true if param not empty' do + sign_in(user) + + put :update, show_global_label_formats: 'true', format: :js + user.reload + expect(user.show_global_label_formats).to be_truthy + end + end + +end diff --git a/spec/controllers/label_formats_controller_spec.rb b/spec/controllers/label_formats_controller_spec.rb new file mode 100644 index 0000000000..10ec25c6bc --- /dev/null +++ b/spec/controllers/label_formats_controller_spec.rb @@ -0,0 +1,106 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +require 'spec_helper' + +describe LabelFormatsController do + + let(:group) { groups(:top_group) } + let(:person) { people(:top_leader) } + + describe 'with admin permissions' do + + before do + sign_in(person) + end + + it 'create global label' do + expect do + post :create, global: 'true', + label_format: { name: 'foo layer', + page_size: 'A4', + landscape: false, + font_size: 12, + width: 60, height: 30, + count_horizontal: 3, + count_vertical: 8, + padding_top: 5, + padding_left: 5 } + end.to change { LabelFormat.count }.by(1) + + expect(LabelFormat.last.person_id).to eq(nil) + end + + it 'create personal label' do + expect do + post :create, global: 'false', + label_format: { name: 'foo layer', + page_size: 'A4', + landscape: false, + font_size: 12, + width: 60, height: 30, + count_horizontal: 3, + count_vertical: 8, + padding_top: 5, + padding_left: 5 } + end.to change { LabelFormat.count }.by(1) + + expect(LabelFormat.last.person_id).to eq(person.id) + end + end + + describe 'without admin permissions' do + + let(:person) { Fabricate(Group::TopGroup::Member.name.to_sym, group: groups(:top_group)).person } + + before do + sign_in(person) + end + + it 'create personal label' do + expect do + post :create, global: 'false', + label_format: { name: 'foo layer', + page_size: 'A4', + landscape: false, + font_size: 12, + width: 60, height: 30, + count_horizontal: 3, + count_vertical: 8, + padding_top: 5, + padding_left: 5 } + end.to change { LabelFormat.count }.by(1) + + expect(LabelFormat.last.person_id).to eq(person.id) + end + + it 'can not create global label' do + expect do + post :create, global: 'true', + label_format: { name: 'foo layer', + page_size: 'A4', + landscape: false, + font_size: 12, + width: 60, height: 30, + count_horizontal: 3, + count_vertical: 8, + padding_top: 5, + padding_left: 5 } + end.to change { LabelFormat.count }.by(1) + + expect(LabelFormat.last.person_id).to eq(person.id) + end + + it 'sorts global formats' do + get :index, sort: 'dimensions', sort_dir: 'desc' + + expect(assigns(:global_entries)).to eq(label_formats(:standard, :large, :envelope)) + end + + end + +end diff --git a/spec/controllers/notes_controller_spec.rb b/spec/controllers/notes_controller_spec.rb new file mode 100644 index 0000000000..7fb3f7472f --- /dev/null +++ b/spec/controllers/notes_controller_spec.rb @@ -0,0 +1,106 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Dachverband Schweizer Jugendparlamente. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +require 'spec_helper' + +describe NotesController do + + let(:top_leader) { people(:top_leader) } + let(:bottom_member) { people(:bottom_member) } + + before { sign_in(top_leader) } + + describe 'GET #index' do + let(:group) { groups(:top_layer) } + let(:top_leader) { people(:top_leader) } + let(:bottom_member) { people(:bottom_member) } + + it 'assignes all notes of layer' do + n1 = Note.create!(author: top_leader, subject: top_leader, text: 'lorem') + _n2 = Note.create!(author: top_leader, subject: bottom_member, text: 'ipsum') + get :index, group_id: group.id + + expect(assigns(:notes)).to eq([n1]) + end + end + + describe 'POST #create' do + it 'creates person notes' do + expect do + post :create, group_id: bottom_member.groups.first.id, + person_id: bottom_member.id, + note: { text: 'Lorem ipsum' }, + format: :js + end.to change { Note.count }.by(1) + + expect(assigns(:note).text).to eq('Lorem ipsum') + expect(assigns(:note).subject).to eq(bottom_member) + expect(response.status).to eq(200) + end + + it 'creates group notes' do + group = bottom_member.groups.first + expect do + post :create, group_id: group.id, + note: { text: 'Lorem ipsum' }, + format: :js + end.to change { Note.count }.by(1) + + expect(assigns(:note).text).to eq('Lorem ipsum') + expect(assigns(:note).subject).to eq(group) + expect(response.status).to eq(200) + is_expected.to render_template('create') + end + + it 'redirects for html requests' do + group = bottom_member.groups.first + expect do + post :create, group_id: group.id, + note: { text: 'Lorem ipsum' } + end.to change { Note.count }.by(1) + is_expected.to redirect_to(group_path(group)) + end + + it 'cannot create notes on lower layer' do + sign_in(Fabricate(Group::TopGroup::LocalGuide.name.to_sym, group: groups(:top_group)).person) + + expect do + expect do + post :create, group_id: bottom_member.groups.first.id, + person_id: bottom_member.id, + note: { text: 'Lorem ipsum' }, + format: :js + end.to raise_error(CanCan::AccessDenied) + end.not_to change { Note.count } + end + end + + describe 'POST #destroy' do + it 'destroys person note' do + n = Note.create!(author: top_leader, subject: top_leader, text: 'lorem') + expect do + post :destroy, group_id: n.subject.groups.first.id, + person_id: n.subject_id, + id: n.id, + format: :js + end.to change { Note.count }.by(-1) + is_expected.to render_template('destroy') + end + + it 'redirects for html requests' do + group = top_leader.groups.first + n = Note.create!(author: top_leader, subject: top_leader, text: 'lorem') + expect do + post :destroy, group_id: group.id, + person_id: n.subject_id, + id: n.id + end.to change { Note.count }.by(-1) + is_expected.to redirect_to(group_person_path(group, top_leader)) + end + end + +end diff --git a/spec/controllers/payment_reminders_controller_spec.rb b/spec/controllers/payment_reminders_controller_spec.rb new file mode 100644 index 0000000000..01dd6dcb9a --- /dev/null +++ b/spec/controllers/payment_reminders_controller_spec.rb @@ -0,0 +1,39 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +require 'spec_helper' + +describe PaymentRemindersController do + + let(:group) { groups(:bottom_layer_one) } + let(:person) { people(:bottom_member) } + let(:invoice) { invoices(:invoice) } + + before { sign_in(person) } + + it 'POST#creates valid arguments create payment_reminder' do + invoice.update(state: :sent) + expect do + post :create, group_id: group.id, invoice_id: invoice.id, + payment_reminder: { due_at: invoice.due_at + 2.weeks } + end.to change { invoice.payment_reminders.count }.by(1) + + expect(flash[:notice]).to be_present + expect(response).to redirect_to(group_invoice_path(group, invoice)) + end + + it 'POST#creates invalid arguments redirect back' do + invoice.update(state: :sent) + expect do + post :create, group_id: group.id, invoice_id: invoice.id, + payment_reminder: { due_at: invoice.due_at } + end.not_to change { invoice.payment_reminders.count } + expect(assigns(:payment_reminder)).to be_invalid + expect(response).to redirect_to(group_invoice_path(group, invoice)) + end + +end diff --git a/spec/controllers/payments_controller_spec.rb b/spec/controllers/payments_controller_spec.rb new file mode 100644 index 0000000000..40baa73603 --- /dev/null +++ b/spec/controllers/payments_controller_spec.rb @@ -0,0 +1,38 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +require 'spec_helper' + +describe PaymentsController do + + let(:group) { groups(:bottom_layer_one) } + let(:person) { people(:bottom_member) } + let(:invoice) { invoices(:invoice) } + + before { sign_in(person) } + + it 'POST#creates valid arguments create payment' do + invoice.update(state: :sent) + expect do + post :create, group_id: group.id, invoice_id: invoice.id, + payment: { amount: invoice.total } + end.to change { invoice.payments.count }.by(1) + + expect(flash[:notice]).to be_present + expect(response).to redirect_to(group_invoice_path(group, invoice)) + end + + it 'POST#creates invalid arguments redirect back' do + invoice.update(state: :sent) + expect do + post :create, group_id: group.id, invoice_id: invoice.id, payment: { amount: '' } + end.not_to change { invoice.payments.count } + expect(assigns(:payment)).to be_invalid + expect(response).to redirect_to(group_invoice_path(group, invoice)) + end + +end diff --git a/spec/controllers/people_controller_spec.rb b/spec/controllers/people_controller_spec.rb index b5f5e231b1..032bb43dab 100644 --- a/spec/controllers/people_controller_spec.rb +++ b/spec/controllers/people_controller_spec.rb @@ -1,6 +1,6 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. @@ -31,6 +31,7 @@ @tg_member = Fabricate(Group::TopGroup::Member.name.to_sym, group: groups(:top_group)).person Fabricate(:phone_number, contactable: @tg_member, number: '123', label: 'Privat', public: true) Fabricate(:phone_number, contactable: @tg_member, number: '456', label: 'Mobile', public: false) + Fabricate(:phone_number, contactable: @tg_member, number: '789', label: 'Office', public: true) Fabricate(:social_account, contactable: @tg_member, name: 'facefoo', label: 'Facebook', public: true) Fabricate(:social_account, contactable: @tg_member, name: 'skypefoo', label: 'Skype', public: false) Fabricate(Group::BottomGroup::Leader.name.to_sym, group: groups(:bottom_group_one_one), person: @tg_member) @@ -55,30 +56,30 @@ context 'default sort' do it "sorts by name" do - get :index, group_id: group, kind: 'layer', role_type_ids: role_type_ids + get :index, group_id: group, range: 'layer', filters: { role: { role_type_ids: role_type_ids } } expect(assigns(:people).collect(&:id)).to eq([@tg_extern, top_leader, @tg_member].collect(&:id)) end it "people.default_sort setting can override it to sort by role" do allow(Settings.people).to receive_messages(default_sort: 'role') - get :index, group_id: group, kind: 'layer', role_type_ids: role_type_ids + get :index, group_id: group, range: 'layer', filters: { role: { role_type_ids: role_type_ids }} expect(assigns(:people).collect(&:id)).to eq([top_leader, @tg_member, @tg_extern].collect(&:id)) end end it "sorts based on last_name" do - get :index, group_id: group, kind: 'layer', role_type_ids: role_type_ids, sort: :last_name, sort_dir: :asc + get :index, group_id: group, range: 'layer', filters: { role: { role_type_ids: role_type_ids } }, sort: :last_name, sort_dir: :asc expect(assigns(:people).collect(&:id)).to eq([@tg_extern, top_leader, @tg_member].collect(&:id)) end it "sorts based on roles" do - get :index, group_id: group, kind: 'layer', role_type_ids: role_type_ids, sort: :roles, sort_dir: :asc + get :index, group_id: group, range: 'layer', filters: { role: { role_type_ids: role_type_ids } }, sort: :roles, sort_dir: :asc expect(assigns(:people)).to eq([top_leader, @tg_member, @tg_extern]) end %w(first_name nickname zip_code town).each do |attr| it "sorts based on #{attr}" do - get :index, group_id: group, kind: 'layer', role_type_ids: role_type_ids, sort: attr, sort_dir: :asc + get :index, group_id: group, range: 'layer', filters: { role: { role_type_ids: role_type_ids } }, sort: attr, sort_dir: :asc expect(assigns(:people)).to eq([@tg_member, top_leader, @tg_extern]) end end @@ -93,13 +94,13 @@ end it 'loads externs of a group when type given' do - get :index, group_id: group, role_type_ids: [Role::External.id].join('-') + get :index, group_id: group, filters: { role: { role_type_ids: [Role::External.id].join('-') } } expect(assigns(:people).collect(&:id)).to match_array([@tg_extern].collect(&:id)) end it 'loads selected roles of a group when types given' do - get :index, group_id: group, role_type_ids: [Role::External.id, Group::TopGroup::Member.id].join('-') + get :index, group_id: group, filters: { role: { role_type_ids: [Role::External.id, Group::TopGroup::Member.id].join('-') } } expect(assigns(:people).collect(&:id)).to match_array([@tg_member, @tg_extern].collect(&:id)) end @@ -134,26 +135,67 @@ end end - context '.csv' do - it 'exports address csv files' do - get :index, group_id: group, format: :csv + context 'background job' do + it 'exports csv' do + expect do + get :index, group_id: group, format: :csv + expect(flash[:notice]).to match(/Export wird im Hintergrund gestartet und nach Fertigstellung an \S+@\S+ versendet./) + end.to change(Delayed::Job, :count).by(1) + end + + it 'exports xlsx' do + expect do + get :index, group_id: group, format: :xlsx + expect(flash[:notice]).to match(/Export wird im Hintergrund gestartet und nach Fertigstellung an \S+@\S+ versendet./) + end.to change(Delayed::Job, :count).by(1) + end - expect(@response.content_type).to eq('text/csv') - expect(@response.body).to match(/^Vorname;Nachname;.*Privat/) - expect(@response.body).to match(/^Top;Leader;.*/) - expect(@response.body).to match(/123/) - expect(@response.body).not_to match(/skypefoo/) - expect(@response.body).not_to match(/Zusätzliche Angaben/) - expect(@response.body).not_to match(/Mobile/) + it 'does not export if no mail is given' do + expect_any_instance_of(Person).to receive(:email).at_least(1).times.and_return(nil) + expect do + get :index, group_id: group, format: :csv + expect(flash[:alert]).to match(/wird eine Email Adresse benötigt/) + end.to change(Delayed::Job, :count).by(0) end + end + + context '.vcf' do + it 'exports vcf files' do + e1 = Fabricate(:additional_email, contactable: @tg_member, public: true) + e2 = Fabricate(:additional_email, contactable: @tg_member, public: false) + @tg_member.update_attributes(birthday: '09.10.1978') + + get :index, group_id: group, format: :vcf - it 'exports full csv files' do - get :index, group_id: group, details: true, format: :csv + expect(@response.content_type).to eq('text/vcard') + cards = @response.body.split("END:VCARD\n") + expect(cards.length).to equal(2); - expect(@response.content_type).to eq('text/csv') - expect(@response.body).to match(/^Vorname;Nachname;.*;Zusätzliche Angaben;.*Privat;.*Mobile;.*Facebook;.*Skype/) - expect(@response.body).to match(/^Top;Leader;.*;bla bla/) - expect(@response.body).to match(/123;456;.*facefoo;skypefoo/) + if cards[1].include?("N:Member;Bottom") + cards.reverse! + end + + expect(cards[0][0..23]).to eq("BEGIN:VCARD\nVERSION:3.0\n") + expect(cards[0]).to match(/^N:Leader;Top;;;/) + expect(cards[0]).to match(/^FN:Top Leader/) + expect(cards[0]).to match(/^ADR:;;;Supertown;;;/) + expect(cards[0]).to match(/^EMAIL;TYPE=pref:top_leader@example.com/) + expect(cards[0]).not_to match(/^TEL.*/) + expect(cards[0]).not_to match(/^NICKNAME.*/) + expect(cards[0]).not_to match(/^BDAY.*/) + + expect(cards[1][0..23]).to eq("BEGIN:VCARD\nVERSION:3.0\n") + expect(cards[1]).to match(/^N:Zoe;Al;;;/) + expect(cards[1]).to match(/^FN:Al Zoe/) + expect(cards[1]).to match(/^NICKNAME:al/) + expect(cards[1]).to match(/^ADR:;;;Eye;;8000;/) + expect(cards[1]).to match(/^EMAIL;TYPE=pref:#{@tg_member.email}/) + expect(cards[1]).to match(/^EMAIL;TYPE=privat:#{e1.email}/) + expect(cards[1]).not_to match(/^EMAIL.*:#{e2.email}/) + expect(cards[1]).to match(/^TEL;TYPE=privat:123/) + expect(cards[1]).to match(/^TEL;TYPE=office:789/) + expect(cards[1]).not_to match(/^TEL.*:456/) + expect(cards[1]).to match(/^BDAY:19781009/) end end @@ -191,7 +233,7 @@ before { sign_in(@bl_leader) } it 'loads group members when no types given' do - get :index, group_id: group, kind: 'layer' + get :index, group_id: group, range: 'layer' expect(assigns(:people).collect(&:id)).to match_array( [people(:bottom_member), @bl_leader].collect(&:id) @@ -200,8 +242,8 @@ it 'loads selected roles of a group when types given' do get :index, group_id: group, - role_type_ids: [Group::BottomGroup::Member.id, Role::External.id].join('-'), - kind: 'layer' + filters: { role: { role_type_ids: [Group::BottomGroup::Member.id, Role::External.id].join('-') } }, + range: 'layer' expect(assigns(:people).collect(&:id)).to match_array([@bg_member, @bl_extern].collect(&:id)) end @@ -213,29 +255,18 @@ body: group, role_type: group.class.role_types.first.sti_name) - get :index, group_id: group.id, kind: 'layer' + get :index, group_id: group.id, range: 'layer' expect(assigns(:person_add_requests)).to be_nil end - it 'exports full csv when types given and ability exists' do - get :index, group_id: group, - role_type_ids: [Group::BottomGroup::Member.id, Role::External.id].join('-'), - kind: 'layer', - details: true, - format: :csv - - expect(@response.content_type).to eq('text/csv') - expect(@response.body).to match(/^Vorname;Nachname;.*Zusätzliche Angaben/) - end - context 'json' do render_views it 'renders json with only the one role in this group' do get :index, group_id: group, - kind: 'layer', - role_type_ids: [Group::BottomGroup::Leader.id, Role::External.id].join('-'), + range: 'layer', + filters: { role: { role_type_ids: [Group::BottomGroup::Leader.id, Role::External.id].join('-') } }, format: :json json = JSON.parse(@response.body) person = json['people'].find { |p| p['id'] == @tg_member.id.to_s } @@ -243,38 +274,21 @@ end end end - - context 'with contact data' do - before { sign_in(@tg_member) } - - it 'exports only address csv when types given and no ability exists' do - get :index, group_id: group, - role_type_ids: [Group::BottomLayer::Leader.id, Group::BottomLayer::Member.id].join('-'), - kind: 'layer', - details: true, - format: :csv - - expect(@response.content_type).to eq('text/csv') - expect(@response.body).to match(/^Vorname;Nachname;.*/) - expect(@response.body).not_to match(/Zusätzliche Angaben/) - expect(@response.body.split("\n").size).to eq(2) - end - end end context 'deep' do let(:group) { groups(:top_layer) } it 'loads group members when no types are given' do - get :index, group_id: group, kind: 'deep' + get :index, group_id: group, range: 'deep' expect(assigns(:people).collect(&:id)).to match_array([]) end it 'loads selected roles of a group when types given' do get :index, group_id: group, - role_type_ids: [Group::BottomGroup::Leader.id, Role::External.id].join('-'), - kind: 'deep' + filters: { role: { role_type_ids: [Group::BottomGroup::Leader.id, Role::External.id].join('-') } }, + range: 'deep' expect(assigns(:people).collect(&:id)).to match_array([@bg_leader, @tg_member, @tg_extern].collect(&:id)) end @@ -284,8 +298,8 @@ it 'renders json with only the one role in this group' do get :index, group_id: group, - kind: 'deep', - role_type_ids: [Group::BottomGroup::Leader.id, Role::External.id].join('-'), + range: 'deep', + filters: { role: { role_type_ids: [Group::BottomGroup::Leader.id, Role::External.id].join('-') } }, format: :json json = JSON.parse(@response.body) person = json['people'].find { |p| p['id'] == @tg_member.id.to_s } @@ -293,6 +307,24 @@ end end end + + context 'filter_id' do + let(:group) { groups(:top_layer) } + + it 'loads selected roles of a group' do + filter = PeopleFilter.create!( + name: 'My Filter', + range: 'deep', + filter_chain: { + role: { role_type_ids: [Group::BottomGroup::Leader.id, Role::External.id].join('-') } + } + ) + + get :index, group_id: group, filter_id: filter.id + + expect(assigns(:people).collect(&:id)).to match_array([@bg_leader, @tg_member, @tg_extern].collect(&:id)) + end + end end context 'PUT update' do @@ -718,4 +750,36 @@ end end + + context 'DELETE #destroy' do + + let(:member) { people(:bottom_member) } + let(:admin) { people(:top_leader) } + + describe 'as admin user' do + before { sign_in(admin) } + + it 'can delete person' do + delete :destroy, group_id: member.primary_group.id, id: member.id + expect(response.status).to eq(302) + end + + it 'deletes person' do + expect do + delete :destroy, group_id: member.primary_group.id, id: member.id + end.to change(Person, :count).by(-1) + end + end + + describe 'as normal user' do + before { sign_in(member) } + + it 'fails without permissions' do + expect do + delete :destroy, group_id: group.id, id: admin.id + end.to raise_error(CanCan::AccessDenied) + end + end + end + end diff --git a/spec/controllers/people_filters_controller_spec.rb b/spec/controllers/people_filters_controller_spec.rb index c6d9c70162..b073e0c6ca 100644 --- a/spec/controllers/people_filters_controller_spec.rb +++ b/spec/controllers/people_filters_controller_spec.rb @@ -15,32 +15,31 @@ let(:group) { groups(:top_group) } let(:role_types) { [Group::TopGroup::Leader, Group::TopGroup::Member] } let(:role_type_ids) { role_types.collect(&:id) } - let(:role_type_ids_string) { role_type_ids.join(RelatedRoleType::Assigners::ID_URL_SEPARATOR) } + let(:role_type_ids_string) { role_type_ids.join(Person::Filter::Base::ID_URL_SEPARATOR) } let(:role_type_names) { role_types.collect(&:sti_name) } context 'GET new' do it 'builds entry with group and existing params' do - get :new, group_id: group.id, people_filter: { role_type_ids: role_type_ids_string } + get :new, group_id: group.id, filters: { role: { role_type_ids: role_type_ids_string } } filter = assigns(:people_filter) expect(filter.group).to eq(group) - expect(filter.role_type_ids).to eq(role_type_ids) - expect(filter.role_types).to eq(role_type_names) + expect(assigns(:qualification_kinds)).to be_present end end context 'POST create' do it 'redirects to show for search' do expect do - post :create, group_id: group.id, people_filter: { role_type_ids: role_type_ids }, button: 'search' + post :create, group_id: group.id, filters: { role: { role_type_ids: role_type_ids } }, button: 'search' end.not_to change { PeopleFilter.count } - is_expected.to redirect_to(group_people_path(group, role_type_ids: role_type_ids_string, kind: 'deep')) + is_expected.to redirect_to(group_people_path(group, filters: { role: { role_type_ids: role_type_ids_string} }, range: 'deep')) end it 'redirects to show for empty search' do expect do - post :create, group_id: group.id, button: 'search', people_filter: {} + post :create, group_id: group.id, button: 'search', people_filter: {}, filters: { qualification: { validity: 'active' }} end.not_to change { PeopleFilter.count } is_expected.to redirect_to(group_people_path(group)) @@ -48,11 +47,10 @@ it 'saves filter and redirects to show with save' do expect do - expect do - post :create, group_id: group.id, people_filter: { role_type_ids: role_type_ids, name: 'Test Filter' }, button: 'save' - is_expected.to redirect_to(group_people_path(group, role_type_ids: role_type_ids_string, kind: 'deep', name: 'Test Filter')) - end.to change { PeopleFilter.count }.by(1) - end.to change { RelatedRoleType.count }.by(2) + post :create, group_id: group.id, filters: { role: { role_type_ids: role_type_ids } }, range: 'deep', name: 'Test Filter', button: 'save' + expect(assigns(:people_filter)).to be_valid + is_expected.to redirect_to(group_people_path(group, filter_id: assigns(:people_filter).id)) + end.to change { PeopleFilter.count }.by(1) end context 'with read only permissions' do @@ -66,48 +64,18 @@ it 'redirects to show with search' do expect do - post :create, group_id: group.id, people_filter: { role_type_ids: role_type_ids }, button: 'search' + post :create, group_id: group.id, filters: { role: { role_type_ids: role_type_ids } }, button: 'search' end.not_to change { PeopleFilter.count } - is_expected.to redirect_to(group_people_path(group, role_type_ids: role_type_ids_string, kind: 'deep')) + is_expected.to redirect_to(group_people_path(group, filters: { role: { role_type_ids: role_type_ids_string } }, range: 'deep')) end it 'is not authorized with save' do expect do - post :create, group_id: group.id, people_filter: { role_type_ids: role_type_ids, name: 'Test Filter' }, button: 'save' + post :create, group_id: group.id, filters: { role: { role_type_ids: role_type_ids } }, name: 'Test Filter' , button: 'save' end.to raise_error(CanCan::AccessDenied) end end end - context 'GET qualification' do - it 'builds entry with group and existing params' do - get :qualification, group_id: group.id - - expect(assigns(:qualification_kinds)).to be_present - end - - context 'user without index_full' do - context 'with group read' do - let(:user) { Fabricate(Group::TopGroup::Member.name, group: groups(:top_group)).person } - - it 'is not authorized' do - expect do - get :qualification, group_id: group.id - end.to raise_error(CanCan::AccessDenied) - end - end - - context 'in other layer' do - let(:user) { Fabricate(Group::TopGroup::LocalGuide.name, group: groups(:top_group)).person } - let(:group) { groups(:bottom_layer_one) } - - it 'is not authorized' do - expect do - get :qualification, group_id: group.id - end.to raise_error(CanCan::AccessDenied) - end - end - end - end end diff --git a/spec/controllers/person/colleagues_controller_spec.rb b/spec/controllers/person/colleagues_controller_spec.rb new file mode 100644 index 0000000000..9cbaf111d6 --- /dev/null +++ b/spec/controllers/person/colleagues_controller_spec.rb @@ -0,0 +1,42 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Dachverband Schweizer Jugendparlamente. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +require 'spec_helper' + +describe Person::ColleaguesController do + + let(:top_leader) { people(:top_leader) } + + before { sign_in(top_leader) } + + describe 'GET #index' do + it 'returns ordered colleagues' do + c1 = create_person(Group::TopGroup::LocalGuide, :top_group) + c2 = create_person(Group::BottomLayer::Leader, :bottom_layer_one) + c3 = create_person(Group::BottomGroup::Leader, :bottom_group_one_one) + c4 = create_person(Group::BottomGroup::Member, :bottom_group_one_one) + + get :index, group_id: groups(:top_group).id, id: c1.id, sort: :roles + + expect(assigns(:colleagues)).to eq([c1, c2, c3, c4]) + end + + it 'contains nobody if persons company_name is blank' do + p = Fabricate(Group::TopGroup::LocalGuide.name.to_sym, group: groups(:top_group)).person + + get :index, group_id: groups(:top_group).id, id: p.id + + expect(assigns(:colleagues)).to eq([]) + end + end + + def create_person(role, group) + Fabricate(role.name.to_sym, + group: groups(group), + person: Fabricate(:person, company_name: 'Foo Inc.')).person + end +end diff --git a/spec/controllers/person/company_name_controller_spec.rb b/spec/controllers/person/company_name_controller_spec.rb new file mode 100644 index 0000000000..71eaac5413 --- /dev/null +++ b/spec/controllers/person/company_name_controller_spec.rb @@ -0,0 +1,28 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Dachverband Schweizer Jugendparlamente. This file is +# part of hitobito and licensed under the Affero General Public License +# version 3 or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +require 'spec_helper' + +describe Person::CompanyNameController do + + let(:top_leader) { people(:top_leader) } + + before { sign_in(top_leader) } + + context 'GET index' do + it 'queries company-names' do + Fabricate(:person, company_name: 'Puzzle ITC') + Fabricate(:person, company_name: 'PuzzleWorks Ltd') + Fabricate(:person, company_name: 'Swisscom') + get :index, q: 'puz' + + expect(response.body).to match(/Puzzle ITC/) + expect(response.body).to match(/PuzzleWorks Ltd/) + end + end + +end diff --git a/spec/controllers/person/csv_imports_controller_spec.rb b/spec/controllers/person/csv_imports_controller_spec.rb index e1f868f4be..e667bdca01 100644 --- a/spec/controllers/person/csv_imports_controller_spec.rb +++ b/spec/controllers/person/csv_imports_controller_spec.rb @@ -111,7 +111,7 @@ expect { post :create, required_params }.to change(Person, :count).by(1) expect(flash[:notice]).to eq ['1 Person (Leader) wurde erfolgreich importiert.'] expect(flash[:alert]).not_to be_present - is_expected.to redirect_to group_people_path(group, role_type_ids: role_type.id, name: 'Leader') + is_expected.to redirect_to group_people_path(group, filters: { role: { role_type_ids: [role_type.id] } }, name: 'Leader') end context 'mapping misses attribute' do @@ -121,7 +121,7 @@ it 'imports first person and displays errors for second person' do expect { post :create, required_params }.to change(Person, :count).by(0) expect(flash[:alert]).to eq ['1 Person (Leader) wurde nicht importiert.'] - is_expected.to redirect_to group_people_path(group, role_type_ids: role_type.id, name: 'Leader') + is_expected.to redirect_to group_people_path(group, filters: { role: { role_type_ids: [role_type.id] } }, name: 'Leader') end end @@ -132,7 +132,7 @@ it 'is ignored' do expect { post :create, required_params }.to change(Person, :count).by(1) expect(flash[:alert]).to be_blank - is_expected.to redirect_to group_people_path(group, role_type_ids: role_type.id, name: 'Leader') + is_expected.to redirect_to group_people_path(group, filters: { role: { role_type_ids: [role_type.id] } }, name: 'Leader') end end @@ -180,7 +180,7 @@ it 'creates request' do person # create post :create, required_params.merge(update_behaviour: 'override') - is_expected.to redirect_to group_people_path(group, role_type_ids: role_type.id, name: 'Member') + is_expected.to redirect_to group_people_path(group, filters: { role: { role_type_ids: [role_type.id] } }, name: 'Member') expect(person.reload.roles.count).to eq(1) expect(person.town).not_to eq('Wabern') @@ -195,7 +195,7 @@ Fabricate(Group::TopGroup::Member.name, group: groups(:top_group), person: user) post :create, required_params.merge(update_behaviour: 'override') - is_expected.to redirect_to group_people_path(group, role_type_ids: role_type.id, name: 'Member') + is_expected.to redirect_to group_people_path(group, filters: { role: { role_type_ids: [role_type.id] } }, name: 'Member') expect(person.reload.roles.count).to eq(2) expect(person.town).to eq('Wabern') @@ -213,7 +213,7 @@ post :create, required_params - is_expected.to redirect_to group_people_path(group, role_type_ids: role_type.id, name: 'Member') + is_expected.to redirect_to group_people_path(group, filters: { role: { role_type_ids: [role_type.id] } }, name: 'Member') expect(person.reload.roles.count).to eq(1) expect(person.add_requests.count).to eq(1) expect(flash[:alert].join).to match(/Zugriffsanfrage .*erhalten/) diff --git a/spec/controllers/person/history_controller_spec.rb b/spec/controllers/person/history_controller_spec.rb index 951fe85865..798043719c 100644 --- a/spec/controllers/person/history_controller_spec.rb +++ b/spec/controllers/person/history_controller_spec.rb @@ -17,15 +17,16 @@ context 'all roles' do - it 'all group roles ordered by group, to date' do + it 'all group roles ordered by group and layer' do person = Fabricate(:person) r1 = Fabricate(Group::BottomGroup::Member.name.to_sym, group: groups(:bottom_group_one_one), person: person) r2 = Fabricate(Group::BottomGroup::Member.name.to_sym, group: groups(:bottom_group_two_one), person: person, created_at: Date.today - 3.years, deleted_at: Date.today - 2.years) r3 = Fabricate(Group::BottomGroup::Leader.name.to_sym, group: groups(:bottom_group_two_one), person: person) + r4 = Fabricate(Group::BottomGroup::Member.name.to_sym, group: groups(:bottom_group_one_one_one), person: person) get :index, group_id: groups(:bottom_group_one_one).id, id: person.id - expect(assigns(:roles)).to eq([r1, r3, r2]) + expect(assigns(:roles)).to eq([r1, r4, r2, r3]) end end diff --git a/spec/controllers/person/invoices_controller_spec.rb b/spec/controllers/person/invoices_controller_spec.rb new file mode 100644 index 0000000000..c8ac208022 --- /dev/null +++ b/spec/controllers/person/invoices_controller_spec.rb @@ -0,0 +1,30 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2015 Pfadibewegung Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +require 'spec_helper' +require_dependency 'person/invoices_controller' + +describe Person::InvoicesController do + let(:group) { groups(:bottom_layer_one) } + let(:top_leader) { people(:top_leader) } + let(:bottom_member) { people(:bottom_member) } + + it 'may not index person invoices if we have no finance permission in layer' do + sign_in(bottom_member) + expect do + get :index, group_id: groups(:top_group).id, id: top_leader.id + end.to raise_error(CanCan::AccessDenied) + end + + it 'may index my own invoices' do + sign_in(top_leader) + get :index, group_id: groups(:top_group).id, id: top_leader.id + expect(assigns(:invoices)).to have(2).items + end + +end + diff --git a/spec/controllers/person/notes_controller_spec.rb b/spec/controllers/person/notes_controller_spec.rb deleted file mode 100644 index 474cf22fae..0000000000 --- a/spec/controllers/person/notes_controller_spec.rb +++ /dev/null @@ -1,43 +0,0 @@ -# encoding: utf-8 - -# Copyright (c) 2012-2016, Dachverband Schweizer Jugendparlamente. This file is part of -# hitobito_dsj and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito_dsj. - -require 'spec_helper' - -describe Person::NotesController do - - let(:top_leader) { people(:top_leader) } - let(:bottom_member) { people(:bottom_member) } - - before { sign_in(top_leader) } - - describe 'GET #index' do - let(:group) { groups(:top_layer) } - let(:top_leader) { people(:top_leader) } - let(:bottom_member) { people(:bottom_member) } - - it 'assignes all notes of layer' do - n1 = Person::Note.create!(author: top_leader, person: top_leader, text: 'lorem') - n2 = Person::Note.create!(author: top_leader, person: bottom_member, text: 'ipsum') - get :index, id: group.id - - expect(assigns(:notes)).to eq([n1]) - end - end - - describe 'POST #create' do - it 'creates person notes' do - post :create, group_id: bottom_member.groups.first.id, - person_id: bottom_member.id, - person_note: { text: 'Lorem ipsum' } - - expect(Person::Note.count).to eq(1) - expect(assigns(:note).text).to eq('Lorem ipsum') - is_expected.to redirect_to group_person_path(bottom_member.groups.first, bottom_member) - end - end - -end diff --git a/spec/controllers/person/query_controller_spec.rb b/spec/controllers/person/query_controller_spec.rb index 2a87ce8280..443e501d80 100644 --- a/spec/controllers/person/query_controller_spec.rb +++ b/spec/controllers/person/query_controller_spec.rb @@ -8,7 +8,7 @@ require 'spec_helper' describe Person::QueryController do - + let(:top_leader) { people(:top_leader) } before { sign_in(top_leader) } diff --git a/spec/controllers/person/tags_controller_spec.rb b/spec/controllers/person/tags_controller_spec.rb index 95ba34e907..512099c9d3 100644 --- a/spec/controllers/person/tags_controller_spec.rb +++ b/spec/controllers/person/tags_controller_spec.rb @@ -1,9 +1,9 @@ # encoding: utf-8 # Copyright (c) 2012-2016, Dachverband Schweizer Jugendparlamente. This file is part of -# hitobito_dsj and licensed under the Affero General Public License version 3 +# hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito_dsj. +# https://github.com/hitobito/hitobito. require 'spec_helper' diff --git a/spec/controllers/roles_controller_spec.rb b/spec/controllers/roles_controller_spec.rb index 6ab321fc2a..1515a1d09b 100644 --- a/spec/controllers/roles_controller_spec.rb +++ b/spec/controllers/roles_controller_spec.rb @@ -288,12 +288,14 @@ end end - describe 'POST destroy' do + describe 'DELETE destroy' do let(:notice) { "Rolle Member für #{person} in TopGroup wurde erfolgreich gelöscht." } it 'redirects to group' do - post :destroy, group_id: group.id, id: role.id + user = Fabricate(Group::TopGroup::LocalGuide.name.to_sym, group: group) + sign_in(user.person) + delete :destroy, group_id: group.id, id: role.id expect(flash[:notice]).to eq notice is_expected.to redirect_to(group_path(group)) @@ -301,7 +303,7 @@ it 'redirects to person if user can still view person' do Fabricate(Group::TopGroup::Leader.name.to_sym, person: person, group: group) - post :destroy, group_id: group.id, id: role.id + delete :destroy, group_id: group.id, id: role.id expect(flash[:notice]).to eq notice is_expected.to redirect_to(person_path(person)) @@ -320,7 +322,7 @@ person.update_attribute(:primary_group, group) - post :destroy, group_id: group.id, id: role.id + delete :destroy, group_id: group.id, id: role.id expect(flash[:alert]).to eq "Hauptgruppe auf #{group2.to_s} geändert." is_expected.to redirect_to(person_path(person)) @@ -338,7 +340,7 @@ person.update_attribute(:primary_group, group) - post :destroy, group_id: group.id, id: role.id + delete :destroy, group_id: group.id, id: role.id expect(flash[:alert]).to be_nil is_expected.to redirect_to(person_path(person)) @@ -357,7 +359,7 @@ person.update_attribute(:primary_group, group) - post :destroy, group_id: group.id, id: role.id + delete :destroy, group_id: group.id, id: role.id expect(flash[:alert]).to be_nil is_expected.to redirect_to(person_path(person)) @@ -376,7 +378,7 @@ person.update_attribute(:primary_group, group2) - post :destroy, group_id: group.id, id: role.id + delete :destroy, group_id: group.id, id: role.id expect(flash[:alert]).to be_nil is_expected.to redirect_to(person_path(person)) diff --git a/spec/controllers/subscriptions_controller_spec.rb b/spec/controllers/subscriptions_controller_spec.rb index f6badd04fc..b1a3eed44d 100644 --- a/spec/controllers/subscriptions_controller_spec.rb +++ b/spec/controllers/subscriptions_controller_spec.rb @@ -34,11 +34,42 @@ expect(assigns(:person_add_requests)).to eq([]) end - it 'renders csv' do - get :index, group_id: group.id, mailing_list_id: mailing_list.id, format: :csv - lines = response.body.split("\n") - expect(lines.size).to eq(3) - expect(lines[0]).to match(/Vorname;Nachname;.*/) + it 'renders csv in backround job' do + expect do + get :index, group_id: group.id, mailing_list_id: mailing_list.id, format: :csv + expect(flash[:notice]).to match(/Export wird im Hintergrund gestartet und nach Fertigstellung an \S+@\S+ versendet./) + end.to change(Delayed::Job, :count).by(1) + end + + it 'renders xlsx in backround job' do + expect do + get :index, group_id: group.id, mailing_list_id: mailing_list.id, format: :xlsx + expect(flash[:notice]).to match(/Export wird im Hintergrund gestartet und nach Fertigstellung an \S+@\S+ versendet./) + end.to change(Delayed::Job, :count).by(1) + end + + it 'exports vcf files' do + get :index, group_id: group.id, mailing_list_id: mailing_list.id, format: :vcf + expect(@response.content_type).to eq('text/vcard') + + cards = @response.body.split("END:VCARD\n") + expect(cards.length).to equal(2); + + if cards[1].include?("N:Member;Bottom") + cards.reverse! + end + + expect(cards[0][0..23]).to eq("BEGIN:VCARD\nVERSION:3.0\n") + expect(cards[0]).to match(/^N:Member;Bottom;;;/) + expect(cards[0]).to match(/^FN:Bottom Member/) + expect(cards[0]).to match(/^ADR:;;Greatstreet 345;Greattown;;3456;CH/) + expect(cards[0]).to match(/^EMAIL;TYPE=pref:bottom_member@example.com/) + + expect(cards[1][0..23]).to eq("BEGIN:VCARD\nVERSION:3.0\n") + expect(cards[1]).to match(/^N:#{@person_subscription.subscriber.last_name};#{@person_subscription.subscriber.first_name};;;/) + expect(cards[1]).to match(/^FN:#{@person_subscription.subscriber.first_name} #{@person_subscription.subscriber.last_name}/) + expect(cards[1]).to match(/^NICKNAME:#{@person_subscription.subscriber.nickname}/) + expect(cards[1]).to match(/^EMAIL;TYPE=pref:#{@person_subscription.subscriber.email}/) end it 'renders email addresses with additional ones' do diff --git a/spec/decorators/contactable_decorator_spec.rb b/spec/decorators/contactable_decorator_spec.rb index 5bcae40b9a..e4e9e18f12 100644 --- a/spec/decorators/contactable_decorator_spec.rb +++ b/spec/decorators/contactable_decorator_spec.rb @@ -59,17 +59,17 @@ context 'only public' do subject { @group.all_phone_numbers } - it { is_expected.to match(/031.*Home/) } - it { is_expected.to match(/041.*Work/) } - it { is_expected.not_to match(/079.*Mobile/) } + it { is_expected.to match(/tel:031.*Home/) } + it { is_expected.to match(/tel:041.*Work/) } + it { is_expected.not_to match(/tel:079.*Mobile/) } end context 'all' do subject { @group.all_phone_numbers(false) } - it { is_expected.to match(/031.*Home/) } - it { is_expected.to match(/041.*Work/) } - it { is_expected.to match(/079.*Mobile/) } + it { is_expected.to match(/tel:031.*Home/) } + it { is_expected.to match(/tel:041.*Work/) } + it { is_expected.to match(/tel:079.*Mobile/) } end end diff --git a/spec/decorators/event/participation_decorator_spec.rb b/spec/decorators/event/participation_decorator_spec.rb deleted file mode 100644 index 62761ef36b..0000000000 --- a/spec/decorators/event/participation_decorator_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -require 'spec_helper' -describe Event::ParticipationDecorator, :draper_with_helpers do - include Rails.application.routes.url_helpers - - let(:course) do - event = Fabricate(:course, kind: event_kind) - event.dates.create!(start_at: quali_date, finish_at: quali_date) - event - end - - let(:participation) do - participation = Fabricate(:event_participation, event: course) - Fabricate(participant_role.name.to_sym, participation: participation) - participation - end - let(:quali_date) { Date.new(2012, 10, 20) } - let(:event_kind) { event_kinds(:slk) } - let(:decorator) { Event::ParticipationDecorator.new(participation) } - let(:participant_role) { Event::Role::Leader } - let(:group) { groups(:top_group) } - - { issue_action: [[nil, :active], [true, :inactive], [false, :active]], - revoke_action: [[nil, :active], [true, :active], [false, :inactive]] }.each do |action, values| - - context "##{action}" do - let(:node) { Capybara::Node::Simple.new(decorator.send(action, group)) } - let(:icon) { node.find('i') } - let(:link) { node.find('a') } - - values.each do |qualified, state| - it "is #{state} if participation.qualified is #{qualified.nil? ? 'nil' : qualified}" do - participation.update_column(:qualified, qualified) - case state - when :active then - expect(link).to be_present - expect(link[:title]).to match(/^Markiert Kurs/) - expect(icon).to be_present - expect(icon[:class]).to match(/disabled/) - when :inactive then - expect { expect(link).to }.to raise_error Capybara::ElementNotFound - expect(icon).to be_present - expect(icon[:class]).not_to match(/disabled/) - end - end - end - end - end -end diff --git a/spec/decorators/event_decorator_spec.rb b/spec/decorators/event_decorator_spec.rb index 948173df6f..0b31999cdf 100644 --- a/spec/decorators/event_decorator_spec.rb +++ b/spec/decorators/event_decorator_spec.rb @@ -14,8 +14,10 @@ let(:event) { events(:top_course) } subject { EventDecorator.new(event) } - its(:labeled_link) { should =~ /SLK Top/ } - its(:labeled_link) { should =~ %r{} } + its(:labeled_link) { is_expected.to match(/SLK TOP\-007 Top/) } + its(:labeled_link) { is_expected.to match(%r{}) } + + its(:label_with_group) { is_expected.to eq('Top: Top Course (TOP-007)')} context 'typeahead label' do subject { EventDecorator.new(event).as_typeahead[:label] } @@ -100,11 +102,11 @@ def parse(str) context 'qualification infos' do context 'with qualifications and prolongations' do its(:issued_qualifications_info_for_leaders) do - should == 'Vergibt die Qualifikation Super Lead (for Leaders) auf den 01.03.2012 (letztes Kursdatum).' + should == 'Vergibt die Qualifikation Super Lead (for Leaders) unmittelbar per 01.03.2012 (letztes Kursdatum).' end its(:issued_qualifications_info_for_participants) do - should == 'Vergibt die Qualifikation Super Lead und verlängert existierende Qualifikationen Group Lead auf den 01.03.2012 (letztes Kursdatum).' + should == 'Vergibt die Qualifikation Super Lead und verlängert existierende Qualifikationen Group Lead unmittelbar per 01.03.2012 (letztes Kursdatum).' end end @@ -112,11 +114,11 @@ def parse(str) before { event.kind = event_kinds(:glk) } its(:issued_qualifications_info_for_leaders) do - should == 'Vergibt die Qualifikation Group Lead (for Leaders) auf den 01.03.2012 (letztes Kursdatum).' + should == 'Vergibt die Qualifikation Group Lead (for Leaders) unmittelbar per 01.03.2012 (letztes Kursdatum).' end its(:issued_qualifications_info_for_participants) do - should == 'Vergibt die Qualifikation Group Lead auf den 01.03.2012 (letztes Kursdatum).' + should == 'Vergibt die Qualifikation Group Lead unmittelbar per 01.03.2012 (letztes Kursdatum).' end end @@ -124,11 +126,11 @@ def parse(str) before { event.kind = event_kinds(:fk) } its(:issued_qualifications_info_for_leaders) do - should == 'Verlängert existierende Qualifikationen Group Lead (for Leaders), Super Lead (for Leaders) auf den 01.03.2012 (letztes Kursdatum).' + should == 'Verlängert existierende Qualifikationen Group Lead (for Leaders), Super Lead (for Leaders) unmittelbar per 01.03.2012 (letztes Kursdatum).' end its(:issued_qualifications_info_for_participants) do - should == 'Verlängert existierende Qualifikationen Group Lead, Super Lead auf den 01.03.2012 (letztes Kursdatum).' + should == 'Verlängert existierende Qualifikationen Group Lead, Super Lead unmittelbar per 01.03.2012 (letztes Kursdatum).' end end diff --git a/spec/decorators/group_decorator_spec.rb b/spec/decorators/group_decorator_spec.rb index 615448d1d1..2e03f1102c 100644 --- a/spec/decorators/group_decorator_spec.rb +++ b/spec/decorators/group_decorator_spec.rb @@ -6,16 +6,16 @@ # https://github.com/hitobito/hitobito. require 'spec_helper' + describe GroupDecorator, :draper_with_helpers do include Rails.application.routes.url_helpers - let(:model) { double('model') } - let(:decorator) { GroupDecorator.new(model) } let(:context) { double('context') } - subject { decorator } + let(:model) { groups(:top_group) } + + subject { GroupDecorator.new(model) } describe 'possible roles' do - let(:model) { groups(:top_group) } its(:possible_roles) do should eq [Group::TopGroup::Leader, Group::TopGroup::LocalGuide, @@ -27,13 +27,18 @@ end describe 'selecting attributes' do + + class DummyGroup < Group + self.used_attributes += [:foo, :bar] + end + + let(:model) { DummyGroup.new } + before do allow(subject).to receive_messages(h: context) - allow(model).to receive_message_chain(:class, :attr_used?) { |val| val } end it '#used_attributes selects via .attr_used?' do - expect(model.class).to receive(:attr_used?).twice expect(subject.used_attributes(:foo, :bar)).to eq %w(foo bar) end diff --git a/spec/decorators/person_decorator_spec.rb b/spec/decorators/person_decorator_spec.rb index 85212ee537..b6ff3d3073 100644 --- a/spec/decorators/person_decorator_spec.rb +++ b/spec/decorators/person_decorator_spec.rb @@ -74,9 +74,23 @@ it 'upcoming_events returns events that are active' do course.dates.build(start_at: 2.days.from_now, finish_at: 5.days.from_now) course.save - participation = Fabricate(:event_participation, event: course, person: person, active: true) + Fabricate(:event_participation, event: course, person: person, active: true) expect(subject.upcoming_events).to eq [course] end end + + context 'layer group' do + let(:label) { PersonDecorator.new(person).layer_group_label } + + it 'creates link for group layer' do + expect(label).to match /Top/ + expect(label).to match /#{groups(:top_layer).id}/ + end + + it 'empty string if no group layer' do + person.update!(primary_group: nil) + expect(label).to be nil + end + end end diff --git a/spec/domain/event/participant_assigner_spec.rb b/spec/domain/event/participant_assigner_spec.rb index 8effa751fd..556e684362 100644 --- a/spec/domain/event/participant_assigner_spec.rb +++ b/spec/domain/event/participant_assigner_spec.rb @@ -126,6 +126,25 @@ assigner2.add_participant expect(assigner1).not_to be_createable end + + context 'waiting list duplicate' do + before do + participation.application.update!(waiting_list: true, priority_2: nil) + + p = Fabricate(:event_participation, event: event2, person: participation.person, + active: false) + p.create_application!(priority_1: event2) + Fabricate(course.participant_types.first.name.to_sym, participation: p) + p.save! + p.reload + p + end + + it 'is false for assigner2 when person on waiting list already applied' do + # regression test for: https://github.com/hitobito/hitobito/issues/162 + expect(assigner2).not_to be_createable + end + end end describe 'event#applicant_count' do diff --git a/spec/domain/event/precondition_checker_spec.rb b/spec/domain/event/precondition_checker_spec.rb index 67a22bf3c1..3459b60285 100644 --- a/spec/domain/event/precondition_checker_spec.rb +++ b/spec/domain/event/precondition_checker_spec.rb @@ -1,11 +1,12 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. require 'spec_helper' + describe Event::PreconditionChecker do let(:course) { events(:top_course) } let(:person) { people(:top_leader) } @@ -17,6 +18,7 @@ def preconditions end subject { Event::PreconditionChecker.new(course, person) } + before do course.kind.event_kind_qualification_kinds. where(category: 'precondition', role: 'participant'). @@ -30,7 +32,7 @@ def preconditions describe 'minimum age person' do before { course.kind.minimum_age = 16 } - let(:too_young_error) { 'Altersgrenze von 16 unterschritten.' } + let(:too_young_error) { 'Altersgrenze von 16 Jahren ist unterschritten.' } context 'has no birthday' do its(:valid?) { should be_falsey } @@ -68,7 +70,7 @@ def preconditions context "person without 'super lead'" do its(:valid?) { should be_falsey } - its('errors_text.last') { should =~ /Super Lead/ } + its('errors_text.last') { should =~ /Qualifikationen fehlen: Super Lead/ } end context "person with expired 'super lead'" do @@ -113,13 +115,58 @@ def preconditions category: 'precondition', role: 'participant') end - its('errors_text.last') { should =~ /Qualifikationen fehlen: Super Lead, Group Lead$/ } + + its('errors_text.last') { should =~ /Qualifikationen fehlen: Super Lead, Group Lead/ } context 'missing only one' do before { qualifications << Fabricate(:qualification, qualification_kind: sl, start_at: valid_date) } its(:valid?) { should be_falsey } - its('errors_text.last') { should =~ /Qualifikationen fehlen: Group Lead$/ } + its('errors_text.last') { should =~ /Qualifikationen fehlen: Group Lead/ } + end + + context 'with both present' do + before do + qualifications << Fabricate(:qualification, qualification_kind: gl, start_at: course_start_at - gl.validity.years) + qualifications << Fabricate(:qualification, qualification_kind: sl, start_at: valid_date) + end + + its(:valid?) { should be_truthy } + end + + context 'in multiple groups' do + let(:ql) { qualification_kinds(:ql) } + + before do + course.kind.event_kind_qualification_kinds.create!(qualification_kind_id: ql.id, + category: 'precondition', + role: 'participant', + grouping: 1) + end + + its('errors_text.last') { should =~ /Erforderliche Qualifikationen fehlen/ } + + context 'missing only one in a grouping' do + before { qualifications << Fabricate(:qualification, qualification_kind: sl, start_at: valid_date) } + + its(:valid?) { should be_falsey } + its('errors_text.last') { should =~ /Erforderliche Qualifikationen fehlen/ } + end + + context 'with both in grouping nil' do + before do + qualifications << Fabricate(:qualification, qualification_kind: gl, start_at: course_start_at - gl.validity.years) + qualifications << Fabricate(:qualification, qualification_kind: sl, start_at: valid_date) + end + + its(:valid?) { should be_truthy } + end + + context 'with the single one in grouping 1' do + before { qualifications << Fabricate(:qualification, qualification_kind: ql, start_at: valid_date) } + + its(:valid?) { should be_truthy } + end end end diff --git a/spec/domain/export/csv/events/list_spec.rb b/spec/domain/export/csv/events/list_spec.rb index 9d6b60022a..10a5cea57d 100644 --- a/spec/domain/export/csv/events/list_spec.rb +++ b/spec/domain/export/csv/events/list_spec.rb @@ -1,162 +1,124 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. require 'spec_helper' -describe Export::Csv::Events::List do - - let(:courses) { double('courses', map: [], first: nil) } - let(:list) { Export::Csv::Events::List.new(courses) } - subject { list } - - its(:contactable_keys) { should eq [:name, :address, :zip_code, :town, :email, :phone_numbers] } - - context 'used labels' do - subject { list } - - its(:attributes) do - should == [:name, :group_names, :number, :kind, :description, :state, :location, - :date_0_label, :date_0_location, :date_0_duration, - :date_1_label, :date_1_location, :date_1_duration, - :date_2_label, :date_2_location, :date_2_duration, - :contact_name, :contact_address, :contact_zip_code, :contact_town, - :contact_email, :contact_phone_numbers, - :leader_name, :leader_address, :leader_zip_code, :leader_town, - :leader_email, :leader_phone_numbers, - :motto, :cost, :application_opening_at, :application_closing_at, - :maximum_participants, :external_applications, :priorization, - :teamer_count, :participant_count, :applicant_count] - end +require 'csv' - its(:labels) do - should == ['Name', 'Organisatoren', 'Kursnummer', 'Kursart', 'Beschreibung', 'Status', 'Ort / Adresse', - 'Datum 1 Bezeichnung', 'Datum 1 Ort', 'Datum 1 Zeitraum', - 'Datum 2 Bezeichnung', 'Datum 2 Ort', 'Datum 2 Zeitraum', - 'Datum 3 Bezeichnung', 'Datum 3 Ort', 'Datum 3 Zeitraum', - 'Kontaktperson Name', 'Kontaktperson Adresse', 'Kontaktperson PLZ', - 'Kontaktperson Ort', 'Kontaktperson Haupt-E-Mail', 'Kontaktperson Telefonnummern', - 'Hauptleitung Name', 'Hauptleitung Adresse', 'Hauptleitung PLZ', 'Hauptleitung Ort', - 'Hauptleitung Haupt-E-Mail', 'Hauptleitung Telefonnummern', - 'Motto', 'Kosten', 'Anmeldebeginn', 'Anmeldeschluss', 'Maximale Teilnehmerzahl', - 'Externe Anmeldungen', 'Priorisierung', 'Anzahl Leitungsteam', - 'Anzahl Teilnehmende', 'Anzahl Anmeldungen'] - end - end +describe Export::Tabular::Events::List do + let(:course) { Fabricate(:course, groups: [groups(:top_group)], location: 'somewhere', state: 'somestate') } + let(:courses) { [course] } + let(:list) { Export::Tabular::Events::List.new(courses) } + let(:csv) { Export::Csv::Generator.new(list).call.split("\n") } - context 'to_csv' do - let(:courses) { [course] } - let(:course) { Fabricate(:course, groups: [groups(:top_group)], location: 'somewhere', state: 'somestate') } - let(:csv) { Export::Csv::Generator.new(list).csv.split("\n") } + context 'headers' do + subject { csv.first } + it { is_expected.to match(/^Name;Organisatoren;Kursnummer;Kursart;.*Anzahl Anmeldungen$/) } + end - context 'headers' do - subject { csv.first } - it { is_expected.to match(/^Name;Organisatoren;Kursnummer;Kursart;.*Anzahl Anmeldungen$/) } - end + context 'first row' do + subject { csv.second.split(';') } + its([1]) { should eq 'TopGroup' } - context 'first row' do - subject { csv.second.split(';') } - its([1]) { should eq 'TopGroup' } + its([5]) { should eq 'somestate' } - its([5]) { should eq 'somestate' } + its([6]) { should eq 'somewhere' } - its([6]) { should eq 'somewhere' } + context 'state' do + # This tests the case where Event.possible_states is empty, + # the case with predefined states is tested in the jubla wagon. - context 'state' do - # This tests the case where Event.possible_states is empty, - # the case with predefined states is tested in the jubla wagon. + context 'present' do + its([5]) { is_expected.to eq 'somestate' } + end - context 'present' do - its([5]) { is_expected.to eq 'somestate' } + context 'empty' do + let(:course) do + Fabricate(:course, groups: [groups(:top_group)], location: 'somewhere') end + let(:list) { Export::Tabular::Events::List.new([course]) } + let(:csv) { Export::Csv::Generator.new(list).call.split("\n") } + subject { csv.second.split(';') } - context 'empty' do - let(:course) do - Fabricate(:course, groups: [groups(:top_group)], location: 'somewhere') - end - let(:list) { Export::Csv::Events::List.new([course]) } - let(:csv) { Export::Csv::Generator.new(list).csv.split("\n") } - subject { csv.second.split(';') } - - its([5]) { is_expected.to eq '' } - end + its([5]) { is_expected.to eq '' } end + end - context 'dates' do - let(:start_at) { Date.parse 'Sun, 09 Jun 2013' } - let(:finish_at) { Date.parse 'Wed, 12 Jun 2013' } - let(:date) { Fabricate(:event_date, event: course, start_at: start_at, finish_at: finish_at, location: 'somewhere') } + context 'dates' do + let(:start_at) { Date.parse 'Sun, 09 Jun 2013' } + let(:finish_at) { Date.parse 'Wed, 12 Jun 2013' } + let(:date) { Fabricate(:event_date, event: course, start_at: start_at, finish_at: finish_at, location: 'somewhere') } - before { allow(course).to receive(:dates).and_return([date]) } - its([7]) { is_expected.to eq 'Hauptanlass' } - its([8]) { is_expected.to eq 'somewhere' } - its([9]) { is_expected.to eq '09.06.2013 - 12.06.2013' } - its([10]) { is_expected.to eq '' } - end + before { allow(course).to receive(:dates).and_return([date]) } + its([7]) { is_expected.to eq 'Hauptanlass' } + its([8]) { is_expected.to eq 'somewhere' } + its([9]) { is_expected.to eq '09.06.2013 - 12.06.2013' } + its([10]) { is_expected.to eq '' } + end - context 'contact' do - let(:person) { Fabricate(:person_with_address_and_phone) } - before { course.contact = person } - its([16]) { is_expected.to eq person.to_s } - its([21]) { is_expected.to eq person.phone_numbers.first.to_s } - its([21]) { is_expected.to_not eq '' } - end + context 'contact' do + let(:person) { Fabricate(:person_with_address_and_phone) } + before { course.contact = person } + its([16]) { is_expected.to eq person.to_s } + its([21]) { is_expected.to eq person.phone_numbers.first.to_s } + its([21]) { is_expected.to_not eq '' } + end - context 'leader' do - let(:participation) { Fabricate(:event_participation, event: course) } - let!(:leader) { Fabricate(Event::Role::Leader.name.to_sym, participation: participation).person } - its([22]) { is_expected.to_not eq '' } - its([22]) { is_expected.to eq leader.to_s } - end + context 'leader' do + let(:participation) { Fabricate(:event_participation, event: course) } + let!(:leader) { Fabricate(Event::Role::Leader.name.to_sym, participation: participation).person } + its([22]) { is_expected.to_not eq '' } + its([22]) { is_expected.to eq leader.to_s } end + end - context 'additional course labels' do - let(:courses) { [course1, course2] } - let(:course1) do - Fabricate(:course, groups: [groups(:top_group)], motto: 'All for one', cost: 1000, - application_opening_at: '01.01.2000', application_closing_at: '01.02.2000', - maximum_participants: 10, external_applications: false, priorization: false) - end - let(:course2) { Fabricate(:course, groups: [groups(:top_group)]) } - - before do - Fabricate(:event_participation, event: course1, active: true, - roles: [Fabricate(:event_role, type: Event::Role::Leader.sti_name)]) - Fabricate(:event_participation, event: course1, active: true, - roles: [Fabricate(:event_role, type: Event::Course::Role::Participant.sti_name)]) - Fabricate(:event_participation, event: course1, active: false, - roles: [Fabricate(:event_role, type: Event::Course::Role::Participant.sti_name)]) - course1.refresh_participant_counts! - course2.refresh_participant_counts! - end + context 'additional course labels' do + let(:courses) { [course1, course2] } + let(:course1) do + Fabricate(:course, groups: [groups(:top_group)], motto: 'All for one', cost: 1000, + application_opening_at: '01.01.2000', application_closing_at: '01.02.2000', + maximum_participants: 10, external_applications: false, priorization: false) + end + let(:course2) { Fabricate(:course, groups: [groups(:top_group)]) } + + before do + Fabricate(:event_participation, event: course1, active: true, + roles: [Fabricate(:event_role, type: Event::Role::Leader.sti_name)]) + Fabricate(:event_participation, event: course1, active: true, + roles: [Fabricate(:event_role, type: Event::Course::Role::Participant.sti_name)]) + Fabricate(:event_participation, event: course1, active: false, + roles: [Fabricate(:event_role, type: Event::Course::Role::Participant.sti_name)]) + course1.refresh_participant_counts! + course2.refresh_participant_counts! + end - context 'first row' do - let(:row) { csv[0].split(';') } - it 'should contain contain the additional course fields' do - expect(row[28..-1]).to eq ['Motto', 'Kosten', 'Anmeldebeginn', 'Anmeldeschluss', - 'Maximale Teilnehmerzahl', 'Externe Anmeldungen', - 'Priorisierung', 'Anzahl Leitungsteam', 'Anzahl Teilnehmende', - 'Anzahl Anmeldungen'] - end + context 'first row' do + let(:row) { csv[0].split(';') } + it 'should contain contain the additional course fields' do + expect(row[28..-1]).to eq ['Motto', 'Kosten', 'Anmeldebeginn', 'Anmeldeschluss', + 'Maximale Teilnehmerzahl', 'Externe Anmeldungen', + 'Priorisierung', 'Anzahl Leitungsteam', 'Anzahl Teilnehmende', + 'Anzahl Anmeldungen'] end + end - context 'second row' do - let(:row) { csv[1].split(';') } - it 'should contain contain the additional course and record fields' do - expect(row[28..-1]).to eq ['All for one', '1000', '2000-01-01', '2000-02-01', '10', - 'nein', 'nein', '1', '1', '2'] - end + context 'second row' do + let(:row) { csv[1].split(';') } + it 'should contain contain the additional course and record fields' do + expect(row[28..-1]).to eq ['All for one', '1000', '01.01.2000', '01.02.2000', '10', + 'nein', 'nein', '1', '1', '2'] end + end - context 'third row (course without record)' do - let(:row) { csv[2].split(';') } - it 'should contain the additional course fields' do - expect(row[28..-1]).to eq ['', '', '', '', '', 'nein', 'ja', '0', '0', '0'] - end + context 'third row (course without record)' do + let(:row) { csv[2].split(';') } + it 'should contain the additional course fields' do + expect(row[28..-1]).to eq ['', '', '', '', '', 'nein', 'ja', '0', '0', '0'] end end end @@ -164,7 +126,9 @@ context 'multiple courses' do let(:course) { Fabricate(:course) } let(:courses) { [course, course, course, course] } - subject { Export::Csv::Generator.new(list).csv.split("\n") } + + subject { Export::Csv::Generator.new(list).call.split("\n") } + it 'has 5 rows' do expect(subject.size).to eq(5) end diff --git a/spec/domain/export/csv/groups/list_spec.rb b/spec/domain/export/csv/groups/list_spec.rb index ee108ee7fa..5672078edf 100644 --- a/spec/domain/export/csv/groups/list_spec.rb +++ b/spec/domain/export/csv/groups/list_spec.rb @@ -1,12 +1,19 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + require 'spec_helper' require 'csv' -describe Export::Csv::Groups::List do +describe Export::Tabular::Groups::List do let(:group) { groups(:bottom_layer_one) } let(:list) { group.self_and_descendants.without_deleted.includes(:contact) } - let(:data) { Export::Csv::Groups::List.export(list) } + let(:data) { Export::Tabular::Groups::List.csv(list) } let(:csv) { CSV.parse(data, headers: true, col_sep: Settings.csv.separator) } subject { csv } diff --git a/spec/domain/export/csv/invoices/list_spec.rb b/spec/domain/export/csv/invoices/list_spec.rb new file mode 100644 index 0000000000..47ec08cf07 --- /dev/null +++ b/spec/domain/export/csv/invoices/list_spec.rb @@ -0,0 +1,71 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +require 'spec_helper' +require 'csv' + +describe Export::Tabular::Invoices::List do + + let(:group) { groups(:bottom_layer_one) } + + let(:list) { group.invoices } + let(:data) { Export::Tabular::Invoices::List.csv(list) } + let(:csv) { CSV.parse(data, headers: true, col_sep: Settings.csv.separator) } + + subject { csv } + + its(:headers) do + should == [ + 'Titel', 'Nummer', 'Status', 'Referenz Nummer', 'Beschreibung', 'Empfänger E-Mail', + 'Empfänger Adresse', 'Verschickt am', 'Fällig am', 'Betrag', + 'MWSt.', 'Total inkl. MWSt.', 'Total bezahlt' + ] + end + + it 'has 2 items' do + expect(subject.size).to eq(2) + end + + context 'first row' do + + subject { csv[0] } + + its(['Titel']) { should == 'Invoice' } + its(['Nummer']) { should == invoices(:invoice).sequence_number } + its(['Status']) { should == 'Entwurf' } + its(['Referenz Nummer']) { should == invoices(:invoice).esr_number } + its(['Betrag']) { should == '5.00 CHF' } + its(['MWSt.']) { should == '0.00 CHF' } + its(['Total inkl. MWSt.']) { should == '2.00 CHF' } + its(['Total bezahlt']) { should == '0.00 CHF' } + its(['Empfänger E-Mail']) { should == nil } + its(['Beschreibung']) { should == nil } + its(['Empfänger Adresse']) { should == nil } + its(['Verschickt am']) { should == nil } + its(['Fällig am']) { should == nil } + end + + context 'second row' do + + subject { csv[1] } + let(:invoice ) { invoices(:sent) } + + its(['Titel']) { should == 'Sent' } + its(['Nummer']) { should == invoice.sequence_number } + its(['Status']) { should == 'Gestellt' } + its(['Referenz Nummer']) { should == invoice.esr_number } + its(['Verschickt am']) { should == I18n.l(invoice.sent_at) } + its(['Fällig am']) { should == I18n.l(invoice.due_at) } + its(['Betrag']) { should == '0.00 CHF' } + its(['MWSt.']) { should == '0.00 CHF' } + its(['Total inkl. MWSt.']) { should == '2.00 CHF' } + its(['Total bezahlt']) { should == '0.00 CHF' } + its(['Empfänger E-Mail']) { should == nil } + its(['Beschreibung']) { should == nil } + its(['Empfänger Adresse']) { should == nil } + end +end diff --git a/spec/domain/export/csv/people/participations_address_spec.rb b/spec/domain/export/csv/people/participations_address_spec.rb index 2e4fac9d4f..a01dface1e 100644 --- a/spec/domain/export/csv/people/participations_address_spec.rb +++ b/spec/domain/export/csv/people/participations_address_spec.rb @@ -1,6 +1,6 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. @@ -8,12 +8,12 @@ require 'spec_helper' require 'csv' -describe Export::Csv::People::ParticipationsAddress do +describe Export::Tabular::People::ParticipationsAddress do let(:person) { people(:top_leader) } let(:participation) { Fabricate(:event_participation, person: person, event: events(:top_course)) } let(:list) { [participation] } - let(:people_list) { Export::Csv::People::ParticipationsAddress.new(list) } + let(:people_list) { Export::Tabular::People::ParticipationsAddress.new(list) } subject { people_list.attribute_labels } @@ -25,10 +25,10 @@ context 'integration' do let(:simple_headers) do ['Vorname', 'Nachname', 'Übername', 'Firmenname', 'Firma', 'Haupt-E-Mail', - 'Adresse', 'PLZ', 'Ort', 'Land', 'Geschlecht', 'Geburtstag', 'Rollen'] + 'Adresse', 'PLZ', 'Ort', 'Land', 'Geschlecht', 'Geburtstag', 'Hauptebene', 'Rollen'] end - let(:data) { Export::Csv::People::ParticipationsAddress.export(list) } + let(:data) { Export::Tabular::People::ParticipationsAddress.export(:csv, list) } let(:csv) { CSV.parse(data, headers: true, col_sep: Settings.csv.separator) } subject { csv } diff --git a/spec/domain/export/csv/people/participations_full_spec.rb b/spec/domain/export/csv/people/participations_full_spec.rb index aed1205918..776de9cc09 100644 --- a/spec/domain/export/csv/people/participations_full_spec.rb +++ b/spec/domain/export/csv/people/participations_full_spec.rb @@ -1,6 +1,6 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. @@ -8,13 +8,12 @@ require 'spec_helper' require 'csv' - -describe Export::Csv::People::ParticipationsFull do +describe Export::Tabular::People::ParticipationsFull do let(:person) { people(:top_leader) } let(:participation) { Fabricate(:event_participation, person: person, event: events(:top_course)) } let(:list) { [participation] } - let(:people_list) { Export::Csv::People::ParticipationsFull.new(list) } + let(:people_list) { Export::Tabular::People::ParticipationsFull.new(list) } subject { people_list.attribute_labels } @@ -28,23 +27,29 @@ context 'questions' do let(:participation) { Fabricate(:event_participation, person: person, event: events(:top_course)) } - let(:question) { events(:top_course).questions.first } - before { participation.init_answers } - it 'has keys and values' do + it 'has keys and values of application questions' do + participation.init_answers expect(subject[:"question_#{event_questions(:top_ov).id}"]).to eq 'GA oder Halbtax?' expect(subject.keys.select { |key| key =~ /question/ }.size).to eq(3) end + + it 'has keys and values of admin questions' do + irgendwas = events(:top_course).questions.create!(question: 'Irgendwas', admin: true) + participation.init_answers + expect(subject[:"question_#{irgendwas.id}"]).to eq 'Irgendwas' + expect(subject.keys.select { |key| key =~ /question/ }.size).to eq(4) + end end context 'integration' do - let(:data) { Export::Csv::People::ParticipationsFull.export(list) } + let(:data) { Export::Tabular::People::ParticipationsFull.export(:csv, list) } let(:csv) { CSV.parse(data, headers: true, col_sep: Settings.csv.separator) } let(:full_headers) do ['Vorname', 'Nachname', 'Firmenname', 'Übername', 'Firma', 'Haupt-E-Mail', 'Adresse', 'PLZ', 'Ort', 'Land', 'Geschlecht', 'Geburtstag', - 'Zusätzliche Angaben', 'Rollen', 'Anmeldedatum'] + 'Zusätzliche Angaben', 'Rollen', 'Anmeldedatum', 'Hauptebene'] end subject { csv } diff --git a/spec/domain/export/csv/people/people_address_spec.rb b/spec/domain/export/csv/people/people_address_spec.rb index 0b730d421e..7e5b765528 100644 --- a/spec/domain/export/csv/people/people_address_spec.rb +++ b/spec/domain/export/csv/people/people_address_spec.rb @@ -1,6 +1,6 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. @@ -8,101 +8,56 @@ require 'spec_helper' require 'csv' -describe Export::Csv::People::PeopleAddress do +describe Export::Tabular::People::PeopleAddress do let(:person) { people(:top_leader) } let(:list) { [person] } - let(:people_list) { Export::Csv::People::PeopleAddress.new(list) } + let(:people_list) { Export::Tabular::People::PeopleAddress.new(list) } subject { people_list } - its(:attributes) do - should == [:first_name, :last_name, :nickname, :company_name, :company, :email, :address, - :zip_code, :town, :country, :gender, :birthday, :roles] - end - - context 'standard attributes' do + let(:data) { Export::Tabular::People::PeopleAddress.export(:csv, list) } + let(:csv) { CSV.parse(data, headers: true, col_sep: Settings.csv.separator) } - context '#attribute_labels' do - subject { people_list.attribute_labels } - its([:id]) { should be_blank } - its([:roles]) { should eq 'Rollen' } - its([:first_name]) { should eq 'Vorname' } + context 'headers' do + let(:simple_headers) do + ['Vorname', 'Nachname', 'Übername', 'Firmenname', 'Firma', 'Haupt-E-Mail', + 'Adresse', 'PLZ', 'Ort', 'Land', 'Geschlecht', 'Geburtstag', 'Hauptebene', + 'Rollen'] end - context 'key list' do - subject { people_list.attribute_labels.keys.join(' ') } - it { is_expected.not_to match(/phone/) } - it { is_expected.not_to match(/social_account/) } - end - end + subject { csv } - context 'phone_numbers' do - before { person.phone_numbers << PhoneNumber.new(label: 'Privat', number: 321) } + its(:headers) { should == simple_headers } + end - subject { people_list.attribute_labels } + context 'first row' do - its([:phone_number_privat]) { should eq 'Telefonnummer Privat' } - its([:phone_number_mobil]) { should be_nil } + subject { csv[0] } - context 'different labels' do - let(:other) { people(:bottom_member) } - let(:list) { [person, other] } + its(['Vorname']) { should eq person.first_name } + its(['Nachname']) { should eq person.last_name } + its(['Haupt-E-Mail']) { should eq person.email } + its(['Ort']) { should eq person.town } + its(['Geschlecht']) { should eq 'unbekannt' } + its(['Hauptebene']) { should eq 'Top' } + context 'roles and phone number' do before do - other.phone_numbers << PhoneNumber.new(label: 'Foobar', number: 321) - person.phone_numbers << PhoneNumber.new(label: 'Privat', number: 321) + Fabricate(Group::BottomGroup::Member.name.to_s, group: groups(:bottom_group_one_one), person: person) + person.phone_numbers.create!(label: 'vater', number: 123) + person.additional_emails.create!(label: 'Vater', email: 'vater@example.com') + person.additional_emails.create!(label: 'Mutter', email: 'mutter@example.com', public: false) end - its([:phone_number_privat]) { should eq 'Telefonnummer Privat' } - its([:phone_number_foobar]) { should eq 'Telefonnummer Foobar' } - end + its(['Telefonnummer Vater']) { should eq '123' } + its(['Weitere E-Mail Vater']) { should eq 'vater@example.com' } + its(['Weitere E-Mail Mutter']) { should be_nil } - context 'blank label is not exported' do - before { person.phone_numbers << PhoneNumber.new(label: '', number: 321) } - its(:keys) { should_not include :phone_number_ } - end - end - - context 'integration' do - let(:simple_headers) do - ['Vorname', 'Nachname', 'Übername', 'Firmenname', 'Firma', 'Haupt-E-Mail', - 'Adresse', 'PLZ', 'Ort', 'Land', 'Geschlecht', 'Geburtstag', 'Rollen'] - end - let(:data) { Export::Csv::People::PeopleAddress.export(list) } - let(:csv) { CSV.parse(data, headers: true, col_sep: Settings.csv.separator) } - - subject { csv } - - its(:headers) { should == simple_headers } - - context 'first row' do - - subject { csv[0] } - - its(['Vorname']) { should eq person.first_name } - its(['Nachname']) { should eq person.last_name } - its(['Haupt-E-Mail']) { should eq person.email } - its(['Ort']) { should eq person.town } - its(['Geschlecht']) { should eq 'unbekannt' } - - context 'roles and phone number' do - before do - Fabricate(Group::BottomGroup::Member.name.to_s, group: groups(:bottom_group_one_one), person: person) - person.phone_numbers.create!(label: 'vater', number: 123) - person.additional_emails.create!(label: 'Vater', email: 'vater@example.com') - person.additional_emails.create!(label: 'Mutter', email: 'mutter@example.com', public: false) - end - - its(['Telefonnummer Vater']) { should eq '123' } - its(['Weitere E-Mail Vater']) { should eq 'vater@example.com' } - its(['Weitere E-Mail Mutter']) { should be_nil } - - it 'roles should be complete' do - expect(subject['Rollen'].split(', ')).to match_array(['Member Bottom One / Group 11', 'Leader Top / TopGroup']) - end - end + it 'roles should be complete' do + expect(subject['Rollen'].split(', ')).to match_array(['Member Bottom One / Group 11', 'Leader Top / TopGroup']) end + end end - end + diff --git a/spec/domain/export/csv/people/people_full_spec.rb b/spec/domain/export/csv/people/people_full_spec.rb index 40ce57123a..726860055e 100644 --- a/spec/domain/export/csv/people/people_full_spec.rb +++ b/spec/domain/export/csv/people/people_full_spec.rb @@ -1,6 +1,6 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. @@ -8,112 +8,74 @@ require 'spec_helper' require 'csv' -describe Export::Csv::People::PeopleFull do +describe Export::Tabular::People::PeopleFull do + + let(:person) { people(:top_leader) } + let(:list) { [person] } + let(:data) { Export::Tabular::People::PeopleFull.export(:csv, list) } + let(:csv) { CSV.parse(data, headers: true, col_sep: Settings.csv.separator) } before do - PeopleRelation.kind_opposites['parent'] = 'child' - PeopleRelation.kind_opposites['child'] = 'parent' + person.update_attribute(:gender, 'm') + person.social_accounts << SocialAccount.new(label: 'skype', name: 'foobar') + person.phone_numbers << PhoneNumber.new(label: 'vater', number: 123, public: false) + person.additional_emails << AdditionalEmail.new(label: 'vater', email: 'vater@example.com', public: false) + person.relations_to_tails << PeopleRelation.new(tail_id: people(:bottom_member).id, kind: 'parent') + person.save + I18n.locale = lang end after do + I18n.locale = I18n.default_locale PeopleRelation.kind_opposites.clear end - let(:person) { people(:top_leader) } - let(:list) { [person] } - let(:people_list) { Export::Csv::People::PeopleFull.new(list) } - - subject { people_list } - - its(:attributes) do should eq [:first_name, :last_name, :company_name, :nickname, :company, - :email, :address, :zip_code, :town, :country, :gender, :birthday, - :additional_information, :roles] end - - context '#attribute_labels' do - subject { people_list.attribute_labels } - - its([:roles]) { should eq 'Rollen' } - its([:social_account_website]) { should be_blank } - - its([:company]) { should eq 'Firma' } - its([:company_name]) { should eq 'Firmenname' } - - context 'social accounts' do - before { person.social_accounts << SocialAccount.new(label: 'Webseite', name: 'foo.bar') } - its([:social_account_webseite]) { should eq 'Social Media Adresse Webseite' } - end + context 'german' do + let(:lang) { :de } - context 'people relations' do - before { person.relations_to_tails << PeopleRelation.new(head_id: person.id, tail_id: people(:bottom_member).id, kind: 'parent') } - its([:people_relation_parent]) { should eq 'Elternteil' } + it 'has correct headers' do + expect(csv.headers).to eq([ + 'Vorname', 'Nachname', 'Firmenname', 'Übername', 'Firma', 'Haupt-E-Mail', + 'Adresse', 'PLZ', 'Ort', 'Land', 'Geschlecht', 'Geburtstag', + 'Zusätzliche Angaben', 'Hauptebene', 'Rollen', 'Weitere E-Mail Vater', 'Telefonnummer Vater', + 'Social Media Adresse Skype', 'Elternteil']) end - end - - context 'integration' do - let(:data) { Export::Csv::People::PeopleFull.export(list) } - let(:csv) { CSV.parse(data, headers: true, col_sep: Settings.csv.separator) } + context 'first row' do + subject { csv[0] } - before do - person.update_attribute(:gender, 'm') - person.social_accounts << SocialAccount.new(label: 'skype', name: 'foobar') - person.phone_numbers << PhoneNumber.new(label: 'vater', number: 123, public: false) - person.additional_emails << AdditionalEmail.new(label: 'vater', email: 'vater@example.com', public: false) - person.relations_to_tails << PeopleRelation.new(tail_id: people(:bottom_member).id, kind: 'parent') - person.save - I18n.locale = lang + its(['Rollen']) { should eq 'Leader Top / TopGroup' } + its(['Telefonnummer Vater']) { should eq '123' } + its(['Weitere E-Mail Vater']) { should eq 'vater@example.com' } + its(['Social Media Adresse Skype']) { should eq 'foobar' } + its(['Elternteil']) { should eq 'Bottom Member' } + its(['Geschlecht']) { should eq 'männlich' } + its(['Hauptebene']) { should eq 'Top' } end + end - after do - I18n.locale = I18n.default_locale - end - - context 'german' do - let(:lang) { :de } - - it 'has correct headers' do - expect(csv.headers).to eq([ - 'Vorname', 'Nachname', 'Firmenname', 'Übername', 'Firma', 'Haupt-E-Mail', - 'Adresse', 'PLZ', 'Ort', 'Land', 'Geschlecht', 'Geburtstag', - 'Zusätzliche Angaben', 'Rollen', 'Weitere E-Mail Vater', 'Telefonnummer Vater', - 'Social Media Adresse Skype', 'Elternteil']) - end - - context 'first row' do - subject { csv[0] } - - its(['Rollen']) { should eq 'Leader Top / TopGroup' } - its(['Telefonnummer Vater']) { should eq '123' } - its(['Weitere E-Mail Vater']) { should eq 'vater@example.com' } - its(['Social Media Adresse Skype']) { should eq 'foobar' } - its(['Elternteil']) { should eq 'Bottom Member' } - its(['Geschlecht']) { should eq 'männlich' } - end + context 'french' do + let(:lang) { :fr } + + it 'has correct headers' do + expect(csv.headers).to eq( + ["Prénom", "Nom", "Nom de l'entreprise", "Surnom", "Entreprise", + "Adresse e-mail principale", "Adresse", "Code postal", "Lieu", "Pays", "Sexe", + "Anniversaire", "Données supplémentaires", "Niveau", "Rôles", + "Adresse e-mail supplémentaire Père", "Numéro de téléphone Père", + "Adresse d'un média social Skype", "Parent"] + ) end - context 'french' do - let(:lang) { :fr } - - it 'has correct headers' do - expect(csv.headers).to eq( - ["Prénom", "Nom", "Nom de l'entreprise", "Surnom", "Entreprise", - "Adresse e-mail principale", "Adresse", "Code postal", "Lieu", "Pays", "Sexe", - "Anniversaire", "Données supplémentaires", "Rôles", - "Adresse e-mail supplémentaire Père", "Numéro de téléphone Père", - "Adresse d'un média social Skype", "Parent"] - ) - end - - context 'first row' do - subject { csv[0] } + context 'first row' do + subject { csv[0] } - its(['Rôles']) { should eq 'Leadre Top / TopGroup' } - its(['Numéro de téléphone Père']) { should eq '123' } - its(['Adresse e-mail supplémentaire Père']) { should eq 'vater@example.com' } - its(["Adresse d'un média social Skype"]) { should eq 'foobar' } - its(['Parent']) { should eq 'Bottom Member' } - its(['Sexe']) { should eq 'Masculin' } - end + its(['Rôles']) { should eq 'Leadre Top / TopGroup' } + its(['Numéro de téléphone Père']) { should eq '123' } + its(['Adresse e-mail supplémentaire Père']) { should eq 'vater@example.com' } + its(["Adresse d'un média social Skype"]) { should eq 'foobar' } + its(['Parent']) { should eq 'Bottom Member' } + its(['Sexe']) { should eq 'Masculin' } end end end diff --git a/spec/domain/export/ics/events_spec.rb b/spec/domain/export/ics/events_spec.rb new file mode 100644 index 0000000000..b7a4fa868b --- /dev/null +++ b/spec/domain/export/ics/events_spec.rb @@ -0,0 +1,38 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +require 'spec_helper' + +describe Export::Ics::Events do + let(:event) { events(:top_course) } + let(:event_date) { event.dates.first } + let(:export) { described_class.new } + + describe '#generate_ical_events' do + subject(:ical_events) { export.generate_ical_events(event) } + + it 'contains the event dates' do + is_expected.to all(be_a(Icalendar::Event)) + expect(ical_events.count).to eq(event.dates.count) + expect(ical_events.first).to have_attributes( + dtstart: event_date.start_at , + dtend: event_date.finish_at, + summary: "#{event.name}: #{event_date.label}" + ) + end + end + + describe '#generate' do + subject(:ical_events) { export.generate([event, event]) } + + it do + is_expected.to include('BEGIN:VCALENDAR') + is_expected.to include('VERSION:2.0') + is_expected.to include('BEGIN:VCALENDAR') + end + end +end diff --git a/spec/domain/export/pdf/labels_spec.rb b/spec/domain/export/pdf/labels_spec.rb new file mode 100644 index 0000000000..740178a524 --- /dev/null +++ b/spec/domain/export/pdf/labels_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe Export::Pdf::Labels do + + let(:contactables) { [people(:top_leader).tap{ |u| u.update(nickname: 'Funny Name') }] } + let(:label_format) { label_formats(:standard) } + let(:pdf) { Export::Pdf::Labels.new(label_format).generate(contactables) } + + let(:subject) { PDF::Inspector::Text.analyze(pdf) } + + context 'for nickname' do + it 'renders pp_post if pp_post given' do + label_format.update!(nickname: true) + expect(subject.strings).to include('Funny Name') + end + + it 'ignores nickname if disabled' do + expect(subject.strings.join(' ')).not_to include('Funny Name') + end + end + + context 'for pp_post' do + it 'renders pp_post if pp_post given' do + label_format.update!(pp_post: 'CH-3030 Bern') + expect(subject.strings).to include("CH-3030 Bern Post CH AG") + end + + it 'ignores pp_post if not given' do + label_format.update!(pp_post: ' ') + expect(subject.strings.join(' ')).not_to include("Post CH AG") + end + end +end diff --git a/spec/domain/export/tabular/events/list_spec.rb b/spec/domain/export/tabular/events/list_spec.rb new file mode 100644 index 0000000000..ab07e069d7 --- /dev/null +++ b/spec/domain/export/tabular/events/list_spec.rb @@ -0,0 +1,51 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +require 'spec_helper' + +describe Export::Tabular::Events::List do + + let(:courses) { double('courses', map: [], first: nil) } + let(:list) { Export::Tabular::Events::List.new(courses) } + + subject { list } + + its(:contactable_keys) { should eq [:name, :address, :zip_code, :town, :email, :phone_numbers] } + + context 'used labels' do + subject { list } + + its(:attributes) do + should == [:name, :group_names, :number, :kind, :description, :state, :location, + :date_0_label, :date_0_location, :date_0_duration, + :date_1_label, :date_1_location, :date_1_duration, + :date_2_label, :date_2_location, :date_2_duration, + :contact_name, :contact_address, :contact_zip_code, :contact_town, + :contact_email, :contact_phone_numbers, + :leader_name, :leader_address, :leader_zip_code, :leader_town, + :leader_email, :leader_phone_numbers, + :motto, :cost, :application_opening_at, :application_closing_at, + :maximum_participants, :external_applications, :priorization, + :teamer_count, :participant_count, :applicant_count] + end + + its(:labels) do + should == ['Name', 'Organisatoren', 'Kursnummer', 'Kursart', 'Beschreibung', 'Status', 'Ort / Adresse', + 'Datum 1 Bezeichnung', 'Datum 1 Ort', 'Datum 1 Zeitraum', + 'Datum 2 Bezeichnung', 'Datum 2 Ort', 'Datum 2 Zeitraum', + 'Datum 3 Bezeichnung', 'Datum 3 Ort', 'Datum 3 Zeitraum', + 'Kontaktperson Name', 'Kontaktperson Adresse', 'Kontaktperson PLZ', + 'Kontaktperson Ort', 'Kontaktperson Haupt-E-Mail', 'Kontaktperson Telefonnummern', + 'Hauptleitung Name', 'Hauptleitung Adresse', 'Hauptleitung PLZ', 'Hauptleitung Ort', + 'Hauptleitung Haupt-E-Mail', 'Hauptleitung Telefonnummern', + 'Motto', 'Kosten', 'Anmeldebeginn', 'Anmeldeschluss', 'Maximale Teilnehmerzahl', + 'Externe Anmeldungen', 'Priorisierung', 'Anzahl Leitungsteam', + 'Anzahl Teilnehmende', 'Anzahl Anmeldungen'] + end + end + +end diff --git a/spec/domain/export/csv/events/row_spec.rb b/spec/domain/export/tabular/events/row_spec.rb similarity index 94% rename from spec/domain/export/csv/events/row_spec.rb rename to spec/domain/export/tabular/events/row_spec.rb index 83d4d0f34e..3b47bf7295 100644 --- a/spec/domain/export/csv/events/row_spec.rb +++ b/spec/domain/export/tabular/events/row_spec.rb @@ -1,12 +1,13 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. require 'spec_helper' -describe Export::Csv::Events::Row do + +describe Export::Tabular::Events::Row do let(:max_dates) { 3 } let(:contactable_keys) { [:name, :address, :zip_code, :town, :email, :phone_numbers] } @@ -16,7 +17,7 @@ description: 'some description', number: 123, location: 'somewhere') end - let(:row) { Export::Csv::Events::Row.new(course) } + let(:row) { Export::Tabular::Events::Row.new(course) } subject { row } diff --git a/spec/domain/export/csv/people/contact_accounts_spec.rb b/spec/domain/export/tabular/people/contact_accounts_spec.rb old mode 100644 new mode 100755 similarity index 80% rename from spec/domain/export/csv/people/contact_accounts_spec.rb rename to spec/domain/export/tabular/people/contact_accounts_spec.rb index 5eb4f737a4..b3108f6d98 --- a/spec/domain/export/csv/people/contact_accounts_spec.rb +++ b/spec/domain/export/tabular/people/contact_accounts_spec.rb @@ -1,6 +1,6 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2017, Pfadibewegung Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. @@ -8,9 +8,9 @@ require 'spec_helper' -describe Export::Csv::People::ContactAccounts do +describe Export::Tabular::People::ContactAccounts do - subject { Export::Csv::People::ContactAccounts } + subject { Export::Tabular::People::ContactAccounts } context 'phone_numbers' do it 'creates standard key and human translations' do @@ -25,4 +25,5 @@ expect(subject.human(SocialAccount, 'foo')).to eq 'Social Media Adresse foo' end end + end diff --git a/spec/domain/export/csv/people/participation_row_spec.rb b/spec/domain/export/tabular/people/participation_row_spec.rb similarity index 92% rename from spec/domain/export/csv/people/participation_row_spec.rb rename to spec/domain/export/tabular/people/participation_row_spec.rb index 5967b5c6d7..d9a435c5d0 100644 --- a/spec/domain/export/csv/people/participation_row_spec.rb +++ b/spec/domain/export/tabular/people/participation_row_spec.rb @@ -7,12 +7,12 @@ require 'spec_helper' -describe Export::Csv::People::ParticipationRow do +describe Export::Tabular::People::ParticipationRow do let(:person) { people(:top_leader) } let(:participation) { Fabricate(:event_participation, person: person, event: events(:top_course)) } - let(:row) { Export::Csv::People::ParticipationRow.new(participation) } + let(:row) { Export::Tabular::People::ParticipationRow.new(participation) } subject { row } it { expect(row.fetch(:first_name)).to eq 'Top' } diff --git a/spec/domain/export/tabular/people/participations_address_spec.rb b/spec/domain/export/tabular/people/participations_address_spec.rb new file mode 100644 index 0000000000..76fc7842da --- /dev/null +++ b/spec/domain/export/tabular/people/participations_address_spec.rb @@ -0,0 +1,24 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +require 'spec_helper' + +describe Export::Tabular::People::ParticipationsAddress do + + let(:person) { people(:top_leader) } + let(:participation) { Fabricate(:event_participation, person: person, event: events(:top_course)) } + let(:list) { [participation] } + let(:people_list) { Export::Tabular::People::ParticipationsAddress.new(list) } + + subject { people_list.attribute_labels } + + context 'address data' do + its([:first_name]) { should eq 'Vorname' } + its([:town]) { should eq 'Ort' } + end + +end diff --git a/spec/domain/export/tabular/people/participations_full_spec.rb b/spec/domain/export/tabular/people/participations_full_spec.rb new file mode 100644 index 0000000000..296f60ae88 --- /dev/null +++ b/spec/domain/export/tabular/people/participations_full_spec.rb @@ -0,0 +1,38 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +require 'spec_helper' + +describe Export::Tabular::People::ParticipationsFull do + + let(:person) { people(:top_leader) } + let(:participation) { Fabricate(:event_participation, person: person, event: events(:top_course)) } + let(:list) { [participation] } + let(:people_list) { Export::Tabular::People::ParticipationsFull.new(list) } + + subject { people_list.attribute_labels } + + context 'additional_information' do + its([:additional_information]) { should eq 'Zusätzliche Angaben' } + end + + context 'participation_additional_information' do + its([:participation_additional_information]) { should eq 'Bemerkungen (Allgemeines, Gesundheitsinformationen, Allergien, usw.)' } + end + + context 'questions' do + let(:participation) { Fabricate(:event_participation, person: person, event: events(:top_course)) } + let(:question) { events(:top_course).questions.first } + + before { participation.init_answers } + it 'has keys and values' do + expect(subject[:"question_#{event_questions(:top_ov).id}"]).to eq 'GA oder Halbtax?' + expect(subject.keys.select { |key| key =~ /question/ }.size).to eq(3) + end + end + +end diff --git a/spec/domain/export/tabular/people/people_address_spec.rb b/spec/domain/export/tabular/people/people_address_spec.rb new file mode 100644 index 0000000000..c856dffbb9 --- /dev/null +++ b/spec/domain/export/tabular/people/people_address_spec.rb @@ -0,0 +1,66 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +require 'spec_helper' + +describe Export::Tabular::People::PeopleAddress do + + let(:person) { people(:top_leader) } + let(:list) { [person] } + let(:people_list) { Export::Tabular::People::PeopleAddress.new(list) } + subject { people_list } + + its(:attributes) do + should == [:first_name, :last_name, :nickname, :company_name, :company, :email, :address, + :zip_code, :town, :country, :gender, :birthday, :layer_group, :roles] + end + + context 'standard attributes' do + + context '#attribute_labels' do + subject { people_list.attribute_labels } + + its([:id]) { should be_blank } + its([:roles]) { should eq 'Rollen' } + its([:first_name]) { should eq 'Vorname' } + end + + context 'key list' do + subject { people_list.attribute_labels.keys.join(' ') } + it { is_expected.not_to match(/phone/) } + it { is_expected.not_to match(/social_account/) } + end + end + + context 'phone_numbers' do + before { person.phone_numbers << PhoneNumber.new(label: 'Privat', number: 321) } + + subject { people_list.attribute_labels } + + its([:phone_number_privat]) { should eq 'Telefonnummer Privat' } + its([:phone_number_mobil]) { should be_nil } + + context 'different labels' do + let(:other) { people(:bottom_member) } + let(:list) { [person, other] } + + before do + other.phone_numbers << PhoneNumber.new(label: 'Foobar', number: 321) + person.phone_numbers << PhoneNumber.new(label: 'Privat', number: 321) + end + + its([:phone_number_privat]) { should eq 'Telefonnummer Privat' } + its([:phone_number_foobar]) { should eq 'Telefonnummer Foobar' } + end + + context 'blank label is not exported' do + before { person.phone_numbers << PhoneNumber.new(label: '', number: 321) } + its(:keys) { should_not include :phone_number_ } + end + end + +end diff --git a/spec/domain/export/tabular/people/people_full_spec.rb b/spec/domain/export/tabular/people/people_full_spec.rb new file mode 100644 index 0000000000..e0191ccfaa --- /dev/null +++ b/spec/domain/export/tabular/people/people_full_spec.rb @@ -0,0 +1,51 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +require 'spec_helper' + +describe Export::Tabular::People::PeopleFull do + + before do + PeopleRelation.kind_opposites['parent'] = 'child' + PeopleRelation.kind_opposites['child'] = 'parent' + end + + after do + PeopleRelation.kind_opposites.clear + end + + let(:person) { people(:top_leader) } + let(:list) { [person] } + let(:people_list) { Export::Tabular::People::PeopleFull.new(list) } + + subject { people_list } + + its(:attributes) do should eq [:first_name, :last_name, :company_name, :nickname, :company, + :email, :address, :zip_code, :town, :country, :gender, :birthday, + :additional_information, :layer_group, :roles] end + + context '#attribute_labels' do + subject { people_list.attribute_labels } + + its([:roles]) { should eq 'Rollen' } + its([:social_account_website]) { should be_blank } + + its([:company]) { should eq 'Firma' } + its([:company_name]) { should eq 'Firmenname' } + + context 'social accounts' do + before { person.social_accounts << SocialAccount.new(label: 'Webseite', name: 'foo.bar') } + its([:social_account_webseite]) { should eq 'Social Media Adresse Webseite' } + end + + context 'people relations' do + before { person.relations_to_tails << PeopleRelation.new(head_id: person.id, tail_id: people(:bottom_member).id, kind: 'parent') } + its([:people_relation_parent]) { should eq 'Elternteil' } + end + end + +end diff --git a/spec/domain/export/csv/people/person_row_spec.rb b/spec/domain/export/tabular/people/person_row_spec.rb similarity index 91% rename from spec/domain/export/csv/people/person_row_spec.rb rename to spec/domain/export/tabular/people/person_row_spec.rb index 99a0b72d9a..3e8edade37 100644 --- a/spec/domain/export/csv/people/person_row_spec.rb +++ b/spec/domain/export/tabular/people/person_row_spec.rb @@ -1,14 +1,13 @@ # encoding: utf-8 -# Copyright (c) 2012-2014, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. require 'spec_helper' -require 'csv' -describe Export::Csv::People::PersonRow do +describe Export::Tabular::People::PersonRow do before do PeopleRelation.kind_opposites['parent'] = 'child' @@ -20,7 +19,7 @@ end let(:person) { people(:top_leader) } - let(:row) { Export::Csv::People::PersonRow.new(person) } + let(:row) { Export::Tabular::People::PersonRow.new(person) } subject { row } diff --git a/spec/domain/export/xlsx/events/list_spec.rb b/spec/domain/export/xlsx/events/list_spec.rb index f9180d1cfe..25f3430fa7 100644 --- a/spec/domain/export/xlsx/events/list_spec.rb +++ b/spec/domain/export/xlsx/events/list_spec.rb @@ -1,15 +1,23 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Pfadibewegung Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + require 'spec_helper' -describe Export::Xlsx::Events::List do - +describe Export::Tabular::Events::List do + let(:courses) { [course1] } let(:course1) { events(:top_course) } it 'exports events list as xlsx' do expect_any_instance_of(Axlsx::Worksheet) .to receive(:add_row) - .exactly(2).times + .exactly(2).times.and_call_original - Export::Xlsx::Events::List.export(courses) + Export::Tabular::Events::List.xlsx(courses) end + end diff --git a/spec/domain/export/xlsx/people/people_address_spec.rb b/spec/domain/export/xlsx/people/people_address_spec.rb new file mode 100755 index 0000000000..8c8306ae94 --- /dev/null +++ b/spec/domain/export/xlsx/people/people_address_spec.rb @@ -0,0 +1,31 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Pfadibewegung Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +require 'spec_helper' + +describe Export::Tabular::People::PeopleAddress do + + before do + PeopleRelation.kind_opposites['parent'] = 'child' + PeopleRelation.kind_opposites['child'] = 'parent' + end + + after do + PeopleRelation.kind_opposites.clear + end + + let(:person) { people(:top_leader) } + let(:list) { [person] } + + it 'exports people list as xlsx' do + expect_any_instance_of(Axlsx::Worksheet) + .to receive(:add_row) + .exactly(2).times.and_call_original + + Export::Tabular::People::PeopleAddress.xlsx(list) + end +end diff --git a/spec/domain/export/xlsx/people/people_full_spec.rb b/spec/domain/export/xlsx/people/people_full_spec.rb new file mode 100755 index 0000000000..fc253f834d --- /dev/null +++ b/spec/domain/export/xlsx/people/people_full_spec.rb @@ -0,0 +1,31 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Pfadibewegung Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +require 'spec_helper' + +describe Export::Tabular::People::PeopleFull do + + before do + PeopleRelation.kind_opposites['parent'] = 'child' + PeopleRelation.kind_opposites['child'] = 'parent' + end + + after do + PeopleRelation.kind_opposites.clear + end + + let(:person) { people(:top_leader) } + let(:list) { [person] } + + it 'exports people list full as xlsx' do + expect_any_instance_of(Axlsx::Worksheet) + .to receive(:add_row) + .exactly(2).times.and_call_original + + Export::Tabular::People::PeopleFull.xlsx(list) + end +end diff --git a/spec/domain/group/deleted_people_spec.rb b/spec/domain/group/deleted_people_spec.rb new file mode 100644 index 0000000000..0ef46fe81e --- /dev/null +++ b/spec/domain/group/deleted_people_spec.rb @@ -0,0 +1,119 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Pfadibewegung Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +require 'spec_helper' + +describe Group::DeletedPeople do + + context 'find deleted people' do + let(:group) { groups(:top_layer) } + let(:person) { role.person.reload } + let(:role) do + Fabricate(Group::TopLayer::TopAdmin.name, group: group, + created_at: Time.zone.now - 1.year) + end + + let(:sibling_group) { groups(:bottom_layer_one) } + let(:sibling_group_one) { groups(:bottom_group_one_one) } + let(:sibling_person) { sibling_role.person.reload } + let(:sibling_role) do + Fabricate(Group::BottomGroup::Leader.name, group: sibling_group_one, + created_at: Time.zone.now - 1.year) + end + + context 'when group has people without role' do + before do + role.destroy + end + + it 'finds those people' do + expect(Group::DeletedPeople.deleted_for(group).first).to eq(person) + end + + it 'doesn\'t find people with new role' do + Group::TopLayer::TopAdmin.create(person: person, group: group) + + expect(person.roles.count).to eq 1 + expect(Group::DeletedPeople.deleted_for(group).count).to eq 0 + end + + it 'finds people from other group in same layer' do + sibling_role.destroy + expect(Group::DeletedPeople.deleted_for(sibling_group)).to include sibling_person + end + end + + context 'when group has no people without role' do + it 'returns empty' do + expect(Group::DeletedPeople.deleted_for(group).count).to eq 0 + end + end + end + + context 'when roles are deleted in different series' do + + let(:group) { groups(:top_layer) } + let(:bottom_group) { groups(:bottom_layer_one) } + let(:person) { role_top.person } + let(:role_top) do + Fabricate(Group::TopLayer::TopAdmin.name, group: group, + created_at: Time.zone.now - 1.year) + end + let(:role_bottom) do + Fabricate(Group::BottomLayer::Leader.name, group: bottom_group, person: person, + created_at: Time.zone.now - 1.year) + end + + before do + bottom_group.update(parent_id: group.id) + end + + it 'doesnt find when last role deleted in bottom group' do + role_top.destroy + role_top.update_column(:deleted_at, 1.hour.ago) + role_bottom.destroy + expect(Group::DeletedPeople.deleted_for(group).count).to eq 0 + end + + it 'find if last deleted role in top group' do + role_bottom.destroy + role_bottom.update_column(:deleted_at, 1.hour.ago) + role_top.destroy + expect(Group::DeletedPeople.deleted_for(group)).to include person + end + + it 'finds people in child group' do + role_top.destroy + role_top.update_column(:deleted_at, 1.hour.ago) + role_bottom.destroy + expect(Group::DeletedPeople.deleted_for(bottom_group)).to include person + end + + it 'doesnt find when not visible from above' do + role_bottom = Fabricate(Role::External.name.to_sym, group: bottom_group, person: person, + created_at: DateTime.current - 30.day) + role_top.destroy + expect(Group::DeletedPeople.deleted_for(group).count).to eq 0 + end + + it 'finds multiple people' do + del = 1.months.ago + role_top.destroy + role_top.update!(deleted_at: del) + top2 = Fabricate(Group::TopLayer::TopAdmin.name, group: group, + created_at: Time.zone.now - 1.year) + Fabricate(Group::BottomLayer::Leader.name, group: bottom_group, person: top2.person, + created_at: Time.zone.now - 9.months) + top2.destroy + top2.update!(deleted_at: del) + + expect(Group::DeletedPeople.deleted_for(group)).to match_array([person]) + end + + end +end + diff --git a/spec/domain/group/merger_spec.rb b/spec/domain/group/merger_spec.rb index 906cf81847..00a1883637 100644 --- a/spec/domain/group/merger_spec.rb +++ b/spec/domain/group/merger_spec.rb @@ -1,6 +1,6 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. @@ -13,6 +13,10 @@ let(:group2) { groups(:bottom_layer_two) } let(:other_group) { groups(:top_layer) } + let(:merger) { Group::Merger.new(group1, group2, 'foo') } + + let(:new_group) { Group.find(merger.new_group.id) } + context 'merge groups' do before do @@ -24,17 +28,18 @@ Fabricate(:event, groups: [group1]) Fabricate(:event, groups: [group1]) Fabricate(:event, groups: [group2]) + + group2.create_invoice_config! + Fabricate(:invoice, group: group2, recipient: @person) + Fabricate(:invoice_article, group: group2) end it 'creates a new group and merges roles, events' do - merge = Group::Merger.new(group1, group2, 'foo') - expect(merge.group2_valid?).to eq true - - merge.merge! + expect(merger.group2_valid?).to eq true + merger.merge! - new_group = Group.find(merge.new_group.id) expect(new_group.name).to eq 'foo' - expect(new_group.type).to eq merge.new_group.type + expect(new_group.type).to eq merger.new_group.type expect(new_group.children.count).to eq 3 @@ -66,20 +71,36 @@ it 'add events from both groups only once' do e = Fabricate(:event, groups: [group1, group2]) - merge = Group::Merger.new(group1, group2, 'foo') - merge.merge! + merger.merge! e.reload - expect(e.group_ids).to match_array([group1, group2, merge.new_group].collect(&:id)) + expect(e.group_ids).to match_array([group1, group2, new_group].collect(&:id)) end - it 'updates layer_group_id for children' do - merge = Group::Merger.new(group1, group2, 'foo') - merge.merge! + it 'updates layer_group_id for descendants' do + ids = (group1.descendants + group2.descendants).map(&:id) + + merger.merge! + + expect(Group.find(ids).map(&:layer_group_id).uniq).to eq [new_group.id] + end + + it 'moves invoices' do + expect(group1.invoices.count).to eq 2 + expect(group2.invoices.count).to eq 1 + + merger.merge! + + expect(new_group.invoices.count).to eq 3 + end + + it 'moves invoice-articles' do + expect(group1.invoice_articles.count).to eq 3 + expect(group2.invoice_articles.count).to eq 1 + + merger.merge! - new_group = Group.find(merge.new_group.id) - expect(group1.children.map(&:layer_group_id).uniq).to eq [new_group.id] - expect(group2.children.map(&:layer_group_id).uniq).to eq [new_group.id] + expect(new_group.invoice_articles.count).to eq 4 end end diff --git a/spec/domain/group/mover_spec.rb b/spec/domain/group/mover_spec.rb index 3a6f856c01..f40487595c 100644 --- a/spec/domain/group/mover_spec.rb +++ b/spec/domain/group/mover_spec.rb @@ -67,6 +67,10 @@ def groups_for(*args) it 'nested set should still be valid' do expect(Group).to be_valid end + + it 'updates layer groups of children' do + expect(groups(:bottom_group_one_one_one).layer_group_id).to eq(target.id) + end end context 'association count' do diff --git a/spec/domain/import/person_export_import_spec.rb b/spec/domain/import/person_export_import_spec.rb index 43367d58e7..a449ca7e1c 100644 --- a/spec/domain/import/person_export_import_spec.rb +++ b/spec/domain/import/person_export_import_spec.rb @@ -55,7 +55,7 @@ end def export(person) - Export::Csv::People::PeopleFull.export([person]) + Export::Tabular::People::PeopleFull.csv([person]) end def import(csv) diff --git a/spec/domain/person/add_request/creator/group_spec.rb b/spec/domain/person/add_request/creator/group_spec.rb index 9a16b2598f..f4e0c04d21 100644 --- a/spec/domain/person/add_request/creator/group_spec.rb +++ b/spec/domain/person/add_request/creator/group_spec.rb @@ -1,6 +1,6 @@ # encoding: utf-8 -# Copyright (c) 2012-2015, Pfadibewegung Schweiz. This file is part of +# Copyright (c) 2012-2017, Pfadibewegung Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. @@ -10,7 +10,7 @@ describe Person::AddRequest::Creator::Group do let(:primary_layer) { person.primary_group.layer_group } - let(:person) { Fabricate(Group::BottomLayer::Member.name, group: groups(:bottom_layer_two)).person } + let(:person) { Fabricate(Group::BottomLayer::Member.name, group: groups(:bottom_layer_two), created_at: 1.year.ago).person } let(:requester) { Fabricate(Group::BottomLayer::Leader.name, group: groups(:bottom_layer_one)).person } let(:group) { groups(:bottom_group_one_one) } @@ -33,11 +33,24 @@ expect(subject).to be_required end + it 'is true if last layer activated requests' do + person.roles.first.destroy + expect(person.primary_group_id).to be_nil + expect(subject).to be_required + end + it 'is false if primary layer deactivated requests' do primary_layer.update_column(:require_person_add_requests, false) expect(subject).not_to be_required end + it 'is false if last layer activated requests' do + primary_layer.update_column(:require_person_add_requests, false) + person.roles.first.destroy + expect(person.primary_group_id).to be_nil + expect(subject).not_to be_required + end + it 'is false if person is not persisted yet' do entity = Group::BottomGroup::Member.new(group: group, person: Person.new(last_name: 'Tester')) creator = Person::AddRequest::Creator::Group.new(entity, ability) diff --git a/spec/domain/person/filter/chain_spec.rb b/spec/domain/person/filter/chain_spec.rb new file mode 100644 index 0000000000..1faf4c077d --- /dev/null +++ b/spec/domain/person/filter/chain_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe Person::Filter::Chain do + + context 'initialize' do + + it 'only build present filters' do + chain = Person::Filter::Chain.new(role: { role_type_ids: '' }, qualification: { }) + expect(chain).to be_blank + end + + it 'ignores unknown filters' do + chain = Person::Filter::Chain.new(ratatui: { query: 'foo' }, qualification: { qualification_kind_ids: '20' }) + expect(chain).to be_present + expect(chain.filters.size).to eq(1) + end + + end + + context 'to_params' do + it 'includes all present filters' do + chain = Person::Filter::Chain.new(role: { role_type_ids: '2-6-9' }, + qualification: { qualification_kind_ids: [] }) + expect(chain.to_params).to eq({ role: { role_type_ids: '2-6-9' } }) + end + end + + context 'dump' do + it 'serializes all present filters' do + chain = Person::Filter::Chain.new(role: { role_type_ids: '2-6-9' }, + qualification: { qualification_kind_ids: ['14'] }) + yaml = Person::Filter::Chain.dump(chain) + roundtrip = Person::Filter::Chain.load(yaml) + expect(roundtrip.to_params.deep_stringify_keys).to eq( + { role: { role_type_ids: '2-6-9' }, + qualification: { qualification_kind_ids: '14' } }.deep_stringify_keys + ) + end + end + +end diff --git a/spec/domain/person/filter/qualification_spec.rb b/spec/domain/person/filter/qualification_spec.rb new file mode 100644 index 0000000000..04cef13791 --- /dev/null +++ b/spec/domain/person/filter/qualification_spec.rb @@ -0,0 +1,477 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Pfadibewegung Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +require 'spec_helper' + +describe Person::Filter::Qualification do + + let(:user) { people(:top_leader) } + let(:group) { groups(:top_layer) } + let(:range) { nil } + let(:validity) { 'all' } + let(:match) { 'one' } + let(:qualification_kind_ids) { [] } + + let(:list_filter) do + Person::Filter::List.new( + group, + user, + range: range, + filters: { qualification: filters.merge(additional_filters) } + ) + end + + let(:filters) do + { + qualification_kind_ids: qualification_kind_ids, + validity: validity, + match: match, + } + end + let(:additional_filters) { {} } + + let(:entries) { list_filter.entries } + + let(:bl_leader) { create_person(Group::BottomLayer::Leader, :bottom_layer_one, 'reactivateable', :sl, :gl_leader) } + + before do + @tg_member = create_person(Group::TopGroup::Member, :top_group, 'active', :sl) + # duplicate qualification + Fabricate(:qualification, person: @tg_member, qualification_kind: qualification_kinds(:sl), start_at: Date.today - 2.weeks) + + @tg_extern = create_person(Role::External, :top_group, 'active', :sl) + + @bl_leader = bl_leader + @bl_extern = create_person(Role::External, :bottom_layer_one, 'reactivateable', :gl_leader) + + @bg_leader = create_person(Group::BottomGroup::Leader, :bottom_group_one_one, 'all', :sl, :ql) + @bg_member = create_person(Group::BottomGroup::Member, :bottom_group_one_one, 'active', :sl) + end + + def create_person(role, group, validity, *qualification_kinds) + person = Fabricate(role.name.to_sym, group: groups(group)).person + qualification_kinds.each do |key| + kind = qualification_kinds(key) + start = case validity + when 'active' then Date.today + when 'reactivateable' then Date.today - kind.validity.years - 1.year + when Fixnum then Date.new(validity, 1, 1) + else Date.today - 20.years + end + Fabricate(:qualification, person: person, qualification_kind: kind, start_at: start) + end + person + end + + context 'no filter' do + it 'loads only entries on group' do + expect(entries).to be_empty + end + + it 'count is 0' do + expect(list_filter.all_count).to eq(0) + end + end + + context 'kind deep' do + let(:range) { 'deep' } + + context 'no qualification kinds' do + it 'loads only entries on group' do + expect(entries).to be_empty + end + end + + context 'with qualification kinds' do + let(:qualification_kind_ids) { qualification_kinds(:sl, :gl_leader).collect(&:id) } + + it 'loads all entries in layer and below' do + expect(entries).to match_array([@tg_member, @tg_extern, @bl_leader, @bg_leader]) + end + + it 'contains only visible people' do + expect(entries.size).to eq(list_filter.all_count - 2) + end + + context 'with years' do + let(:qualification_kind_ids) { [qualification_kinds(:sl_leader).id] } + + before do + @sl_2013 = create_person(Group::TopGroup::Member, :top_group, 2013, :sl_leader) + @sl_2014 = create_person(Group::TopGroup::Member, :top_group, 2014, :sl_leader) + @sl_2015 = create_person(Group::TopGroup::Member, :top_group, 2015, :sl_leader) + @sl_2016 = create_person(Group::TopGroup::Member, :top_group, 2016, :sl_leader) + end + + context 'loads entry with start_at later' do + let(:additional_filters) do + { + start_at_year_from: 2015 + } + end + it 'correctly' do + expect(entries).to match_array([@sl_2015, @sl_2016]) + end + end + + context 'loads entry with start_at before' do + let(:additional_filters) do + { + start_at_year_until: 2015 + } + end + it 'correctly' do + expect(entries).to match_array([@sl_2015, @sl_2014, @sl_2013]) + end + end + + context 'loads entry with start_at between' do + let(:additional_filters) do + { + start_at_year_from: 2014, + start_at_year_until: 2015 + } + end + it 'correctly' do + expect(entries).to match_array([@sl_2015, @sl_2014]) + end + end + + context 'loads entry with finish_at later' do + let(:additional_filters) do + { + finish_at_year_from: 2016 + } + end + it 'correctly' do + expect(entries).to match_array([@sl_2014, @sl_2015, @sl_2016]) + end + end + + context 'loads entry with finish_at before' do + let(:additional_filters) do + { + finish_at_year_until: 2016 + } + end + it 'correctly' do + expect(entries).to match_array([@sl_2014, @sl_2013]) + end + end + + context 'loads entry with finish_at between' do + let(:additional_filters) do + { + finish_at_year_from: 2016, + finish_at_year_until: 2017 + } + end + it 'correctly' do + expect(entries).to match_array([@sl_2015, @sl_2014]) + end + end + + context 'only active' do + + context 'loads entry with finish_at before' do + let(:additional_filters) do + { + validity: 'active', + finish_at_year_until: 2016 + } + end + it 'correctly' do + expect(entries).to match_array([]) + end + end + + end + end + + context 'as bottom leader' do + let(:user) { bl_leader } + + it 'loads all accessible entries' do + expect(entries).to match_array([@bl_leader, @bg_leader, @bg_member, @bl_extern]) + end + + it 'contains only visible people' do + expect(entries.size).to eq(list_filter.all_count - 2) + end + + context 'combined with role filter' do + let(:list_filter) do + Person::Filter::List.new(group, + user, + range: range, + filters: { + role: { + role_types: [ + Group::TopGroup::Member.sti_name, + Group::BottomLayer::Leader.sti_name, + Group::BottomGroup::Leader.sti_name + ] + }, + qualification: { + qualification_kind_ids: qualification_kind_ids, + validity: validity + } + }) + end + + it 'loads all accessible entries' do + expect(entries).to match_array([@bl_leader, @bg_leader]) + end + + it 'contains only visible people' do + expect(entries.size).to eq(list_filter.all_count - 1) + end + + end + end + end + + end + + context 'kind layer' do + let(:range) { 'layer' } + + context 'with qualification kinds' do + let(:qualification_kind_ids) { qualification_kinds(:sl, :gl_leader).collect(&:id) } + + it 'loads all entries in layer' do + expect(entries).to match_array([@tg_member, @tg_extern]) + end + + it 'contains all people' do + expect(entries.size).to eq(list_filter.all_count) + end + end + end + + context 'in bottom layer' do + let(:user) { bl_leader } + let(:range) { 'layer' } + let(:group) { groups(:bottom_layer_one) } + let(:qualification_kind_ids) { qualification_kinds(:sl, :gl_leader).collect(&:id) } + + context 'active validities' do + + let(:validity) { 'active' } + + it 'loads matched entries' do + expect(entries).to match_array([@bg_member]) + end + + it 'contains all people' do + expect(entries.size).to eq(list_filter.all_count) + end + + context 'with infinite qualifications' do + let(:qualification_kind_ids) { qualification_kinds(:sl, :ql).collect(&:id) } + + it 'contains them' do + expect(entries).to match_array([@bg_member, @bg_leader]) + end + end + + context 'match all' do + let(:match) { 'all' } + let(:qualification_kind_ids) { qualification_kinds(:sl, :ql).collect(&:id) } + + it 'contains only people with all qualifications' do + Fabricate(:qualification, + person: @bg_leader, + qualification_kind: qualification_kinds(:sl), + start_at: Date.today) + + expect(entries).to match_array([@bg_leader]) + end + + it 'contains people with additional qualifications' do + Fabricate(:qualification, + person: @bg_leader, + qualification_kind: qualification_kinds(:sl), + start_at: Date.today) + Fabricate(:qualification, + person: @bg_leader, + qualification_kind: qualification_kinds(:gl_leader), + start_at: Date.today) + + expect(entries).to match_array([@bg_leader]) + end + + context 'loads entry with start_at between' do + let(:start_at) { Date.today - 2.years } + let(:additional_filters) do + { + start_at_year_from: start_at.year, + start_at_year_until: start_at.year + } + end + + it 'correctly' do + @bg_leader.qualifications. + find { |q| q.qualification_kind == qualification_kinds(:ql) }. + update!(start_at: start_at) + Fabricate(:qualification, + person: @bg_leader, + qualification_kind: qualification_kinds(:sl), + start_at: start_at) + + expect(entries).to match_array([@bg_leader]) + end + end + + context 'does not contain entries outside start_at between' do + let(:start_at) { Date.today - 2.years } + let(:additional_filters) do + { + start_at_year_from: start_at.year - 2, + start_at_year_until: start_at.year - 1 + } + end + + it 'correctly' do + @bg_leader.qualifications. + find { |q| q.qualification_kind == qualification_kinds(:ql) }. + update!(start_at: start_at) + Fabricate(:qualification, + person: @bg_leader, + qualification_kind: qualification_kinds(:sl), + start_at: start_at) + + expect(entries).to match_array([]) + end + end + + it 'does not contain people with all, but expired qualifications' do + expect(entries).to match_array([]) + end + + end + + context 'as top leader' do + let(:user) { people(:top_leader) } + + it 'does not load non-visible entries' do + expect(entries).to match_array([]) + end + + it 'contains only visible people' do + expect(entries.size).to eq(list_filter.all_count - 1) + end + end + end + + context 'reactivateable validities' do + let(:validity) { 'reactivateable' } + + it 'loads matched entries' do + expect(entries).to match_array([@bg_member, @bl_extern, @bl_leader]) + end + + it 'contains all people' do + expect(entries.size).to eq(list_filter.all_count) + end + + context 'with infinite qualifications' do + let(:qualification_kind_ids) { qualification_kinds(:sl, :ql).collect(&:id) } + it 'contains them' do + expect(entries).to match_array([@bg_member, @bg_leader]) + end + end + + context 'match all' do + let(:match) { 'all' } + + before { qualification_kinds(:sl).update!(reactivateable: 2) } + + it 'loads matched entries' do + expect(entries).to match_array([@bl_leader]) + end + + it 'loads matched entries with multiple, old qualifications just once' do + kind = qualification_kinds(:sl) + Fabricate(:qualification, + person: @bg_member, + qualification_kind: kind, + start_at: Date.today - kind.validity.years - 1.year) + kind = qualification_kinds(:gl_leader) + Fabricate(:qualification, + person: @bg_member, + qualification_kind: kind, + start_at: Date.today - kind.validity.years - 1.year) + + expect(entries).to match_array([@bg_member, @bl_leader]) + end + + it 'does not contain people with all, but expired qualifications' do + Fabricate(:qualification, + person: @bg_member, + qualification_kind: qualification_kinds(:gl_leader), + start_at: Date.today - 10.years) + + expect(entries).to match_array([@bl_leader]) + end + end + end + + context 'all validities' do + let(:validity) { 'all' } + + it 'loads matched entries' do + expect(entries).to match_array([@bg_member, @bl_extern, @bg_leader, @bl_leader]) + end + + it 'contains all people' do + expect(entries.size).to eq(list_filter.all_count) + end + + context 'match all' do + let(:match) { 'all' } + + it 'loads matched entries with multiple, old qualifications just once' do + kind = qualification_kinds(:sl) + Fabricate(:qualification, + person: @bg_member, + qualification_kind: kind, + start_at: Date.today - kind.validity.years - 1.year) + Fabricate(:qualification, + person: @bg_member, + qualification_kind: qualification_kinds(:gl_leader), + start_at: Date.today - 10.years) + + expect(entries).to match_array([@bg_member, @bl_leader]) + end + end + end + end + + it 'does not fail if sorting by role and person has only group_read' do + allow(Settings.people).to receive(:default_sort).and_return('role') + list_filter = Person::Filter::List.new( + group, + create_person(Group::BottomGroup::Member, :bottom_group_one_one, 'active'), + range: "214", + filters: { qualification: { qualification_kind_ids: "2", validity: "1" }} + ) + expect(list_filter.entries).to be_empty + end + + it 'does not fail if sorting by role and person has layer_and_below_full' do + allow(Settings.people).to receive(:default_sort).and_return('role') + list_filter = Person::Filter::List.new( + group, + user, + range: "214", + filters: { qualification: { qualification_kind_ids: "2", validity: "1" }} + ) + expect(list_filter.entries).to be_empty + end + +end diff --git a/spec/domain/person/filter/role_spec.rb b/spec/domain/person/filter/role_spec.rb new file mode 100644 index 0000000000..6b03cb8df7 --- /dev/null +++ b/spec/domain/person/filter/role_spec.rb @@ -0,0 +1,290 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +require 'spec_helper' + +describe Person::Filter::Role do + + let(:user) { people(:top_leader) } + let(:group) { groups(:top_group) } + let(:range) { nil } + let(:role_types) { [] } + let(:role_type_ids_string) { role_types.collect(&:id).join(Person::Filter::Role::ID_URL_SEPARATOR) } + let(:list_filter) do + Person::Filter::List.new(group, + user, + range: range, + filters: { + role: {role_type_ids: role_type_ids_string } + }) + end + + let(:entries) { list_filter.entries } + + context 'initialize' do + + it 'ignores unknown role types' do + filter = Person::Filter::Role.new(:role, role_types: %w(Group::TopGroup::Leader Group::BottomGroup::OldRole File Group::BottomGroup::Member)) + expect(filter.to_hash).to eq(role_types: %w(Group::TopGroup::Leader Group::BottomGroup::Member)) + end + + it 'ignores unknown role ids' do + filter = Person::Filter::Role.new(:role, role_type_ids: %w(1 304 3 judihui)) + expect(filter.to_params).to eq(role_type_ids: '1-3') + end + + end + + context 'filtering' do + + before do + @tg_member = Fabricate(Group::TopGroup::Member.name.to_sym, group: groups(:top_group)).person + Fabricate(:phone_number, contactable: @tg_member, number: '123', label: 'Privat', public: true) + Fabricate(:phone_number, contactable: @tg_member, number: '456', label: 'Mobile', public: false) + Fabricate(:social_account, contactable: @tg_member, name: 'facefoo', label: 'Facebook', public: true) + Fabricate(:social_account, contactable: @tg_member, name: 'skypefoo', label: 'Skype', public: false) + # duplicate role + Fabricate(Group::TopGroup::Member.name.to_sym, group: groups(:top_group), person: @tg_member) + @tg_extern = Fabricate(Role::External.name.to_sym, group: groups(:top_group)).person + + @bl_leader = Fabricate(Group::BottomLayer::Leader.name.to_sym, group: groups(:bottom_layer_one)).person + @bl_extern = Fabricate(Role::External.name.to_sym, group: groups(:bottom_layer_one)).person + + @bg_leader = Fabricate(Group::BottomGroup::Leader.name.to_sym, group: groups(:bottom_group_one_one)).person + @bg_member = Fabricate(Group::BottomGroup::Member.name.to_sym, group: groups(:bottom_group_one_one)).person + end + + context 'group' do + it 'loads all members of a group' do + expect(entries.collect(&:id)).to match_array([user, @tg_member].collect(&:id)) + end + + it 'contains all existing members' do + expect(entries.size).to eq(list_filter.all_count) + end + + context 'with external types' do + let(:role_types) { [Role::External] } + it 'loads externs of a group' do + expect(entries.collect(&:id)).to match_array([@tg_extern].collect(&:id)) + end + + it 'contains all existing externals' do + expect(entries.size).to eq(list_filter.all_count) + end + end + + context 'with specific types' do + let(:role_types) { [Role::External, Group::TopGroup::Member] } + it 'loads selected roles of a group' do + expect(entries.collect(&:id)).to match_array([@tg_member, @tg_extern].collect(&:id)) + end + + it 'contains all existing people' do + expect(entries.size).to eq(list_filter.all_count) + end + end + end + + context 'layer' do + let(:group) { groups(:bottom_layer_one) } + let(:range) { 'layer' } + + context 'with layer and below full' do + let(:user) { @bl_leader } + + it 'loads group members when no types given' do + expect(entries.collect(&:id)).to match_array([people(:bottom_member), @bl_leader].collect(&:id)) + expect(list_filter.all_count).to eq(2) + end + + context 'with specific types' do + let(:role_types) { [Group::BottomGroup::Member, Role::External] } + + it 'loads selected roles of a group when types given' do + expect(entries.collect(&:id)).to match_array([@bg_member, @bl_extern].collect(&:id)) + expect(list_filter.all_count).to eq(2) + end + end + end + + end + + context 'deep' do + let(:group) { groups(:top_layer) } + let(:range) { 'deep' } + + it 'loads group members when no types are given' do + expect(entries.collect(&:id)).to match_array([]) + end + + context 'with specific types' do + let(:role_types) { [Group::BottomGroup::Leader, Role::External] } + + it 'loads selected roles of a group when types given' do + expect(entries.collect(&:id)).to match_array([@bg_leader, @tg_extern].collect(&:id)) + end + + it 'contains not all existing people' do + expect(entries.size).to eq(list_filter.all_count - 1) + end + end + end + end + + context 'filering specific timeframe' do + include ActiveSupport::Testing::TimeHelpers + + let(:person) { people(:top_leader) } + let(:now) { Time.zone.parse('2017-02-01 10:00:00') } + + around(:each) { |example| travel_to(now) { example.run } } + + def transform(attrs) + attrs.slice(:start_at, :finish_at).transform_values do |value| + value.to_date.to_s + end + end + + context :time_range do + def time_range(attrs = {}) + Person::Filter::Role.new(:role, transform(attrs)).time_range + end + + it 'sets min to beginning_of_time if missing' do + expect(time_range.min).to eq Time.zone.at(0).beginning_of_day + end + + it 'sets max to Date.today#end_of_day if missing' do + expect(time_range.max).to eq now.end_of_day + end + + it 'sets min to start_at#beginning_of_day' do + expect(time_range(start_at: now).min).to eq now.beginning_of_day + end + + it 'sets max to finish_at#end_of_day' do + expect(time_range(finish_at: now).max).to eq now.end_of_day + end + + it 'accepts start_at and finish_at on same day' do + range = time_range(start_at: now, finish_at: now) + expect(range.min).to eq now.beginning_of_day + expect(range.max).to eq now.end_of_day + end + + it 'min and max are nil if range is invalid' do + range = time_range(start_at: now, finish_at: 1.day.ago) + expect(range.min).to be_nil + expect(range.max).to be_nil + end + end + + context :filter do + def filter(attrs) + kind = described_class.to_s + filters = { role: transform(attrs).merge(role_type_ids: [role_type.id], kind: kind) } + Person::Filter::List.new(group, user, range: kind, filters: filters) + end + + context :created do + let(:role) { roles(:top_leader) } + let(:role_type) { Group::TopGroup::Leader } + + it 'finds role created on same day' do + role.update_columns(created_at: now) + expect(filter(start_at: now).entries).to have(1).item + end + + it 'finds role created within range' do + role.update_columns(created_at: now) + expect(filter(start_at: now, finish_at: now).entries).to have(1).item + end + + it 'does not find role created before start_at' do + role.update(created_at: 1.day.ago) + expect(filter(start_at: now).entries).to be_empty + end + + it 'does not find role created after finish_at' do + role.update_columns(created_at: 1.day.from_now) + expect(filter(finish_at: now).entries).to be_empty + end + + it 'does not find role when invalid range is given' do + role.update_columns(created_at: now, deleted_at: now) + expect(filter(start_at: now, finish_at: 1.day.ago).entries).to be_empty + end + + it 'does not find deleted role' do + role.update_columns(created_at: now, deleted_at: now) + expect(filter(start_at: now).entries).to be_empty + end + end + + context :deleted do + let(:role_type) { Group::TopGroup::Member } + let(:role) { person.roles.create!(type: role_type.sti_name, group: group) } + + it 'finds role deleted on same day' do + role.update(deleted_at: now) + expect(filter(start_at: now).entries).to have(1).item + end + + it 'finds role deleted within range' do + role.update(deleted_at: now) + expect(filter(start_at: now, finish_at: now).entries).to have(1).item + end + + it 'does not find role deleted before start_at' do + role.update(deleted_at: 1.day.ago) + expect(filter(start_at: now).entries).to be_empty + end + + it 'does not find role deleted after finish_at' do + role.update(deleted_at: 1.day.from_now) + expect(filter(finish_at: now).entries).to be_empty + end + + it 'does not find role deleted on same when invalid range is given' do + role.update(deleted_at: now) + expect(filter(start_at: now, finish_at: 1.day.ago).entries).to be_empty + end + + it 'does not find active role' do + role.update_columns(created_at: now) + expect(filter(start_at: now).entries).to be_empty + end + end + + context :active do + let(:role_type) { Group::TopGroup::Member } + let(:role) { person.roles.create!(type: role_type.sti_name, group: group) } + + it 'does not find role deleted before timeframe' do + role.update(deleted_at: 1.day.ago) + expect(filter(start_at: now).entries).to be_empty + end + + it 'does not find role created after timeframe' do + role.update(created_at: 1.day.from_now) + expect(filter(start_at: now).entries).to be_empty + end + + it 'finds role deleted within range' do + role.update(deleted_at: now) + expect(filter(start_at: now, finish_at: now).entries).to have(1).item + end + + it 'finds role created within range' do + role.update(created_at: now) + expect(filter(start_at: now, finish_at: now).entries).to have(1).item + end + end + end + end +end diff --git a/spec/domain/person/qualification_filter_spec.rb b/spec/domain/person/qualification_filter_spec.rb deleted file mode 100644 index e6af396b5d..0000000000 --- a/spec/domain/person/qualification_filter_spec.rb +++ /dev/null @@ -1,177 +0,0 @@ -# encoding: utf-8 - -# Copyright (c) 2012-2015, Pfadibewegung Schweiz. This file is part of -# hitobito and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito. - -require 'spec_helper' - -describe Person::QualificationFilter do - - let(:user) { people(:top_leader) } - let(:group) { groups(:top_layer) } - let(:kind) { nil } - let(:validity) { 'all' } - let(:qualification_kind_ids) { [] } - - let(:list_filter) do - Person::QualificationFilter.new(group, - user, - kind: kind, - qualification_kind_id: qualification_kind_ids, - validity: validity) - end - - let(:entries) { list_filter.filter_entries } - - let(:bl_leader) { create_person(Group::BottomLayer::Leader, :bottom_layer_one, 'reactivateable', :sl, :gl_leader) } - - before do - @tg_member = create_person(Group::TopGroup::Member, :top_group, 'active', :sl) - # duplicate qualification - Fabricate(:qualification, person: @tg_member, qualification_kind: qualification_kinds(:sl), start_at: Date.today - 2.weeks) - - @tg_extern = create_person(Role::External, :top_group, 'active', :sl) - - @bl_leader = bl_leader - @bl_extern = create_person(Role::External, :bottom_layer_one, 'reactivateable', :gl_leader) - - @bg_leader = create_person(Group::BottomGroup::Leader, :bottom_group_one_one, 'all', :sl, :ql) - @bg_member = create_person(Group::BottomGroup::Member, :bottom_group_one_one, 'active', :sl) - end - - def create_person(role, group, validity, *qualification_kinds) - person = Fabricate(role.name.to_sym, group: groups(group)).person - qualification_kinds.each do |key| - kind = qualification_kinds(key) - start = case validity - when 'active' then Date.today - when 'reactivateable' then Date.today - kind.validity.years - 1.year - else Date.today - 20.years - end - Fabricate(:qualification, person: person, qualification_kind: kind, start_at: start) - end - person - end - - context 'no filter' do - it 'loads only entries on group' do - expect(entries).to be_empty - end - - it 'count is 0' do - expect(list_filter.all_count).to eq(0) - end - end - - context 'kind deep' do - let(:kind) { 'deep' } - - context 'no qualification kinds' do - it 'loads only entries on group' do - expect(entries).to be_empty - end - - end - - context 'with qualification kinds' do - let(:qualification_kind_ids) { qualification_kinds(:sl, :gl_leader).collect(&:id) } - - it 'loads all entries in layer and below' do - expect(entries).to match_array([@tg_member, @tg_extern, @bl_leader, @bg_leader]) - end - - it 'contains only visible people' do - expect(entries.size).to eq(list_filter.all_count - 2) - end - end - end - - context 'kind layer' do - let(:kind) { 'layer' } - - context 'with qualification kinds' do - let(:qualification_kind_ids) { qualification_kinds(:sl, :gl_leader).collect(&:id) } - - it 'loads all entries in layer' do - expect(entries).to match_array([@tg_member, @tg_extern]) - end - - it 'contains all people' do - expect(entries.size).to eq(list_filter.all_count) - end - end - end - - context 'in bottom layer' do - let(:user) { bl_leader } - let(:kind) { 'layer' } - let(:group) { groups(:bottom_layer_one) } - let(:qualification_kind_ids) { qualification_kinds(:sl, :gl_leader).collect(&:id) } - - context 'active validities' do - - let(:validity) { 'active' } - - it 'loads matched entries' do - expect(entries).to match_array([@bg_member]) - end - - it 'contains all people' do - expect(entries.size).to eq(list_filter.all_count) - end - - context 'with infinite qualifications' do - let(:qualification_kind_ids) { qualification_kinds(:sl, :ql).collect(&:id) } - it 'contains them' do - expect(entries).to match_array([@bg_member, @bg_leader]) - end - end - - context 'as top leader' do - let(:user) { people(:top_leader) } - - it 'does not load non-visible entries' do - expect(entries).to match_array([]) - end - - it 'contains only visible people' do - expect(entries.size).to eq(list_filter.all_count - 1) - end - end - end - - context 'reactivateable validities' do - let(:validity) { 'reactivateable' } - - it 'loads matched entries' do - expect(entries).to match_array([@bg_member, @bl_extern, @bl_leader]) - end - - it 'contains all people' do - expect(entries.size).to eq(list_filter.all_count) - end - - context 'with infinite qualifications' do - let(:qualification_kind_ids) { qualification_kinds(:sl, :ql).collect(&:id) } - it 'contains them' do - expect(entries).to match_array([@bg_member, @bg_leader]) - end - end - end - - context 'all validities' do - let(:validity) { 'alll' } - - it 'loads matched entries' do - expect(entries).to match_array([@bg_member, @bl_extern, @bg_leader, @bl_leader]) - end - - it 'contains all people' do - expect(entries.size).to eq(list_filter.all_count) - end - end - end - -end \ No newline at end of file diff --git a/spec/domain/person/role_filter_spec.rb b/spec/domain/person/role_filter_spec.rb deleted file mode 100644 index a7295ab6f9..0000000000 --- a/spec/domain/person/role_filter_spec.rb +++ /dev/null @@ -1,112 +0,0 @@ -# encoding: utf-8 - -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of -# hitobito and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito. - -require 'spec_helper' - -describe Person::RoleFilter do - - let(:user) { people(:top_leader) } - let(:group) { groups(:top_group) } - let(:kind) { nil } - let(:role_types) { [] } - let(:role_type_ids_string) { role_types.collect(&:id).join(RelatedRoleType::Assigners::ID_URL_SEPARATOR) } - let(:list_filter) { Person::RoleFilter.new(group, user, kind: kind, role_type_ids: role_type_ids_string) } - - let(:entries) { list_filter.filter_entries } - - before do - @tg_member = Fabricate(Group::TopGroup::Member.name.to_sym, group: groups(:top_group)).person - Fabricate(:phone_number, contactable: @tg_member, number: '123', label: 'Privat', public: true) - Fabricate(:phone_number, contactable: @tg_member, number: '456', label: 'Mobile', public: false) - Fabricate(:social_account, contactable: @tg_member, name: 'facefoo', label: 'Facebook', public: true) - Fabricate(:social_account, contactable: @tg_member, name: 'skypefoo', label: 'Skype', public: false) - # duplicate role - Fabricate(Group::TopGroup::Member.name.to_sym, group: groups(:top_group), person: @tg_member) - @tg_extern = Fabricate(Role::External.name.to_sym, group: groups(:top_group)).person - - @bl_leader = Fabricate(Group::BottomLayer::Leader.name.to_sym, group: groups(:bottom_layer_one)).person - @bl_extern = Fabricate(Role::External.name.to_sym, group: groups(:bottom_layer_one)).person - - @bg_leader = Fabricate(Group::BottomGroup::Leader.name.to_sym, group: groups(:bottom_group_one_one)).person - @bg_member = Fabricate(Group::BottomGroup::Member.name.to_sym, group: groups(:bottom_group_one_one)).person - end - - context 'group' do - it 'loads all members of a group' do - expect(entries.collect(&:id)).to match_array([user, @tg_member].collect(&:id)) - end - - it 'contains all existing members' do - expect(entries.size).to eq(list_filter.all_count) - end - - context 'with external types' do - let(:role_types) { [Role::External] } - it 'loads externs of a group' do - expect(entries.collect(&:id)).to match_array([@tg_extern].collect(&:id)) - end - - it 'contains all existing externals' do - expect(entries.size).to eq(list_filter.all_count) - end - end - - context 'with specific types' do - let(:role_types) { [Role::External, Group::TopGroup::Member] } - it 'loads selected roles of a group' do - expect(entries.collect(&:id)).to match_array([@tg_member, @tg_extern].collect(&:id)) - end - - it 'contains all existing people' do - expect(entries.size).to eq(list_filter.all_count) - end - end - end - - context 'layer' do - let(:group) { groups(:bottom_layer_one) } - let(:kind) { 'layer' } - - context 'with layer and below full' do - let(:user) { @bl_leader } - - it 'loads group members when no types given' do - expect(entries.collect(&:id)).to match_array([people(:bottom_member), @bl_leader].collect(&:id)) - end - - context 'with specific types' do - let(:role_types) { [Group::BottomGroup::Member, Role::External] } - - it 'loads selected roles of a group when types given' do - expect(entries.collect(&:id)).to match_array([@bg_member, @bl_extern].collect(&:id)) - end - end - end - - end - - context 'deep' do - let(:group) { groups(:top_layer) } - let(:kind) { 'deep' } - - it 'loads group members when no types are given' do - expect(entries.collect(&:id)).to match_array([]) - end - - context 'with specific types' do - let(:role_types) { [Group::BottomGroup::Leader, Role::External] } - - it 'loads selected roles of a group when types given' do - expect(entries.collect(&:id)).to match_array([@bg_leader, @tg_extern].collect(&:id)) - end - - it 'contains not all existing people' do - expect(entries.size).to eq(list_filter.all_count - 1) - end - end - end -end diff --git a/spec/domain/search_strategies/sphinx_spec.rb b/spec/domain/search_strategies/sphinx_spec.rb new file mode 100644 index 0000000000..0b39a56f28 --- /dev/null +++ b/spec/domain/search_strategies/sphinx_spec.rb @@ -0,0 +1,259 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Hitobito AG. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +require 'spec_helper' + +describe SearchStrategies::Sphinx, :mysql do + + sphinx_environment(:people, :groups, :events) do + + before do + Rails.cache.clear + @tg_member = Fabricate(Group::TopGroup::Member.name.to_sym, group: groups(:top_group)).person + @tg_extern = Fabricate(Role::External.name.to_sym, group: groups(:top_group)).person + + @bl_leader = Fabricate(Group::BottomLayer::Leader.name.to_sym, group: groups(:bottom_layer_one)).person + @bl_extern = Fabricate(Role::External.name.to_sym, group: groups(:bottom_layer_one)).person + + @bg_leader = Fabricate(Group::BottomGroup::Leader.name.to_sym, + group: groups(:bottom_group_one_one), + person: Fabricate(:person, last_name: 'Schurter', first_name: 'Franz', town: 'St-Luc')).person + @bg_member = Fabricate(Group::BottomGroup::Member.name.to_sym, + group: groups(:bottom_group_one_one), + person: Fabricate(:person, last_name: 'Bindella', first_name: 'Yasmine')).person + + @bg_member_with_deleted = Fabricate(Group::BottomGroup::Member.name.to_sym, group: groups(:bottom_group_one_one)).person + leader = Fabricate(Group::BottomGroup::Leader.name.to_sym, group: groups(:bottom_group_one_one), person: @bg_member_with_deleted) + leader.update(created_at: Time.now - 1.year) + leader.destroy! + + @no_role = Fabricate(:person) + + role = Fabricate(Group::BottomGroup::Leader.name.to_sym, group: groups(:bottom_group_one_one)) + role.update(created_at: Time.now - 1.year) + role.destroy + @deleted_leader = role.person + + role = Fabricate(Group::BottomGroup::Member.name.to_sym, group: groups(:bottom_group_one_one)) + role.update(created_at: Time.now - 1.year) + role.destroy + @deleted_bg_member = role.person + + index_sphinx + end + + describe '#list_people' do + + context 'as admin' do + let(:user) { people(:top_leader) } + + it 'finds accessible person' do + result = strategy(@bg_leader.last_name[1..5]).list_people + + expect(result).to include(@bg_leader) + end + + it 'does not find not accessible person' do + result = strategy(@bg_member.last_name[1..5]).list_people + + expect(result).not_to include(@bg_member) + end + + it 'does not search for too short queries' do + result = strategy('e').list_people + + expect(result).to eq([]) + end + + it 'finds values with dashes' do + result = strategy('st-l').list_people + + expect(result.to_a).to eq([@bg_leader]) + end + + it 'finds people without any roles' do + result = strategy(@no_role.last_name[1..5]).list_people + + expect(result).to include(@no_role) + end + + it 'does not find people not accessible person with deleted role' do + result = strategy(@bg_member_with_deleted.last_name[1..5]).list_people + + expect(result).not_to include(@bg_member_with_deleted) + end + + it 'finds deleted people' do + result = strategy(@deleted_leader.last_name[1..5]).list_people + + expect(result).to include(@deleted_leader) + end + + it 'finds deleted, not accessible people' do + result = strategy(@deleted_bg_member.last_name[1..5]).list_people + + expect(result).to include(@deleted_bg_member) + end + + context 'without any params' do + it 'returns nothing' do + result = strategy.list_people + + expect(result).to eq([]) + end + end + end + + context 'as leader' do + let(:user) { @bl_leader } + + it 'finds accessible person' do + result = strategy(@bg_leader.last_name[1..5]).list_people + + expect(result).to include(@bg_leader) + end + + it 'finds local accessible person' do + result = strategy(@bg_member.last_name[1..5]).list_people + + expect(result).to include(@bg_member) + end + + it 'does not find people without any roles' do + result = strategy(@no_role.last_name[1..5]).list_people + + expect(result).not_to include(@no_role) + end + + it 'does not find deleted people' do + result = strategy(@deleted_leader.last_name[1..5]).list_people + + expect(result).not_to include(@deleted_leader) + end + end + + context 'as root' do + let(:user) { people(:root) } + + it 'finds every person' do + result = strategy(@bg_member.last_name[1..5]).list_people + + expect(result).to include(@bg_member) + end + end + + end + + describe '#query_people' do + + context 'as leader' do + let(:user) { people(:top_leader) } + + it 'finds accessible person' do + result = strategy(@bg_leader.last_name[1..5]).query_people + + expect(result).to include(@bg_leader) + end + + it 'does not find not accessible person' do + result = strategy(@bg_member.last_name[1..5]).query_people + + expect(result).not_to include(@bg_member) + end + + context 'without any params' do + it 'returns nothing' do + result = strategy.query_people + + expect(result).to eq(Person.none) + end + end + end + + context 'as unprivileged person' do + let(:user) { Fabricate(:person) } + + it 'finds zero people' do + result = strategy(@bg_member.last_name[1..5]).query_people + + expect(result).to eq(Person.none) + end + end + + end + + describe '#query_groups' do + + context 'as leader' do + let(:user) { people(:top_leader) } + + it 'finds groups' do + result = strategy(groups(:bottom_layer_one).to_s[1..5]).query_groups + + expect(result).to include(groups(:bottom_layer_one)) + end + + context 'without any params' do + it 'returns nothing' do + result = strategy.query_groups + + expect(result).to eq([]) + end + end + end + + context 'as unprivileged person' do + let(:user) { Fabricate(:person) } + + it 'finds groups' do + result = strategy(groups(:bottom_layer_one).to_s[1..5]).query_groups + + expect(result).to include(groups(:bottom_layer_one)) + end + end + + end + + describe '#query_events' do + + context 'as leader' do + let(:user) { people(:top_leader) } + + it 'finds events' do + result = strategy(events(:top_course).to_s[1..5]).query_events + + expect(result).to include(events(:top_course)) + end + + context 'without any params' do + it 'returns nothing' do + result = strategy.query_events + + expect(result).to eq([]) + end + end + end + + context 'as unprivileged person' do + let(:user) { Fabricate(:person) } + + it 'finds events' do + result = strategy(events(:top_course).to_s[1..5]).query_events + + expect(result).to include(events(:top_course)) + end + end + + end + + end + + def strategy(term = nil, page = nil) + SearchStrategies::Sphinx.new(user, term, page) + end + +end diff --git a/spec/domain/search_strategies/sql_spec.rb b/spec/domain/search_strategies/sql_spec.rb new file mode 100644 index 0000000000..4adc952c1c --- /dev/null +++ b/spec/domain/search_strategies/sql_spec.rb @@ -0,0 +1,259 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +require 'spec_helper' + +describe SearchStrategies::Sql do + + before do + Rails.cache.clear + @tg_member = Fabricate(Group::TopGroup::Member.name.to_sym, group: groups(:top_group)).person + @tg_extern = Fabricate(Role::External.name.to_sym, group: groups(:top_group)).person + + @bl_leader = Fabricate(Group::BottomLayer::Leader.name.to_sym, group: groups(:bottom_layer_one)).person + @bl_extern = Fabricate(Role::External.name.to_sym, group: groups(:bottom_layer_one)).person + + @bg_leader = Fabricate(Group::BottomGroup::Leader.name.to_sym, + group: groups(:bottom_group_one_one), + person: Fabricate(:person, last_name: 'Schurter', first_name: 'Franz')).person + @bg_member = Fabricate(Group::BottomGroup::Member.name.to_sym, + group: groups(:bottom_group_one_one), + person: Fabricate(:person, last_name: 'Bindella', first_name: 'Yasmine')).person + + @bg_member_with_deleted = Fabricate(Group::BottomGroup::Member.name.to_sym, group: groups(:bottom_group_one_one)).person + leader = Fabricate(Group::BottomGroup::Leader.name.to_sym, group: groups(:bottom_group_one_one), person: @bg_member_with_deleted) + leader.update(created_at: Time.now - 1.year) + leader.destroy! + + @no_role = Fabricate(:person) + + role = Fabricate(Group::BottomGroup::Leader.name.to_sym, group: groups(:bottom_group_one_one)) + role.update(created_at: Time.now - 1.year) + role.destroy + @deleted_leader = role.person + + role = Fabricate(Group::BottomGroup::Member.name.to_sym, group: groups(:bottom_group_one_one)) + role.update(created_at: Time.now - 1.year) + role.destroy + @deleted_bg_member = role.person + + # make sure names are long enough + [@tg_member, @tg_extern, @bl_leader, @bl_extern, @bg_leader, @bg_member, + @bg_member_with_deleted, @no_role, @deleted_leader, @deleted_bg_member].each do |p| + p.update_columns(last_name: p.last_name * 3, first_name: p.first_name * 3) + end + end + + describe '#list_people' do + + context 'as admin' do + let(:user) { people(:top_leader) } + + it 'finds accessible person' do + result = strategy(@bg_leader.last_name[1..5]).list_people + + expect(result).to include(@bg_leader) + end + + it 'finds accessible person with two terms' do + result = strategy("#{@bg_leader.last_name[1..5]} #{@bg_leader.first_name[0..3]}").list_people + + expect(result).to include(@bg_leader) + end + + it 'does not find not accessible person' do + result = strategy(@bg_member.last_name[1..5]).list_people + + expect(result).not_to include(@bg_member) + end + + it 'does not search for too short queries' do + result = strategy('e').list_people + + expect(result).to eq([]) + end + + it 'finds people without any roles' do + result = strategy(@no_role.last_name[1..5]).list_people + + expect(result).to include(@no_role) + end + + it 'does not find people not accessible person with deleted role' do + result = strategy(@bg_member_with_deleted.last_name[1..5]).list_people + + expect(result).not_to include(@bg_member_with_deleted) + end + + it 'finds deleted people' do + result = strategy(@deleted_leader.last_name[1..5]).list_people + + expect(result).to include(@deleted_leader) + end + + it 'finds deleted, not accessible people' do + result = strategy(@deleted_bg_member.last_name[1..5]).list_people + + expect(result).to include(@deleted_bg_member) + end + + context 'without any params' do + it 'returns nothing' do + result = strategy.list_people + + expect(result).to eq([]) + end + end + end + + context 'as leader' do + let(:user) { @bl_leader } + + it 'finds accessible person' do + result = strategy(@bg_leader.last_name[1..5]).list_people + + expect(result).to include(@bg_leader) + end + + it 'finds local accessible person' do + result = strategy(@bg_member.last_name[1..5]).list_people + + expect(result).to include(@bg_member) + end + + it 'does not find people without any roles' do + result = strategy(@no_role.last_name[1..5]).list_people + + expect(result).not_to include(@no_role) + end + + it 'does not find deleted people' do + result = strategy(@deleted_leader.last_name[1..5]).list_people + + expect(result).not_to include(@deleted_leader) + end + end + + context 'as root' do + let(:user) { people(:root) } + + it 'finds every person' do + result = strategy(@bg_member.last_name[1..5]).list_people + + expect(result).to include(@bg_member) + end + end + + end + + describe '#query_people' do + + context 'as leader' do + let(:user) { people(:top_leader) } + + it 'finds accessible person' do + result = strategy(@bg_leader.last_name[1..5]).query_people + + expect(result).to include(@bg_leader) + end + + it 'does not find not accessible person' do + result = strategy(@bg_member.last_name[1..5]).query_people + + expect(result).not_to include(@bg_member) + end + + context 'without any params' do + it 'returns nothing' do + result = strategy.query_people + + expect(result).to eq(Person.none) + end + end + end + + context 'as unprivileged person' do + let(:user) { Fabricate(:person) } + + it 'finds zero people' do + result = strategy(@bg_member.last_name[1..5]).query_people + + expect(result).to eq(Person.none) + end + end + + end + + describe '#query_groups' do + + context 'as leader' do + let(:user) { people(:top_leader) } + + it 'finds groups' do + result = strategy(groups(:bottom_layer_one).to_s[1..5]).query_groups + + expect(result).to include(groups(:bottom_layer_one)) + end + + context 'without any params' do + it 'returns nothing' do + result = strategy.query_groups + + expect(result).to eq([]) + end + end + end + + context 'as unprivileged person' do + let(:user) { Fabricate(:person) } + + it 'finds groups' do + result = strategy(groups(:bottom_layer_one).to_s[1..5]).query_groups + + expect(result).to include(groups(:bottom_layer_one)) + end + end + + end + + describe '#query_events' do + + context 'as leader' do + let(:user) { people(:top_leader) } + + it 'finds events' do + result = strategy(events(:top_course).to_s[1..5]).query_events + + expect(result).to include(events(:top_course)) + end + + context 'without any params' do + it 'returns nothing' do + result = strategy.query_events + + expect(result).to eq([]) + end + end + end + + context 'as unprivileged person' do + let(:user) { Fabricate(:person) } + + it 'finds events' do + result = strategy(events(:top_course).to_s[1..5]).query_events + + expect(result).to include(events(:top_course)) + end + end + + end + + def strategy(term = nil, page = nil) + SearchStrategies::Sql.new(user, term, page) + end + +end diff --git a/spec/fabricators/event_fabricator.rb b/spec/fabricators/event_fabricator.rb index cd2aefec4e..d5fb0565bc 100644 --- a/spec/fabricators/event_fabricator.rb +++ b/spec/fabricators/event_fabricator.rb @@ -37,6 +37,7 @@ # signature_confirmation_text :string # creator_id :integer # updater_id :integer +# applications_cancelable :boolean default(FALSE), not null # Fabricator(:event) do diff --git a/spec/fabricators/invoice_fabricator.rb b/spec/fabricators/invoice_fabricator.rb new file mode 100644 index 0000000000..ab894e66c9 --- /dev/null +++ b/spec/fabricators/invoice_fabricator.rb @@ -0,0 +1,16 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +Fabricator(:invoice) do + title { Faker::Name.name } +end + +Fabricator(:invoice_article) do + number { Faker::Number.hexadecimal(5).to_s.upcase } + name { Faker::Commerce.product_name } + unit_cost { Faker::Commerce.price } +end diff --git a/spec/fabricators/label_format_fabricator.rb b/spec/fabricators/label_format_fabricator.rb index a818f5270a..eddc6365c9 100644 --- a/spec/fabricators/label_format_fabricator.rb +++ b/spec/fabricators/label_format_fabricator.rb @@ -13,6 +13,9 @@ # count_vertical :integer not null # padding_top :float not null # padding_left :float not null +# person_id :integer +# nickname :boolean default(FALSE), not null +# pp_post :string(23) # # Copyright (c) 2014, Insieme Schweiz. This file is part of diff --git a/spec/features/event/application_market_controller_spec.rb b/spec/features/event/application_market_controller_spec.rb index 18637bf098..dbfd2efaf1 100644 --- a/spec/features/event/application_market_controller_spec.rb +++ b/spec/features/event/application_market_controller_spec.rb @@ -1,6 +1,6 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. @@ -74,7 +74,7 @@ describe 'requests are mutually undoable', js: true do context 'waiting_list' do - it 'starting from application' do + it 'starting from application', unstable: true do obsolete_node_safe do sign_in visit group_event_application_market_index_path(group.id, event.id) diff --git a/spec/features/events_controller_spec.rb b/spec/features/event/events_controller_spec.rb similarity index 100% rename from spec/features/events_controller_spec.rb rename to spec/features/event/events_controller_spec.rb diff --git a/spec/features/event/kinds_controller_spec.rb b/spec/features/event/kinds_controller_spec.rb new file mode 100644 index 0000000000..b29712ecff --- /dev/null +++ b/spec/features/event/kinds_controller_spec.rb @@ -0,0 +1,66 @@ +# encoding: utf-8 + +# Copyright (c) 2017 Pfadibewegung Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +require 'spec_helper' + +describe Event::KindsController, js: true do + + it 'may add new preconditions' do + obsolete_node_safe do + sign_in + visit edit_event_kind_path(event_kinds(:slk)) + + expect(page).to have_selector('.precondition-grouping', text: 'Group Lead') + + find('#add_precondition_grouping').click + + expect(page).to have_selector('select#event_kind_precondition_kind_ids') + select('Super Lead', from: 'event_kind_precondition_kind_ids') + select('Quality Lead', from: 'event_kind_precondition_kind_ids') + select('Group Lead (for Leaders)', from: 'event_kind_precondition_kind_ids') + find('#precondition_fields button').click + + expect(page).to have_selector('.precondition-grouping', text: 'Group Lead (for Leaders), Quality Lead und Super Lead') + + find('button.btn-primary').click + + expect(page).to have_selector('h1', text: 'Kursarten') + + grouped_ids = event_kinds(:slk).reload.grouped_qualification_kind_ids('precondition', 'participant') + expect(grouped_ids).to eq([[qualification_kinds(:gl).id], qualification_kinds(:gl_leader, :ql, :sl).map(&:id)]) + end + end + + it 'may remove preconditions' do + event_kinds(:slk).event_kind_qualification_kinds.create!( + qualification_kind: qualification_kinds(:ql), + category: 'precondition', + role: 'participant', + grouping: 2) + + obsolete_node_safe do + sign_in + visit edit_event_kind_path(event_kinds(:slk)) + + expect(page).to have_selector('.precondition-grouping', count: 2) + expect(page).to have_selector('.precondition-grouping', text: 'oder Quality Lead') + + find('.precondition-grouping:first-child .remove-precondition-grouping').click + expect(page).to have_selector('.precondition-grouping', text: 'Quality Lead') + + find('.precondition-grouping:first-child .remove-precondition-grouping').click + expect(page).to have_no_selector('.precondition-grouping') + + find('button.btn-primary').click + + expect(page).to have_selector('h1', text: 'Kursarten') + + expect(event_kinds(:slk).reload.qualification_kinds('precondition', 'participant').count).to eq(0) + end + end + +end diff --git a/spec/features/event/qualifications_controller_spec.rb b/spec/features/event/qualifications_controller_spec.rb deleted file mode 100644 index ac02b2da89..0000000000 --- a/spec/features/event/qualifications_controller_spec.rb +++ /dev/null @@ -1,63 +0,0 @@ -# encoding: utf-8 - -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of -# hitobito and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito. - -require 'spec_helper' - -describe Event::QualificationsController do - - let(:event) do - event = Fabricate(:course, kind: event_kinds(:slk)) - event.dates.create!(start_at: 10.days.ago, finish_at: 5.days.ago) - event - end - - let(:group) { event.groups.first } - - let(:participant_1) do - participation = Fabricate(:event_participation, event: event, active: true) - Fabricate(Event::Course::Role::Participant.name.to_sym, participation: participation) - participation - end - - let(:participant_2) do - participation = Fabricate(:event_participation, event: event, active: true) - Fabricate(Event::Course::Role::Participant.name.to_sym, participation: participation) - participation - end - - before do - # init required data - participant_1 - participant_2 - end - - it 'qualification requests are mutually undoable', js: true do - obsolete_node_safe do - sign_in - visit group_event_qualifications_path(group.id, event.id) - - appl_id = "#event_participation_#{participant_1.id}" - - # both links are active at begin - expect(find("#{appl_id} td.issue")).to have_selector('a i.icon-ok.disabled') - expect(find("#{appl_id} td.revoke")).to have_selector('a i.icon-remove.disabled') - - find("#{appl_id} td.issue a").click - expect(find("#{appl_id} td.issue")).to have_no_selector('a') - expect(find("#{appl_id} td.issue")).to have_no_selector('i.disabled') - expect(find("#{appl_id} td.revoke")).to have_selector('a') - expect(find("#{appl_id} td.revoke")).to have_selector('i.disabled') - - find("#{appl_id} td.revoke a").click - expect(find("#{appl_id} td.revoke")).to have_no_selector('a') - expect(find("#{appl_id} td.revoke")).to have_no_selector('i.disabled') - expect(find("#{appl_id} td.issue")).to have_selector('a') - expect(find("#{appl_id} td.issue")).to have_selector('i.disabled') - end - end - -end diff --git a/spec/features/group/deleted_people_controller_spec.rb b/spec/features/group/deleted_people_controller_spec.rb new file mode 100644 index 0000000000..9e9b230ef2 --- /dev/null +++ b/spec/features/group/deleted_people_controller_spec.rb @@ -0,0 +1,73 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Pfadibewegung Schweiz. +# This file is part of hitobito and licensed under the Affero General Public +# License version 3 or later. See the COPYING file at the top-level +# directory or at https://github.com/hitobito/hitobito. + + +require 'spec_helper' + + +describe Group::DeletedPeopleController, js: true do + + subject { page } + + context 'inline creation of role' do + + let(:group) { groups(:bottom_layer_one) } + let(:row) { find('#content table.table').all('tr').last } + let(:cell) { row.all('td')[2] } + let(:user) { people(:top_leader) } + + before do + Fabricate(Group::BottomLayer::Member.name.to_sym, + group: groups(:bottom_layer_one), + created_at: 1.year.ago, + deleted_at: 1.month.ago) + Fabricate(Group::BottomGroup::Leader.name.to_sym, + group: groups(:bottom_group_one_one_one), + created_at: 1.year.ago, + deleted_at: 1.month.ago) + + sign_in(user) + visit group_deleted_people_path(group_id: group.id) + within(cell) { click_link 'Bearbeiten' } + end + + + it 'cancel closes popover' do + obsolete_node_safe do + click_link 'Abbrechen' + expect(page).to have_no_css('.popover') + end + end + + it 'creates role' do + obsolete_node_safe do + find('#role_type_select a.chosen-single').click + find('#role_type_select ul.chosen-results').find('li', text: 'Leader').click + + click_button 'Speichern' + expect(page).to have_no_css('.popover') + expect(cell).to have_text 'Leader' + end + end + + it 'informs about missing type selection' do + obsolete_node_safe do + find('#role_group_id_chosen a.chosen-single').click + find('#role_group_id_chosen ul.chosen-results').find('li', text: 'Group 12').click + fill_in('role_label', with: 'dummy') + click_button 'Speichern' + expect(page).to have_selector('.popover .alert-error', text: 'Rolle muss ausgefüllt werden') + + find('#role_type_select a.chosen-single').click + find('#role_type_select ul.chosen-results').find('li', text: 'Leader').click + click_button 'Speichern' + expect(cell).to have_text 'Group 12' + end + end + end + +end diff --git a/spec/features/group/invoices_controller_spec.rb b/spec/features/group/invoices_controller_spec.rb new file mode 100644 index 0000000000..dc54dd0f4b --- /dev/null +++ b/spec/features/group/invoices_controller_spec.rb @@ -0,0 +1,100 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz, Pfadibewegung Schweiz. +# This file is part of hitobito and licensed under the Affero General Public +# License version 3 or later. See the COPYING file at the top-level +# directory or at https://github.com/hitobito/hitobito. + +require 'spec_helper' + +describe InvoicesController do + let(:group) { groups(:bottom_layer_one) } + let(:invoice) { invoices(:invoice) } + + it 'hides invoices link when person is not authorised' do + top_group_member = Fabricate(Group::TopGroup::Member.sti_name, group: groups(:top_group)) + sign_in(top_group_member.person) + visit root_path + expect(page.find('#page-navigation')).not_to have_link 'Rechnungen' + end + + context 'authenticated' do + let(:person) { people(:bottom_member) } + + before { sign_in(person) } + + it 'shows invoices link' do + visit root_path + expect(page.find('#page-navigation')).to have_link 'Rechnungen' + end + + it 'shows invoices subnav' do + visit group_invoices_path(group) + expect(page).to have_link 'Rechnungen' + expect(page).to have_css('nav.nav-left', text: 'Einstellungen') + end + + it 'updating invoice_item updates total', js: true do + visit group_people_path(group) + click_link 'Rechnung erstellen' + click_link 'Eintrag hinzufügen' + fill_in 'Preis', with: 1 + expect(page).to have_content 'Total inkl. MWSt. 1.00 CHF' + end + + it 'adding articles fills new invoice item', js: true do + visit group_people_path(group) + click_link 'Rechnung erstellen' + select 'BEI-JU', from: 'invoice_item_article' + expect(page).to have_content 'Total inkl. MWSt. 5.40 CHF' + + # TODO why does this part not execute success + # expect(page).to have_content 'ermässiger Beitrage für Kinder und Jugendliche' + end + + it 'creates payment reminders', js: true do + invoice = invoices(:sent) + visit group_invoice_path(group, invoice) + expect(page).not_to have_css('#new_payment_reminder') + click_link 'Mahnung erstellen' + fill_in 'Fällig am', with: invoice.due_at + 2.weeks + click_button 'Speichern' + expect(page).to have_content(/Mahnung.*wurde erfolgreich erstellt/) + expect(page).not_to have_css('#new_payment_reminder') + end + end + + context 'export single invoice' do + before do + sign_in(people(:bottom_member)) + visit group_invoice_path(group, invoice) + end + + it 'dropdown is available' do + expect(page).to have_link 'Export' + expect(page).to have_link 'Rechnung inkl. Einzahlungsschein' + expect(page).to have_link 'Rechnung separat' + expect(page).to have_link 'Einzahlungsschein separat' + end + + it 'exports full invoice' do + click_link('Export') + click_link('Rechnung inkl. Einzahlungsschein') + expect(page).to have_current_path("/groups/#{group.id}/invoices/#{invoice.id}.pdf") + end + + it 'exports only articles' do + click_link('Export') + click_link('Rechnung separat') + expect(page).to have_current_path("/groups/#{group.id}/invoices/#{invoice.id}.pdf?esr=false") + end + + it 'exports only esr' do + click_link('Export') + click_link('Einzahlungsschein separat') + expect(page).to have_current_path("/groups/#{group.id}/invoices/#{invoice.id}.pdf?articles=false") + end + end + +end + diff --git a/spec/features/label_formats_spec.rb b/spec/features/label_formats_spec.rb new file mode 100644 index 0000000000..eed0b3d551 --- /dev/null +++ b/spec/features/label_formats_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' + +describe LabelFormatsController, js: true do + + subject { page } + let(:user) { people(:top_leader) } + let(:toggle) { find('label[for=show_global_label_formats]') } + + before :each do + sign_in + visit label_formats_path + end + + def expect_global_to_be(state) + if state == :visible + is_expected.to have_selector('.global-formats', visible: true) + else + is_expected.to have_selector('.global-formats', visible: false) + end + end + + context 'if display global enabled' do + before :each do + user.update(show_global_label_formats: true) + end + + it 'displays global label formats' do + obsolete_node_safe do + expect_global_to_be :visible + end + end + + it 'hides global formats if switch is toggled' do + obsolete_node_safe do + toggle.click + expect_global_to_be :invisible + end + end + end + + context 'if display global is disabled' do + before :each do + user.update(show_global_label_formats: false) + end + + it 'displays global label formats' do + obsolete_node_safe do + expect_global_to_be :invisible + end + end + + it 'hides global formats if switch is toggled' do + obsolete_node_safe do + toggle.click + expect_global_to_be :visible + end + end + end +end diff --git a/spec/features/people_filter_spec.rb b/spec/features/people_filter_spec.rb deleted file mode 100644 index f778970f70..0000000000 --- a/spec/features/people_filter_spec.rb +++ /dev/null @@ -1,92 +0,0 @@ -# encoding: utf-8 - -# Copyright (c) 2012-2014, Jungwacht Blauring Schweiz, Pfadibewegung Schweiz. -# This file is part of hitobito and licensed under the Affero General Public -# License version 3 or later. See the COPYING file at the top-level -# directory or at https://github.com/hitobito/hitobito. - -require 'spec_helper' - -describe PeopleController, js: true do - - let(:group) { groups(:top_layer) } - - it 'may define role filter, display and edit it again' do - member = people(:bottom_member) - leader = Fabricate(Group::BottomLayer::Leader.name.to_sym, group: groups(:bottom_layer_two)).person - Fabricate(Group::BottomGroup::Leader.name.to_sym, group: groups(:bottom_group_one_two)) - - obsolete_node_safe do - sign_in_and_create_filter - - find("#people_filter_role_type_ids_#{Group::BottomLayer::Leader.id}").set(true) - find("#people_filter_role_type_ids_#{Group::BottomLayer::Member.id}").set(true) - fill_in('people_filter_name', with: 'Bottom Layer') - all('form .btn-toolbar').first.click_button('Suche speichern') - - expect(page).to have_selector('.table tbody tr', count: 2) - expect(page).to have_selector("#person_#{leader.id}") - expect(page).to have_selector("#person_#{member.id}") - - # edit the current filter - click_link 'Bottom Layer' - click_link 'Neuer Rollen Filter...' - - expect(page).to have_checked_field("people_filter_role_type_ids_#{Group::BottomLayer::Leader.id}") - expect(page).to have_checked_field("people_filter_role_type_ids_#{Group::BottomLayer::Member.id}") - - find("#people_filter_role_type_ids_#{Group::BottomLayer::Member.id}").set(false) - all('form .btn-toolbar').first.click_button('Suchen') - - expect(page).to have_selector('.table tbody tr', count: 1) - expect(page).to have_selector("tr#person_#{leader.id}") - - # open the previously defined filter again - click_link 'Eigener Filter' - click_link 'Bottom Layer' - - expect(page).to have_selector('.table tbody tr', count: 2) - - # open other tab - click_link 'Externe' - expect(page).to have_no_selector('.table-striped tbody tr') - end - end - - context 'toggling roles' do - it 'toggles roles when clicking layer' do - obsolete_node_safe do - sign_in_and_create_filter - - find('h4.filter-toggle', text: 'Top Layer').click - expect(page).to have_css('input:checked', count: 6) - - find('h4.filter-toggle', text: 'Top Layer').click - expect(page).to have_css('input:checked', count: 0) - end - end - - it 'toggles roles when clicking group' do - obsolete_node_safe do - sign_in_and_create_filter - - find('label.filter-toggle', text: 'Top Group').click - expect(page).to have_css('input:checked', count: 5) - - find('label.filter-toggle', text: 'Top Group').click - expect(page).to have_css('input:checked', count: 0) - end - end - end - - def sign_in_and_create_filter - sign_in - visit group_people_path(group) - expect(page).to have_no_selector('.table tbody tr') - - click_link 'Weitere Ansichten' - click_link 'Neuer Rollen Filter...' - - expect(page).to have_css('input:checked', count: 0) - end -end diff --git a/spec/features/people_controller_spec.rb b/spec/features/person/people_controller_spec.rb similarity index 100% rename from spec/features/people_controller_spec.rb rename to spec/features/person/people_controller_spec.rb diff --git a/spec/features/person/people_filter_spec.rb b/spec/features/person/people_filter_spec.rb new file mode 100644 index 0000000000..6ad100ed87 --- /dev/null +++ b/spec/features/person/people_filter_spec.rb @@ -0,0 +1,126 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2014, Jungwacht Blauring Schweiz, Pfadibewegung Schweiz. +# This file is part of hitobito and licensed under the Affero General Public +# License version 3 or later. See the COPYING file at the top-level +# directory or at https://github.com/hitobito/hitobito. + +require 'spec_helper' + +describe PeopleController, js: true do + + let(:group) { groups(:top_layer) } + + it 'may define role filter, display and edit it again' do + member = people(:bottom_member) + leader = Fabricate(Group::BottomLayer::Leader.name.to_sym, group: groups(:bottom_layer_two)).person + Fabricate(Group::BottomGroup::Leader.name.to_sym, group: groups(:bottom_group_one_two)) + + obsolete_node_safe do + sign_in_and_create_filter + + find("#filters_role_role_type_ids_#{Group::BottomLayer::Leader.id}").set(true) + find("#filters_role_role_type_ids_#{Group::BottomLayer::Member.id}").set(true) + fill_in('people_filter_name', with: 'Bottom Layer') + all('form .btn-toolbar').first.click_button('Suche speichern') + + expect(page).to have_selector('.table tbody tr', count: 2) + expect(page).to have_selector("#person_#{leader.id}") + expect(page).to have_selector("#person_#{member.id}") + + # edit the current filter + click_link 'Bottom Layer' + click_link 'Neuer Filter...' + click_link 'Rollen' + + expect(page).to have_checked_field("filters_role_role_type_ids_#{Group::BottomLayer::Leader.id}") + expect(page).to have_checked_field("filters_role_role_type_ids_#{Group::BottomLayer::Member.id}") + + find("#filters_role_role_type_ids_#{Group::BottomLayer::Member.id}").set(false) + all('form .btn-toolbar').first.click_button('Suchen') + + expect(page).to have_selector('.table tbody tr', count: 1) + expect(page).to have_selector("tr#person_#{leader.id}") + + # open the previously defined filter again + click_link 'Eigener Filter' + click_link 'Bottom Layer' + click_link 'Rollen' + + expect(page).to have_selector('.table tbody tr', count: 2) + + # open other tab + click_link 'Externe' + expect(page).to have_no_selector('.table-striped tbody tr') + end + end + + context 'toggling roles' do + it 'toggles roles when clicking layer' do + obsolete_node_safe do + sign_in_and_create_filter + + find('h4.filter-toggle', text: 'Top Layer').click + expect(page).to have_css('#roles input:checked', count: 6) + + find('h4.filter-toggle', text: 'Top Layer').click + expect(page).to have_css('#roles input:checked', count: 0) + end + end + + it 'toggles roles when clicking group' do + obsolete_node_safe do + sign_in_and_create_filter + + find('h5.filter-toggle', text: 'Top Group').click + expect(page).to have_css('#roles input:checked', count: 5) + + find('h5.filter-toggle', text: 'Top Group').click + expect(page).to have_css('#roles input:checked', count: 0) + end + end + + it 'toggles groups and layers when changing range' do + obsolete_node_safe do + sign_in + visit group_people_path(group, range: 'group') + + click_link 'Weitere Ansichten' + click_link 'Neuer Filter...' + click_link 'Rollen' + + expect(page).to have_no_selector('h4', text: 'Bottom Layer') + expect(page).to have_selector('h4', text: 'Top Layer') + expect(page).to have_selector('h5', text: 'Top Layer') + expect(page).to have_no_selector('h5', text: 'Top Group') + + find('#range_deep').set(true) + expect(page).to have_selector('h4', text: 'Bottom Layer') + + find('#range_group').set(true) + expect(page).to have_no_selector('h4', text: 'Bottom Layer') + expect(page).to have_selector('h4', text: 'Top Layer') + expect(page).to have_selector('h5', text: 'Top Layer') + expect(page).to have_no_selector('h5', text: 'Top Group') + + find('#range_layer').set(true) + expect(page).to have_no_selector('h4', text: 'Bottom Layer') + expect(page).to have_selector('h4', text: 'Top Layer') + expect(page).to have_selector('h5', text: 'Top Layer') + expect(page).to have_selector('h5', text: 'Top Group') + end + end + end + + def sign_in_and_create_filter + sign_in + visit group_people_path(group) + expect(page).to have_no_selector('.table tbody tr') + + click_link 'Weitere Ansichten' + click_link 'Neuer Filter...' + click_link 'Rollen' + + expect(page).to have_css('#roles .label-columns input:checked', count: 0) + end +end diff --git a/spec/features/person/person_notes_spec.rb b/spec/features/person/person_notes_spec.rb new file mode 100644 index 0000000000..111bb51142 --- /dev/null +++ b/spec/features/person/person_notes_spec.rb @@ -0,0 +1,77 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Dachverband Schweizer Jugendparlamente. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +require 'spec_helper' + +describe 'Person Notes', js: true do + + subject { page } + let(:group) { groups(:top_group) } + let(:leader) { Fabricate(Group::TopGroup::Leader.name.to_sym, group: group).person } + let(:secretary) { Fabricate(Group::TopGroup::LocalSecretary.name.to_sym, group: group).person } + let(:user) { leader } + let(:person) { people(:top_leader) } + + before do + sign_in(user) + end + + context 'creation' do + before do + visit group_person_path(group_id: group.id, id: person.id) + end + + it 'adds newly created notes' do + expect(page).to have_content('Keine Einträge gefunden') + + # open form + find('#notes-new-button').click + expect(page).to have_selector('#note_text') + fill_in('note_text', with: 'ladida') + + # cancel + find('#new_note .cancel').click + expect(page).to have_no_selector('#note_text') + + # open again + find('#notes-new-button').click + expect(page).to have_selector('#note_text', text: '') + + # submit without input + find('#new_note button').click + expect(page).to have_selector('#notes-error', text: 'Text muss ausgefüllt werden') + + # submit with input + expect do + fill_in('note_text', with: 'ladida') + find('#new_note button').click + expect(page).to have_no_content('Keine Einträge gefunden') + expect(page).to have_selector('#notes-list .note', count: 1) + end.to change { Note.count }.by(1) + end + end + + context 'deletion' do + before do + @n1 = group.notes.create!(text: 'foo', author: user) + @n2 = group.notes.create!(text: 'bar', author: user) + visit group_path(id: group.id) + end + + it 'removes deleted notes' do + expect(page).to have_selector('#notes-list .note', count: 2) + + expect do + accept_confirm do + find("#note_#{@n1.id} a[data-method=delete]").click + end + expect(page).to have_selector('#notes-list .note', count: 1) + end.to change { Note.count }.by(-1) + end + end + +end diff --git a/spec/features/person_tags_spec.rb b/spec/features/person/person_tags_spec.rb similarity index 97% rename from spec/features/person_tags_spec.rb rename to spec/features/person/person_tags_spec.rb index 039f03aa02..b7c841c929 100644 --- a/spec/features/person_tags_spec.rb +++ b/spec/features/person/person_tags_spec.rb @@ -1,9 +1,9 @@ # encoding: utf-8 # Copyright (c) 2012-2016, Dachverband Schweizer Jugendparlamente. This file is part of -# hitobito_dsj and licensed under the Affero General Public License version 3 +# hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito_dsj. +# https://github.com/hitobito/hitobito. require 'spec_helper' diff --git a/spec/features/person_typeahead_spec.rb b/spec/features/person/person_typeahead_spec.rb similarity index 100% rename from spec/features/person_typeahead_spec.rb rename to spec/features/person/person_typeahead_spec.rb diff --git a/spec/features/quicksearch_spec.rb b/spec/features/quicksearch_spec.rb index e49f86f1c3..e453c0f670 100644 --- a/spec/features/quicksearch_spec.rb +++ b/spec/features/quicksearch_spec.rb @@ -9,7 +9,7 @@ describe 'Quicksearch', :mysql do - sphinx_environment(:people, :groups) do + sphinx_environment(:people, :groups, :events) do it 'finds people and groups', js: true do obsolete_node_safe do index_sphinx @@ -22,6 +22,7 @@ expect(dropdown).to have_content('Top Leader, Supertown') expect(dropdown).to have_content('Top > TopGroup') expect(dropdown).to have_content('Top') + expect(dropdown).to have_content('Top: Top Course (TOP-007)') end end end diff --git a/spec/fixtures/event/kind_qualification_kinds.yml b/spec/fixtures/event/kind_qualification_kinds.yml index 573672ed21..7ad22ba4eb 100644 --- a/spec/fixtures/event/kind_qualification_kinds.yml +++ b/spec/fixtures/event/kind_qualification_kinds.yml @@ -2,7 +2,6 @@ # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. - # == Schema Information # # Table name: event_kind_qualification_kinds @@ -12,6 +11,7 @@ # qualification_kind_id :integer not null # category :string not null # role :string not null +# grouping :integer # slkgl_pre: diff --git a/spec/fixtures/events.yml b/spec/fixtures/events.yml index ec39601916..0e2e9102fa 100644 --- a/spec/fixtures/events.yml +++ b/spec/fixtures/events.yml @@ -35,17 +35,19 @@ # signature_confirmation_text :string # creator_id :integer # updater_id :integer +# applications_cancelable :boolean default(FALSE), not null # top_event: name: Top Event groups: top_layer - type: + type: top_course: name: Top Course groups: top_layer type: Event::Course + number: TOP-007 kind: slk priorization: true requires_approval: true diff --git a/spec/fixtures/groups.yml b/spec/fixtures/groups.yml index d57774f401..162255cbcc 100644 --- a/spec/fixtures/groups.yml +++ b/spec/fixtures/groups.yml @@ -35,6 +35,7 @@ top_layer: layer_group_id: <%= ActiveRecord::FixtureSet.identify(:top_layer) %> lft: 1 rgt: 18 + created_at: 2012-09-01 12:00:00 top_group: name: TopGroup diff --git a/spec/fixtures/invoice_articles.yml b/spec/fixtures/invoice_articles.yml new file mode 100644 index 0000000000..8da2a8394f --- /dev/null +++ b/spec/fixtures/invoice_articles.yml @@ -0,0 +1,50 @@ +# == Schema Information +# +# Table name: invoice_articles +# +# id :integer not null, primary key +# number :string(255) +# name :string(255) not null +# description :text(65535) +# category :string(255) +# unit_cost :decimal(12, 2) +# vat_rate :decimal(5, 2) +# cost_center :string(255) +# account :string(255) +# created_at :datetime not null +# updated_at :datetime not null +# group_id :integer not null +# + +beitrag: + group: bottom_layer_one + number: BEI-18 + name: Beitrag Erwachsene + description: normaler Beitrag für Erwachsene + category: Beiträge + unit_cost: 10 + vat_rate: 8 + cost_center: BEI + account: 23 + +ermaessigt: + group: bottom_layer_one + number: BEI-JU + name: Beitrag Kinder + description: ermässiger Beitrage für Kinder und Jugendliche + category: Beiträge + unit_cost: 5 + vat_rate: 8 + cost_center: BEI + account: 23 + +abo: + group: bottom_layer_one + number: ABO-NEWS + name: Abonnement der Mitgliederzeitschrift + description: monatliche Mitgliederzeitschrift + category: Publikationen + unit_cost: 120 + vat_rate: 8 + cost_center: PUB + account: 42 diff --git a/spec/fixtures/invoice_configs.yml b/spec/fixtures/invoice_configs.yml new file mode 100644 index 0000000000..774395408e --- /dev/null +++ b/spec/fixtures/invoice_configs.yml @@ -0,0 +1,20 @@ +# == Schema Information +# +# Table name: invoice_configs +# +# id :integer not null, primary key +# group_id :integer not null +# contact_id :integer +# sequence_number :integer default(1), not null +# due_days :integer default(30), not null +# address :text +# payment_information :text +# + +top_layer: + group: top_layer + sequence_number: 1 + +bottom_layer_one: + group: bottom_layer_one + sequence_number: 1 diff --git a/spec/fixtures/invoice_items.yml b/spec/fixtures/invoice_items.yml new file mode 100644 index 0000000000..17dab8d353 --- /dev/null +++ b/spec/fixtures/invoice_items.yml @@ -0,0 +1,12 @@ +pens: + invoice: invoice + name: pens + unit_cost: 1.5 + vat_rate: 0.08 + count: 3 + +pins: + invoice: invoice + name: pins + unit_cost: 0.5 + count: 1 diff --git a/spec/fixtures/invoices.yml b/spec/fixtures/invoices.yml new file mode 100644 index 0000000000..27732a3a0d --- /dev/null +++ b/spec/fixtures/invoices.yml @@ -0,0 +1,18 @@ +invoice: + title: Invoice + group: bottom_layer_one + recipient: top_leader + sequence_number: <%= ActiveRecord::FixtureSet.identify(:bottom_layer_one) %>-2 + esr_number: <%= ActiveRecord::FixtureSet.identify(:bottom_layer_one) %>-2 + total: 2 + +sent: + title: Sent + group: bottom_layer_one + recipient: top_leader + sequence_number: <%= ActiveRecord::FixtureSet.identify(:bottom_layer_one) %>-3 + esr_number: <%= ActiveRecord::FixtureSet.identify(:bottom_layer_one) %>-3 + sent_at: <%= 10.days.ago.to_date %> + due_at: <%= 20.days.from_now.to_date %> + state: sent + total: 2 diff --git a/spec/fixtures/label_formats.yml b/spec/fixtures/label_formats.yml index 6709f02d77..743fa7df8c 100644 --- a/spec/fixtures/label_formats.yml +++ b/spec/fixtures/label_formats.yml @@ -12,6 +12,9 @@ # count_vertical :integer not null # padding_top :float not null # padding_left :float not null +# person_id :integer +# nickname :boolean default(FALSE), not null +# pp_post :string(23) # # Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of diff --git a/spec/fixtures/people.yml b/spec/fixtures/people.yml index 8fd8c1bcd7..258e5911da 100644 --- a/spec/fixtures/people.yml +++ b/spec/fixtures/people.yml @@ -6,40 +6,41 @@ # # Table name: people # -# id :integer not null, primary key -# first_name :string -# last_name :string -# company_name :string -# nickname :string -# company :boolean default(FALSE), not null -# email :string -# address :string(1024) -# zip_code :string -# town :string -# country :string -# gender :string(1) -# birthday :date -# additional_information :text -# contact_data_visible :boolean default(FALSE), not null -# created_at :datetime -# updated_at :datetime -# encrypted_password :string -# reset_password_token :string -# reset_password_sent_at :datetime -# remember_created_at :datetime -# sign_in_count :integer default(0) -# current_sign_in_at :datetime -# last_sign_in_at :datetime -# current_sign_in_ip :string -# last_sign_in_ip :string -# picture :string -# last_label_format_id :integer -# creator_id :integer -# updater_id :integer -# primary_group_id :integer -# failed_attempts :integer default(0) -# locked_at :datetime -# authentication_token :string +# id :integer not null, primary key +# first_name :string +# last_name :string +# company_name :string +# nickname :string +# company :boolean default(FALSE), not null +# email :string +# address :string(1024) +# zip_code :string +# town :string +# country :string +# gender :string(1) +# birthday :date +# additional_information :text +# contact_data_visible :boolean default(FALSE), not null +# created_at :datetime +# updated_at :datetime +# encrypted_password :string +# reset_password_token :string +# reset_password_sent_at :datetime +# remember_created_at :datetime +# sign_in_count :integer default(0) +# current_sign_in_at :datetime +# last_sign_in_at :datetime +# current_sign_in_ip :string +# last_sign_in_ip :string +# picture :string +# last_label_format_id :integer +# creator_id :integer +# updater_id :integer +# primary_group_id :integer +# failed_attempts :integer default(0) +# locked_at :datetime +# authentication_token :string +# show_global_label_formats :boolean default(TRUE), not null # top_leader: diff --git a/spec/fixtures/qualification_kind_translations.yml b/spec/fixtures/qualification_kind_translations.yml index 832578c2a6..7e37d9d377 100644 --- a/spec/fixtures/qualification_kind_translations.yml +++ b/spec/fixtures/qualification_kind_translations.yml @@ -33,6 +33,12 @@ gl_leader_de: created_at: <%= Time.zone.now %> updated_at: <%= Time.zone.now %> +ql: + qualification_kind_id: <%= ActiveRecord::FixtureSet.identify(:ql) %> + locale: de + label: Quality Lead + created_at: <%= Time.zone.now %> + updated_at: <%= Time.zone.now %> old_de: qualification_kind_id: <%= ActiveRecord::FixtureSet.identify(:old) %> diff --git a/spec/helpers/action_helper_spec.rb b/spec/helpers/action_helper_spec.rb new file mode 100644 index 0000000000..5eef883e96 --- /dev/null +++ b/spec/helpers/action_helper_spec.rb @@ -0,0 +1,71 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +require 'spec_helper' + + +describe ActionHelper do + + include LayoutHelper + include I18nHelper + include UtilityHelper + include ActionHelper + include FormatHelper + include CrudTestHelper + + before(:all) do + reset_db + setup_db + create_test_data + end + + after(:all) { reset_db } + + + describe '#button_action_destroy' do + let(:entry) { people(:top_leader) } + + context 'without options' do + subject do + button_action_destroy + end + + it 'should contain person path' do + is_expected.to have_selector("a[href='/people/#{entry.id}']") + end + + it 'should have method delete' do + is_expected.to have_selector("a[data-method=delete]") + end + + it 'should have standard prompt' do + is_expected.to have_selector("a[data-confirm='#{ti(:confirm_delete)}']") + end + end + + context 'with options' do + it 'should override data-confirm' do + label = t('person.confirm_delete', person: entry) + button = button_action_destroy(nil, { data: { confirm: label }}) + expect(button).to have_selector("a[data-confirm='#{label}']") + expect(button).to have_selector("a[data-method='delete']") + end + + it 'should override data-method' do + button = button_action_destroy(nil, { data: { method: :put }}) + expect(button).to have_selector("a[data-method=put]") + expect(button).to have_selector("a[data-confirm='Wollen Sie diesen Eintrag wirklich löschen?']") + end + + it 'should override path' do + button = button_action_destroy('/sample_path') + expect(button).to have_selector("a[href='/sample_path']") + expect(button).to have_selector("a[data-method='delete']") + end + end + end +end diff --git a/spec/helpers/dropdown/people_export_spec.rb b/spec/helpers/dropdown/people_export_spec.rb index 30b83093b4..201b0bbf0c 100644 --- a/spec/helpers/dropdown/people_export_spec.rb +++ b/spec/helpers/dropdown/people_export_spec.rb @@ -1,6 +1,6 @@ # encoding: utf-8 -# Copyright (c) 2012-2014, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. @@ -41,8 +41,29 @@ def can?(*args) expect(pdf).to have_content 'Standard' end end + is_expected.to have_selector 'a' do |tag| + expect(tag).to have_content 'vCard' + end is_expected.to have_selector 'a' do |tag| expect(tag).to have_content 'E-Mail Adressen' end end + + context 'for global labels' do + before :each do + Fabricate(:label_format, name: 'SampleFormat') + end + + it 'includes global formats if Person#show_global_label_formats is true' do + user.update(show_global_label_formats: true) + + is_expected.to include 'SampleFormat' + end + + it 'excludes global formats if Person#show_global_label_formats is false' do + user.update(show_global_label_formats: false) + + is_expected.not_to include 'SampleFormat' + end + end end diff --git a/spec/helpers/filter_navigation/people_spec.rb b/spec/helpers/filter_navigation/people_spec.rb index 6e9b4fea69..e98eafee27 100644 --- a/spec/helpers/filter_navigation/people_spec.rb +++ b/spec/helpers/filter_navigation/people_spec.rb @@ -1,6 +1,6 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. @@ -14,24 +14,28 @@ allow(t).to receive_messages(can?: true) allow(t).to receive_messages(group_people_path: 'people_path') allow(t).to receive_messages(group_people_filter_path: 'people_filter_path') + allow(t).to receive_messages(edit_group_people_filter_path: 'edit_people_filter_path') allow(t).to receive_messages(new_group_people_filter_path: 'new_group_people_filter_path') - allow(t).to receive_messages(qualification_group_people_filters_path: 'qualification_group_people_filters_path') allow(t).to receive_messages(link_action_destroy: '') allow(t).to receive_messages(icon: '') allow(t).to receive_messages(ti: 'delete') + allow(t).to receive_messages(t: 'global.link.edit') + allow(t).to receive_messages(t: 'global.link.delete') + allow(t).to receive_messages(safe_join: ["", " ", "global.link.edit"]) + allow(t).to receive_messages(safe_join: ["", " ", "global.link.delete"]) allow(t).to receive(:link_to) { |label, path| "#{label}" } allow(t).to receive(:content_tag) { |tag, content, options| "<#{tag} #{options.inspect}>#{content}" } end end - subject { FilterNavigation::People.new(template, group, {}) } + subject { FilterNavigation::People.new(template, group, Person::Filter::List.new(group, nil)) } context 'top layer' do let(:group) { groups(:top_layer).decorate } let(:role_types) do - [Group::TopGroup::Leader.sti_name, - Group::BottomLayer::Leader.sti_name] + [Group::TopGroup::Leader, + Group::BottomLayer::Leader] end context 'without params' do @@ -40,7 +44,7 @@ its(:active_label) { should == 'Mitglieder' } its('dropdown.active') { should be_falsey } its('dropdown.label') { should == 'Weitere Ansichten' } - its('dropdown.items') { should have(4).items } + its('dropdown.items') { should have(3).items } it 'contains external item with count' do expect(subject.main_items.last).to match(/Externe \(0\)/) @@ -60,12 +64,12 @@ before do group.people_filters.create!(name: 'Leaders', - role_types: role_types) + filter_chain: { role: { role_types: role_types.map(&:id) } }) end its('dropdown.active') { should be_falsey } its('dropdown.label') { should == 'Weitere Ansichten' } - its('dropdown.items') { should have(5).items } + its('dropdown.items') { should have(4).items } end end @@ -74,16 +78,22 @@ before do group.people_filters.create!(name: 'Leaders', - role_types: role_types) + filter_chain: { role: { role_types: role_types.map(&:id) } }) end - subject { FilterNavigation::People.new(template, group, name: 'Leaders', role_type_ids: role_types) } + subject do + filter = Person::Filter::List.new(group, + nil, + name: 'Leaders', + filters: { role: { role_type_ids: role_types.map(&:id) } }) + FilterNavigation::People.new(template, group, filter) + end its(:main_items) { should have(2).items } its(:active_label) { should == nil } its('dropdown.active') { should be_truthy } its('dropdown.label') { should == 'Leaders' } - its('dropdown.items') { should have(5).item } + its('dropdown.items') { should have(4).item } end end @@ -103,7 +113,7 @@ context 'bottom group' do let(:group) { groups(:bottom_group_one_one).decorate } - its('dropdown.items') { should have(4).items } + its('dropdown.items') { should have(3).items } it 'entire sub groups contains only sub groups role types' do subject.dropdown.items.first.url =~ /#{[Role::External, diff --git a/spec/helpers/form_helper_spec.rb b/spec/helpers/form_helper_spec.rb index 4591b66cf0..f38cc37168 100644 --- a/spec/helpers/form_helper_spec.rb +++ b/spec/helpers/form_helper_spec.rb @@ -90,7 +90,7 @@ let(:entry) { crud_test_models(:AAAAA) } it { is_expected.to have_selector("form.special.form-horizontal[action='/crud_test_models/#{entry.id}'][method=post]") } - it { is_expected.to have_selector("input[name=_method][type=hidden][value=patch]") } + it { is_expected.to have_selector("input[name=_method][type=hidden][value=patch]", visible: false) } it { is_expected.to have_selector("input[name='crud_test_model[name]'][type=text][value=AAAAA]") } it { is_expected.to have_selector("input[name='crud_test_model[birthdate]'][type=text][value='01.01.1910']") } it { is_expected.to have_selector("input[name='crud_test_model[children]'][type=text][value='9']") } @@ -120,7 +120,7 @@ it { is_expected.to have_selector("div#error_explanation") } it { is_expected.to have_selector("div.control-group.error input[name='crud_test_model[name]'][type=text]") } - it { is_expected.to have_selector("input[name=_method][type=hidden][value=patch]") } + it { is_expected.to have_selector("input[name=_method][type=hidden][value=patch]", visible: false) } end end diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb new file mode 100644 index 0000000000..132e10afe1 --- /dev/null +++ b/spec/helpers/groups_helper_spec.rb @@ -0,0 +1,34 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +require 'spec_helper' + +describe GroupsHelper, type: :helper do + let(:button_label) { 'Export Button' } + + describe '#export_events_ical_button' do + it 'displays ical export button' do + expect(helper).to receive_messages(can?: true) + expect(I18n).to receive_messages(t: button_label) + expect(helper).to receive(:action_button) + .with(button_label, hash_including(format: :ics), :calendar) + .and_return(button_label) + expect(helper.export_events_ical_button).to eq(button_label) + end + end + + describe '#export_events_button' do + it 'displays csv export button' do + expect(helper).to receive_messages(can?: true) + expect(I18n).to receive_messages(t: button_label) + expect(helper).to receive(:action_button) + .with(button_label, hash_including(format: :csv), :download) + .and_return(button_label) + expect(helper.export_events_button).to eq(button_label) + end + end +end diff --git a/spec/helpers/sheet/group/nav_left_spec.rb b/spec/helpers/sheet/group/nav_left_spec.rb index f578f25f40..c0284136fa 100644 --- a/spec/helpers/sheet/group/nav_left_spec.rb +++ b/spec/helpers/sheet/group/nav_left_spec.rb @@ -22,7 +22,7 @@ def can?(*_args) true end - it { is_expected.to have_selector('li', count: 3) } + it { is_expected.to have_selector('li', count: 4) } it { is_expected.to have_selector('ul', count: 2) } @@ -97,6 +97,10 @@ def can?(*_args) is_expected.not_to have_link('Group 112') is_expected.not_to have_link('Group 121') end + + it 'displays deleted peoples' do + is_expected.to have_link(t('groups.global.link.deleted_person')) + end end context 'Group 11' do @@ -122,6 +126,10 @@ def can?(*_args) it 'hides decendents of ancestor siblings' do is_expected.not_to have_link('Group 121') end + + it 'displays deleted peoples' do + is_expected.to have_link(t('groups.global.link.deleted_person')) + end end context 'Group 111' do @@ -144,6 +152,10 @@ def can?(*_args) it 'hides decendents of ancestor siblings' do is_expected.not_to have_link('Group 121') end + + it 'displays deleted peoples' do + is_expected.to have_link(t('groups.global.link.deleted_person')) + end end context 'Group 1111' do @@ -159,6 +171,10 @@ def can?(*_args) it 'hides decendents of ancestor siblings' do is_expected.not_to have_link('Group 121') end + + it 'displays deleted peoples' do + is_expected.to have_link(t('groups.global.link.deleted_person')) + end end end diff --git a/spec/jobs/event/participation_confirmation_job_spec.rb b/spec/jobs/event/participation_confirmation_job_spec.rb index b73cedc2fc..56950b430d 100644 --- a/spec/jobs/event/participation_confirmation_job_spec.rb +++ b/spec/jobs/event/participation_confirmation_job_spec.rb @@ -97,7 +97,6 @@ expect(ActionMailer::Base.deliveries.size).to eq(2) - first_email = ActionMailer::Base.deliveries.first expect(last_email.to.to_set).to eq([app1.email, app2.email].to_set) end end diff --git a/spec/jobs/export/event_participations_export_job_spec.rb b/spec/jobs/export/event_participations_export_job_spec.rb new file mode 100644 index 0000000000..0fdd302998 --- /dev/null +++ b/spec/jobs/export/event_participations_export_job_spec.rb @@ -0,0 +1,106 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +require 'spec_helper' + +describe Export::EventParticipationsExportJob do + + subject { Export::EventParticipationsExportJob.new(format, user.id, event.id, params) } + + let(:participation) { event_participations(:top) } + let(:user) { participation.person } + let(:event) { participation.event } + let(:params) { { filter: 'all' } } + + before do + SeedFu.quiet = true + SeedFu.seed [Rails.root.join('db', 'seeds')] + end + + context 'creates a CSV-Export' do + let(:format) { :csv } + + it 'and sends it via mail' do + expect do + subject.perform + end.to change { ActionMailer::Base.deliveries.size }.by 1 + + expect(last_email.subject).to eq('Export der Event-Teilnehmer') + + lines = last_email.attachments.first.body.to_s.split("\n") + expect(lines.size).to eq(2) + expect(lines[0]).to match(/Vorname;Nachname;Übername;Firmenname;.*/) + expect(lines[0].split(';').count).to match(14) + end + + it 'send exports zipped if larger than 512kb' do + export = subject.export_file + expect(export).to receive(:size) { 1.megabyte } # trigger compression by faking the size + + expect do + subject.perform + end.to change { ActionMailer::Base.deliveries.size }.by 1 + + file = last_email.attachments.first + expect(file.content_type).to match(%r{application/zip}) + expect(file.content_type).to match(/filename=event_participations_export.zip/) + end + + it 'zips exports larger than 512kb' do + 20.times { Fabricate(:event_participation) } + expect_any_instance_of(Export::EventParticipationsExportJob) + .to receive(:entries) + .at_least(1).times + .and_return(Event::Participation.all) + + export = subject.export_file + export_size = export.size + expect(export).to receive(:size) { 1.megabyte } # trigger compression by faking the size + + file, format = subject.export_file_and_format + + expect(format).to eq :zip + expect(file.size).to be < export_size + end + end + + context 'creates a full CSV-Export' do + let(:format) { :csv } + let(:params) { { details: true } } + + it 'and sends it via mail' do + expect do + subject.perform + end.to change { ActionMailer::Base.deliveries.size }.by 1 + + expect(last_email.subject).to eq('Export der Event-Teilnehmer') + + lines = last_email.attachments.first.body.to_s.split("\n") + expect(lines.size).to eq(2) + expect(lines[0]).to match(/Vorname;Nachname;Firmenname;Übername.*/) + expect(lines[0]).to match(/;Bemerkungen \(Allgemeines.*/) + expect(lines[0].split(';').count).to match(17) + end + end + + context 'creates an Excel-Export' do + let(:format) { :xlsx } + + it 'and sends it via mail' do + expect do + subject.perform + end.to change { ActionMailer::Base.deliveries.size }.by 1 + + expect(last_email.subject).to eq('Export der Event-Teilnehmer') + + file = last_email.attachments.first + expect(file.content_type).to match(/officedocument.spreadsheetml.sheet/) + expect(file.content_type).to match(/filename=event_participations_export.xlsx/) + end + end + +end diff --git a/spec/jobs/export/events_export_job_spec.rb b/spec/jobs/export/events_export_job_spec.rb new file mode 100644 index 0000000000..79777718a9 --- /dev/null +++ b/spec/jobs/export/events_export_job_spec.rb @@ -0,0 +1,81 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +require 'spec_helper' + +describe Export::EventsExportJob do + + subject { Export::EventsExportJob.new(format, user.id, nil, year, group) } + + let(:user) { people(:top_leader) } + let(:group) { groups(:top_layer) } + let(:year) { 2012 } + + before do + SeedFu.quiet = true + SeedFu.seed [Rails.root.join('db', 'seeds')] + Fabricate(:event) + end + + context 'creates a CSV-Export' do + let(:format) { :csv } + + it 'and sends it via mail' do + expect do + subject.perform + end.to change { ActionMailer::Base.deliveries.size }.by 1 + + expect(last_email.subject).to eq('Export der Anlässe') + + lines = last_email.attachments.first.body.to_s.split("\n") + expect(lines.size).to eq(3) + expect(lines[0]).to match(/Name;Organisatoren;Beschreibung;.*/) + expect(lines[0].split(';').count).to match(34) + end + + it 'send exports zipped if larger than 512kb' do + export = subject.export_file + expect(export).to receive(:size) { 1.megabyte } # trigger compression by faking the size + + expect do + subject.perform + end.to change { ActionMailer::Base.deliveries.size }.by 1 + + file = last_email.attachments.first + expect(file.content_type).to match(%r{application/zip}) + expect(file.content_type).to match(/filename=events_export.zip/) + end + + it 'zips exports larger than 512kb' do + export = subject.export_file + export_size = export.size + expect(export).to receive(:size) { 1.megabyte } # trigger compression by faking the size + + file, format = subject.export_file_and_format + + expect(format).to eq :zip + expect(file.size).to be < export_size + end + end + + context 'creates an Excel-Export' do + let(:format) { :xlsx } + + it 'and sends it via mail' do + expect do + subject.perform + end.to change { ActionMailer::Base.deliveries.size }.by 1 + + expect(last_email.subject).to eq('Export der Anlässe') + + file = last_email.attachments.first + expect(file.content_type).to match(/officedocument.spreadsheetml.sheet/) + expect(file.content_type).to match(/filename=events_export.xlsx/) + end + end + +end diff --git a/spec/jobs/export/people_export_job_spec.rb b/spec/jobs/export/people_export_job_spec.rb new file mode 100644 index 0000000000..6364c7e2f8 --- /dev/null +++ b/spec/jobs/export/people_export_job_spec.rb @@ -0,0 +1,104 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +require 'spec_helper' + +describe Export::PeopleExportJob do + + subject { Export::PeopleExportJob.new(format, full, user.id, person_filter) } + + let(:user) { Fabricate(Group::BottomLayer::Leader.name.to_sym, group: group).person } + let(:person_filter) { Person::Filter::List.new(group, user) } + let(:group) { groups(:bottom_layer_one) } + + before do + SeedFu.quiet = true + SeedFu.seed [Rails.root.join('db', 'seeds')] + + end + + context 'creates a CSV-Export' do + let(:format) { :csv } + let(:full) { false } + + it 'and sends it via mail' do + expect do + subject.perform + end.to change { ActionMailer::Base.deliveries.size }.by 1 + + expect(last_email.subject).to eq('Export der Personen') + + lines = last_email.attachments.first.body.to_s.split("\n") + expect(lines.size).to eq(3) + expect(lines[0]).to match(/Vorname;Nachname;.*/) + expect(lines[0].split(';').count).to match(14) + end + + it 'send exports zipped if larger than 512kb' do + export = subject.export_file + expect(export).to receive(:size) { 1.megabyte } # trigger compression by faking the size + + expect do + subject.perform + end.to change { ActionMailer::Base.deliveries.size }.by 1 + + file = last_email.attachments.first + expect(file.content_type).to match(%r!application/zip!) + expect(file.content_type).to match(/filename=people_export.zip/) + end + + it 'zips exports larger than 512kb' do + 10.times { Fabricate(Group::BottomLayer::Member.name.to_sym, group: group) } # create a few entries to make zipping worth it. + + export = subject.export_file + export_size = export.size + expect(export).to receive(:size) { 1.megabyte } # trigger compression by faking the size + + file, format = subject.export_file_and_format + + expect(format).to eq :zip + expect(file.size).to be < export_size + end + end + + context 'creates a full CSV-Export' do + let(:format) { :csv } + let(:full) { true } + + it 'and sends it via mail' do + expect do + subject.perform + end.to change { ActionMailer::Base.deliveries.size }.by 1 + + expect(last_email.subject).to eq('Export der Personen') + + lines = last_email.attachments.first.body.to_s.split("\n") + expect(lines.size).to eq(3) + expect(lines[0]).to match(/Vorname;Nachname;.*/) + expect(lines[0]).to match(/Zusätzliche Angaben;.*/) + expect(lines[0].split(';').count).not_to match(14) + end + end + + context 'creates an Excel-Export' do + let(:format) { :xlsx } + let(:full) { false } + + it 'and sends it via mail' do + expect do + subject.perform + end.to change { ActionMailer::Base.deliveries.size }.by 1 + + expect(last_email.subject).to eq('Export der Personen') + + file = last_email.attachments.first + expect(file.content_type).to match(/officedocument.spreadsheetml.sheet/) + expect(file.content_type).to match(/filename=people_export.xlsx/) + end + end + +end diff --git a/spec/jobs/export/subscriptions_job_spec.rb b/spec/jobs/export/subscriptions_job_spec.rb new file mode 100644 index 0000000000..f642f0ee4d --- /dev/null +++ b/spec/jobs/export/subscriptions_job_spec.rb @@ -0,0 +1,86 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +require 'spec_helper' + +describe Export::SubscriptionsJob do + + subject { Export::SubscriptionsJob.new(format, mailing_list.id, user.id) } + + let(:mailing_list) { mailing_lists(:info) } + let(:user) { people(:top_leader)} + + let(:group) { groups(:top_layer) } + let(:mailing_list) { Fabricate(:mailing_list, group: group) } + + before do + SeedFu.quiet = true + SeedFu.seed [Rails.root.join('db', 'seeds')] + + Fabricate(:subscription, mailing_list: mailing_list) + Fabricate(:subscription, mailing_list: mailing_list) + end + + context 'creates an CSV-Export' do + let(:format) { :csv } + + it 'and sends it via mail' do + expect do + subject.perform + end.to change { ActionMailer::Base.deliveries.size }.by 1 + + expect(last_email.subject).to eq('Export der Abonnenten') + + lines = last_email.attachments.first.body.to_s.split("\n") + expect(lines.size).to eq(3) + expect(lines[0]).to match(/Vorname;Nachname;.*/) + end + + it 'send exports zipped if larger than 512kb' do + export = subject.export_file + expect(export).to receive(:size) { 1.megabyte } # trigger compression by faking the size + + expect do + subject.perform + end.to change { ActionMailer::Base.deliveries.size }.by 1 + + file = last_email.attachments.first + expect(file.content_type).to match(%r!application/zip!) + expect(file.content_type).to match(/filename=subscriptions.zip/) + end + + it 'zips exports larger than 512kb' do + 10.times { Fabricate(:subscription, mailing_list: mailing_list) } # create a few entries to make zipping worth it. + + export = subject.export_file + export_size = export.size + expect(export).to receive(:size) { 1.megabyte } # trigger compression by faking the size + + file, format = subject.export_file_and_format + + expect(format).to eq :zip + expect(file.size).to be < export_size + end + end + + context 'creates an Excel-Export' do + let(:format) { :xlsx } + + it 'and sends it via mail' do + expect do + subject.perform + end.to change { ActionMailer::Base.deliveries.size }.by 1 + + expect(last_email.subject).to eq('Export der Abonnenten') + + file = last_email.attachments.first + expect(file.content_type).to match(/officedocument.spreadsheetml.sheet/) + expect(file.content_type).to match(/filename=subscriptions.xlsx/) + end + end + +end diff --git a/spec/jobs/person/send_add_request_job_spec.rb b/spec/jobs/person/send_add_request_job_spec.rb index 677eeeefc0..a3583a78d1 100644 --- a/spec/jobs/person/send_add_request_job_spec.rb +++ b/spec/jobs/person/send_add_request_job_spec.rb @@ -32,7 +32,7 @@ job.perform end - it 'sends no email to person if it has a login password' do + it 'sends no email to person if it has no login password' do person.update_column(:encrypted_password, nil) expect(Person::AddRequestMailer).not_to receive(:ask_person_to_add) expect(Person::AddRequestMailer).not_to receive(:ask_responsibles) @@ -43,7 +43,19 @@ r1 = Fabricate(Group::BottomLayer::Leader.name, group: groups(:bottom_layer_two)).person r2 = Fabricate(Group::BottomLayer::LocalGuide.name, group: groups(:bottom_layer_two)).person Fabricate(Group::BottomLayer::LocalGuide.name, group: groups(:bottom_layer_two), person: r1) - g = r1.primary_group + + mail = double('mail') + expect(mail).to receive(:deliver_now) + expect(Person::AddRequestMailer).to receive(:ask_responsibles).with(request, [r1, r2]).and_return(mail) + job.perform + end + + it 'sends email to last resposibles if person has no roles' do + person.roles.first.update!(created_at: 1.year.ago) + person.roles.first.destroy! + r1 = Fabricate(Group::BottomLayer::Leader.name, group: groups(:bottom_layer_two)).person + r2 = Fabricate(Group::BottomLayer::LocalGuide.name, group: groups(:bottom_layer_two)).person + Fabricate(Group::BottomLayer::LocalGuide.name, group: groups(:bottom_layer_two), person: r1) mail = double('mail') expect(mail).to receive(:deliver_now) diff --git a/spec/jobs/sphinx_index_job_spec.rb b/spec/jobs/sphinx_index_job_spec.rb new file mode 100644 index 0000000000..5d7130d661 --- /dev/null +++ b/spec/jobs/sphinx_index_job_spec.rb @@ -0,0 +1,22 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +require 'spec_helper' + +describe SphinxIndexJob do + + subject { SphinxIndexJob.new } + + it 'disables job if sphinx running on external host' do + SphinxIndexJob.new.schedule + expect(Hitobito::Application).to receive(:sphinx_local?).twice.and_return(false) + expect do + subject.perform + end.to change { Delayed::Job.count }.by(-1) + end + +end diff --git a/spec/mailers/event/participation_mailer_spec.rb b/spec/mailers/event/participation_mailer_spec.rb index 93a0007e16..bce706980b 100644 --- a/spec/mailers/event/participation_mailer_spec.rb +++ b/spec/mailers/event/participation_mailer_spec.rb @@ -60,12 +60,15 @@ is_expected.to match(%r{Top Leader

Supertown

top_leader@example.com}) end - it 'renders questions if present' do + it 'renders application questions if present' do question = event_questions(:top_ov) event.questions << event_questions(:top_ov) + question2 = event.questions.create!(question: 'foo', admin: true) participation.answers.detect { |a| a.question_id == question.id }.update!(answer: 'GA') + participation.answers.detect { |a| a.question_id == question2.id }.update!(answer: 'Bar') is_expected.to match(%r{Fragen:.*GA}) + is_expected.not_to match(%r{Fragen:.*Bar}) end end @@ -114,4 +117,31 @@ it { is_expected.to match(/Hallo firsty, lasty/) } it { is_expected.to match(/Top Leader hat sich/) } end + + describe '#cancel' do + let(:mail) { Event::ParticipationMailer.cancel(event, person) } + subject { mail.body } + + it 'renders dates if set' do + event.dates.clear + event.dates.build(label: 'Vorweekend', start_at: Date.parse('2012-10-18'), finish_at: Date.parse('2012-10-21')) + is_expected.to match(/Daten:Vorweekend: 18.10.2012 - 21.10.2012/) + end + + it 'renders multiple dates below each other' do + event.dates.clear + event.dates.build(label: 'Vorweekend', start_at: Date.parse('2012-10-18'), finish_at: Date.parse('2012-10-21')) + event.dates.build(label: 'Anlass', start_at: Date.parse('2012-10-21')) + is_expected.to match(/Daten:Vorweekend: 18.10.2012 - 21.10.2012Anlass: 21.10.2012/) + end + + it 'renders the headers' do + expect(mail.subject).to eq 'Bestätigung der Abmeldung' + expect(mail.to).to eq(['top_leader@example.com']) + expect(mail.from).to eq(['noreply@localhost']) + end + + it { is_expected.to match(/Hallo Top/) } + + end end diff --git a/spec/models/acts_as_taggable_on/tag_spec.rb b/spec/models/acts_as_taggable_on/tag_spec.rb index 05a0e040b6..4296538f99 100644 --- a/spec/models/acts_as_taggable_on/tag_spec.rb +++ b/spec/models/acts_as_taggable_on/tag_spec.rb @@ -1,9 +1,9 @@ # encoding: utf-8 # Copyright (c) 2012-2016, Dachverband Schweizer Jugendparlamente. This file is part of -# hitobito_dsj and licensed under the Affero General Public License version 3 +# hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito_dsj. +# https://github.com/hitobito/hitobito. require 'spec_helper' diff --git a/spec/models/custom_content_spec.rb b/spec/models/custom_content_spec.rb index 459f579c86..638815d79c 100644 --- a/spec/models/custom_content_spec.rb +++ b/spec/models/custom_content_spec.rb @@ -1,4 +1,10 @@ # encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + # == Schema Information # # Table name: custom_contents @@ -9,10 +15,6 @@ # placeholders_optional :string # -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of -# hitobito and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito. require 'spec_helper' describe CustomContent do @@ -59,6 +61,13 @@ it 'succeeds if all required placeholders are used' do is_expected.to be_valid end + + it 'succeeds if placeholder is used in subject' do + subject.placeholders_required = 'login-url, sender' + subject.subject = "Mail from {sender}" + + is_expected.to be_valid + end end context '#body_with_values' do @@ -87,4 +96,34 @@ end end + context '#subject_with_values' do + it 'replaces all placeholders' do + subject.subject = 'New Login for {user} at {login-url}' + output = subject.subject_with_values('user' => 'Fred', 'login-url' => 'example.com/login') + expect(output).to eq('New Login for Fred at example.com/login') + end + + it 'handles contents without placeholders' do + subject.subject = 'Hi There' + output = subject.subject_with_values + expect(output).to eq('Hi There') + end + + it 'raises an error if placeholder is missing' do + subject.subject = 'Your new Login at {login-url}' + expect { subject.subject_with_values('user' => 'Fred') }.to raise_error(KeyError) + end + + it 'raises an error if non-defined placeholder is given' do + subject.subject = 'Your new Login' + expect { subject.subject_with_values('foo' => 'bar') }.to raise_error(ArgumentError) + end + + it 'does not care about unused placeholders' do + subject.subject = 'Your new Login at {login-url}' + output = subject.subject_with_values('user' => 'Fred', 'login-url' => 'example.com/login') + expect(output).to eq('Your new Login at example.com/login') + end + end + end diff --git a/spec/models/duration_spec.rb b/spec/models/duration_spec.rb index 68ba0dc50a..3522739d00 100644 --- a/spec/models/duration_spec.rb +++ b/spec/models/duration_spec.rb @@ -74,27 +74,27 @@ context 'by date' do it 'today is active' do - @duration = Duration.new(Date.today, Date.today) + @duration = Duration.new(Time.zone.today, Time.zone.today) is_expected.to be_active end it 'until today is active' do - @duration = Duration.new(Date.today - 10.days, Date.today) + @duration = Duration.new(Time.zone.today - 10.days, Time.zone.today) is_expected.to be_active end it 'from today is active' do - @duration = Duration.new(Date.today, Date.today + 10.days) + @duration = Duration.new(Time.zone.today, Time.zone.today + 10.days) is_expected.to be_active end it 'from tomorrow is not active' do - @duration = Duration.new(Date.today + 1.day, Date.today + 10.days) + @duration = Duration.new(Time.zone.today + 1.day, Time.zone.today + 10.days) is_expected.not_to be_active end it 'until yesterday is not active' do - @duration = Duration.new(Date.today - 10.days, Date.today - 1.day) + @duration = Duration.new(Time.zone.today - 10.days, Time.zone.today - 1.day) is_expected.not_to be_active end end diff --git a/spec/models/event/course_spec.rb b/spec/models/event/course_spec.rb index c37636e1c9..8d5cc6fe5a 100644 --- a/spec/models/event/course_spec.rb +++ b/spec/models/event/course_spec.rb @@ -98,4 +98,36 @@ def add_date(start_at, event = subject) expect(course.signature_confirmation).to be_truthy end + context '#duplicate' do + + let(:event) { events(:top_course) } + + it 'resets participant counts' do + d = event.duplicate + expect(d.participant_count).to eq(0) + expect(d.teamer_count).to eq(0) + expect(d.applicant_count).to eq(0) + end + + it 'resets state' do + d = event.duplicate + expect(d.state).to be_nil + end + + it 'keeps empty questions' do + event.questions = [] + d = event.duplicate + expect(d.application_questions.size).to eq(0) + end + + it 'copies existing questions' do + d = event.duplicate + expect do + d.dates << Fabricate.build(:event_date, event: d) + d.save! + end.to change { Event::Question.count }.by(3) + end + + end + end diff --git a/spec/models/event/participation_contact_data_spec.rb b/spec/models/event/participation_contact_data_spec.rb new file mode 100644 index 0000000000..85f8bfe713 --- /dev/null +++ b/spec/models/event/participation_contact_data_spec.rb @@ -0,0 +1,64 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Pfadibewegung Schweiz. This file is part of +# hitobito_youth and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito_youth. + +require 'spec_helper' + +describe Event::ParticipationContactData do + + let(:event) { events(:top_event) } + let(:person) { people(:top_leader) } + + let(:attributes) do + h = ActiveSupport::HashWithIndifferentAccess.new + h.merge({ first_name: 'John', last_name: 'Gonzales', + email: 'top_leader@example.com', + nickname: '' }) + end + + context 'validations' do + + it 'validates contact attributes' do + contact_data = participation_contact_data(attributes) + event.update!(required_contact_attrs: ['nickname']) + + expect(contact_data.valid?).to be false + expect(contact_data.errors.full_messages.first).to eq('Übername muss ausgefüllt werden') + end + + it 'validates person attributes' do + attrs = attributes + attrs[:birthday] = 'invalid' + contact_data = participation_contact_data(attrs) + + expect(contact_data.valid?).to be false + expect(contact_data.errors.full_messages.first).to eq('Geburtstag ist kein gültiges Datum') + end + + end + + context 'update person data' do + + it 'updates person attributes' do + contact_data = participation_contact_data(attributes) + + contact_data.save + + person.reload + + expect(person.first_name).to eq('John') + expect(person.last_name).to eq('Gonzales') + end + + end + + private + + def participation_contact_data(attributes) + Event::ParticipationContactData.new(event, person, attributes) + end + +end diff --git a/spec/models/event/participation_spec.rb b/spec/models/event/participation_spec.rb index 7e6f832078..3a6c234155 100644 --- a/spec/models/event/participation_spec.rb +++ b/spec/models/event/participation_spec.rb @@ -28,12 +28,18 @@ context '#init_answers' do subject { course.participations.new } - context do - before { subject.init_answers } + it 'creates answers from event' do + subject.init_answers + expect(subject.answers.collect(&:question).to_set).to eq(course.questions.to_set) + end - it 'creates answers from event' do - expect(subject.answers.collect(&:question).to_set).to eq(course.questions.to_set) - end + it 'creates missing answers' do + subject.answers_attributes = [{ question_id: course.questions.first.id, answer: 'Foo' }] + subject.init_answers + + expect(subject.answers.size).to eq(2) + expect(subject.answers.first.answer).to eq('Foo') + expect(subject.answers.last.answer).to be_blank end it 'does not save associations in database' do diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index f4befe50f3..422da91f59 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -82,7 +82,7 @@ end context 'with closing date in the future' do - before { subject.application_closing_at = Date.today + 1 } + before { subject.application_closing_at = Time.zone.today + 1 } it 'is open without maximum participant' do is_expected.to be_application_possible @@ -97,7 +97,7 @@ end context 'with closing date today' do - before { subject.application_closing_at = Date.today } + before { subject.application_closing_at = Time.zone.today } it 'is open without maximum participant' do is_expected.to be_application_possible @@ -111,7 +111,7 @@ end context 'with closing date in the past' do - before { subject.application_closing_at = Date.today - 1 } + before { subject.application_closing_at = Time.zone.today - 1 } it 'is closed without maximum participant' do is_expected.not_to be_application_possible @@ -126,7 +126,7 @@ context 'with opening date in the past' do - before { subject.application_opening_at = Date.today - 1 } + before { subject.application_opening_at = Time.zone.today - 1 } it 'is open without maximum participant' do is_expected.to be_application_possible @@ -140,7 +140,7 @@ end context 'with opening date today' do - before { subject.application_opening_at = Date.today } + before { subject.application_opening_at = Time.zone.today } it 'is open without maximum participant' do is_expected.to be_application_possible @@ -154,7 +154,7 @@ end context 'with opening date in the future' do - before { subject.application_opening_at = Date.today + 1 } + before { subject.application_opening_at = Time.zone.today + 1 } it 'is closed without maximum participant' do is_expected.not_to be_application_possible @@ -163,8 +163,8 @@ context 'with opening and closing dates' do before do - subject.application_opening_at = Date.today - 2 - subject.application_closing_at = Date.today + 2 + subject.application_opening_at = Time.zone.today - 2 + subject.application_closing_at = Time.zone.today + 2 end it 'is open' do @@ -186,8 +186,8 @@ context 'with opening and closing dates in the future' do before do - subject.application_opening_at = Date.today + 1 - subject.application_closing_at = Date.today + 2 + subject.application_opening_at = Time.zone.today + 1 + subject.application_closing_at = Time.zone.today + 2 end it 'is closed' do @@ -197,8 +197,8 @@ context 'with opening and closing dates in the past' do before do - subject.application_opening_at = Date.today - 2 - subject.application_closing_at = Date.today - 1 + subject.application_opening_at = Time.zone.today - 2 + subject.application_closing_at = Time.zone.today - 1 end it 'is closed' do @@ -289,28 +289,28 @@ end it 'is valid with application closing after opening' do - subject.application_opening_at = Date.today - 5 - subject.application_closing_at = Date.today + 5 + subject.application_opening_at = Time.zone.today - 5 + subject.application_closing_at = Time.zone.today + 5 subject.valid? is_expected.to be_valid end it 'is not valid with application closing before opening' do - subject.application_opening_at = Date.today - 5 - subject.application_closing_at = Date.today - 6 + subject.application_opening_at = Time.zone.today - 5 + subject.application_closing_at = Time.zone.today - 6 is_expected.not_to be_valid end it 'is valid with application closing and without opening' do - subject.application_closing_at = Date.today - 6 + subject.application_closing_at = Time.zone.today - 6 is_expected.to be_valid end it 'is valid with application opening and without closing' do - subject.application_opening_at = Date.today - 6 + subject.application_opening_at = Time.zone.today - 6 is_expected.to be_valid end @@ -326,13 +326,13 @@ it 'adds 3 default questions for courses' do e = Event::Course.new e.init_questions - expect(e.questions.size).to eq(3) + expect(e.application_questions.size).to eq(3) end it 'does nothing for regular events' do e = Event.new e.init_questions - expect(e.questions).to be_blank + expect(e.application_questions).to be_blank end end @@ -389,7 +389,7 @@ def create_participation(prio, attrs = { active: true }) participation = Fabricate(:event_participation, participation_attrs.merge(attrs)) participation.create_application!(application_attrs) - Fabricate(event.class.participant_types.first.name.to_sym, participation: participation) + Fabricate(event.participant_types.first.name.to_sym, participation: participation) participation.save! Event::ParticipantAssigner.new(event, participation).add_participant if attrs[:active] @@ -448,7 +448,7 @@ def assert_counts(attrs) Fabricate(Event::Role::Cook.name.to_sym, participation: p) assert_counts(participant: 0, applicant: 0) - r = Fabricate(Event::Course::Role::Participant.name.to_sym, participation: p) + Fabricate(Event::Course::Role::Participant.name.to_sym, participation: p) assert_counts(participant: 1, applicant: 1) # in courses, participant roles are removed like that @@ -537,6 +537,87 @@ def assert_counts(attrs) end + context 'contact attributes' do + + let(:event) { events(:top_course) } + + it 'does not accept invalid person attributes' do + event.update({required_contact_attrs: ['foobla'], + hidden_contact_attrs: ['foofofofo']}) + + expect(event.errors.full_messages.first).to match /'foobla' ist kein gültiges Personen-Attribut/ + expect(event.errors.full_messages.second).to match /'foofofofo' ist kein gültiges Personen-Attribut/ + end + + it 'is not possible to set same attr as hidden and required' do + event.update({required_contact_attrs: ['nickname'], + hidden_contact_attrs: ['nickname']}) + + expect(event.errors.full_messages.first).to match /'nickname' kann nicht als obligatorisch und 'nicht anzeigen' gesetzt werden/ + end + + it 'is not possible to set mandatory attr as hidden' do + event.update({hidden_contact_attrs: ['email']}) + + expect(event.errors.full_messages.first).to match /'email' ist ein Pflichtfeld und kann nicht als optional oder 'nicht anzeigen' gesetzt werden/ + end + + it 'is not possible to set contact association as required' do + event.update({required_contact_attrs: ['additional_emails']}) + + expect(event.errors.full_messages.first).to match /'additional_emails' ist kein gültiges Personen-Attribut/ + end + + it 'is possible to hide contact association' do + event.update({hidden_contact_attrs: ['additional_emails']}) + + expect(event.reload.hidden_contact_attrs).to include('additional_emails') + end + + end + + context '#duplicate' do + + let(:event) { events(:top_event) } + + it 'resets participant counts' do + Fabricate(Event::Role::Leader.name, participation: Fabricate(:event_participation, event: event)) + Fabricate(Event::Role::Participant.name, participation: Fabricate(:event_participation, event: event)) + + expect(event.participant_count).not_to eq(0) + expect(event.teamer_count).not_to eq(0) + + d = event.duplicate + expect(d.participant_count).to eq(0) + expect(d.teamer_count).to eq(0) + expect(d.applicant_count).to eq(0) + end + + it 'keeps empty questions' do + d = event.duplicate + expect(d.application_questions.size).to eq(0) + end + + it 'copies existing questions' do + event.questions << Fabricate(:event_question) + event.questions << Fabricate(:event_question, admin: true) + d = event.duplicate + + expect do + d.dates << Fabricate.build(:event_date, event: d) + d.save! + end.to change { Event::Question.count }.by(2) + end + + it 'copies all groups' do + event.groups << Fabricate(Group::TopGroup.name.to_sym, name: 'CCC', parent: groups(:top_layer)) + + d = event.duplicate + expect(d.group_ids.size).to eq(2) + end + + end + def set_start_finish(event, start_at) start_at = Time.zone.parse(start_at) diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 20344d2260..8ab3319800 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -315,6 +315,13 @@ expect(group.children).to be_present expect(group.children.first.layer_group_id).to eq group.id end + + it 'sets the layer group on all descendants if parent changes' do + group = groups(:bottom_group_one_one) + group.update!(parent_id: groups(:bottom_layer_two).id) + expect(group.reload.layer_group_id).to eq(groups(:bottom_layer_two).id) + expect(groups(:bottom_group_one_one_one).layer_group_id).to eq(groups(:bottom_layer_two).id) + end end context '#destroy' do @@ -403,5 +410,22 @@ end + context 'invoice_config' do + let (:parent) { groups(:top_layer) } + + it 'is created for layer group' do + group = Fabricate(Group::BottomLayer.sti_name, name: 'g', parent: parent) + expect(group.invoice_config).to be_present + end + + it 'is not created for non layer group' do + group = Fabricate(Group::TopGroup.sti_name, name: 'g', parent: parent) + expect(group.invoice_config).not_to be_present + end + it 'is destroyed group when group gets destroyed' do + group = Fabricate(Group::BottomLayer.sti_name, name: 'g', parent: parent) + expect { group.destroy }.to change { InvoiceConfig.count }.by(-1) + end + end end diff --git a/spec/models/invoice_article_spec.rb b/spec/models/invoice_article_spec.rb new file mode 100644 index 0000000000..c06804de61 --- /dev/null +++ b/spec/models/invoice_article_spec.rb @@ -0,0 +1,17 @@ +# encoding: utf-8 + +# Copyright (c) 2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +require 'spec_helper' + +RSpec.describe InvoiceArticle, type: :model do + subject { invoice_articles(:beitrag)} + + it 'has a nice string represenation' do + expect(subject.to_s).to eq 'BEI-18 - Beitrag Erwachsene' + end + +end diff --git a/spec/models/invoice_item_spec.rb b/spec/models/invoice_item_spec.rb new file mode 100644 index 0000000000..598c581235 --- /dev/null +++ b/spec/models/invoice_item_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe InvoiceItem do + let(:invoice) { invoices(:invoice) } + + + it 'can calculate total, cost and vat' do + item = InvoiceItem.new(invoice: invoice, + name: :pens, + count: 3, + unit_cost: 1, + vat_rate: 4) + + expect(item.total).to eq 3.12 + expect(item.cost).to eq 3 + expect(item.vat).to eq 0.12 + end + + it 'calculates with 1 as default count' do + item = InvoiceItem.new(invoice: invoice, + name: :pens, + unit_cost: 1, + vat_rate: 4) + expect(item.total).to eq 1.04 + expect(item.cost).to eq 1 + expect(item.vat).to eq 0.04 + end + + + it 'calculates without vat if vat_rate is missing' do + item = InvoiceItem.new(invoice: invoice, + name: :pens, + unit_cost: 1) + expect(item.total).to eq 1 + expect(item.cost).to eq 1 + expect(item.vat).to eq 0 + end + + it 'calculates to 0 if unit_cost is missing' do + item = InvoiceItem.new(invoice: invoice, + name: :pens, + unit_cost: 0) + expect(item.total).to eq 0 + expect(item.cost).to eq 0 + expect(item.vat).to eq 0 + end + +end diff --git a/spec/models/invoice_spec.rb b/spec/models/invoice_spec.rb new file mode 100644 index 0000000000..8b91b6430b --- /dev/null +++ b/spec/models/invoice_spec.rb @@ -0,0 +1,181 @@ +# encoding: utf-8 +# frozen_string_literal: true + +# Copyright (c) 2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +require 'spec_helper' + +describe Invoice do + let(:group) { groups(:top_layer) } + let(:person) { people(:top_leader) } + let(:other_person) { people(:bottom_member) } + let(:invoice_config) { group.invoice_config } + + it 'saving requires group, title and recipient' do + invoice = create_invoice + expect(invoice).to be_valid + end + + it 'saving increments number on invoice_config' do + expect do + 2.times { create_invoice } + end.to change { invoice_config.reload.sequence_number }.by(2) + end + + it 'validates that at least one email or an address is specified if no recipient' do + invoice = Invoice.create(title: 'invoice', group: group) + expect(invoice).not_to be_valid + expect(invoice.errors.full_messages).to include('Empfänger Addresse oder E-Mail muss ausgefüllt werden') + end + + it 'computes sequence_number based of group_id and invoice_config.sequence_number' do + expect(create_invoice.sequence_number).to eq "#{group.id}-1" + end + + it 'computes esr_number based of group_id and invoice_config.sequence_number' do + expect(create_invoice.esr_number).to eq "#{group.id}-1" + end + + it '#save sets recipient and related fields' do + invoice = create_invoice + expect(invoice.recipient).to eq person + expect(invoice.recipient_email).to eq person.email + expect(invoice.recipient_address).to eq "Top Leader\nSupertown" + end + + it '#save calcuates total for invoices at once' do + invoice = Invoice.new(title: 'invoice', group: group, recipient: person) + invoice.invoice_items.build(name: 'pens', unit_cost: 1.5) + invoice.invoice_items.build(name: 'pins', unit_cost: 0.5, count: 2) + expect { invoice.save! }.to change { InvoiceItem.count }.by(2) + expect(invoice.total).to eq 2.5 + end + + it '#recalculate must be called when invoice item is added' do + invoice = Invoice.create!(group: group, title: :title, recipient: person) + expect(invoice.total).to eq(0) + invoice.invoice_items.create!(name: 'pens', unit_cost: 1.5) + invoice.recalculate + expect(invoice.total).to eq(1.5) + end + + it '#recipients loads people from recipient_ids' do + invoice = Invoice.new(title: 'invoice', group: group) + invoice.recipient_ids = "2,b,#{person.id},c," + expect(invoice.recipients).to eq [person] + end + + it '#multi_create creates invoices for multiple recipients' do + invoice = Invoice.new(title: 'invoice', group: group) + invoice.recipient_ids = [person.id, other_person.id].join(',') + invoice.invoice_items.build(name: 'pens', unit_cost: 1.5) + invoice.invoice_items.build(name: 'pins', unit_cost: 0.5, count: 2) + + expect do + invoice.multi_create + end.to change { [group.invoices.count, group.invoice_items.count] }.by([2,4]) + end + + it '#multi_create does rollsback if any save fails' do + invoice = Invoice.new(title: 'invoice', group: group) + invoice.recipient_ids = [person.id, other_person.id].join(',') + invoice.invoice_items.build(name: 'pens', unit_cost: 1.5) + + allow_any_instance_of(Invoice).to receive(:save).and_wrap_original do |m| + @saved = @saved ? false : m.call + end + + expect do + invoice.multi_create + end.not_to change { [group.invoices.count, group.invoice_items.count] } + end + + it '#to_s returns total amount' do + invoice = invoices(:invoice) + expect(invoice.to_s).to eq "Invoice(#{invoice.sequence_number}): 2.0" + end + + it '#calculated returns summed fields of invoice_items' do + calculated = invoices(:invoice).calculated + expect(calculated[:total]).to eq 5.0036 + expect(calculated[:cost]).to eq 5.0 + expect(calculated[:vat]).to eq 0.0036 + end + + context 'state changes' do + include ActiveSupport::Testing::TimeHelpers + + let(:now) { Time.zone.parse('2017-09-18 14:00:00') } + let(:invoice) { invoices(:invoice) } + before { travel_to(now) } + after { travel_back } + + it 'creating sets state to draft' do + expect(create_invoice.state).to eq 'draft' + end + + it 'changing state to issued sets issued_at and due_at dates' do + expect { invoice.update(state: :issued) }.to change { [invoice.issued_at, invoice.due_at] } + expect(invoice.due_at).to eq(now.to_date + 30.days) + expect(invoice.issued_at).to eq(now.to_date) + expect(invoice.sent_at).to be_nil + end + it 'changing state to sent sets sent_at and due_at dates' do + expect { invoice.update(state: :sent) }.to change { [invoice.issued_at, invoice.sent_at, invoice.due_at] } + + expect(invoice.due_at).to eq(now.to_date + 30.days) + expect(invoice.issued_at).to eq(now.to_date) + expect(invoice.sent_at).to eq(now.to_date) + end + end + + context '#remindable?' do + %w(sent overdue reminded).each do |state| + it "#{state} invoice is remindable" do + expect(Invoice.new(state: state)).to be_remindable + end + end + %w(draft payed cancelled).each do |state| + it "#{state} invoice is not remindable" do + expect(Invoice.new(state: state)).not_to be_remindable + end + end + end + + it 'knows a filename for the invoice-pdf' do + invoice = create_invoice + expect(invoice.sequence_number).to eq '834963567-1' + expect(invoice.filename(:pdf)).to eq 'Rechnung-834963567-1.pdf' + end + + it '.to_contactable' do + expect(contactables(recipient_address: 'test')).to have(1).item + expect(contactables(recipient_address: 'test').first.address).to eq 'test' + expect(contactables({})).to be_empty + expect(contactables({}, { recipient_address: 'test' })).to have(1).item + end + + it 'amount_open returns total amount minus payments' do + invoice = invoices(:invoice) + expect(invoice.amount_open).to eq 2.0 + invoice.payments.create!(amount: 1.5) + expect(invoice.amount_open).to eq 0.5 + invoice.payments.create!(amount: 1) + expect(invoice.amount_open).to eq -0.5 + end + + private + + def contactables(*args) + invoices = args.collect { |attrs| Invoice.new(attrs) } + Invoice.to_contactables(invoices) + end + + def create_invoice(attrs = {}) + Invoice.create!(attrs.merge(title: 'invoice', group: group, recipient: person)) + end + +end diff --git a/spec/models/label_format_spec.rb b/spec/models/label_format_spec.rb index 18f36dd3c3..b6fa16ebfa 100644 --- a/spec/models/label_format_spec.rb +++ b/spec/models/label_format_spec.rb @@ -32,4 +32,26 @@ expect(p.reload.last_label_format_id).to be_nil end + context '.for_person' do + + let(:person) { Person.first } + + before do + Fabricate(:label_format, person: person) + end + + it 'includes all label formats if show_global_label_formats' do + person.show_global_label_formats = true + + expect(LabelFormat.for_person(person).size).to eq(4) + end + + it 'includes only personal label formats if !show_global_label_formats' do + person.show_global_label_formats = false + + expect(LabelFormat.for_person(person).size).to eq(1) + end + + end + end diff --git a/spec/models/mailing_list_spec.rb b/spec/models/mailing_list_spec.rb index 73db1664c5..09b063af6d 100644 --- a/spec/models/mailing_list_spec.rb +++ b/spec/models/mailing_list_spec.rb @@ -99,7 +99,7 @@ context 'groups' do it 'is true if in group' do - sub = create_subscription(groups(:bottom_layer_one), false, + create_subscription(groups(:bottom_layer_one), false, Group::BottomGroup::Leader.sti_name) p = Fabricate(Group::BottomGroup::Leader.name.to_sym, group: groups(:bottom_group_one_one)).person @@ -107,7 +107,7 @@ end it 'is false if different role in group' do - sub = create_subscription(groups(:bottom_layer_one), false, + create_subscription(groups(:bottom_layer_one), false, Group::BottomGroup::Leader.sti_name) p = Fabricate(Group::BottomGroup::Member.name.to_sym, group: groups(:bottom_group_one_one)).person @@ -151,7 +151,7 @@ end it 'is false if explicitly excluded' do - sub = create_subscription(groups(:bottom_layer_one), false, + create_subscription(groups(:bottom_layer_one), false, Group::BottomGroup::Leader.sti_name) p = Fabricate(Group::BottomGroup::Leader.name.to_sym, group: groups(:bottom_group_one_one)).person create_subscription(p, true) @@ -224,7 +224,7 @@ context 'only groups' do it 'includes people with the given roles' do - sub = create_subscription(groups(:bottom_layer_one), false, + create_subscription(groups(:bottom_layer_one), false, Group::BottomGroup::Leader.sti_name) role = Group::BottomGroup::Leader.name.to_sym @@ -245,10 +245,10 @@ end it 'includes people with the given roles in multiple groups' do - sub1 = create_subscription(groups(:bottom_layer_one), false, + create_subscription(groups(:bottom_layer_one), false, Group::BottomLayer::Leader.sti_name, Group::BottomGroup::Leader.sti_name) - sub2 = create_subscription(groups(:bottom_group_one_one), false, + create_subscription(groups(:bottom_group_one_one), false, Group::BottomGroup::Member.sti_name) p1 = Fabricate(Group::BottomLayer::Leader.name.to_sym, group: groups(:bottom_layer_one)).person @@ -297,7 +297,7 @@ context 'groups with excluded' do it 'excludes person from groups' do - sub = create_subscription(groups(:bottom_layer_one), false, + create_subscription(groups(:bottom_layer_one), false, Group::BottomGroup::Leader.sti_name) role = Group::BottomGroup::Leader.name.to_sym @@ -329,7 +329,7 @@ # groups - sub1 = create_subscription(groups(:bottom_layer_one), false, + create_subscription(groups(:bottom_layer_one), false, Group::BottomLayer::Leader.sti_name, Group::BottomGroup::Leader.sti_name) sub2 = create_subscription(groups(:bottom_group_one_one), false, @@ -375,10 +375,10 @@ # groups - sub1 = create_subscription(groups(:bottom_layer_one), false, + create_subscription(groups(:bottom_layer_one), false, Group::BottomLayer::Leader.sti_name, Group::BottomGroup::Leader.sti_name) - sub2 = create_subscription(groups(:bottom_group_one_one), false, + create_subscription(groups(:bottom_group_one_one), false, Group::BottomGroup::Member.sti_name) pg1 = Fabricate(Group::BottomLayer::Leader.name.to_sym, group: groups(:bottom_layer_one)).person @@ -415,10 +415,10 @@ # groups - sub1 = create_subscription(groups(:bottom_layer_one), false, + create_subscription(groups(:bottom_layer_one), false, Group::BottomLayer::Leader.sti_name, Group::BottomGroup::Leader.sti_name) - sub2 = create_subscription(groups(:bottom_group_one_one), false, + create_subscription(groups(:bottom_group_one_one), false, Group::BottomGroup::Member.sti_name) pg1 = Fabricate(Group::BottomLayer::Leader.name.to_sym, group: groups(:bottom_layer_one)).person diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb new file mode 100644 index 0000000000..d91c203c63 --- /dev/null +++ b/spec/models/note_spec.rb @@ -0,0 +1,79 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2016, Dachverband Schweizer Jugendparlamente. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +# == Schema Information +# +# Table name: person_notes +# +# id :integer not null, primary key +# person_id :integer not null +# author_id :integer not null +# text :text +# created_at :datetime +# updated_at :datetime +# + +require 'spec_helper' + +describe Note do + + let(:author) { Fabricate(:person) } + + context '.in_or_layer_below' do + it 'includes only notes from this layer for layer group' do + n1 = create_person_note(Group::TopLayer::TopAdmin, groups(:top_layer)) + n2 = create_person_note(Group::TopGroup::LocalGuide, groups(:top_group)) + _n3 = create_person_note(Group::BottomLayer::Leader, groups(:bottom_layer_one)) + n4 = create_group_note(groups(:top_layer)) + n5 = create_group_note(groups(:top_group)) + _n6 = create_group_note(groups(:bottom_layer_one)) + expect(Note.in_or_layer_below(groups(:top_layer))).to match_array([n1, n2, n4,n5]) + end + + it 'includes only notes from children for non-layer group' do + n1 = create_person_note(Group::BottomGroup::Leader, groups(:bottom_group_one_one)) + n2 = create_person_note(Group::BottomGroup::Leader, groups(:bottom_group_one_one_one)) + _n3 = create_person_note(Group::BottomGroup::Leader, groups(:bottom_group_one_two)) + n4 = create_group_note(groups(:bottom_group_one_one)) + n5 = create_group_note(groups(:bottom_group_one_one_one)) + _n6 = create_group_note(groups(:bottom_group_one_two)) + _n7 = create_group_note(groups(:bottom_layer_two)) + expect(Note.in_or_layer_below(groups(:bottom_group_one_one))).to match_array([n1, n2, n4,n5]) + end + + def create_person_note(role, group) + Note.create!(subject: Fabricate(role.name.to_sym, group: group).person, + author_id: author.id, + text: 'foo') + end + + def create_group_note(group) + Note.create!(subject: group, + author_id: author.id, + text: 'foo') + end + end + + context 'dependent destroy' do + let(:subject) { Fabricate(:person) } + + it 'gets destroyed if the person is destroyed' do + subject.notes.create!(author_id: author.id, text: 'Lorem ipsum') + expect(Note.count).to eq(1) + subject.destroy! + expect(Note.count).to eq(0) + end + + it 'gets destroyed if the author is destroyed' do + subject.notes.create!(author_id: author.id, text: 'Lorem ipsum') + expect(Note.count).to eq(1) + author.destroy! + expect(Note.count).to eq(0) + end + end + +end diff --git a/spec/models/payment_reminder_spec.rb b/spec/models/payment_reminder_spec.rb new file mode 100644 index 0000000000..458f4eb25f --- /dev/null +++ b/spec/models/payment_reminder_spec.rb @@ -0,0 +1,48 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. +# == Schema Information +# +# Table name: payment_reminders +# +# id :integer not null, primary key +# invoice_id :integer not null +# message :text(65535) +# due_at :date not null +# created_at :datetime not null +# updated_at :datetime not null +# + +require 'spec_helper' + +describe PaymentReminder do + let(:invoice) { invoices(:sent) } + + it 'creating a payment_reminder updates invoice' do + due_at = invoice.due_at + 2.weeks + expect do + invoice.payment_reminders.create!(due_at: due_at) + end.to change { [invoice.due_at, invoice.state] } + expect(invoice.due_at).to eq due_at + expect(invoice.state).to eq 'overdue' + end + + it 'validates invoice is in state sent' do + reminder = Invoice.new.payment_reminders.build + expect(reminder).to have(1).error_on(:invoice) + end + + it 'validates due_at is set' do + reminder = invoice.payment_reminders.build + expect(reminder).to have(1).error_on(:due_at) + end + + it 'validates due_at is after invoice.due_date' do + reminder = invoice.payment_reminders.build(due_at: invoice.due_at) + expect(reminder).to have(1).error_on(:due_at) + end + +end diff --git a/spec/models/payment_spec.rb b/spec/models/payment_spec.rb new file mode 100644 index 0000000000..33005ea5e6 --- /dev/null +++ b/spec/models/payment_spec.rb @@ -0,0 +1,26 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +require 'spec_helper' + +describe Payment do + let(:invoice) { invoices(:sent) } + + it 'creating a big enough payment marks invoice as payed' do + expect do + invoice.payments.create!(amount: invoice.total) + end.to change { invoice.state } + expect(invoice.state).to eq 'payed' + end + + it 'creating a smaller payment does not change invoice state' do + expect do + invoice.payments.create!(amount: invoice.total - 1) + end.not_to change { invoice.state } + end + +end diff --git a/spec/models/people_filter_spec.rb b/spec/models/people_filter_spec.rb index a2d7b90f63..6ccb7d7c94 100644 --- a/spec/models/people_filter_spec.rb +++ b/spec/models/people_filter_spec.rb @@ -1,9 +1,10 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. + # == Schema Information # # Table name: people_filters @@ -18,57 +19,13 @@ describe PeopleFilter do - it 'creates RoleTypes on name assignment' do - group = groups(:top_layer) - filter = group.people_filters.new(name: 'Test') - filter.role_types = ['Group::TopGroup::Leader', 'Group::TopGroup::Member'] - types = filter.related_role_types - - expect(types.size).to eq(2) - expect(types.first.role_type).to eq('Group::TopGroup::Leader') - - expect(filter).to be_valid - expect { filter.save }.to change { RelatedRoleType.count }.by(2) - end - - it 'creates RoleTypes on id Array assignment' do - group = groups(:top_layer) - filter = group.people_filters.new(name: 'Test') - filter.role_type_ids = [Group::TopGroup::Leader.id, Group::TopGroup::Member.id] - types = filter.related_role_types - - expect(types.size).to eq(2) - expect(types.first.role_type).to eq('Group::TopGroup::Leader') - expect(types.last.role_type).to eq('Group::TopGroup::Member') + context '#filter_chain=' do - expect(filter).to be_valid - expect { filter.save }.to change { RelatedRoleType.count }.by(2) - end - - it 'creates RoleTypes on id String List assignment' do - group = groups(:top_layer) - filter = group.people_filters.new(name: 'Test') - filter.role_type_ids = [Group::TopGroup::Leader.id, Group::TopGroup::Member.id].join('-') - types = filter.related_role_types - - expect(types.size).to eq(2) - expect(types.first.role_type).to eq('Group::TopGroup::Leader') - expect(types.last.role_type).to eq('Group::TopGroup::Member') + it 'assigns hash to filter_chain' do + filter = PeopleFilter.new(filter_chain: { role: { role_type_ids: [1, 2, 3] }}) + expect(filter.filter_chain[:role].to_params).to eq(role_type_ids: '1-2-3') + end - expect(filter).to be_valid - expect { filter.save }.to change { RelatedRoleType.count }.by(2) end - it 'creates RoleTypes with invalid id String List assignment' do - group = groups(:top_layer) - filter = group.people_filters.new(name: 'Test') - filter.role_type_ids = [33_256, Group::TopGroup::Member.id].join('-') - types = filter.related_role_types - - expect(types.size).to eq(1) - expect(types.first.role_type).to eq('Group::TopGroup::Member') - - expect(filter).to be_valid - expect { filter.save }.to change { RelatedRoleType.count }.by(1) - end end diff --git a/spec/models/person/add_request_spec.rb b/spec/models/person/add_request_spec.rb index 6c28b6141a..2e38499cc8 100644 --- a/spec/models/person/add_request_spec.rb +++ b/spec/models/person/add_request_spec.rb @@ -43,6 +43,27 @@ expect(people).to match_array([admin, topper].collect(&:id)) end + it 'contains deleted people' do + admin = Fabricate(Group::TopLayer::TopAdmin.name, group: groups(:top_layer)).person + ex_topper = Fabricate(Group::TopGroup::Member.name, group: groups(:top_group), created_at: 1.year.ago).person + ex_topper.roles.first.destroy + bottom = Fabricate(Group::BottomLayer::Leader.name, group: groups(:bottom_layer_one)).person + # deleted role in layer + del = Fabricate(Group::TopGroup::Member.name, group: groups(:top_group), person: bottom, created_at: 1.year.ago) + del.destroy + [admin, ex_topper, bottom].each do |p| + Person::AddRequest::Group.create!( + person: p, + requester: bottom, + body: groups(:bottom_layer_one), + role_type: Group::BottomLayer::Member.sti_name) + end + + people = Person::AddRequest.for_layer(groups(:top_layer)).pluck(:person_id) + + expect(people).to match_array([admin, ex_topper].collect(&:id)) + end + end context 'uniqueness' do @@ -115,6 +136,21 @@ it '#to_s contains group type' do expect(@rg.to_s).to eq("Top Group TopGroup") end + + it '#last_layer_group contains last layer' do + topper = Fabricate(Group::TopGroup::Member.name, group: groups(:top_group), created_at: 1.year.ago).person + bottom = Fabricate(Group::BottomLayer::Leader.name, group: groups(:bottom_layer_one)).person + # second role in layer + Fabricate(Group::TopGroup::Member.name, group: groups(:top_group), person: bottom) + Person::AddRequest::Group.create!( + person: topper, + requester: bottom, + body: groups(:bottom_layer_one), + role_type: Group::BottomLayer::Member.sti_name) + topper.roles.first.destroy! + add_request = Person::AddRequest.where(person_id: topper.id) + expect(add_request.first.send(:last_layer_group)).to eq(groups(:top_layer)) + end end context 'event' do diff --git a/spec/models/person/note_spec.rb b/spec/models/person/note_spec.rb deleted file mode 100644 index 0bb6ba9a67..0000000000 --- a/spec/models/person/note_spec.rb +++ /dev/null @@ -1,41 +0,0 @@ -# encoding: utf-8 - -# Copyright (c) 2012-2016, Dachverband Schweizer Jugendparlamente. This file is part of -# hitobito_dsj and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito_dsj. - -# == Schema Information -# -# Table name: person_notes -# -# id :integer not null, primary key -# person_id :integer not null -# author_id :integer not null -# text :text -# created_at :datetime -# updated_at :datetime -# - -require 'spec_helper' - -describe Person::Note do - context 'dependent destroy' do - let(:person) { Fabricate(:person) } - let(:author) { Fabricate(:person) } - - it 'gets destroyed if the person is destroyed' do - person.notes.create!(author_id: author.id, text: 'Lorem ipsum') - expect(Person::Note.count).to eq(1) - person.destroy! - expect(Person::Note.count).to eq(0) - end - - it 'gets destroyed if the author is destroyed' do - person.notes.create!(author_id: author.id, text: 'Lorem ipsum') - expect(Person::Note.count).to eq(1) - author.destroy! - expect(Person::Note.count).to eq(0) - end - end -end diff --git a/spec/models/person_spec.rb b/spec/models/person_spec.rb index be3ad11a76..9a26ff18f6 100644 --- a/spec/models/person_spec.rb +++ b/spec/models/person_spec.rb @@ -120,6 +120,15 @@ it 'has layer_and_below_full permission in top_group' do expect(person.groups_with_permission(:layer_and_below_full)).to eq([groups(:top_group)]) end + + it 'found deleted last role' do + deletion_date = DateTime.current + expect(person.roles.count).to eq 1 + role.update(deleted_at: deletion_date) + expect(person.roles.count).to eq 0 + expect(person.decorate.last_role.deleted_at.to_time.to_i).to eq(deletion_date.to_time.to_i) + end + end @@ -495,4 +504,8 @@ def should_not_be_valid_swiss_post_code end end + it '#finance_groups returns list of group on which user may manage invoices' do + expect(people(:bottom_member).finance_groups).to eq [groups(:bottom_layer_one)] + end + end diff --git a/spec/models/qualification_spec.rb b/spec/models/qualification_spec.rb index 7a00e8a5c0..e23bd1f78b 100644 --- a/spec/models/qualification_spec.rb +++ b/spec/models/qualification_spec.rb @@ -83,7 +83,7 @@ context '#set_finish_at' do - let(:date) { Date.today } + let(:date) { Time.zone.today } it 'set current end of year if validity is 0' do quali = build_qualification(0, date) @@ -128,25 +128,25 @@ def build_qualification(validity, start_at) subject { person.reload.qualifications.active } it 'contains from today' do - q = Fabricate(:qualification, person: person, start_at: Date.today) + q = Fabricate(:qualification, person: person, start_at: Time.zone.today) expect(q).to be_active is_expected.to include(q) end it 'does contain until this year' do - q = Fabricate(:qualification, person: person, start_at: Date.today - 2.years) + q = Fabricate(:qualification, person: person, start_at: Time.zone.today - 2.years) expect(q).to be_active is_expected.to include(q) end it 'does not contain past' do - q = Fabricate(:qualification, person: person, start_at: Date.today - 5.years) + q = Fabricate(:qualification, person: person, start_at: Time.zone.today - 5.years) expect(q).not_to be_active is_expected.not_to include(q) end it 'does not contain future' do - q = Fabricate(:qualification, person: person, start_at: Date.today + 1.day) + q = Fabricate(:qualification, person: person, start_at: Time.zone.today + 1.day) expect(q).not_to be_active is_expected.not_to include(q) end @@ -155,7 +155,7 @@ def build_qualification(validity, start_at) context 'reactivateable qualification kind' do subject { person.reload.qualifications } - let(:today) { Date.today } + let(:today) { Time.zone.today } let(:kind) { qualification_kinds(:sl) } let(:start_date) { today - 1.years } let(:q) { Fabricate(:qualification, qualification_kind: kind, person: person, start_at: start_date) } @@ -219,7 +219,7 @@ def build_qualification(validity, start_at) expect do person.qualifications.create!(qualification_kind: qualification_kinds(:sl), origin: 'Bar', - start_at: Date.today) + start_at: Time.zone.today) end.to change { PaperTrail::Version.count }.by(1) version = PaperTrail::Version.order(:created_at, :id).last @@ -230,7 +230,7 @@ def build_qualification(validity, start_at) it 'sets main on update' do quali = person.qualifications.create!(qualification_kind: qualification_kinds(:sl), origin: 'Bar', - start_at: Date.today) + start_at: Time.zone.today) expect do quali.update_attributes!(origin: 'Bur') end.to change { PaperTrail::Version.count }.by(1) @@ -243,7 +243,7 @@ def build_qualification(validity, start_at) it 'sets main on destroy' do quali = person.qualifications.create!(qualification_kind: qualification_kinds(:sl), origin: 'Bar', - start_at: Date.today) + start_at: Time.zone.today) expect do quali.destroy! end.to change { PaperTrail::Version.count }.by(1) diff --git a/spec/models/role_spec.rb b/spec/models/role_spec.rb index a147a08a50..93c1266f60 100644 --- a/spec/models/role_spec.rb +++ b/spec/models/role_spec.rb @@ -1,6 +1,6 @@ # encoding: utf-8 -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# Copyright (c) 2012-2017, Jungwacht Blauring Schweiz. This file is part of # hitobito and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. @@ -168,7 +168,7 @@ it 'reuses existing label' do a1 = Fabricate(Group::BottomLayer::Leader.name.to_s, label: 'foo', group: groups(:bottom_layer_one)) a2 = Fabricate(Group::BottomLayer::Leader.name.to_s, label: 'fOO', group: groups(:bottom_layer_one)) - expect(a2.label).to eq('foo') + expect(a2.label).to eq(a1.label) end end diff --git a/spec/regressions/event/lists_controller_spec.rb b/spec/regressions/event/lists_controller_spec.rb index 8fed76e9d9..968dce5642 100644 --- a/spec/regressions/event/lists_controller_spec.rb +++ b/spec/regressions/event/lists_controller_spec.rb @@ -17,7 +17,7 @@ let(:dom) { Capybara::Node::Simple.new(response.body) } - let(:dropdown) { dom.find('.dropdown-menu') } + let(:dropdown) { dom.find('.nav .dropdown-menu') } let(:year) { Date.today.year } let(:top_layer) { groups(:top_layer) } let(:top_group) { groups(:top_group) } @@ -53,10 +53,10 @@ get :events expect(link.text.strip).to eq 'Anmelden' - expect(link[:href]).to eq new_group_event_participation_path(event.groups.first, + expect(link[:href]).to eq contact_data_group_event_participations_path(event.groups.first, event, event_role: { - type: event.class.participant_types.first.sti_name}) + type: event.participant_types.first.sti_name}) end end end @@ -99,10 +99,10 @@ it 'tabs contain year based pagination' do first, last = tabs.all('a')[1], tabs.all('a')[-2] expect(first.text).to eq (year - 2).to_s - expect(first[:href]).to eq list_courses_path(year: year - 2, group_id: top_group.id) + expect(first[:href]).to eq list_courses_path(year: year - 2, group_id: people(:top_leader).primary_group.layer_group_id) expect(last.text).to eq (year + 1).to_s - expect(last[:href]).to eq list_courses_path(year: year + 1, group_id: top_group.id) + expect(last[:href]).to eq list_courses_path(year: year + 1, group_id: people(:top_leader).primary_group.layer_group_id) end end diff --git a/spec/regressions/event/participation_contact_datas_controller_spec.rb b/spec/regressions/event/participation_contact_datas_controller_spec.rb new file mode 100644 index 0000000000..28d5b1632c --- /dev/null +++ b/spec/regressions/event/participation_contact_datas_controller_spec.rb @@ -0,0 +1,121 @@ +# encoding: utf-8 + +# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of +# hitobito and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito. + +# encoding: utf-8 + +require 'spec_helper' + +describe Event::ParticipationContactDatasController, type: :controller do + + render_views + + let(:group) { groups(:top_layer) } + let(:course) { Fabricate(:course, groups: [group]) } + let(:person) { people(:top_leader) } + let(:dom) { Capybara::Node::Simple.new(response.body) } + + before { sign_in(person) } + + describe 'GET edit' do + + it 'does not show hidden contact fields' do + + course.update!({ hidden_contact_attrs: ['address', 'nickname', 'social_accounts'] }) + + get :edit, group_id: course.groups.first.id, event_id: course.id, + event_role: { type: 'Event::Course::Role::Participant' } + + expect(dom).to have_selector('input#event_participation_contact_data_first_name') + expect(dom).to have_selector('input#event_participation_contact_data_last_name') + expect(dom).to have_selector('input#event_participation_contact_data_email') + expect(dom).to have_selector('#additional_emails_fields') + expect(dom).to have_selector('#phone_numbers_fields') + + expect(dom).to have_no_selector('textarea#event_participation_contact_data_address') + expect(dom).to have_no_selector('input#event_participation_contact_data_nickname') + expect(dom).to have_no_selector('#social_accounts_fields') + + end + + it 'shows all contact fields by default' do + + get :edit, group_id: course.groups.first.id, event_id: course.id, + event_role: { type: 'Event::Course::Role::Participant' } + + contact_attrs = [:first_name, :last_name, :nickname, + :company_name, :zip_code, :town, + :gender_w, :gender_m, :gender_, + :birthday, :email] + + contact_attrs.each do |a| + expect(dom).to have_selector("input#event_participation_contact_data_#{a}") + end + + expect(dom).to have_selector("textarea#event_participation_contact_data_address") + + expect(dom).to have_selector('#additional_emails_fields') + expect(dom).to have_selector('#phone_numbers_fields') + expect(dom).to have_selector('#social_accounts_fields') + + end + + it 'marks required attributes with an asterisk' do + + course.update!({ required_contact_attrs: ['address', 'nickname'] }) + + get :edit, group_id: course.groups.first.id, event_id: course.id, + event_role: { type: 'Event::Course::Role::Participant' } + + + end + + end + + context 'POST update' do + + before do + course.update!({ required_contact_attrs: ['nickname', 'address']}) + end + + it 'validates contact attributes and person attributes' do + + contact_data_params = { first_name: 'Hans', last_name: 'Gugger', email: 'invalid', nickname: '' } + + post :update, group_id: group.id, event_id: course.id, + event_participation_contact_data: contact_data_params, + event_role: { type: 'Event::Course::Role::Participant' } + + is_expected.to render_template(:edit) + + expect(dom).to have_selector('.alert-error li', text: 'Übername muss ausgefüllt werden') + expect(dom).to have_selector('.alert-error li', text: 'Adresse muss ausgefüllt werden') + expect(dom).to have_selector('.alert-error li', text: /Haupt-E-Mail ist nicht gültig/) + + end + + it 'updates person attributes and redirects to event questions' do + + contact_data_params = { first_name: 'Hans', last_name: 'Gugger', + email: 'dude@example.com', nickname: 'Jojo', + address: 'Street 33' } + + post :update, group_id: group.id, event_id: course.id, + event_participation_contact_data: contact_data_params, + event_role: { type: 'Event::Course::Role::Participant' } + + is_expected.to redirect_to new_group_event_participation_path(group, + course, + event_role: { type: 'Event::Course::Role::Participant' }) + + person.reload + expect(person.nickname).to eq('Jojo') + expect(person.email).to eq('dude@example.com') + + end + end + +end diff --git a/spec/regressions/event/participations_controller_spec.rb b/spec/regressions/event/participations_controller_spec.rb index 9b483557ee..b10f0f351b 100644 --- a/spec/regressions/event/participations_controller_spec.rb +++ b/spec/regressions/event/participations_controller_spec.rb @@ -5,8 +5,6 @@ # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito. -# encoding: utf-8 - require 'spec_helper' describe Event::ParticipationsController, type: :controller do @@ -78,18 +76,6 @@ def scope_params end end - describe 'POST create' do - [:event_base, :course].each do |event_sym| - it "prompts to change contact data for #{event_sym}" do - event = send(event_sym) - post :create, group_id: group.id, event_id: event.id, event_participation: test_entry_attrs - expect(flash[:notice]).to match(/Bitte überprüfe die Kontaktdaten/) - is_expected.to redirect_to group_event_participation_path(group, event, - assigns(:participation)) - end - end - end - describe 'GET new' do subject { Capybara::Node::Simple.new(response.body) } [:event_base, :course].each do |event_sym| @@ -103,8 +89,8 @@ def scope_params get :new, group_id: group.id, event_id: course.id, for_someone_else: true person_field = subject.all('form .control-group')[0] expect(person_field).to have_content 'Person' - expect(person_field).to have_css('input', count: 2) - expect(person_field.all('input').first[:type]).to eq 'hidden' + expect(person_field).to have_css('input', visible: false, count: 2) + expect(person_field.all('input', visible: false).first[:type]).to eq 'hidden' end it 'renders alternatives' do @@ -115,16 +101,6 @@ def scope_params end end - describe_action :delete, :destroy, format: :html, id: true do - it 'redirects to application market' do - is_expected.to redirect_to group_event_application_market_index_path(group, course) - end - - it 'has flash noting the application' do - expect(flash[:notice]).to match(/Anmeldung/) - end - end - describe 'GET print' do let(:person) { Fabricate(:person_with_address) } let(:application) do @@ -188,7 +164,7 @@ def scope_params Fabricate(:event_role, type: Event::Course::Role::Participant.sti_name, participation: test_entry) get :show, group_id: group.id, event_id: course.id, id: test_entry.id - expect(dom).to have_content 'Vorbedingungen für Anmeldung sind nicht erfüllt.' + expect(dom).to have_content 'Vorbedingungen für Anmeldung sind nicht erfüllt' end end diff --git a/spec/regressions/events_controller_spec.rb b/spec/regressions/events_controller_spec.rb index 7b0180234a..d09c31a1fc 100644 --- a/spec/regressions/events_controller_spec.rb +++ b/spec/regressions/events_controller_spec.rb @@ -9,6 +9,8 @@ describe EventsController, type: :controller do + render_views + # always use fixtures with crud controller examples, otherwise request reuse might produce errors let(:test_entry) { ev = events(:top_course); ev.dates.clear; ev } let(:group) { test_entry.groups.first } @@ -38,7 +40,6 @@ def deep_attributes(*args) describe 'GET #index' do context '.html' do - render_views let(:group) { groups(:top_layer) } let(:dom) { Capybara::Node::Simple.new(response.body) } let(:today) { Date.today } @@ -56,7 +57,7 @@ def deep_attributes(*args) it 'renders button to export courses' do get :index, group_id: group.id, type: 'Event::Course', year: 2012 - expect(dom.all('.btn-toolbar .btn')[1].text).to include 'CSV Export' + expect(dom.all('.btn-toolbar .btn')[1].text).to include 'Export' end it 'lists entries for current year' do @@ -91,28 +92,59 @@ def event_with_date(opts = {}) let(:group) { groups(:top_layer) } it 'renders events csv' do - get :index, group_id: group.id, format: :csv, year: 2012 - expect(response.body.lines.to_a.size).to eq(2) + expect do + get :index, group_id: group.id, format: :csv, year: 2012 + expect(flash[:notice]).to match(/Export wird im Hintergrund gestartet und nach Fertigstellung an \S+@\S+ versendet./) + end.to change(Delayed::Job, :count).by(1) + end + + it 'renders courses csv' do + expect do + get :index, group_id: group.id, format: :csv, year: 2012, type: Event::Course.sti_name + expect(flash[:notice]).to match(/Export wird im Hintergrund gestartet und nach Fertigstellung an \S+@\S+ versendet./) + end.to change(Delayed::Job, :count).by(1) + end + + end + + context '.ics' do + + let(:group) { groups(:top_layer) } + + it 'renders events ics' do + get :index, group_id: group.id, format: :ics, year: 2012 + expect(response.content_type).to eq('text/calendar') end it 'renders courses csv' do - get :index, group_id: group.id, format: :csv, year: 2012, type: Event::Course.sti_name - expect(response.body.lines.to_a.size).to eq(2) + get :index, group_id: group.id, format: :ics, year: 2012, type: Event::Course.sti_name + expect(response.content_type).to eq('text/calendar') end end end + describe 'GET #show' do + context '.ics' do + + let(:group) { groups(:top_layer) } + + it 'renders event ics' do + get :show, group_id: group.id, id: test_entry.to_param, format: :ics + expect(response.content_type).to eq('text/calendar') + end + end + end + describe 'GET #new' do - render_views let(:group) { groups(:top_group) } let(:dom) { Capybara::Node::Simple.new(response.body) } it 'renders new form' do get :new, group_id: group.id, event: { type: 'Event::Course' } - expect(dom.find('input#event_type')[:type]).to eq 'hidden' - expect(dom.all('#questions_fields .fields').count).to eq 3 + expect(dom.find('input#event_type', visible: false)[:type]).to eq 'hidden' + expect(dom.all('#application_questions_fields .fields').count).to eq 3 expect(dom.all('#dates_fields').count).to eq 1 end end diff --git a/spec/regressions/full_text_controller_spec.rb b/spec/regressions/full_text_controller_spec.rb deleted file mode 100644 index c88aba9f15..0000000000 --- a/spec/regressions/full_text_controller_spec.rb +++ /dev/null @@ -1,126 +0,0 @@ -# encoding: utf-8 - -# Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of -# hitobito and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/hitobito/hitobito. - -require 'spec_helper' - -describe FullTextController, :mysql, type: :controller do - - sphinx_environment(:people, :groups) do - - before do - Rails.cache.clear - @tg_member = Fabricate(Group::TopGroup::Member.name.to_sym, group: groups(:top_group)).person - @tg_extern = Fabricate(Role::External.name.to_sym, group: groups(:top_group)).person - - @bl_leader = Fabricate(Group::BottomLayer::Leader.name.to_sym, group: groups(:bottom_layer_one)).person - @bl_extern = Fabricate(Role::External.name.to_sym, group: groups(:bottom_layer_one)).person - - @bg_leader = Fabricate(Group::BottomGroup::Leader.name.to_sym, - group: groups(:bottom_group_one_one), - person: Fabricate(:person, last_name: 'Schurter', first_name: 'Franz')).person - @bg_member = Fabricate(Group::BottomGroup::Member.name.to_sym, - group: groups(:bottom_group_one_one), - person: Fabricate(:person, last_name: 'Bindella', first_name: 'Yasmine')).person - - index_sphinx - end - - describe 'GET index' do - - context 'as top leader' do - before { sign_in(people(:top_leader)) } - - it 'finds accessible person' do - get :index, q: @bg_leader.last_name[1..5] - - expect(assigns(:people)).to include(@bg_leader) - end - - it 'does not find not accessible person' do - get :index, q: @bg_member.last_name[1..5] - - expect(assigns(:people)).not_to include(@bg_member) - end - - it 'does not search for too short queries' do - get :index, q: 'e' - - expect(assigns(:people)).to eq([]) - end - - context 'without any params' do - it 'returns nothing' do - get :index - - expect(@response).to be_ok - end - end - end - - context 'as root' do - before { sign_in(people(:root)) } - - it 'finds every person' do - get :index, q: @bg_member.last_name[1..5] - - expect(assigns(:people)).to include(@bg_member) - end - end - - end - - describe 'GET query' do - - context 'as leader' do - before { sign_in(people(:top_leader)) } - - it 'finds accessible person' do - get :query, q: @bg_leader.last_name[1..5] - - expect(@response.body).to include(@bg_leader.full_name) - end - - it 'does not find not accessible person' do - get :query, q: @bg_member.last_name[1..5] - - expect(@response.body).not_to include(@bg_member.full_name) - end - - it 'finds groups' do - get :query, q: groups(:bottom_layer_one).to_s[1..5] - - expect(@response.body).to include(groups(:bottom_layer_one).to_s) - end - - context 'without any params' do - it 'returns nothing' do - get :query - - expect(@response).to be_ok - expect(JSON.parse(@response.body)).to eq([]) - end - end - end - - context 'as unprivileged person' do - before do - person = Fabricate(:person) - sign_in(person) - end - - it 'finds zero people' do - get :query, q: @bg_member.last_name[1..5] - - expect(assigns(:people)).to be_nil - end - end - end - - - end - -end diff --git a/spec/regressions/label_formats_controller_spec.rb b/spec/regressions/label_formats_controller_spec.rb index b230de7ad8..53af8bae52 100644 --- a/spec/regressions/label_formats_controller_spec.rb +++ b/spec/regressions/label_formats_controller_spec.rb @@ -31,6 +31,8 @@ def it_should_redirect_to_show padding_left: 2.0 } end + before { Fabricate(:label_format, person: people(:top_leader)) } + before { sign_in(people(:top_leader)) } include_examples 'crud controller', skip: [%w(show)] diff --git a/spec/regressions/people_controller_spec.rb b/spec/regressions/people_controller_spec.rb index cc1e8ceac4..8e6cb2b5b1 100644 --- a/spec/regressions/people_controller_spec.rb +++ b/spec/regressions/people_controller_spec.rb @@ -237,7 +237,7 @@ def create_participation(date, active_participation = false) return_url: 'foo' expect(dom.all('a', text: 'Abbrechen').first[:href]).to eq 'foo' - expect(dom.find('input#return_url').value).to eq 'foo' + expect(dom.find('input#return_url', visible: false).value).to eq 'foo' end end diff --git a/spec/regressions/person/csv_imports_controller_spec.rb b/spec/regressions/person/csv_imports_controller_spec.rb index 2c23fbd66a..6f3ad778e9 100644 --- a/spec/regressions/person/csv_imports_controller_spec.rb +++ b/spec/regressions/person/csv_imports_controller_spec.rb @@ -46,7 +46,7 @@ it 'imports single person only' do expect { post :create, group_id: group.id, data: data, role_type: role_type.sti_name, field_mappings: mapping }.to change(Person, :count).by(1) - is_expected.to redirect_to group_people_path(group, name: 'Leader', role_type_ids: role_type.id) + is_expected.to redirect_to group_people_path(group, name: 'Leader', filters: { role: { role_type_ids: [role_type.id] } }) end end diff --git a/spec/regressions/person/history_controller_spec.rb b/spec/regressions/person/history_controller_spec.rb index be3ba1f1fa..049247a7a0 100644 --- a/spec/regressions/person/history_controller_spec.rb +++ b/spec/regressions/person/history_controller_spec.rb @@ -27,7 +27,7 @@ get :index, params expect(dom.all('table tbody tr').size).to eq 1 role_row = dom.find('table tbody tr:eq(1)') - expect(role_row.find('td:eq(1) a').text).to eq 'TopGroup' + expect(role_row.find('td:eq(1) a:eq(2)').text).to eq 'TopGroup' expect(role_row.find('td:eq(2)').text.strip).to eq 'Member' expect(role_row.find('td:eq(3)').text).to be_present expect(role_row.find('td:eq(4)').text).not_to be_present @@ -40,7 +40,7 @@ get :index, params expect(dom.all('table tbody tr').size).to eq 2 role_row = dom.find('table tbody tr:eq(1)') - expect(role_row.find('td:eq(1) a').text).to eq 'Group 11' + expect(role_row.find('td:eq(1) a:eq(2)').text).to eq 'Group 11' expect(role_row.find('td:eq(2)').text.strip).to eq 'Member' expect(role_row.find('td:eq(3)').text).to be_present expect(role_row.find('td:eq(4)').text).to be_present @@ -51,7 +51,7 @@ get :index, params expect(dom.all('table tbody tr').size).to eq 2 role_row = dom.find('table tbody tr:eq(2)') - expect(role_row.find('td:eq(1) a').text).to eq 'TopGroup' + expect(role_row.find('td:eq(1) a:eq(2)').text).to eq 'TopGroup' expect(role_row.find('td:eq(4)').text).not_to be_present end @@ -62,7 +62,7 @@ get :index, params expect(dom.all('table tbody tr').size).to eq 2 role_row = dom.find('table tbody tr:eq(2)') - expect(role_row.find('td:eq(1) a').text).to eq 'TopGroup' + expect(role_row.find('td:eq(1) a:eq(2)').text).to eq 'TopGroup' expect(role_row.find('td:eq(4)').text).to be_present end diff --git a/spec/regressions/roles_controller_spec.rb b/spec/regressions/roles_controller_spec.rb index 98372d3dc6..c487936e64 100644 --- a/spec/regressions/roles_controller_spec.rb +++ b/spec/regressions/roles_controller_spec.rb @@ -38,9 +38,6 @@ let(:scope_params) { { group_id: group.id } } - - before { sign_in(people(:top_leader)) } - # Override a few methods to match the actual behavior. class << self def it_should_redirect_to_show @@ -64,6 +61,7 @@ def it_should_redirect_to_index include_examples 'crud controller', skip: [%w(index), %w(show), %w(new plain)] + let!(:user) { Fabricate(Group::BottomLayer::Leader.name.to_sym, group: group).person } describe_action :get, :new do context '.html', format: :html do @@ -81,4 +79,33 @@ def it_should_redirect_to_index end end + context 'using js' do + + before { sign_in(user) } + + let(:person) { Fabricate(:person) } + + it 'new role for existing person returns new role' do + xhr :post, :create, + group_id: group.id, + role: { group_id: group.id, + person_id: person.id, + type: Group::BottomLayer::Member.sti_name } + + expect(response).to have_http_status(:ok) + is_expected.to render_template('create') + expect(response.body).to include('Bottom One') + end + + it 'creation of role without type returns error' do + xhr :post, :create, + group_id: group.id, + role: { group_id: group.id, person_id: person.id } + + expect(response).to have_http_status(:ok) + is_expected.to render_template('create') + expect(response.body).to include('alert') + end + end + end diff --git a/spec/serializers/person_serializer_spec.rb b/spec/serializers/person_serializer_spec.rb index 4efb5ce269..638e0ec2c9 100644 --- a/spec/serializers/person_serializer_spec.rb +++ b/spec/serializers/person_serializer_spec.rb @@ -3,40 +3,41 @@ # # Table name: people # -# id :integer not null, primary key -# first_name :string -# last_name :string -# company_name :string -# nickname :string -# company :boolean default(FALSE), not null -# email :string -# address :string(1024) -# zip_code :string -# town :string -# country :string -# gender :string(1) -# birthday :date -# additional_information :text -# contact_data_visible :boolean default(FALSE), not null -# created_at :datetime -# updated_at :datetime -# encrypted_password :string -# reset_password_token :string -# reset_password_sent_at :datetime -# remember_created_at :datetime -# sign_in_count :integer default(0) -# current_sign_in_at :datetime -# last_sign_in_at :datetime -# current_sign_in_ip :string -# last_sign_in_ip :string -# picture :string -# last_label_format_id :integer -# creator_id :integer -# updater_id :integer -# primary_group_id :integer -# failed_attempts :integer default(0) -# locked_at :datetime -# authentication_token :string +# id :integer not null, primary key +# first_name :string +# last_name :string +# company_name :string +# nickname :string +# company :boolean default(FALSE), not null +# email :string +# address :string(1024) +# zip_code :string +# town :string +# country :string +# gender :string(1) +# birthday :date +# additional_information :text +# contact_data_visible :boolean default(FALSE), not null +# created_at :datetime +# updated_at :datetime +# encrypted_password :string +# reset_password_token :string +# reset_password_sent_at :datetime +# remember_created_at :datetime +# sign_in_count :integer default(0) +# current_sign_in_at :datetime +# last_sign_in_at :datetime +# current_sign_in_ip :string +# last_sign_in_ip :string +# picture :string +# last_label_format_id :integer +# creator_id :integer +# updater_id :integer +# primary_group_id :integer +# failed_attempts :integer default(0) +# locked_at :datetime +# authentication_token :string +# show_global_label_formats :boolean default(TRUE), not null # # Copyright (c) 2014, CEVI Regionalverband ZH-SH-GL. This file is part of diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 57bea33bf7..c75ed9ea81 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -7,12 +7,14 @@ DB_CLEANER_STRATEGY = :truncation -require 'simplecov' -require 'simplecov-rcov' -SimpleCov.start 'rails' -SimpleCov.coverage_dir 'spec/coverage' -# use this formatter for jenkins compatibility -SimpleCov.formatter = SimpleCov::Formatter::RcovFormatter +if ENV['CI'] + require 'simplecov' + require 'simplecov-rcov' + SimpleCov.start 'rails' + SimpleCov.coverage_dir 'spec/coverage' + # use this formatter for jenkins compatibility + SimpleCov.formatter = SimpleCov::Formatter::RcovFormatter +end ENV['RAILS_ENV'] = 'test' ENV['RAILS_GROUPS'] = 'assets' @@ -53,6 +55,7 @@ config.order = 'random' config.backtrace_exclusion_patterns = [/lib\/rspec/] + config.example_status_persistence_file_path = Rails.root.join('tmp','examples.txt').to_s config.include(MailerMacros) config.include(EventMacros) @@ -120,17 +123,20 @@ Capybara.server_port = ENV['CAPYBARA_SERVER_PORT'].to_i if ENV['CAPYBARA_SERVER_PORT'] Capybara.default_max_wait_time = 10 + require 'capybara-screenshot/rspec' + Capybara::Screenshot.prune_strategy = :keep_last_run + Capybara::Screenshot::RSpec::REPORTERS['RSpec::Core::Formatters::ProgressFormatter'] = + CapybaraScreenshotPlainTextReporter + if ENV['FIREFOX_PATH'] Capybara.register_driver :selenium do |app| require 'selenium/webdriver' Selenium::WebDriver::Firefox::Binary.path = ENV['FIREFOX_PATH'] - Capybara::Selenium::Driver.new(app, :browser => :firefox) + Capybara::Selenium::Driver.new(app, browser: :firefox) end end - if ENV['HEADLESS'] == 'false' - # use selenium-webkit driver - else + if ENV['HEADLESS'] != 'false' require 'headless' headless = Headless.new diff --git a/spec/support/capybara_screenshot_plain_text_reporter.rb b/spec/support/capybara_screenshot_plain_text_reporter.rb new file mode 100644 index 0000000000..c53ebe6460 --- /dev/null +++ b/spec/support/capybara_screenshot_plain_text_reporter.rb @@ -0,0 +1,34 @@ +require 'capybara-screenshot/rspec/base_reporter' +require 'capybara-screenshot/helpers' + +module CapybaraScreenshotPlainTextReporter + extend Capybara::Screenshot::RSpec::BaseReporter + + if RSpec::Core::Version::STRING.to_i <= 2 + enhance_with_screenshot :dump_failure_info + else + enhance_with_screenshot :example_failed + end + + def dump_failure_info_with_screenshot(example) + dump_failure_info_without_screenshot example + output_screenshot_info(example) + end + + def example_failed_with_screenshot(notification) + example_failed_without_screenshot notification + output_screenshot_info(notification.example) + end + + private + + def output_screenshot_info(example) + return unless (screenshot = example.metadata[:screenshot]) + output.puts(long_padding + "HTML screenshot:\nfile://#{screenshot[:html]}") if screenshot[:html] + output.puts(long_padding + "Image screenshot: file://#{screenshot[:image]}") if screenshot[:image] + end + + def long_padding + ' ' + end +end diff --git a/spec/support/group/bottom_layer.rb b/spec/support/group/bottom_layer.rb index 406d80bf08..ba65fbeed3 100644 --- a/spec/support/group/bottom_layer.rb +++ b/spec/support/group/bottom_layer.rb @@ -25,7 +25,7 @@ class LocalGuide < ::Role end class Member < ::Role - self.permissions = [:layer_and_below_read] + self.permissions = [:layer_and_below_read, :finance] end roles Leader, LocalGuide, Member diff --git a/spec/support/group/top_group.rb b/spec/support/group/top_group.rb index 2435f7911f..172c4f64fe 100644 --- a/spec/support/group/top_group.rb +++ b/spec/support/group/top_group.rb @@ -10,7 +10,7 @@ class Group::TopGroup < Group self.event_types = [Event, Event::Course] class Leader < ::Role - self.permissions = [:admin, :layer_and_below_full, :contact_data] + self.permissions = [:admin, :finance, :layer_and_below_full, :contact_data] end class LocalGuide < ::Role diff --git a/spec/utils/changelog_reader_spec.rb b/spec/utils/changelog_reader_spec.rb index 778a6e4044..0ad1e6ff64 100644 --- a/spec/utils/changelog_reader_spec.rb +++ b/spec/utils/changelog_reader_spec.rb @@ -20,6 +20,8 @@ '* change', '* change two', '', # invalid line + '## Version 1.X', + '* far future change', '## Version 2.3', '* change', '## Version 1.1', @@ -36,7 +38,7 @@ subject.send(:parse_changelog_lines, changelog_lines) changelogs = subject.instance_variable_get(:@changelogs) - expect(changelogs.count).to eq(2) + expect(changelogs.count).to eq(3) version11 = changelogs[0] expect(version11.log_entries.count).to eq(3) @@ -45,7 +47,12 @@ expect(version11.log_entries[1]).to eq('change two') expect(version11.log_entries[2]).to eq('another change') - version23 = changelogs[1] + version1x = changelogs[1] + expect(version1x.log_entries.count).to eq(1) + expect(version1x.version).to eq('1.X') + expect(version1x.log_entries[0]).to eq('far future change') + + version23 = changelogs[2] expect(version23.log_entries.count).to eq(1) expect(version23.version).to eq('2.3') expect(version23.log_entries[0]).to eq('change') @@ -75,14 +82,18 @@ v2 = ChangelogVersion.new('2.3') v3 = ChangelogVersion.new('1.11') v4 = ChangelogVersion.new('2.15') - a = [v1, v2, v3, v4] + v5 = ChangelogVersion.new('1.X') + unsorted = [v1, v2, v3, v4, v5] + + sorted = unsorted.sort.reverse - a = a.sort.reverse + expect(sorted[0]).to eq(v4) + expect(sorted[1]).to eq(v2) + expect(sorted[2]).to eq(v5) + expect(sorted[3]).to eq(v3) + expect(sorted[4]).to eq(v1) - expect(a.last).to eq(v1) - expect(a[1]).to eq(v2) - expect(a[2]).to eq(v3) - expect(a.first).to eq(v4) + expect(sorted.map(&:version)).to eq(%w( 2.15 2.3 1.X 1.11 1.1 )) end it 'reads existing changelog file' do @@ -117,4 +128,4 @@ expect(files_path[0]).to eq('CHANGELOG.md') expect(files_path[1]).to eq('files/CHANGELOG.md') end -end \ No newline at end of file +end diff --git a/spec/views/event/participations/_actions_show.html.haml_spec.rb b/spec/views/event/participations/_actions_show.html.haml_spec.rb index d4c6bdf9c8..d0f0f70dc5 100644 --- a/spec/views/event/participations/_actions_show.html.haml_spec.rb +++ b/spec/views/event/participations/_actions_show.html.haml_spec.rb @@ -38,5 +38,4 @@ its(:text) { should eq ' Kontaktdaten ändern' } # space because of icon end end - end diff --git a/spec/views/event/participations/_form.html.haml_spec.rb b/spec/views/event/participations/_form.html.haml_spec.rb index fc8c237761..fa9121f53f 100644 --- a/spec/views/event/participations/_form.html.haml_spec.rb +++ b/spec/views/event/participations/_form.html.haml_spec.rb @@ -34,6 +34,7 @@ allow(controller).to receive_messages(current_user: user) assign(:event, event.decorate) assign(:group, group) + assign(:answers, participation.answers) end context 'course' do diff --git a/spec/views/event/participations/_list.html.haml_spec.rb b/spec/views/event/participations/_list.html.haml_spec.rb index c5f7bb745c..d08a1404c4 100644 --- a/spec/views/event/participations/_list.html.haml_spec.rb +++ b/spec/views/event/participations/_list.html.haml_spec.rb @@ -11,7 +11,6 @@ let(:event) { EventDecorator.decorate(Fabricate(:course, groups: [groups(:top_layer)])) } let(:participation) { Fabricate(:event_participation, event: event) } - #let(:leader) { Fabricate(Event::Role::Leader.name.to_sym, participation: participation) } let(:dom) { render; Capybara::Node::Simple.new(@rendered) } let(:dropdowns) { dom.all('.dropdown-toggle') }