acts_as_bookable 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +2 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +43 -0
  6. data/Appraisals +21 -0
  7. data/Gemfile +21 -0
  8. data/Gemfile.lock +168 -0
  9. data/Guardfile +5 -0
  10. data/MIT-LICENSE +20 -0
  11. data/README.md +473 -0
  12. data/Rakefile +19 -0
  13. data/acts_as_bookable.gemspec +41 -0
  14. data/app/assets/images/acts_as_bookable/.keep +0 -0
  15. data/app/assets/javascripts/acts_as_bookable/application.js +13 -0
  16. data/app/assets/stylesheets/acts_as_bookable/application.css +15 -0
  17. data/app/controllers/acts_as_bookable/application_controller.rb +4 -0
  18. data/app/helpers/acts_as_bookable/application_helper.rb +4 -0
  19. data/app/views/layouts/acts_as_bookable/application.html.erb +14 -0
  20. data/bin/rails +12 -0
  21. data/config/locales/en.yml +12 -0
  22. data/config/routes.rb +2 -0
  23. data/db/migrate/20160217085200_create_acts_as_bookable_bookings.rb +14 -0
  24. data/gemfiles/activerecord_3.2.gemfile +16 -0
  25. data/gemfiles/activerecord_4.0.gemfile +16 -0
  26. data/gemfiles/activerecord_4.1.gemfile +16 -0
  27. data/gemfiles/activerecord_4.2.gemfile +17 -0
  28. data/gemfiles/activerecord_5.0.gemfile +17 -0
  29. data/lib/acts_as_bookable/bookable/core.rb +285 -0
  30. data/lib/acts_as_bookable/bookable.rb +56 -0
  31. data/lib/acts_as_bookable/booker.rb +70 -0
  32. data/lib/acts_as_bookable/booking.rb +54 -0
  33. data/lib/acts_as_bookable/db_utils.rb +39 -0
  34. data/lib/acts_as_bookable/engine.rb +5 -0
  35. data/lib/acts_as_bookable/t.rb +11 -0
  36. data/lib/acts_as_bookable/time_utils.rb +135 -0
  37. data/lib/acts_as_bookable/version.rb +3 -0
  38. data/lib/acts_as_bookable.rb +42 -0
  39. data/lib/tasks/acts_as_bookable_tasks.rake +4 -0
  40. data/spec/acts_as_bookable/acts_as_bookable_spec.rb +52 -0
  41. data/spec/acts_as_bookable/acts_as_booker_spec.rb +57 -0
  42. data/spec/acts_as_bookable/bookable/core_spec.rb +593 -0
  43. data/spec/acts_as_bookable/bookable_spec.rb +124 -0
  44. data/spec/acts_as_bookable/booker_spec.rb +140 -0
  45. data/spec/acts_as_bookable/booking_spec.rb +241 -0
  46. data/spec/acts_as_bookable/schedule_spec.rb +137 -0
  47. data/spec/acts_as_bookable/time_utils_spec.rb +525 -0
  48. data/spec/factories/bookable.rb +7 -0
  49. data/spec/factories/booker.rb +5 -0
  50. data/spec/factories/room.rb +11 -0
  51. data/spec/internal/app/models/Bookable.rb +3 -0
  52. data/spec/internal/app/models/Booker.rb +3 -0
  53. data/spec/internal/app/models/Event.rb +3 -0
  54. data/spec/internal/app/models/Generic.rb +2 -0
  55. data/spec/internal/app/models/NotBooker.rb +2 -0
  56. data/spec/internal/app/models/Room.rb +3 -0
  57. data/spec/internal/app/models/Show.rb +3 -0
  58. data/spec/internal/app/models/Unbookable.rb +2 -0
  59. data/spec/internal/config/database.yml.sample +17 -0
  60. data/spec/internal/db/schema.rb +51 -0
  61. data/spec/spec_helper.rb +26 -0
  62. data/spec/support/0-helpers.rb +32 -0
  63. data/spec/support/1-database.rb +42 -0
  64. data/spec/support/2-database_cleaner.rb +21 -0
  65. data/spec/support/3-factory_girl.rb +12 -0
  66. metadata +296 -0
