Skip to content

Instantly share code, notes, and snippets.

@tomasc
Last active August 18, 2018 18:54
Show Gist options
  • Save tomasc/cb288f83d8254fc08a83db83449d8925 to your computer and use it in GitHub Desktop.
Save tomasc/cb288f83d8254fc08a83db83449d8925 to your computer and use it in GitHub Desktop.
mongoid_recurring_views
require 'mongoid'
module Mongoid
class CreateView < Struct.new(:collection_name, :view_on, :pipeline)
def self.call(*args)
new(*args).call
end
def call
Mongoid.clients.each do |name, _|
client = Mongoid.client(name)
unless client.collections.map(&:name).include?(collection_name)
client.command(create: collection_name, viewOn: view_on, pipeline: pipeline)
end
end
end
end
end
class EventPage < Modulor::Page
concerning :Views do
VIEW_NAME = [EventPage.collection.name, 'view'].join('__').freeze
EXPANDED_VIEW_NAME = [EventPage.collection.name, 'expanded_view'].join('__').freeze
end
concerning :ViewFields do
# this page is using two MongoDB views (https://docs.mongodb.com/manual/core/views/)
# • `event_pages__view`
# • `event_pages__expanded_view`
# generated automatically on app boot
# to filter events either as grouped (ie one page represents multiple occurrences)
# or as expanded (ie page is multiplied according to the number of occurrences it assumes)
included do
field :_dtstart, type: DateTime
field :_dtend, type: DateTime
field :_all_day, type: Boolean
end
end
end
require 'mongoid/create_view'
# EVENTS
Mongoid::CreateView.call(
::EventPage::EXPANDED_VIEW_NAME,
EventPage.collection.name,
[
{ '$match': { '_type': EventPage.to_s } },
{ '$addFields': { '_web_modules': '$web_modules' } },
{ '$unwind': '$_web_modules' },
{ '$unwind': '$_web_modules.expanded_occurrences' },
{ '$addFields': {
'_dtstart': '$_web_modules.expanded_occurrences.dtstart',
'_dtend': '$_web_modules.expanded_occurrences.dtend',
'_all_day': '$_web_modules.expanded_occurrences.all_day',
'_sort_key': '$_web_modules.expanded_occurrences.dtstart'
}
}
]
)
Mongoid::CreateView.call(
::EventPage::VIEW_NAME,
EventPage.collection.name,
[
{ '$match': { 'web_modules._type': EventHeaderModule.to_s } },
{ '$addFields': {
'_sort_key': {
'$ifNull': [
{ '$min': { '$filter': { 'input': { '$arrayElemAt': ['$web_modules.expanded_occurrences.dtstart', 0] }, 'as': 'dtstart', 'cond': { '$gte': ['$$dtstart', 'new Date()'] } } } },
{ '$max': { '$filter': { 'input': { '$arrayElemAt': ['$web_modules.expanded_occurrences.dtstart', 0] }, 'as': 'dtstart', 'cond': { '$lt': ['$$dtstart', 'new Date()'] } } } }
]
}
}
}
]
)
# inherit from this class to create events with multiple occurences
#
# concerning :Occurences do
# included do
# # these are user-defined
# embeds_many :occurrences, class_name: 'Occurrence', order: :dtstart.asc
# accepts_nested_attributes_for :occurrences, allow_destroy: true
#
# # these are generated
# embeds_many :expanded_occurrences, class_name: 'Occurrence', order: :dtstart.asc
# before_validation :update_expanded_occurrences
#
# validates :occurrences, presence: true
# validates :expanded_occurrences, presence: true
# end
#
# def recurring?
# occurrences.all?(&:recurring?)
# end
#
# private
#
# def update_expanded_occurrences
# self.expanded_occurrences = occurrences.flat_map(&:expand_occurrences)
# end
# end
module Modulor
class Occurrence
include Mongoid::Document
include Modulor::HasCacheKey
SCHEDULE_DURATION = 3.months
concerning :Fields do
included do
field :dtstart, type: DateTime
field :dtend, type: DateTime
field :all_day, type: Boolean, default: false
before_validation :set_dtend
before_validation :adjust_dates_for_all_day
validates :dtstart, presence: true, unless: -> { recurring? && all_day? }
validates :dtend, presence: true, unless: -> { recurring? && all_day? }
end
def <=>(other)
sort_key <=> other.sort_key
end
def sort_key
dtstart
end
def dstart
dtstart.to_date
end
def dend
dtend.to_date
end
private
def set_dtend
return if dtend.present?
self.dtend ||= dtstart
end
def adjust_dates_for_all_day
return unless all_day?
self.dtstart = dtstart.beginning_of_day
self.dtend = dtend.end_of_day
end
end
concerning :Recurrence do
included do
field :schedule, type: MongoidIceCubeExtension::Schedule
field :schedule_dtend, type: DateTime
before_validation :nil_schedule, unless: -> { schedule.present? }
def schedule_dtend
super || Time.zone.now + SCHEDULE_DURATION
end
end
def recurring?
schedule.present?
end
def recurrence_rule
return unless schedule
schedule.recurrence_rules.first
end
def recurrence_rule=(value)
case value
when NilClass, 'null'
@recurrence_rule = nil
self.schedule = nil
else
@recurrence_rule = IceCube::Rule.from_hash(JSON.parse(value))
schedule_start_time = dtstart.try(:in_time_zone) || Time.zone.now
self.schedule = IceCube::Schedule.new(schedule_start_time) do |s|
s.add_recurrence_rule(@recurrence_rule)
end
end
end
def expand_occurrences
return unless dtstart
return unless dtend
return expand_recurring_occurrences if recurring?
Range.new(dtstart.to_date, dtend.to_date).each_with_index.map do |date, index|
occurence_dtstart = dtstart + index.days
occurence_dtend = occurence_dtstart.change(hour: dtend.hour, min: dtend.min, sec: dtend.sec)
self.class.new(dtstart: occurence_dtstart, dtend: occurence_dtend, all_day: all_day)
end
end
private
def expand_recurring_occurrences
schedule.occurrences(schedule_dtend).map do |occurrence|
occurrence_dtstart = occurrence.start_time
occurrence_dtstart = occurrence_dtstart.change(hour: dtstart.hour, min: dtstart.minute) if dtstart
occurrence_dtend = occurrence.end_time
occurrence_dtend = occurrence_dtend.change(hour: dtend.hour, min: dtend.minute) if dtend
self.class.new(dtstart: occurrence_dtstart, dtend: occurrence_dtend, all_day: all_day)
end
end
def nil_schedule
self.schedule = nil
end
end
concerning :Scopes do
included do
scope :for_datetime, -> (datetime) {
lte(dtstart: datetime).gte(dtend: datetime)
}
scope :for_datetime_range, ->(dtstart, dtend) {
dtstart = dtstart.beginning_of_day if dtstart.instance_of?(Date)
dtstart = dtstart.utc
dtend = dtend.end_of_day if dtend.instance_of?(Date)
dtend = dtend.utc
lte(dtstart: dtend.to_datetime).gte(dtend: dtstart.to_datetime)
}
scope :from_datetime, ->(dtstart) {
dtstart = dtstart.beginning_of_day if dtstart.instance_of?(Date)
dtstart = dtstart.utc
gte(dtstart: dtstart.to_datetime)
}
scope :to_datetime, ->(dtend) {
dtend = dtend.end_of_day if dtend.instance_of?(Date)
dtend = dtend.utc
lte(dtend: dtend.to_datetime)
}
scope :now, -> { for_datetime(Time.zone.now) }
scope :next, -> { from_datetime(Time.zone.now.beginning_of_day) }
scope :past, -> { to_datetime(Time.zone.now.beginning_of_day) }
scope :order_by_dtstart, -> (order = :asc) { order(dtstart: order) }
scope :order_by_dtend, -> (order = :asc) { order(dtend: order) }
end
end
end
end
- EventPage.with(collection: ::EventPage::VIEW_NAME) do
= EventPage.criteria.…
- EventPage.with(collection: ::EventPage::EXPANDED_VIEW_NAME) do
= EventPage.criteria.…
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment