Ability to assign issues to groups (#2964).
Option is disabled by default. It can be turned on in application settings. git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@6306 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
+15
-2
@@ -1,5 +1,5 @@
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2009 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2011 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
|
||||
@@ -24,11 +24,15 @@ class Group < Principal
|
||||
validates_presence_of :lastname
|
||||
validates_uniqueness_of :lastname, :case_sensitive => false
|
||||
validates_length_of :lastname, :maximum => 30
|
||||
|
||||
|
||||
before_destroy :remove_references_before_destroy
|
||||
|
||||
def to_s
|
||||
lastname.to_s
|
||||
end
|
||||
|
||||
alias :name :to_s
|
||||
|
||||
def user_added(user)
|
||||
members.each do |member|
|
||||
next if member.project.nil?
|
||||
@@ -46,4 +50,13 @@ class Group < Principal
|
||||
:conditions => ["#{Member.table_name}.user_id = ? AND #{MemberRole.table_name}.inherited_from IN (?)", user.id, member.member_role_ids]).each(&:destroy)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Removes references that are not handled by associations
|
||||
def remove_references_before_destroy
|
||||
return if self.id.nil?
|
||||
|
||||
Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
|
||||
end
|
||||
end
|
||||
|
||||
+14
-6
@@ -22,7 +22,7 @@ class Issue < ActiveRecord::Base
|
||||
belongs_to :tracker
|
||||
belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
|
||||
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
|
||||
belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
|
||||
belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
|
||||
belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
|
||||
belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
|
||||
belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
|
||||
@@ -93,9 +93,11 @@ class Issue < ActiveRecord::Base
|
||||
when 'all'
|
||||
nil
|
||||
when 'default'
|
||||
"(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id = #{user.id})"
|
||||
user_ids = [user.id] + user.groups.map(&:id)
|
||||
"(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids}))"
|
||||
when 'own'
|
||||
"(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id = #{user.id})"
|
||||
user_ids = [user.id] + user.groups.map(&:id)
|
||||
"(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids}))"
|
||||
else
|
||||
'1=0'
|
||||
end
|
||||
@@ -109,9 +111,9 @@ class Issue < ActiveRecord::Base
|
||||
when 'all'
|
||||
true
|
||||
when 'default'
|
||||
!self.is_private? || self.author == user || self.assigned_to == user
|
||||
!self.is_private? || self.author == user || user.is_or_belongs_to?(assigned_to)
|
||||
when 'own'
|
||||
self.author == user || self.assigned_to == user
|
||||
self.author == user || user.is_or_belongs_to?(assigned_to)
|
||||
else
|
||||
false
|
||||
end
|
||||
@@ -482,7 +484,13 @@ class Issue < ActiveRecord::Base
|
||||
# Author and assignee are always notified unless they have been
|
||||
# locked or don't want to be notified
|
||||
notified << author if author && author.active? && author.notify_about?(self)
|
||||
notified << assigned_to if assigned_to && assigned_to.active? && assigned_to.notify_about?(self)
|
||||
if assigned_to
|
||||
if assigned_to.is_a?(Group)
|
||||
notified += assigned_to.users.select {|u| u.active? && u.notify_about?(self)}
|
||||
else
|
||||
notified << assigned_to if assigned_to.active? && assigned_to.notify_about?(self)
|
||||
end
|
||||
end
|
||||
notified.uniq!
|
||||
# Remove users that can not view the issue
|
||||
notified.reject! {|user| !visible?(user)}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
class IssueCategory < ActiveRecord::Base
|
||||
belongs_to :project
|
||||
belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
|
||||
belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
|
||||
has_many :issues, :foreign_key => 'category_id', :dependent => :nullify
|
||||
|
||||
validates_presence_of :name
|
||||
|
||||
@@ -261,8 +261,7 @@ class MailHandler < ActionMailer::Base
|
||||
|
||||
# Returns a Hash of issue attributes extracted from keywords in the email body
|
||||
def issue_attributes_from_keywords(issue)
|
||||
assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_user_from_keyword(k)
|
||||
assigned_to = nil if assigned_to && !issue.assignable_users.include?(assigned_to)
|
||||
assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_assignee_from_keyword(k, issue)
|
||||
|
||||
attrs = {
|
||||
'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id),
|
||||
@@ -353,14 +352,19 @@ class MailHandler < ActionMailer::Base
|
||||
end
|
||||
body.strip
|
||||
end
|
||||
|
||||
def find_user_from_keyword(keyword)
|
||||
user ||= User.find_by_mail(keyword)
|
||||
user ||= User.find_by_login(keyword)
|
||||
if user.nil? && keyword.match(/ /)
|
||||
|
||||
def find_assignee_from_keyword(keyword, issue)
|
||||
keyword = keyword.to_s.downcase
|
||||
assignable = issue.assignable_users
|
||||
assignee = nil
|
||||
assignee ||= assignable.detect {|a| a.mail.to_s.downcase == keyword || a.login.to_s.downcase == keyword}
|
||||
if assignee.nil? && keyword.match(/ /)
|
||||
firstname, lastname = *(keyword.split) # "First Last Throwaway"
|
||||
user ||= User.find_by_firstname_and_lastname(firstname, lastname)
|
||||
assignee ||= assignable.detect {|a| a.is_a?(User) && a.firstname.to_s.downcase == firstname && a.lastname.to_s.downcase == lastname}
|
||||
end
|
||||
user
|
||||
if assignee.nil?
|
||||
assignee ||= assignable.detect {|a| a.is_a?(Group) && a.name.downcase == keyword}
|
||||
end
|
||||
assignee
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2009 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2011 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
|
||||
@@ -21,6 +21,7 @@ class Principal < ActiveRecord::Base
|
||||
has_many :members, :foreign_key => 'user_id', :dependent => :destroy
|
||||
has_many :memberships, :class_name => 'Member', :foreign_key => 'user_id', :include => [ :project, :roles ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name"
|
||||
has_many :projects, :through => :memberships
|
||||
has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
|
||||
|
||||
# Groups and active users
|
||||
named_scope :active, :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status = 1)"
|
||||
|
||||
@@ -426,9 +426,10 @@ class Project < ActiveRecord::Base
|
||||
Member.delete_all(['project_id = ?', id])
|
||||
end
|
||||
|
||||
# Users issues can be assigned to
|
||||
# Users/groups issues can be assigned to
|
||||
def assignable_users
|
||||
members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
|
||||
assignable = Setting.issue_group_assignment? ? member_principals : members
|
||||
assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
|
||||
end
|
||||
|
||||
# Returns the mail adresses of users that should be always notified on project events
|
||||
|
||||
+14
-6
@@ -223,15 +223,14 @@ class Query < ActiveRecord::Base
|
||||
"estimated_hours" => { :type => :float, :order => 13 },
|
||||
"done_ratio" => { :type => :integer, :order => 14 }}
|
||||
|
||||
user_values = []
|
||||
user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
|
||||
principals = []
|
||||
if project
|
||||
user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
|
||||
principals += project.principals.sort
|
||||
else
|
||||
all_projects = Project.visible.all
|
||||
if all_projects.any?
|
||||
# members of visible projects
|
||||
user_values += User.active.find(:all, :conditions => ["#{User.table_name}.id IN (SELECT DISTINCT user_id FROM members WHERE project_id IN (?))", all_projects.collect(&:id)]).sort.collect{|s| [s.name, s.id.to_s] }
|
||||
principals += Principal.active.find(:all, :conditions => ["#{User.table_name}.id IN (SELECT DISTINCT user_id FROM members WHERE project_id IN (?))", all_projects.collect(&:id)]).sort
|
||||
|
||||
# project filter
|
||||
project_values = []
|
||||
@@ -242,8 +241,17 @@ class Query < ActiveRecord::Base
|
||||
@available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty?
|
||||
end
|
||||
end
|
||||
@available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
|
||||
@available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
|
||||
users = principals.select {|p| p.is_a?(User)}
|
||||
|
||||
assigned_to_values = []
|
||||
assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
|
||||
assigned_to_values += (Setting.issue_group_assignment? ? principals : users).collect{|s| [s.name, s.id.to_s] }
|
||||
@available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => assigned_to_values } unless assigned_to_values.empty?
|
||||
|
||||
author_values = []
|
||||
author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
|
||||
author_values += users.collect{|s| [s.name, s.id.to_s] }
|
||||
@available_filters["author_id"] = { :type => :list, :order => 5, :values => author_values } unless author_values.empty?
|
||||
|
||||
group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
|
||||
@available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty?
|
||||
|
||||
+14
-4
@@ -45,7 +45,6 @@ class User < Principal
|
||||
|
||||
has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
|
||||
:after_remove => Proc.new {|user, group| group.user_removed(user)}
|
||||
has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
|
||||
has_many :changesets, :dependent => :nullify
|
||||
has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
|
||||
has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'"
|
||||
@@ -388,6 +387,17 @@ class User < Principal
|
||||
@projects_by_role
|
||||
end
|
||||
|
||||
# Returns true if user is arg or belongs to arg
|
||||
def is_or_belongs_to?(arg)
|
||||
if arg.is_a?(User)
|
||||
self == arg
|
||||
elsif arg.is_a?(Group)
|
||||
arg.users.include?(self)
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
# Return true if the user is allowed to do the specified action on a specific context
|
||||
# Action can be:
|
||||
# * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
|
||||
@@ -469,7 +479,7 @@ class User < Principal
|
||||
true
|
||||
when 'selected'
|
||||
# user receives notifications for created/assigned issues on unselected projects
|
||||
if object.is_a?(Issue) && (object.author == self || object.assigned_to == self)
|
||||
if object.is_a?(Issue) && (object.author == self || is_or_belongs_to?(object.assigned_to))
|
||||
true
|
||||
else
|
||||
false
|
||||
@@ -477,13 +487,13 @@ class User < Principal
|
||||
when 'none'
|
||||
false
|
||||
when 'only_my_events'
|
||||
if object.is_a?(Issue) && (object.author == self || object.assigned_to == self)
|
||||
if object.is_a?(Issue) && (object.author == self || is_or_belongs_to?(object.assigned_to))
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
when 'only_assigned'
|
||||
if object.is_a?(Issue) && object.assigned_to == self
|
||||
if object.is_a?(Issue) && is_or_belongs_to?(object.assigned_to)
|
||||
true
|
||||
else
|
||||
false
|
||||
|
||||
Reference in New Issue
Block a user