diff --git a/.env.example b/.env.example index cff6d4fee..3f56d772c 100644 --- a/.env.example +++ b/.env.example @@ -12,7 +12,6 @@ # database setup: DATABASE_ADAPTER=postgres DATABASE_USER=postgres -DATABASE_PASSWORD=postgres DATABASE_HOST=localhost # host part of the url for mail links: diff --git a/.travis.yml b/.travis.yml index cdb33fd2f..e0e22e1d0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,4 @@ language: ruby -sudo: false cache: bundler bundler_args: '--without production development' rvm: @@ -11,3 +10,5 @@ before_script: - sleep 10 services: - elasticsearch +addons: + chrome: stable diff --git a/Gemfile b/Gemfile index 88aec66df..3a36ae4cb 100644 --- a/Gemfile +++ b/Gemfile @@ -23,6 +23,11 @@ gem 'prawn-table', '~> 0.2.2' gem 'elasticsearch-model' gem 'elasticsearch-rails' gem 'skylight' +gem 'sidekiq', '5.1.3' +gem 'sidekiq-cron', '0.6.3' +# TODO: remove this once the following issue has been addressed +# https://github.com/ondrejbartas/sidekiq-cron/issues/199 +gem 'rufus-scheduler', '~> 3.4.2' # Assets gem 'jquery-rails', '>= 4.2.0' @@ -49,17 +54,17 @@ end group :development, :test do gem "rspec-rails", '~> 3.7.2' - gem "capybara", '~> 2.4.4' gem "byebug" end group :test do - # Do not upgrade until - # https://github.com/DatabaseCleaner/database_cleaner/issues/317 is fixed - gem "database_cleaner", '1.3.0' - gem 'shoulda', ">= 3.5" + gem "database_cleaner", '1.6.2' + gem 'shoulda-matchers', '~> 3.1.2' gem 'fabrication' gem 'faker' + gem 'capybara', '~> 2.7' + gem 'capybara-selenium', '~> 0.0.6' + gem 'chromedriver-helper', '~> 1.0' end group :production do diff --git a/Gemfile.lock b/Gemfile.lock index fb5b77c75..6822349c2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -53,12 +53,14 @@ GEM sshkit (>= 1.6.1, != 1.7.0) arbre (1.1.1) activesupport (>= 3.0.0) + archive-zip (0.11.0) + io-like (~> 0.3.0) arel (6.0.4) ast (2.4.0) autoprefixer-rails (6.3.1) execjs json - bcrypt (3.1.11) + bcrypt (3.1.12) better_errors (2.4.0) coderay (>= 1.0.0) erubi (>= 1.0.0) @@ -84,12 +86,21 @@ GEM capistrano-rbenv (2.1.3) capistrano (~> 3.1) sshkit (~> 1.3) - capybara (2.4.4) - mime-types (>= 1.16) + capybara (2.18.0) + addressable + mini_mime (>= 0.1.3) nokogiri (>= 1.3.3) rack (>= 1.0.0) rack-test (>= 0.5.4) - xpath (~> 2.0) + xpath (>= 2.0, < 4.0) + capybara-selenium (0.0.6) + capybara + selenium-webdriver + childprocess (0.9.0) + ffi (~> 1.0, >= 1.0.11) + chromedriver-helper (1.2.0) + archive-zip (~> 0.10) + nokogiri (~> 1.8) chronic (0.10.2) coderay (1.1.2) coffee-rails (4.1.0) @@ -101,14 +112,15 @@ GEM coffee-script-source (1.8.0) columnize (0.9.0) concurrent-ruby (1.0.5) - crass (1.0.3) + connection_pool (2.2.1) + crass (1.0.4) dalli (2.7.2) - database_cleaner (1.3.0) + database_cleaner (1.6.2) debug_inspector (0.0.3) - devise (4.4.1) + devise (4.4.3) bcrypt (~> 3.0) orm_adapter (~> 0.1) - railties (>= 4.1.0, < 5.2) + railties (>= 4.1.0, < 6.0) responders warden (~> 1.2.3) diff-lcs (1.3) @@ -132,12 +144,15 @@ GEM multi_json erubi (1.7.1) erubis (2.7.0) + et-orbi (1.1.2) + tzinfo execjs (2.6.0) fabrication (2.11.3) faker (1.4.3) i18n (~> 0.5) faraday (0.9.1) multipart-post (>= 1.2, < 3) + ffi (1.9.23) formtastic (3.1.5) actionpack (>= 3.2.13) formtastic_i18n (0.6.0) @@ -160,6 +175,7 @@ GEM has_scope (~> 0.6) railties (>= 4.2, <= 5.2) responders + io-like (0.3.0) jquery-rails (4.3.1) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) @@ -225,7 +241,9 @@ GEM public_suffix (2.0.5) pundit (0.3.0) activesupport (>= 3.0.0) - rack (1.6.9) + rack (1.6.10) + rack-protection (2.0.1) + rack rack-test (0.6.3) rack (>= 1.0) rails (4.2.10) @@ -262,7 +280,7 @@ GEM thor (>= 0.18.1, < 2.0) rainbow (3.0.0) raindrops (0.16.0) - rake (12.3.0) + rake (12.3.1) ransack (1.8.6) actionpack (>= 3.0) activerecord (>= 3.0) @@ -270,8 +288,10 @@ GEM i18n polyamorous (~> 1.3.2) rdiscount (2.1.7.1) - responders (2.0.2) - railties (>= 4.2.0.alpha, < 5) + redis (4.0.1) + responders (2.4.0) + actionpack (>= 4.2.0, < 5.3) + railties (>= 4.2.0, < 5.3) rest-client (2.0.1) http-cookie (>= 1.0.2, < 2.0) mime-types (>= 1.16, < 4.0) @@ -303,6 +323,9 @@ GEM ruby-progressbar (~> 1.7) unicode-display_width (~> 1.0, >= 1.0.1) ruby-progressbar (1.9.0) + rubyzip (1.2.1) + rufus-scheduler (3.4.2) + et-orbi (~> 1.0) sass (3.4.21) sass-rails (5.0.7) railties (>= 4.0.0, < 6) @@ -312,18 +335,27 @@ GEM tilt (>= 1.1, < 3) select2-rails (4.0.1) thor (~> 0.14) - shoulda (3.5.0) - shoulda-context (~> 1.0, >= 1.0.1) - shoulda-matchers (>= 1.4.1, < 3.0) - shoulda-context (1.2.1) - shoulda-matchers (2.8.0) - activesupport (>= 3.0.0) + selenium-webdriver (3.11.0) + childprocess (~> 0.5) + rubyzip (~> 1.2) + shoulda-matchers (3.1.2) + activesupport (>= 4.0.0) + sidekiq (5.1.3) + concurrent-ruby (~> 1.0) + connection_pool (~> 2.2, >= 2.2.0) + rack-protection (>= 1.5.0) + redis (>= 3.3.5, < 5) + sidekiq-cron (0.6.3) + rufus-scheduler (>= 3.3.0) + sidekiq (>= 4.2.1) simple_form (3.1.0) actionpack (~> 4.0) activemodel (~> 4.0) - skylight (1.5.0) - activesupport (>= 3.0.0) - sprockets (3.7.1) + skylight (2.0.1) + skylight-core (= 2.0.1) + skylight-core (2.0.1) + activesupport (>= 4.2.0) + sprockets (3.7.2) concurrent-ruby (~> 1.0) rack (> 1, < 3) sprockets-rails (3.2.1) @@ -333,7 +365,7 @@ GEM sshkit (1.8.1) net-scp (>= 1.1.2) net-ssh (>= 2.8.0) - thor (0.19.4) + thor (0.20.0) thread_safe (0.3.6) tilt (2.0.8) ttfunk (1.5.1) @@ -359,8 +391,8 @@ GEM sprockets-rails (>= 2.0, < 4.0) whenever (0.9.4) chronic (>= 0.6.3) - xpath (2.0.0) - nokogiri (~> 1.3) + xpath (3.0.0) + nokogiri (~> 1.8) PLATFORMS ruby @@ -375,10 +407,12 @@ DEPENDENCIES capistrano (~> 3.1) capistrano-rails (~> 1.1) capistrano-rbenv (~> 2.1) - capybara (~> 2.4.4) + capybara (~> 2.7) + capybara-selenium (~> 0.0.6) + chromedriver-helper (~> 1.0) coffee-rails dalli - database_cleaner (= 1.3.0) + database_cleaner (= 1.6.2) devise (~> 4.4.1) dotenv-rails (= 1.0.2) elasticsearch-model @@ -404,9 +438,12 @@ DEPENDENCIES rollbar (= 2.8.3) rspec-rails (~> 3.7.2) rubocop (~> 0.52.1) + rufus-scheduler (~> 3.4.2) sass-rails (~> 5.0.7) select2-rails - shoulda (>= 3.5) + shoulda-matchers (~> 3.1.2) + sidekiq (= 5.1.3) + sidekiq-cron (= 0.6.3) simple_form (>= 3.0.0) skylight uglifier (= 2.7.2) @@ -418,4 +455,4 @@ RUBY VERSION ruby 2.3.0p0 BUNDLED WITH - 1.16.1 + 1.16.2 diff --git a/README.md b/README.md index 8456cc4fb..564c5ef6d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# TimeOverflow [![Build Status](https://travis-ci.org/coopdevs/timeoverflow.svg)](https://travis-ci.org/coopdevs/timeoverflow) [![Code Climate](https://codeclimate.com/github/timeoverflow/timeoverflow/badges/gpa.svg)](https://codeclimate.com/github/timeoverflow/timeoverflow) +# TimeOverflow [![View performance data on Skylight](https://badges.skylight.io/problem/grDTNuzZRnyu.svg)](https://oss.skylight.io/app/applications/grDTNuzZRnyu) [![Build Status](https://travis-ci.org/coopdevs/timeoverflow.svg)](https://travis-ci.org/coopdevs/timeoverflow) [![Code Climate](https://codeclimate.com/github/timeoverflow/timeoverflow/badges/gpa.svg)](https://codeclimate.com/github/timeoverflow/timeoverflow) #### www.timeoverflow.org :globe_with_meridians: Read this [in English](docs/README.en.md) diff --git a/app/admin/user.rb b/app/admin/user.rb index 09d337b42..203743025 100644 --- a/app/admin/user.rb +++ b/app/admin/user.rb @@ -43,7 +43,7 @@ end f.inputs "Members" do f.has_many :members do |m| - m.input :organization + m.input :organization, collection: Organization.order(id: :asc).pluck(:name, :id) m.input :manager end end diff --git a/app/assets/images/ajuntament_bcn_activa.png b/app/assets/images/ajuntament_bcn_activa.png new file mode 100644 index 000000000..f89a56374 Binary files /dev/null and b/app/assets/images/ajuntament_bcn_activa.png differ diff --git a/app/assets/images/home_back.jpg b/app/assets/images/home_back.jpg index e3026ee45..249efd559 100644 Binary files a/app/assets/images/home_back.jpg and b/app/assets/images/home_back.jpg differ diff --git a/app/assets/images/home_back_mobile.jpg b/app/assets/images/home_back_mobile.jpg new file mode 100644 index 000000000..d12b5b737 Binary files /dev/null and b/app/assets/images/home_back_mobile.jpg differ diff --git a/app/assets/images/home_back_overlay.jpg b/app/assets/images/home_back_overlay.jpg deleted file mode 100644 index 3a9ac6090..000000000 Binary files a/app/assets/images/home_back_overlay.jpg and /dev/null differ diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index d37fff8f0..7bdc261c1 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -2,6 +2,7 @@ #= require datepicker #= require give_time #= require tags +#= require mobile_app_libs $(document).on 'click', 'a[data-popup]', (event) -> window.open($(this).attr('href'), 'popup', 'width=600,height=600') diff --git a/app/assets/javascripts/mobile_app_libs.js b/app/assets/javascripts/mobile_app_libs.js new file mode 100644 index 000000000..99fa2e04b --- /dev/null +++ b/app/assets/javascripts/mobile_app_libs.js @@ -0,0 +1,5 @@ +// Used by the mobile app to register the device token +// https://github.com/coopdevs/timeoverflow-mobile-app +window.TimeOverflowRegisterExpoDeviceToken = function (token) { + $.post('/device_tokens', { token: token }); +} diff --git a/app/assets/stylesheets/_bootstrap-custom.scss b/app/assets/stylesheets/_bootstrap-custom.scss new file mode 100644 index 000000000..4d9e6df45 --- /dev/null +++ b/app/assets/stylesheets/_bootstrap-custom.scss @@ -0,0 +1,60 @@ +/*! + * Bootstrap v3.3.7 (http://getbootstrap.com) + * Copyright 2011-2016 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * + * This file lists all the available Bootstrap modules, uncomment + * the modules you want to import + */ + +// Core variables and mixins +@import "bootstrap-overrides"; +@import "bootstrap/variables"; +@import "bootstrap/mixins"; + +// Reset and dependencies +@import "bootstrap/normalize"; +@import "bootstrap/print"; +@import "bootstrap/glyphicons"; + +// Core CSS +@import "bootstrap/scaffolding"; +@import "bootstrap/type"; +@import "bootstrap/code"; +@import "bootstrap/grid"; +@import "bootstrap/tables"; +@import "bootstrap/forms"; +@import "bootstrap/buttons"; + +// Components +@import "bootstrap/component-animations"; +@import "bootstrap/dropdowns"; +@import "bootstrap/button-groups"; +@import "bootstrap/input-groups"; +@import "bootstrap/navs"; +@import "bootstrap/navbar"; +// @import "bootstrap/breadcrumbs"; +@import "bootstrap/pagination"; +@import "bootstrap/pager"; +@import "bootstrap/labels"; +@import "bootstrap/badges"; +// @import "bootstrap/jumbotron"; +// @import "bootstrap/thumbnails"; +@import "bootstrap/alerts"; +// @import "bootstrap/progress-bars"; +@import "bootstrap/media"; +@import "bootstrap/list-group"; +@import "bootstrap/panels"; +@import "bootstrap/responsive-embed"; +// @import "bootstrap/wells"; +@import "bootstrap/close"; + +// Components w/ JavaScript +@import "bootstrap/modals"; +@import "bootstrap/tooltip"; +// @import "bootstrap/popovers"; +// @import "bootstrap/carousel"; + +// Utility classes +@import "bootstrap/utilities"; +@import "bootstrap/responsive-utilities"; diff --git a/app/assets/stylesheets/_bootstrap-overrides.scss b/app/assets/stylesheets/_bootstrap-overrides.scss new file mode 100644 index 000000000..209c59064 --- /dev/null +++ b/app/assets/stylesheets/_bootstrap-overrides.scss @@ -0,0 +1,13 @@ +$brand-primary: $palette-turkey; + +$navbar-inverse-brand-hover-color: $palette-light-blue; +$navbar-inverse-brand-color: $white; + +$navbar-inverse-link-color: $white; +$navbar-inverse-link-active-color: $palette-light-blue; +$navbar-inverse-link-active-bg: $palette-turkey; +$navbar-inverse-link-hover-color: $palette-light-blue; +$navbar-inverse-toggle-border-color: $white; +$navbar-inverse-toggle-hover-bg: $palette-dark-turkey; + +$navbar-inverse-bg: $palette-turkey; diff --git a/app/assets/stylesheets/_footer.scss b/app/assets/stylesheets/_footer.scss new file mode 100644 index 000000000..dfc4a67af --- /dev/null +++ b/app/assets/stylesheets/_footer.scss @@ -0,0 +1,87 @@ +.footer { + padding: 8px 0; + line-height: 34px; + width: 100%; + background: rgba($palette-dark-turkey ,0.8); + + @media(max-width: $screen-sm-min) { + padding: 0; + } + + a { + margin: 0 1rem; + color: white; + font-weight: 500; + font-size: 16px; + + @media(max-width: $screen-sm-min) { + text-align: center; + font-size: 14px; + } + } +} + +.footer-left-col { + text-align: left; + + @media(max-width: $screen-sm-min) { + text-align: center; + } +} + +.footer-center-col { + text-align: center; +} + +.footer-right-col { + text-align: right; + + @media(max-width: $screen-sm-min) { + text-align: center; + } +} + +.landing-page, .login-page, .pages, .unlocks-page, .confirmations-page, .passwords-page { + .footer { + position: absolute; + bottom: 0; + left: 0; + } +} + +.language-selector { + & { + display: inline-block; + + @media(max-width: $screen-sm-min) { + display: block; + position: relative; + } + } + + a.btn-default.dropdown-toggle { + & { + background-color: transparent; + border: 1px solid white; + padding: 4px 8px; + } + + &:hover { + background-color: transparent; + } + } + + &.open > a.btn-default.dropdown-toggle { + & { + background-color: transparent; + color: white; + border-color: #ddd; + } + + &:hover { + background-color: transparent; + color: white; + border-color: #ddd; + } + } +} diff --git a/app/assets/stylesheets/_to-categories-dropdown.scss b/app/assets/stylesheets/_to-categories-dropdown.scss new file mode 100644 index 000000000..648d224e9 --- /dev/null +++ b/app/assets/stylesheets/_to-categories-dropdown.scss @@ -0,0 +1,5 @@ +.to-categories-dropdown { + @media(max-width: $screen-sm-min) { + display: none; + } +} diff --git a/app/assets/stylesheets/_variables.scss b/app/assets/stylesheets/_variables.scss new file mode 100644 index 000000000..3a06a6086 --- /dev/null +++ b/app/assets/stylesheets/_variables.scss @@ -0,0 +1,29 @@ +// New palette +$palette-turkey: #2797af; +$palette-dark-turkey: #165e6d; +$palette-light-blue: #d9edf7; +$palette-grey: #aaa; +$palette-black: #333; + +// Old colors +$bg-color: #f5f8fa; +$white: #ffffff; +$border: #e1e8ed; +$menu-disabled: #ddd; +$black: #000; +$form-input-glyph: #555; +$form-input-bg-color: #f5f5f5; +$form-input-button: #2a292a; +$form-input-button-hover: #3a393a; +$form-a-color: gray; +$form-a-hover-color: $palette-black; +$form-login-gray-text: #9b9b9b; +$btn-landing: rgba(255, 255, 255, 0.7); +$btn-landing-hover: rgba(255, 255, 255, 0.9); +$pages-title: #4a4a4a; +$pages-text: #4a4a4a; +$pages-anchor: #4a4a4a; +$pages-anchor-hover: #8a8a8a; +$features-background: #f5f5f5; +$features-separator: #d8d8d8; + diff --git a/app/assets/stylesheets/application.css.scss b/app/assets/stylesheets/application.css.scss index bead08185..b1455c6bd 100644 --- a/app/assets/stylesheets/application.css.scss +++ b/app/assets/stylesheets/application.css.scss @@ -2,64 +2,45 @@ *= require_self */ +@import "variables"; @import "bootstrap-sprockets"; -@import "bootstrap"; - -$bg-color: #f5f8fa; -$navs-bg-color: #1e1e1e; -$white: #ffffff; -$border: #e1e8ed; -$navs-hover-color: #aaa; -$menu-drop: #333; -$menu-drop-hover: #333; -$menu-disabled: #ddd; -$black: #000; -$form-input-glyph: #555; -$form-input-bg-color: #f5f5f5; -$form-input-button: #2a292a; -$form-input-button-hover: #3a393a; -$form-a-color: gray; -$form-a-hover-color: #333; -$form-login-gray-text: #9b9b9b; -$btn-landing: rgba(255, 255, 255, 0.7); -$btn-landing-hover: rgba(255, 255, 255, 0.9); -$pages-title: #4a4a4a; -$pages-text: #4a4a4a; -$pages-anchor: #4a4a4a; -$pages-anchor-hover: #8a8a8a; -$features-background: #f5f5f5; -$features-separator: #d8d8d8; +@import "bootstrap-custom"; +@import "to-categories-dropdown"; +@import "footer"; html { - font-size:62.5%; + font-size: 62.5%; } -.content -{ - min-height:500px; - margin-bottom: 40px; +.content { + margin-bottom: 76px; } -.language-selector{ - margin: 1.2rem 3rem 1.2rem 1rem; -} - -.language-selector a span { - display: block; - float: left; -} +.actions-menu { + margin-bottom: 16px; + padding: 0 15px; -.actions-menu > .dropdown > .language b { - display: inline-block; - margin-left: 0.7rem; + @media(max-width: $screen-sm-min) { + padding: 0; + } } .actions-menu > li > a { color: #101010; + padding: 10px 16px; + + @media(max-width: $screen-sm-min) { + padding: 8px; + font-size: 15px; + } } -.actions-menu > li{ - margin-right:5px; +.actions-menu > li { + margin-right: 6px; + + @media(max-width: $screen-sm-min) { + margin-right: 2px; + } } #login-box { @@ -69,7 +50,7 @@ html { border: 0; border-radius: 0.3rem; margin: 0; - padding: 1rem 0 1.4rem 0; + padding: 0; } form .material-icons { @@ -90,14 +71,6 @@ html { transition: background-color 100000s ease-in-out 0s; } - span.input-group-addon { - padding: 2rem 2rem 2rem 3rem; - - @media (max-width: $screen-xs-min) { - display: none; - } - } - span.show-password { border-radius: 0 0.3rem 0.3rem 0; cursor: pointer; @@ -124,10 +97,6 @@ html { text-align: center; } - .panel-body { - padding: 0 3rem 0 3rem; - } - .panel-footer { padding: 2rem 3rem; @@ -143,10 +112,15 @@ html { .input-group-addon { border-radius: 0.3rem 0 0 0.3rem; + + @media(max-width: $screen-sm-min) { + padding: 10px; + } } .input-lg, #user_email { border-radius: 0 0.3rem 0.3rem 0; + padding: 0 4px; } #user_password { @@ -160,13 +134,20 @@ html { color: $form-input-glyph; font-size: 2.24rem; letter-spacing: -0.0040rem; - padding: 2rem; + + @media(max-width: $screen-sm-min) { + font-size: 1.8rem; + } } .form-control { border: 0; font-weight: 400; height: 7.5rem; + + @media(max-width: $screen-sm-min) { + height: 6rem; + } } .form-control:focus { @@ -198,7 +179,7 @@ html { background-color: $white; border: 1px solid $border; border-radius: 5px; - margin: 10px; + margin: 14px 0; padding: 5px 20px; h4 { @@ -216,6 +197,12 @@ html { margin-right: 10px; } + &__datetime { + color: $palette-grey; + padding: 4px 0 0; + font-size: 10px; + text-align: right; + } } .break-word { @@ -231,36 +218,6 @@ html { padding: 20px; } -.footer { - bottom: 0; - left: 0; - line-height: 40px; - padding: 0; - position: absolute; - vertical-align: middle; - width: 100%; - - p { - font-size: 1.6rem; - letter-spacing: 0.013rem; - margin: 0; - text-align: center; - } - - .row { - margin: 0; - } - - a { - color: $pages-anchor; - font-weight: 500; - margin-left: 1.5rem; - } - - a:hover { - color: $pages-anchor-hover; - } -} .row.exports { padding: 10px; @@ -285,7 +242,6 @@ table.users { background-color: $bg-color; font-family: 'Work Sans', "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: medium; - margin: 0 0 20px; } form { @@ -351,38 +307,11 @@ ul.statistics li{ } /*flash*/ -.alert > p, .alert > ul { +.alert > ul { + list-style: none; padding-left: 1.5rem; } -.alert-error { - background-color: #f2dede; - border-color: #eed3d7; - color: #b94a48; - text-align: left; - } - -.alert-alert { - background-color: #f2dede; - border-color: #eed3d7; - color: #b94a48; - text-align: left; - } - -.alert-success { - background-color: #dff0d8; - border-color: #d6e9c6; - color: #468847; - text-align: left; - } - -.alert-notice { - background-color: #dff0d8; - border-color: #d6e9c6; - color: #468847; - text-align: left; -} - // if not navbar hidden datepicker in small windows .ui-datepicker{ z-index: 1000 !important; @@ -409,13 +338,13 @@ label[required]::after{ } .navbar-form { - padding: 0; + border: 0; background-color: transparent; -} + padding: 0; -form .checkbox input[type="checkbox"] { - margin-left: 5px; - position: relative; + @media(max-width: $screen-sm-min) { + padding: 10px 15px; + } } .back-overlay { @@ -429,7 +358,11 @@ form .checkbox input[type="checkbox"] { z-index: -1; } -.navbar-block{ +.navbar .container-fluid { + padding: 0; +} + +.navbar-block { margin-top: 0.2rem; } @@ -442,123 +375,53 @@ form .checkbox input[type="checkbox"] { } .navbar-static-top { - background-color: $navs-bg-color; + background-color: rgba($palette-turkey, 0.9); } .navbar-inverse { border: 0; } -.navbar-inverse .navbar-brand { - color: $white; -} - -.navbar-inverse .navbar-nav > li > a { - color: $white; -} - -.navbar-inverse .navbar-brand a:hover, .navbar-inverse .navbar-nav > li > a:hover, .navbar-inverse .navbar-brand:hover, .navbar-inverse .navbar-brand:focus { - color: $navs-hover-color; -} - .navbar-brand { font-size: 2.4rem; font-weight: 600; letter-spacing: 0.019rem; } -.language-selector{ - margin: 0.5rem 2rem 0 2rem; - width: 13.5rem; -} - -.dropdown{ - &.dropdown-language-selector{ - .dropdown-toggle{ - background: none; - border: 0.2rem solid $white; - border-radius: 0.3rem; - color: $white; - font-size: 1.6rem; - font-weight: 500; - min-width: 13.5rem; - text-align: left; - - .caret{ - display: block; - float: right; - margin-top: 1.1rem; - } - &:focus, - &:active{ - box-shadow: none; - outline: none; - } - } - &.open>.btn-default.dropdown-toggle{ - background: none; - border-color: $white; - border-bottom: 0; - border-bottom-right-radius: 0; - border-bottom-left-radius: 0; - color: $white; - - .caret{ - -ms-transform: rotate(180deg); - -webkit-transform: rotate(180deg); - transform: rotate(180deg); - } - } - .dropdown-menu{ - background: $navs-bg-color; - border: 0.2rem solid $white; - border-top: 0; - border-top-left-radius: 0; - border-top-right-radius: 0; - box-shadow: none; - margin-top: 0; - min-width: 13.5rem; - li{ - a{ - color: $white; - font-size: 1.6rem; - font-weight: 500; - padding: 0.6rem 1.2rem; - &:hover{ - background: $menu-drop-hover; - opacity: 0.8; - } - &:focus, - &:active{ - background: none; - box-shadow: none; - outline: none; - } - } - } - } - } -} - .landing-page, .login-page, .pages, .unlocks-page, .confirmations-page, .passwords-page { background: image-url('home_back.jpg') no-repeat center center fixed; background-size: cover; + @media(max-width: $screen-sm-min) { + background: image-url('home_back_mobile.jpg') no-repeat center center fixed; + background-size: cover; + } + .back-overlay { display: block; filter: alpha(opacity = 60); /* For IE8 and earlier */ - opacity: 0.6; + opacity: 0.2; } - .vertical-align { - left: 50%; + + .login-wrapper { position: absolute; - top: 47%; - transform: translate(-50%, -50%); - width: 75%; + top: 20%; + left: 0; + width: 100%; + z-index: 1; + + @media(max-width: $screen-sm-min) { + top: 60px; + } } - .container-fluid { - padding-bottom: 0.8rem; - padding-top: 0.8rem; + + .home-wrapper { + position: absolute; + top: 20%; + left: 0; + padding: 26px; + width: 100%; + z-index: 1; } .container-inner { @@ -578,10 +441,15 @@ form .checkbox input[type="checkbox"] { .landing-page { background: image-url('home_back.jpg') no-repeat center center fixed; - background-color: $navs-bg-color; + background-color: $palette-turkey; background-size: cover; text-align: center; + @media(max-width: $screen-sm-min) { + background: image-url('home_back_mobile.jpg') no-repeat center center fixed; + background-size: cover; + } + .back-overlay { filter: alpha(opacity = 15); /* For IE8 and earlier */ opacity: 0.15; @@ -595,6 +463,7 @@ form .checkbox input[type="checkbox"] { font-weight: 700; letter-spacing: -0.073rem; margin: 0; + text-shadow: 1px 1px $palette-black; } h3 { @@ -603,6 +472,7 @@ form .checkbox input[type="checkbox"] { font-weight: 500; letter-spacing: -0.107rem; margin-top: 1rem; + text-shadow: 1px 1px $palette-black; } .btn { @@ -625,18 +495,6 @@ form .checkbox input[type="checkbox"] { } .login-page { - h1 { - font-size: 3.84rem; - font-weight: 600; - letter-spacing: 0.031rem; - margin-bottom: 2rem; - margin-top: 3rem; - - @media (max-width: $screen-xs-min) { - display: none; - } - } - .material-icons { font-size: 3rem; } @@ -682,10 +540,6 @@ form .checkbox input[type="checkbox"] { padding: 0; } - #login-box .panel-body { - padding: 3rem 3rem 2rem 3rem; - } - .panel-body h2 { font-size: 1.9rem; font-weight: 500; @@ -786,12 +640,17 @@ form .checkbox input[type="checkbox"] { } .banner { - background: image-url('home_back_overlay.jpg') no-repeat center center; + background: image-url('home_back.jpg') no-repeat center center; background-size: cover; margin: 8rem 0 3rem 0; padding: 8rem 1rem; text-align: center; + @media(max-width: $screen-sm-min) { + background: image-url('home_back_mobile.jpg') no-repeat center center fixed; + background-size: cover; + } + h2 { font-size: 2.88em; font-weight: 600; @@ -831,78 +690,14 @@ form .checkbox input[type="checkbox"] { } } -.footer-left-col, .footer-left-col p { - text-align: left; -} - -.footer-center, .footer-center-col p { - text-align: center; -} - -.footer-right-col, .footer-right-col p { - text-align: right; -} - @media(max-width:767px){ html{ font-size:55.5%; } - .footer-left-col, .footer-left-col p { + .navbar-nav > li { text-align: center; - } - - .footer-center-col, .footer-center-col p { - text-align: center; - } - - .footer-right-col, .footer-right-col p { - text-align: center; - } - - .navbar-nav > li > a{ - text-align: center; - } - - .footer { - margin-top: 4rem; - position: static; - } - - .landing-page, .login-page, .pages, .unlocks-page, .confirmations-page, .passwords-page { - .language-selector{ - margin: 0 auto; - width: 13.5rem; - } - - .footer { - bottom: 1rem; - margin-top: 0; - } - - .footer a{ - margin-left: 0rem; - } - - .footer-right-col a{ - margin: 0 1rem; - } - } - - .landing-page, .unlocks-page, .confirmations-page, .passwords-page { - .footer { - position: absolute; - } - } - - .login-page { - .vertical-align{ - height: auto; - position: static; - top: 0; - transform: none; - width: 100%; - } + color: $palette-dark-turkey; } } @@ -916,4 +711,4 @@ form .checkbox input[type="checkbox"] { padding: 1em; line-height: 1.6em; } -} \ No newline at end of file +} diff --git a/app/controllers/device_tokens_controller.rb b/app/controllers/device_tokens_controller.rb new file mode 100644 index 000000000..681105c30 --- /dev/null +++ b/app/controllers/device_tokens_controller.rb @@ -0,0 +1,19 @@ +class DeviceTokensController < ApplicationController + before_filter :authenticate_user! + + def create + @device_token = DeviceToken.new device_token_params.merge! user_id: current_user.id + + if @device_token.save + render nothing: true, status: :created + else + render nothing: true, status: :unprocessable_entity + end + end + + private + + def device_token_params + params.permit(:token) + end +end diff --git a/app/controllers/organizations_controller.rb b/app/controllers/organizations_controller.rb index 1d84b947d..3bd9abf9b 100644 --- a/app/controllers/organizations_controller.rb +++ b/app/controllers/organizations_controller.rb @@ -1,20 +1,12 @@ class OrganizationsController < ApplicationController - before_filter :load_resource - - def load_resource - if params[:id] - @organization = Organization.find(params[:id]) - else - @organizations = Organization.all - end - end + before_filter :load_resource, only: [:show, :edit, :update, :destroy, :set_current] def new @organization = Organization.new end def index - @organizations = @organizations.matching(params[:q]) if params[:q].present? + @organizations = Organization.all end def show @@ -27,7 +19,8 @@ def show end def create - @organization = @organizations.build organization_params + @organization = Organization.new(organization_params) + if @organization.save redirect_to @organization, status: :created else @@ -48,6 +41,8 @@ def destroy redirect_to organizations_path, notice: "deleted" end + # POST /organizations/:organization_id/set_current + # def set_current if current_user session[:current_organization_id] = @organization.id @@ -57,6 +52,10 @@ def set_current private + def load_resource + @organization = Organization.find(params[:id]) + end + def organization_params params[:organization].permit(*%w[name theme email phone web public_opening_times description address diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 08c47942c..220d68715 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -41,7 +41,10 @@ def new def create post = model.new(post_params) post.organization = current_organization - if post.save + + persister = ::Persister::PostPersister.new(post) + + if persister.save redirect_to send("#{resource}_path", post) else instance_variable_set("@#{resource}", post) @@ -68,7 +71,10 @@ def update post = current_organization.posts.find params[:id] authorize post instance_variable_set("@#{resource}", post) - if post.update_attributes(post_params) + + persister = ::Persister::PostPersister.new(post) + + if persister.update_attributes(post_params) redirect_to post else render action: :edit, status: :unprocessable_entity diff --git a/app/controllers/transfers_controller.rb b/app/controllers/transfers_controller.rb index 29704e42b..b322f59ab 100644 --- a/app/controllers/transfers_controller.rb +++ b/app/controllers/transfers_controller.rb @@ -2,14 +2,18 @@ class TransfersController < ApplicationController def create @source = find_source @account = Account.find(transfer_params[:destination]) + transfer = Transfer.new( transfer_params.merge(source: @source, destination: @account) ) - transfer.save! - redirect_to redirect_target - rescue ActiveRecord::RecordInvalid - flash[:error] = transfer.errors.full_messages.to_sentence + persister = ::Persister::TransferPersister.new(transfer) + + if persister.save + redirect_to redirect_target + else + flash[:error] = transfer.errors.full_messages.to_sentence + end end def new diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index b56fba5c2..9f1dd87a6 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -2,22 +2,13 @@ class UsersController < ApplicationController before_filter :authenticate_user! def index - @search = User.ransack(params[:q]) - @search.sorts = 'members_member_uid asc' if @search.sorts.empty? - - @users = @search - .result(distinct: false) - .joins(members: :account) - .eager_load(members: :account) - .where(members: { organization: current_organization.id }) - .page(params[:page]) - .per(25) - - @memberships = current_organization.members. - where(user_id: @users.map(&:id)). - includes(:account).each_with_object({}) do |mem, ob| - ob[mem.user_id] = mem - end + @search = current_organization.members.ransack(search_params) + + @members = + @search.result.eager_load(:account, :user).page(params[:page]).per(25) + + @member_view_models = + @members.map { |m| MemberDecorator.new(m, self.class.helpers) } end def show @@ -39,7 +30,6 @@ def edit def create authorize User - # New User email = user_params[:email] @user = User.find_or_initialize_by(email: email) do |u| u.attributes = user_params @@ -69,17 +59,21 @@ def update private + def search_params + {s: 'member_uid asc'}.merge(params.fetch(:q, {})) + end + def scoped_users current_organization.users end def user_params fields_to_permit = %w"gender username email date_of_birth phone - alt_phone active description notifications" + alt_phone active description notifications push_notifications" fields_to_permit += %w"admin registration_number registration_date" if admin? fields_to_permit += %w"organization_id superadmin" if superadmin? - # params[:user].permit(*fields_to_permit).tap &method(:ap) + params.require(:user).permit *fields_to_permit end diff --git a/app/decorators/member_decorator.rb b/app/decorators/member_decorator.rb new file mode 100644 index 000000000..ab1b6926a --- /dev/null +++ b/app/decorators/member_decorator.rb @@ -0,0 +1,49 @@ +class MemberDecorator < ViewModel + delegate :user, :member_uid, :active?, to: :object + delegate :phone, :alt_phone, :username, to: :user + + def manager? + !!object.manager + end + + def row_css_class + 'bg-danger' unless active? + end + + def inactive_icon + view.glyph('time') unless active? + end + + def link_to_self + view.link_to(user.username, routes.user_path(user)) + end + + def mail_to + email = user.unconfirmed_email || user.email + view.mail_to(email) if email && !email.end_with?('example.com') + end + + def avatar_img + view.image_tag(view.avatar_url(user, 32), width: 32, height: 32) + end + + def account_balance + view.seconds_to_hm(object.account.try(:balance) || 0) + end + + def edit_user_path + routes.edit_user_path(user) + end + + def toggle_manager_member_path + routes.toggle_manager_member_path(object) + end + + def cancel_member_path + routes.member_path(object) + end + + def toggle_active_member_path + routes.toggle_active_member_path(object) + end +end diff --git a/app/decorators/view_model.rb b/app/decorators/view_model.rb new file mode 100644 index 000000000..8de925637 --- /dev/null +++ b/app/decorators/view_model.rb @@ -0,0 +1,39 @@ +# View model base class +# --------------------- +# +# Create a subclass to expose some specific methods from a business layer model +# plus view helpers and route helpers. The business object is readable as +# `object`, the view helpers as `view` and the route helpers as `routes`. +# +# Examples +# -------- +# +# class UserDecorator < ViewModel +# def path_to_edit +# routes.edit_user_path(object) +# end +# +# def email_link +# view.mail_to(object.email) +# end +# end +# +# How to use +# ---------- +# +# The first argument to the initializer is an arbitrary object, and the second +# is expected to respond correctly to view helpers like `link_to` and similar. +# +# From controllers, one can pass `self.class.helpers`, and from tests it is +# enough to use ApplicationController.new.view_context. +# +class ViewModel + attr_reader :object, :view, :routes + + def initialize(object, view) + @object = object + @view = view + @routes = Rails.application.routes.url_helpers + end +end + diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 5c4512e0d..6c198475c 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -6,11 +6,14 @@ module ApplicationHelper # from gravatar def avatar_url(user, size = 32) gravatar_id = Digest::MD5::hexdigest(user.email).downcase - gravatar_options = Hash[set: "set1", - gravatar: "hashed", - size: "#{size}x#{size}"] - "https://www.gravatar.com/avatar/#{gravatar_id}.png?" + - "#{Rack::Utils.build_query(gravatar_options)}&d=identicon" + gravatar_options = { + set: "set1", + gravatar: "hashed", + size: "#{size}x#{size}", + d: "identicon" + } + + "https://www.gravatar.com/avatar/#{gravatar_id}.png?#{gravatar_options.to_param}" end def mdash @@ -38,7 +41,7 @@ def show_error_messages!(resource) messages = resource.errors. full_messages.map { |msg| content_tag(:li, msg) }.join html = <<-HTML -
+
@@ -42,25 +47,23 @@ <% if current_user && current_organization %> - -<% end %> \ No newline at end of file + +<% end %> diff --git a/app/views/application/menus/_language_switcher.html.erb b/app/views/application/menus/_language_switcher.html.erb index 2ec7adbd7..9c6b05ba4 100644 --- a/app/views/application/menus/_language_switcher.html.erb +++ b/app/views/application/menus/_language_switcher.html.erb @@ -1,19 +1,15 @@ -
  • -
    - -
    -
  • +
    + + <%= t("locales.#{locale}") %> + + + +
    diff --git a/app/views/application/menus/_user_admin_menu.html.erb b/app/views/application/menus/_user_admin_menu.html.erb index 2d9312ecd..7efe8b599 100644 --- a/app/views/application/menus/_user_admin_menu.html.erb +++ b/app/views/application/menus/_user_admin_menu.html.erb @@ -5,38 +5,6 @@ diff --git a/app/views/application/menus/_user_admin_menu_items.html.erb b/app/views/application/menus/_user_admin_menu_items.html.erb new file mode 100644 index 000000000..4efeee1da --- /dev/null +++ b/app/views/application/menus/_user_admin_menu_items.html.erb @@ -0,0 +1,38 @@ +<% if current_user.organizations.count > 1 %> + <%= render 'application/menus/organization_switcher' %> + +<% end %> + +
  • + <%= link_to current_user do %> + <%= glyph :user %> + <%= t "layouts.application.edit_profile" %> + <% end %> +
  • + +<% current_user.members.where(manager: true).each do |m| %> +
  • + <%= link_to m.organization do %> + <%= glyph :pencil %> + <%= t "layouts.application.edit_org", organization: m.organization %> + <% end %> +
  • +<% end %> + +<% if superadmin? %> +
  • + <%= link_to admin_root_path do %> + <%= glyph :cog %> + <%= t "application.navbar.adminshort" %> + <% end %> +
  • +<% end %> + + + +
  • + <%= link_to destroy_user_session_path, method: :delete do %> + <%= glyph :log_out %> + <%= t "application.navbar.sign_out" %> + <% end %> +
  • diff --git a/app/views/application/menus/_user_list_link.html.erb b/app/views/application/menus/_user_list_link.html.erb index e3bf2c45e..1b27a7da3 100644 --- a/app/views/application/menus/_user_list_link.html.erb +++ b/app/views/application/menus/_user_list_link.html.erb @@ -1,6 +1,6 @@
  • "> <%= link_to users_path do %> <%= glyph :user %> - <%= User.model_name.human(count: :many) %> + <%= t("users.index.members") %> <% end %>
  • diff --git a/app/views/application/menus/_visitor_menu.html.erb b/app/views/application/menus/_visitor_menu.html.erb new file mode 100644 index 000000000..513a63e63 --- /dev/null +++ b/app/views/application/menus/_visitor_menu.html.erb @@ -0,0 +1,2 @@ +
  • <%= link_to t("layouts.application.login"), new_user_session_path %>
  • +
  • <%= link_to t("layouts.application.about"), page_path("about") %>
  • diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb index 53da0b629..636442477 100644 --- a/app/views/devise/sessions/new.html.erb +++ b/app/views/devise/sessions/new.html.erb @@ -1,11 +1,6 @@ -
    +
    -
    -

    - TimeOverflow -

    -
    <%= render 'layouts/messages' %> <%= show_error_messages!(resource) %> diff --git a/app/views/home/index.html.erb b/app/views/home/index.html.erb index e8cdafdcf..f737c6b90 100644 --- a/app/views/home/index.html.erb +++ b/app/views/home/index.html.erb @@ -1,7 +1,7 @@
    -
    +

    <%= t("application.landing.slogan") %>

    <%= t("application.landing.sub_slogan") %>

    <%= t("application.landing.button") %>
    -
    \ No newline at end of file +
    diff --git a/app/views/inquiries/index.html.erb b/app/views/inquiries/index.html.erb index a5671a86c..3dabc4c83 100644 --- a/app/views/inquiries/index.html.erb +++ b/app/views/inquiries/index.html.erb @@ -1,57 +1,77 @@ <% @category = Category.where(id: params[:cat]).first %> -

    - <%= Inquiry.model_name.human(count: :many) %> - <%= render "shared/show_filter_hint" %> -

    -