acts_as_holdable 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +156 -0
- data/Rakefile +34 -0
- data/app/assets/config/acts_as_booked_manifest.js +0 -0
- data/config/locales/en.yml +10 -0
- data/config/locales/es.yml +10 -0
- data/config/routes.rb +2 -0
- data/db/migrate/20180530211641_acts_as_holdable_holdings.rb +12 -0
- data/lib/acts_as_holdable.rb +40 -0
- data/lib/acts_as_holdable/db_utils.rb +32 -0
- data/lib/acts_as_holdable/engine.rb +6 -0
- data/lib/acts_as_holdable/holdable.rb +44 -0
- data/lib/acts_as_holdable/holdable/core.rb +129 -0
- data/lib/acts_as_holdable/holder.rb +75 -0
- data/lib/acts_as_holdable/holding.rb +36 -0
- data/lib/acts_as_holdable/t.rb +13 -0
- data/lib/acts_as_holdable/version.rb +5 -0
- data/lib/jobs/unhold_job.rb +16 -0
- data/lib/tasks/acts_as_booked_tasks.rake +6 -0
- data/lib/tasks/acts_as_holdable_tasks.rake +6 -0
- metadata +203 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ab95215af6bfe8d8811807c536ca35ca837a7c83d030c087986fd4d78fb37716
|
4
|
+
data.tar.gz: 548d98f061d57a4c7e12cf3a71cefa9808c0109e793c124d06747a52dfacd073
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f43dd7e5ff2f853233b100bcc814615faad4ade99b49b5a4107e3a65c7caf1a73e2d99111ba786f7ea144098db761e92c7f345ba19a3655ae8a5fa6730bb0641
|
7
|
+
data.tar.gz: 54f10fd2c304f946f6bcab51aff8fced9c88842e44873e47611a9dbc44ad5de98b4d0479a1ddc094fa6565d7fb0cd06dbcfdf4092b4a5449a45535ebe50d6bed
|
data/README.md
ADDED
@@ -0,0 +1,156 @@
|
|
1
|
+
# ActsAsHoldable
|
2
|
+
ActsAsHoldable allows resources to be held by users. It:
|
3
|
+
|
4
|
+
* Is a MVC solution based on Rails engines
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'acts_as_holdable'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
```bash
|
15
|
+
$ bundle
|
16
|
+
```
|
17
|
+
|
18
|
+
Or install it yourself as:
|
19
|
+
```bash
|
20
|
+
$ gem install acts_as_holdable
|
21
|
+
```
|
22
|
+
|
23
|
+
## Migrations
|
24
|
+
|
25
|
+
```bash
|
26
|
+
bundle exec rake acts_as_holdable_engine:install:migrations
|
27
|
+
```
|
28
|
+
|
29
|
+
## Holdables, Holders and Holdings
|
30
|
+
|
31
|
+
To set-up a Holdable model, use acts_as_holdable. A Holdable model is enabled to be held.
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
class Ticket < ApplicationRecord
|
35
|
+
acts_as_holdable
|
36
|
+
end
|
37
|
+
```
|
38
|
+
|
39
|
+
To set-up a Holder model, use acts_as_holder. Only Holders can create holdings.
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
class User < ApplicationRecord
|
43
|
+
acts_as_holder
|
44
|
+
end
|
45
|
+
```
|
46
|
+
|
47
|
+
From this time on, a User can hold a Ticket with
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
@user.hold! @ticket
|
51
|
+
```
|
52
|
+
|
53
|
+
A User can unhold a Ticket with
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
@holding = @user.hold! @ticket
|
57
|
+
@user.unhold! @holding
|
58
|
+
```
|
59
|
+
|
60
|
+
You can access holdings both from the Holdable and the Holder
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
@ticket.holdings # return all holdings created on this ticket
|
64
|
+
@user.holdings # return all holdings made by this user
|
65
|
+
```
|
66
|
+
|
67
|
+
## Configuring Options
|
68
|
+
|
69
|
+
There are a number available options to make your models behave differently. They are all configurable in the Holdable model, passing a hash to `acts_as_holdable`
|
70
|
+
|
71
|
+
Available options (with values) are:
|
72
|
+
|
73
|
+
* `:on_hand_type`: Specifies how the `amount` of a holding (e.g. number of tickets from an event) affects the future availability of the holdable. Allowed values are:
|
74
|
+
* `:none`
|
75
|
+
* `:open`
|
76
|
+
|
77
|
+
### No constraints
|
78
|
+
|
79
|
+
The model accepts holdings without any constraint. This means every holder can create an infinite number of holdings on it and no capacity or time checks are performed.
|
80
|
+
|
81
|
+
Creating a holding on this model means holding it forever and without care for other existing holdings. In other words, the number of holdings do not affect the availability of this holdable. (e.g. pre-ordering a product that will be released soon)
|
82
|
+
|
83
|
+
### Capacity constraints
|
84
|
+
|
85
|
+
The option `on_hand_type` may be used to set a constraint over the `amount` attribute of the holding
|
86
|
+
|
87
|
+
#### No capacity constraints - `on_hand_type: :none`
|
88
|
+
|
89
|
+
The model is holdable without capacity constraints.
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
class Product < ActiveRecord::Base
|
93
|
+
# As `on_hand_type: :none` is a default and can be ommited
|
94
|
+
acts_as_holdable on_hand_type: :none
|
95
|
+
end
|
96
|
+
```
|
97
|
+
|
98
|
+
#### Open capacity - `on_hand_type: :open`
|
99
|
+
|
100
|
+
> WARNING - **migration needed!** - with this option the model must have an attribute `on_hand: :integer`
|
101
|
+
|
102
|
+
The model is holdable until its `capacity` is reached. (e.g. an event't tickets types)
|
103
|
+
|
104
|
+
**Configuration**
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
class Event < ActiveRecord::Base
|
108
|
+
acts_as_holdable on_hand_type: :open
|
109
|
+
end
|
110
|
+
```
|
111
|
+
|
112
|
+
**Creating a new holdable**
|
113
|
+
|
114
|
+
Each instance of the model must define its capacity.
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
@ticket = Ticket.new(...)
|
118
|
+
@ticket.on_hand = 30 # This event allows 30 tickets
|
119
|
+
@ticket.save!
|
120
|
+
```
|
121
|
+
|
122
|
+
**Holding**
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
# Holding a model with `on_hand_type: :open` requires `:amount`
|
126
|
+
|
127
|
+
@user1.hold! @ticket, amount: 5 # holding tickets for 5 people, OK
|
128
|
+
@user2.hold! @ticket, amount: 20 # holding tickets for other 20 people, OK
|
129
|
+
@user3.hold! @ticket, amount: 10 # overholding! raise ActsAsHoldable::AvailabilityError
|
130
|
+
```
|
131
|
+
|
132
|
+
## Holding resources on a span of time
|
133
|
+
|
134
|
+
Holdings can be created to be 'living' within a span of time, if the holding doesn't gets confirmed within the time span, it will be destroyed.
|
135
|
+
|
136
|
+
|
137
|
+
```ruby
|
138
|
+
@holding = @user.hold_for(@ticket, duration: 10.minutes, amount: 1)
|
139
|
+
```
|
140
|
+
|
141
|
+
A User can confirm a holding with
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
@user.confirm_holding!(@holding)
|
145
|
+
```
|
146
|
+
|
147
|
+
|
148
|
+
## Contributing
|
149
|
+
Contribution directions go here.
|
150
|
+
|
151
|
+
## License
|
152
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
153
|
+
|
154
|
+
### Acknowledgements
|
155
|
+
|
156
|
+
To speed-up the initialization process of this project, the structure of this repository was strongly influenced by ActsAsBookable by Tandù srl.
|
data/Rakefile
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'bundler/setup'
|
5
|
+
rescue LoadError
|
6
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'rdoc/task'
|
10
|
+
|
11
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
12
|
+
rdoc.rdoc_dir = 'rdoc'
|
13
|
+
rdoc.title = 'ActsAsHoldable'
|
14
|
+
rdoc.options << '--line-numbers'
|
15
|
+
rdoc.rdoc_files.include('README.md')
|
16
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
17
|
+
end
|
18
|
+
|
19
|
+
APP_RAKEFILE = File.expand_path('test/dummy/Rakefile', __dir__)
|
20
|
+
load 'rails/tasks/engine.rake'
|
21
|
+
|
22
|
+
load 'rails/tasks/statistics.rake'
|
23
|
+
|
24
|
+
require 'bundler/gem_tasks'
|
25
|
+
|
26
|
+
require 'rake/testtask'
|
27
|
+
|
28
|
+
Rake::TestTask.new(:test) do |t|
|
29
|
+
t.libs << 'test'
|
30
|
+
t.pattern = 'test/**/*_test.rb'
|
31
|
+
t.verbose = false
|
32
|
+
end
|
33
|
+
|
34
|
+
task default: :test
|
File without changes
|
@@ -0,0 +1,10 @@
|
|
1
|
+
en:
|
2
|
+
acts_as_holdable:
|
3
|
+
errors:
|
4
|
+
messages:
|
5
|
+
holding:
|
6
|
+
must_be_holdable: "cannot hold a %{model} as it\'s not holdable"
|
7
|
+
must_be_holder: "a %{model} cannot hold a resource, because %{model} is not a holder"
|
8
|
+
availability:
|
9
|
+
amount_gt_on_hand: "amount cannot be greater than %{model} on_hand"
|
10
|
+
already_held: "the %{model} is full"
|
@@ -0,0 +1,10 @@
|
|
1
|
+
es-MX:
|
2
|
+
acts_as_holdable:
|
3
|
+
errors:
|
4
|
+
messages:
|
5
|
+
holding:
|
6
|
+
must_be_holdable: "No se puede apartar %{model} ya que no es apartable"
|
7
|
+
must_be_holder: "%{model} no puede apartar un recurso, porque %{model} no es un agente de apartados"
|
8
|
+
availability:
|
9
|
+
amount_gt_on_hand: "cantidad no puede ser mayor que la capacidad de %{model}"
|
10
|
+
already_held: "%{model} está totalmente apartado"
|
data/config/routes.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
class ActsAsHoldableHoldings < ActiveRecord::Migration[5.2]
|
2
|
+
def change
|
3
|
+
create_table :acts_as_holdable_holdings, force: true do |t|
|
4
|
+
t.references :holdable, polymorphic: true, index: { name: "index_acts_as_holdable_holdings_holdable" }
|
5
|
+
t.references :holder, polymorphic: true, index: { name: "index_acts_as_holdable_holdings_holder" }
|
6
|
+
t.column :amount, :integer
|
7
|
+
t.column :job_pid, :string
|
8
|
+
|
9
|
+
t.timestamps
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_record'
|
4
|
+
require 'acts_as_holdable/engine'
|
5
|
+
require 'active_job'
|
6
|
+
require 'sidekiq-status'
|
7
|
+
|
8
|
+
module ActsAsHoldable
|
9
|
+
extend ActiveSupport::Autoload
|
10
|
+
|
11
|
+
autoload :T
|
12
|
+
autoload :Holdable
|
13
|
+
autoload :Holder
|
14
|
+
autoload :Holding
|
15
|
+
autoload :DbUtils
|
16
|
+
|
17
|
+
autoload_under 'holdable' do
|
18
|
+
autoload :Core
|
19
|
+
end
|
20
|
+
|
21
|
+
class InitializationError < StandardError
|
22
|
+
def initialize(model, message)
|
23
|
+
super "Error initializing acts_as_holdable on #{model} - " + message
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class OptionsInvalid < StandardError
|
28
|
+
def initialize(model, message)
|
29
|
+
super "Error validating options for #{model} - " + message
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class AvailabilityError < StandardError
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
ActiveSupport.on_load(:active_record) do
|
38
|
+
extend ActsAsHoldable::Holdable
|
39
|
+
include ActsAsHoldable::Holder
|
40
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActsAsHoldable
|
4
|
+
module DBUtils
|
5
|
+
class << self
|
6
|
+
def connection
|
7
|
+
ActsAsHoldable::Holding.connection
|
8
|
+
end
|
9
|
+
|
10
|
+
def using_postgresql?
|
11
|
+
connection && connection.adapter_name == 'PostgreSQL'
|
12
|
+
end
|
13
|
+
|
14
|
+
def using_mysql?
|
15
|
+
connection && connection.adapter_name == 'Mysql2'
|
16
|
+
end
|
17
|
+
|
18
|
+
def using_sqlite?
|
19
|
+
connection && connection.adapter_name == 'SQLite'
|
20
|
+
end
|
21
|
+
|
22
|
+
def like_operator
|
23
|
+
using_postgresql? ? 'ILIKE' : 'LIKE'
|
24
|
+
end
|
25
|
+
|
26
|
+
# escape _ and % characters in strings, since these are wildcards in SQL.
|
27
|
+
def escape_like(str)
|
28
|
+
str.gsub(/[!%_]/) { |x| '!' + x }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActsAsHoldable
|
4
|
+
module Holdable
|
5
|
+
def holdable?
|
6
|
+
false
|
7
|
+
end
|
8
|
+
|
9
|
+
def acts_as_holdable(options = {})
|
10
|
+
holdable(options)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
# rubocop:disable Metrics/MethodLength
|
16
|
+
def holdable(options)
|
17
|
+
if holdable?
|
18
|
+
self.holding_opts = options
|
19
|
+
else
|
20
|
+
class_attribute :holding_opts
|
21
|
+
self.holding_opts = options
|
22
|
+
|
23
|
+
class_eval do
|
24
|
+
has_many :holdings, as: :holdable, dependent: :destroy, class_name: '::ActsAsHoldable::Holding'
|
25
|
+
|
26
|
+
validates :on_hand, presence: true, if: :on_hand_required?,
|
27
|
+
numericality: { only_integer: true,
|
28
|
+
greater_than_or_equal_to: 0 }
|
29
|
+
|
30
|
+
def self.holdable?
|
31
|
+
true
|
32
|
+
end
|
33
|
+
|
34
|
+
def on_hand_required?
|
35
|
+
holding_opts && holding_opts[:on_hand_type] != :none
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
include Core
|
41
|
+
end
|
42
|
+
# rubocop:enable Metrics/MethodLength
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActsAsHoldable
|
4
|
+
module Holdable
|
5
|
+
module Core
|
6
|
+
def self.included(base)
|
7
|
+
base.extend ActsAsHoldable::Holdable::Core::ClassMethods
|
8
|
+
base.include ActsAsHoldable::Holdable::Core::InstanceMethods
|
9
|
+
|
10
|
+
base.initialize_acts_as_holdable_core
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
def initialize_acts_as_holdable_core
|
15
|
+
set_options
|
16
|
+
end
|
17
|
+
|
18
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
19
|
+
def validate_holding_options!(options)
|
20
|
+
unpermitted_params = []
|
21
|
+
required_params = {}
|
22
|
+
|
23
|
+
case holding_opts[:on_hand_type]
|
24
|
+
when :closed
|
25
|
+
required_params[:amount] = [Integer]
|
26
|
+
when :open
|
27
|
+
required_params[:amount] = [Integer]
|
28
|
+
when :none
|
29
|
+
unpermitted_params << :amount
|
30
|
+
end
|
31
|
+
|
32
|
+
# Actual validation
|
33
|
+
unpermitted_params = unpermitted_params.select { |p| options.key?(p) }
|
34
|
+
.map { |p| "'#{p}'" }
|
35
|
+
|
36
|
+
wrong_types = required_params
|
37
|
+
.select { |k, v| options.key?(k) && v.select { |type| options[k].is_a?(type) }.length.zero? }
|
38
|
+
.map { |k, v| "'#{k}' must be a '#{v.join(' or ')}' but '#{options[k].class}' found" }
|
39
|
+
|
40
|
+
required_params = required_params.reject { |k, _v| options.key?(k) }
|
41
|
+
.map { |k, _v| "'#{k}'" }
|
42
|
+
|
43
|
+
# Raise OptionsInvalid if some invalid parameters were found
|
44
|
+
if (unpermitted_params.length + required_params.length + wrong_types.length).positive?
|
45
|
+
message = ''
|
46
|
+
message << " unpermitted parameters: #{unpermitted_params.join(',')}." unless unpermitted_params.empty?
|
47
|
+
message << " missing parameters: #{required_params.join(',')}." unless required_params.empty?
|
48
|
+
message << " parameters type mismatch: #{wrong_types.join(',')}" unless wrong_types.empty?
|
49
|
+
raise ActsAsHoldable::OptionsInvalid.new(self, message)
|
50
|
+
end
|
51
|
+
true
|
52
|
+
end
|
53
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
# rubocop:disable Metrics/MethodLength
|
58
|
+
def set_options
|
59
|
+
holding_opts[:preset] = :ticket
|
60
|
+
|
61
|
+
# Validates options
|
62
|
+
permitted_options = {
|
63
|
+
on_hand_type: %i[open closed none],
|
64
|
+
preset: [:ticket]
|
65
|
+
}
|
66
|
+
|
67
|
+
holding_opts.each_pair do |key, val|
|
68
|
+
# rubocop:disable Style/GuardClause
|
69
|
+
if !permitted_options.key? key
|
70
|
+
raise ActsAsHoldable::InitializationError.new(self, "#{key} is not a valid option")
|
71
|
+
elsif !permitted_options[key].include? val
|
72
|
+
raise ActsAsHoldable::InitializationError.new(self, "#{val} is not a valid value for #{key}. Allowed values: #{permitted_options[key]}")
|
73
|
+
end
|
74
|
+
# rubocop:enable Style/GuardClause
|
75
|
+
end
|
76
|
+
|
77
|
+
defaults = case holding_opts[:preset]
|
78
|
+
when :ticket
|
79
|
+
{
|
80
|
+
on_hand_type: :open
|
81
|
+
}
|
82
|
+
else
|
83
|
+
{
|
84
|
+
on_hand_type: :none
|
85
|
+
}
|
86
|
+
end
|
87
|
+
|
88
|
+
# Merge options with defaults
|
89
|
+
holding_opts.reverse_merge!(defaults)
|
90
|
+
end
|
91
|
+
# rubocop:enable Metrics/MethodLength
|
92
|
+
end
|
93
|
+
|
94
|
+
module InstanceMethods
|
95
|
+
def check_availability!(opts)
|
96
|
+
# validates options
|
97
|
+
validate_holding_options!(opts)
|
98
|
+
|
99
|
+
if holding_opts[:on_hand_type] != :none
|
100
|
+
# Amount > on_hand
|
101
|
+
if opts[:amount] > on_hand
|
102
|
+
raise ActsAsHoldable::AvailabilityError,
|
103
|
+
ActsAsHoldable::T.er('.availability.amount_gt_on_hand', model: self.class.to_s)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
true
|
107
|
+
end
|
108
|
+
|
109
|
+
def check_availability(opts)
|
110
|
+
check_availability!(opts)
|
111
|
+
rescue ActsAsHoldable::AvailabilityError
|
112
|
+
false
|
113
|
+
end
|
114
|
+
|
115
|
+
def hold!(holder, opts)
|
116
|
+
holder.hold(self, opts)
|
117
|
+
end
|
118
|
+
|
119
|
+
def validate_holding_options!(opts)
|
120
|
+
self.class.validate_holding_options!(opts)
|
121
|
+
end
|
122
|
+
|
123
|
+
def holder?
|
124
|
+
self.class.holder?
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'jobs/unhold_job'
|
4
|
+
module ActsAsHoldable
|
5
|
+
module Holder
|
6
|
+
def self.included(base)
|
7
|
+
base.extend ClassMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
def acts_as_holder(_opts = {})
|
12
|
+
class_eval do
|
13
|
+
has_many :holdings, as: :holder, dependent: :destroy, class_name: '::ActsAsHoldable::Holding'
|
14
|
+
end
|
15
|
+
|
16
|
+
include ActsAsHoldable::Holder::InstanceMethods
|
17
|
+
extend ActsAsHoldable::Holder::SingletonMethods
|
18
|
+
end
|
19
|
+
|
20
|
+
def holder?
|
21
|
+
false
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
module InstanceMethods
|
26
|
+
def hold!(holdable, opts = {})
|
27
|
+
# validates availability
|
28
|
+
holdable.check_availability!(opts) if holdable.class.holdable?
|
29
|
+
|
30
|
+
holding_params = opts.merge(holder: self, holdable: holdable)
|
31
|
+
holding = ActsAsHoldable::Holding.create!(holding_params)
|
32
|
+
|
33
|
+
holdable.reload
|
34
|
+
holding
|
35
|
+
end
|
36
|
+
|
37
|
+
def unhold!(holding)
|
38
|
+
# deletes current holding
|
39
|
+
ActsAsHoldable::Holding.destroy(holding.id)
|
40
|
+
reload
|
41
|
+
true
|
42
|
+
end
|
43
|
+
|
44
|
+
def hold_for(holdable, opts = {})
|
45
|
+
# Clean params for Bookings
|
46
|
+
holding_opts = opts.except(:duration)
|
47
|
+
|
48
|
+
# Creates the Booking
|
49
|
+
holding = hold!(holdable, holding_opts)
|
50
|
+
|
51
|
+
# Sets a Job to delete the Booking
|
52
|
+
job = UnholdJob.set(wait: opts[:duration]).perform_later(holding.id)
|
53
|
+
holding.update(job_pid: job.provider_job_id)
|
54
|
+
holding
|
55
|
+
end
|
56
|
+
|
57
|
+
def confirm_holding!(holding)
|
58
|
+
return false unless holding.job_pid
|
59
|
+
Sidekiq::Status.unschedule(holding.job_pid)
|
60
|
+
holding.update(job_pid: nil)
|
61
|
+
holding
|
62
|
+
end
|
63
|
+
|
64
|
+
def holder?
|
65
|
+
self.class.holder?
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
module SingletonMethods
|
70
|
+
def holder?
|
71
|
+
true
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActsAsHoldable
|
4
|
+
# Holding model. Store in host database holdings made by holders on holdables
|
5
|
+
class Holding < ::ActiveRecord::Base
|
6
|
+
self.table_name = 'acts_as_holdable_holdings'
|
7
|
+
|
8
|
+
belongs_to :holdable, polymorphic: true
|
9
|
+
belongs_to :holder, polymorphic: true
|
10
|
+
|
11
|
+
validates_presence_of :holdable
|
12
|
+
validates_presence_of :holder
|
13
|
+
|
14
|
+
validate :holdable_is_holdable,
|
15
|
+
:holder_is_holder
|
16
|
+
|
17
|
+
validates :amount, presence: true, numericality: { only_integer: true,
|
18
|
+
greater_than_or_equal_to: 0 }
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
# Validation method. Check if the holded model is actually holdable
|
23
|
+
def holdable_is_holdable
|
24
|
+
return unless holdable.present? && !holdable.class.holdable?
|
25
|
+
errors.add(:holdable, T.er('holding.must_be_holdable',
|
26
|
+
model: holdable.class.to_s))
|
27
|
+
end
|
28
|
+
|
29
|
+
# Validation method. Check if the holder model is actually a holder
|
30
|
+
def holder_is_holder
|
31
|
+
return unless holder.present? && !holder.class.holder?
|
32
|
+
errors.add(:holder, T.er('holder.must_be_holder',
|
33
|
+
model: holder.class.to_s))
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class UnholdJob < ActiveJob::Base
|
4
|
+
discard_on ActiveRecord::RecordNotFound
|
5
|
+
queue_as :default
|
6
|
+
|
7
|
+
def perform(holding_id)
|
8
|
+
ActsAsHoldable::Holding.destroy(holding_id) if booking_exists?(holding_id)
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def booking_exists?(holding_id)
|
14
|
+
ActsAsHoldable::Holding.find_by(id: holding_id)
|
15
|
+
end
|
16
|
+
end
|
metadata
ADDED
@@ -0,0 +1,203 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: acts_as_holdable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Nora Gabriela Alvarado
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-05-31 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 5.2.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 5.2.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: sidekiq
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: sidekiq-status
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: bump
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: combustion
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: coveralls
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rspec
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rubocop
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: simplecov
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: sqlite3
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
description: Description of ActsAsHoldable.
|
154
|
+
email:
|
155
|
+
- noragmora@gmail.com
|
156
|
+
executables: []
|
157
|
+
extensions: []
|
158
|
+
extra_rdoc_files: []
|
159
|
+
files:
|
160
|
+
- README.md
|
161
|
+
- Rakefile
|
162
|
+
- app/assets/config/acts_as_booked_manifest.js
|
163
|
+
- config/locales/en.yml
|
164
|
+
- config/locales/es.yml
|
165
|
+
- config/routes.rb
|
166
|
+
- db/migrate/20180530211641_acts_as_holdable_holdings.rb
|
167
|
+
- lib/acts_as_holdable.rb
|
168
|
+
- lib/acts_as_holdable/db_utils.rb
|
169
|
+
- lib/acts_as_holdable/engine.rb
|
170
|
+
- lib/acts_as_holdable/holdable.rb
|
171
|
+
- lib/acts_as_holdable/holdable/core.rb
|
172
|
+
- lib/acts_as_holdable/holder.rb
|
173
|
+
- lib/acts_as_holdable/holding.rb
|
174
|
+
- lib/acts_as_holdable/t.rb
|
175
|
+
- lib/acts_as_holdable/version.rb
|
176
|
+
- lib/jobs/unhold_job.rb
|
177
|
+
- lib/tasks/acts_as_booked_tasks.rake
|
178
|
+
- lib/tasks/acts_as_holdable_tasks.rake
|
179
|
+
homepage: https://gitlab.com/aromaron/acts_as_holdable
|
180
|
+
licenses:
|
181
|
+
- MIT
|
182
|
+
metadata: {}
|
183
|
+
post_install_message:
|
184
|
+
rdoc_options: []
|
185
|
+
require_paths:
|
186
|
+
- lib
|
187
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
188
|
+
requirements:
|
189
|
+
- - ">="
|
190
|
+
- !ruby/object:Gem::Version
|
191
|
+
version: '0'
|
192
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
193
|
+
requirements:
|
194
|
+
- - ">="
|
195
|
+
- !ruby/object:Gem::Version
|
196
|
+
version: '0'
|
197
|
+
requirements: []
|
198
|
+
rubyforge_project:
|
199
|
+
rubygems_version: 2.7.6
|
200
|
+
signing_key:
|
201
|
+
specification_version: 4
|
202
|
+
summary: Summary of ActsAsHoldable.
|
203
|
+
test_files: []
|