diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d048c6a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +.git/ +Dockerfile +docker-compose.yml + diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..e7cdec9 --- /dev/null +++ b/.env.development @@ -0,0 +1,14 @@ +FRAB_HOST=localhost +FRAB_PROTOCOL=http +FROM_EMAIL=frab@localhost +#FRAB_CURRENCY_UNIT="€" +#FRAB_CURRENCY_FORMAT="%n%u" +#RAILS_SERVE_STATIC_FILES=1 +#EXCEPTION_EMAIL=frab-owner@example.com +#SMTP_ADDRESS=localhost +#SMTP_PORT=587 +#SMTP_DOMAIN=localdomain +#SMTP_USER_NAME=root +#SMTP_PASSWORD=toor +#SMTP_AUTHENTICATION=1 +#SMTP_NOTLS=1 diff --git a/.env.test b/.env.test new file mode 100644 index 0000000..c49d402 --- /dev/null +++ b/.env.test @@ -0,0 +1,3 @@ +FRAB_HOST=frab.test +FRAB_PROTOCOL=http +FROM_EMAIL=frab@frab.test diff --git a/.gitignore b/.gitignore index bf324fe..25840de 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,32 @@ .bundle +.env +.env.local +.env.production +.ruby-version config/database.yml -config/settings.yml db/*.sqlite3 log/*.log tmp/* tmp/**/* public/system index *.swp rerun.txt .sass-cache/ .idea/ .redcar/ +bundler/ +cache/ +gems/ +specifications/ +*~ +.vagrant +doc/ +tags +config/deploy/production.rb +.DS_Store +*.patch +config/deploy/*.rb +config/puma.rb +contrib/ +db/development.sqlite3.* diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..8f8f535 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,99 @@ +AllCops: + TargetRubyVersion: 2.4 + Exclude: + - bin/**/* + - contrib/**/* + - config/**/* + - db/**/* + - tmp/**/* + - Capfile + - Gemfile + - Rakefile + +# Nested module syntax is fine, just be careful with +# scoping, i.e. on include +Style/ClassAndModuleChildren: + Enabled: false + +# Old ruby style syntax may still be needed +Style/HashSyntax: + Enabled: true + +# Never break line due to length, except in data +# vim: set wrap +Metrics/LineLength: + Max: 1024 + +# Rails controllers and such +Metrics/MethodLength: + Max: 20 + +# Use and/or for flow control, but not in boolean assignments +# http://devblog.avdi.org/2010/08/02/using-and-and-or-in-ruby/ +Style/AndOr: + Enabled: false + +# Use not with .select and flow control +Style/Not: + Enabled: false + +# Use { only for single line blocks, but allow block content on its own line to keep line length short +# each { |l| +# l.apply_long_method_name +# } +Style/BlockDelimiters: + Enabled: false + +# Do not use lambda +Style/Lambda: + Enabled: false + +# Allow TODO instead of requiring TODO: +Style/CommentAnnotation: + Enabled: false + +# Do not write 1234 as 1_234 +Style/NumericLiterals: + Enabled: false + +# Relax for controllers with multiple formats +Metrics/AbcSize: + Max: 40 + +# Too spammy +Style/Documentation: + Enabled: false + +# Will probably be default in ruby 3 +Style/FrozenStringLiteralComment: + Enabled: false + +# Use raise if you expect to catch the expception +Style/SignalException: + Enabled: false + +# False positive for if var = value +Lint/AssignmentInCondition: + Enabled: false + +# Too much manual horizontal alignment +Style/AlignHash: + Enabled: false + +Style/AlignParameters: + Enabled: false + +Style/AlignArray: + Enabled: false + +# Vim prefers fixed indent, avoid manual vertical alignment +Style/AlignParameters: + Enabled: true + EnforcedStyle: with_fixed_indentation + +Style/MultilineMethodCallIndentation: + EnforcedStyle: indented + +# Load order is important +Bundler/OrderedGems: + Enabled: false diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..845b5ea --- /dev/null +++ b/.travis.yml @@ -0,0 +1,18 @@ +language: ruby +dist: precise +rvm: + - 2.4.1 +env: + - DB=mysql + - DB=postgresql + - DB=sqlite +bundler_args: --without capistrano doc production --jobs 2 +before_script: + - cp config/database.yml.travis config/database.yml + - sed -i 's/config.eager_load = .*/config.eager_load = true/' config/environments/test.rb + - mysql -e 'create database frab_test' + - psql -c 'create database frab_test' -U postgres +script: + - RAILS_ENV=test bundle exec rails db:schema:load --trace + - bundle exec rails db:test:prepare + - bundle exec rails test diff --git a/Capfile b/Capfile new file mode 100644 index 0000000..afe639d --- /dev/null +++ b/Capfile @@ -0,0 +1,32 @@ +# Load DSL and set up stages +require 'capistrano/setup' + +# Include default deployment tasks +require 'capistrano/deploy' + +require "capistrano/scm/git" +install_plugin Capistrano::SCM::Git + +# Include tasks from other gems included in your Gemfile +# +# For documentation on these, see for example: +# +# https://github.com/capistrano/rvm +# https://github.com/capistrano/rbenv +# https://github.com/capistrano/chruby +# https://github.com/capistrano/bundler +# https://github.com/capistrano/rails +# https://github.com/capistrano/passenger +# +#require 'capistrano/rvm' +require 'capistrano/bundler' +require 'capistrano/rails/assets' +require 'capistrano/rails/migrations' +#require 'capistrano/puma' +#require 'capistrano/passenger' + +require 'dotenv' +Dotenv.load + +# Load custom tasks from `lib/capistrano/tasks` if you have any defined +Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r } diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5abf350 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,37 @@ +FROM ruby:latest + +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y nodejs file imagemagick git && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +RUN adduser --disabled-password --gecos "FRAB" --uid 1000 frab + +COPY . /home/frab/app +RUN chown -R frab:frab /home/frab/app + +USER frab + +WORKDIR /home/frab/app + +RUN bundle install + +RUN cp config/database.yml.template config/database.yml + +VOLUME /home/frab/app/public + +EXPOSE 3000 + +ENV RACK_ENV=production \ + SECRET_KEY_BASE=changeme \ + FRAB_HOST=localhost \ + FRAB_PROTOCOL=http \ + RAILS_SERVE_STATIC_FILES=true \ + CAP_USER=frab \ + FROM_EMAIL=frab@localhost \ + SMTP_ADDRESS=172.17.0.1 \ + SMTP_PORT=25 \ + SMTP_NOTLS=true \ + DATABASE_URL=sqlite3://localhost/home/frab/data/database.db + +CMD ["/home/frab/app/docker-cmd.sh"] diff --git a/Gemfile b/Gemfile index 9178e29..bc91771 100644 --- a/Gemfile +++ b/Gemfile @@ -1,55 +1,109 @@ -source 'http://rubygems.org' +source 'https://rubygems.org' +git_source(:github) do |repo_name| + repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/") + "https://github.com/#{repo_name}.git" +end -gem 'rails', '3.2.11' +install_if -> { RbConfig::CONFIG['target_os'] =~ /(?i-mx:bsd|dragonfly)/ } do + gem 'rb-kqueue', ">= 0.2", platforms: :ruby +end -gem 'mysql2' -gem 'bcrypt-ruby' -gem 'haml' +if ENV['CUSTOM_RUBY_VERSION'] + ruby ENV['CUSTOM_RUBY_VERSION'] # i.e.: '2.3' +end + +gem 'rails', '~> 5.1.0' + +# Use SCSS for stylesheets +gem 'sass-rails' +# Use Uglifier as compressor for JavaScript assets +gem 'uglifier' +# Use CoffeeScript for .coffee assets and views +gem 'coffee-rails' + +gem 'mysql2', group: :mysql +gem 'pg', group: :postgresql +gem 'sqlite3', group: :sqlite3 + +# Use Puma as the app server +gem 'puma' + +# Capistrano for deployment +group :capistrano do + gem 'capistrano', '3.8.2', require: false + gem 'capistrano-rails', require: false + gem 'capistrano-bundler', require: false + gem 'capistrano-rvm', require: false + gem 'capistrano3-puma', require: false +end + +# Use jquery as the JavaScript library +gem 'jquery-rails' +gem 'jquery-migrate-rails' +gem 'jquery-ui-rails' +gem 'rangesliderjs-rails', '~> 2.3' + +# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder +gem 'jbuilder' + +gem 'activemodel-serializers-xml' +gem 'activeresource', github: 'rails/activeresource', branch: 'master' gem 'acts_as_commentable' -gem 'will_paginate' -gem 'paperclip', '< 3.0' -gem 'gravatar-ultimate' -gem 'formtastic', '~> 2.0.2' -gem 'jquery-rails', '2.1.4' -gem 'acts_as_indexed' +gem 'bcrypt' gem 'cocoon' -gem 'paper_trail', '2.3.3' -gem 'localized_language_select', '0.2.0', :git => "git://github.com/oneiros/localized_language_select.git" -gem 'ransack' -gem 'transitions', :require => ["transitions", "active_record/transitions"] -gem 'json' -gem 'therubyracer' -gem 'barista' -gem 'ri_cal' +gem 'devise' +gem 'dotenv-rails' +gem 'github-markdown' +gem 'haml' +gem 'localized_language_select', github: 'frab/localized_language_select', branch: 'master' gem 'nokogiri' -gem 'settingslogic' -gem 'twitter-bootstrap-rails', '< 1.9' -gem 'formtastic-bootstrap' -gem 'prawn' +gem 'paperclip' +gem 'paper_trail' +gem 'prawn', '< 1.0' gem 'prawn_rails' -gem 'ruby-rc4' -gem 'net-ldap' -gem "devise_ldap_authenticatable" -gem 'coffee-rails', " ~> 3.2.0" +gem 'pundit', github: 'elabs/pundit', branch: 'master' +gem 'ransack' +gem 'redcarpet' +gem 'ri_cal' +gem 'roust' +gem 'rqrcode' +gem 'simple_form' +gem 'sucker_punch' +gem 'transitions', require: ['transitions', 'active_record/transitions'] +gem 'will_paginate' +gem 'yard' +gem 'country_select', '~> 3.1', '>= 3.1.1' +gem 'therubyracer' +gem 'mysql2' + +group :production do + gem 'exception_notification' +end group :development, :test do - gem 'ruby-debug', :platforms => :mri_18 - gem 'ruby-debug19', :platforms => :mri_19 + gem 'listen' + gem 'bullet' + gem 'pry-rails' + gem 'pry-byebug' + gem 'letter_opener' + gem 'faker' end group :test do - gem 'minitest' - gem 'factory_girl_rails' - gem 'turn', :require => false + gem 'database_cleaner' + gem 'factory_girl_rails', '~> 4.0' + gem 'shoulda' + gem 'rails-controller-testing' + gem 'minitest-rails-capybara' + gem 'poltergeist' end -group :development do - gem 'hpricot' - gem 'yaml_db' +group :doc do + # gem 'rails-erd' # graph + # gem 'ruby-graphviz', require: 'graphviz' # Optional: only required for graphing end -group :assets do - gem 'sass-rails', " ~> 3.2.0" - gem 'uglifier' -end +# Devise LDAP (for KDE Identity) + +gem "devise_ldap_authenticatable" diff --git a/Gemfile.lock b/Gemfile.lock index 2a89def..ce82932 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,266 +1,442 @@ GIT - remote: git://github.com/oneiros/localized_language_select.git - revision: 12aba5fb945cf50ea3919b3ae774d9205a6cfcc4 + remote: https://github.com/elabs/pundit.git + revision: ac2a25d93ccd044ac3ea777a70ffd5e31db10d3a + branch: master specs: - localized_language_select (0.2.0) - rails (>= 2.3.5) - rails (>= 2.3.5) + pundit (1.1.0) + activesupport (>= 3.0.0) + +GIT + remote: https://github.com/frab/localized_language_select.git + revision: 85df6b97789de6e29c630808b630e56a1b76f80c + branch: master + specs: + localized_language_select (0.3.0) + rails (>= 4.1.0) + +GIT + remote: https://github.com/rails/activeresource.git + revision: 505825bd1ae5c4100f1571d0842a9a99f471f87a + branch: master + specs: + activeresource (5.0.0) + activemodel (>= 5.0, < 6) + activemodel-serializers-xml (~> 1.0) + activesupport (>= 5.0, < 6) GEM - remote: http://rubygems.org/ + remote: https://rubygems.org/ specs: - Ascii85 (1.0.2) - actionmailer (3.2.11) - actionpack (= 3.2.11) - mail (~> 2.4.4) - actionpack (3.2.11) - activemodel (= 3.2.11) - activesupport (= 3.2.11) - builder (~> 3.0.0) - erubis (~> 2.7.0) - journey (~> 1.0.4) - rack (~> 1.4.0) - rack-cache (~> 1.2) - rack-test (~> 0.6.1) - sprockets (~> 2.2.1) - activemodel (3.2.11) - activesupport (= 3.2.11) - builder (~> 3.0.0) - activerecord (3.2.11) - activemodel (= 3.2.11) - activesupport (= 3.2.11) - arel (~> 3.0.2) - tzinfo (~> 0.3.29) - activeresource (3.2.11) - activemodel (= 3.2.11) - activesupport (= 3.2.11) - activesupport (3.2.11) - i18n (~> 0.6) - multi_json (~> 1.0) - acts_as_commentable (4.0.0) - acts_as_indexed (0.8.2) - afm (0.2.0) - ansi (1.4.3) - archive-tar-minitar (0.5.2) - arel (3.0.2) - barista (1.3.0) - coffee-script (~> 2.2) - bcrypt-ruby (3.0.1) - builder (3.0.4) - climate_control (0.0.3) - activesupport (>= 3.0) - cocaine (0.5.1) + actioncable (5.1.2) + actionpack (= 5.1.2) + nio4r (~> 2.0) + websocket-driver (~> 0.6.1) + actionmailer (5.1.2) + actionpack (= 5.1.2) + actionview (= 5.1.2) + activejob (= 5.1.2) + mail (~> 2.5, >= 2.5.4) + rails-dom-testing (~> 2.0) + actionpack (5.1.2) + actionview (= 5.1.2) + activesupport (= 5.1.2) + rack (~> 2.0) + rack-test (~> 0.6.3) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + actionview (5.1.2) + activesupport (= 5.1.2) + builder (~> 3.1) + erubi (~> 1.4) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.0.3) + activejob (5.1.2) + activesupport (= 5.1.2) + globalid (>= 0.3.6) + activemodel (5.1.2) + activesupport (= 5.1.2) + activemodel-serializers-xml (1.0.1) + activemodel (> 5.x) + activerecord (> 5.x) + activesupport (> 5.x) + builder (~> 3.1) + activerecord (5.1.2) + activemodel (= 5.1.2) + activesupport (= 5.1.2) + arel (~> 8.0) + activesupport (5.1.2) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (~> 0.7) + minitest (~> 5.1) + tzinfo (~> 1.1) + acts_as_commentable (4.0.2) + addressable (2.5.1) + public_suffix (~> 2.0, >= 2.0.2) + airbrussh (1.3.0) + sshkit (>= 1.6.1, != 1.7.0) + arel (8.0.0) + bcrypt (3.1.11) + builder (3.2.3) + bullet (5.5.1) + activesupport (>= 3.0.0) + uniform_notifier (~> 1.10.0) + byebug (9.0.6) + capistrano (3.8.2) + airbrussh (>= 1.0.0) + i18n + rake (>= 10.0.0) + sshkit (>= 1.9.0) + capistrano-bundler (1.2.0) + capistrano (~> 3.1) + sshkit (~> 1.2) + capistrano-rails (1.3.0) + capistrano (~> 3.1) + capistrano-bundler (~> 1.1) + capistrano-rvm (0.1.2) + capistrano (~> 3.0) + sshkit (~> 1.2) + capistrano3-puma (3.1.0) + capistrano (~> 3.7) + capistrano-bundler + puma (~> 3.4) + capybara (2.14.4) + addressable + mime-types (>= 1.16) + nokogiri (>= 1.3.3) + rack (>= 1.0.0) + rack-test (>= 0.5.4) + xpath (~> 2.0) + chunky_png (1.3.8) + climate_control (0.2.0) + cliver (0.3.2) + cocaine (0.5.8) climate_control (>= 0.0.3, < 1.0) - cocoon (1.1.2) - coffee-rails (3.2.2) + cocoon (1.2.10) + coderay (1.1.1) + coffee-rails (4.2.2) coffee-script (>= 2.2.0) - railties (~> 3.2.0) - coffee-script (2.2.0) + railties (>= 4.0.0) + coffee-script (2.4.1) coffee-script-source execjs - coffee-script-source (1.4.0) - columnize (0.3.6) - commonjs (0.2.6) - devise (2.2.3) - bcrypt-ruby (~> 3.0) + coffee-script-source (1.12.2) + concurrent-ruby (1.0.5) + countries (2.1.3) + i18n_data (~> 0.8.0) + money (~> 6.9) + sixarm_ruby_unaccent (~> 1.1) + unicode_utils (~> 1.4) + country_select (3.1.1) + countries (~> 2.0) + sort_alphabetical (~> 1.0) + database_cleaner (1.6.1) + devise (4.3.0) + bcrypt (~> 3.0) orm_adapter (~> 0.1) - railties (~> 3.1) - warden (~> 1.2.1) - devise_ldap_authenticatable (0.4.4) - devise (> 1.0.4) - net-ldap (>= 0.1.1) - erubis (2.7.0) - execjs (1.4.0) - multi_json (~> 1.0) - factory_girl (4.2.0) + railties (>= 4.1.0, < 5.2) + responders + warden (~> 1.2.3) + devise_ldap_authenticatable (0.8.5) + devise (>= 3.4.1) + net-ldap (>= 0.6.0, <= 0.11) + dotenv (2.2.1) + dotenv-rails (2.2.1) + dotenv (= 2.2.1) + railties (>= 3.2, < 5.2) + erubi (1.6.1) + exception_notification (4.2.1) + actionmailer (>= 4.0, < 6) + activesupport (>= 4.0, < 6) + execjs (2.7.0) + factory_girl (4.8.0) activesupport (>= 3.0.0) - factory_girl_rails (4.2.1) - factory_girl (~> 4.2.0) + factory_girl_rails (4.8.0) + factory_girl (~> 4.8.0) railties (>= 3.0.0) - formtastic (2.0.2) - rails (~> 3.0) - formtastic-bootstrap (1.1.1) - formtastic - rails (~> 3.1) - gravatar-ultimate (1.0.3) - haml (4.0.0) + faker (1.7.3) + i18n (~> 0.5) + ffi (1.9.18) + github-markdown (0.6.9) + globalid (0.4.0) + activesupport (>= 4.2.0) + haml (5.0.1) + temple (>= 0.8.0) tilt - hashery (2.1.0) - hike (1.2.1) - hpricot (0.8.6) - i18n (0.6.1) - journey (1.0.4) - jquery-rails (2.1.4) - railties (>= 3.0, < 5.0) + httparty (0.15.5) + multi_xml (>= 0.5.2) + i18n (0.8.4) + i18n_data (0.8.0) + jbuilder (2.7.0) + activesupport (>= 4.2.0) + multi_json (>= 1.2) + jquery-migrate-rails (1.2.1) + jquery-rails (4.3.1) + rails-dom-testing (>= 1, < 3) + railties (>= 4.2.0) thor (>= 0.14, < 2.0) - json (1.7.7) - less (2.0.12) - commonjs (~> 0.2.0) - therubyracer (~> 0.9.9) - less-rails (2.1.8) - actionpack (>= 3.1) - less (~> 2.0.7) - libv8 (3.3.10.4) - linecache (0.46) - rbx-require-relative (> 0.0.4) - linecache19 (0.5.12) - ruby_core_source (>= 0.1.4) - mail (2.4.4) - i18n (>= 0.4.0) - mime-types (~> 1.16) - treetop (~> 1.4.8) - mime-types (1.21) - minitest (4.6.1) - multi_json (1.6.1) - mysql2 (0.3.11) - net-ldap (0.3.1) - nokogiri (1.5.6) - orm_adapter (0.4.0) - paper_trail (2.3.3) - rails (~> 3) - paperclip (2.8.0) - activerecord (>= 2.3.0) - activesupport (>= 2.3.2) - cocaine (>= 0.0.2) + jquery-ui-rails (6.0.1) + railties (>= 3.2.16) + launchy (2.4.3) + addressable (~> 2.3) + letter_opener (1.4.1) + launchy (~> 2.2) + libv8 (3.16.14.19) + listen (3.1.5) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) + ruby_dep (~> 1.2) + loofah (2.0.3) + nokogiri (>= 1.5.9) + mail (2.6.6) + mime-types (>= 1.16, < 4) + method_source (0.8.2) + mime-types (3.1) + mime-types-data (~> 3.2015) + mime-types-data (3.2016.0521) + mimemagic (0.3.2) + mini_portile2 (2.2.0) + minitest (5.10.2) + minitest-capybara (0.8.2) + capybara (~> 2.2) + minitest (~> 5.0) + rake + minitest-metadata (0.6.0) + minitest (>= 4.7, < 6.0) + minitest-rails (3.0.0) + minitest (~> 5.8) + railties (~> 5.0) + minitest-rails-capybara (3.0.1) + capybara (~> 2.7) + minitest-capybara (~> 0.8) + minitest-metadata (~> 0.6) + minitest-rails (~> 3.0) + money (6.10.1) + i18n (>= 0.6.4, < 1.0) + multi_json (1.12.1) + multi_xml (0.6.0) + mysql2 (0.4.6) + net-ldap (0.11) + net-scp (1.2.1) + net-ssh (>= 2.6.5) + net-ssh (4.1.0) + nio4r (2.1.0) + nokogiri (1.8.0) + mini_portile2 (~> 2.2.0) + orm_adapter (0.5.0) + paper_trail (7.0.3) + activerecord (>= 4.0, < 5.2) + request_store (~> 1.1) + paperclip (5.1.0) + activemodel (>= 4.2.0) + activesupport (>= 4.2.0) + cocaine (~> 0.5.5) mime-types - pdf-reader (1.3.1) - Ascii85 (~> 1.0.0) - afm (~> 0.2.0) - hashery (~> 2.0) - ruby-rc4 - ttfunk - polyamorous (0.5.0) - activerecord (~> 3.0) - polyglot (0.3.3) - prawn (0.12.0) - pdf-reader (>= 0.9.0) - ttfunk (~> 1.0.2) + mimemagic (~> 0.3.0) + pdf-core (0.1.6) + pg (0.21.0) + poltergeist (1.15.0) + capybara (~> 2.1) + cliver (~> 0.3.1) + websocket-driver (>= 0.2.0) + polyamorous (1.3.1) + activerecord (>= 3.0) + prawn (0.15.0) + pdf-core (~> 0.1.3) + ttfunk (~> 1.1.0) prawn_rails (0.0.11) prawn (>= 0.11.1) railties (>= 3.0.0) - rack (1.4.5) - rack-cache (1.2) - rack (>= 0.4) - rack-ssl (1.3.3) - rack - rack-test (0.6.2) + pry (0.10.4) + coderay (~> 1.1.0) + method_source (~> 0.8.1) + slop (~> 3.4) + pry-byebug (3.4.2) + byebug (~> 9.0) + pry (~> 0.10) + pry-rails (0.3.6) + pry (>= 0.10.4) + public_suffix (2.0.5) + puma (3.9.1) + rack (2.0.3) + rack-test (0.6.3) rack (>= 1.0) - rails (3.2.11) - actionmailer (= 3.2.11) - actionpack (= 3.2.11) - activerecord (= 3.2.11) - activeresource (= 3.2.11) - activesupport (= 3.2.11) - bundler (~> 1.0) - railties (= 3.2.11) - railties (3.2.11) - actionpack (= 3.2.11) - activesupport (= 3.2.11) - rack-ssl (~> 1.3.2) + rails (5.1.2) + actioncable (= 5.1.2) + actionmailer (= 5.1.2) + actionpack (= 5.1.2) + actionview (= 5.1.2) + activejob (= 5.1.2) + activemodel (= 5.1.2) + activerecord (= 5.1.2) + activesupport (= 5.1.2) + bundler (>= 1.3.0, < 2.0) + railties (= 5.1.2) + sprockets-rails (>= 2.0.0) + rails-controller-testing (1.0.2) + actionpack (~> 5.x, >= 5.0.1) + actionview (~> 5.x, >= 5.0.1) + activesupport (~> 5.x) + rails-dom-testing (2.0.3) + activesupport (>= 4.2.0) + nokogiri (>= 1.6) + rails-html-sanitizer (1.0.3) + loofah (~> 2.0) + railties (5.1.2) + actionpack (= 5.1.2) + activesupport (= 5.1.2) + method_source rake (>= 0.8.7) - rdoc (~> 3.4) - thor (>= 0.14.6, < 2.0) - rake (10.0.3) - ransack (0.7.2) - actionpack (~> 3.0) - activerecord (~> 3.0) - polyamorous (~> 0.5.0) - rbx-require-relative (0.0.9) - rdoc (3.12.1) - json (~> 1.4) + thor (>= 0.18.1, < 2.0) + rake (12.0.0) + rangesliderjs-rails (2.3.1) + ransack (1.8.3) + actionpack (>= 3.0) + activerecord (>= 3.0) + activesupport (>= 3.0) + i18n + polyamorous (~> 1.3) + rb-fsevent (0.10.2) + rb-inotify (0.9.10) + ffi (>= 0.5.0, < 2) + rb-kqueue (0.2.5) + ffi (>= 0.5.0) + redcarpet (3.4.0) + ref (2.0.0) + request_store (1.3.2) + responders (2.4.0) + actionpack (>= 4.2.0, < 5.3) + railties (>= 4.2.0, < 5.3) ri_cal (0.8.8) - ruby-debug (0.10.4) - columnize (>= 0.1) - ruby-debug-base (~> 0.10.4.0) - ruby-debug-base (0.10.4) - linecache (>= 0.3) - ruby-debug-base19 (0.11.25) - columnize (>= 0.3.1) - linecache19 (>= 0.5.11) - ruby_core_source (>= 0.1.4) - ruby-debug19 (0.11.6) - columnize (>= 0.3.1) - linecache19 (>= 0.5.11) - ruby-debug-base19 (>= 0.11.19) - ruby-rc4 (0.1.5) - ruby_core_source (0.1.5) - archive-tar-minitar (>= 0.5.2) - sass (3.2.6) - sass-rails (3.2.6) - railties (~> 3.2.0) - sass (>= 3.1.10) - tilt (~> 1.3) - settingslogic (2.0.9) - sprockets (2.2.2) - hike (~> 1.2) - multi_json (~> 1.0) - rack (~> 1.0) - tilt (~> 1.1, != 1.3.0) - therubyracer (0.9.10) - libv8 (~> 3.3.10) - thor (0.17.0) - tilt (1.3.3) - transitions (0.1.5) - treetop (1.4.12) - polyglot - polyglot (>= 0.3.1) - ttfunk (1.0.3) - turn (0.9.6) - ansi - twitter-bootstrap-rails (1.4.3) - actionpack - jquery-rails (>= 1.0) - less-rails (~> 2.1.0) - railties - tzinfo (0.3.35) - uglifier (1.3.0) - execjs (>= 0.3.0) - multi_json (~> 1.0, >= 1.0.2) - warden (1.2.1) + roust (1.8.9) + activesupport (>= 4.0.10) + httparty (>= 0.13.1) + mail (>= 2.5.4) + rqrcode (0.10.1) + chunky_png (~> 1.0) + ruby_dep (1.5.0) + sass (3.4.24) + 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, < 3) + shoulda (3.5.0) + shoulda-context (~> 1.0, >= 1.0.1) + shoulda-matchers (>= 1.4.1, < 3.0) + shoulda-context (1.2.2) + shoulda-matchers (2.8.0) + activesupport (>= 3.0.0) + simple_form (3.5.0) + actionpack (> 4, < 5.2) + activemodel (> 4, < 5.2) + sixarm_ruby_unaccent (1.2.0) + slop (3.6.0) + sort_alphabetical (1.1.0) + unicode_utils (>= 1.2.2) + 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) + sshkit (1.14.0) + net-scp (>= 1.1.2) + net-ssh (>= 2.8.0) + sucker_punch (2.0.2) + concurrent-ruby (~> 1.0.0) + temple (0.8.0) + therubyracer (0.12.3) + libv8 (~> 3.16.14.15) + ref + thor (0.19.4) + thread_safe (0.3.6) + tilt (2.0.7) + transitions (1.2.1) + ttfunk (1.1.1) + tzinfo (1.2.3) + thread_safe (~> 0.1) + uglifier (3.2.0) + execjs (>= 0.3.0, < 3) + unicode_utils (1.4.0) + uniform_notifier (1.10.0) + warden (1.2.7) rack (>= 1.0) - will_paginate (3.0.4) - yaml_db (0.2.3) + websocket-driver (0.6.5) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.2) + will_paginate (3.1.6) + xpath (2.1.0) + nokogiri (~> 1.3) + yard (0.9.9) PLATFORMS ruby DEPENDENCIES + activemodel-serializers-xml + activeresource! acts_as_commentable - acts_as_indexed - barista - bcrypt-ruby + bcrypt + bullet + capistrano (= 3.8.2) + capistrano-bundler + capistrano-rails + capistrano-rvm + capistrano3-puma cocoon - coffee-rails (~> 3.2.0) + coffee-rails + country_select (~> 3.1, >= 3.1.1) + database_cleaner + devise devise_ldap_authenticatable - factory_girl_rails - formtastic (~> 2.0.2) - formtastic-bootstrap - gravatar-ultimate + dotenv-rails + exception_notification + factory_girl_rails (~> 4.0) + faker + github-markdown haml - hpricot - jquery-rails (= 2.1.4) - json - localized_language_select (= 0.2.0)! - minitest + jbuilder + jquery-migrate-rails + jquery-rails + jquery-ui-rails + letter_opener + listen + localized_language_select! + minitest-rails-capybara mysql2 - net-ldap nokogiri - paper_trail (= 2.3.3) - paperclip (< 3.0) - prawn + paper_trail + paperclip + pg + poltergeist + prawn (< 1.0) prawn_rails - rails (= 3.2.11) + pry-byebug + pry-rails + puma + pundit! + rails (~> 5.1.0) + rails-controller-testing + rangesliderjs-rails (~> 2.3) ransack + rb-kqueue (>= 0.2) + redcarpet ri_cal - ruby-debug - ruby-debug19 - ruby-rc4 - sass-rails (~> 3.2.0) - settingslogic + roust + rqrcode + sass-rails + shoulda + simple_form + sqlite3 + sucker_punch therubyracer transitions - turn - twitter-bootstrap-rails (< 1.9) uglifier will_paginate - yaml_db + yard + +BUNDLED WITH + 1.16.1 diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..1b2be7a --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,104 @@ +## Development Setup + +Basically, to get started you need git, ruby (>= 2.4) and bundler +and follow these steps: + +1) Install nodejs: + +frab needs a javascript runtime. You should use +nodejs, as it is easier to install than v8. + + apt-get install nodejs + +2) Install Imagemagick and `file`: + +These are dependencies of the paperclip gem. Imagemagick +tools need to be installed to identify and resize images. + +Imagemagick and file should be easy to install using your OS's +preferred package manager (apt-get, yum, brew etc.). + +3) Clone the repository + + git clone git://github.com/frab/frab.git + +4) cd into the directory: + + cd frab + +5) Modify settings: + +Settings are defined via environment variables. frab uses dotenv files to +set these variables. The variables for development mode are set in `.env.development`. +You can also use `.env.local` for local overrides. + +6) Run setup + + bin/setup + +10) Start the server + +To start frab in the development environment simply run + + rails server + +Navigate to http://localhost:3000/ and login as +"admin@example.org" with password "test123". + +## Vagrant Server + +frab can more easily be tested by using vagrant with chef recipes taking care of the installation process. +More information can be found in these github projects: + +* [frab/vagrant-frab](https://github.com/frab/vagrant-frab) +* [frab/chef-frab](https://github.com/frab/chef-frab) + + +## Docker + +See [Docker Readme](README.docker.md) + +## Production Deployment + +1) Installing database drivers + +Instead of running `bin/setup` you need to run bundle install manually, so +you can choose your database gems. To avoid installing database drivers you don't +want to use, exclude drivers with + + bundle install --without="postgresql mysql" + +2) Create (and possibly modify) the database configuration: + + cp config/database.yml.template config/database.yml + +3) Configuration + +In Production make sure the config variables are set, copy and edit the file +`env.example` to `.env.production`. + +4) Precompile assets + + rake assets:precompile + +5) Security considerations + +If you are running frab in a production environment you have to +take additional steps to build a secure and stable site. + +* Change the password of the initial admin account +* Change the initial secret token +* Add a content disposition header, so attachments get downloaded and +are not displayed in the browser. See `./public/system/attachments/.htaccess` for an example. +* Add a gem like `exception_notification` to get emails in case of errors. + +6) Start the server + +To start frab in the production environment run + + RACK_ENV=production bundle rails s + +Note that when seeding the database in production mode, the password for +admin@example.org will be a random one. It will be printed to the console +in when `rake db:seed` is invoked. + diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..21cd7da --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: bundle exec rails server -p $PORT -e $RACK_ENV diff --git a/README.PaaS.md b/README.PaaS.md new file mode 100644 index 0000000..f6df998 --- /dev/null +++ b/README.PaaS.md @@ -0,0 +1,95 @@ +# Deploying Frab with Dokku + +[Dokku](http://dokku.viewdocs.io/dokku/) is a Platform-as-a-Service (PaaS) engine which allows for simple `git push` deployments. +It builds on [`herokuish`](https://github.com/gliderlabs/herokuish) and is highly customizable via [plugins](http://dokku.viewdocs.io/dokku/plugins/). + +To deploy a Frab application with `dokku`, please proceed as follows from within your local source repository. + +## 1. Setting up Dokku + +Given you have access to your Dokku service via a simple shell alias (`alias dokku='ssh -t dokku@'`) and `dokku version` works, you will also need to install [the PostgreSQL](https://github.com/dokku/dokku-postgres) and [Let's Encrypt](https://github.com/dokku/dokku-letsencrypt) plugins. + +You can then proceed setting up your application. + +``` +dokku create +``` + +Set up your Ruby version: + +``` +dokku config:set CUSTOM_RUBY_VERSION 2.3 +``` + +## 2. Setting up frab + +For your application you need + +1. an [environmental configuration](http://12factor.net/config), +2. an [attached database](http://12factor.net/backing-services) and +3. a valid TLS setup due to Rails' [CSRF](https://en.wikipedia.org/wiki/Cross-site_request_forgery) protection. + +### Environmental configuration + +Your local `.env` environment file could then look similarly to + +``` +SECRET_KEY_BASE= +FRAB_HOST= +FRAB_PROTOCOL= +FROM_EMAIL= +SMTP_ADDRESS= +SMTP_PORT=25 +SMTP_NOTLS= +SMTP_USER_NAME= +SMTP_PASSWORD= +BUNDLE_WITHOUT=development:test:mysql:sqlite3 +RAILS_SERVE_STATIC_FILES=true +``` + +Pipe this configuration to Dokku with + + dokku config:set `paste -d " " -s .env` + +### Database setup + +The associated database service is created and linked with + + dokku postgresql:create + dokku postgresql:link + +`dokku config ` should now report your whole configuration. + +### TLS setup + +This will only work after your application is (already partly) running, due to the way the Let's Encrypt plugin works, so let's + +## 3. Deploy Frab! + +Add the desired `APP_FQDN` domain to the application and remove the standard Dokku subdomain `APP_NAME.DOKKU_HOST` for later generation of a valid TLS certificate. + +### domain configuration + + dokku domains:add + dokku domains:remove + +Only then issue + +### git deployment + + git remote add dokku dokku@: + git push dokku master + +### TLS setup + +To omit an appearing **502 Bad Gateway** error, we finish the TLS setup with + + dokku letsencrypt + +After this has completed successfully, manually load the database schema + + dokku run bundle exec rake db:setup + +--- + +That's it. Your application should be running at `://.`. diff --git a/README.docker.md b/README.docker.md new file mode 100644 index 0000000..0e63c5c --- /dev/null +++ b/README.docker.md @@ -0,0 +1,60 @@ +# Docker Setup + +Frab can also be run inside a Docker container. Basic familiarity with docker is assumed in this guide. + +In addition to a `Dockerfile` a basic `docker-compose.yml` file is also provided. + + +## Downloading the Docker Image + +To download a pre-built docker image for frab from the [Docker Hub](https://hub.docker.com/r/frab/frab/): + + +``` +docker pull frab/frab +``` + +## Building the Docker Image + +You can also build the image yourself: + + +``` +docker-compose build +``` + +or + +``` +docker build -t frab/frab . +``` + + +## Configuration + +The `Dockerfile` sets some basic default environment variables for frab to use including a sqlite3 database. However you should tune them to your own needs. This can be done by editing the `docker-compose.yml` file or passing the environment variables to the `docker run` command with the `-e` flag. + +At a minimum you should change the default `SECRET_KEY_BASE` variable. + +### Database Configuration + +The default setup uses a sqlite3 database located in `/home/frab/data`. If you want it to persist across container restarts you should add a docker volume to that directory. Alternatively you can pass a `DATABASE_URL` environment variable to use another database like postgresql or mysql. + +The example docker-compose file used another postgres container as a database. + +# Running + +To run frab with docker-compose just run: + +``` +docker-compose up +``` + +The initial admin username and password will be printed to stdout on first run. + + +To start the containers as a service run: + +``` +docker-compose up -d +``` diff --git a/README.md b/README.md index d187681..06bb155 100644 --- a/README.md +++ b/README.md @@ -1,115 +1,113 @@ # frab - conference management system -frab is a web-based conference planning and management system. -It helps to collect submissions, to manage talks and speakers +frab is a web-based conference planning and management system. +It helps to collect submissions, to manage talks and speakers and to create a schedule. +[![Build Status](https://travis-ci.org/frab/frab.svg?branch=master)](https://travis-ci.org/frab/frab) +[![Code Climate](https://codeclimate.com/github/frab/frab.png)](https://codeclimate.com/github/frab/frab) +[![Docker Build Status](https://img.shields.io/docker/build/frab/frab.svg)](https://hub.docker.com/r/frab/frab/) + ## Background -frab was created for the organization of FrOSCon 2011 (http://www.froscon.de). +frab was originally created for the organization of [FrOSCon 2011](http://www.froscon.de). FrOSCon has previously used pentabarf (http://pentabarf.org), and although frab is a completely new implementation, it borrows heavily from pentabarf. Both FrOSCon and frab owe a lot to pentabarf. But sadly, pentabarf seems to be abandoned. And several problems make it hard to maintain. Thus we decided to create a new system. -## Current status +## Current Status frab is under heavy development. There is no stable release yet. You may want to try to use frab regardless, but be warned, that it may be a rocky ride. -That being said, frab has been used to organize FrOSCon 2011, a +That being said, frab has been used to organize FrOSCon since 2011, a conference with more than 100 talks (and as many speakers) in more than 5 parallel tracks (plus devrooms) over 2 days. +The [frab wiki](https://github.com/frab/frab/wiki) hosts a list of conferences using frab. +Take a look at the [screenshots](https://github.com/frab/frab/wiki/Screenshots) to get an idea +of what frab does. The [manual](https://github.com/frab/frab/wiki/Manual) can be found in the wiki. + ## Installing -frab is a pretty standard Ruby on Rails (version 3.1) application. +frab is a pretty standard Ruby on Rails (version 5.0) application. There should be plenty of tutorials online on how to install, deploy and setup these. -Basically, to get started you need git, ruby (>= 1.9.2) and bundler -and follow these steps: - -1) Clone the repository - - git clone git://github.com/oneiros/frab.git +See [installation](INSTALL.md) for more frab specific information. -2) cd into the directory: +## Rake Tasks - cd frab +### Export / Import conferences -3) Install all necessary gems: +Creates a folder under tmp/frab\_export containing serialized data and +all attachments: - bundle install + RAILS_ENV=production CONFERENCE=acronym rake frab:conference_export -4) Install Imagemagick: +Import a conference into another frab: -This is a dependency of the paperclip gem. Imagemagick -tools need to be installed to identify and resize images. + RAILS_ENV=production rake frab:conference_import -Imagemagick should be easy to install using your OS's -preferred package manager (apt-get, yum, brew etc.). - -5) Create (and possibly modify) the database configuration: +### Sending Mails - cp config/database.yml.template config/database.yml + RAILS_ENV=production rake frab:bulk_mailer subject="Conference Invite" from=conference@example.org emails=emails.lst body=body.txt.erb -frab bundles all three built-in rails database drivers. -And it should work with all three, although it is best tested -with MySQL and SQLite3 (for development). +### Migrating from pentabarf -6) Create and modify settings: +frab comes with a script that offers limited capabilities of +migrating data from pentabarf. For it to work, you need access +to pentabarf's database and configure it in config/database.yml +under the key "pentabarf". - cp config/settings.yml.template config/settings.yml +Then simply run -7) Create and setup the database + rake pentabarf:import:all - rake db:setup +Please note, that the script has not been tested with HEAD +and will most probably not work. If you still want to try it +out, checkout the code at the revision the script was last +changed at and upgrade the code and migrate the database +from there. -8) Precompile assets (only needed for production) +### Create fake data - rake assets:precompile +For development, it might be helpful to have some fake data around that allows for better testing. +The following command will create a bunch of tracks, persons and events in a random existing +conference. Call it multiple times if you need more records. -9) Start the server + rake frab:add_fake_data -To start frab in the development environment simply run +You may also call the following tasks manually. - rails server + rake frab:add_fake_tracks + rake frab:add_fake_persons -To start frab in the production environment make sure you -did not skip step 8 and run: +## Ticket Server - rails server -e production +frab supports OTRS, RT and Redmine ticket servers. Instead of sending +event acceptance/rejection mails directly to submitters, frab adds +a ticket to a request tracker. -(Note that for a "real" production environment you -probably do not want to use this script, but rather something -like unicorn or passenger.) +The ticket server type can be configured for every conference. -Navigate to http://localhost:3000/ and login as -"admin@example.org" with password "test123". +The iPHoneHandle support needs to be installed in OTRS. -## Migrating from pentabarf -frab comes with a script that offers limited capabilities of -migrating data from pentabarf. For it to work, you need access -to pentabarf's database and configure it in config/database.yml -under the key "pentabarf". + rake frab:add_fake_events -Then simply run +## Contact - rake pentabarf:import:all +For updates and discussions around frab, please join our mailinglist -Please note, that the script has not been tested with HEAD -and will most probably not work. If you still want to try it -out, checkout the code at the revision the script was last -changed at and upgrade the code and migrate the database -from there. + frab (at) librelist.com - to subscribe just send a mail to it ## License frab is licensed under an MIT-License. It bundles some third-party libraries and assets that might be licensed differently. See LICENSE. diff --git a/README.pentabarf.md b/README.pentabarf.md new file mode 100644 index 0000000..4c44104 --- /dev/null +++ b/README.pentabarf.md @@ -0,0 +1,77 @@ +# frab - pentabarf import + +These notes may help to import data from a pentabarf postgresql database. + +Using postgresql as a database for frab is still somewhat untested. The pentabarf +import is however likely to fail, as pentabarf uses text fields instead of char(255) + +Imagemagick needs to be installed as we will convert pjpeg and tiff to png. + +## postgresql installation + +Install postgresql + +## postgresql setup + +* make it listen on localhost +* create a psql user and grant some access on relations +* add a pentabarf entry for the postgresql database to your rails db environment + +## postgresql copy + +Make a copy of your postgresql database, as we need to do some changes + + pg_dump -Fc DBNAME > backup.dump + createdb NEWNAME + pg_restore -O -d NEWNAME backup.dump > /dev/null + +## postgresql permissions + +Grant all permissions on the database copy to the import user account: + + psql NEWNAME + -- generate the grant statements + select 'grant all on '||schemaname||'.'||tablename||' to frab;' from pg_tables + order by schemaname, tablename; + -- copy&paste the generated statements into psql + + -- in case you re-created the NEWNAME copy, re-grant permissions to the user + REVOKE ALL ON SCHEMA public FROM frab; + GRANT ALL ON SCHEMA public TO frab; + REVOKE ALL ON SCHEMA auth FROM frab; + GRANT ALL ON SCHEMA auth TO frab; + +## data migration + +Conference acronyms need to be within /^[a-zA-Z0-9_-]*$/ +Whitespaces are removed automatically, but you need to replace unicode characters manually. + + -- conference acronyms appear in URLs, they may not contain whitespace in frab: + UPDATE conference SET acronym = replace(acronym, ' ', ''); + UPDATE conference SET acronym = 'mrmcdX' where conference_id=86; -- mrmcdⅩ + +## import + +Delete any old mappings from previous imports. Maybe delete the old filess, too. + + rm tmp/*mappings.yml + rm -fr public/system + RAILS_ENV=production rake db:reset + RAILS_ENV=production rake pentabarf:import:all + +## testing + +You can check on the barf data like this: + + RAILS_ENV="development" rails console + @p = PentabarfImportHelper.new + @barf = @p.instance_variable_get('@barf') + @barf.select_all("SELECT * FROM conference") + +## privileges + +You maybe want to drop all users to the coordinator role, to start fresh. + +User.all.select { |u| u.role == "admin" or u.role == "orga" }.each { |u| puts "dropping ${u.email}"; u.role = "coordinator"; u.save } + + diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index b9c772b..4d86c7a 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -1,14 +1,28 @@ +// 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 any plugin's vendor/assets/javascripts directory 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 +// compiled file. +// +// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details +// about supported directives. +// //= require jquery +//= require jquery-migrate-min //= require jquery_ujs //= require jquery-ui +//= require jquery-ui-timepicker-addon +//= require bootstrap-alerts +//= require bootstrap-dropdown +//= require bootstrap-modal +//= require bootstrap-twipsy +//= require bootstrap-popover +//= require bootstrap-scrollspy +//= require bootstrap-tabs +//= require bootstrap-buttons +//= require rangeslider //= require cocoon -//= require twitter/bootstrap - -$(function() { - $('.topbar').dropdown(); - $('.alert-message').alert(); - $('a[data-original-title]').popover(); - $('div.date input').datepicker({ - dateFormat: "yy-mm-dd" - }); -}); +//= require_tree . diff --git a/app/assets/javascripts/bootstrap.js.coffee b/app/assets/javascripts/bootstrap.js.coffee new file mode 100644 index 0000000..7ccc07c --- /dev/null +++ b/app/assets/javascripts/bootstrap.js.coffee @@ -0,0 +1,4 @@ +jQuery -> + $("a[rel=popover]").popover() + $(".tooltip").tooltip() + $("a[rel=tooltip]").tooltip() diff --git a/app/assets/javascripts/color_pickers.js.coffee b/app/assets/javascripts/color_pickers.js.coffee new file mode 100644 index 0000000..e29da51 --- /dev/null +++ b/app/assets/javascripts/color_pickers.js.coffee @@ -0,0 +1,10 @@ +addColorPickers = -> + $('input.color').ColorPicker( + onSubmit: (hsb, hex, rgb, el) -> + $(el).val(hex) + $(el).ColorPickerHide() + onBeforeShow: -> + $(this).ColorPickerSetColor(this.value) + ) +$ -> + addColorPickers() diff --git a/app/assets/javascripts/event.js.coffee b/app/assets/javascripts/event.js.coffee new file mode 100644 index 0000000..3cad40b --- /dev/null +++ b/app/assets/javascripts/event.js.coffee @@ -0,0 +1,31 @@ +rangeSlider = -> + $('.category-slider').rangeslider( + polyfill: false + ) + $(document).on 'cocoon:after-insert', (event, insertedItem) -> + $(insertedItem).find('input[type="range"]').rangeslider( + polyfill: false + ) + $(document).on 'input', '.category-slider', (event) -> + $('.category-output-' + event.target.getAttribute('category')).html(event.target.value + ' %') +checkbox_click_listener = -> + $('.classifier-checkbox').on 'change', (event) -> + box = $(event.currentTarget) + classifier_id = box.attr('name').replace(/^classifier-checkbox-/, '') + classifier_remove_link = $('#' + "remove_classifier_#{classifier_id}") + + # trigger the hidden cocoon dynamic links + if not box.is(':checked') + classifier_remove_link.trigger('click') + return + + # if we removed a classifier slider before, unremove it and show it again + exists = $(".classifier-block-#{classifier_id}") + if exists.length + exists.show() + classifier_remove_link.prev("input[type=hidden]").val(false) + else + box.prev('.add_fields').trigger('click') +$ -> + rangeSlider() + checkbox_click_listener() diff --git a/app/assets/javascripts/initialize.js b/app/assets/javascripts/initialize.js new file mode 100644 index 0000000..ca12e6a --- /dev/null +++ b/app/assets/javascripts/initialize.js @@ -0,0 +1,9 @@ +$(function() { + $('.topbar').dropdown(); + $('.alert-message').alert(); + $('a[data-original-title]').popover(); + $('[data-function="toggle"]').click(function(){ + var args = $(this).data("args"); + $(args.target).toggle(); + }); +}); diff --git a/app/assets/javascripts/notifications.js.erb b/app/assets/javascripts/notifications.js.erb new file mode 100644 index 0000000..c121af7 --- /dev/null +++ b/app/assets/javascripts/notifications.js.erb @@ -0,0 +1,35 @@ +$(function() { + + NotificationDefaults = { + + fill: function(options) { + var topDiv = options.topDiv; + var url = options.url; + NotificationDefaults._fetch(topDiv, url); + }, + + _fetch: function(topDiv, url) { + var code = topDiv.find('select').val(); + $.ajax({ + type: "GET", + dataType: "json", + url: url, + data: {'code':code}, + success: function(result){ + var texts = result.notification; + var inputs = topDiv.find('input[type=text]'); + $(inputs.get(0)).val(texts.accept_subject); + $(inputs.get(1)).val(texts.reject_subject); + $(inputs.get(2)).val(texts.schedule_subject); + inputs = topDiv.find('textarea'); + $(inputs.get(0)).val(texts.accept_body); + $(inputs.get(1)).val(texts.reject_body); + $(inputs.get(2)).val(texts.schedule_body); + } + }); + }, + + }; + +}); + diff --git a/app/assets/javascripts/raty.js b/app/assets/javascripts/raty.js new file mode 100644 index 0000000..d28eae5 --- /dev/null +++ b/app/assets/javascripts/raty.js @@ -0,0 +1,26 @@ +$(document).ready(function(){ + $('div[data-raty-input="on"]').each(function(){ + var source = $(this).data('source'); + $(this).raty({ + half: true, + path: $(this).data('path'), + starOn: $(this).data('star-on'), + starOff: $(this).data('star-off'), + starHalf: $(this).data('star-half'), + score: $(source).val(), + scoreName: $(this).data('target') + }) + }); + + $('div[data-raty="on"]').each(function(index) { + $(this).raty({ + half: true, + path: $(this).data('path'), + starOn: $(this).data('star-on'), + starOff: $(this).data('star-off'), + starHalf: $(this).data('star-half'), + readOnly: true, + score: $(this).data('rating') + }); + }); +}); diff --git a/app/assets/javascripts/schedule.js.coffee b/app/assets/javascripts/schedule.js.coffee new file mode 100644 index 0000000..960d7c6 --- /dev/null +++ b/app/assets/javascripts/schedule.js.coffee @@ -0,0 +1,150 @@ +# Schedule Editor +update_event_position = (event) -> + td = $(event).data("slot") + $(event).css("position", "absolute") + $(event).css("left", td.offset().left) + $(event).css("top", td.offset().top) + return + +update_unscheduled_events = (track_id = "") -> + $.ajax( + url: $("form#update-track").attr("action"), + data: {track_id: track_id}, + dataType: "html", + success: (data) -> + $("ul#unscheduled-events").html(data) + return + ) + return + +add_event_to_slot = (event, td, update = true) -> + event = $(event) + td = $(td) + event.data("slot", td) + td.append($(event)) + update_event_position(event) + if update + event.data("time", td.data("time")) + event.data("room", td.data("room")) + $.ajax( + url: event.data("update-url"), + data: {"event": {"start_time": td.data("time"), "room_id": td.parents("table.room").data("room-id")}}, + type: "PUT", + dataType: "script", + success: -> + event.effect('highlight') + return + ) + return + +make_draggable = (element) -> + element.draggable(revert: "invalid", opacity: 0.4, cursorAt: {left: 5, top: 5}) + true + +$ -> + $("body").delegate("div.event", "mouseenter", -> + event_div = $(this) + return if event_div.find("a.close").length > 0 + unschedule = $("×") + unschedule.addClass("close").addClass("small") + event_div.prepend(unschedule) + unschedule.click (click_event) -> + $.ajax( + url: event_div.data("update-url"), + data: {"event": {"start_time": null, "room_id": null}}, + type: "PUT", + dataType: "script", + success: -> + event_div.remove() + update_unscheduled_events() + return + ) + click_event.stopPropagation() + click_event.preventDefault() + false + return + ) + $("body").delegate("div.event", "mouseleave", -> + $(this).find("a.close").remove() + return + ) + $("body").delegate("div.event", "click", (click_event) -> + click_event.stopPropagation() + click_event.preventDefault() + false + ) + + # Buttons + for button in $("a.toggle-room") + $(button).click -> + current_button = $(this) + $("table[data-room='" + current_button.data('room') + "']").toggle() + if current_button.hasClass("success") + current_button.removeClass("success") + else + current_button.addClass("success") + for event in $("table.room div.event") + update_event_position(event) + true + preventDefault() + false + true + + $("a#hide-all-rooms").click -> + $("a.toggle-room").removeClass("success") + $("table.room").hide() + false + + # Track filter + $("select#track_select").change -> + update_unscheduled_events($(this).val()) + true + + for timeslot in $("table.room td") + $(timeslot).droppable( + hoverClass: "event-hover", + tolerance: "pointer", + drop: (event, ui) -> + add_event_to_slot(ui.draggable, this) + true + ) + true + + $("#add-event-modal").modal('hide') + $("body").delegate("table.room td", "click", (click_event) -> + td = $(this) + $("#add-event-modal #current-time").html(td.data("time")) + $("ul#unscheduled-events").undelegate("click") + $("ul#unscheduled-events").delegate("li a", "click", (click_event) -> + li = $(this).parent() + new_event = $("
") + new_event.html(li.children().first().html()) + new_event.addClass("event") + new_event.attr("id", li.attr("id")) + new_event.css("height", li.data("height")) + new_event.data("update-url", li.data("update-url")) + $("#event-pane").append(new_event) + add_event_to_slot(new_event, td) + make_draggable(new_event) + li.remove() + $("#add-event-modal").modal('hide') + click_event.preventDefault() + false + ) + $("#add-event-modal").modal('show') + click_event.stopPropagation() + false + ) + + for event in $("div.event") + if $(event).data("room") and $(event).data("time") + starting_cell = $("table[data-room='" + $(event).data("room") + "']").find("td[data-time='" + $(event).data("time") + "']") + add_event_to_slot(event, starting_cell, false) + make_draggable($(event)) + true + + # for new_pdf view + $("#select_all_rooms").click -> + $('input[name^=room_ids]').attr('checked', this.checked); + + return diff --git a/app/assets/stylesheets/admin.css b/app/assets/stylesheets/admin.css index 287739b..d910caa 100644 --- a/app/assets/stylesheets/admin.css +++ b/app/assets/stylesheets/admin.css @@ -1,3 +1,5 @@ /* *= require colorpicker + *= require jquery.raty +//= require jquery-ui-timepicker-addon */ diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 7c99fc3..44942bd 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -1,7 +1,7 @@ /* - *= require formtastic-bootstrap - *= require twitter/bootstrap - *= require formtastic_changes - *= require jquery-ui-bootstrap + *= require bootstrap + *= require jquery-ui-bootstrap *= require frab -*/ + *= require rangeslider + *= require_self +*/ \ No newline at end of file diff --git a/app/assets/stylesheets/frab.css b/app/assets/stylesheets/frab.css index 60e4c50..9e62ce7 100644 --- a/app/assets/stylesheets/frab.css +++ b/app/assets/stylesheets/frab.css @@ -1,129 +1,299 @@ body { padding-top: 40px; background-color: #eeeeee; } +.topbar .fill { + padding: 0 1em; +} + +.topbar .container { + width: 100%; +} + .main-content { background-color: #fff; border-radius: 0 0 6px 6px; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15); margin: 0 -20px; padding: 20px; } section { padding-top: 20px; } .image { text-align: center; vertical-align: middle; border: 1px solid #DDDDDD; border-radius: 4px 4px 4px 4px; box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075); padding: 4px; } .image.small { width: 32px; height: 32px; } p.small{ font-size: 8px; } .image.large { width: 128px; height: 128px; } .image img { vertical-align: middle; } #my_rating { padding-top: 8px; } div.rating { white-space: nowrap; } table.room { width: 150px; margin: 10px; float: left; border-collapse: collapse; } table.room thead tr th { padding: 5px; } table.room tbody tr { margin: 0px; } table.room tbody tr td { border: 1px solid white; height: 20px; padding: 0px; } table.room tbody tr td.event-hover { background: #FFFF00; } div.event { background: #33EE33; width: 143px; z-index: 10; font-size: 10px; -moz-border-radius: 3px; -webkit-border-radius: 3px; border-radius: 3px; padding: 3px; } div.event.fatal { background: #EE3333; } div.event.warning { background: #EEEE00; } div.unscheduled-event { background: #F9F9F9; border: 1px solid #EEEEEE; -moz-border-radius: 3px; -webkit-border-radius: 3px; border-radius: 3px; padding: 3px; margin: 5px; } table.room tr.odd {background: #F9F9F9} table.room tr.even {background: #EEEEEE;} span.right { float: right; } -td.buttons { width: 160px; } +td.buttons { white-space: nowrap; } +td.nowrap { white-space: nowrap; } a.assoc { margin-left: 150px; } textarea { height: auto; width: 420px; } -ul#unscheduled-events{ - overflow: auto; +ul#unscheduled-events { height: 400px; } +div#add-event-modal { + overflow: auto; +} + +p.availability span { + float: right; + margin-left: 5px; + margin-right: 140px; + display: block; +} + +.dl-horizontal { + clear: both; +} +.dl-horizontal, .dl-horizontal { + content: ""; + display: table; + line-height: 0; +} + +.dl-horizontal dt { + clear: left; + float: left; + overflow-x: hidden; + overflow-y: hidden; + text-align: right; + text-overflow: ellipsis; + white-space: nowrap; + width: 160px; +} + + +.dl-horizontal dd { + margin-left: 180px; +} + +.info-block { + background-color: #ddd; + border-radius: 5px; + padding: 0.2em; +} +.rangeslider--horizontal { + height: 10px !important; + width: 430px !important; +} + +.rangeslider--horizontal .rangeslider__handle { + height: 20px; + width: 20px; + top: -5px !important; +} +.rangeslider__fill { + background-color: #239d6a !important; +} +.category-output { + margin-top: 8px; + float: left; +} + +.nested-fields .event_event_classifiers_value { + float: left; +} + +.nested-fields .event_event_classifiers_value .input { + margin-left: 20px; + margin-right: 10px; + margin-top: 10px; +} + +.nested-fields .event_event_classifiers_value .help-block { + margin-top: 1em; +} + +#classifier_sliders fieldset.inputs { + margin-bottom: 0 !important; +} + +.classifier-meter { + width: 8em; + margin-right: 0.5em; +} + +ul.event_classifiers_checkboxes { + list-style-type: none; + padding: 0; + margin: 0; + columns: 2; + width: 430px; +} + +ul.event_classifiers_checkboxes li { + margin: 0; + padding: 0.5em 0; +} + +ul.event_classifiers_checkboxes input[type=checkbox] { + margin-right: 0.5em; +} +.help-block { + width: 430px; +} + +.panel { + margin-bottom: 20px; + background-color: #fff; + border: 1px solid #ededed; + border-radius: 4px; + -webkit-box-shadow: 0 1px 1px rgba(0,0,0,.05); + box-shadow: 0 1px 1px rgba(0,0,0,.05); +} + +.panel-heading { + border-bottom: 1px solid transparent; + border-top-left-radius: 3px; + border-top-right-radius: 3px; + background-color: #2b8ddb; + padding: 0px; +} + +.panel-heading > center > h3{ + color: white; +} + +.panel-body { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + + padding: 10px; +} + +.panel-form-group { + margin-top: 10px; +} + +.panel-form-group > input { + width: 97%; +} + +.login-button { + margin-top: 5px; +} + +#new_user > fieldset > div > label { + text-align: left; + font-weight: 800; + margin-bottom: 2px; +} + +.grouped_checkboxes > div.clearfix.stringish.boolean.optional > div > label { + width: 15px; +} + +.grouped_checkboxes > .boolean { + display: inline-block; +} + +.grouped_checkboxes > .boolean > div { + margin-left: 130px; +} -.registration textarea { - height: 80px !important; +.grouped_checkboxes > .boolean > label { + width: 110px; } -#registration textarea { - height: 80px !important; +#edit_event_1 > fieldset:nth-child(5) > div.clearfix.stringish.boolean.optional.event_feedback_wanted > div > span { + display: inline-block; + padding-left: 10px; + width: 100%; } \ No newline at end of file diff --git a/app/assets/stylesheets/public_schedule.css b/app/assets/stylesheets/public_schedule.css index c4965c7..1270d4e 100644 --- a/app/assets/stylesheets/public_schedule.css +++ b/app/assets/stylesheets/public_schedule.css @@ -1,333 +1,510 @@ -/* - *= require formtastic - *= require formtastic_changes + /* *= require_self * */ +article, aside, details, figcaption, figure, footer, header, hgroup, nav, section, summary { + display:block; +} + * { margin: 0; padding: 0; } body { font:normal 12px verdana, sans-serif; background: #ffffff; } #wrapper { width: 100%; } +#header { + background-color: #41afff; +} + #header h1 { - padding: 15px 26px; + -x-system-font: none; + color: #fff; + font-family: sans-serif; + font-feature-settings: normal; + font-kerning: auto; + font-language-override: normal; + font-size: 4em; + font-size-adjust: none; + font-stretch: normal; + font-style: normal; + font-synthesis: weight style; + font-variant-alternates: normal; + font-variant-caps: normal; + font-variant-east-asian: normal; + font-variant-ligatures: normal; + font-variant-numeric: normal; + font-variant-position: normal; + font-weight: 300; + line-height: 1em; + margin-bottom: 0; + margin-left: 20px; + margin-right: 0; + margin-top: 0; + padding-bottom: 5px; + padding-left: 0; + padding-right: 0; + padding-top: 10px; +} + +#navigation p.tracks { + padding-top: 20px; + padding-bottom: 10px; +} + +#navigation ul.tracks { + list-style: none; + list-style-type: none; } -#top-navigation { +#navigation ul.tracks li { + width: 150px; + padding-top: 0; + margin-top: 0; } #main-content p.release { float: right; - background-color: #48B7E4; - color: white; + background-color: #41afff; + color: black; font-size: 100%; margin-bottom: 10px; margin-left: 0; margin-right: 0; margin-top: 0; padding-bottom: 5px; padding-left: 10px; padding-right: 10px; padding-top: 5px; } #main-content > ul { margin-left: 20px; } -ul.horizontal-navigation { - height: 35px; - width: 135px; - left: 20px; - position: absolute; - top: 170px; +#navigation { + float: left; + padding: 10px 0.7em 1em 2em; + vertical-align: top; +} + +#navigation ul { + margin: 0; + min-width: 135px; } -ul.horizontal-navigation li { - background-color: #E1E1E1; /*#00afeb;*/ - border: solid 1px #C7C8C6; +#navigation ul li { + padding-top: 5px; list-style: none; margin: 5px 1px 0 0; - /*height:32px;*/ } -ul.horizontal-navigation li a { - color: #666; - padding: 10px 25px; - text-align: center; - display:block; - font: bold 12px verdana, sans-serif; - text-decoration: none; - line-height: 125%; +#navigation ul li a { + text-decoration:none; + color:#494947; + text-align:center; + font:400 1.12em/1.4em 'Open Sans', sans-serif; + margin:0; + border-bottom:5px solid transparent; } +#navigation ul li a:hover { + text-decoration: underline; +} + + .clear { clear: both; line-height: 1px; height: 1px; } #main-content { - position: absolute; - left: 175px; - right: 2em; + overflow: hidden; + margin: 1em 2em 1em 0; + padding-left: 1em; height: auto; border-style: solid; - border-width: 5px 1px 1px; - border-color: #48B7E4 #C7C8C6 #C7C8C6; + border-width: 1px 1px 1px; + border-color: #41afff; background-color: #FEFEFE; } #main-content > h2 { font-size: 24px; padding-top: 15px; + margin-bottom: 10px; } #main-table { margin: 10px 20px; width: auto; height: auto; overflow: visible; } -table#time-line { - border-spacing: 0; - width: 45px; - border: 1px solid transparent; - height: auto; - background: #fff; - font: bold 12px verdana, sans-serif; - color:#666; - float: left; +#main-table hr { + margin: 25px 0px 0px; } -#time-line td { - border-top: 5px solid #48B7E4; - vertical-align: middle; - text-align: center; +#main-table h2 { + padding: 15px 0px; } td#top-left { border-top: none; height: 25px; } #conference-rooms { left: 45px; top: 0px; } table.rooms-table { - table-layout: fixed; border-spacing: 0; - background: #e4e4e4; + background: #41afff; position: relative; } -table.rooms-table td { - border: 1px solid white; - border-top: none; - vertical-align: top; -} - table.rooms-table th { height: 25px; padding: 3px; border: 1px solid #fff; text-align: center; color: #fff; margin: 0; - border-bottom: 1px solid #9D9D9D; font-weight: bold; background: #666; } +table.rooms-table td { + border: 1px solid white; + border-top: none; + vertical-align: top; + width: 200px; +} + +table.rooms-table td.cell-time, table.rooms-table th.cell-time { + width: 45px; + padding: 0px; + border-bottom: 1px solid #9D9D9D; + vertical-align: top; + text-align: center; + font: bold 12px verdana, sans-serif; + color:#666; + background: #fff; +} + .event { width: 98%; - height: 98%; + height: 100%; border:1px solid; + margin: 0 auto; -webkit-border-radius: 5px; -moz-border-radius: 5px; border-radius: 5px; } .event-wrapper { height: 100%; -webkit-border-radius: 5px; -moz-border-radius: 5px; border-radius: 5px; - background: -moz-linear-gradient(top, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0.6) 100%); /* FF3.6+ */ - background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(255,255,255,0.2)), color-stop(100%,rgba(255,255,255,0.6))); /* Chrome,Safari4+ */ - background: -webkit-linear-gradient(top, rgba(255,255,255,0.2) 0%,rgba(255,255,255,0.6) 100%); /* Chrome10+,Safari5.1+ */ - background: -o-linear-gradient(top, rgba(255,255,255,0.2) 0%,rgba(255,255,255,0.6) 100%); /* Opera11.10+ */ - background: -ms-linear-gradient(top, rgba(255,255,255,0.2) 0%,rgba(255,255,255,0.6) 100%); /* IE10+ */ - filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#33ffffff', endColorstr='#66ffffff',GradientType=0 ); /* IE6-9 */ - background: linear-gradient(top, rgba(255,255,255,0.2) 0%,rgba(255,255,255,0.6) 100%); /* W3C */ + background: -moz-linear-gradient(top, rgba(255,255,255,0.3) 0%, rgba(255,255,255,0.7) 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(255,255,255,0.3)), color-stop(100%,rgba(255,255,255,0.7))); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, rgba(255,255,255,0.3) 0%,rgba(255,255,255,0.7) 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, rgba(255,255,255,0.3) 0%,rgba(255,255,255,0.7) 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, rgba(255,255,255,0.3) 0%,rgba(255,255,255,0.7) 100%); /* IE10+ */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#4dffffff', endColorstr='#b2ffffff',GradientType=0 ); /* IE6-9 */ + background: linear-gradient(top, rgba(255,255,255,0.3) 0%,rgba(255,255,255,0.7) 100%); /* W3C */ + overflow: auto; } .event-header { position: relative; width: 100%; - height:auto; + height: auto; } .event-header h2 { padding: 5px; font-size: 11px; } .event-details { - overflow: auto; - width: 100%; - height: 67%; font-size: 11px; + padding: 2px; } .event-details p { margin: 5px; } .cell { width: 96%; border-bottom: 1px solid #9D9D9D; } -div.event.track-default { - background-color: #fefd7f; - border-color: #fefd7f; -} - -div.event.track-default div.event-header { - background-color: #fefd7f; -} - div.subtitle { font-style: italic; display: block; + margin-top: 3px; margin-bottom: 10px; } div.speakers { font-weight: bold; } table.list { border-spacing: 2px; font-size: 14px; } table.list td { - background: #e4e4e4; + background: #41afff; padding: 10px; } ul { margin-left: 1em; } a { color: #000; text-decoration: none; } table.list a:hover { text-decoration: underline; } table.list a img { border: none; } .column-left { width: 30%; float: left; } .column-right { width: 30%; float: right; } h2.title, h3.title { margin-bottom: 30px; } +h3 { + padding-top: 10px; +} + .column { font-size: 14px; } .column h3, .column p, .column ul { - margin-bottom: 20px; + margin-bottom: 10px; } .column.left { float: left; margin-right: 20px; } .column.right { float: right; } .column#basic { width: 40%; } .column#details { width: 20%; } .column#sidebar { width: 30%; } .column#basic div.image { margin-right: 10px; margin-bottom: 10px; float: left; } p.abstract { font-style: italic; } #pagination { - text-align: right; + margin-bottom: 10px; } .page-button { background: #5d5e5d; border-bottom: 1px solid #9D9D9D; font-weight: bold; font-size: 24px; padding: 5px 10px 5px 10px; margin: 5px; color: #fff; -webkit-border-radius: 5px; -moz-border-radius: 5px; border-radius: 5px; } .page-button.disabled { background: #9D9D9D; color: #9D9D9D; } span#pages { padding: 10px 10px 10px 10px; font-size: 14px; font-weight: bold; } .column a { text-decoration: underline; } + +.event_feedback li.choice { + display: inline-block; + padding: 0 10px; +} + +.event_feedback fieldset.inputs { + margin-top: 1em; +} + +.event_feedback fieldset.inputs ul.choices-group { + float: none; +} + +.event_feedback fieldset.inputs .help-block { + display: block; + padding-top: 0.5em; +} + +/* + mainly used for concurrent events +*/ +.dl dd { + margin-left: 180px; +} + +dd { + margin-left: 10px; + margin-bottom: 5px; +} + +dt, dd { + line-height: 20px; +} + +dt { + font: bold 12px verdana, sans-serif; +} + +.qr { + padding-top: 10px; +} + +.qr table { + border-width: 0; + border-style: none; + border-color: #0000ff; + border-collapse: collapse; +} + +.qr table td { + border-left: solid 5px #000; + padding: 0; + margin: 0; + width: 0px; + height: 5px; +} + +.qr table td.black { border-color: #000; } +.qr table td.white { border-color: #fff; } + +.event-time { + float: left; + width: 15%; +} + +.event-list { + float: left; + margin-left: 15%; + width: 85%; +} + +.event-list .event { + float: left; + width: 25%; + height: 25%; + margin: 10px; +} + +.event-list .event-wrapper { + height: 100px; + overflow: hidden; +} + +.event-book { + width: 98%; + height: 100%; + margin: 10px 0px; +} + +.event-book p { + margin: 5px 0px; +} + +.event-book .label { +} + +.event-navigation ul { + margin: 0; + min-width: 135px; +} + +.event-navigation ul li { + background: #41afff; + padding-top: 5px; + padding-left: 5px; + list-style: none; + margin: 5px 1px 0 0; + width: 50%; + overflow: hidden; + height: 1.5em; +} + +.event-navigation ul li a { + text-decoration:none; + color:#494947; + text-align:center; + font:400 1.12em/1.4em 'Open Sans', sans-serif; + margin:0; + border-bottom:5px solid transparent; +} + +.event-navigation ul li a:hover { + text-decoration: underline; +} + +hr.short { + width: 50%; +} diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index ec0a8ee..4fdd01a 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,122 +1,114 @@ class ApplicationController < ActionController::Base + rescue_from DeviseLdapAuthenticatable::LdapException do |exception| + render :text => exception, :status => 500 + end + include Pundit + protect_from_forgery - before_filter :set_locale - prepend_before_filter :load_conference + before_action :set_locale + before_action :set_paper_trail_whodunnit + prepend_before_action :load_conference - helper_method :current_user + rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized protected + def layout_if_conference + return 'conference' if @conference && !@conference.new_record? + 'application' + end + + def page_param + page = params[:page].to_i + return page if page.positive? + 1 + end + def set_locale - if %w{en de}.include?( params[:locale] ) + if %w(en de es pt-BR).include?(params[:locale]) I18n.locale = params[:locale] else I18n.locale = 'en' params[:locale] = 'en' end end def load_conference - if params[:conference_acronym] - @conference = Conference.find_by_acronym(params[:conference_acronym]) - raise ActionController::RoutingError.new("Not found") unless @conference - elsif Conference.count > 0 - @conference = Conference.current - end - Time.zone = @conference.timezone if @conference + @conference = conference_from_params + @conference ||= conference_from_session + + session[:conference_acronym] = @conference&.acronym + Time.zone = @conference&.timezone end def info_for_paper_trail - {:conference_id => @conference.id} if @conference + return {} unless @conference + { conference_id: @conference.id } end def default_url_options - result = {:locale => params[:locale]} - if @conference - result.merge!(:conference_acronym => @conference.acronym) - end + result = { locale: params[:locale] } + result[:conference_acronym] = @conference.acronym if @conference result end - def current_user - @current_user ||= User.find(session[:user_id]) if session[:user_id] + def not_submitter! + redirect_to cfp_person_path, alert: 'This action is not allowed' if current_user&.is_submitter? end - def authenticate_user! - redirect_to scoped_sign_in_path unless current_user + def orga_only! + authorize @conference, :orga? end - def login_as(user) - session[:user_id] = user.id - @current_user = user - user.record_login! - end - - def require_admin - require_role("admin", new_session_path) - end - - def require_cfp_admin - require_role("cfp_admin", new_session_path) + def manage_only! + authorize @conference, :manage? end - def require_conference_admin - require_role("conf_admin", new_session_path) + def crew_only! + authorize @conference, :read? end - def require_people_admin - require_role("people_admin", new_session_path) - end - - def require_sponsorship_admin - require_role("sponsorship_admin", new_session_path) + def check_cfp_open + redirect_to cfp_root_path unless @conference.cfp_open? end - def require_submitter - require_role("submitter", new_cfp_session_path) - end + private - def require_role(role, redirect_path) - user = current_user - if current_user.role == "admin" - return - end - if role == "conf_admin" and @conference - unless AdminPrivs.new.can_admin_conference user, @conference.id - redirect_to new_session_path - end - else - if role == "cfp_admin" and @conference - unless AdminPrivs.new.can_admin_cfp user, @conference.id - redirect_to new_session_path - end + def user_not_authorized(ex) + Rails.logger.info "[ !!! ] Access Denied for #{current_user.email}/#{current_user.id}/#{current_user.role}: #{ex.message}" + begin + if current_user.is_submitter? + redirect_to cfp_person_path, notice: t(:"ability.denied") else - if role == "sponsorship_admin" and @conference - unless AdminPrivs.new.can_admin_sponsorships user, @conference.id - redirect_to new_session_path - end - else - if role == "people_admin" and @conference - unless AdminPrivs.new.can_admin_people_view user, @conference.id - redirect_to new_session_path - end - else - unless user and user.role == role - redirect_to redirect_path - end - end - end + redirect_back fallback_location: root_path, notice: t(:"ability.denied") end + rescue ActionController::RedirectBackError + redirect_to root_path end end - def scoped_sign_in_path - if request.path =~ /\/cfp/ - new_cfp_session_path - else - new_session_path - end + def conference_from_session + return unless session.key?(:conference_acronym) + Conference.includes(:parent).find_by(acronym: session[:conference_acronym]) + end + + def conference_from_params + return unless params.key?(:conference_acronym) + + conference = Conference.includes(:parent).find_by(acronym: params[:conference_acronym]) + raise ActionController::RoutingError, 'Specified conference not found' unless conference + conference + end + + # maybe conference got deleted + def deleted_conference_redirect_path + return users_last_conference_path if current_user.last_conference + new_conference_path + end + + def users_last_conference_path + conference_path(conference_acronym: current_user.last_conference.acronym) end end diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb new file mode 100644 index 0000000..5368f05 --- /dev/null +++ b/app/controllers/auth/registrations_controller.rb @@ -0,0 +1,65 @@ +class Auth::RegistrationsController < Devise::RegistrationsController + # before_action :configure_sign_up_params, only: [:create] + # before_action :configure_account_update_params, only: [:update] + + # GET /resource/sign_up + # def new + # super + # end + + # POST /resource + # def create + # super + # end + + # GET /resource/edit + # def edit + # super + # end + + # PUT /resource + # def update + # super + # end + + # DELETE /resource + # def destroy + # super + # end + + # GET /resource/cancel + # Forces the session data which is usually expired after sign + # in to be expired now. This is useful if the user wants to + # cancel oauth signing in/up in the middle of the process, + # removing all OAuth session data. + # def cancel + # super + # end + + # protected + + # If you have extra params to permit, append them to the sanitizer. + # def configure_sign_up_params + # devise_parameter_sanitizer.permit(:sign_up, keys: [:attribute]) + # end + + # If you have extra params to permit, append them to the sanitizer. + # def configure_account_update_params + # devise_parameter_sanitizer.permit(:account_update, keys: [:attribute]) + # end + + # The path used after sign up. + # def after_sign_up_path_for(resource) + # binding.pry + # super(resource) + # end + + # The path used after sign up for inactive accounts. + def after_inactive_sign_up_path_for(resource) + if session[:conference_acronym] + cfp_root_path(conference_acronym: session[:conference_acronym]) + else + root_path + end + end +end diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb new file mode 100644 index 0000000..7d3bd00 --- /dev/null +++ b/app/controllers/auth/sessions_controller.rb @@ -0,0 +1,37 @@ +class Auth::SessionsController < Devise::SessionsController + # before_action :configure_sign_in_params, only: [:create] + + # GET /resource/sign_in + # def new + # super + # end + + # POST /resource/sign_in + # def create + # super + # end + + # DELETE /resource/sign_out + # def destroy + # super + # end + + protected + + def after_sign_in_path_for(resource) + if session[:conference_acronym] + if @conference && policy(@conference).manage? + conference_path(conference_acronym: session[:conference_acronym]) + else + edit_cfp_person_path(conference_acronym: session[:conference_acronym]) + end + else + root_path + end + end + + # If you have extra params to permit, append them to the sanitizer. + # def configure_sign_in_params + # devise_parameter_sanitizer.permit(:sign_in, keys: [:attribute]) + # end +end diff --git a/app/controllers/availabilities_controller.rb b/app/controllers/availabilities_controller.rb index 9f56aa6..a53028d 100644 --- a/app/controllers/availabilities_controller.rb +++ b/app/controllers/availabilities_controller.rb @@ -1,26 +1,28 @@ -class AvailabilitiesController < ApplicationController - - before_filter :authenticate_user! - before_filter :require_admin - before_filter :find_person +class AvailabilitiesController < BaseConferenceController + before_action :find_person def new @availabilities = Availability.build_for(@conference) + flash[:alert] = "#{@person.full_name} does not currently have any availabilities." end def edit @availabilities = @person.availabilities_in(@conference) end def update - @person.update_attributes(params[:person]) - redirect_to(person_url(@person), :notice => 'Availibility was successfully updated.') + @person.update_attributes_from_slider_form(person_params) + redirect_to(person_url(@person), notice: 'Availibility was successfully updated.') end private def find_person @person = Person.find(params[:person_id]) + authorize @conference, :manage? end + def person_params + params.require(:person).permit(:first_name, :last_name, :public_name, :email, :email_public, :gender, :avatar, :abstract, :description, :include_in_mailings, availabilities_attributes: %i(id start_date end_date conference_id day_id)) + end end diff --git a/app/controllers/base_conference_controller.rb b/app/controllers/base_conference_controller.rb new file mode 100644 index 0000000..50874a1 --- /dev/null +++ b/app/controllers/base_conference_controller.rb @@ -0,0 +1,6 @@ +class BaseConferenceController < ApplicationController + layout 'conference' + before_action :authenticate_user! + before_action :not_submitter! + after_action :verify_authorized +end diff --git a/app/controllers/base_crew_controller.rb b/app/controllers/base_crew_controller.rb new file mode 100644 index 0000000..8653121 --- /dev/null +++ b/app/controllers/base_crew_controller.rb @@ -0,0 +1,12 @@ +class BaseCrewController < ApplicationController + before_action :authenticate_user! + before_action :not_submitter! + before_action :any_crew! + after_action :verify_authorized + + private + + def any_crew! + authorize Conference, :index? + end +end diff --git a/app/controllers/call_for_participations_controller.rb b/app/controllers/call_for_participations_controller.rb new file mode 100644 index 0000000..4c43461 --- /dev/null +++ b/app/controllers/call_for_participations_controller.rb @@ -0,0 +1,46 @@ +class CallForParticipationsController < BaseConferenceController + def show + authorize @conference, :read? + @call_for_participation = @conference.call_for_participation + end + + def new + authorize @conference, :manage? + @call_for_participation = CallForParticipation.new + @call_for_participation.conference = @conference + end + + def create + authorize @conference, :manage? + @call_for_participation = CallForParticipation.new(call_for_participation_params) + @call_for_participation.conference = @conference + + if @call_for_participation.save + redirect_to edit_call_for_participation_path, notice: 'Launched Call for Participation.' + else + render action: 'new' + end + end + + def edit + authorize @conference, :manage? + @call_for_participation = @conference.call_for_participation + end + + def update + authorize @conference, :manage? + @call_for_participation = @conference.call_for_participation + if @call_for_participation.update_attributes(call_for_participation_params) + redirect_to edit_call_for_participation_path, notice: 'Changes saved successfully!' + else + flash[:alert] = 'Failed to update' + render action: 'edit' + end + end + + private + + def call_for_participation_params + params.require(:call_for_participation).permit(:start_date, :end_date, :hard_deadline, :welcome_text, :info_url, :contact_email) + end +end diff --git a/app/controllers/cfp/availabilities_controller.rb b/app/controllers/cfp/availabilities_controller.rb index 228bed1..7f8929a 100644 --- a/app/controllers/cfp/availabilities_controller.rb +++ b/app/controllers/cfp/availabilities_controller.rb @@ -1,21 +1,26 @@ class Cfp::AvailabilitiesController < ApplicationController - layout 'cfp' - before_filter :authenticate_user! - before_filter :require_submitter + before_action :authenticate_user! def new @availabilities = Availability.build_for(@conference) end def edit @availabilities = current_user.person.availabilities_in(@conference) end def update - current_user.person.update_attributes(params[:person]) - redirect_to cfp_root_path, :notice => t("cfp.update_availability_notice") + if params.key? :person + current_user.person.update_attributes_from_slider_form(person_params) + end + redirect_to cfp_person_path, notice: t('cfp.update_availability_notice') end + private + + def person_params + params.require(:person).permit(:first_name, :last_name, :public_name, :email, :email_public, :gender, :avatar, :abstract, :description, :include_in_mailings, availabilities_attributes: %i(id start_date end_date conference_id day_id)) + end end diff --git a/app/controllers/cfp/events_controller.rb b/app/controllers/cfp/events_controller.rb index ccfaa4f..4b24519 100644 --- a/app/controllers/cfp/events_controller.rb +++ b/app/controllers/cfp/events_controller.rb @@ -1,115 +1,107 @@ class Cfp::EventsController < ApplicationController + layout 'cfp' - layout "cfp" - - before_filter :authenticate_user!, :except => :confirm - before_filter :require_submitter, :except => :confirm + before_action :authenticate_user!, except: :confirm # GET /cfp/events - # GET /cfp/events.xml def index - @events = current_user.person.events.all + @events = current_user.person.events + @events&.map(&:clean_event_attributes!) respond_to do |format| - format.html # index.html.erb - format.xml { render :xml => @events } + format.html { redirect_to cfp_person_path } end end # GET /cfp/events/1 - # GET /cfp/events/1.xml def show - @event = current_user.person.events.find(params[:id]) - - respond_to do |format| - format.html # show.html.erb - format.xml { render :xml => @event } - end + redirect_to(edit_cfp_event_path) end # GET /cfp/events/new - # GET /cfp/events/new.xml def new - @event = Event.new(:time_slots => @conference.default_timeslots) - - @event.bio = @current_user.person.abstract + @event = Event.new(time_slots: @conference.default_timeslots) + @event.recording_license = @conference.default_recording_license respond_to do |format| format.html # new.html.erb - format.xml { render :xml => @event } end end # GET /cfp/events/1/edit def edit - @event = @current_user.person.events.find(params[:id]) - @event.bio = current_user.person.abstract - @event.save + @event = current_user.person.events.find(params[:id]) end # POST /cfp/events - # POST /cfp/events.xml def create - @event = Event.new(params[:event]) + @event = Event.new(event_params.merge(recording_license: @conference.default_recording_license)) @event.conference = @conference - @event.event_people << EventPerson.new(:person => current_user.person, :event_role => "submitter") - @event.event_people << EventPerson.new(:person => current_user.person, :event_role => "speaker") + @event.event_people << EventPerson.new(person: current_user.person, event_role: 'submitter') + @event.event_people << EventPerson.new(person: current_user.person, event_role: 'speaker') respond_to do |format| if @event.save - format.html { redirect_to(cfp_person_path, :notice => t("cfp.event_created_notice")) } - format.xml { render :xml => @event, :status => :created, :location => @event } - - @current_user.person.abstract = @event.abstract - @current_user.person.save + format.html { redirect_to(cfp_person_path, notice: t('cfp.event_created_notice')) } else - format.html { render :action => "new" } - format.xml { render :xml => @event.errors, :status => :unprocessable_entity } + format.html { render action: 'new' } end end end # PUT /cfp/events/1 - # PUT /cfp/events/1.xml def update - @event = current_user.person.events.find(params[:id], :readonly => false) + @event = current_user.person.events.readonly(false).find(params[:id]) + @event.recording_license = @event.conference.default_recording_license unless @event.recording_license respond_to do |format| - if @event.update_attributes(params[:event]) - @current_user.person.abstract = params[:event]['bio'] - @current_user.person.save - format.html { redirect_to(cfp_person_path, :notice => t("cfp.event_updated_notice")) } - format.xml { head :ok } + if @event.update_attributes(event_params) + format.html { redirect_to(cfp_person_path, notice: t('cfp.event_updated_notice')) } else - format.html { render :action => "edit" } - format.xml { render :xml => @event.errors, :status => :unprocessable_entity } + format.html { render action: 'edit' } end end end def withdraw - @event = current_user.person.events.find(params[:id], :readonly => false) + @event = current_user.person.events.find(params[:id], readonly: false) @event.withdraw! - redirect_to(cfp_person_path, :notice => t("cfp.event_withdrawn_notice")) + redirect_to(cfp_person_path, notice: t('cfp.event_withdrawn_notice')) end def confirm - if params[:token] - event_person = EventPerson.find_by_confirmation_token(params[:token]) - event_people = event_person.person.event_people.find_all_by_event_id(params[:id]) - login_as(event_person.person.user) if event_person.person.user - else - raise "Unauthenticated" unless current_user - event_people = current_user.person.event_people.find_all_by_event_id(params[:id]) - end - event_people.each do |event_person| - event_person.confirm! + event_people = event_people_from_params + if event_people.blank? + return redirect_to cfp_person_path, flash: { error: t('cfp.no_confirmation_token') } end + event_people.each(&:confirm!) + if current_user - redirect_to cfp_person_path, :notice => t("cfp.thanks_for_confirmation") + redirect_to cfp_person_path, notice: t('cfp.thanks_for_confirmation') else - render :layout => "signup" + redirect_to new_user_session_path end end + private + + def event_people_from_params + if params[:token] + event_person = EventPerson.find_by(confirmation_token: params[:token]) + return if event_person.nil? + event_person.person.event_people.where(event_id: params[:id]) + + elsif current_user + current_user.person.event_people.where(event_id: params[:id]) + end + end + + def event_params + params.require(:event).permit( + :title, :subtitle, :event_type, :time_slots, :language, :abstract, :description, :logo, :track_id, :submission_note, :tech_rider, + event_attachments_attributes: %i(id title attachment public _destroy), + event_classifiers_attributes: %i(id classifier_id value _destroy), + links_attributes: %i(id title url _destroy) + ) + end end diff --git a/app/controllers/cfp/people_controller.rb b/app/controllers/cfp/people_controller.rb index 31eac9d..933ae8f 100644 --- a/app/controllers/cfp/people_controller.rb +++ b/app/controllers/cfp/people_controller.rb @@ -1,63 +1,125 @@ class Cfp::PeopleController < ApplicationController + layout 'cfp' - layout "cfp" - - before_filter :authenticate_user! - before_filter :require_submitter + before_action :authenticate_user! + before_action :check_cfp_open def show - if params[:commit] and params[:commit] == "Update Events" - if params[:event]['bio'] - @current_user.person.abtract = params[:event]['bio'] - @current_user.person.save - end + @person = current_user.person + + # if !@conference.in_the_past && !@person.events_in(@conference).empty? && @person.availabilities_in(@conference).count.zero? + # flash[:alert] = t('cfp.specify_availability') + # end + + return redirect_to action: 'new' unless @person + if @person.public_name == current_user.email + flash[:alert] = 'Your email address is not a valid public name, please change it.' + redirect_to action: 'edit' + end + + respond_to do |format| + format.html end - + end + + def import @person = current_user.person - redirect_to :action => "new" unless @person + respond_to do |format| + foaf = ActionController::Parameters.new(JSON.parse(foaf_params)) + @person.avatar = StringIO.new(Base64.decode64(foaf['avatar'])) if foaf['avatar'] + if @person.update_attributes(person_foaf_params(foaf)) + format.html { redirect_to(cfp_person_path, notice: t('cfp.person_updated_notice')) } + else + format.html { render action: 'export' } + end + end + end + + def export + @person = current_user.person + @foaf = Cfp::PeopleController.renderer.render( + action: :export, + formats: ['json'], + locals: { conference: @conference, person: @person } + ) end + # It is possbile to create a person object via XML, but not to view it. + # That's because not all fields should be visible to the user. def new - @person = Person.new(:email => current_user.email) + @person = Person.new(email: current_user.email) respond_to do |format| format.html # new.html.erb - format.xml { render :xml => @person } + format.xml { render xml: @person } end end def edit - @person = current_user.person + @person = current_user.person + + # puts "=-=-=-=-=-=-=-=-" + # puts @person.inspect + # puts "=-=-=-=-=-=-=-=-" + + if @person.nil? + flash[:alert] = 'Not a valid person' + return redirect_to action: :index + end end def create - @person = Person.new(params[:person]) - @person.user = current_user + @person = current_user.person + if @person.nil? + @person = Person.new(person_params) + @person.user = current_user + end respond_to do |format| if @person.save - format.html { redirect_to(cfp_person_path, :notice => t("cfp.person_created_notice")) } - format.xml { render :xml => @person, :status => :created, :location => @person } + format.html { redirect_to(cfp_person_path, notice: t('cfp.person_created_notice')) } + format.xml { render xml: @person, status: :created, location: @person } else - format.html { render :action => "new" } - format.xml { render :xml => @person.errors, :status => :unprocessable_entity } + format.html { render action: 'new' } + format.xml { render xml: @person.errors, status: :unprocessable_entity } end end end def update - @person = current_user.person + @person = current_user.person respond_to do |format| - if @person.update_attributes(params[:person]) - format.html { redirect_to(cfp_person_path, :notice => t("cfp.person_updated_notice")) } + if @person.update_attributes(person_params) + format.html { redirect_to(cfp_person_path, notice: t('cfp.person_updated_notice')) } format.xml { head :ok } else - format.html { render :action => "edit" } - format.xml { render :xml => @person.errors, :status => :unprocessable_entity } + format.html { render action: 'edit' } + format.xml { render xml: @person.errors, status: :unprocessable_entity } end end end + private + + def person_params + params.require(:person).permit( + :first_name, :last_name, :public_name, :email, :email_public, :gender, :avatar, :abstract, :description, :include_in_mailings, + :irc_nick, :country, :primary_role, :other_roles, :other_role_artist, :other_role_community, :other_role_development, :other_role_promo, :other_role_translator, :other_role_user, :other_role_other, :emergency_contact, :dietary, :allergy, + :other_dietary_glutenfree, :other_dietary_lactosefree, :other_dietary_nutfree, :other_dietary_vegan, :other_dietary_vegetarian, :other_dietary_other, + im_accounts_attributes: %i(id im_type im_address _destroy), + languages_attributes: %i(id code _destroy), + links_attributes: %i(id title url _destroy), + phone_numbers_attributes: %i(id phone_type phone_number _destroy) + ) + end + + def foaf_params + params.require(:foaf) + end + + def person_foaf_params(foaf) + foaf.permit(:first_name, :public_name, :email, :email_public, :include_in_mailings, :gender, :abstract, :description) + end end diff --git a/app/controllers/cfp/users_controller.rb b/app/controllers/cfp/users_controller.rb index 04bdddf..3f534c5 100644 --- a/app/controllers/cfp/users_controller.rb +++ b/app/controllers/cfp/users_controller.rb @@ -1,36 +1,23 @@ class Cfp::UsersController < ApplicationController - - layout 'signup' - - before_filter :authenticate_user!, :only => [:edit, :update] - - def new - @user = User.new - end - - def create - @user = User.new(params[:user]) - @user.call_for_papers = @conference.call_for_papers - - if @user.save - redirect_to new_cfp_session_path, :notice => t(:"cfp.signed_up") - else - render :action => "new" - end - end + layout 'cfp' + before_action :authenticate_user! def edit @user = current_user - render :layout => "cfp" end def update @user = current_user - if @user.save - redirect_to cfp_person_path, :notice => t(:"cfp.updated") + if @user.update_attributes(user_params) + redirect_to cfp_person_path(@user.person), notice: t(:"cfp.updated") else - render :action => "new" + render action: 'edit' end end + private + + def user_params + params.require(:user).permit(:email, :password, :password_confirmation) + end end diff --git a/app/controllers/cfp/welcome_controller.rb b/app/controllers/cfp/welcome_controller.rb index 800f9c4..882ec67 100644 --- a/app/controllers/cfp/welcome_controller.rb +++ b/app/controllers/cfp/welcome_controller.rb @@ -1,9 +1,10 @@ class Cfp::WelcomeController < ApplicationController - layout 'cfp' - - def open_soon - redirect_to new_cfp_session_path unless @conference.call_for_papers.start_date > Date.today + def show + if @conference.call_for_participation.blank? + render 'not_existing' + elsif @conference.call_for_participation.in_the_future? + render 'open_soon' + end end - end diff --git a/app/controllers/concerns/searchable.rb b/app/controllers/concerns/searchable.rb new file mode 100644 index 0000000..9306578 --- /dev/null +++ b/app/controllers/concerns/searchable.rb @@ -0,0 +1,19 @@ +require 'active_support/concern' + +module Searchable + extend ActiveSupport::Concern + + private + + def perform_search(models, params, options) + if params.key?(:term) and params[:term].present? + term = params[:term] + terms = options.map { |o| [o, term] }.to_h + terms[:m] = 'or' + terms[:s] = params.dig(:q, :s) + models.ransack(terms) + else + models.ransack(params[:q]) + end + end +end diff --git a/app/controllers/conference_users_controller.rb b/app/controllers/conference_users_controller.rb new file mode 100644 index 0000000..b0e571c --- /dev/null +++ b/app/controllers/conference_users_controller.rb @@ -0,0 +1,13 @@ +class ConferenceUsersController < BaseCrewController + def index + @users = policy_scope(ConferenceUser.all) + .includes(user: :person) + .order(:user_id, :role, :conference_id) + .paginate(page: page_param) + end + + def admins + authorize User, :index? + @users = User.all_admins.order(:email).paginate(page: page_param) + end +end diff --git a/app/controllers/conferences_controller.rb b/app/controllers/conferences_controller.rb index c4404da..fa85c38 100644 --- a/app/controllers/conferences_controller.rb +++ b/app/controllers/conferences_controller.rb @@ -1,81 +1,231 @@ -class ConferencesController < ApplicationController - - skip_before_filter :load_conference, :only => :new - - before_filter :authenticate_user! - before_filter :require_conference_admin +class ConferencesController < BaseConferenceController + include Searchable + # these methods don't need a conference + skip_before_action :load_conference, only: %i[new index create] + layout :layout_if_conference # GET /conferences - # GET /conferences.xml def index - @conferences = Conference.all + authorize Conference + result = search respond_to do |format| - format.html # index.html.erb + format.html { @conferences = result.paginate(page: page_param) } + format.json { render template: 'conferences/index', locals: { conferences: result } } end end # GET /conferences/1 - # GET /conferences/1.xml def show - @conference = Conference.find(params[:id]) + return redirect_to new_conference_path if Conference.count.zero? + return redirect_to deleted_conference_redirect_path if @conference.nil? + authorize @conference + + @versions = PaperTrail::Version.where(conference_id: @conference.id).includes(:item).order('created_at DESC').limit(5) respond_to do |format| - format.html # show.html.erb + format.html + format.json end end # GET /conferences/new - # GET /conferences/new.xml def new params.delete(:conference_acronym) - @conference = Conference.new + @conference = authorize Conference.new + @possible_parents = Conference.where(parent: nil) @first = true if Conference.count == 0 respond_to do |format| - format.html # new.html.erb + format.html end end # GET /conferences/1/edit def edit + authorize @conference, :orga? + end + + def edit_days + authorize @conference, :orga? + respond_to do |format| + format.html + end + end + + def edit_notifications + authorize @conference, :orga? + respond_to do |format| + format.html + end + end + + def edit_rooms + authorize @conference, :orga? + respond_to do |format| + format.html + end + end + + def edit_schedule + authorize @conference, :orga? + respond_to do |format| + format.html + end + end + + def edit_tracks + authorize @conference, :orga? + respond_to do |format| + format.html + end + end + + def edit_ticket_server + authorize @conference, :orga? + respond_to do |format| + format.html + end + end + + def edit_ticket_server + authorize @conference, :orga? + respond_to do |format| + format.html + end + end + + def edit_classifiers + authorize @conference, :orga? + respond_to do |format| + format.html + end + end + + def send_notification + SendBulkTicketJob.new.async.perform @conference, params[:notification] + redirect_to edit_notifications_conference_path, notice: 'Bulk notifications for events in ' + params[:notification] + ' enqueued.' end # POST /conferences - # POST /conferences.xml def create - @conference = Conference.new(params[:conference]) + @conference = Conference.new(conference_params) + authorize @conference, :new? + + if @conference.sub_conference? && ! policy(@conference.parent).manage? + @conference.parent = nil + end respond_to do |format| if @conference.save - format.html { redirect_to(conference_home_path(:conference_acronym => @conference.acronym), :notice => 'Conference was successfully created.') } + format.html { redirect_to(conference_path(conference_acronym: @conference.acronym), notice: 'Conference was successfully created.') } else - format.html { render :action => "new" } + @possible_parents = Conference.where(parent: nil) + flash[:errors] = @conference.errors.full_messages.join + format.html { render action: 'new' } end end end # PUT /conferences/1 - # PUT /conferences/1.xml def update + authorize @conference, :orga? respond_to do |format| - if @conference.update_attributes(params[:conference]) - format.html { redirect_to(edit_conference_path(:conference_acronym => @conference.acronym), :notice => 'Conference was successfully updated.') } + if @conference.update_attributes(existing_conference_params) + format.html { redirect_to(edit_conference_path(conference_acronym: @conference.acronym), notice: 'Conference was successfully updated.') } else - format.html { render :action => "edit" } + # redirect to the right nested form page + flash[:errors] = @conference.errors.full_messages.join + format.html { render action: get_previous_nested_form(existing_conference_params) } end end end + def default_notifications + authorize @conference, :orga? + locale = params[:code] || @conference.language_codes.first + @notification = Notification.new(locale: locale) + @notification.default_text = locale + end + # DELETE /conferences/1 - # DELETE /conferences/1.xml def destroy - @conference = Conference.find(params[:id]) + authorize @conference, :orga? @conference.destroy respond_to do |format| - format.html { redirect_to(conferences_url) } - format.xml { head :ok } + format.html { redirect_to(conferences_path) } + end + end + + private + + def get_previous_nested_form(parameters) + parameters.keys.each { |name| + attribs = name.index('_attributes') + next if attribs.nil? + next unless attribs.positive? + test = name.gsub('_attributes', '') + next unless %w(rooms days schedule notifications tracks classifiers ticket_server).include?(test) + return "edit_#{test}" + } + 'edit' + end + + def search + @search = perform_search(Conference, params, %i(title_cont acronym_cont)) + result = @search.result(distinct: true) + result = result.accessible_by_crew(current_user) if current_user.is_crew? + result + end + + def allowed_params + [ + :acronym, :bulk_notification_enabled, :color, :default_recording_license, :default_timeslots, :email, + :event_state_visible, :expenses_enabled, :feedback_enabled, :max_timeslots, :program_export_base_url, + :schedule_custom_css, :schedule_html_intro, :schedule_public, :schedule_version, :ticket_type, + :title, :transport_needs_enabled, + languages_attributes: %i(language_id code _destroy id), + ticket_server_attributes: %i(url user password queue _destroy id), + notifications_attributes: %i(id locale accept_subject accept_body reject_subject reject_body schedule_subject schedule_body _destroy) + ] + end + + def conference_params + allowed = allowed_params + + allowed += if params[:conference][:parent_id].present? + [:parent_id] + else + [ + :timezone, :timeslot_duration, + days_attributes: %i(start_date end_date _destroy id) + ] + end + + params.require(:conference).permit(allowed) + end + + def existing_conference_params + allowed = allowed_params + + allowed += [:parent_id] if @conference.new_record? + + if @conference.main_conference? + allowed += [ + :timezone, :timeslot_duration, + days_attributes: %i(start_date end_date _destroy id) + ] end + + if @conference.main_conference? || policy(@conference.parent).manage? + allowed += [ + classifiers_attributes: %i(name description _destroy id), + rooms_attributes: %i(name size public rank _destroy id), + tracks_attributes: %i(name color _destroy id) + ] + end + + params.require(:conference).permit(allowed) end end diff --git a/app/controllers/crew_profiles_controller.rb b/app/controllers/crew_profiles_controller.rb new file mode 100644 index 0000000..35b725a --- /dev/null +++ b/app/controllers/crew_profiles_controller.rb @@ -0,0 +1,30 @@ +class CrewProfilesController < BaseCrewController + def edit + @person = current_user.person + end + + def update + @person = current_user.person + + respond_to do |format| + if @person.update_attributes(person_params) + format.html { redirect_to(edit_crew_profile_path, notice: 'Your profile was successfully updated.') } + else + format.html { render action: 'edit' } + end + end + end + + private + + def person_params + params.require(:person).permit( + :first_name, :last_name, :public_name, :email, :email_public, :gender, :avatar, :abstract, :description, :include_in_mailings, :note, + im_accounts_attributes: %i(id im_type im_address _destroy), + languages_attributes: %i(id code _destroy), + links_attributes: %i(id title url _destroy), + phone_numbers_attributes: %i(id phone_type phone_number _destroy), + ticket_attributes: %i(id remote_ticket_id) + ) + end +end diff --git a/app/controllers/event_feedbacks_controller.rb b/app/controllers/event_feedbacks_controller.rb index 703f110..a29162e 100644 --- a/app/controllers/event_feedbacks_controller.rb +++ b/app/controllers/event_feedbacks_controller.rb @@ -1,11 +1,11 @@ class EventFeedbacksController < ApplicationController - - before_filter :authenticate_user! - before_filter :require_cfp_admin + before_action :authenticate_user! + before_action :not_submitter! + after_action :verify_authorized def index + authorize Conference, :index? @event = Event.find(params[:event_id]) @event_feedbacks = @event.event_feedbacks end - end diff --git a/app/controllers/event_ratings_controller.rb b/app/controllers/event_ratings_controller.rb index 95fb53f..e6c9815 100644 --- a/app/controllers/event_ratings_controller.rb +++ b/app/controllers/event_ratings_controller.rb @@ -1,34 +1,60 @@ -class EventRatingsController < ApplicationController - - before_filter :authenticate_user! - before_filter :require_cfp_admin - before_filter :find_event +class EventRatingsController < BaseConferenceController + before_action :find_event + before_action :crew_only! def show - @rating = @event.event_ratings.find_by_person_id(@current_user.person.id) || EventRating.new - if session[:review_ids] and current_index = session[:review_ids].index(@event.id) and session[:review_ids].last != @event.id - @next_event = Event.find(session[:review_ids][current_index + 1]) - end + @rating = @event.event_ratings.find_by(person_id: current_user.person.id) || EventRating.new + setup_batch_reviews_next_event end def create - @rating = EventRating.new(params[:event_rating]) - @rating.event = @event - @rating.person = current_user.person - @rating.save - redirect_to event_event_rating_path, :notice => "Rating saved successfully." + # only one rating allowed, if one exists update instead + return update if @event.event_ratings.find_by(person_id: current_user.person.id) + + @rating = new_event_rating + if @rating.save + redirect_to event_event_rating_path, notice: 'Rating saved successfully.' + else + flash[:alert] = 'Failed to create event rating: ' + @rating.errors.full_messages.join + render action: 'show' + end end def update - @rating = @event.event_ratings.find_by_person_id(current_user.person.id) - @rating.update_attributes(params[:event_rating]) - redirect_to event_event_rating_path, :notice => "Rating saved successfully." + @rating = @event.event_ratings.find_by!(person_id: current_user.person.id) + + if @rating.update_attributes(event_rating_params) + redirect_to event_event_rating_path, notice: 'Rating updated successfully.' + else + flash[:alert] = 'Failed to update event rating' + render action: 'show' + end end protected + def setup_batch_reviews_next_event + return unless session[:review_ids] + current_index = session[:review_ids].index(@event.id) + return unless current_index + return if session[:review_ids].last == @event.id + @next_event = Event.find(session[:review_ids][current_index + 1]) + end + + def new_event_rating + rating = EventRating.new(event_rating_params) + rating.event = @event + rating.person = current_user.person + rating + end + + # filter according to users abilities def find_event @event = Event.find(params[:event_id]) + @event_ratings = policy_scope(@event.event_ratings) end + def event_rating_params + params.require(:event_rating).permit(:rating, :comment, :text) + end end diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb index 004b4c7..659561b 100644 --- a/app/controllers/events_controller.rb +++ b/app/controllers/events_controller.rb @@ -1,164 +1,262 @@ -class EventsController < ApplicationController +class EventsController < BaseConferenceController + include Searchable - before_filter :authenticate_user! - before_filter :require_cfp_admin - # GET /events - # GET /events.xml + # GET /events.json def index - if params[:term] - @search = @conference.events.with_query(params[:term]).search(params[:q]) - @events = @search.result.paginate :page => params[:page] - else - @search = @conference.events.search(params[:q]) - @events = @search.result.paginate :page => params[:page] + authorize @conference, :read? + @events = search @conference.events.includes(:track) + + clean_events_attributes + respond_to do |format| + format.html { @events = @events.paginate page: page_param } + format.json end + end + + def export_accepted + authorize @conference, :read? + @events = @conference.events.is_public.accepted respond_to do |format| - format.html # index.html.erb - format.xml { render :xml => @events } + format.json { render :export } end end - def my - if params[:term] - @search = @conference.events.associated_with(current_user.person).with_query(params[:term]).search(params[:q]) - @events = @search.result.paginate :page => params[:page] - else - @search = @conference.events.associated_with(current_user.person).search(params[:q]) - @events = @search.result.paginate :page => params[:page] + def export_confirmed + authorize @conference, :read? + @events = @conference.events.is_public.confirmed + + respond_to do |format| + format.json { render :export } end end + # current_users events + def my + authorize @conference, :read? + + result = search @conference.events.associated_with(current_user.person) + clean_events_attributes + @events = result.paginate page: page_param + end + + # events as pdf def cards - if params[:accepted] - @events = @conference.events.accepted - else - @events = @conference.events - end - + authorize @conference, :manage? + @events = if params[:accepted] + @conference.events.accepted + else + @conference.events + end + respond_to do |format| format.pdf end end + # show event ratings def ratings - @search = @conference.events.search(params[:q]) - @events = @search.result.paginate :page => params[:page] + authorize @conference, :read? + + result = search @conference.events + @events = result.paginate page: page_param + clean_events_attributes # total ratings: @events_total = @conference.events.count - @events_reviewed_total = @conference.events.select{|e| e.event_ratings_count != nil and e.event_ratings_count > 0 }.count + @events_reviewed_total = @conference.events.to_a.count { |e| !e.event_ratings_count.nil? && e.event_ratings_count > 0 } @events_no_review_total = @events_total - @events_reviewed_total # current_user rated: - @events_reviewed = @conference.events.joins(:event_ratings).where("event_ratings.person_id" => current_user.person.id).count + @events_reviewed = @conference.events.joins(:event_ratings).where('event_ratings.person_id' => current_user.person.id).count @events_no_review = @events_total - @events_reviewed end + # show event feedbacks def feedbacks - @search = @conference.events.accepted.search(params[:q]) - @events = @search.result.paginate :page => params[:page] + authorize @conference, :read? + result = search @conference.events.accepted + @events = result.paginate page: page_param end + # start batch event review def start_review + authorize @conference, :read? ids = Event.ids_by_least_reviewed(@conference, current_user.person) if ids.empty? - redirect_to :action => "ratings", :notice => "You have already reviewed all events:" + redirect_to action: 'ratings', notice: 'You have already reviewed all events:' else session[:review_ids] = ids - redirect_to event_event_rating_path(:event_id => ids.first) + redirect_to event_event_rating_path(event_id: ids.first) end end # GET /events/1 - # GET /events/1.xml + # GET /events/1.json def show - @event = Event.find(params[:id]) + @event = authorize Event.find(params[:id]) + clean_events_attributes respond_to do |format| format.html # show.html.erb - format.xml { render :xml => @event } + format.json end end + # people tab of event detail page, the rating and + # feedback tabs are handled in routes.rb + # GET /events/2/people def people - @event = Event.find(params[:id]) + @event = authorize Event.find(params[:id]) end - + # GET /events/new - # GET /events/new.xml def new + authorize @conference, :manage? @event = Event.new + @start_time_options = @conference.start_times_by_day respond_to do |format| format.html # new.html.erb - format.xml { render :xml => @event } end end # GET /events/1/edit def edit - @event = Event.find(params[:id]) + @event = authorize Event.find(params[:id]) + @start_time_options = PossibleStartTimes.new(@event).all end + # GET /events/2/edit_people def edit_people - @event = Event.find(params[:id]) + @event = authorize Event.find(params[:id]) + @persons = Person.fullname_options end # POST /events - # POST /events.xml def create - @event = Event.new(params[:event]) + @event = Event.new(event_params) @event.conference = @conference + authorize @event respond_to do |format| if @event.save - format.html { redirect_to(@event, :notice => 'Event was successfully created.') } - format.xml { render :xml => @event, :status => :created, :location => @event } + format.html { redirect_to(@event, notice: 'Event was successfully created.') } else - format.html { render :action => "new" } - format.xml { render :xml => @event.errors, :status => :unprocessable_entity } + @start_time_options = @conference.start_times_by_day + format.html { render action: 'new' } end end end # PUT /events/1 - # PUT /events/1.xml def update - @event = Event.find(params[:id]) + @event = authorize Event.find(params[:id]) respond_to do |format| - if @event.update_attributes(params[:event]) - format.html { redirect_to(@event, :notice => 'Event was successfully updated.') } - format.xml { head :ok } + if @event.update_attributes(event_params) + format.html { redirect_to(@event, notice: 'Event was successfully updated.') } format.js { head :ok } else - format.html { render :action => "edit" } - format.xml { render :xml => @event.errors, :status => :unprocessable_entity } + @start_time_options = PossibleStartTimes.new(@event).all + format.html { render action: 'edit' } + format.js { render json: @event.errors, status: :unprocessable_entity } end end end + # update event state + # GET /events/2/update_state?transition=cancel def update_state - @event = Event.find(params[:id]) + @event = authorize Event.find(params[:id]) + if params[:send_mail] - redirect_to(@event, :alert => "Cannot send mails: Please specify an email address for this conference.") and return unless @conference.email - redirect_to(@event, :alert => "Cannot send mails: Not all speakers have email addresses.") and return unless @event.speakers.all?{|s| s.email} + + # If integrated mailing is used, take care that a notification text is present. + if @event.conference.notifications.empty? + return redirect_to edit_conference_path, alert: 'No notification text present. Please change the default text for your needs, before accepting/ rejecting events.' + end + + return redirect_to(@event, alert: 'Cannot send mails: Please specify an email address for this conference.') unless @conference.email + + return redirect_to(@event, alert: 'Cannot send mails: Not all speakers have email addresses.') unless @event.speakers.all?(&:email) + end + + begin + @event.send(:"#{params[:transition]}!", send_mail: params[:send_mail], coordinator: current_user.person) + rescue => ex + return redirect_to(@event, alert: "Cannot update state: #{ex}.") end - @event.send(:"#{params[:transition]}!", :send_mail => params[:send_mail], :coordinator => current_user.person) - redirect_to @event, :notice => 'Event was successfully updated.' + + redirect_to @event, notice: 'Event was successfully updated.' + end + + # add custom notifications to all the event's speakers + # POST /events/2/custom_notification + def custom_notification + @event = authorize Event.find(params[:id]) + + case @event.state + when 'accepting' + state = 'accept' + when 'rejecting' + state = 'reject' + when 'confirmed' + state = 'schedule' + else + return redirect_to(@event, alert: 'Event not in a notifiable state.') + end + + begin + @event.event_people.presenter.each { |p| p.set_default_notification(state) } + rescue NotificationMissingException => ex + return redirect_to(@event, alert: "Failed to set default notification: #{ex}.") + end + + redirect_to edit_people_event_path(@event) end # DELETE /events/1 - # DELETE /events/1.xml def destroy - @event = Event.find(params[:id]) + @event = authorize Event.find(params[:id]) @event.destroy respond_to do |format| format.html { redirect_to(events_url) } - format.xml { head :ok } end end + + private + + def clean_events_attributes + return if policy(@conference).manage? + @event&.clean_event_attributes! + @events&.map(&:clean_event_attributes!) + end + + # returns duplicates if ransack has to deal with the associated model + def search(events) + filter = events + filter = filter.where(state: params[:event_state]) if params[:event_state].present? + filter = filter.where(event_type: params[:event_type]) if params[:event_type].present? + filter = filter.where(track: @conference.tracks.find_by(:name => params[:track_name])) if params[:track_name].present? + @search = perform_search(filter, params, %i(title_cont description_cont abstract_cont track_name_cont event_type_is)) + if params.dig('q', 's')&.match('track_name') + @search.result + else + @search.result(distinct: true) + end + end + + def event_params + params.require(:event).permit( + :id, :title, :subtitle, :event_type, :time_slots, :state, :start_time, :public, :language, :abstract, :description, :logo, :track_id, :room_id, :note, :submission_note, :do_not_record, :recording_license, :tech_rider, + event_attachments_attributes: %i(id title attachment public _destroy), + ticket_attributes: %i(id remote_ticket_id), + links_attributes: %i(id title url _destroy), + event_classifiers_attributes: %i(id classifier_id value _destroy), + event_people_attributes: %i(id person_id event_role role_state notification_subject notification_body _destroy) + ) + end end diff --git a/app/controllers/expenses_controller.rb b/app/controllers/expenses_controller.rb new file mode 100644 index 0000000..c21d784 --- /dev/null +++ b/app/controllers/expenses_controller.rb @@ -0,0 +1,54 @@ +class ExpensesController < BaseConferenceController + before_action :orga_only! + before_action :find_person + before_action :check_enabled + + def new + @expense = Expense.new + flash[:alert] = "#{@person.full_name} does not currently have any expenses." + end + + def edit + @expense = @person.expenses.find(params[:id]) + end + + def index + @expenses = @person.expenses.where(conference_id: @conference.id) + @expenses_sum_reimbursed = @person.sum_of_expenses(@conference, true) + @expenses_sum_non_reimbursed = @person.sum_of_expenses(@conference, false) + end + + def update + expense = @person.expenses.find(params[:id]) + expense.update_attributes(expenses_params) + redirect_to(person_url(@person), notice: 'Expense was successfully updated.') + end + + def create + e = Expense.new(expenses_params) + e.conference = @conference + @person.expenses << e + redirect_to(person_url(@person), notice: 'Expense was successfully added.') + end + + def destroy + @person.expenses.find(params[:id]).destroy + redirect_to(person_url(@person), notice: 'Expense was successfully destroyed.') + end + + private + + def find_person + @person = Person.find(params[:person_id]) + end + + def check_enabled + unless @conference.expenses_enabled? + redirect_to(person_url(@person), notice: 'Expenses are not enabled for this conference') + end + end + + def expenses_params + params.require(:expense).permit(:name, :reimbursed, :value) + end +end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index fdaf9a0..89aa6b1 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -1,12 +1,10 @@ class HomeController < ApplicationController - - before_filter :authenticate_user! - before_filter :require_admin - def index - if Conference.count == 0 - redirect_to new_conference_path and return - end - @versions = Version.where(:conference_id => @conference.id).order("created_at DESC").limit(5) + @conferences = Conference.future.includes(:call_for_participation).paginate(page: page_param) + end + + def past + @conferences = Conference.past.includes(:call_for_participation).paginate(page: page_param) + render 'index' end end diff --git a/app/controllers/mail_templates_controller.rb b/app/controllers/mail_templates_controller.rb new file mode 100644 index 0000000..7606f10 --- /dev/null +++ b/app/controllers/mail_templates_controller.rb @@ -0,0 +1,90 @@ +class MailTemplatesController < BaseConferenceController + before_action :orga_only! + + def new + @mail_template = MailTemplate.new + end + + def edit + @mail_template = @conference.mail_templates.find(params[:id]) + end + + def show + @mail_template = @conference.mail_templates.find(params[:id]) + @send_filter_options = [ + ['All speakers involved in all confirmed events', :all_speakers_in_confirmed_events], + ['All speakers involved in all unconfirmed events', :all_speakers_in_unconfirmed_events], + ['All speakers involved in all scheduled events', :all_speakers_in_scheduled_events] + ] + end + + def send_mail + @mail_template = @conference.mail_templates.find(params[:id]) + send_filter = params[:send_filter] + + if Rails.env.production? + @mail_template.send_async(send_filter) + redirect_to(@mail_template, notice: 'Mail deliveries queued.') + else + @mail_template.send_sync(send_filter) + redirect_to(@mail_template, notice: 'Mails delivered.') + end + end + + def index + result = search @conference.mail_templates, params + @mail_templates = result.paginate page: page_param + end + + def update + @mail_template = @conference.mail_templates.find(params[:id]) + + respond_to do |format| + if @mail_template.update_attributes(mail_template_params) + format.html { redirect_to(@mail_template, notice: 'Mail template was successfully updated.') } + format.xml { head :ok } + format.js { head :ok } + else + format.html { render action: 'edit' } + format.xml { render xml: @mail_template.errors, status: :unprocessable_entity } + end + end + end + + def create + t = MailTemplate.new(mail_template_params) + @conference.mail_templates << t + redirect_to(mail_templates_path, notice: 'Transport need was successfully added.') + end + + def destroy + @conference.mail_templates.find(params[:id]).destroy + redirect_to(mail_templates_path, notice: 'Mail template was successfully destroyed.') + end + + private + + def search(mail_templates, params) + if params.key?(:term) and not params[:term].empty? + term = params[:term] + sort = begin + params[:q][:s] + rescue + nil + end + @search = mail_templates.ransack(name_cont: term, + subject_cont: term, + content_cont: term, + m: 'or', + s: sort) + else + @search = mail_templates.ransack(params[:q]) + end + + @search.result(distinct: true) + end + + def mail_template_params + params.require(:mail_template).permit(:name, :subject, :content) + end +end diff --git a/app/controllers/people_controller.rb b/app/controllers/people_controller.rb index 3f52fe8..8caac61 100644 --- a/app/controllers/people_controller.rb +++ b/app/controllers/people_controller.rb @@ -1,119 +1,134 @@ -class PeopleController < ApplicationController - - before_filter :authenticate_user! - before_filter :require_people_admin +class PeopleController < BaseConferenceController + before_action :manage_only!, except: %i[show] + include Searchable # GET /people - # GET /people.xml + # GET /people.json def index - if params[:term] - @people = Person.involved_in(@conference).with_query(params[:term]).paginate :page => params[:page] - else - @people = Person.involved_in(@conference).paginate :page => params[:page] + @people = search Person.involved_in(@conference) + + respond_to do |format| + format.html { @people = @people.paginate page: page_param } + format.json end end def speakers respond_to do |format| format.html do - if params[:term] - @people = Person.speaking_at(@conference).with_query(params[:term]).paginate :page => params[:page] - else - @people = Person.speaking_at(@conference).paginate :page => params[:page] - end + result = search Person.involved_in(@conference) + @people = result.paginate page: page_param end format.text do @people = Person.speaking_at(@conference) - render :text => @people.map(&:email).join("\n") + render text: @people.map(&:email).join("\n") end end end - def registrations - if params[:term] - @people = Person.involved_in(@conference).with_query(params[:term]).paginate :page => params[:page] - else - @people = Person.involved_in(@conference).paginate :page => params[:page] - end - end - def all - if params[:term] - @people = Person.with_query(params[:term]).paginate :page => params[:page] - else - @people = Person.paginate :page => params[:page] + authorize Person, :manage? + result = search Person + @people = result.paginate page: page_param + + respond_to do |format| + format.html end end # GET /people/1 - # GET /people/1.xml + # GET /people/1.json def show - @person = Person.find(params[:id]) - @current_events = @person.events.where(:conference_id => @conference.id).all - @other_events = @person.events.where(Event.arel_table[:conference_id].not_eq(@conference.id)).all + @person = authorize Person.find(params[:id]) + @view_model = PersonViewModel.new(current_user, @person, @conference) + respond_to do |format| - format.html # show.html.erb - format.xml { render :xml => @person } + format.html + format.json end end + def feedbacks + @person = Person.find(params[:id]) + authorize Conference, :index? + @current_events = @person.events_as_presenter_in(@conference) + @other_events = @person.events_as_presenter_not_in(@conference) + end + + def attend + @person = authorize Person.find(params[:id]) + @person.set_role_state(@conference, 'attending') + redirect_to action: :show + end + # GET /people/new - # GET /people/new.xml def new + authorize Person @person = Person.new respond_to do |format| - format.html # new.html.erb - format.xml { render :xml => @person } + format.html end end # GET /people/1/edit def edit - @person = Person.find(params[:id]) + @person = authorize Person.find(params[:id]) end # POST /people - # POST /people.xml def create - @person = Person.new(params[:person]) + @person = authorize Person.new(person_params) respond_to do |format| if @person.save - format.html { redirect_to(@person, :notice => 'Person was successfully created.') } - format.xml { render :xml => @person, :status => :created, :location => @person } + format.html { redirect_to(@person, notice: 'Person was successfully created.') } else - format.html { render :action => "new" } - format.xml { render :xml => @person.errors, :status => :unprocessable_entity } + format.html { render action: 'new' } end end end # PUT /people/1 - # PUT /people/1.xml def update - @person = Person.find(params[:id]) + @person = authorize Person.find(params[:id]) respond_to do |format| - if @person.update_attributes(params[:person]) - format.html { redirect_to(@person, :notice => 'Person was successfully updated.') } - format.xml { head :ok } + if @person.update_attributes(person_params) + format.html { redirect_to(@person, notice: 'Person was successfully updated.') } else - format.html { render :action => "edit" } - format.xml { render :xml => @person.errors, :status => :unprocessable_entity } + format.html { render action: 'edit' } end end end # DELETE /people/1 - # DELETE /people/1.xml def destroy - @person = Person.find(params[:id]) + @person = authorize Person.find(params[:id]) @person.destroy respond_to do |format| format.html { redirect_to(people_url) } - format.xml { head :ok } end end + + private + + def search(people) + @search = perform_search(people, params, + %i(first_name_cont last_name_cont public_name_cont email_cont + abstract_cont description_cont user_email_cont)) + @search.result(distinct: true) + end + + def person_params + params.require(:person).permit( + :first_name, :last_name, :public_name, :email, :email_public, :gender, :avatar, :abstract, :description, :include_in_mailings, :note, + im_accounts_attributes: %i(id im_type im_address _destroy), + languages_attributes: %i(id code _destroy), + links_attributes: %i(id title url _destroy), + phone_numbers_attributes: %i(id phone_type phone_number _destroy), + ticket_attributes: %i(id remote_ticket_id) + ) + end end diff --git a/app/controllers/public/feedback_controller.rb b/app/controllers/public/feedback_controller.rb index 7c133d7..79f8bcc 100644 --- a/app/controllers/public/feedback_controller.rb +++ b/app/controllers/public/feedback_controller.rb @@ -1,22 +1,26 @@ class Public::FeedbackController < ApplicationController - - layout "public_schedule" + layout 'public_schedule' def new @event = @conference.events.find(params[:event_id]) @feedback = EventFeedback.new @feedback.rating = 3 end def create @event = @conference.events.find(params[:event_id]) - @feedback = @event.event_feedbacks.new(params[:event_feedback]) - + @feedback = @event.event_feedbacks.new(event_feedback_params) + if @feedback.save - render :action => "thank_you" + render action: 'thank_you' else - render :action => "new" + render action: 'new' end end + private + + def event_feedback_params + params.require(:event_feedback).permit(:rating, :comment) + end end diff --git a/app/controllers/public/schedule_controller.rb b/app/controllers/public/schedule_controller.rb index 8da935f..ec9454d 100644 --- a/app/controllers/public/schedule_controller.rb +++ b/app/controllers/public/schedule_controller.rb @@ -1,63 +1,110 @@ class Public::ScheduleController < ApplicationController - layout 'public_schedule' + before_action :maybe_authenticate_user! + after_action :cors_set_access_control_headers def index @days = @conference.days respond_to do |format| format.html format.xml format.xcal format.ics - format.json { render :file => "public/schedule/index.json.erb", :content_type => 'application/json' } + format.json end end def style + respond_to do |format| + format.css + end end def day - @day = Date.parse(params[:date]) - @day_index = @conference.days.index(@day) + 1 - @all_rooms = @conference.rooms.public.all - @rooms = Array.new - @events = Hash.new - @skip_row = Hash.new - @all_rooms.each do |room| - events = room.events.confirmed.public.scheduled_on(@day).order(:start_time).all - unless events.empty? - @events[room] = events - @skip_row[room] = 0 - @rooms << room - end + unless @day = find_day(params[:day].to_i) + return redirect_to public_schedule_index_path, alert: 'Failed to find day.' end + if @day.rooms_with_events.empty? + return redirect_to public_schedule_index_path, notice: 'No events are public and scheduled.' + end + + @view_model = ScheduleViewModel.new(@conference).for_day(@day) + respond_to do |format| format.html format.pdf do - @page_size = "A4" - render :template => "schedule/custom_pdf" + @layout = CustomPDF::FullPageLayout.new('A4') + @rooms_per_page = 5 + render template: 'schedule/custom_pdf' end end end def events - @events = @conference.events.public.confirmed.scheduled.sort {|a,b| - a.to_sortable <=> b.to_sortable - } + @view_model = ScheduleViewModel.new(@conference) + respond_to do |format| + format.html + format.json + format.xls { render file: 'public/schedule/events.xls.erb', content_type: 'application/xls' } + end + end + + def timeline + @view_model = ScheduleViewModel.new(@conference) + respond_to do |format| + format.html + end + end + + def booklet + @view_model = ScheduleViewModel.new(@conference) + respond_to do |format| + format.html + end end def event - @event = @conference.events.public.confirmed.scheduled.find(params[:id]) + @view_model = ScheduleViewModel.new(@conference).for_event(params[:id]) + respond_to do |format| + format.html + format.ics + end end def speakers - @speakers = Person.publicly_speaking_at(@conference).confirmed(@conference).order(:public_name, :first_name, :last_name) + @view_model = ScheduleViewModel.new(@conference) + respond_to do |format| + format.html + format.json + format.xls { render file: 'public/schedule/speakers.xls.erb', content_type: 'application/xls' } + end end def speaker - @speaker = Person.publicly_speaking_at(@conference).confirmed(@conference).find(params[:id]) + @view_model = ScheduleViewModel.new(@conference).for_speaker(params[:id]) end + def qrcode + @qr = RQRCode::QRCode.new(public_schedule_index_url(format: :xml), size: 8, level: :h) + end + + private + + def find_day(day_index) + return @conference.days.first if day_index < 1 + return @conference.days.last if day_index > @conference.days.count + @conference.days[day_index - 1] + end + + def maybe_authenticate_user! + authenticate_user! unless @conference.schedule_public + end + + private + + def cors_set_access_control_headers + headers['Access-Control-Allow-Origin'] = '*' + end end diff --git a/app/controllers/recent_changes_controller.rb b/app/controllers/recent_changes_controller.rb index fed0b27..c5b05f6 100644 --- a/app/controllers/recent_changes_controller.rb +++ b/app/controllers/recent_changes_controller.rb @@ -1,17 +1,20 @@ -class RecentChangesController < ApplicationController - - before_filter :authenticate_user! - before_filter :require_admin +class RecentChangesController < BaseConferenceController + before_action :orga_only! def index - @versions = Version.where(:conference_id => @conference.id).order("created_at DESC").paginate( - :page => params[:page], - :per_page => 25 + @all_versions = PaperTrail::Version.where(conference_id: @conference.id).order('created_at DESC') + @versions = @all_versions.paginate( + page: page_param, + per_page: 25 ) + respond_to do |format| + format.html + format.xml { render xml: @all_versions } + format.json { render json: @all_versions.to_json } + end end def show - @version = Version.where(:conference_id => @conference.id, :id => params[:id]).first + @version = PaperTrail::Version.where(conference_id: @conference.id, id: params[:id]).first end - end diff --git a/app/controllers/reports_controller.rb b/app/controllers/reports_controller.rb index 7265b4e..8c2821b 100644 --- a/app/controllers/reports_controller.rb +++ b/app/controllers/reports_controller.rb @@ -1,120 +1,200 @@ -class ReportsController < ApplicationController - - before_filter :authenticate_user! - before_filter :require_admin +class ReportsController < BaseConferenceController + before_action :orga_only! def index + respond_to do |format| + format.html + end end def show_events @report_type = params[:id] @events = [] @search_count = 0 + @extra_fields = [] conference_events = @conference.events if params[:term] - conference_events = @conference.events.with_query(params[:term]) + conference_events = @conference.events.ransack(params[:term]).result end case @report_type when 'lectures_with_speaker' - r = conference_events.with_speaker.where(:event_type => :lecture) + r = conference_events.with_speaker.where(event_type: :lecture) when 'lectures_not_confirmed' - r = conference_events.with_speaker.where(:event_type => :lecture, :state => [:new,:review] ) + r = conference_events.with_speaker.where(event_type: :lecture, state: [:new, :review]) + when 'events_not_public' + r = conference_events.where(public: false) when 'events_that_are_workshops' r = conference_events.where(Event.arel_table[:event_type].eq(:workshop)) when 'event_timeslot_deviation' - r = conference_events.where(:event_type => :lecture).where('time_slots != ?', @conference.default_timeslots) + r = conference_events.where(event_type: :lecture).where('time_slots != ?', @conference.default_timeslots) when 'events_that_are_no_lectures' r = conference_events.where(Event.arel_table[:event_type].not_eq(:lecture).and(Event.arel_table[:event_type].not_eq(:workshop))) when 'events_without_speaker' r = conference_events.without_speaker + when 'events_with_more_than_one_speaker' + r = conference_events.with_more_than_one_speaker + when 'events_without_abstract' + r = conference_events.where(Event.arel_table[:abstract].eq('')) when 'unconfirmed_events' - r = conference_events.where(:event_type => :lecture, :state => :unconfirmed) + r = conference_events.where(event_type: :lecture, state: :unconfirmed) + when 'events_with_a_note' + r = conference_events.where(Event.arel_table[:note].not_eq('').or(Event.arel_table[:submission_note].not_eq(''))) when 'events_with_unusual_state_speakers' - r = conference_events.joins(:event_people).where(:event_people => { :role_state => [:canceled, :declined, :idea, :offer, :unclear], :event_role => :speaker } ) + r = conference_events.joins(:event_people).where(event_people: { role_state: [:canceled, :declined, :idea, :offer, :unclear], event_role: [:moderator, :speaker] }) + when 'do_not_record_events' + r = conference_events.where(do_not_record: true) + when 'events_with_tech_rider' + r = conference_events + .scheduled + .where(Event.arel_table[:tech_rider].not_eq('')) + @extra_fields << :tech_rider end unless r.nil? or r.empty? @search = r.search(params[:q]) @search_count = r.count - @events = @search.result.paginate :page => params[:page] + @events = @search.result.paginate page: page_param + end + respond_to do |format| + format.html { render :show } end - render :show end def show_people @report_type = params[:id] @people = [] @search_count = 0 + @extra_fields = [] conference_people = Person - if params[:term] - conference_people = Person.with_query(params[:term]) - end + conference_people = Person.ransack(params[:term]).result if params[:term] case @report_type + when 'expected_speakers' + r = Person.joins(events: :conference) + .where('conferences.id': @conference.id) + .where('event_people.event_role': EventPerson::SPEAKER) + .where('event_people.role_state': 'confirmed') + .where('events.public': true) + .where('events.start_time > ?', Time.now) + .where('events.start_time < ?', Time.now.since(4.hours)) + .where('events.state': %w(unconfirmed confirmed scheduled)) + .distinct when 'people_speaking_at' r = conference_people.speaking_at(@conference) when 'people_with_a_note' - r = conference_people.involved_in(@conference).where(Person.arel_table[:note].not_eq("")) + r = conference_people.involved_in(@conference).where(Person.arel_table[:note].not_eq('')) + when 'people_with_more_than_one' + r = conference_people.involved_in(@conference).where('event_people.event_role' => ['submitter']).group('event_people.person_id').having('count(*) > 1') + when 'people_with_non_reimbursed_expenses' + r = conference_people.involved_in(@conference).joins(:expenses).where('expenses.value > 0 AND expenses.reimbursed = ? AND expenses.conference_id = ?', false, @conference.id) + @total_sum = 0 + r.each do |p| + @total_sum += p.sum_of_expenses(@conference, false) + end + + @extra_fields << :expenses + when 'non_attending_speakers' + r = Person.joins(events: :conference) + .where('conferences.id': @conference.id) + .where('event_people.event_role': 'speaker') + .where("event_people.role_state != 'attending'") + .where('events.public': true) + .where('events.start_time > ?', Time.now) + .where('events.start_time < ?', Time.now.since(2.hours)) + .where('events.state': %w(accepting unconfirmed confirmed scheduled)) + .distinct + when 'speakers_without_availabilities' + r = Person.joins(events: :conference) + .includes(:availabilities) + .where('conferences.id': @conference.id) + .where('event_people.event_role': EventPerson::SPEAKER) + .where('event_people.role_state': [ 'confirmed', 'scheduled' ]) + .where(availabilities: { person_id: nil }) end unless r.nil? or r.empty? @search = r.search(params[:q]) - @search_count = r.count - @people = @search.result.paginate :page => params[:page] + @search_count = r.length + @people = @search.result.paginate page: page_param + end + respond_to do |format| + format.html { render :show } end - render :show - end - - def event_duration_sum(events) - # FIXME adjust for configurable duration and move to model - hours = events.map { |e| - if e.time_slots < 5 - 1 - else - v = e.time_slots / 4 - v += 1 if e.time_slots % 4 - v - end - } - hours.sum end def show_statistics @report_type = params[:id] @search_count = 0 case @report_type + when 'confirmed_events_by_track' + @data = [] + row = [] + @labels = @conference.tracks.collect(&:name) + @labels.each { |track| + row << @conference.events.confirmed.joins(:track).where(tracks: { name: track }).count + } + @data << row + @search_count = row.inject(:+) + when 'events_by_track' @data = [] row = [] - @labels = Track.all.collect { |t| t.name } + @labels = @conference.tracks.collect(&:name) @labels.each { |track| - row << @conference.events.confirmed.joins(:track).where(:tracks => { :name => track}).count + row << @conference.events.candidates.joins(:track).where(tracks: { name: track }).count } @data << row @search_count = row.inject(:+) when 'event_timeslot_sum' @data = [] row = [] - @labels = %w{LecturesCommited LecturesConfirmed LecturesUnconfirmed Lectures Workshops} - events = @conference.events.where(:event_type => :lecture, :state => [:confirmed, :unconfirmed]) - row << event_duration_sum(events) - events = @conference.events.where(:event_type => :lecture, :state => :confirmed) - row << event_duration_sum(events) - events = @conference.events.where(:event_type => :lecture, :state => :unconfirmed) - row << event_duration_sum(events) - events = @conference.events.where(:event_type => :lecture) - row << event_duration_sum(events) - events = @conference.events.where(:event_type => :workshops) - row << event_duration_sum(events) + @labels = %w(LecturesCommited LecturesConfirmed LecturesUnconfirmed Lectures Workshops) + events = @conference.events.where(event_type: :lecture, state: [:accepting, :confirmed, :unconfirmed, :scheduled]) + row << @conference.event_duration_sum(events) + events = @conference.events.where(event_type: :lecture, state: [:confirmed, :scheduled]) + row << @conference.event_duration_sum(events) + events = @conference.events.where(event_type: :lecture, state: [:acepting, :unconfirmed]) + row << @conference.event_duration_sum(events) + events = @conference.events.where(event_type: :lecture) + row << @conference.event_duration_sum(events) + events = @conference.events.where(event_type: :workshops) + row << @conference.event_duration_sum(events) @data << row + @search_count = nil + + when 'people_speaking_by_day' + @data = [] + row = [] + @labels = %w(Day FullName PublicName Email Event Role State) # TODO translate + + @conference.days.each do |day| + @conference.events.confirmed.no_conflicts.is_public.scheduled_on(day).order(:start_time).each do |event| + event.event_people.presenter.each do |event_person| + person = event_person.person + row = [l(day.date), person.full_name, person.public_name, person.email, event.title, event_person.event_role, event_person.role_state] + @data << row + end + end + end + + @search_count = nil end - render :show + respond_to do |format| + format.html { render :show } + end end + def show_transport_needs + @search = @conference.transport_needs.search(params[:q]) + @transport_needs = @search.result + @report_type = params[:id] + + render :show + end end diff --git a/app/controllers/schedule_controller.rb b/app/controllers/schedule_controller.rb index 13970f0..fc32e97 100644 --- a/app/controllers/schedule_controller.rb +++ b/app/controllers/schedule_controller.rb @@ -1,46 +1,113 @@ -class ScheduleController < ApplicationController - - before_filter :authenticate_user! - before_filter :require_cfp_admin +class ScheduleController < BaseConferenceController + before_action :crew_only!, except: %i[update_track update_event] def index params[:day] ||= 0 + @schedules_events = [] @day = @conference.days[params[:day].to_i] - @scheduled_events = @conference.events.accepted.scheduled_on(@day).order(:title) - @unscheduled_events = @conference.events.accepted.unscheduled.order(:title) + + @scheduled_events = @conference.events.accepted.includes([:track, :room, :conflicts]).scheduled_on(@day).order(:title) unless @day.nil? + @unscheduled_events = @conference.events.accepted.includes([:track, :room, :conflicts]).unscheduled.order(:title) end def update_track - if params[:track_id] and params[:track_id] =~ /\d+/ - @unscheduled_events = @conference.events.accepted.unscheduled.where(:track_id => params[:track_id]) - else - @unscheduled_events = @conference.events.accepted.unscheduled - end - render :partial => "unscheduled_events" + authorize @conference, :manage? + @unscheduled_events = if params[:track_id] and params[:track_id] =~ /\d+/ + @conference.events.accepted.unscheduled.where(track_id: params[:track_id]) + else + @conference.events.accepted.unscheduled + end + render partial: 'unscheduled_events' end def update_event + authorize @conference, :manage? event = @conference.events.find(params[:id]) - affected_event_ids = event.update_attributes_and_return_affected_ids(params[:event]) + affected_event_ids = event.update_attributes_and_return_affected_ids(event_params) @affected_events = @conference.events.find(affected_event_ids) end def new_pdf + @orientations = %w[auto landscape portrait] end def custom_pdf + return redirect_to :new_schedule_pdf unless params.key?(:room_ids) + @page_size = params[:page_size] - @day = Date.parse(params[:date]) - @rooms = @conference.rooms.public.find(params[:room_ids]) - @events = Hash.new - @rooms.each do |room| - @events[room] = room.events.accepted.public.scheduled_on(@day).order(:start_time).all - end + @day = @conference.days.find(params[:date_id]) + rooms = @conference.rooms.find(params[:room_ids]) + @view_model = ScheduleViewModel.new(@conference).for_day(@day) + @view_model.select_rooms(rooms) + + @layout = page_layout(params[:page_size], params[:half_page]) + @rooms_per_page = params[:rooms_per_page].to_i + @rooms_per_page = 1 if @rooms_per_page.zero? + @events = filter_events_by_day_and_rooms(@day, rooms) + + @orientation = case params[:orientation] + when 'landscape' + :landscape + when 'portrait' + :portrait + else + rooms.size > 3 ? :landscape : :portrait + end respond_to do |format| format.pdf end + rescue ActiveRecord::RecordNotFound => e + flash[:notice] = e.message + redirect_to action: :new_pdf end + def html_exports + end + + def create_static_export + redirect_to schedule_path, notice: 'program_export_base_url needs to be set' if @conference.program_export_base_url.blank? + StaticProgramExportJob.new.async.perform @conference, check_conference_locale(params[:export_locale]) + redirect_to schedule_html_exports_path, notice: 'Static schedule export started. Please reload this page after a minute.' + end + + def download_static_export + conference_export = @conference.conference_export(check_conference_locale(params[:export_locale])) + if conference_export&.tarball && File.readable?(conference_export.tarball.path) + send_file conference_export.tarball.path, type: 'application/x-tar-gz' + else + redirect_to schedule_path, notice: 'No export found to download.' + end + end + + private + + def event_params + params.require(:event).permit(:start_time, :room_id) + end + + def check_conference_locale(locale = 'en') + if @conference.language_codes.include?(locale) + locale + else + @conference.language_codes.first + end + end + + def page_layout(page_size, half_page) + if half_page + CustomPDF::HalfPageLayout.new(page_size) + else + CustomPDF::FullPageLayout.new(page_size) + end + end + + def filter_events_by_day_and_rooms(day, rooms) + events = {} + rooms.each do |room| + events[room] = room.events.accepted.is_public.scheduled_on(day).order(:start_time) + end + events + end end diff --git a/app/controllers/statistics_controller.rb b/app/controllers/statistics_controller.rb index 70f51f4..daa5b34 100644 --- a/app/controllers/statistics_controller.rb +++ b/app/controllers/statistics_controller.rb @@ -1,29 +1,37 @@ -class StatisticsController < ApplicationController +class StatisticsController < BaseConferenceController + before_action :crew_only!, except: %i[update_track update_event] def events_by_state case params[:type] - when "lectures" - result = @conference.events_by_state_and_type(:lecture) - when "workshops" - result = @conference.events_by_state_and_type(:workshop) - when "others" - remaining = Event::TYPES - [:workshop,:lecture] - result = @conference.events_by_state_and_type(remaining) + when 'lectures' + result = @conference.events_by_state_and_type(:lecture) + when 'workshops' + result = @conference.events_by_state_and_type(:workshop) + when 'others' + remaining = Event::TYPES - [:workshop, :lecture] + result = @conference.events_by_state_and_type(remaining) else - result = @conference.events_by_state + result = @conference.events_by_state end respond_to do |format| - format.json { render :json => result.to_json } + format.json { render json: result.to_json } end end def language_breakdown result = @conference.language_breakdown(params[:accepted_only]) respond_to do |format| - format.json { render :json => result.to_json } + format.json { render json: result.to_json } end end + def gender_breakdown + result = @conference.gender_breakdown(params[:accepted_only]) + + respond_to do |format| + format.json { render json: result.to_json } + end + end end diff --git a/app/controllers/tickets_controller.rb b/app/controllers/tickets_controller.rb new file mode 100644 index 0000000..ab92084 --- /dev/null +++ b/app/controllers/tickets_controller.rb @@ -0,0 +1,60 @@ +class TicketsController < BaseConferenceController + before_action :manage_only! + before_action :check_ticket_server + + def create_event + @event = Event.find(params[:id]) + server = @conference.ticket_server + + begin + title = t(:your_submission, locale: @event.language) + ' ' + @event.title.truncate(30) + remote_id = server.create_remote_ticket(title: title, + requestors: server.create_ticket_requestors(@event.speakers), + owner_email: current_user.email, + frab_url: event_url(@event), + test_only: params[:test_only]) + rescue => ex + return redirect_to event_path(id: params[:id], method: :get), alert: "Failed to create ticket: #{ex.message}" + end + + if remote_id.nil? + return redirect_to event_path(id: params[:id], method: :get), alert: 'Failed to receive remote id' + end + + @event.ticket = Ticket.new if @event.ticket.nil? + @event.ticket.remote_ticket_id = remote_id + @event.save + redirect_to event_path(id: params[:id], method: :get) + end + + def create_person + @person = Person.find(params[:id]) + server = @conference.ticket_server + + begin + remote_id = server.create_remote_ticket(title: @person.full_name, + requestors: @person.email, + owner_email: current_user.email, + frab_url: person_url(@person), + test_only: params[:test_only]) + rescue => ex + return redirect_to person_path(id: params[:id], method: :get), alert: "Failed to create ticket: #{ex.message}" + end + + if remote_id.nil? + return redirect_to person_path(id: params[:id], method: :get), alert: 'Failed to receive remote id' + end + + @person.ticket = Ticket.new if @person.ticket.nil? + @person.ticket.remote_ticket_id = remote_id + @person.save + redirect_to person_path(id: params[:id], method: :get) + end + + private + + def check_ticket_server + return if @conference.ticket_server && @conference.ticket_server_enabled? + redirect_to edit_conference_path(conference_acronym: @conference.acronym), alert: 'No ticket server configured' + end +end diff --git a/app/controllers/transport_needs_controller.rb b/app/controllers/transport_needs_controller.rb new file mode 100644 index 0000000..eef09f4 --- /dev/null +++ b/app/controllers/transport_needs_controller.rb @@ -0,0 +1,52 @@ +class TransportNeedsController < BaseConferenceController + before_action :find_person + before_action :check_enabled + before_action :orga_only! + + def new + @transport_need = TransportNeed.new + @transport_need.seats = 1 + end + + def edit + @transport_need = @person.transport_needs.find(params[:id]) + end + + def index + @transport_needs = @person.transport_needs.where(conference_id: @conference.id) + end + + def update + transport_need = @person.transport_needs.find(params[:id]) + transport_need.update_attributes(transport_needs_params) + redirect_to(person_url(@person), notice: 'Transport need was successfully updated.') + end + + def create + tn = TransportNeed.new(transport_needs_params) + tn.conference = @conference + @person.transport_needs << tn + redirect_to(person_url(@person), notice: 'Transport need was successfully added.') + end + + def destroy + @person.transport_needs.find(params[:id]).destroy + redirect_to(person_url(@person), notice: 'Transport need was successfully destroyed.') + end + + private + + def find_person + @person = Person.find(params[:person_id]) + end + + def check_enabled + unless @conference.transport_needs_enabled? + redirect_to(person_url(@person), notice: 'Transport needs are not enabled for this conference') + end + end + + def transport_needs_params + params.require(:transport_need).permit(:at, :transport_type, :seats, :booked, :note) + end +end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 2a037ad..94c9d13 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,81 +1,113 @@ -class UsersController < ApplicationController - - before_filter :authenticate_user! - before_filter :require_admin - before_filter :find_person +class UsersController < BaseCrewController + before_action :find_person + before_action :authorize_person_user, except: %i[new create] + before_action :ensure_user, except: %i[new create] + layout :layout_if_conference # GET /users/1 - # GET /users/1.xml def show - @user = @person.user - - redirect_to new_person_user_path(@person) unless @user end # GET /users/new - # GET /users/new.xml def new @user = User.new respond_to do |format| format.html # new.html.erb - format.xml { render :xml => @user } end end # GET /users/1/edit def edit - @user = @person.user + @user.conference_users = policy_scope(@user.conference_users) end # POST /users - # POST /users.xml def create - @user = User.new(params[:user]) - @user.role = params[:user][:role] + @user = authorize User.new(user_params) + + set_allowed_user_roles(user_params[:role], 'submitter') @user.person = @person @user.skip_confirmation! respond_to do |format| if @user.save - format.html { redirect_to(person_user_path(@person), :notice => 'User was successfully created.') } - format.xml { render :xml => @user, :status => :created, :location => @user } + format.html { redirect_to(edit_person_user_path(@person), notice: 'User was successfully created.') } else - format.html { render :action => "new" } - format.xml { render :xml => @user.errors, :status => :unprocessable_entity } + format.html { render action: 'new' } end end end # PUT /users/1 - # PUT /users/1.xml def update - @user = @person.user [:password, :password_confirmation].each do |password_key| params[:user].delete(password_key) if params[:user][password_key].blank? end - @user.role = params[:user][:role] + + set_allowed_user_roles(params[:user][:role]) + params[:user].delete(:role) + + # only allowed user.conference_users from selection + if !current_user.is_admin? && policy(@conference).orga? && params[:user][:conference_users_attributes].present? + filter_conference_users(params[:user][:conference_users_attributes]) + end respond_to do |format| - if @user.update_attributes(params[:user]) - format.html { redirect_to(person_user_path(@person), :notice => 'User was successfully updated.') } - format.xml { head :ok } + if @user.update_attributes(user_params) + @user.confirm unless @user.confirmed? + bypass_sign_in(@user) if current_user == @user + format.html { redirect_to(edit_crew_user_path(@person), notice: 'User was successfully updated.') } else - format.html { render :action => "edit" } - format.xml { render :xml => @user.errors, :status => :unprocessable_entity } + flash[:errors] = @user.errors.full_messages.join + format.html { render action: 'edit' } end end end # DELETE /users/1 - # DELETE /users/1.xml def destroy end private + def user_params + params.require(:user).permit(:id, :role, :email, :password, :password_confirmation, + conference_users_attributes: %i(id role conference_id _destroy)) + end + + def assign_user_role?(role) + policy(Conference).orga? && User::USER_ROLES.include?(role) + end + + def set_allowed_user_roles(role, fallback=nil) + if current_user.is_admin? + @user.role = role + elsif assign_user_role?(role) + @user.role = role + elsif fallback + @user.role = fallback + end + end + + def ensure_user + return if current_user.is_admin? + redirect_to new_person_user_path(@person) unless @user + end + def find_person @person = Person.find(params[:person_id]) end + def authorize_person_user + @user = authorize @person.user + end + + def filter_conference_users(conference_users) + orga_conferences = policy_scope(current_user.conference_users).map(&:conference_id) + conference_users.delete_if do |_, conference_user| + conference_id = conference_user[:conference_id] + conference_id.nil? || !orga_conferences.include?(conference_id.to_i) + end + end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 8eaa8bf..c39d938 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,56 +1,132 @@ module ApplicationHelper + def management_page_title + title = '' + title += @conference.acronym + if @event.present? + title += "- #{@event.title}" + elsif @person.present? + title += "- #{@person.full_name}" + end + title += '- Conference Management' + title + end + + def home_page_title + 'KDE Conference Management' + end + + def accessible_conferences + if current_user.is_admin? + Conference.creation_order + elsif current_user.is_crew? + Conference.accessible_by_crew(current_user).creation_order + else + Conference.accessible_by_submitter(current_user) + end + end + + def manageable_conferences + if current_user.is_admin? + Conference.creation_order + elsif current_user.is_crew? + Conference.accessible_by_orga(current_user).creation_order + else + [] + end + end + + def active_class?(*paths) + 'active' if paths.any? { |path| current_page?(path.gsub(/\?.*/, '')) } + end def image_box(image, size) - content_tag(:div, :class => "image #{size}") do + content_tag(:div, class: "image #{size}") do image_tag image.url(size) end end + def image_input_box(image) + content_tag(:div, class: 'clearfix input image small') do + image_tag image.url(:small) + end + end + def duration_to_time(duration_in_minutes) - minutes = sprintf("%02d", duration_in_minutes % 60) - hours = sprintf("%02d", duration_in_minutes / 60) - "#{hours}:#{minutes}" + '%02d:%02d' % [duration_in_minutes / 60, duration_in_minutes % 60] end def icon(name) image_tag "icons/#{name}.png" end def action_button(button_type, link_name, path, options = {}) options[:class] = "btn #{button_type}" if options[:hint] - options[:rel] = "popover" - options["data-original-title"] = "Hint" - options["data-content"] = options[:hint] - options["data-placement"] = "below" + options[:rel] = 'popover' + options['data-original-title'] = 'Hint' + options['data-content'] = options[:hint] + options['data-placement'] = 'below' options[:hint] = nil end link_to link_name, path, options end - def add_association_link(text, form_builder, div_class, html_options = {}) - link_to_add_association text, form_builder, div_class, html_options.merge(:class => "assoc btn") + def add_association_link(association_name, form_builder, div_class, html_options = {}) + link_to_add_association t(:add_association, name: t('activerecord.models.' + association_name.to_s.singularize)), form_builder, div_class, html_options.merge(class: 'assoc btn') end - def remove_association_link(text, form_builder) - link_to_remove_association(text, form_builder, :class => "assoc btn danger") + tag(:hr) + def remove_association_link(association_name, form_builder) + link_to_remove_association(t(:remove_association, name: t('activerecord.models.' + association_name.to_s.singularize)), form_builder, class: 'assoc btn danger') + tag(:hr) end def dynamic_association(association_name, title, form_builder, options = {}) - render "shared/dynamic_association", :association_name => association_name, :title => title, :f => form_builder, :hint => options[:hint] + render 'shared/dynamic_association', association_name: association_name, title: title, f: form_builder, hint: options[:hint] end def translated_options(collection) - result = Array.new + result = [] collection.each do |element| result << [t("options.#{element}"), element] end result end + def t_boolean(b) + if b + t('simple_form.yes') + else + t('simple_form.no') + end + end + def available_conference_locales - conference_locales = @conference.language_codes.map {|c| c.to_sym} + conference_locales = @conference.language_codes.map(&:to_sym) I18n.available_locales & conference_locales end + def by_speakers(event) + speakers = event.speakers.map { |p| link_to(p.public_name, p) } + if speakers.present? + 'by '.html_safe + safe_join(speakers, ', ') + else + '' + end + end + + def show_cfp?(user, conference) + return unless user + return true if conference.call_for_participation&.still_running? && conference.days.present? + return true if user.person.involved_in?(conference) + false + end + + def humanized_access_level + return t('role.admin') if current_user.is_admin? + return t('role.orga') if current_user.has_role?(@conference, 'orga') + return t('role.coordinator') if current_user.has_role?(@conference, 'coordinator') + return t('role.reviewer') if current_user.has_role?(@conference, 'reviewer') + return t('role.crew') if current_user.is_crew? + return t('role.submitter') if current_user.is_submitter? + fail 'should not happen: user without acl' + end end diff --git a/app/helpers/call_for_participations_helper.rb b/app/helpers/call_for_participations_helper.rb new file mode 100644 index 0000000..4fbc459 --- /dev/null +++ b/app/helpers/call_for_participations_helper.rb @@ -0,0 +1,6 @@ +module CallForParticipationsHelper + def available_locales(conference) + codes = conference.language_codes + codes | Person.involved_in(conference).map { |p| p.languages.all }.flatten.map { |l| l.code.downcase } + end +end diff --git a/app/helpers/cfp/events_helper.rb b/app/helpers/cfp/events_helper.rb index 0b3c892..588c1a1 100644 --- a/app/helpers/cfp/events_helper.rb +++ b/app/helpers/cfp/events_helper.rb @@ -1,2 +1,6 @@ module Cfp::EventsHelper + def deny_accepted(event) + accepted = event.accepted? + { readonly: accepted, disabled: accepted } + end end diff --git a/app/helpers/cfp/people_helper.rb b/app/helpers/cfp/people_helper.rb index af36b67..960becb 100644 --- a/app/helpers/cfp/people_helper.rb +++ b/app/helpers/cfp/people_helper.rb @@ -1,8 +1,6 @@ module Cfp::PeopleHelper - def cfp_hard_deadline_over? - return false unless @conference.call_for_papers.hard_deadline - Date.today > @conference.call_for_papers.hard_deadline + return false unless @conference.call_for_participation.hard_deadline + Date.today > @conference.call_for_participation.hard_deadline end - end diff --git a/app/helpers/conferences_helper.rb b/app/helpers/conferences_helper.rb index 54c81b7..0830f42 100644 --- a/app/helpers/conferences_helper.rb +++ b/app/helpers/conferences_helper.rb @@ -1,17 +1,22 @@ module ConferencesHelper + def conference_tab(current, active) + css_class = 'active' if current == active + haml_tag :li, class: css_class do + yield + end + end def timeslot_durations(conference) - result = Array.new - durations = [1,5,10,15,20,30,45,60,90,120] + result = [] + durations = [1, 5, 10, 15, 20, 30, 45, 60, 90, 120] if conference.timeslot_duration and conference.events.count > 0 - durations.reject! do |duration| + durations.reject! do |duration| duration > conference.timeslot_duration or (conference.timeslot_duration % duration) != 0 end end durations.each do |duration| result << [duration_to_time(duration), duration] end result end - end diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index b1abc4f..6fe481c 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -1,40 +1,27 @@ module EventsHelper - def fix_http_proto(url) if url.start_with?('https') or url.start_with?('http') or url.start_with?('ftp') url else "http://#{url}" end end + def event_start_time + return t(:date_not_set) unless @event.start_time + I18n.l(@event.start_time, format: :pretty_datetime) + end + def timeslots - slots = Array.new - @conference.max_timeslots.times do |i| + slots = [] + (@conference.max_timeslots + 1).times do |i| slots << [format_time_slots(i), i] end slots end - def start_times - times = Array.new - date = @conference.first_day - while date <= @conference.last_day - time = date.to_time_in_current_zone - time = time.since(7.hours) - end_time = time.since(16.hours) - while time <= end_time - times << [time.strftime("%Y-%m-%d %H:%M"), time] - time = time.since(@conference.timeslot_duration.minutes) - end - date = date.tomorrow - end - times - end - def format_time_slots(number_of_time_slots) duration_in_minutes = number_of_time_slots * @conference.timeslot_duration duration_to_time(duration_in_minutes) end - end diff --git a/app/helpers/number_helper.rb b/app/helpers/number_helper.rb new file mode 100644 index 0000000..4a98a09 --- /dev/null +++ b/app/helpers/number_helper.rb @@ -0,0 +1,9 @@ +module NumberHelper + def to_currency(i) + options = {} + options[:unit] = ENV['FRAB_CURRENCY_UNIT'] unless ENV['FRAB_CURRENCY_UNIT'].nil? + options[:format] = ENV['FRAB_CURRENCY_FORMAT'] unless ENV['FRAB_CURRENCY_FORMAT'].nil? + + number_to_currency i, options + end +end diff --git a/app/helpers/public/feedback_helper.rb b/app/helpers/public/feedback_helper.rb index d4d79de..c2cf1ae 100644 --- a/app/helpers/public/feedback_helper.rb +++ b/app/helpers/public/feedback_helper.rb @@ -1,7 +1,5 @@ module Public::FeedbackHelper - def feedback_page? request.path =~ /feedback/ end - end diff --git a/app/helpers/public/schedule_helper.rb b/app/helpers/public/schedule_helper.rb index 6339165..386b478 100644 --- a/app/helpers/public/schedule_helper.rb +++ b/app/helpers/public/schedule_helper.rb @@ -1,26 +1,39 @@ module Public::ScheduleHelper + require 'scanf' def each_timeslot(&block) each_minutes(@conference.timeslot_duration, &block) end + def color_dark?(color) + parts = color.scanf('%02x%02x%02x') + return parts.sum < 384 if parts.length == 3 + + parts = color.scanf('%01x%01x%01x') + return parts.sum < 24 if parts.length == 3 + + false + end + def track_class(event) if event.track "track-#{event.track.name.parameterize}" else - "track-default" + 'track-default' end end - def selected(regex) - "selected" if request.path =~ regex + def different_track_colors? + colors = @conference.tracks_including_subs.map(&:color) + colors.uniq.size > 1 end - def day_at(day, time) - day.to_time.change(:hour => time.hour, :min => time.min) + def selected(regex) + #'selected' if request.path.match?(regex) + 'selected' if request.path =~ regex end - def day_at_i(day, time) - day.to_time.change(:hour => time/60, :min => time%60) + def day_selected(index) + 'selected' if request.path.ends_with?(index.to_s) end end diff --git a/app/helpers/raty_helper.rb b/app/helpers/raty_helper.rb new file mode 100644 index 0000000..bcf3ef7 --- /dev/null +++ b/app/helpers/raty_helper.rb @@ -0,0 +1,29 @@ +module RatyHelper + def raty_for_input(id, score_input_id, score_name) + content_tag :div, id: id, class: 'rating', data: { + 'raty-input': 'on', + source: score_input_id, + target: score_name + }.merge(image_data) do + yield + end + end + + def raty_for(id, score) + content_tag :div, '', id: id, class: 'rating', data: { + raty: 'on', + rating: score + }.merge(image_data) + end + + private + + def image_data + { + path: '', + 'star-on': image_path('raty/star-on.png'), + 'star-off': image_path('raty/star-off.png'), + 'star-half': image_path('raty/star-half.png') + } + end +end diff --git a/app/helpers/recent_changes_helper.rb b/app/helpers/recent_changes_helper.rb index 4006f3d..4e01fe6 100644 --- a/app/helpers/recent_changes_helper.rb +++ b/app/helpers/recent_changes_helper.rb @@ -1,27 +1,28 @@ module RecentChangesHelper + def yaml_load_version(version) + YAML.safe_load(version.object_changes, %w(Time Date), [], true) + rescue => e + Rails.logger.error "Invalid YAML in recent changes version #{version.id}: #{e.message}" + [] + end def associated_link_for(version) - begin - associated = version.associated_type.constantize.find(version.associated_id) - if associated.is_a? Conference - link_to associated.to_s, edit_conference_path - else - link_to associated.to_s, associated - end - rescue - "[deleted #{version.associated_type.constantize} with id=#{version.associated_id}]" + associated = version.associated_type.constantize.find(version.associated_id) + if associated.is_a? Conference + link_to associated.to_s, edit_conference_path + else + link_to associated.to_s, associated end rescue ActiveRecord::RecordNotFound - version.associated_type + "[deleted #{version.associated_type.constantize} with id=#{version.associated_id}]" end def verb_for(event) case event - when "destroy" - "deleted" + when 'destroy' + 'deleted' else "#{event}d" end end - end diff --git a/app/helpers/schedule_helper.rb b/app/helpers/schedule_helper.rb index 54afb0e..70c791c 100644 --- a/app/helpers/schedule_helper.rb +++ b/app/helpers/schedule_helper.rb @@ -1,46 +1,40 @@ module ScheduleHelper - def day_active?(index) - "active" if params[:day].to_i == index + def schedule_button_text + return 'Preview public schedule' if @conference.schedule_public + 'Preview schedule' end - def landscape? - @rooms.size > 3 + def day_active?(index) + 'active' if params[:day].to_i == index end - # for pdf - def number_of_rows - ((@conference.day_end - @conference.day_start) * 60 / @conference.timeslot_duration).to_i - end - # for pdf def number_of_timeslots - number_of_rows * 15.0 / @conference.timeslot_duration + timeslots_between(@day.start_date, @day.end_date) end - # for event boxes in public schedule + # for pdf: event boxes in public schedule def event_coordinates(room_index, event, column_width, row_height, offset = 0) x = 1.5.cm - 1 + room_index * column_width - day_end = event.start_time.change(:hour => @conference.day_end, :min => 0) - y = ((day_end - event.start_time) / (@conference.timeslot_duration * 60)) * row_height + y = (timeslots_between(event.start_time, @day.end_date) - 1) * row_height y += offset [x, y] end - def each_minutes(minutes, &block) - time = @conference.day_start*60 - while time < @conference.day_end*60 + def each_minutes(minutes) + time = @day.start_date + while time < @day.end_date yield time - time += minutes + time = time.since(minutes.minutes) end end def each_15_minutes(&block) each_minutes(15, &block) end - def minutes_to_time_str(minutes) - "%02d:%02d" % [minutes/60, minutes%60] + def timeslots_between(start_date, end_date) + ((end_date - start_date) / 60 / @conference.timeslot_duration).to_i + 1 end - end diff --git a/app/helpers/tickets_helper.rb b/app/helpers/tickets_helper.rb new file mode 100644 index 0000000..bda327c --- /dev/null +++ b/app/helpers/tickets_helper.rb @@ -0,0 +1,16 @@ +module TicketsHelper + def get_ticket_view_url(remote_id = 0) + return if @conference.nil? + return if @conference.ticket_server.nil? + @conference.ticket_server.get_ticket_view_url(remote_id) + end + + private + + def is_a_number(test) + Integer(test) + true + rescue + false + end +end diff --git a/app/helpers/web_app_theme_helper.rb b/app/helpers/web_app_theme_helper.rb index a679da6..8c1652f 100644 --- a/app/helpers/web_app_theme_helper.rb +++ b/app/helpers/web_app_theme_helper.rb @@ -1,25 +1,23 @@ -module WebAppThemeHelper - +module WebAppThemeHelper def block(&block) - content_tag(:div, {:class => "block"}, &block) + content_tag(:div, { class: 'block' }, &block) end def content(&block) - content_tag(:div, {:class => "content"}, &block) + content_tag(:div, { class: 'content' }, &block) end def inner(&block) - content_tag(:div, {:class => "inner"}, &block) + content_tag(:div, { class: 'inner' }, &block) end def actions_bar(&block) - content_tag(:div, {:class => "actions-bar"}, &block) + content_tag(:div, { class: 'actions-bar' }, &block) end def actions_block(&block) - content_tag(:div, {:id => "actions", :class => "block"} ) do - content_tag(:h3, "Actions") + content(&block) + content_tag(:div, id: 'actions', class: 'block') do + content_tag(:h3, 'Actions') + content(&block) end end - end diff --git a/app/inputs/inline_boolean_input.rb b/app/inputs/inline_boolean_input.rb new file mode 100644 index 0000000..f65eab8 --- /dev/null +++ b/app/inputs/inline_boolean_input.rb @@ -0,0 +1,7 @@ +class InlineBooleanInput < SimpleForm::Inputs::BooleanInput + # Render 'inline' boolean control, regardless of the value of config.boolean_style + def input(wrapper_options = nil) + merged_input_options = merge_wrapper_options(input_html_options, wrapper_options) + build_check_box(unchecked_value, merged_input_options) + end +end diff --git a/app/inputs/language_select_input.rb b/app/inputs/language_select_input.rb new file mode 100644 index 0000000..5860e06 --- /dev/null +++ b/app/inputs/language_select_input.rb @@ -0,0 +1,28 @@ +class LanguageSelectInput < SimpleForm::Inputs::CollectionSelectInput + def input(wrapper_options = nil) + @collection = [] + + prepend_priority_languages(options.delete(:priority)) + @collection += LocalizedLanguageSelect.localized_languages_array(wrapper_options[:collection] || {}) + filter_collection(options[:only]) + + super(wrapper_options) + end + + private + + SEP = '----------'.freeze + BLANK = ''.freeze + + def filter_collection(only) + return unless only + only += [SEP, BLANK] + @collection.delete_if { |_language, code| not code.in?(only) } + end + + def prepend_priority_languages(priority_languages) + return unless priority_languages + @collection += LocalizedLanguageSelect.priority_languages_array(priority_languages) + @collection << [SEP, BLANK] + end +end diff --git a/app/inputs/rating_input.rb b/app/inputs/rating_input.rb index f14ce8c..b4888ad 100644 --- a/app/inputs/rating_input.rb +++ b/app/inputs/rating_input.rb @@ -1,7 +1,7 @@ -class RatingInput < FormtasticBootstrap::Inputs::HiddenInput - def to_html - generic_input_wrapping do - builder.hidden_field(method, input_html_options) + "
".html_safe - end +class RatingInput < SimpleForm::Inputs::HiddenInput + def input(_wrapper_options = nil) + out = ActiveSupport::SafeBuffer.new + out << @builder.hidden_field(attribute_name.to_s).html_safe + template.raty_for_input('my_rating', '#event_rating_rating', 'event_rating[rating]') { out } end end diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb new file mode 100644 index 0000000..a009ace --- /dev/null +++ b/app/jobs/application_job.rb @@ -0,0 +1,2 @@ +class ApplicationJob < ActiveJob::Base +end diff --git a/app/jobs/send_bulk_mail_job.rb b/app/jobs/send_bulk_mail_job.rb new file mode 100644 index 0000000..25fa963 --- /dev/null +++ b/app/jobs/send_bulk_mail_job.rb @@ -0,0 +1,33 @@ +class SendBulkMailJob + include SuckerPunch::Job + + def perform(template, send_filter) + persons = Person + .joins(events: :conference) + .where('conferences.id': template.conference.id) + + case send_filter + when 'all_speakers_in_confirmed_events' + persons = persons + .where('events.state': 'confirmed') + .where('event_people.event_role': 'speaker') + + when 'all_speakers_in_unconfirmed_events' + persons = persons + .where('events.state': 'unconfirmed') + .where('event_people.event_role': 'speaker') + + when 'all_speakers_in_scheduled_events' + persons = persons + .where('events.state': 'scheduled') + .where('event_people.event_role': 'speaker') + end + + persons = persons.group(:'people.id') + + persons.each do |p| + UserMailer.bulk_mail(p, template).deliver_now + Rails.logger.info "Mail template #{template.name} delivered to #{p.first_name} #{p.last_name} (#{p.email})" + end + end +end diff --git a/app/jobs/send_bulk_ticket_job.rb b/app/jobs/send_bulk_ticket_job.rb new file mode 100644 index 0000000..33fc91f --- /dev/null +++ b/app/jobs/send_bulk_ticket_job.rb @@ -0,0 +1,15 @@ +class SendBulkTicketJob + include SuckerPunch::Job + + def perform(conference, filter) + Rails.logger.debug 'performing ' + filter + ' on ' + conference.acronym + case filter + when 'accepting' + conference.events.where(state: filter).map(&:notify!) + when 'rejecting' + conference.events.where(state: filter).map(&:notify!) + when 'confirmed' + conference.events.where(state: filter).scheduled.map(&:notify!) + end + end +end diff --git a/app/jobs/static_program_export_job.rb b/app/jobs/static_program_export_job.rb new file mode 100644 index 0000000..e8917d0 --- /dev/null +++ b/app/jobs/static_program_export_job.rb @@ -0,0 +1,24 @@ +class StaticProgramExportJob + require 'static_schedule' + require 'tempfile' + include SuckerPunch::Job + + def perform(conference, locale = 'en') + Dir.mktmpdir('static_export') do |dir| + Rails.logger.info "Create static export for #{conference} in #{dir}" + + exporter = StaticSchedule::Export.new(conference, locale, dir) + exporter.run_export + file = exporter.create_tarball + + unless File.readable?(file) + Rails.logger.error "Static export failed to create tarball at #{dir}" + raise StandardError, "Static export failed to create tarball at #{dir}" + end + + Rails.logger.info "Attach static export tarball #{file}" + conference_export = ConferenceExport.where(conference_id: conference.id, locale: locale).first_or_create + conference_export.update_attributes tarball: File.open(file) + end + end +end diff --git a/app/lib/bulk_mailer.rb b/app/lib/bulk_mailer.rb new file mode 100644 index 0000000..59ea04a --- /dev/null +++ b/app/lib/bulk_mailer.rb @@ -0,0 +1,47 @@ +class BulkMailer + def initialize(subject, from, mail_file, body_file, force = false) + @subject = subject + @from_email = from + @force = force + @emails = File.readlines(mail_file).collect(&:chomp) + @body = File.read(body_file) + + @emails.each { |email| + send_mail_to(email) + } + end + + private + + class BulkMail < ActionMailer::Base + def notify(template, person, args) + puts "send mail to: #{args[:to]}" + @person = person + mail(args) do |format| + format.text { render inline: template } + end + end + end + + def send_mail_to(email) + p = Person.find_by(email: email) + email_address_with_name = p.nil? ? email : "#{p.public_name} <#{email}>" + + email_address_with_name = if p.nil? + email + else + "#{p.public_name} <#{email}>" + end + + unless @force + unless p.include_in_mailings? + puts "skipped due to settings: #{email}" + return + end + end + + args = { subject: @subject, to: email_address_with_name, from: @from_email } + + BulkMail.notify(@body, p, args).deliver + end +end diff --git a/app/lib/conference_scrubber.rb b/app/lib/conference_scrubber.rb new file mode 100644 index 0000000..e1507b0 --- /dev/null +++ b/app/lib/conference_scrubber.rb @@ -0,0 +1,78 @@ +class ConferenceScrubber + include RakeLogger + + DUMMY_MAIL = 'root@localhost.localdomain'.freeze + + def initialize(conference, dry_run = false) + @conference = conference + @dry_run = dry_run + @current_conferences = last_years_conferences + PaperTrail.enabled = false + end + + def scrub! + log "dry run, won't change anything!" if @dry_run + ActiveRecord::Base.transaction do + scrub_people + scrub_event_ratings + end + end + + private + + def scrub_people + Person.involved_in(@conference).each { |person| + unless still_active(person) + log "scrubbing #{person.public_name} <#{person.email}>" + scrub_person(person) + end + } + end + + def last_years_conferences + Conference.all.select do |c| + date = if c.first_day + c.first_day.date + else + c.updated_at + end + date.since(1.year) > Time.now + end + end + + def still_active(person) + @current_conferences.each { |c| + return true if person.involved_in?(c) + } + false + end + + def scrub_person(person) + # get a writable record + person = Person.find person.id + + unless person.email_public or person.include_in_mailings + person.email = DUMMY_MAIL + end + person.phone_numbers.destroy_all unless @dry_run + person.im_accounts.destroy_all unless @dry_run + person.note = nil + + unless person.active_in_any_conference? + log "scrubbing description of #{person.public_name}" + person.abstract = nil + person.description = nil + person.avatar.destroy unless @dry_run + person.links.destroy_all unless @dry_run + end + person.save! unless @dry_run + end + + def scrub_event_ratings + return if @dry_run + log "scrubbing conference ratings of #{@conference.acronym}" + # keeps events average rating for performance reasons + EventRating.skip_callback(:save, :after, :update_average) + EventRating.joins(:event).where(Event.arel_table[:conference_id].eq(@conference.id)).destroy_all + end +end diff --git a/app/lib/custom_pdf.rb b/app/lib/custom_pdf.rb new file mode 100644 index 0000000..03dae70 --- /dev/null +++ b/app/lib/custom_pdf.rb @@ -0,0 +1,2 @@ +module CustomPDF +end diff --git a/app/lib/custom_pdf/full_page_layout.rb b/app/lib/custom_pdf/full_page_layout.rb new file mode 100644 index 0000000..a49638c --- /dev/null +++ b/app/lib/custom_pdf/full_page_layout.rb @@ -0,0 +1,50 @@ +module CustomPDF + class FullPageLayout + def initialize(page_size) + @page_size = page_size + end + attr_reader :page_size + attr_writer :bounds + + def header_left_anchor + [@bounds.left, @bounds.top + 0.5.cm] + end + + def header_center_anchor + [@bounds.left + 12.cm, @bounds.top + 0.5.cm] + end + + def header_right_anchor + [@bounds.right - 1.cm, @bounds.top + 0.5.cm] + end + + def header_height + 0.8.cm + end + + # determine borders by page size, because all timeslots need to fit + # on one page + def margin_height + return 3.5.cm if bigger_than_a4 + 2.5.cm + end + + def timeslot_height(number_of_timeslots) + (@bounds.height - margin_height - header_height) / number_of_timeslots + end + + def margin_width + 1.5.cm + end + + def page_width + @bounds.width - margin_width + end + + private + + def bigger_than_a4 + Prawn::Document::PageGeometry::SIZES['A4'].inject(:*) < Prawn::Document::PageGeometry::SIZES[@page_size].inject(:*) + end + end +end diff --git a/app/lib/custom_pdf/half_page_layout.rb b/app/lib/custom_pdf/half_page_layout.rb new file mode 100644 index 0000000..63313eb --- /dev/null +++ b/app/lib/custom_pdf/half_page_layout.rb @@ -0,0 +1,7 @@ +module CustomPDF + class HalfPageLayout < FullPageLayout + def page_width + @bounds.width / 2 - margin_width + end + end +end diff --git a/app/lib/humanized_date_range.rb b/app/lib/humanized_date_range.rb new file mode 100644 index 0000000..dd050ab --- /dev/null +++ b/app/lib/humanized_date_range.rb @@ -0,0 +1,8 @@ +module HumanizedDateRange + def humanized_date_range(format = :short_datetime) + return '' unless start_date.present? + I18n.localize(start_date, format: format) + + I18n.t('time.time_range_seperator') + + I18n.localize(end_date, format: :time) + end +end diff --git a/app/lib/import_export_helper.rb b/app/lib/import_export_helper.rb new file mode 100644 index 0000000..0709c2f --- /dev/null +++ b/app/lib/import_export_helper.rb @@ -0,0 +1,368 @@ +class ImportExportHelper + DEBUG = true + EXPORT_DIR = 'tmp/frab_export'.freeze + + def initialize(conference = nil) + @export_dir = EXPORT_DIR + @conference = conference + PaperTrail.enabled = false + end + + # everything except: RecentChanges + def run_export + if @conference.nil? + puts "[!] the conference wasn't found." + exit + end + + FileUtils.mkdir_p(@export_dir) + + ActiveRecord::Base.transaction do + dump 'conference', @conference + dump 'conference_tracks', @conference.tracks + dump 'conference_cfp', @conference.call_for_participation + dump 'conference_ticket_server', @conference.ticket_server + dump 'conference_rooms', @conference.rooms + dump 'conference_days', @conference.days + dump 'conference_languages', @conference.languages + events = dump 'events', @conference.events + dump_has_many 'tickets', @conference.events, 'ticket' + dump_has_many 'event_people', @conference.events, 'event_people' + dump_has_many 'event_feedbacks', @conference.events, 'event_feedbacks' + people = dump_has_many 'people', @conference.events, 'people' + dump_has_many 'event_links', @conference.events, 'links' + attachments = dump_has_many 'event_attachments', @conference.events, 'event_attachments' + dump_has_many 'event_ratings', @conference.events, 'event_ratings' + dump_has_many 'people_phone_numbers', people, 'phone_numbers' + dump_has_many 'people_im_accounts', people, 'im_accounts' + dump_has_many 'people_links', people, 'links' + dump_has_many 'people_languages', people, 'languages' + dump 'people_availabilities', Availability.where(conference: @conference, person: people) + dump_has_many 'users', people, 'user' + # TODO languages + # TODO videos + # TODO notifications + export_paperclip_files(events, people, attachments) + end + end + + def run_import(export_dir = EXPORT_DIR) + @export_dir = export_dir + unless File.directory? @export_dir + puts "Directory #{@export_dir} does not exist!" + exit + end + disable_callbacks + + # old => new + @mappings = { + conference: {}, tracks: {}, cfp: {}, rooms: {}, days: {}, + people: {}, users: {}, + events: {}, + people_user: {} + } + + ActiveRecord::Base.transaction do + unpack_paperclip_files + restore_all_data + end + end + + private + + def restore_all_data + restore('conference', Conference) do |id, c| + test = Conference.find_by(acronym: c.acronym) + if test + puts "conference #{c} already exists!" + exit + end + puts " #{c}" if DEBUG + c.save! + @mappings[:conference][id] = c.id + @conference_id = c.id + end + + restore_conference_data + + restore_multiple('people', Person) do |id, obj| + # TODO could be the wrong person if persons share email addresses!? + persons = Person.where(email: obj.email, public_name: obj.public_name) + person = persons.first + + if person + # don't create a new person + @mappings[:people][id] = person.id + @mappings[:people_user][obj.user_id] = person + if person.avatar.nil? && (file = import_file('people/avatars', id, obj.avatar_file_name)) + person.avatar = file + person.save + end + else + if (file = import_file('people/avatars', id, obj.avatar_file_name)) + obj.avatar = file + end + obj.save! + @mappings[:people][id] = obj.id + @mappings[:people_user][obj.user_id] = obj + end + end + + restore_users do |id, yaml, obj| + user = User.find_by(email: obj.email) + if user + # don't create a new user + @mappings[:users][id] = user.id + else + %w( confirmation_sent_at confirmation_token confirmed_at created_at + current_sign_in_at current_sign_in_ip last_sign_in_at + last_sign_in_ip encrypted_password + remember_created_at remember_token + reset_password_token role sign_in_count updated_at).each { |var| + obj.send("#{var}=", yaml[var]) + } + obj.confirmed_at ||= Time.now + obj.person = @mappings[:people_user][id] + obj.save(validate: false) + @mappings[:users][id] = obj.id + end + end + + restore_multiple('events', Event) do |id, obj| + obj.conference_id = @conference_id + obj.track_id = @mappings[:tracks][obj.track_id] + obj.room_id = @mappings[:rooms][obj.room_id] + if (file = import_file('events/logos', id, obj.logo_file_name)) + obj.logo = file + end + obj.save! + @mappings[:events][id] = obj.id + end + + # uses mappings: events, people + restore_events_data + + # uses mappings: people, days + restore_people_data + + update_counters + Event.all.each(&:update_conflicts) + end + + def restore_conference_data + restore_multiple('conference_tracks', Track) do |id, obj| + obj.conference_id = @conference_id + obj.save! + @mappings[:tracks][id] = obj.id + end + + restore('conference_cfp', CallForParticipation) do |_id, obj| + obj.conference_id = @conference_id + obj.save! + end + + restore('conference_ticket_server', TicketServer) do |_id, obj| + obj.conference_id = @conference_id + obj.save! + end + + restore_multiple('conference_rooms', Room) do |id, obj| + obj.conference_id = @conference_id + obj.save! + @mappings[:rooms][id] = obj.id + end + + restore_multiple('conference_days', Day) do |id, obj| + obj.conference_id = @conference_id + obj.save! + @mappings[:days][id] = obj.id + end + + restore_multiple('conference_languages', Language) do |_id, obj| + obj.attachable_id = @conference_id + obj.save! + end + end + + def restore_events_data + restore_multiple('tickets', Ticket) do |_id, obj| + obj.event_id = @mappings[:events][obj.event_id] + obj.save! + end + + restore_multiple('event_people', EventPerson) do |_id, obj| + obj.event_id = @mappings[:events][obj.event_id] + obj.person_id = @mappings[:people][obj.person_id] + obj.save! + end + + restore_multiple('event_feedbacks', EventFeedback) do |_id, obj| + obj.event_id = @mappings[:events][obj.event_id] + obj.save! + end + + restore_multiple('event_ratings', EventRating) do |_id, obj| + obj.event_id = @mappings[:events][obj.event_id] + obj.person_id = @mappings[:people][obj.person_id] + obj.save! if obj.valid? + end + + restore_multiple('event_links', Link) do |_id, obj| + obj.linkable_id = @mappings[:events][obj.linkable_id] + obj.save! + end + + restore_multiple('event_attachments', EventAttachment) do |id, obj| + obj.event_id = @mappings[:events][obj.event_id] + if (file = import_file('event_attachments/attachments', id, obj.attachment_file_name)) + obj.attachment = file + end + obj.save! + end + end + + def restore_people_data + restore_multiple('people_phone_numbers', PhoneNumber) do |_id, obj| + new_id = @mappings[:people][obj.person_id] + test = PhoneNumber.where(person_id: new_id, phone_number: obj.phone_number) + unless test + obj.person_id = new_id + obj.save! + end + end + + restore_multiple('people_im_accounts', ImAccount) do |_id, obj| + new_id = @mappings[:people][obj.person_id] + test = ImAccount.where(person_id: new_id, im_address: obj.im_address) + unless test + obj.person_id = new_id + obj.save! + end + end + + restore_multiple('people_links', Link) do |_id, obj| + new_id = @mappings[:people][obj.linkable_id] + test = Link.where(linkable_id: new_id, linkable_type: obj.linkable_type, + url: obj.url) + unless test + obj.linkable_id = new_id + obj.save! + end + end + + restore_multiple('people_languages', Language) do |_id, obj| + new_id = @mappings[:people][obj.attachable_id] + test = Language.where(attachable_id: new_id, attachable_type: obj.attachable_type, + code: obj.code) + unless test + obj.attachable_id = new_id + obj.save! + end + end + + restore_multiple('people_availabilities', Availability) do |_id, obj| + next if obj.nil? or obj.start_date.nil? or obj.end_date.nil? + obj.conference_id = @conference_id + obj.person_id = @mappings[:people][obj.person_id] + obj.day_id = @mappings[:days][obj.day_id] + obj.save! + end + end + + def dump_has_many(name, obj, attr) + arr = obj.collect { |t| t.send(attr) } + .flatten.select { |t| not t.nil? }.sort.uniq + dump name, arr + end + + def dump(name, obj) + return if obj.nil? + File.open(File.join(@export_dir, name) + '.yaml', 'w') { |f| + if obj.respond_to?('collect') + f.puts obj.collect(&:attributes).to_yaml + else + f.puts obj.attributes.to_yaml + end + } + obj + end + + def restore(name, obj) + puts "[ ] restore #{name}" if DEBUG + file = File.join(@export_dir, name) + '.yaml' + return unless File.readable? file + records = YAML.load_file(file) + tmp = obj.new(records) + tmp.id = nil + yield records['id'], tmp + end + + def restore_multiple(name, obj) + puts "[ ] restore all #{name}" if DEBUG + records = YAML.load_file(File.join(@export_dir, name) + '.yaml') + records.each do |record| + tmp = obj.new(record) + tmp.id = nil + yield record['id'], tmp + end + end + + def restore_users(name = 'users', obj = User) + puts "[ ] restore all #{name}" if DEBUG + records = YAML.load_file(File.join(@export_dir, name) + '.yaml') + records.each do |record| + tmp = obj.new(record) + tmp.id = nil + yield record['id'], record, tmp + end + end + + def export_paperclip_files(events, people, attachments) + out_path = File.join(@export_dir, 'attachments.tar.gz') + + paths = [] + paths << events.reject { |e| e.logo.path.nil? }.collect { |e| e.logo.path.gsub(/^#{Rails.root}\//, '') } + paths << people.reject { |e| e.avatar.path.nil? }.collect { |e| e.avatar.path.gsub(/^#{Rails.root}\//, '') } + paths << attachments.reject { |e| e.attachment.path.nil? }.collect { |e| e.attachment.path.gsub(/^#{Rails.root}\//, '') } + paths.flatten! + + # TODO don't use system + system('tar', *['-cpz', '-f', out_path, paths].flatten) + end + + def import_file(dir, id, file_name) + return unless file_name.present? + + # ':rails_root/public/system/:class/:attachment/:id_partition/:style/:filename' + id_partition = ('%09d'.freeze % id).scan(/\d{3}/).join('/'.freeze) + path = File.join(@export_dir, 'public/system', dir, id_partition, 'original', file_name) + return File.open(path, 'r') if File.readable?(path) + + nil + end + + def unpack_paperclip_files + path = File.join(@export_dir, 'attachments.tar.gz') + system('tar', *['-xz', '-f', path, '-C', @export_dir].flatten) + end + + def disable_callbacks + EventPerson.skip_callback(:save, :after, :update_speaker_count) + Event.skip_callback(:save, :after, :update_conflicts) + Availability.skip_callback(:save, :after, :update_event_conflicts) + EventRating.skip_callback(:save, :after, :update_average) + EventFeedback.skip_callback(:save, :after, :update_average) + end + + def update_counters + ActiveRecord::Base.connection.execute("UPDATE events SET speaker_count=(SELECT count(*) FROM event_people WHERE events.id=event_people.event_id AND event_people.event_role='speaker')") + update_event_average('event_ratings', 'average_rating') + update_event_average('event_feedbacks', 'average_feedback') + end + + def update_event_average(table, field) + ActiveRecord::Base.connection.execute "UPDATE events SET #{field}=( + SELECT sum(rating)/count(rating) + FROM #{table} WHERE events.id = #{table}.event_id)" + end +end diff --git a/app/lib/merge_persons.rb b/app/lib/merge_persons.rb new file mode 100644 index 0000000..900d86d --- /dev/null +++ b/app/lib/merge_persons.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true +class MergePersons + def initialize(keep_last_updated = true) + @keep_last_updated = keep_last_updated + end + + def combine!(keep, kill) + return merge_persons(kill, keep) if retain_second_person?(keep, kill) + merge_persons(keep, kill) + end + + private + + def retain_second_person?(keep, kill) + @keep_last_updated && kill.newer_than?(keep) + end + + def merge_persons(keep, kill) + # Merge or move user model + if keep.user.present? + keep.user = merge_users(keep.user, kill.user) if kill.user.present? + else + keep.user = kill.user + kill.user = nil + end + + # Get list of all conferences for which keep already has set the availabilities, then only + # import availabilities for the others + keep_cons = keep.availabilities.select(:conference_id).distinct + kill.availabilities.all do |avail| + next if keep_cons.include? avail.conference_id + avail.update_attributes(person_id: keep.id) + end + + # Merge ticket. Orphan ticket in kill, if both have one + if keep.ticket.nil? + keep.ticket = kill.ticket + kill.ticket = nil + end + + # Merge languages + kill.languages.all do |lang| + keep.languages << lang unless keep.languages.include? lang + end + + # Merge event_person, if the person does not already have the same role in the same event + kill.event_people.all do |event_person| + next if keep.event_people.find_by(eventid: event_person.event_id, role: event_person.role) + event_person.update_attributes(person_id: keep.id) + end + + # steal all members that need no special treatment + kill.event_ratings.all { |rating| rating.update_attributes(person_id: keep.id) } + kill.im_accounts.all { |im| im.update_attributes(person_id: keep.id) } + kill.links.all { |link| link.update_attributes(associated_id: keep.id) } + kill.phone_numbers.all { |phone| phone.update_attributes(person_id: keep.id) } + + # update conflicts on all associated events + keep.events.all(&:update_conflicts) + + # remove merged user and save the one to keep + kill.destroy + keep.save! + keep + end + + def merge_users(keep, kill) + keep, kill = kill, keep if @keep_last_updated && kill.newer_than?(keep) + + # merge conference users, if both were in the same conference, keep the one from keep + kill.conference_users.all.each do |u| + next if u.conference.nil? + collision = keep.conference_users.find_by conference_id: u.conference_id + if collision + if ConferenceUser::ROLES.index(u.role) > ConferenceUser::ROLES.index(collision.role) + collision.update_attributes role: u.role + end + u.destroy + else + u.update_attributes user_id: keep.id + end + end + + # get rid of older user and return the one we keep + kill.destroy + keep.save + keep + end +end diff --git a/app/lib/person_view_model.rb b/app/lib/person_view_model.rb new file mode 100644 index 0000000..5126c75 --- /dev/null +++ b/app/lib/person_view_model.rb @@ -0,0 +1,62 @@ +class PersonViewModel + def initialize(current_user, person, conference) + @current_user = current_user + @person = person + @conference = conference + end + + def redact_events? + @redact ||= !Pundit.policy(@current_user, @conference).manage? + end + + def current_events + return @current_events if @current_events + @current_events = @person.events_as_presenter_in(@conference) + @current_events.to_a.map!(&:clean_event_attributes!) if redact_events? + @current_events + end + + def other_events + return @other_events if @other_events + @other_events = @person.events_as_presenter_not_in(@conference) + @other_events.to_a.map!(&:clean_event_attributes!) if redact_events? + @other_events + end + + def availabilities + @availabilities ||= @person.availabilities.where("conference_id = #{@conference.id}") + end + + def expenses + @expenses = @person.expenses.where(conference_id: @conference.id) + end + + def expenses_sum_reimbursed + @expenses_sum_reimbursed ||= @person.sum_of_expenses(@conference, true) + end + + def expenses_sum_non_reimbursed + @expenses_sum_non_reimbursed ||= @person.sum_of_expenses(@conference, false) + end + + def transport_needs + return unless @conference + @transport_needs ||= @person.transport_needs.where(conference_id: @conference.id) + end + + def can_add_ticket? + @conference.ticket_server_enabled? && !@person.remote_ticket? + end + + def show_expenses? + @conference.expenses_enabled? && expenses.any? + end + + def show_transports? + @conference.transport_needs_enabled? && transport_needs.any? + end + + def remote_ticket_present? + @person&.ticket&.remote_ticket_id + end +end diff --git a/app/lib/possible_start_times.rb b/app/lib/possible_start_times.rb new file mode 100644 index 0000000..68f622b --- /dev/null +++ b/app/lib/possible_start_times.rb @@ -0,0 +1,54 @@ +class PossibleStartTimes + def initialize(event) + @event = event + @conference = event.conference + end + + def all + possible = {} + + # Retrieve a list of persons that are presenting this event, + # and filter out those who don't have any availabilities configured. + + @conference.days.each do |day| + availabilities = Availability.where(person: available_presenters, day: day) + + times = day.start_times_map do |time, pretty| + # People with no availability at all are not present in available_presenters. + # Hence, if the number of availability records for this day is less + # than the number of presenters in available_presenters, we know that at least + # one of them is not available. + if presenters_available_at(availabilities, time).all? + [pretty, time.to_s] + elsif @event.start_time == time + # Special case: if the event is already scheduled, offer that start time + # in the list as well, but add a warning, so that records are not accidentally + # modified through HTML forms. + + [pretty + ' (not all presenters available!)', time.to_s] + end + end + + times.compact! + possible[day.to_s] = times if times.any? + end + + possible + end + + private + + def presenters_available_at(availabilities, time) + if availabilities.length == available_presenters.length + availabilities.map { |a| a.within_range?(time) } + else + [false] + end + end + + def available_presenters + @available_presenters ||= @event.event_people.presenter.group(:person_id, :id) + .select { |ep| ep.person.availabilities.any? } + .map(&:person_id) + end +end diff --git a/app/lib/rake_logger.rb b/app/lib/rake_logger.rb new file mode 100644 index 0000000..b3d5314 --- /dev/null +++ b/app/lib/rake_logger.rb @@ -0,0 +1,13 @@ +module RakeLogger + def error(msg) + STDERR.puts(msg) + end + + def warning(msg) + STDERR.puts(msg) unless Rails.env.test? + end + + def log(msg) + STDOUT.puts(msg) unless ENV['QUIET'] + end +end diff --git a/app/lib/ri_cal_templates.rb b/app/lib/ri_cal_templates.rb new file mode 100644 index 0000000..52e6406 --- /dev/null +++ b/app/lib/ri_cal_templates.rb @@ -0,0 +1,4 @@ +module RiCalTemplates +end + +ActionView::Template.register_template_handler :ri_cal, RiCalTemplates::TemplateHandler diff --git a/app/lib/ri_cal_templates/template_handler.rb b/app/lib/ri_cal_templates/template_handler.rb new file mode 100644 index 0000000..6cca767 --- /dev/null +++ b/app/lib/ri_cal_templates/template_handler.rb @@ -0,0 +1,10 @@ +module RiCalTemplates + class TemplateHandler + def self.call(template) + require 'ri_cal' + "::RiCal.Calendar do |cal|\n" + + template.source + + "\n end.to_s" + end + end +end diff --git a/app/lib/schedule_view_model.rb b/app/lib/schedule_view_model.rb new file mode 100644 index 0000000..4c4d1dc --- /dev/null +++ b/app/lib/schedule_view_model.rb @@ -0,0 +1,86 @@ +class ScheduleViewModel + def initialize(conference) + @conference = conference + end + attr_reader :event, :speaker, :day + + def events + @events ||= @conference.schedule_events.sort_by(&:to_sortable) + end + + def events_by_track + @events_by_track ||= events.group_by(&:track_id) + end + + def events_by_day + @conference.days.each_with_object({}) { |day, h| + h[day] = @conference.schedule_events.scheduled_on(day).group_by(&:start_time) + } + end + + def concurrent_events + @concurrent_events ||= @conference.schedule_events.where(start_time: event.start_time) + end + + def for_event(id) + @event = @conference.schedule_events.find(id) + self + end + + def for_day(day) + @day = day + self + end + + def room_slices + @day.rooms_with_events.each_slice(7) do |s| + yield s + end + end + + def skip_rows + @skip_rows ||= @day.rooms_with_events.inject({}) { |h,k| h.merge(k => 0) } + end + + def events_by_room(room) + build_events_by_room unless @events_by_room + @events_by_room[room] + end + + def rooms + @selected_rooms || @day.rooms_with_events + end + + def select_rooms(selected) + @selected_rooms = @day.rooms_with_events & selected + end + + def event_now?(room, time) + events_by_room(room).first.start_time == time + end + + def room_slice_names + @day.rooms_with_events.each_slice(7).map do |s| + s.map(&:name) + end + end + + def speakers + @speakers ||= Person.publicly_speaking_at(@conference.include_subs).confirmed(@conference.include_subs).order(:public_name, :first_name, :last_name) + end + + def for_speaker(id) + @speaker = Person.publicly_speaking_at(@conference.include_subs).confirmed(@conference.include_subs).find(id) + self + end + + private + + def build_events_by_room + @events_by_room = {} + @day.rooms_with_events.each do |room| + @events_by_room[room] = room.events.confirmed.no_conflicts.is_public.scheduled_on(@day).order(:start_time).to_a + end + @events_by_room + end +end diff --git a/app/lib/static_schedule.rb b/app/lib/static_schedule.rb new file mode 100644 index 0000000..9d77ae2 --- /dev/null +++ b/app/lib/static_schedule.rb @@ -0,0 +1,2 @@ +module StaticSchedule +end diff --git a/app/lib/static_schedule/export.rb b/app/lib/static_schedule/export.rb new file mode 100644 index 0000000..6b4f09c --- /dev/null +++ b/app/lib/static_schedule/export.rb @@ -0,0 +1,214 @@ +module StaticSchedule + class Export + include RakeLogger + + EXPORT_PATH = Rails.root.join('tmp', 'static_export').to_s + STATIC_ASSET_PATHS = %w[app/assets/stylesheets/public_schedule.css app/assets/stylesheets/public_schedule_print.css].freeze + STATIC_ASSET_REGEX = %r{.*/(.*)-(?:[a-f0-9]+)\.(.{3})} + + # Export a static html version of the conference program. + # + # @param conference [Conference] conference to export + # @param locale [String] export conference in supported locale + # @param destination [String] export into this directory + def initialize(conference, locale = 'en', destination = EXPORT_PATH) + @conference = conference + @locale = locale + @renderer = ProgramRenderer.new(@conference, @locale) + @pages = Pages.new(@renderer, @conference) + @destination = destination || EXPORT_PATH + end + + # create a tarball from the conference export directory + def create_tarball + out_file = tarball_filename + File.unlink out_file if File.exist? out_file + system('tar', *['-cpz', '-f', out_file.to_s, '-C', @destination, @conference.acronym].flatten) + out_file.to_s + end + + # export the conference to disk + # + # only run by rake task, cannot run in the same thread as rails + def run_export + fail 'No conference found!' if @conference.nil? + + Time.zone = @conference&.timezone + + @asset_paths = [] + @base_directory = File.join(@destination, @conference.acronym) + @base_url = @renderer.base_url + @original_schedule_public = @conference.schedule_public + + ActiveRecord::Base.transaction do + unlock_schedule unless @original_schedule_public + + setup_directories + download_pages + copy_stripped_assets + copy_static_assets + + lock_schedule unless @original_schedule_public + end + end + + private + + def tarball_filename + File.join(@destination, "#{@conference.acronym}-#{@locale}.tar.gz") + end + + def setup_directories + FileUtils.rm_r(@base_directory, secure: true) if File.exist? @base_directory + FileUtils.mkdir_p(@base_directory) + end + + def download_pages + @pages.all.each do |p| + filename = p.delete(:target) + puts "Downloading #{filename}" unless Rails.env.test? + response = if p[:template] + @renderer.render_with_template(p) + else + @renderer.render(p) + end + save_response(response, filename) + end + end + + def copy_stripped_assets + @asset_paths.uniq.each do |asset_path| + original_path = Rails.root.join('public', URI.unescape(asset_path)) + if File.exist?(original_path) + new_path = File.join(@base_directory, URI.unescape(asset_path)) + FileUtils.mkdir_p(File.dirname(new_path)) + FileUtils.cp(original_path, new_path) + elsif Rails.env.production? + warning('?? We might be missing "%s"' % original_path) + end + end + end + + def copy_static_assets + STATIC_ASSET_PATHS.each do |path| + path = Rails.root.join(path) + fail 'update source code to include necessary assets' unless File.exist?(path) + new_path = File.join(@base_directory, File.basename(path)) + FileUtils.cp(path, new_path) + end + end + + def save_response(response, filename) + file_path = File.join(@base_directory, URI.decode(filename)) + FileUtils.mkdir_p(File.dirname(file_path)) + + if filename.match?(/\.html$/) + document = modify_response_html(response) + File.open(file_path, 'w') do |f| + # FIXME corrupts events and speakers? + # document.write_html_to(f, encoding: "UTF-8") + f.puts(document.to_html) + end + elsif filename.match?(/\.pdf$/) + File.open(file_path, 'wb') do |f| + f.write(response) + end + else + # CSS,... + File.open(file_path, 'w:utf-8') do |f| + f.write(response.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?')) + end + end + end + + def modify_response_html(body) + document = Nokogiri::HTML(body, nil, 'UTF-8') + + # + document.css('link').each do |link| + href_attr = link.attributes['href'] + if href_attr.value.index("/#{@conference.acronym}/public/schedule/style.css") + link.attributes['href'].value = @base_url + 'style.css' + elsif static_assets_link?(href_attr.value) + link.attributes['href'].value = @base_url + strip_asset_hash(href_attr.value) + elsif href_attr + strip_asset_path(link, 'href') + end + end + + #