@@ -0,0 +1,135 @@
1
+ module ActsAsBookable
2
+ ##
3
+ # Provide helper functions to manage operations and queries related to times
4
+ # and schedules
5
+ #
6
+ module TimeUtils
7
+ class << self
8
+ ##
9
+ # Check if time is included in a time interval. The ending time is excluded
10
+ #
11
+ # @param time The time to check
12
+ # @param interval_start The beginning time of the interval to match against
13
+ # @param interval_end The ending time of the interval to match against
14
+ #
15
+ def time_in_interval? (time, interval_start, interval_end)
16
+ time >= interval_start && time < interval_end
17
+ end
18
+
19
+ ##
20
+ # Check if there is an occurrence of a schedule that contains a time interval
21
+ #
22
+ # @param schedule The schedule
23
+ # @param interval_start The beginning Time of the interval
24
+ # @param interval_end The ending Time of the interval
25
+ # @return true if the interval falls within an occurrence of the schedule, otherwise false
26
+ #
27
+ def interval_in_schedule?(schedule, interval_start, interval_end)
28
+ # Check if interval_start and interval_end falls within any occurrence
29
+ return false if(!time_in_schedule?(schedule,interval_start) || !time_in_schedule?(schedule,interval_end))
30
+
31
+ # Check if both interval_start and interval_end falls within the SAME occurrence
32
+ between = schedule.occurrences_between(interval_start, interval_end, true)
33
+ contains = false
34
+ between.each do |oc|
35
+ oc_end = oc + schedule.duration
36
+ contains = true if (time_in_interval?(interval_start,oc,oc_end) && time_in_interval?(interval_end,oc,oc_end))
37
+ break if contains
38
+ end
39
+
40
+ contains
41
+ end
42
+
43
+ ##
44
+ # Check if there is an occurrence of a schedule that contains a time
45
+ # @param schedule The schedule
46
+ # @param time The time
47
+ # @return true if the time falls within an occurrence of the schedule, otherwise false
48
+ #
49
+ def time_in_schedule?(schedule, time)
50
+ return schedule.occurring_at? time
51
+ end
52
+
53
+ ##
54
+ # Returns an array of sub-intervals given another array of intervals, which are the overlapping insersections of each-others.
55
+ #
56
+ # @param intervals an array of intervals
57
+ # @return an array of subintervals, sorted by time_start
58
+ #
59
+ # An interval is defined as a hash with at least the following fields: `time_from` and `time_end`. An interval may contain more
60
+ # fields. In that case, it's suggested to give a block with the instructions to correctly merge two intervals when needed.
61
+ #
62
+ # e.g: given these 7 intervals
63
+ # |------| |---| |----------|
64
+ # |---| |--|
65
+ # |------| |--| |-------------|
66
+ # the output is an array containing these 8 intervals:
67
+ # |--| |--| |---| |--| |---| |------|
68
+ # |---| |------|
69
+ # the number of subintervals may increase or decrease because some intervals may be split, while
70
+ # some others may be merged.
71
+ #
72
+ # If a block is given, it's called before merging two intervals. The block should provide instructions to merge intervals, and should return the merged fields in a hash
73
+ def subintervals(intervals, &block)
74
+ raise ArgumentError.new('intervals must be an array') unless intervals.is_a? Array
75
+
76
+ steps = [] # Steps will be extracted from intervals
77
+ subintervals = [] # The output
78
+ last_time = nil
79
+ last_attrs = nil
80
+ started_count = 0 # The number of intervals opened inside the cycle
81
+
82
+ # Extract start times and end times from intervals, and create steps
83
+ intervals.each do |el|
84
+ begin
85
+ ts = el[:time_start].to_time
86
+ te = el[:time_end].to_time
87
+ rescue NoMethodError
88
+ raise ArgumentError.new('intervals must define :time_start and :time_end as Time or Date')
89
+ end
90
+ attrs = el.clone
91
+ attrs.delete(:time_start)
92
+ attrs.delete(:time_end)
93
+ steps << { opening: 1, time: el[:time_start], attrs: attrs } # Start step
94
+ steps << { opening: -1, time: el[:time_end], attrs: attrs.clone } # End step
95
+ end
96
+
97
+ # Sort steps by time (and opening if time is the same)
98
+ steps.sort! do |a,b|
99
+ diff = a[:time] <=> b[:time]
100
+ diff = a[:opening] <=> b[:opening] if (diff == 0)
101
+ diff
102
+ end
103
+
104
+ # Iterate over steps
105
+ steps.each do |step|
106
+ if (started_count == 0)
107
+ last_time = step[:time]
108
+ last_attrs = step[:attrs]
109
+ else
110
+ if(step[:time] > last_time)
111
+ subintervals << ({
112
+ time_start: last_time,
113
+ time_end: step[:time]
114
+ }.merge(last_attrs))
115
+
116
+ last_time = step[:time]
117
+ end
118
+
119
+ if block_given?
120
+ last_attrs = block.call(last_attrs.clone, step[:attrs],(step[:opening] == 1 ? :open : :close))
121
+ else
122
+ last_attrs = step[:attrs]
123
+ end
124
+ end
125
+
126
+ # Update started_count
127
+ started_count += step[:opening]
128
+ end
129
+
130
+ subintervals
131
+ end
132
+
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,3 @@
1
+ module ActsAsBookable
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,42 @@
1
+ require 'active_record'
2
+ require 'active_record/version'
3
+ require 'active_support/core_ext/module'
4
+ require_relative 'acts_as_bookable/engine' if defined?(Rails)
5
+ require 'ice_cube'
6
+ IceCube.compatibility = 12 # Drop compatibility for :start_date, avoiding a bunch of warnings caused by serialization
7
+
8
+ module ActsAsBookable
9
+ extend ActiveSupport::Autoload
10
+
11
+ autoload :Bookable
12
+ autoload :Booker
13
+ autoload :Booking
14
+ autoload :T
15
+ autoload :VERSION
16
+ autoload :TimeUtils
17
+ autoload :DBUtils
18
+
19
+ autoload_under 'bookable' do
20
+ autoload :Core
21
+ end
22
+
23
+ class InitializationError < StandardError
24
+ def initialize model, message
25
+ super "Error initializing acts_as_bookable on #{model.to_s} - " + message
26
+ end
27
+ end
28
+
29
+ class OptionsInvalid < StandardError
30
+ def initialize model, message
31
+ super "Error validating options for #{model.to_s} - " + message
32
+ end
33
+ end
34
+
35
+ class AvailabilityError < StandardError
36
+ end
37
+ end
38
+
39
+ ActiveSupport.on_load(:active_record) do
40
+ extend ActsAsBookable::Bookable
41
+ include ActsAsBookable::Booker
42
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :acts_as_bookable do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,52 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'acts_as_bookable' do
4
+ it "should provide a class method 'bookable?' that is false for unbookable models" do
5
+ expect(Unbookable).not_to be_bookable
6
+ end
7
+
8
+ describe 'Bookable Method Generation' do
9
+ before :each do
10
+ Unbookable.acts_as_bookable
11
+ @bookable = Unbookable.new()
12
+ end
13
+
14
+ it "should respond 'true' to bookable?" do
15
+ expect(@bookable.class).to be_bookable
16
+ end
17
+ end
18
+
19
+ describe 'class configured as Bookable' do
20
+ before(:each) do
21
+ @bookable = Bookable.new
22
+ end
23
+
24
+ it 'should add #bookable? query method to the class-side' do
25
+ expect(Bookable).to respond_to(:bookable?)
26
+ end
27
+
28
+ it 'should return true from the class-side #bookable?' do
29
+ expect(Bookable.bookable?).to be_truthy
30
+ end
31
+
32
+ it 'should return false from the base #bookable?' do
33
+ expect(ActiveRecord::Base.bookable?).to be_falsy
34
+ end
35
+
36
+ # it 'should add #tag method on the instance-side' do
37
+ # expect(@bookable).to respond_to(:tag)
38
+ # end
39
+
40
+ # it 'should generate an association for #owned_taggings and #owned_tags' do
41
+ # expect(@bookable).to respond_to(:owned_taggings, :owned_tags)
42
+ # end
43
+ end
44
+
45
+ describe 'Reloading' do
46
+ it 'should save a model instantiated by Model.find' do
47
+ bookable = create(:bookable)
48
+ found_bookable = Bookable.find(bookable.id)
49
+ expect(found_bookable.save).to eq true
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,57 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'acts_as_booker' do
4
+ it "should provide a class method 'booker?' that is false for not booker models" do
5
+ expect(NotBooker).not_to be_booker
6
+ end
7
+
8
+ describe 'Booker Method Generation' do
9
+ before :each do
10
+ NotBooker.acts_as_booker
11
+ @booker = NotBooker.new()
12
+ end
13
+
14
+ it "should respond 'true' to booker?" do
15
+ expect(@booker.class).to be_booker
16
+ end
17
+ end
18
+
19
+ describe 'class configured as Booker' do
20
+ before(:each) do
21
+ @booker = Booker.new
22
+ end
23
+
24
+ it 'should add #booker? query method to the class-side' do
25
+ expect(Booker).to respond_to(:booker?)
26
+ end
27
+
28
+ it 'should return true from the class-side #booker?' do
29
+ expect(Booker.booker?).to be_truthy
30
+ end
31
+
32
+ it 'should return false from the base #booker?' do
33
+ expect(ActiveRecord::Base.booker?).to be_falsy
34
+ end
35
+
36
+ it 'should add #booker? query method to the instance-side' do
37
+ expect(@booker).to respond_to(:booker?)
38
+ end
39
+
40
+ it 'should add #booker? query method to the instance-side' do
41
+ expect(@booker.booker?).to be_truthy
42
+ end
43
+
44
+
45
+ # it 'should generate an association for #owned_taggings and #owned_tags' do
46
+ # expect(@booker).to respond_to(:owned_taggings, :owned_tags)
47
+ # end
48
+ end
49
+
50
+ describe 'Reloading' do
51
+ it 'should save a model instantiated by Model.find' do
52
+ booker = Booker.create!(name: 'Booker')
53
+ found_booker = Booker.find(booker.id)
54
+ expect(found_booker.save).to eq true
55
+ end
56
+ end
57
+ end