Merge branch 'develop' into search_member_uid

Conflicts:
	app/views/users/index.html.erb
This commit is contained in:
Marc Anguera Insa
2018-06-13 21:09:43 +02:00
68 changed files with 1334 additions and 679 deletions
-1
View File
@@ -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:
+6 -1
View File
@@ -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'
+23 -8
View File
@@ -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
+1 -1
View File
@@ -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)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 891 KiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 592 KiB

@@ -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";
@@ -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;
+86
View File
@@ -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;
}
}
@@ -0,0 +1,5 @@
.to-categories-dropdown {
@media(max-width: $screen-sm-min) {
display: none;
}
}
+29
View File
@@ -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;
+54 -269
View File
@@ -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;
}
}
}
+8 -11
View File
@@ -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
+11
View File
@@ -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
+20
View File
@@ -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
+2 -10
View File
@@ -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
+6
View File
@@ -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
@@ -1,11 +0,0 @@
module PushNotifications
class PostNotification
def title
"Notification title"
end
def body
"Notification body"
end
end
end
+27 -1
View File
@@ -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
+32 -2
View File
@@ -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
+27 -1
View File
@@ -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
@@ -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: <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
@@ -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 [<DeviceToken>]
def device_tokens
organization = post.organization
DeviceToken.where(user_id: organization.user_ids)
end
private
attr_accessor :event, :post
end
end
end
@@ -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
+17
View File
@@ -0,0 +1,17 @@
<div class="footer container-fluid">
<div class="container">
<div class="row">
<div class="col-xs-12 col-sm-4 col-md-4 col-lg-4 footer-left-col">
<a href="http://community.coopdevs.org/" target="_blank"><%= t('layouts.application.help') %></a>
<%= link_to t('application.terms_conditions'), tnc_path, data: { target: '#modal', toggle: 'modal' } %>
</div>
<div class="col-xs-12 col-sm-4 col-md-4 col-lg-4 footer-center-col">
<%= render 'application/menus/language_switcher' %>
</div>
<div class="col-xs-12 col-sm-4 col-md-4 col-lg-4 footer-right-col">
<a href="https://twitter.com/timeoverflow" target="_blank">Twitter</a>
<a href="https://github.com/coopdevs/timeoverflow" target="_blank">Github</a>
</div>
</div>
</div>
</div>
+15 -20
View File
@@ -30,9 +30,6 @@
<%= link_to t("layouts.application.about"), page_path("about") %>
</li>
<% end %>
<li class="divider" role="presentation"></li>
<%= render 'application/menus/language_switcher' %>
</ul>
</div>
</div>
@@ -43,24 +40,22 @@
<% if current_user && current_organization %>
<nav role="navigation">
<div class="container-fluid">
<div class="container">
<div class="row">
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
<ul class="nav nav-pills actions-menu">
<%= render 'application/menus/user_list_link' %>
<%= render 'application/menus/offers_dashboard_link' %>
<%= render 'application/menus/inquiries_list_link' %>
<%= render 'application/menus/offers_by_tag_link' %>
<% if current_user.manages? current_organization %>
<%= render 'application/menus/organization_reports_menu' %>
<%= render 'application/menus/organization_statistics_menu' %>
<%= render 'application/menus/organization_listings_menu' %>
<% end %>
</ul>
</div>
<div class="container">
<div class="row">
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
<ul class="nav nav-pills actions-menu">
<%= render 'application/menus/user_list_link' %>
<%= render 'application/menus/offers_dashboard_link' %>
<%= render 'application/menus/inquiries_list_link' %>
<% if current_user.manages? current_organization %>
<%= render 'application/menus/offers_by_tag_link' %>
<%= render 'application/menus/organization_reports_menu' %>
<%= render 'application/menus/organization_statistics_menu' %>
<%= render 'application/menus/organization_listings_menu' %>
<% end %>
</ul>
</div>
</div>
</div>
</nav>
<% end %>
<% end %>
@@ -1,19 +1,15 @@
<li>
<div class="language-selector">
<div class="dropdown dropdown-language-selector">
<a class="btn btn-default dropdown-toggle language" type="button" id="dropdownLanguage" data-toggle="dropdown">
<span><%= t("locales.#{locale}") %></span>
<b class="caret"></b>
</a>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1" >
<% I18n.available_locales.each do |locale| %>
<% if I18n.locale != locale %>
<li>
<%= link_to t("locales.#{locale}"), switch_lang_path(locale: locale) %>
</li>
<% end %>
<% end %>
</ul>
</div>
</div>
</li>
<div class="dropup language-selector">
<a class="btn btn-default dropdown-toggle language" type="button" id="dropdownLanguage" data-toggle="dropdown">
<span><%= t("locales.#{locale}") %></span>
<b class="caret"></b>
</a>
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenu1" >
<% I18n.available_locales.each do |locale| %>
<% if I18n.locale != locale %>
<li>
<%= link_to t("locales.#{locale}", locale: locale), switch_lang_path(locale: locale) %>
</li>
<% end %>
<% end %>
</ul>
</div>
@@ -1,6 +1,6 @@
<li class="<%= "active" if current_page?(users_path) %>">
<%= link_to users_path do %>
<%= glyph :user %>
<%= User.model_name.human(count: :many) %>
<%= t("users.index.members") %>
<% end %>
</li>
-5
View File
@@ -1,11 +1,6 @@
<div class="vertical-align">
<div class="col-xs-12 col-sm-8 col-md-6 col-lg-6 col-sm-offset-2 col-md-offset-3 col-lg-offset-3">
<div class="panel" id="login-box">
<div class="panel-heading">
<h1>
TimeOverflow
</h1>
</div>
<div class="panel-body">
<%= render 'layouts/messages' %>
<%= show_error_messages!(resource) %>
+70 -50
View File
@@ -1,57 +1,77 @@
<% @category = Category.where(id: params[:cat]).first %>
<h1>
<%= Inquiry.model_name.human(count: :many) %>
<%= render "shared/show_filter_hint" %>
</h1>
<div class="navbar">
<form action="<%= inquiries_path %>"
class="navbar-form navbar-left"
method="get"
role="search">
<div class="form-group">
<input class="form-control"
name="q"
placeholder="<%= t("global.search") %>"
type="text"
value="<%= params[:q] %>">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<h1>
<%= Inquiry.model_name.human(count: :many) %>
<%= render "shared/show_filter_hint" %>
</h1>
</div>
<button class="btn btn-default" type="submit">
<%= t("global.search") %>
</button>
</form>
<ul class="nav navbar-nav">
<li class="dropdown">
<a class="dropdown-toggle" href="#" data-toggle="dropdown">
<%= @category ? @category.name : Category.model_name.human %>
<span class="caret"></span>
</a>
<ul class="dropdown-menu" role="menu">
<li>
<%= link_to "× #{t("global.all")}", inquiries_path %>
</li>
<% Category.all.sort_by { |a| a.name.downcase }.each do |c| %>
<% next if c == @category %>
<li>
<%= link_to c.name, inquiries_path(cat: c.id) %>
</li>
<% end %>
</ul>
</li>
</ul>
<% if current_user && current_organization && !params[:org] %>
<ul class="nav navbar-nav pull-right">
<li>
<%= link_to new_inquiry_path do %>
<%= glyph :plus %>
<%= t "helpers.submit.create", model: Inquiry.model_name.human %>
<% end %>
</li>
</ul>
<% end %>
</div>
</div>
<div class="inquiries">
<%= render "shared/posts", posts: @inquiries %>
<div class="container-fluid">
<div class="row">
<div class="col-md-8">
<form action="<%= inquiries_path %>"
class="navbar-form navbar-left"
method="get"
role="search">
<div class="form-group">
<input class="form-control"
name="q"
placeholder="<%= t("global.search") %>"
type="text"
value="<%= params[:q] %>">
</div>
<button class="btn btn-default" type="submit">
<%= t("global.search") %>
</button>
</form>
<ul class="to-categories-dropdown nav navbar-nav">
<li class="dropdown">
<a class="dropdown-toggle" href="#" data-toggle="dropdown">
<%= @category ? @category.name : Category.model_name.human %>
<span class="caret"></span>
</a>
<ul class="dropdown-menu" role="menu">
<li>
<%= link_to "× #{t("global.all")}", inquiries_path %>
</li>
<% Category.all.sort_by { |a| a.name.downcase }.each do |c| %>
<% next if c == @category %>
<li>
<%= link_to c.name, inquiries_path(cat: c.id) %>
</li>
<% end %>
</ul>
</li>
</ul>
</div>
<div class="col-md-4">
<% if current_user && current_organization && !params[:org] %>
<ul class="nav navbar-nav pull-right">
<li>
<%= link_to new_inquiry_path do %>
<%= glyph :plus %>
<%= t "helpers.submit.create", model: Inquiry.model_name.human %>
<% end %>
</li>
</ul>
<% end %>
</div>
</div>
</div>
<div class="container-fluid inquiries">
<div class="row">
<div class="col-md-12">
<%= render "shared/posts", posts: @inquiries %>
</div>
</div>
</div>
<%= paginate @inquiries %>
+3 -24
View File
@@ -34,30 +34,9 @@
<div class="modal-content"></div>
</div>
</div>
<div class="footer container-fluid">
<div class="container">
<div class="row">
<div class="col-xs-12 col-sm-4 col-md-4 col-lg-4 footer-left-col">
<p>
<a href="http://community.coopdevs.org/" target="_blank"><%= t('layouts.application.help') %></a>
</p>
</div>
<div class="col-xs-12 col-sm-4 col-md-4 col-lg-4 footer-center-col">
<p>
<%= link_to t('application.terms_conditions'),
tnc_path,
data: { target: '#modal', toggle: 'modal' } %>
</p>
</div>
<div class="col-xs-12 col-sm-4 col-md-4 col-lg-4 footer-right-col">
<p>
<a href="https://twitter.com/timeoverflow" target="_blank">Twitter</a>
<a href="https://github.com/coopdevs/timeoverflow" target="_blank">Github</a>
</p>
</div>
</div>
</div>
</div>
<%= render 'application/footer' %>
<div class="back-overlay"></div>
<!-- Piwik -->
<script type="text/javascript">
+76 -57
View File
@@ -1,59 +1,78 @@
<h1>
<%= Offer.model_name.human(count: :many) %>
<%= render "shared/show_filter_hint" %>
</h1>
<div class="navbar">
<form action="<%= offers_path %>"
class="navbar-form navbar-left"
method="get"
role="search">
<div class="form-group">
<input class="form-control"
name="q"
placeholder="<%= t "global.search" %>"
type="text"
value="<%= params[:q] %>">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<h1>
<%= Offer.model_name.human(count: :many) %>
<%= render "shared/show_filter_hint" %>
</h1>
</div>
<button class="btn btn-default" type="submit">
<%= t("global.search") %>
</button>
</form>
<ul class="nav navbar-nav">
<li class="dropdown">
<a class="dropdown-toggle" href="#" data-toggle="dropdown">
<%= @category ? @category.name : Category.model_name.human %>
<span class="caret"></span>
</a>
<ul class="dropdown-menu" role="menu">
<li>
<%= link_to "× #{t "global.all"}", offers_path %>
</li>
<% Category.all.sort_by { |a| a.name.downcase }.each do |c| %>
<% next if c == @category %>
<li>
<%= link_to c.name, offers_path(cat: c.id) %>
</li>
<% end %>
</ul>
</li>
</ul>
<ul class="nav navbar-nav pull-right">
<li>
<%= link_to new_offer_path do %>
<%= glyph :plus %>
<%= t "helpers.submit.create", model: Offer.model_name.human %>
<% end %>
</li>
</ul>
</div>
<% @offers.each do |cat, offs| %>
<div class="offers">
<h3>
<%= cat.name.capitalize %>
</h3>
<%= render 'shared/posts', posts: offs %>
</div>
<%= link_to offers_path(cat: cat.id), class: "more btn btn-primary" do %>
<%= t('global.more') %>
<% end %>
<% end %>
</div>
<div class="container-fluid">
<div class="row">
<div class="col-md-8">
<form action="<%= offers_path %>"
class="navbar-form navbar-left"
method="get"
role="search">
<div class="form-group">
<input class="form-control"
name="q"
placeholder="<%= t "global.search" %>"
type="text"
value="<%= params[:q] %>">
</div>
<button class="btn btn-default" type="submit">
<%= t("global.search") %>
</button>
</form>
<ul class="to-categories-dropdown nav navbar-nav">
<li class="dropdown">
<a class="dropdown-toggle" href="#" data-toggle="dropdown">
<%= @category ? @category.name : Category.model_name.human %>
<span class="caret"></span>
</a>
<ul class="dropdown-menu" role="menu">
<li>
<%= link_to "× #{t "global.all"}", offers_path %>
</li>
<% Category.all.sort_by { |a| a.name.downcase }.each do |c| %>
<% next if c == @category %>
<li>
<%= link_to c.name, offers_path(cat: c.id) %>
</li>
<% end %>
</ul>
</li>
</ul>
</div>
<div class="col-md-4">
<ul class="nav navbar-nav pull-right">
<li>
<%= link_to new_offer_path do %>
<%= glyph :plus %>
<%= t "helpers.submit.create", model: Offer.model_name.human %>
<% end %>
</li>
</ul>
</div>
</div>
</div>
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<% @offers.each do |cat, offs| %>
<div class="offers">
<h3><%= cat.name.capitalize %></h3>
<%= render 'shared/posts', posts: offs %>
</div>
<%= link_to offers_path(cat: cat.id), class: "more btn btn-primary" do %>
<%= t('global.more') %>
<% end %>
<% end %>
</div>
</div>
</div>
+70 -49
View File
@@ -1,55 +1,76 @@
<h1>
<% @category = Category.where(id: params[:cat]).first %>
<%= Offer.model_name.human(count: :many) %>
<%= render "shared/show_filter_hint" %>
</h1>
<div class="navbar">
<form action="<%= offers_path %>"
class="navbar-form navbar-left"
method="get"
role="search">
<div class="form-group">
<input class="form-control"
name="q"
placeholder="<%= t 'global.search' %>"
type="text"
value="<%= params[:q] %>">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<h1>
<% @category = Category.where(id: params[:cat]).first %>
<%= Offer.model_name.human(count: :many) %>
<%= render "shared/show_filter_hint" %>
</h1>
</div>
<button class="btn btn-default" type="submit">
<%= t 'global.search' %>
</button>
</form>
<ul class="nav navbar-nav">
<li class="dropdown">
<a class="dropdown-toggle" href="#" data-toggle="dropdown">
<%= @category ? @category.name : Category.model_name.human %>
<span class="caret"></span>
</a>
<ul class="dropdown-menu" role="menu">
<li>
<%= link_to "× #{t 'global.all'}", dashboard_offers_path %>
</div>
</div>
<div class="container-fluid">
<div class="row">
<div class="col-md-8">
<form action="<%= offers_path %>"
class="navbar-form navbar-left"
method="get"
role="search">
<div class="form-group">
<input class="form-control"
name="q"
placeholder="<%= t 'global.search' %>"
type="text"
value="<%= params[:q] %>">
</div>
<button class="btn btn-default" type="submit">
<%= t 'global.search' %>
</button>
</form>
<ul class="to-categories-dropdown nav navbar-nav">
<li class="dropdown">
<a class="dropdown-toggle" href="#" data-toggle="dropdown">
<%= @category ? @category.name : Category.model_name.human %>
<span class="caret"></span>
</a>
<ul class="dropdown-menu" role="menu">
<li>
<%= link_to "× #{t 'global.all'}", dashboard_offers_path %>
</li>
<% Category.all.sort_by { |a| a.name.downcase }.each do |c| %>
<% next if c == @category %>
<li>
<%= link_to c.name, offers_path(cat: c.id) %>
</li>
<% end %>
</ul>
</li>
<% Category.all.sort_by { |a| a.name.downcase }.each do |c| %>
<% next if c == @category %>
<li>
<%= link_to c.name, offers_path(cat: c.id) %>
</li>
<% end %>
</ul>
</li>
</ul>
<% if current_user && current_organization && !params[:org] %>
<ul class="nav navbar-nav pull-right">
<li>
<%= link_to new_offer_path do %>
<%= glyph :plus %>
<%= t "helpers.submit.create", model: Offer.model_name.human %>
<% end %>
</li>
</ul>
<% end %>
</div>
<div class="col-md-4">
<% if current_user && current_organization && !params[:org] %>
<ul class="nav navbar-nav pull-right">
<li>
<%= link_to new_offer_path do %>
<%= glyph :plus %>
<%= t "helpers.submit.create", model: Offer.model_name.human %>
<% end %>
</li>
</ul>
<% end %>
</div>
</div>
</div>
<div class="offers">
<%= render "shared/posts", posts: @offers %>
<div class="container-fluid offers">
<div class="row">
<div class="col-md-12">
<%= render "shared/posts", posts: @offers %>
</div>
</div>
</div>
<%= paginate @offers %>
+1
View File
@@ -1,3 +1,4 @@
<%= show_error_messages!(@organization) %>
<%= simple_form_for @organization do |f| %>
<%= f.input :name %>
<%= f.input :email %>
-7
View File
@@ -1,10 +1,3 @@
<% if @organization.errors.present? %>
<% @organization.errors.full_messages.each do |msg| %>
<div class="alert alert-danger" role="alert">
<%= msg %>
</div>
<% end %>
<% end %>
<h1>
<%= link_to @organization, organization_path(@organization) %>
<small><%= t 'global.edit' %></small>
+1 -1
View File
@@ -2,7 +2,7 @@
<%= Organization.model_name.human(count: :many) %>
</h1>
<ul class="nav nav-pills">
<ul class="nav navbar-nav pull-right">
<% if superadmin? %>
<li>
<%= link_to new_organization_path do %>
+6 -5
View File
@@ -18,12 +18,13 @@
<%= glyph :tag %>
<%= link_to tag, get_index_path(post,tag: tag) %>
<% end %>
<% if post.type == "Inquiry" %>
<span class="pull-right">
<%= l post.updated_at.to_date, format: :long %>
</span>
<% end %>
</small>
</p>
<% if post.type == "Inquiry" %>
<div class="post__datetime">
<%= l post.updated_at.to_date, format: :long %>
</div>
<% end %>
</div>
<% end %>
@@ -12,19 +12,19 @@
<th><%= t '.last_movement' %></th>
</tr>
</thead>
<tbody></tbody>
<%# Por días sin movimientos %>
<% @members.includes(:account)
.sort_by(&:days_without_swaps)
.reverse
.each do |mem| %>
<tr>
<td><%= mem.member_uid %></td>
<td><%= link_to mem.user.username, mem.user %></td>
<td><%= mem.days_without_swaps %></td>
<td><%= (mem.account.updated_at == mem.account.created_at) ? t(".no_movements") : l(mem.account.updated_at, format: :long) %></td>
</tr>
<% end %>
<tbody>
<% @members.includes(:account)
.sort_by(&:days_without_swaps)
.reverse
.each do |mem| %>
<tr>
<td><%= mem.member_uid %></td>
<td><%= link_to mem.user.username, mem.user %></td>
<td><%= mem.days_without_swaps %></td>
<td><%= (mem.account.updated_at == mem.account.created_at) ? t(".no_movements") : l(mem.account.updated_at, format: :long) %></td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
+78 -60
View File
@@ -1,65 +1,83 @@
<div>
<h1>
<%= User.model_name.human.pluralize %>
<%= link_to current_organization.name,
organization_path(current_organization) %>
</h1>
<div class="navbar">
<%= search_form_for(@search, class: "navbar-form navbar-left", url: users_path) do |f| %>
<%= f.search_field :user_username_or_user_email_or_member_uid_contains, class: "form-control" %>
<button class="btn btn-default" type="submit">
<%= t 'global.search' %>
</button>
<% end %>
<ul class="nav navbar-nav pull-right">
<% if admin? %>
<li>
<a href="<%= new_user_path %>">
<span class="glyphicon glyphicon-plus"></span>
<%= t "helpers.submit.create", model: User.model_name.human %>
</a>
</li>
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<h1><%= t(".members") %></h1>
</div>
</div>
</div>
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<%= search_form_for(@search, class: "navbar-form navbar-left", url: users_path) do |f| %>
<div class="form-group">
<%= f.search_field :user_username_or_user_email_or_member_uid_contains, class: "form-control" %>
</div>
<button class="btn btn-default" type="submit">
<%= t 'global.search' %>
</button>
<% end %>
</ul>
<ul class="nav navbar-nav pull-right">
<% if admin? %>
<li>
<a href="<%= new_user_path %>">
<span class="glyphicon glyphicon-plus"></span>
<%= t "helpers.submit.create", model: User.model_name.human %>
</a>
</li>
<% end %>
</ul>
</div>
</div>
<div class="table-responsive">
<table class="table table-hover table-condensed users panel">
<thead>
<tr></tr>
<tr>
<th></th>
<th>
<%= sort_link @search, :member_uid, 'ID' %>
</th>
<th>
<%= sort_link @search, :user_username, User.human_attribute_name(:username) %>
</th>
<th>
<%= sort_link @search, :user_email, User.human_attribute_name(:email) %>
</th>
<th>
<%= User.human_attribute_name(:phone) %>
</th>
<th>
<%= User.human_attribute_name(:alt_phone) %>
</th>
<th>
<%= sort_link @search, 'account_balance', Account.human_attribute_name(:balance) %>
</th>
<% if current_user.manages? current_organization %>
<th>
<span class="glyphicon glyphicon-hand-down"></span>
<%= t(".actions") %>
</th>
<% end %>
</tr>
</thead>
<tbody>
<%= render "user_rows", members: @member_view_models %>
</tbody>
</table>
</div>
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<h2>
<%= link_to current_organization.name,
organization_path(current_organization) %>
</h2>
<div class="table-responsive">
<table class="table table-hover table-condensed users panel">
<thead>
<tr></tr>
<tr>
<th></th>
<th>
<%= sort_link @search, :member_uid, 'ID' %>
</th>
<th>
<%= sort_link @search, :user_username, User.human_attribute_name(:username) %>
</th>
<th>
<%= sort_link @search, :user_email, User.human_attribute_name(:email) %>
</th>
<th>
<%= User.human_attribute_name(:phone) %>
</th>
<th>
<%= User.human_attribute_name(:alt_phone) %>
</th>
<th>
<%= sort_link @search, 'account_balance', Account.human_attribute_name(:balance) %>
</th>
<% if current_user.manages? current_organization %>
<th>
<span class="glyphicon glyphicon-hand-down"></span>
<%= t(".actions") %>
</th>
<% end %>
</tr>
</thead>
<tbody>
<%= render "user_rows", members: @member_view_models %>
</tbody>
</table>
<%= paginate @members %>
<%= paginate @members %>
</div>
</div>
</div>
</div>
+3
View File
@@ -32,5 +32,8 @@ module Timeoverflow
# Activate the Skylight agent in staging. You need to provision the
# SKYLIGHT_AUTHENTICATION env var for this to work.
config.skylight.environments += ["staging"]
# ActiveJob configuration
config.active_job.queue_adapter = :sidekiq
end
end
+3 -3
View File
@@ -12,9 +12,10 @@
defaults: &defaults
adapter: postgresql
host: <%= ENV['DATABASE_HOST'] %> # default is localhost
username: <%= ENV['DATABASE_USER'] %> # default is null
password: <%= ENV['DATABASE_PASSWORD'] %> # default is null
collation: 'es_ES.UTF-8'
ctype: 'es_ES.UTF-8'
template: 'template0'
encoding: utf8
development:
@@ -23,7 +24,6 @@ development:
test:
<<: *defaults
host: localhost
database: 'timeoverflow_test'
staging:
+3
View File
@@ -44,4 +44,7 @@ Rails.application.configure do
# Avoid seeing all that stuff in tests
config.log_level = :warn
# ActiveJob configuration
config.active_job.queue_adapter = :test
end
+5
View File
@@ -0,0 +1,5 @@
schedule_file = 'config/schedule.yml'
if File.exist?(schedule_file) && Sidekiq.server?
Sidekiq::Cron::Job.load_from_hash YAML.load_file(schedule_file)
end
+1
View File
@@ -499,6 +499,7 @@ ca:
give_time:
give_time: Donar Temps a
index:
members: Membres
actions: Accions
active_warning: Estàs a punt de canviar l'estat de l'usuari %{username}
active_warning_angular: Vas a canviar l'estat del compte de l'usuari {{username}}
+1
View File
@@ -499,6 +499,7 @@ en:
give_time:
give_time: Give time to
index:
members: Members
actions: Actions
active_warning: You are going to change user account status for %{username}
active_warning_angular: You are going to change user account status for {{username}}
+1
View File
@@ -499,6 +499,7 @@ es:
give_time:
give_time: Dar Tiempo a
index:
members: Miembros
actions: Acciones
active_warning: Estás a punto de cambiar el estado del usuario %{username}
active_warning_angular: Va a cambiar el estado de la cuenta del usuario {{username}}
+7
View File
@@ -1,6 +1,13 @@
require 'sidekiq/web'
require 'sidekiq/cron/web'
Rails.application.routes.draw do
root to: "home#index"
authenticate :user, lambda { |u| Rails.env.development? || u.superadmin? } do
mount Sidekiq::Web => '/sidekiq'
end
devise_for :users, controllers: { sessions: "sessions" }
devise_scope :user do
+3
View File
@@ -0,0 +1,3 @@
:queues:
- default
- cron
@@ -0,0 +1,43 @@
# This migration does not use Rails ActiveRecord ORM DSL since
# it doesn't provide data integrity (foreign key) for polymorphic models.
#
# The technique applied is called Exclusive Belongs To (AKA Exclusive Arc)
#
# Please read the following article for more details:
# https://hashrocket.com/blog/posts/modeling-polymorphic-associations-in-a-relational-database
#
class CreateEvents < ActiveRecord::Migration
def up
create_table :events do |t|
t.integer :action, null:false
t.integer :post_id
t.integer :member_id
t.integer :transfer_id
t.timestamps
end
add_foreign_key :events, :posts, name: 'events_post_id_fkey'
add_foreign_key :events, :members, name: 'events_member_id_fkey'
add_foreign_key :events, :transfers, name: 'events_transfer_id_fkey'
add_index :events, :post_id, unique: true, where: 'post_id IS NOT NULL'
add_index :events, :member_id, unique: true, where: 'member_id IS NOT NULL'
add_index :events, :transfer_id, unique: true, where: 'transfer_id IS NOT NULL'
execute <<-SQL
ALTER TABLE events
ADD CHECK(action IN (0, 1)),
ADD CHECK(
(
(post_id IS NOT NULL)::integer +
(member_id IS NOT NULL)::integer +
(transfer_id IS NOT NULL)::integer
) = 1
);
SQL
end
def down
drop_table :events
end
end
@@ -0,0 +1,11 @@
class ChangeOrganizationsNameConstraints < ActiveRecord::Migration
def up
change_column_null :organizations, :name, false
add_index :organizations, :name, unique: true
end
def down
change_column_null :organizations, :name, true
remove_index :organizations, :name
end
end
@@ -0,0 +1,14 @@
class CreatePushNotifications < ActiveRecord::Migration
def change
create_table :push_notifications do |t|
t.references :event, null: false
t.references :device_token, null: false
t.datetime :processed_at
t.timestamps null: false
end
add_foreign_key :push_notifications, :events
add_foreign_key :push_notifications, :device_tokens
end
end
@@ -0,0 +1,9 @@
class RemovePasswordDigestFromUser < ActiveRecord::Migration
def up
remove_column :users, :password_digest
end
def down
add_column :users, :password_digest, :string
end
end
@@ -0,0 +1,11 @@
class ChangeIndexOnEvents < ActiveRecord::Migration
def change
remove_index :events, :post_id
remove_index :events, :member_id
remove_index :events, :transfer_id
add_index :events, :post_id, where: 'post_id IS NOT NULL'
add_index :events, :member_id, where: 'member_id IS NOT NULL'
add_index :events, :transfer_id, where: 'transfer_id IS NOT NULL'
end
end
+30 -3
View File
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20180221161343) do
ActiveRecord::Schema.define(version: 20180529144243) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -76,6 +76,19 @@ ActiveRecord::Schema.define(version: 20180221161343) do
add_index "documents", ["documentable_id", "documentable_type"], name: "index_documents_on_documentable_id_and_documentable_type", using: :btree
add_index "documents", ["label"], name: "index_documents_on_label", using: :btree
create_table "events", force: :cascade do |t|
t.integer "action", null: false
t.integer "post_id"
t.integer "member_id"
t.integer "transfer_id"
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "events", ["member_id"], name: "index_events_on_member_id", where: "(member_id IS NOT NULL)", using: :btree
add_index "events", ["post_id"], name: "index_events_on_post_id", where: "(post_id IS NOT NULL)", using: :btree
add_index "events", ["transfer_id"], name: "index_events_on_transfer_id", where: "(transfer_id IS NOT NULL)", using: :btree
create_table "members", force: :cascade do |t|
t.integer "user_id"
t.integer "organization_id"
@@ -102,7 +115,7 @@ ActiveRecord::Schema.define(version: 20180221161343) do
add_index "movements", ["transfer_id"], name: "index_movements_on_transfer_id", using: :btree
create_table "organizations", force: :cascade do |t|
t.string "name"
t.string "name", limit: 255, null: false
t.datetime "created_at"
t.datetime "updated_at"
t.integer "reg_number_seq"
@@ -118,6 +131,8 @@ ActiveRecord::Schema.define(version: 20180221161343) do
t.string "domain"
end
add_index "organizations", ["name"], name: "index_organizations_on_name", unique: true, using: :btree
create_table "posts", force: :cascade do |t|
t.string "title"
t.string "type"
@@ -143,6 +158,14 @@ ActiveRecord::Schema.define(version: 20180221161343) do
add_index "posts", ["tags"], name: "index_posts_on_tags", using: :gin
add_index "posts", ["user_id"], name: "index_posts_on_user_id", using: :btree
create_table "push_notifications", force: :cascade do |t|
t.integer "event_id", null: false
t.integer "device_token_id", null: false
t.datetime "processed_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "transfers", force: :cascade do |t|
t.integer "post_id"
t.text "reason"
@@ -162,7 +185,6 @@ ActiveRecord::Schema.define(version: 20180221161343) do
create_table "users", force: :cascade do |t|
t.string "username", null: false
t.string "email", null: false
t.string "password_digest"
t.date "date_of_birth"
t.string "identity_document"
t.string "phone"
@@ -198,4 +220,9 @@ ActiveRecord::Schema.define(version: 20180221161343) do
add_index "users", ["email"], name: "index_users_on_email", using: :btree
add_foreign_key "accounts", "organizations"
add_foreign_key "events", "members", name: "events_member_id_fkey"
add_foreign_key "events", "posts", name: "events_post_id_fkey"
add_foreign_key "events", "transfers", name: "events_transfer_id_fkey"
add_foreign_key "push_notifications", "device_tokens"
add_foreign_key "push_notifications", "events"
end
@@ -0,0 +1,2 @@
Fabricator(:device_token) do
end
+2
View File
@@ -0,0 +1,2 @@
Fabricator(:event) do
end
+5
View File
@@ -0,0 +1,5 @@
Fabricator(:transfer) do
source { Fabricate(:account) }
destination { Fabricate(:account) }
amount 10
end
@@ -0,0 +1,31 @@
require 'spec_helper'
RSpec.describe CreatePushNotificationsJob, type: :job do
describe '#perform' do
context 'with an Event that doesn\'t exist' do
let(:event_id) { nil }
it 'raises an error' do
expect {
described_class.new.perform(event_id: event_id)
}.to raise_error 'A valid Event must be provided'
end
end
context 'with an Event that does exist' do
let(:post) { Fabricate(:post) }
let(:event) { Fabricate(:event, post: post, action: :created) }
let(:event_id) { event.id }
it 'calls the PushNotification creator' do
creator = instance_double(::PushNotifications::Creator)
expect(::PushNotifications::Creator).to receive(:new)
.with(event: event)
.and_return(creator)
expect(creator).to receive(:create!)
described_class.new.perform(event_id: event_id)
end
end
end
end
+49
View File
@@ -0,0 +1,49 @@
require 'spec_helper'
describe Event do
describe 'Validations' do
it { is_expected.to validate_presence_of(:action) }
it do
is_expected.to define_enum_for(:action)
.with([:created, :updated])
end
describe '#resource_presence validation' do
let(:post) { Fabricate(:post) }
let(:member) { Fabricate(:member) }
let(:event) { Event.new action: 'created' }
context 'has no resources' do
it { expect(event).to_not be_valid }
end
context 'has one resource' do
before { event.post = post }
it { expect(event).to be_valid }
end
context 'has two resources' do
before { event.post = post; event.member = member }
it { expect(event).to_not be_valid }
end
end
end
describe 'Relations' do
it { is_expected.to belong_to(:post) }
it { is_expected.to belong_to(:member) }
it { is_expected.to belong_to(:transfer) }
it { is_expected.to have_db_column(:post_id) }
it { is_expected.to have_db_column(:member_id) }
it { is_expected.to have_db_column(:transfer_id) }
end
describe 'Indexes' do
it { is_expected.to have_db_index(:post_id) }
it { is_expected.to have_db_index(:member_id) }
it { is_expected.to have_db_index(:transfer_id) }
end
end
+42 -29
View File
@@ -17,35 +17,48 @@ describe Organization do
end
end
it "1: without http & https" do
organization.web = "www.casa.com"
expect(organization).to be_valid
expect(organization.web).to eq "http://www.casa.com"
describe 'ensure_url validation' do
it "without http & https" do
organization.web = "www.casa.com"
expect(organization).to be_valid
expect(organization.web).to eq "http://www.casa.com"
end
it "with http" do
organization.web = "http://www.casa.com"
expect(organization).to be_valid
expect(organization.web).to eq "http://www.casa.com"
end
it "with https" do
organization.web = "https://www.casa.com"
expect(organization).to be_valid
expect(organization.web).to eq "https://www.casa.com"
end
it "with blank value" do
organization.web = ""
expect(organization).to be_valid
expect(organization.web).to eq ""
end
it "with nil value" do
organization.web = nil
expect(organization).to be_valid
expect(organization.web).to eq nil
end
it "with an invalid" do
organization.web = "la casa"
expect(organization).not_to be_valid
expect(organization.web).to eq "la casa"
expect(organization.errors.size).to eq 1
end
end
it "2: with http" do
organization.web = "http://www.casa.com"
expect(organization).to be_valid
expect(organization.web).to eq "http://www.casa.com"
end
it "3: with https" do
organization.web = "https://www.casa.com"
expect(organization).to be_valid
expect(organization.web).to eq "https://www.casa.com"
end
it "4: blank" do
organization.web = ""
expect(organization).to be_valid
expect(organization.web).to eq ""
end
it "5: nil" do
organization.web = ""
expect(organization).to be_valid
expect(organization.web).to eq ""
end
it "6: no url" do
organization.web = "la casa"
expect(organization).not_to be_valid
expect(organization.web).to eq "la casa"
expect(organization.errors.size).to eq 1
it 'name is mandatory' do
organization.name = nil
organization.save
expect(organization.errors[:name]).to include(I18n.t('errors.messages.blank'))
end
end
+16
View File
@@ -0,0 +1,16 @@
require 'spec_helper'
RSpec.describe PushNotification do
describe 'Validations' do
it { is_expected.to validate_presence_of(:event) }
it { is_expected.to validate_presence_of(:device_token) }
end
describe 'Associations' do
it { is_expected.to belong_to(:event).with_foreign_key('event_id') }
it { is_expected.to belong_to(:device_token).with_foreign_key('device_token_id') }
it { is_expected.to have_db_column(:event_id) }
it { is_expected.to have_db_column(:device_token_id) }
end
end
@@ -3,15 +3,32 @@ require 'spec_helper'
describe Persister::MemberPersister do
let(:organization) { Fabricate(:organization) }
let(:user) { Fabricate(:user) }
let(:member) { Fabricate.build(:member, user: user, organization: organization) }
let(:persister) { ::Persister::MemberPersister.new(member) }
describe '#save' do
before { persister.save }
it 'saves the member' do
member = Member.new(user: user, organization: organization)
persister = ::Persister::MemberPersister.new(member)
persister.save
expect(member).to be_persisted
end
# TODO: write better expectation
it 'creates an event' do
expect(Event.where(member_id: member.id).first.action).to eq('created')
end
end
describe '#update_attributes' do
before { persister.update_attributes(member_uid: 666) }
it 'updates the resource attributes' do
expect(member.member_uid).to eq(666)
end
# TODO: write better expectation
it 'creates an event' do
expect(Event.where(member_id: member.id).first.action).to eq('updated')
end
end
end
+49 -7
View File
@@ -1,28 +1,70 @@
require 'spec_helper'
describe Persister::PostPersister do
RSpec.describe Persister::PostPersister do
let(:organization) { Fabricate(:organization) }
let(:user) { Fabricate(:user) }
let(:category) { Fabricate(:category) }
let(:post) { Fabricate(:post, organization: organization) }
let(:post) do
Fabricate.build(
:offer,
organization: organization,
user: user,
category: category,
title: 'Title'
)
end
let(:persister) { ::Persister::PostPersister.new(post) }
let(:event) { Fabricate.build(:event, id: 27) }
describe '#save' do
it 'saves the post' do
post = Offer.new(organization: organization, user: user, category: category, title: 'Title')
persister = ::Persister::PostPersister.new(post)
persister.save
expect(post).to be_persisted
end
it 'creates an event' do
expect(::Event).to receive(:create!).with(action: :created, post: post).and_return(event)
persister.save
end
context 'background job' do
before do
allow(::Event).to receive(:create!).and_return(event)
end
it 'enqueues a CreatePushNotificationsJob background job' do
expect {
persister.save
}.to enqueue_job(CreatePushNotificationsJob).with(event_id: 27)
end
end
end
describe '#update_attributes' do
it 'updates the attributes' do
persister = ::Persister::PostPersister.new(post)
it 'updates the resource attributes' do
persister.update_attributes(title: 'New title')
expect(post.title).to eq('New title')
end
it 'creates an event' do
expect(::Event).to receive(:create!).with(action: :updated, post: post).and_return(event)
persister.update_attributes(title: 'New title')
end
context 'background job' do
before do
allow(::Event).to receive(:create!).and_return(event)
end
it 'enqueues a CreatePushNotificationsJob background job' do
expect {
persister.update_attributes(title: 'New title')
}.to enqueue_job(CreatePushNotificationsJob).with(event_id: 27)
end
end
end
end
@@ -5,15 +5,39 @@ describe Persister::TransferPersister do
let(:destination_account) { Fabricate(:account) }
let(:organization) { Fabricate(:organization) }
let(:post) { Fabricate(:post, organization: organization) }
let(:transfer) do
Fabricate.build(
:transfer,
post: post,
source: source_account,
destination: destination_account
)
end
let(:persister) { ::Persister::TransferPersister.new(transfer) }
describe '#save' do
before { persister.save }
it 'saves the transfer' do
transfer = Transfer.new(post: post, source: source_account, destination: destination_account)
persister = ::Persister::TransferPersister.new(transfer)
persister.save
expect(transfer).to be_persisted
end
# TODO: write better expectation
it 'creates an event' do
expect(Event.where(transfer_id: transfer.id).first.action).to eq('created')
end
end
describe '#update_attributes' do
before { persister.update_attributes(amount: 666) }
it 'updates the resource attributes' do
expect(transfer.amount).to eq(666)
end
# TODO: write better expectation
it 'creates an event' do
expect(Event.where(transfer_id: transfer.id).first.action).to eq('updated')
end
end
end
@@ -0,0 +1,25 @@
require 'spec_helper'
RSpec.describe PushNotifications::Creator do
let(:user) { Fabricate(:user) }
let!(:device_token) { Fabricate(:device_token, user: user, token: 'aloha') }
let(:organization) { Fabricate(:organization) }
let(:post) { Fabricate(:post, organization: organization, user: user) }
let(:event) { Fabricate.build(:event, post: post, action: :created) }
let(:creator) { described_class.new(event: event) }
before do
organization.members.create(user: user)
end
describe '#create!' do
it 'creates as many PushNotification resources as needed' do
expect(PushNotification).to receive(:create!).with(
event: event,
device_token: device_token
).once
creator.create!
end
end
end
@@ -0,0 +1,37 @@
require 'spec_helper'
RSpec.describe ::PushNotifications::EventNotifier::Post do
let(:user) { Fabricate.build(:user) }
let(:organization) { Fabricate(:organization) }
let(:post) { Fabricate(:post, organization: organization, user: user) }
let(:event) { Fabricate.build(:event, post: post, action: :created) }
describe '#users' do
context 'when a user pertains to the Post\'s organization' do
before { organization.members.create(user: user) }
context 'and the user has a DeviceToken associated' do
let!(:device_token) { Fabricate(:device_token, user: user, token: 'aloha') }
it 'returns the device token associated with the user' do
expect(described_class.new(event: event).device_tokens).to eq([device_token])
end
end
context 'but the user has no DeviceToken associated' do
it 'doesn\'t return the user' do
expect(described_class.new(event: event).device_tokens).to eq([])
end
end
end
context 'when a user doesn\'t pertain to the Post\'s organization' do
let(:other_user) { Fabricate(:user) }
let!(:device_token) { Fabricate(:device_token, user: other_user, token: 'WAT') }
it 'doesn\'t return the user' do
expect(described_class.new(event: event).device_tokens).to_not include(device_token)
end
end
end
end
@@ -0,0 +1,31 @@
require 'spec_helper'
RSpec.describe PushNotifications::EventNotifierFactory do
describe '#build' do
let(:user) { Fabricate.build(:user) }
let(:organization) { Fabricate(:organization) }
let(:factory) { described_class.new(event: event) }
context 'when the given Event is associated to a Post' do
let(:post) { Fabricate(:post, organization: organization, user: user) }
let(:event) { Fabricate.build(:event, post: post, action: :created) }
it 'returns a Post notifier' do
notifier = instance_double(::PushNotifications::EventNotifier::Post)
allow(::PushNotifications::EventNotifier::Post).to receive(:new).and_return(notifier)
expect(factory.build).to be(notifier)
end
end
context 'when the given Event is associated to a resource not supported' do
let(:event) { Fabricate.build(:event, action: :created) }
it 'raises an error' do
expect {
factory.build
}.to raise_error('The resource associated to the Event is not supported')
end
end
end
end
+8
View File
@@ -10,6 +10,7 @@ require 'database_cleaner'
require 'fabrication'
require 'selenium/webdriver'
require 'faker'
require 'shoulda/matchers'
I18n.reload!
Capybara.register_driver :chrome do |app|
@@ -149,3 +150,10 @@ RSpec.shared_context 'stub browser locale' do
end
RSpec.configure(&:infer_spec_type_from_file_location!)
Shoulda::Matchers.configure do |config|
config.integrate do |with|
with.test_framework :rspec
with.library :rails
end
end