# # In this project, user group memberships do not neccessarily last forever. # They can begin at some time and end at some time. This is expressed by the # ValidityRange of a membership. # # Examples: # # membership.valid_from # => time # membership.valid_to # => time # membership.invalidate # # Scopes: # # UserGroupMembership.with_invalid # UserGroupMembership.only_valid # UserGroupMembership.only_invalid # UserGroupMembership.at_time(time) # # By default, the `only_valid` scope is applied, i.e. only memberships are # found that are valid at present time. To override this scope, use either # `with_invalid` or `unscoped`. # module UserGroupMembershipMixins::ValidityRange extend ActiveSupport::Concern included do attr_accessible :valid_from, :valid_to, :valid_from_localized_date, :valid_to_localized_date before_validation :set_valid_from_to_now default_scope { only_valid } end # Attributes in the database # ==================================================================================================== def valid_from_localized_date self.valid_from ? I18n.localize(self.valid_from.try(:to_date)) : "" end def valid_from_localized_date=(new_date) self.valid_from = new_date.to_datetime valid_from_will_change! end def set_valid_from_to_now(force = false) self.valid_from ||= Time.zone.now if self.new_record? or force return self end def valid_to_localized_date self.valid_to ? I18n.localize(self.valid_to.try(:to_date)) : "" end def valid_to_localized_date=(new_date) if new_date == "-" self.valid_to = nil else self.valid_to = new_date.to_datetime end valid_to_will_change! end # Invalidation # ==================================================================================================== # This method ends the membership, i.e. sets the end of the validity range # to the given time. # # The following examples are equivalent (despite the return value): # # membership.make_invalid # membership.make_invalid at: Time.zone.now # membership.make_invalid Time.zone.now # membership.invalidate # => membership # membership.update_attribute :valid_to, Time.zone.now # => true # def make_invalid(time = Time.zone.now) time = time[:at] if time.kind_of?(Hash) && time[:at] self.update_attribute(:valid_to, time) return self end # This is just an alias for `make_invalid`. # def invalidate(time = Time.zone.now) self.make_invalid(time) end # This method determines whether the membership can be invalidated. # Direct memberships can be invalidated, whereas indirect memberships cannot. # The validity of indirect memberships is derived from the validity of the direct ones. # def can_be_invalidated? self.direct? end # Validity Check # ==================================================================================================== # This method checks whether the membership is valid at the given time. # # This is not to be confused with ActiveRecord's `valid` method, which checks whether the # record matches the requirements to store it in the database. # # The following examples are equivalent: # # membership.currently_valid? # membership.valid_at? Time.zone.now # def valid_at?(time) (self.valid_from == nil || self.valid_from <= time) && (self.valid_to == nil || self.valid_to >= time) end # This method checks whether the present time lies within the validity range # of the membership. # def currently_valid? valid_at?(Time.zone.now) end # Temporal scopes # ==================================================================================================== module ClassMethods # This scope returns limits the query to memberships whose validity ranges match the # given time. # # Example: # # UserGroupMembership.find_all_by_user( u ).at_time( 1.hour.ago ).count # def at_time( time ) with_invalid .where("valid_from IS NULL OR valid_from <= ?", time) .where("valid_to IS NULL OR valid_to >= ?", time) end # This scope limits the query to memberships that are valid at the present time. # This is the default bahaviour. # def only_valid where("valid_from IS NULL OR valid_from <= ?", Time.zone.now) .where("valid_to IS NULL OR valid_to >= ?", Time.zone.now) end # This scope widens the query such that also memberships that are not valid at the # present time are returned. # def with_invalid # TODO: Replace the brute-force `unscoped` by a more specific term like # `except(:valid_from).except(:valid_to)`. # But so far, this has caused several tests to fail, which would have to be # fixed. # unscoped end # This scope limits the query to memberships that are invalid at the present time. # def only_invalid with_invalid.where("valid_to < ?", Time.zone.now) end # This scope limits the query to memberships that are valid at the present time. # This is the default bahaviour. # def now only_valid end # This scope limits the query to memberships that are invalid at the present time. # def in_the_past only_invalid end # This scope widens the query such that also memberships that are not valid at the # present time are returned. # def now_and_in_the_past with_invalid end def this_year with_invalid.where("valid_from >= ?", "#{Time.zone.now.year}-01-01 00:00:00") end def started_after(time) where('NOT valid_from IS NULL').where("valid_from >= ?", time) end end end class Array def started_after(time) self.select { |membership| membership.valid_from && membership.valid_from >= time } end end