fiedl/your_platform

View on GitHub
app/models/concerns/group_memberships.rb

Summary

Maintainability
B
4 hrs
Test Coverage
#
# This module contains the methods of the Group model regarding the associated
# user group memberships and users, i.e. members.
#
concern :GroupMemberships do

  included do

    # User Group Memberships
    # ==========================================================================================

    # This associates all Membership objects of the group, including indirect
    # memberships.
    #
    has_many :memberships, -> { where ancestor_type: 'Group', descendant_type: 'User' },
        foreign_key: :ancestor_id

    # This associates all memberships of the group that are direct, i.e. direct
    # parent_group-child_user memberships.
    #
    has_many :direct_memberships, -> { where ancestor_type: 'Group', descendant_type: 'User', direct: true },
        foreign_key: :ancestor_id, class_name: "Membership"

    # This associates all memberships of the group that are indirect, i.e.
    # ancestor_group-descendant_user memberships, where groups are between the
    # ancestor_group and the descendant_user.
    #
    has_many :indirect_memberships, -> { where ancestor_type: 'Group', descendant_type: 'User', direct: false },
        foreign_key: :ancestor_id, class_name: "Membership"


    #  This method builds a new membership having this group (self) as group associated.
    #
    def build_membership
      direct_memberships.build(descendant_type: 'User')
    end

    # This returns the Membership object that represents the membership of the
    # given user in this group.
    #
    # options:
    #   - also_in_the_past
    #
    def membership_of(user, options = {})
      if options[:also_in_the_past]
        base = Membership.with_invalid
      else
        base = Membership
      end
      base.find_by_user_and_group(user, self)
    end

    # This returns a string of the titles of the direct members of this group. This is used
    # for in-place editing, for example.
    #
    # The string would be something like this:
    #
    #    "#{user1.title}, #{user2.title}, ..."
    #
    def direct_members_titles_string
      direct_members.collect { |user| user.title }.join( ", " )
    end

    # This sets the memberships of a group according to the given string of user titles.
    #
    # For example, after calling
    #
    #    direct_members_titles_string = "#{user1.title}, #{user2.title}",
    #
    # the users `user1` and `user2` are the only direct members of the group.
    # The memberships are removed using the standard methods, which means that the memberships
    # are only marked as deleted. See: acts_as_paranoid_dag gem.
    #
    def direct_members_titles_string=(titles_string)
      new_members_titles = titles_string.to_s.split(",")
      new_members = new_members_titles.collect do |title|
        u = User.find_by_title( title.strip )
        self.errors.add :direct_member_titles_string, 'user not found: #{title}' unless u
        u
      end
      self.members = new_members
    end

    def members=(new_members)
      for member in self.direct_members
        unassign_user member unless member.in? new_members if member
      end
      for new_member in new_members
        assign_user new_member if new_member
      end
      self.touch
    end


    # User Assignment
    # ==========================================================================================

    # This assings the given user as a member to the group, i.e. this will
    # create a Membership.
    #
    # If a membership already exists, it will be extended.
    #
    def assign_user(user, options = {})
      raise RuntimeError, "no user given" if not user
      time_of_joining = options[:joined_at] || options[:at] || options[:time] || Time.zone.now
      if m = Membership.with_past.find_by_user_and_group(user, self)
        m.valid_from = time_of_joining if m.valid_from && time_of_joining < m.valid_from
        m.valid_to = nil
        m.save
        m
      else
        m = Membership.create descendant_id: user.id, ancestor_id: self.id
        m.update_attributes valid_from: time_of_joining # It does not work when added in `create`.
        m
      end
    end

    # This method will remove a Membership, i.e. terminate the membership
    # of the given user in this group.
    #
    def unassign_user( user, options = {} )
      if user and user.in?(self.members)
        time_of_unassignment = options[:at] || options[:time] || Time.zone.now
        Membership.find_by(user: user, group: self).invalidate(at: time_of_unassignment)
      end
    end


    def calculate_validity_range_of_indirect_memberships
      self.indirect_memberships.where(valid_from: nil).each do |membership|
        membership.recalculate_validity_range_from_direct_memberships
        membership.save
      end
    end


    # Members
    # ==========================================================================================

    # This associates the group members (users), direct ones as well as indirect ones.
    #
    # Attention! The conditions on the `memberships` association are ignored by Rails 3
    # when generating the SQL query. This is why the conditions have to be repeated here.
    #
    has_many(:members,
      -> { where('dag_links.ancestor_type' => 'Group').distinct },
      through: :memberships,
      source: :descendant, source_type: 'User'
      )

    # This associates only the direct group members (users).
    #
    has_many(:direct_members,
      -> { where('dag_links.ancestor_type' => 'Group', 'dag_links.direct' => true).distinct },
      through: :direct_memberships,
      source: :descendant, source_type: 'User'
      )

    # This associates only the indirect group members (users).
    #
    has_many(:indirect_members,
      -> { where('dag_links.ancestor_type' => 'Group', 'dag_links.direct' => false).distinct },
      through: :indirect_memberships,
      source: :descendant, source_type: 'User'
      )

  end
end