mongoid_occurrence_views 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ec270d211cd1ad0bc58b60980d77f4defff948f5c8bd0d63db70f450f1f7d5c7
4
+ data.tar.gz: 2b351998c8815c1d1cd0aa7af2778e44e390f606cd35ae140bf9b246abc20bdb
5
+ SHA512:
6
+ metadata.gz: f915e5edb172f9a21bdb48bd1442fbcc677716212b9314e6463b93b0f6f755481bdf9a690bce663f623968edc0f312b1643d6e9f568e1420519583bb5dd277d2
7
+ data.tar.gz: b818d09e3c18d21b994a7250cf785ad898822d3d134cdc68e57b3e7844e39935b68b9a1ef8f4eeb1e6ff781c7b1031368a0bd551e52110a3979324c8f98ccf90
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ Gemfile.lock
data/.travis.yml ADDED
@@ -0,0 +1,19 @@
1
+ language: ruby
2
+ cache: bundler
3
+ script: 'bundle exec rake'
4
+ rvm:
5
+ - 2.5.1
6
+ services:
7
+ - mongodb
8
+
9
+ notifications:
10
+ email:
11
+ recipients:
12
+ - tomas.celizna@gmail.com
13
+ on_failure: change
14
+ on_success: never
15
+
16
+ matrix:
17
+ include:
18
+ - rvm: 2.5.1
19
+ env: MONGOID_VERSION=7
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # CHANGELOG
2
+
3
+ ## 0.1.0
4
+
5
+ * initial release
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in mongoid_occurrence_views.gemspec
6
+ gemspec
7
+
8
+ case version = ENV['MONGOID_VERSION'] || '~> 7.0'
9
+ when /7/ then gem 'mongoid', '~> 7.0'
10
+ else gem 'mongoid', version
11
+ end
data/Guardfile ADDED
@@ -0,0 +1,5 @@
1
+ guard :minitest do
2
+ watch(%r{^lib/(.+)\.rb$}) { |m| "test/#{m[1]}_test.rb" }
3
+ watch(%r{^test/.+_test\.rb$})
4
+ watch(%r{^test/test_helper\.rb$}) { 'test' }
5
+ end
data/README.md ADDED
@@ -0,0 +1,136 @@
1
+ # Mongoid Occurrence Views
2
+
3
+ [![Build Status](https://travis-ci.org/tomasc/mongoid_occurrence_views.svg)](https://travis-ci.org/tomasc/mongoid_occurrence_views) [![Gem Version](https://badge.fury.io/rb/mongoid_occurrence_views.svg)](http://badge.fury.io/rb/mongoid_occurrence_views) [![Coverage Status](https://img.shields.io/coveralls/tomasc/mongoid_occurrence_views.svg)](https://coveralls.io/r/tomasc/mongoid_occurrence_views)
4
+
5
+ By taking advantage of [Mongodb views](https://docs.mongodb.com/manual/core/views), this gem simplifies querying for events with multiple occurrences or a recurring schedule.
6
+
7
+ ## Requirements
8
+
9
+ * Mongoid 7.1+
10
+ * MongoDB 3.4+
11
+
12
+ ## Installation
13
+
14
+ Add this line to your application's Gemfile:
15
+
16
+ ```ruby
17
+ gem 'mongoid_occurrence_views'
18
+ ```
19
+
20
+ And then execute:
21
+
22
+ $ bundle
23
+
24
+ Or install it yourself as:
25
+
26
+ $ gem install mongoid_occurrence_views
27
+
28
+ ## Usage
29
+
30
+ ### Occurrences
31
+
32
+ Define a Mongoid document class that will hold information about each occurrence.
33
+
34
+ ```ruby
35
+ class Occurrence
36
+ include Mongoid::Document
37
+ include MongoidOccurrenceViews::Event::Occurrence
38
+
39
+ embedded_in_event class_name: 'Event'
40
+ end
41
+ ```
42
+
43
+ This will create the following fields on `Occurrence`:
44
+
45
+ * `dtstart`, (`DateTime`)
46
+ * `dtend` (`DateTime`)
47
+ * `schedule` (`MongoidIceCubeExtension::Schedule`)
48
+
49
+ And the `all_day` (`Boolean`) `attr_accessor`, which is useful for user-facing forms.
50
+
51
+ ### Events
52
+
53
+ ```ruby
54
+ class Event
55
+ include Mongoid::Document
56
+ include MongoidOccurrenceViews::Event
57
+
58
+ embeds_many_occurrences class_name: 'Occurrence'
59
+ end
60
+ ```
61
+
62
+ The `embeds_many_occurences` macro will setup an embedded relation that holds a definition of occurrences. For example:
63
+
64
+ ```ruby
65
+ <Occurrence dtstart: …, dtend: …, schedule: …>
66
+ <Occurrence dtstart: …, dtend: …, schedule: …>
67
+ <Occurrence dtstart: …, dtend: …, schedule: …>
68
+ ```
69
+
70
+ Before each validation callback, each occurrence will expand its definition as follows:
71
+
72
+ * multi-day occurrences are split into single-day occurrences
73
+ * recurring schedules are expanded into single-day occurrences
74
+
75
+ The following scopes are available for querying:
76
+
77
+ * `occurs_between(Time, Time)`
78
+ * `occurs_from(Time)`
79
+ * `occurs_on(Time)`
80
+ * `occurs_until(Time)`
81
+
82
+ And these scopes for ordering:
83
+
84
+ * `order_by_start(:asc / :desc)`
85
+ * `order_by_end(:asc / :desc)`
86
+
87
+ ### Views & queries
88
+
89
+ The gem creates two views (virtual collections):
90
+
91
+ * `expanded_occurrences_view` for working with the expanded events
92
+ * `occurrences_ordering_view` for ordering the unexpanded events
93
+
94
+ #### Expanded Occurrences View
95
+
96
+ This view lists expanded events – meaning events expanded for each daily occurrence.
97
+
98
+ Use the `with_expanded_occurrences_view` method to query the expanded events:
99
+
100
+ ```ruby
101
+ Event.with_expanded_occurrences_view do
102
+ Event.occurs_from(Time.zone.now).…
103
+ end
104
+ ```
105
+
106
+ The `.with_expanded_occurrences_view` method is simply a syntactic sugar on top of Mongoid's `.with(collection: …)` method, which specifies the collection to be the expanded occurrences view (`event__expanded_occurrences_view`).
107
+
108
+ The view adds a `:_dtstart` and `:_dtend` fields to each `Event`.
109
+
110
+ #### Occurrences Ordering View
111
+
112
+ This view lists unexpanded events, circumnavigating Mongodb limitation for sorting by embedded documents. It adds `dtstart` field with a value coming from occurrence chronologically first, and `dtend` field with a value of occurrence chronologically last.
113
+
114
+ Use the `with_occurrences_ordering_view` method to order the unexpanded events.
115
+
116
+ ```ruby
117
+ Event.with_occurrences_ordering_view do
118
+ Event.order_by_start(:asc)…
119
+ end
120
+ ```
121
+
122
+ The `.with_occurrences_ordering_view` method is simply a syntactic sugar on top of Mongoid's `.with(collection: …)` method, which specifies the collection to be the expanded occurrences view (`event__expanded_occurrences_view`).
123
+
124
+ ## Development
125
+
126
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
127
+
128
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
129
+
130
+ ## Contributing
131
+
132
+ Bug reports and pull requests are welcome on GitHub at https://github.com/tomasc/mongoid_occurrence_views.
133
+
134
+ ## License
135
+
136
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << 'test'
6
+ t.libs << 'lib'
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ end
9
+
10
+ task default: :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "mongoid_occurrence_views"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,25 @@
1
+ require 'mongoid_occurrence_views/version'
2
+
3
+ require 'mongoid'
4
+ require 'mongoid_ice_cube_extension'
5
+
6
+ require 'mongoid_occurrence_views/create_mongodb_view'
7
+ require 'mongoid_occurrence_views/destroy_mongodb_view'
8
+
9
+ require 'mongoid_occurrence_views/event'
10
+ require 'mongoid_occurrence_views/event/occurrence'
11
+
12
+ require 'mongoid_occurrence_views/event/has_views_on_occurrences'
13
+ require 'mongoid_occurrence_views/event/has_occurrence_scopes'
14
+
15
+ require 'mongoid_occurrence_views/event/create_view'
16
+ require 'mongoid_occurrence_views/event/create_expanded_occurrences_view'
17
+ require 'mongoid_occurrence_views/event/create_occurrences_ordering_view'
18
+
19
+ require 'mongoid_occurrence_views/queries/query'
20
+ require 'mongoid_occurrence_views/queries/occurs_between'
21
+ require 'mongoid_occurrence_views/queries/occurs_from'
22
+ require 'mongoid_occurrence_views/queries/occurs_on'
23
+ require 'mongoid_occurrence_views/queries/occurs_until'
24
+ require 'mongoid_occurrence_views/queries/order_by_end'
25
+ require 'mongoid_occurrence_views/queries/order_by_start'
@@ -0,0 +1,25 @@
1
+ module MongoidOccurrenceViews
2
+ class CreateMongodbView
3
+ def initialize(name:, collection:, pipeline:)
4
+ @name = name
5
+ @collection = collection
6
+ @pipeline = pipeline
7
+ end
8
+
9
+ def self.call(*args)
10
+ new(*args).call
11
+ end
12
+
13
+ def call
14
+ Mongoid.clients.each do |client_name, _|
15
+ client = Mongoid.client(client_name)
16
+ next if client.collections.map(&:name).include?(name)
17
+ client.command(create: name, viewOn: collection, pipeline: pipeline)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :name, :collection, :pipeline
24
+ end
25
+ end
@@ -0,0 +1,23 @@
1
+ module MongoidOccurrenceViews
2
+ class DestroyMongodbView
3
+ def initialize(name:)
4
+ @name = name
5
+ end
6
+
7
+ def self.call(*args)
8
+ new(*args).call
9
+ end
10
+
11
+ def call
12
+ Mongoid.clients.each do |client_name, _|
13
+ client = Mongoid.client(client_name)
14
+ next unless client.collections.map(&:name).include?(name)
15
+ client.command(drop: name)
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :name
22
+ end
23
+ end
@@ -0,0 +1,21 @@
1
+ module MongoidOccurrenceViews
2
+ module Event
3
+ def self.included(base)
4
+ base.class_eval do
5
+ include HasViewsOnOccurrences
6
+ include HasOccurrenceScopes
7
+ end
8
+ base.extend ClassMethods
9
+ end
10
+
11
+ module ClassMethods
12
+ def embeds_many_occurrences(options = {})
13
+ embeds_many :occurrences, class_name: options.fetch(:class_name)
14
+ accepts_nested_attributes_for :occurrences, allow_destroy: true, reject_if: :all_blank
15
+
16
+ CreateExpandedOccurrencesView.call(self) unless embedded?
17
+ CreateOccurrencesOrderingView.call(self) unless embedded?
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,34 @@
1
+ module MongoidOccurrenceViews
2
+ module Event
3
+ class CreateExpandedOccurrencesView < CreateView
4
+ def view_name
5
+ klass.expanded_occurrences_view_name
6
+ end
7
+
8
+ def pipeline
9
+ [add_fields,
10
+ unwind_associations_to_parent,
11
+ add_datetime_fields].flatten
12
+ end
13
+
14
+ private
15
+
16
+ def add_fields
17
+ { '$addFields': { "_#{occurrence_relation_chain.first}": "$#{occurrence_relation_chain.first}" } }
18
+ end
19
+
20
+ def unwind_associations_to_parent
21
+ occurrence_relations_chained.map do |chain|
22
+ { '$unwind': "$_#{chain}" }
23
+ end
24
+ end
25
+
26
+ def add_datetime_fields
27
+ { '$addFields': {
28
+ '_dtstart': "$_#{occurrence_relations_chained.last}.ds",
29
+ '_dtend': "$_#{occurrence_relations_chained.last}.de",
30
+ } }
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,31 @@
1
+ module MongoidOccurrenceViews
2
+ module Event
3
+ class CreateOccurrencesOrderingView < CreateView
4
+ def view_name
5
+ klass.occurrences_ordering_view_name
6
+ end
7
+
8
+ def pipeline
9
+ [add_fields].flatten
10
+ end
11
+
12
+ private
13
+
14
+ def add_fields
15
+ { '$addFields': { '_dtstart': order_dtstart_field, '_dtend': order_dtend_field } }
16
+ end
17
+
18
+ def order_dtstart_field
19
+ min = "$#{occurrence_relations_chained.last}.ds"
20
+ occurrence_relations_chained.length.times { min = { '$min': min } }
21
+ min
22
+ end
23
+
24
+ def order_dtend_field
25
+ max = "$#{occurrence_relations_chained.last}.de"
26
+ occurrence_relations_chained.length.times { max = { '$max': max } }
27
+ max
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,40 @@
1
+ module MongoidOccurrenceViews
2
+ module Event
3
+ class CreateView
4
+ delegate(
5
+ :occurrence_relations_chained,
6
+ :occurrence_relation_chain,
7
+
8
+ to: :klass
9
+ )
10
+
11
+ def initialize(klass)
12
+ @klass = klass
13
+ end
14
+
15
+ def self.call(*args)
16
+ new(*args).call
17
+ end
18
+
19
+ def call
20
+ CreateMongodbView.call(
21
+ name: view_name,
22
+ collection: klass.collection.name,
23
+ pipeline: pipeline
24
+ )
25
+ end
26
+
27
+ def pipeline
28
+ raise NotImplementedError
29
+ end
30
+
31
+ def view_name
32
+ raise NotImplementedError
33
+ end
34
+
35
+ private
36
+
37
+ attr_reader :klass
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,32 @@
1
+ module MongoidOccurrenceViews
2
+ module Event
3
+ module HasOccurrenceScopes
4
+ def self.included(base)
5
+ base.extend ClassMethods
6
+ base.class_eval do
7
+ scope :occurs_between, ->(dtstart, dtend, dtstart_field = dtstart_query_field, dtend_field = dtend_query_field) { MongoidOccurrenceViews::Queries::OccursBetween.criteria(criteria, dtstart, dtend, dtstart_field: dtstart_field, dtend_field: dtend_field) }
8
+ scope :occurs_from, ->(date_time, dtstart_field = dtstart_query_field) { MongoidOccurrenceViews::Queries::OccursFrom.criteria(criteria, date_time, dtstart_field: dtstart_field) }
9
+ scope :occurs_on, -> (date_time, dtstart_field = dtstart_query_field, dtend_field = dtend_query_field) { MongoidOccurrenceViews::Queries::OccursOn.criteria(criteria, date_time, dtstart_field: dtstart_field, dtend_field: dtend_field) }
10
+ scope :occurs_until, ->(date_time, dtend_field = dtend_query_field) { MongoidOccurrenceViews::Queries::OccursUntil.criteria(criteria, date_time, dtend_field: dtend_field) }
11
+
12
+ scope :order_by_start, ->(order = :asc, dtstart_field = dtstart_query_field) { MongoidOccurrenceViews::Queries::OrderByStart.criteria(criteria, order, dtstart_field: dtstart_field) }
13
+ scope :order_by_end, ->(order = :asc, dtend_field = dtend_query_field) { MongoidOccurrenceViews::Queries::OrderByEnd.criteria(criteria, order, dtend_field: dtend_field) }
14
+ end
15
+ end
16
+
17
+ module ClassMethods
18
+ def dtstart_query_field
19
+ within_view? ? :_dtstart : :"#{occurrence_relations_chained.last}.ds"
20
+ end
21
+
22
+ def dtend_query_field
23
+ within_view? ? :_dtend : :"#{occurrence_relations_chained.last}.de"
24
+ end
25
+
26
+ def within_view?
27
+ collection.name =~ /#{MongoidOccurrenceViews::Event::HasViewsOnOccurrences::EXPANDED_VIEW_NAME_SUFFIX}|#{MongoidOccurrenceViews::Event::HasViewsOnOccurrences::ORDERING_VIEW_NAME_SUFFIX}/
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,81 @@
1
+ module MongoidOccurrenceViews
2
+ module Event
3
+ module HasViewsOnOccurrences
4
+ ORDERING_VIEW_NAME_SUFFIX = 'occurrences_ordering_view'.freeze
5
+ EXPANDED_VIEW_NAME_SUFFIX = 'expanded_occurrences_view'.freeze
6
+
7
+ def self.included(base)
8
+ base.extend ClassMethods
9
+ end
10
+
11
+ def dtstart
12
+ return unless self.class.within_view?
13
+ DateTime.demongoize(self[:_dtstart])
14
+ end
15
+
16
+ def dtend
17
+ return unless self.class.within_view?
18
+ DateTime.demongoize(self[:_dtend])
19
+ end
20
+
21
+ module ClassMethods
22
+ def occurrences_ordering_view_name
23
+ [collection.name, ORDERING_VIEW_NAME_SUFFIX].join('__').freeze
24
+ end
25
+
26
+ def expanded_occurrences_view_name
27
+ [collection.name, EXPANDED_VIEW_NAME_SUFFIX].join('__').freeze
28
+ end
29
+
30
+ def with_expanded_occurrences_view(&block)
31
+ with_occurrences_view(expanded: true, &block)
32
+ end
33
+
34
+ def with_occurrences_ordering_view(&block)
35
+ with_occurrences_view(expanded: false, &block)
36
+ end
37
+
38
+ def with_occurrences_view(options = {}, &block)
39
+ expanded = options.delete(:expanded) { |_| true }
40
+ options[:collection] ||= expanded ? expanded_occurrences_view_name : occurrences_ordering_view_name
41
+ criteria.with(options, &block)
42
+ end
43
+
44
+ def occurrence_relations_chained
45
+ occurrence_relation_chain.each_with_index.map do |_, i|
46
+ occurrence_relation_chain[0..i].join('.')
47
+ end
48
+ end
49
+
50
+ def occurrence_relation_chain
51
+ return occurrence_relation_names unless event_relations.present?
52
+ [event_relations.first.store_as, occurrence_relation_names].flatten
53
+ end
54
+
55
+ def event_relations
56
+ return unless self.relations.present?
57
+ self.relations.values.select { |rel| is_event_relation?(rel) }
58
+ end
59
+
60
+ def is_event_relation?(rel)
61
+ relation_class_name = rel.options[:class_name]
62
+ return false unless relation_class_name.present?
63
+ return true if event_class_names.include?(relation_class_name)
64
+ return unless relation_class = relation_class_name.safe_constantize
65
+ event_superclasses = event_class_names.map { |cn| cn.safe_constantize.ancestors }.flatten
66
+ event_superclasses.include? relation_class
67
+ end
68
+
69
+ def event_class_names
70
+ ObjectSpace.each_object(Class).select do |cls|
71
+ cls.included_modules.include?(MongoidOccurrenceViews::Event)
72
+ end.map(&:to_s)
73
+ end
74
+
75
+ def occurrence_relation_names
76
+ %w[occurrences daily_occurrences]
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,122 @@
1
+ module MongoidOccurrenceViews
2
+ module Event
3
+ module Occurrence
4
+ SCHEDULE_DURATION = 1.year
5
+
6
+ def self.included(base)
7
+ base.include MongoidOccurrenceViews::Event::HasOccurrenceScopes
8
+ base.extend ClassMethods
9
+ end
10
+
11
+ module ClassMethods
12
+ def embedded_in_event(options = {})
13
+ field :dtstart, type: DateTime
14
+ field :dtend, type: DateTime
15
+
16
+ field :schedule, type: MongoidIceCubeExtension::Schedule
17
+ field :schedule_dtend, type: Time
18
+
19
+ embedded_in :event, class_name: options.fetch(:class_name, nil)
20
+ embeds_many :daily_occurrences, class_name: 'MongoidOccurrenceViews::Event::Occurrence::DailyOccurrence', order: :dtstart.asc
21
+
22
+ validates_presence_of :dtstart
23
+ validates_presence_of :dtend
24
+
25
+ before_validation :nil_schedule, unless: :recurring?
26
+
27
+ after_validation :adjust_dates_for_all_day, if: :changed?
28
+ after_validation :assign_daily_occurrences, if: :changed?
29
+ end
30
+
31
+ def dtstart_query_field
32
+ :"daily_occurrences.ds"
33
+ end
34
+
35
+ def dtend_query_field
36
+ :"daily_occurrences.de"
37
+ end
38
+ end
39
+
40
+ def all_day
41
+ return unless dtstart.present?
42
+ return unless dtend.present?
43
+
44
+ @all_day ||= dtstart == dtstart.beginning_of_day && dtend == dtend.end_of_day
45
+ end
46
+ alias all_day? all_day
47
+
48
+ def all_day=(val)
49
+ @all_day = [true, 'true', 1, '1'].include?(val)
50
+ end
51
+
52
+ def recurring?
53
+ schedule.present?
54
+ end
55
+
56
+ def schedule_dtend
57
+ read_attribute(:schedule_dtend) || (dtstart.try(:to_time) || Time.now) + SCHEDULE_DURATION
58
+ end
59
+
60
+ private
61
+
62
+ def nil_schedule
63
+ self.schedule = nil
64
+ end
65
+
66
+ def adjust_dates_for_all_day
67
+ return unless all_day?
68
+
69
+ self.dtstart = dtstart.beginning_of_day
70
+ self.dtend = dtend.end_of_day
71
+ end
72
+
73
+ def assign_daily_occurrences
74
+ return unless dtstart_changed? || dtend_changed? || schedule_changed? || schedule_dtend_changed?
75
+ self.daily_occurrences = daily_occurrences_from_schedule + daily_occurrences_from_date_range
76
+ end
77
+
78
+ def daily_occurrences_from_schedule
79
+ return [] unless recurring?
80
+
81
+ schedule.occurrences(schedule_dtend).map do |occurrence|
82
+ relations['daily_occurrences'].klass.new(
83
+ dtstart: occurrence.start_time,
84
+ dtend: occurrence.end_time.change(hour: dtend.hour, min: dtend.minute)
85
+ )
86
+ end.compact
87
+ end
88
+
89
+ def daily_occurrences_from_date_range
90
+ return [] if recurring?
91
+ date_range = Range.new(dtstart.to_date, dtend.to_date)
92
+ is_single_day = (date_range.first == date_range.last)
93
+
94
+ date_range.map do |date|
95
+ occurence_dtstart = is_single_day || date == date_range.first ? dtstart : date.beginning_of_day
96
+ occurence_dtend = is_single_day || date == date_range.last ? dtend : date.end_of_day
97
+
98
+ relations['daily_occurrences'].klass.new(
99
+ dtstart: occurence_dtstart,
100
+ dtend: occurence_dtend
101
+ )
102
+ end.compact
103
+ end
104
+
105
+ class DailyOccurrence
106
+ include Mongoid::Document
107
+
108
+ # alias fields to keep document size small
109
+ field :ds, as: :dtstart, type: DateTime
110
+ field :de, as: :dtend, type: DateTime
111
+
112
+ def all_day
113
+ return unless dtstart.present?
114
+ return unless dtend.present?
115
+
116
+ dtstart == dtstart.beginning_of_day && dtend == dtend.end_of_day
117
+ end
118
+ alias all_day? all_day
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,28 @@
1
+ module MongoidOccurrenceViews
2
+ module Queries
3
+ class OccursBetween < Query
4
+ def initialize(base_criteria, dtstart, dtend, options = {})
5
+ @base_criteria = base_criteria
6
+ @dtstart = dtstart
7
+ @dtend = dtend
8
+ @dtstart_field = options.fetch(:dtstart_field)
9
+ @dtend_field = options.fetch(:dtend_field)
10
+ end
11
+
12
+ def criteria
13
+ _dtstart = dtstart.beginning_of_day if dtstart.instance_of?(Date)
14
+ _dtstart = dtstart.utc
15
+
16
+ _dtend = dtend.end_of_day if dtend.instance_of?(Date)
17
+ _dtend = dtend.utc
18
+
19
+ base_criteria.lte(dtstart_field => dtend.to_datetime)
20
+ .gte(dtend_field => dtstart.to_datetime)
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :dtstart, :dtend, :dtstart_field, :dtend_field
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,22 @@
1
+ module MongoidOccurrenceViews
2
+ module Queries
3
+ class OccursFrom < Query
4
+ def initialize(base_criteria, date_time, options = {})
5
+ @base_criteria = base_criteria
6
+ @date_time = date_time
7
+ @dtstart_field = options.fetch(:dtstart_field)
8
+ end
9
+
10
+ def criteria
11
+ _date_time = date_time.end_of_day if date_time.instance_of?(Date)
12
+ _date_time = date_time.utc
13
+
14
+ base_criteria.gte(dtstart_field => _date_time)
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :date_time, :dtstart_field
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,24 @@
1
+ module MongoidOccurrenceViews
2
+ module Queries
3
+ class OccursOn < Query
4
+ def initialize(base_criteria, date_time, options = {})
5
+ @base_criteria = base_criteria
6
+ @date_time = date_time
7
+ @dtstart_field = options.fetch(:dtstart_field)
8
+ @dtend_field = options.fetch(:dtend_field)
9
+ end
10
+
11
+ def criteria
12
+ _date_time = date_time.beginning_of_day if date_time.instance_of?(Date)
13
+ _date_time = date_time.utc
14
+
15
+ base_criteria.lte(dtstart_field => _date_time)
16
+ .gte(dtend_field => _date_time)
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :date_time, :dtstart_field, :dtend_field
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,22 @@
1
+ module MongoidOccurrenceViews
2
+ module Queries
3
+ class OccursUntil < Query
4
+ def initialize(base_criteria, date_time, options = {})
5
+ @base_criteria = base_criteria
6
+ @date_time = date_time
7
+ @dtend_field = options.fetch(:dtend_field)
8
+ end
9
+
10
+ def criteria
11
+ _date_time = date_time.beginning_of_day if date_time.instance_of?(Date)
12
+ _date_time = date_time.utc
13
+
14
+ base_criteria.lte(dtend_field => _date_time)
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :date_time, :dtend_field
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,19 @@
1
+ module MongoidOccurrenceViews
2
+ module Queries
3
+ class OrderByEnd < Query
4
+ def initialize(base_criteria, order, options = {})
5
+ @base_criteria = base_criteria
6
+ @order = order
7
+ @dtend_field = options.fetch(:dtend_field)
8
+ end
9
+
10
+ def criteria
11
+ base_criteria.order_by(dtend_field => order)
12
+ end
13
+
14
+ private
15
+
16
+ attr_reader :order, :dtend_field
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ module MongoidOccurrenceViews
2
+ module Queries
3
+ class OrderByStart < Query
4
+ def initialize(base_criteria, order, options = {})
5
+ @base_criteria = base_criteria
6
+ @order = order
7
+ @dtstart_field = options.fetch(:dtstart_field)
8
+ end
9
+
10
+ def criteria
11
+ base_criteria.order_by(dtstart_field => order)
12
+ end
13
+
14
+ private
15
+
16
+ attr_reader :order, :dtstart_field
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,21 @@
1
+ module MongoidOccurrenceViews
2
+ module Queries
3
+ class Query
4
+ def initialize(base_criteria)
5
+ @base_criteria = base_criteria
6
+ end
7
+
8
+ def self.criteria(*args)
9
+ new(*args).criteria
10
+ end
11
+
12
+ def criteria
13
+ raise NotImplementedError
14
+ end
15
+
16
+ private
17
+
18
+ attr_reader :base_criteria
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,3 @@
1
+ module MongoidOccurrenceViews
2
+ VERSION = '0.1.0'.freeze
3
+ end
@@ -0,0 +1,36 @@
1
+ lib = File.expand_path('lib', __dir__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'mongoid_occurrence_views/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'mongoid_occurrence_views'
7
+ spec.version = MongoidOccurrenceViews::VERSION
8
+ spec.authors = ['Tomas Celizna', 'Asger Behncke Jacobsen']
9
+ spec.email = ['tomas.celizna@gmail.com', 'a@asgerbehnckejacobsen.dk']
10
+
11
+ spec.summary = "Makes one's life easier when working with events that have multiple occurrences, or a recurring schedule."
12
+ spec.homepage = 'https://github.com/tomasc/mongoid_occurrence_views'
13
+ spec.license = 'MIT'
14
+
15
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
16
+ f.match(%r{^(test|spec|features)/})
17
+ end
18
+ spec.bindir = 'exe'
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ['lib']
21
+
22
+ spec.add_dependency 'ice_cube'
23
+ spec.add_dependency 'mongoid', '>= 7.0.2', '< 8'
24
+ spec.add_dependency 'mongoid_ice_cube_extension', '>= 0.1.1'
25
+
26
+ spec.add_development_dependency 'activesupport', '> 3.0'
27
+ spec.add_development_dependency 'bundler', '~> 1.16'
28
+ spec.add_development_dependency 'coveralls'
29
+ spec.add_development_dependency 'database_cleaner'
30
+ spec.add_development_dependency 'factory_bot', '~> 4.0'
31
+ spec.add_development_dependency 'guard'
32
+ spec.add_development_dependency 'guard-minitest'
33
+ spec.add_development_dependency 'minitest', '~> 5.0'
34
+ spec.add_development_dependency 'minitest-implicit-subject'
35
+ spec.add_development_dependency 'rake', '~> 10.0'
36
+ end
metadata ADDED
@@ -0,0 +1,263 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mongoid_occurrence_views
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Tomas Celizna
8
+ - Asger Behncke Jacobsen
9
+ autorequire:
10
+ bindir: exe
11
+ cert_chain: []
12
+ date: 2018-10-09 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: ice_cube
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: mongoid
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: 7.0.2
35
+ - - "<"
36
+ - !ruby/object:Gem::Version
37
+ version: '8'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: 7.0.2
45
+ - - "<"
46
+ - !ruby/object:Gem::Version
47
+ version: '8'
48
+ - !ruby/object:Gem::Dependency
49
+ name: mongoid_ice_cube_extension
50
+ requirement: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 0.1.1
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 0.1.1
62
+ - !ruby/object:Gem::Dependency
63
+ name: activesupport
64
+ requirement: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ type: :development
70
+ prerelease: false
71
+ version_requirements: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ - !ruby/object:Gem::Dependency
77
+ name: bundler
78
+ requirement: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.16'
83
+ type: :development
84
+ prerelease: false
85
+ version_requirements: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.16'
90
+ - !ruby/object:Gem::Dependency
91
+ name: coveralls
92
+ requirement: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ type: :development
98
+ prerelease: false
99
+ version_requirements: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ - !ruby/object:Gem::Dependency
105
+ name: database_cleaner
106
+ requirement: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ type: :development
112
+ prerelease: false
113
+ version_requirements: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ - !ruby/object:Gem::Dependency
119
+ name: factory_bot
120
+ requirement: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '4.0'
125
+ type: :development
126
+ prerelease: false
127
+ version_requirements: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '4.0'
132
+ - !ruby/object:Gem::Dependency
133
+ name: guard
134
+ requirement: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ type: :development
140
+ prerelease: false
141
+ version_requirements: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ - !ruby/object:Gem::Dependency
147
+ name: guard-minitest
148
+ requirement: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ type: :development
154
+ prerelease: false
155
+ version_requirements: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ - !ruby/object:Gem::Dependency
161
+ name: minitest
162
+ requirement: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '5.0'
167
+ type: :development
168
+ prerelease: false
169
+ version_requirements: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: '5.0'
174
+ - !ruby/object:Gem::Dependency
175
+ name: minitest-implicit-subject
176
+ requirement: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ type: :development
182
+ prerelease: false
183
+ version_requirements: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ - !ruby/object:Gem::Dependency
189
+ name: rake
190
+ requirement: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - "~>"
193
+ - !ruby/object:Gem::Version
194
+ version: '10.0'
195
+ type: :development
196
+ prerelease: false
197
+ version_requirements: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - "~>"
200
+ - !ruby/object:Gem::Version
201
+ version: '10.0'
202
+ description:
203
+ email:
204
+ - tomas.celizna@gmail.com
205
+ - a@asgerbehnckejacobsen.dk
206
+ executables: []
207
+ extensions: []
208
+ extra_rdoc_files: []
209
+ files:
210
+ - ".gitignore"
211
+ - ".travis.yml"
212
+ - CHANGELOG.md
213
+ - Gemfile
214
+ - Guardfile
215
+ - README.md
216
+ - Rakefile
217
+ - bin/console
218
+ - bin/setup
219
+ - lib/mongoid_occurrence_views.rb
220
+ - lib/mongoid_occurrence_views/create_mongodb_view.rb
221
+ - lib/mongoid_occurrence_views/destroy_mongodb_view.rb
222
+ - lib/mongoid_occurrence_views/event.rb
223
+ - lib/mongoid_occurrence_views/event/create_expanded_occurrences_view.rb
224
+ - lib/mongoid_occurrence_views/event/create_occurrences_ordering_view.rb
225
+ - lib/mongoid_occurrence_views/event/create_view.rb
226
+ - lib/mongoid_occurrence_views/event/has_occurrence_scopes.rb
227
+ - lib/mongoid_occurrence_views/event/has_views_on_occurrences.rb
228
+ - lib/mongoid_occurrence_views/event/occurrence.rb
229
+ - lib/mongoid_occurrence_views/queries/occurs_between.rb
230
+ - lib/mongoid_occurrence_views/queries/occurs_from.rb
231
+ - lib/mongoid_occurrence_views/queries/occurs_on.rb
232
+ - lib/mongoid_occurrence_views/queries/occurs_until.rb
233
+ - lib/mongoid_occurrence_views/queries/order_by_end.rb
234
+ - lib/mongoid_occurrence_views/queries/order_by_start.rb
235
+ - lib/mongoid_occurrence_views/queries/query.rb
236
+ - lib/mongoid_occurrence_views/version.rb
237
+ - mongoid_occurrence_views.gemspec
238
+ homepage: https://github.com/tomasc/mongoid_occurrence_views
239
+ licenses:
240
+ - MIT
241
+ metadata: {}
242
+ post_install_message:
243
+ rdoc_options: []
244
+ require_paths:
245
+ - lib
246
+ required_ruby_version: !ruby/object:Gem::Requirement
247
+ requirements:
248
+ - - ">="
249
+ - !ruby/object:Gem::Version
250
+ version: '0'
251
+ required_rubygems_version: !ruby/object:Gem::Requirement
252
+ requirements:
253
+ - - ">="
254
+ - !ruby/object:Gem::Version
255
+ version: '0'
256
+ requirements: []
257
+ rubyforge_project:
258
+ rubygems_version: 2.7.6
259
+ signing_key:
260
+ specification_version: 4
261
+ summary: Makes one's life easier when working with events that have multiple occurrences,
262
+ or a recurring schedule.
263
+ test_files: []