summaryrefslogtreecommitdiffstats
path: root/app/models/member.rb
blob: c9c5c2d54ac5b0f8756aca623eb55432940f676d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# frozen_string_literal: false

# Redmine - project management software
# Copyright (C) 2006-2017  Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

class Member < ActiveRecord::Base
  belongs_to :user
  belongs_to :principal, :foreign_key => 'user_id'
  has_many :member_roles, :dependent => :destroy
  has_many :roles, lambda { distinct }, :through => :member_roles
  belongs_to :project

  validates_presence_of :principal, :project
  validates_uniqueness_of :user_id, :scope => :project_id
  validate :validate_role

  before_destroy :set_issue_category_nil, :remove_from_project_default_assigned_to

  scope :active, lambda { joins(:principal).where(:users => {:status => Principal::STATUS_ACTIVE})}

  # Sort by first role and principal
  scope :sorted, lambda {
    includes(:member_roles, :roles, :principal).
      reorder("#{Role.table_name}.position").
      order(Principal.fields_for_order_statement)
  }
  scope :sorted_by_project, lambda {
    includes(:project).
      reorder("#{Project.table_name}.lft")
  }

  alias :base_reload :reload
  def reload(*args)
    @managed_roles = nil
    base_reload(*args)
  end

  def role
  end

  def role=
  end

  def name
    self.user.name
  end

  alias :base_role_ids= :role_ids=
  def role_ids=(arg)
    ids = (arg || []).collect(&:to_i) - [0]
    # Keep inherited roles
    ids += member_roles.select {|mr| !mr.inherited_from.nil?}.collect(&:role_id)

    new_role_ids = ids - role_ids
    # Add new roles
    new_role_ids.each {|id| member_roles << MemberRole.new(:role_id => id, :member => self) }
    # Remove roles (Rails' #role_ids= will not trigger MemberRole#on_destroy)
    member_roles_to_destroy = member_roles.select {|mr| !ids.include?(mr.role_id)}
    if member_roles_to_destroy.any?
      member_roles_to_destroy.each(&:destroy)
    end
  end

  def <=>(member)
    a, b = roles.sort, member.roles.sort
    if a == b
      if principal
        principal <=> member.principal
      else
        1
      end
    elsif a.any?
      b.any? ? a <=> b : -1
    else
      1
    end
  end

  # Set member role ids ignoring any change to roles that
  # user is not allowed to manage
  def set_editable_role_ids(ids, user=User.current)
    ids = (ids || []).collect(&:to_i) - [0]
    editable_role_ids = user.managed_roles(project).map(&:id)
    untouched_role_ids = self.role_ids - editable_role_ids
    touched_role_ids = ids & editable_role_ids
    self.role_ids = untouched_role_ids + touched_role_ids
  end

  # Returns true if one of the member roles is inherited
  def any_inherited_role?
    member_roles.any? {|mr| mr.inherited_from}
  end

  # Returns true if the member has the role and if it's inherited
  def has_inherited_role?(role)
    member_roles.any? {|mr| mr.role_id == role.id && mr.inherited_from.present?}
  end

  # Returns an Array of Project and/or Group from which the given role
  # was inherited, or an empty Array if the role was not inherited
  def role_inheritance(role)
    member_roles.
      select {|mr| mr.role_id == role.id && mr.inherited_from.present?}.
      map {|mr| mr.inherited_from_member_role.try(:member)}.
      compact.
      map {|m| m.project == project ? m.principal : m.project}
  end

  # Returns true if the member's role is editable by user
  def role_editable?(role, user=User.current)
    if has_inherited_role?(role)
      false
    else
      user.managed_roles(project).include?(role)
    end
  end

  # Returns true if the member is deletable by user
  def deletable?(user=User.current)
    if any_inherited_role?
      false
    else
      roles & user.managed_roles(project) == roles
    end
  end

  # Destroys the member
  def destroy
    member_roles.reload.each(&:destroy_without_member_removal)
    super
  end

  # Returns true if the member is user or is a group
  # that includes user
  def include?(user)
    if principal.is_a?(Group)
      !user.nil? && user.groups.include?(principal)
    else
      self.principal == user
    end
  end

  def set_issue_category_nil
    if user_id && project_id
      # remove category based auto assignments for this member
      IssueCategory.where(["project_id = ? AND assigned_to_id = ?", project_id, user_id]).
        update_all("assigned_to_id = NULL")
    end
  end

  def remove_from_project_default_assigned_to
    if user_id && project && project.default_assigned_to_id == user_id
      # remove project based auto assignments for this member
      project.update_column(:default_assigned_to_id, nil)
    end
  end

  # Returns the roles that the member is allowed to manage
  # in the project the member belongs to
  def managed_roles
    @managed_roles ||= begin
      if principal.try(:admin?)
        Role.givable.to_a
      else
        members_management_roles = roles.select do |role|
          role.has_permission?(:manage_members)
        end
        if members_management_roles.empty?
          []
        elsif members_management_roles.any?(&:all_roles_managed?)
          Role.givable.to_a
        else
          members_management_roles.map(&:managed_roles).reduce(&:|)
        end
      end
    end
  end

  # Creates memberships for principal with the attributes, or add the roles
  # if the membership already exists.
  # * project_ids : one or more project ids
  # * role_ids : ids of the roles to give to each membership
  #
  # Example:
  #   Member.create_principal_memberships(user, :project_ids => [2, 5], :role_ids => [1, 3]
  def self.create_principal_memberships(principal, attributes)
    members = []
    if attributes
      project_ids = Array.wrap(attributes[:project_ids] || attributes[:project_id])
      role_ids = Array.wrap(attributes[:role_ids])
      project_ids.each do |project_id|
        member = Member.find_or_new(project_id, principal)
        member.role_ids |= role_ids
        member.save
        members << member
      end
    end
    members
  end

  # Finds or initializes a Member for the given project and principal
  def self.find_or_new(project, principal)
    project_id = project.is_a?(Project) ? project.id : project
    principal_id = principal.is_a?(Principal) ? principal.id : principal

    member = Member.find_by_project_id_and_user_id(project_id, principal_id)
    member ||= Member.new(:project_id => project_id, :user_id => principal_id)
    member
  end

  protected

  def validate_role
    errors.add(:role, :empty) if member_roles.empty? && roles.empty?
  end
end