diff --git a/.env.example b/.env.example index cff6d4f..3f56d77 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/Gemfile b/Gemfile index dde7f6d..3a36ae4 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' @@ -54,7 +59,7 @@ end group :test do gem "database_cleaner", '1.6.2' - gem 'shoulda', ">= 3.5" + gem 'shoulda-matchers', '~> 3.1.2' gem 'fabrication' gem 'faker' gem 'capybara', '~> 2.7' diff --git a/Gemfile.lock b/Gemfile.lock index 6a7480c..77f77a7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -112,6 +112,7 @@ GEM coffee-script-source (1.8.0) columnize (0.9.0) concurrent-ruby (1.0.5) + connection_pool (2.2.1) crass (1.0.3) dalli (2.7.2) database_cleaner (1.6.2) @@ -143,6 +144,8 @@ 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) @@ -239,6 +242,8 @@ GEM pundit (0.3.0) activesupport (>= 3.0.0) rack (1.6.9) + rack-protection (2.0.1) + rack rack-test (0.6.3) rack (>= 1.0) rails (4.2.10) @@ -283,6 +288,7 @@ GEM i18n polyamorous (~> 1.3.2) rdiscount (2.1.7.1) + redis (4.0.1) responders (2.0.2) railties (>= 4.2.0.alpha, < 5) rest-client (2.0.1) @@ -317,6 +323,8 @@ GEM 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) @@ -329,12 +337,16 @@ GEM selenium-webdriver (3.11.0) childprocess (~> 0.5) rubyzip (~> 1.2) - 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) + 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) @@ -425,9 +437,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) @@ -439,4 +454,4 @@ RUBY VERSION ruby 2.3.0p0 BUNDLED WITH - 1.16.1 + 1.16.2 diff --git a/README.md b/README.md index 8456cc4..564c5ef 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/assets/images/home_back.jpg b/app/assets/images/home_back.jpg index e3026ee..e3cbe7b 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_overlay.jpg b/app/assets/images/home_back_overlay.jpg deleted file mode 100644 index 3a9ac60..0000000 Binary files a/app/assets/images/home_back_overlay.jpg and /dev/null differ diff --git a/app/assets/stylesheets/_bootstrap-custom.scss b/app/assets/stylesheets/_bootstrap-custom.scss new file mode 100644 index 0000000..4d9e6df --- /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 0000000..209c590 --- /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 0000000..676abeb --- /dev/null +++ b/app/assets/stylesheets/_footer.scss @@ -0,0 +1,86 @@ +.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; + } + + &: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; + } + } +} + +.footer { + padding: 8px 0; + line-height: 40px; + 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; + } +} diff --git a/app/assets/stylesheets/_to-categories-dropdown.scss b/app/assets/stylesheets/_to-categories-dropdown.scss new file mode 100644 index 0000000..648d224 --- /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 0000000..3a06a60 --- /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 bead081..c135e8e 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 { + min-height: 500px; + margin-bottom: 36px; } -.language-selector{ - margin: 1.2rem 3rem 1.2rem 1rem; -} +.actions-menu { + margin-bottom: 16px; + padding: 0 15px; -.language-selector a span { - display: block; - float: left; -} - -.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: 10px; + } } -.actions-menu > li{ - margin-right:5px; +.actions-menu > li { + margin-right: 6px; + + @media(max-width: $screen-sm-min) { + margin-right: 2px; + } } #login-box { @@ -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; @@ -160,7 +129,6 @@ html { color: $form-input-glyph; font-size: 2.24rem; letter-spacing: -0.0040rem; - padding: 2rem; } .form-control { @@ -198,7 +166,7 @@ html { background-color: $white; border: 1px solid $border; border-radius: 5px; - margin: 10px; + margin: 14px 0; padding: 5px 20px; h4 { @@ -216,6 +184,12 @@ html { margin-right: 10px; } + &__datetime { + color: $palette-grey; + padding: 4px 0 0; + font-size: 10px; + text-align: right; + } } .break-word { @@ -231,36 +205,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 +229,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 { @@ -409,8 +352,13 @@ label[required]::after{ } .navbar-form { - padding: 0; + border: 0; background-color: transparent; + padding: 0; + + @media(max-width: $screen-sm-min) { + padding: 10px 15px; + } } form .checkbox input[type="checkbox"] { @@ -429,7 +377,11 @@ form .checkbox input[type="checkbox"] { z-index: -1; } -.navbar-block{ +.navbar .container-fluid { + padding: 0; +} + +.navbar-block { margin-top: 0.2rem; } @@ -442,104 +394,19 @@ 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; @@ -547,18 +414,15 @@ form .checkbox input[type="checkbox"] { .back-overlay { display: block; filter: alpha(opacity = 60); /* For IE8 and earlier */ - opacity: 0.6; + opacity: 0.2; } + .vertical-align { - left: 50%; position: absolute; - top: 47%; + top: 48%; + left: 50%; transform: translate(-50%, -50%); - width: 75%; - } - .container-fluid { - padding-bottom: 0.8rem; - padding-top: 0.8rem; + width: 100%; } .container-inner { @@ -578,7 +442,7 @@ 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; @@ -595,6 +459,7 @@ form .checkbox input[type="checkbox"] { font-weight: 700; letter-spacing: -0.073rem; margin: 0; + text-shadow: 1px 1px $palette-black; } h3 { @@ -603,6 +468,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 +491,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 +536,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,7 +636,7 @@ 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; @@ -831,79 +681,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 { - 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%; - } - } } .errors { @@ -916,4 +701,4 @@ form .checkbox input[type="checkbox"] { padding: 1em; line-height: 1.6em; } -} \ No newline at end of file +} diff --git a/app/controllers/organizations_controller.rb b/app/controllers/organizations_controller.rb index 1d84b94..0a394f1 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] 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 @@ class OrganizationsController < ApplicationController end def create - @organization = @organizations.build organization_params + @organization = Organization.new(organization_params) + if @organization.save redirect_to @organization, status: :created else @@ -57,6 +50,10 @@ class OrganizationsController < ApplicationController 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/jobs/create_push_notifications_job.rb b/app/jobs/create_push_notifications_job.rb new file mode 100644 index 0000000..3e7ccff --- /dev/null +++ b/app/jobs/create_push_notifications_job.rb @@ -0,0 +1,11 @@ +class CreatePushNotificationsJob < ActiveJob::Base + queue_as :default + + def perform(event_id:) + event = ::Event.find_by_id(event_id) + + raise 'A valid Event must be provided' unless event + + ::PushNotifications::Creator.new(event: event).create! + end +end diff --git a/app/models/event.rb b/app/models/event.rb new file mode 100644 index 0000000..2058045 --- /dev/null +++ b/app/models/event.rb @@ -0,0 +1,20 @@ +class Event < ActiveRecord::Base + enum action: { created: 0, updated: 1 } + + belongs_to :post + belongs_to :member + belongs_to :transfer + + validates :action, presence: true + validate :resource_presence + + private + + # Validates that only one resource id is set + # + def resource_presence + return if post_id.present? ^ member_id.present? ^ transfer_id.present? + + errors.add(:base, 'Specify only one resource id: `post_id`, `member_id` or `transfer_id`') + end +end diff --git a/app/models/organization.rb b/app/models/organization.rb index 47d81c7..9f664d9 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -2,11 +2,7 @@ class Organization < ActiveRecord::Base has_many :members, dependent: :destroy has_many :users, -> { order "members.created_at DESC" }, through: :members has_many :all_accounts, class_name: "Account", inverse_of: :organization - has_many :all_movements, class_name: "Movement", through: :all_accounts, source: :movements do - def with_transfer - joins(transfer: :movements) - end - end + has_many :all_movements, class_name: "Movement", through: :all_accounts, source: :movements has_many :all_transfers, class_name: "Transfer", through: :all_movements, source: :transfer has_one :account, as: :accountable has_many :member_accounts, through: :members, source: :account @@ -15,11 +11,7 @@ class Organization < ActiveRecord::Base has_many :inquiries has_many :documents, as: :documentable - scope :matching, ->(str) { - where(Organization.arel_table[:name].matches("%#{str}%")) - } - - validates :name, uniqueness: true + validates :name, presence: true, uniqueness: true before_validation :ensure_url after_create :create_account diff --git a/app/models/push_notification.rb b/app/models/push_notification.rb new file mode 100644 index 0000000..5aff862 --- /dev/null +++ b/app/models/push_notification.rb @@ -0,0 +1,6 @@ +class PushNotification < ActiveRecord::Base + belongs_to :event, foreign_key: 'event_id' + belongs_to :device_token, foreign_key: 'device_token_id' + + validates :event, :device_token, presence: true +end diff --git a/app/models/push_notifications/post_notification.rb b/app/models/push_notifications/post_notification.rb deleted file mode 100644 index 9a48f37..0000000 --- a/app/models/push_notifications/post_notification.rb +++ /dev/null @@ -1,11 +0,0 @@ -module PushNotifications - class PostNotification - def title - "Notification title" - end - - def body - "Notification body" - end - end -end diff --git a/app/services/persister/member_persister.rb b/app/services/persister/member_persister.rb index 4bed82c..51594ff 100644 --- a/app/services/persister/member_persister.rb +++ b/app/services/persister/member_persister.rb @@ -7,7 +7,33 @@ module Persister end def save - member.save + ::ActiveRecord::Base.transaction do + member.save! + create_save_event! + member + end + rescue ActiveRecord::RecordInvalid => _exception + false + end + + def update_attributes(params) + ::ActiveRecord::Base.transaction do + member.update_attributes!(params) + create_update_event! + member + end + rescue ActiveRecord::RecordInvalid => _exception + false + end + + private + + def create_save_event! + ::Event.create! action: :created, member: member + end + + def create_update_event! + ::Event.create! action: :updated, member: member end end end diff --git a/app/services/persister/post_persister.rb b/app/services/persister/post_persister.rb index eb4e94f..d4f0a02 100644 --- a/app/services/persister/post_persister.rb +++ b/app/services/persister/post_persister.rb @@ -7,11 +7,41 @@ module Persister end def save - post.save + ::ActiveRecord::Base.transaction do + post.save! + create_save_event! + enqueue_push_notification_job! + post + end + rescue ActiveRecord::RecordInvalid => _exception + false end def update_attributes(params) - post.update_attributes(params) + ::ActiveRecord::Base.transaction do + post.update_attributes!(params) + create_update_event! + enqueue_push_notification_job! + post + end + rescue ActiveRecord::RecordInvalid => _exception + false + end + + private + + attr_accessor :event + + def create_save_event! + @event = ::Event.create! action: :created, post: post + end + + def create_update_event! + @event = ::Event.create! action: :updated, post: post + end + + def enqueue_push_notification_job! + CreatePushNotificationsJob.perform_later(event_id: event.id) end end end diff --git a/app/services/persister/transfer_persister.rb b/app/services/persister/transfer_persister.rb index 5e72e46..a5fd86f 100644 --- a/app/services/persister/transfer_persister.rb +++ b/app/services/persister/transfer_persister.rb @@ -7,7 +7,33 @@ module Persister end def save - transfer.save + ::ActiveRecord::Base.transaction do + transfer.save! + create_save_event! + transfer + end + rescue ActiveRecord::RecordInvalid => _exception + false + end + + def update_attributes(params) + ::ActiveRecord::Base.transaction do + transfer.update_attributes!(params) + create_update_event! + transfer + end + rescue ActiveRecord::RecordInvalid => _exception + false + end + + private + + def create_save_event! + ::Event.create! action: :created, transfer: transfer + end + + def create_update_event! + ::Event.create! action: :updated, transfer: transfer end end end diff --git a/app/services/push_notifications/creator.rb b/app/services/push_notifications/creator.rb new file mode 100644 index 0000000..9096cbd --- /dev/null +++ b/app/services/push_notifications/creator.rb @@ -0,0 +1,22 @@ +module PushNotifications + class Creator + # Given an Event it will create as many PushNotification resources + # necessary as the resource associated to the Event will require. + # + # @param [Hash] event: + def initialize(event:) + @event = event + end + + def create! + event_notifier = EventNotifierFactory.new(event: event).build + event_notifier.device_tokens.each do |device_token| + PushNotification.create!(event: event, device_token: device_token) + end + end + + private + + attr_accessor :event + end +end diff --git a/app/services/push_notifications/event_notifier/post.rb b/app/services/push_notifications/event_notifier/post.rb new file mode 100644 index 0000000..6e5592d --- /dev/null +++ b/app/services/push_notifications/event_notifier/post.rb @@ -0,0 +1,26 @@ +module PushNotifications + module EventNotifier + class Post + def initialize(event:) + @event = event + @post = event.post + end + + # Conditions for Post: + # + # We need to notify all the users that: + # - are members of the Post's organization + # - have a DeviceToken associated + # + # @return [] + def device_tokens + organization = post.organization + DeviceToken.where(user_id: organization.user_ids) + end + + private + + attr_accessor :event, :post + end + end +end diff --git a/app/services/push_notifications/event_notifier_factory.rb b/app/services/push_notifications/event_notifier_factory.rb new file mode 100644 index 0000000..0925830 --- /dev/null +++ b/app/services/push_notifications/event_notifier_factory.rb @@ -0,0 +1,17 @@ +module PushNotifications + class EventNotifierFactory + def initialize(event:) + @event = event + end + + def build + return EventNotifier::Post.new(event: event) if event.post_id + + raise 'The resource associated to the Event is not supported' + end + + private + + attr_accessor :event + end +end diff --git a/app/views/application/_footer.html.erb b/app/views/application/_footer.html.erb new file mode 100644 index 0000000..295b6ff --- /dev/null +++ b/app/views/application/_footer.html.erb @@ -0,0 +1,17 @@ + diff --git a/app/views/application/_navbar.html.erb b/app/views/application/_navbar.html.erb index f652088..74364f5 100644 --- a/app/views/application/_navbar.html.erb +++ b/app/views/application/_navbar.html.erb @@ -30,9 +30,6 @@ <%= link_to t("layouts.application.about"), page_path("about") %> <% end %> - - - <%= render 'application/menus/language_switcher' %> @@ -43,24 +40,22 @@ <% 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 2ec7adb..9c6b05b 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_list_link.html.erb b/app/views/application/menus/_user_list_link.html.erb index e3bf2c4..1b27a7d 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/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb index 53da0b6..051e353 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/inquiries/index.html.erb b/app/views/inquiries/index.html.erb index a5671a8..3dabc4c 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" %> -

    -