Role-based issue custom field visibility (#5037).
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@12012 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
parent
a74d55edd9
commit
628d05629b
|
@ -103,6 +103,7 @@ class IssuesController < ApplicationController
|
||||||
@journals = @issue.journals.includes(:user, :details).reorder("#{Journal.table_name}.id ASC").all
|
@journals = @issue.journals.includes(:user, :details).reorder("#{Journal.table_name}.id ASC").all
|
||||||
@journals.each_with_index {|j,i| j.indice = i+1}
|
@journals.each_with_index {|j,i| j.indice = i+1}
|
||||||
@journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
|
@journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
|
||||||
|
@journals.select! {|journal| journal.notes? || journal.visible_details.any?}
|
||||||
@journals.reverse! if User.current.wants_comments_in_reverse_order?
|
@journals.reverse! if User.current.wants_comments_in_reverse_order?
|
||||||
|
|
||||||
@changesets = @issue.changesets.visible.all
|
@changesets = @issue.changesets.visible.all
|
||||||
|
@ -230,7 +231,7 @@ class IssuesController < ApplicationController
|
||||||
else
|
else
|
||||||
@available_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
|
@available_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
|
||||||
end
|
end
|
||||||
@custom_fields = target_projects.map{|p|p.all_issue_custom_fields}.reduce(:&)
|
@custom_fields = target_projects.map{|p|p.all_issue_custom_fields.visible}.reduce(:&)
|
||||||
@assignables = target_projects.map(&:assignable_users).reduce(:&)
|
@assignables = target_projects.map(&:assignable_users).reduce(:&)
|
||||||
@trackers = target_projects.map(&:trackers).reduce(:&)
|
@trackers = target_projects.map(&:trackers).reduce(:&)
|
||||||
@versions = target_projects.map {|p| p.shared_versions.open}.reduce(:&)
|
@versions = target_projects.map {|p| p.shared_versions.open}.reduce(:&)
|
||||||
|
|
|
@ -160,12 +160,13 @@ module IssuesHelper
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_custom_fields_rows(issue)
|
def render_custom_fields_rows(issue)
|
||||||
return if issue.custom_field_values.empty?
|
values = issue.visible_custom_field_values
|
||||||
|
return if values.empty?
|
||||||
ordered_values = []
|
ordered_values = []
|
||||||
half = (issue.custom_field_values.size / 2.0).ceil
|
half = (values.size / 2.0).ceil
|
||||||
half.times do |i|
|
half.times do |i|
|
||||||
ordered_values << issue.custom_field_values[i]
|
ordered_values << values[i]
|
||||||
ordered_values << issue.custom_field_values[i + half]
|
ordered_values << values[i + half]
|
||||||
end
|
end
|
||||||
s = "<tr>\n"
|
s = "<tr>\n"
|
||||||
n = 0
|
n = 0
|
||||||
|
|
|
@ -22,11 +22,20 @@ module WorkflowsHelper
|
||||||
field.is_a?(CustomField) ? field.is_required? : %w(project_id tracker_id subject priority_id is_private).include?(field)
|
field.is_a?(CustomField) ? field.is_required? : %w(project_id tracker_id subject priority_id is_private).include?(field)
|
||||||
end
|
end
|
||||||
|
|
||||||
def field_permission_tag(permissions, status, field)
|
def field_permission_tag(permissions, status, field, role)
|
||||||
name = field.is_a?(CustomField) ? field.id.to_s : field
|
name = field.is_a?(CustomField) ? field.id.to_s : field
|
||||||
options = [["", ""], [l(:label_readonly), "readonly"]]
|
options = [["", ""], [l(:label_readonly), "readonly"]]
|
||||||
options << [l(:label_required), "required"] unless field_required?(field)
|
options << [l(:label_required), "required"] unless field_required?(field)
|
||||||
|
html_options = {}
|
||||||
|
selected = permissions[status.id][name]
|
||||||
|
|
||||||
select_tag("permissions[#{name}][#{status.id}]", options_for_select(options, permissions[status.id][name]))
|
hidden = field.is_a?(CustomField) && !field.visible? && !role.custom_fields.to_a.include?(field)
|
||||||
|
if hidden
|
||||||
|
options[0][0] = l(:label_hidden)
|
||||||
|
selected = ''
|
||||||
|
html_options[:disabled] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
select_tag("permissions[#{name}][#{status.id}]", options_for_select(options, selected), html_options)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -19,6 +19,7 @@ class CustomField < ActiveRecord::Base
|
||||||
include Redmine::SubclassFactory
|
include Redmine::SubclassFactory
|
||||||
|
|
||||||
has_many :custom_values, :dependent => :delete_all
|
has_many :custom_values, :dependent => :delete_all
|
||||||
|
has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}custom_fields_roles#{table_name_suffix}", :foreign_key => "custom_field_id"
|
||||||
acts_as_list :scope => 'type = \'#{self.class}\''
|
acts_as_list :scope => 'type = \'#{self.class}\''
|
||||||
serialize :possible_values
|
serialize :possible_values
|
||||||
|
|
||||||
|
@ -26,12 +27,31 @@ class CustomField < ActiveRecord::Base
|
||||||
validates_uniqueness_of :name, :scope => :type
|
validates_uniqueness_of :name, :scope => :type
|
||||||
validates_length_of :name, :maximum => 30
|
validates_length_of :name, :maximum => 30
|
||||||
validates_inclusion_of :field_format, :in => Redmine::CustomFieldFormat.available_formats
|
validates_inclusion_of :field_format, :in => Redmine::CustomFieldFormat.available_formats
|
||||||
|
|
||||||
validate :validate_custom_field
|
validate :validate_custom_field
|
||||||
|
|
||||||
before_validation :set_searchable
|
before_validation :set_searchable
|
||||||
after_save :handle_multiplicity_change
|
after_save :handle_multiplicity_change
|
||||||
|
after_save do |field|
|
||||||
|
if field.visible_changed? && field.visible
|
||||||
|
field.roles.clear
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
scope :sorted, lambda { order("#{table_name}.position ASC") }
|
scope :sorted, lambda { order("#{table_name}.position ASC") }
|
||||||
|
scope :visible, lambda {|*args|
|
||||||
|
user = args.shift || User.current
|
||||||
|
if user.admin?
|
||||||
|
# nop
|
||||||
|
elsif user.memberships.any?
|
||||||
|
where("#{table_name}.visible = ? OR #{table_name}.id IN (SELECT DISTINCT cfr.custom_field_id FROM #{Member.table_name} m" +
|
||||||
|
" INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
|
||||||
|
" INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
|
||||||
|
" WHERE m.user_id = ?)",
|
||||||
|
true, user.id)
|
||||||
|
else
|
||||||
|
where(:visible => true)
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
CUSTOM_FIELDS_TABS = [
|
CUSTOM_FIELDS_TABS = [
|
||||||
{:name => 'IssueCustomField', :partial => 'custom_fields/index',
|
{:name => 'IssueCustomField', :partial => 'custom_fields/index',
|
||||||
|
@ -215,6 +235,7 @@ class CustomField < ActiveRecord::Base
|
||||||
" ON #{join_alias}.customized_type = '#{self.class.customized_class.base_class.name}'" +
|
" ON #{join_alias}.customized_type = '#{self.class.customized_class.base_class.name}'" +
|
||||||
" AND #{join_alias}.customized_id = #{self.class.customized_class.table_name}.id" +
|
" AND #{join_alias}.customized_id = #{self.class.customized_class.table_name}.id" +
|
||||||
" AND #{join_alias}.custom_field_id = #{id}" +
|
" AND #{join_alias}.custom_field_id = #{id}" +
|
||||||
|
" AND (#{visibility_by_project_condition})" +
|
||||||
" AND #{join_alias}.value <> ''" +
|
" AND #{join_alias}.value <> ''" +
|
||||||
" AND #{join_alias}.id = (SELECT max(#{join_alias}_2.id) FROM #{CustomValue.table_name} #{join_alias}_2" +
|
" AND #{join_alias}.id = (SELECT max(#{join_alias}_2.id) FROM #{CustomValue.table_name} #{join_alias}_2" +
|
||||||
" WHERE #{join_alias}_2.customized_type = #{join_alias}.customized_type" +
|
" WHERE #{join_alias}_2.customized_type = #{join_alias}.customized_type" +
|
||||||
|
@ -227,6 +248,7 @@ class CustomField < ActiveRecord::Base
|
||||||
" ON #{join_alias}.customized_type = '#{self.class.customized_class.base_class.name}'" +
|
" ON #{join_alias}.customized_type = '#{self.class.customized_class.base_class.name}'" +
|
||||||
" AND #{join_alias}.customized_id = #{self.class.customized_class.table_name}.id" +
|
" AND #{join_alias}.customized_id = #{self.class.customized_class.table_name}.id" +
|
||||||
" AND #{join_alias}.custom_field_id = #{id}" +
|
" AND #{join_alias}.custom_field_id = #{id}" +
|
||||||
|
" AND (#{visibility_by_project_condition})" +
|
||||||
" AND #{join_alias}.value <> ''" +
|
" AND #{join_alias}.value <> ''" +
|
||||||
" AND #{join_alias}.id = (SELECT max(#{join_alias}_2.id) FROM #{CustomValue.table_name} #{join_alias}_2" +
|
" AND #{join_alias}.id = (SELECT max(#{join_alias}_2.id) FROM #{CustomValue.table_name} #{join_alias}_2" +
|
||||||
" WHERE #{join_alias}_2.customized_type = #{join_alias}.customized_type" +
|
" WHERE #{join_alias}_2.customized_type = #{join_alias}.customized_type" +
|
||||||
|
@ -237,6 +259,7 @@ class CustomField < ActiveRecord::Base
|
||||||
" ON #{join_alias}.customized_type = '#{self.class.customized_class.base_class.name}'" +
|
" ON #{join_alias}.customized_type = '#{self.class.customized_class.base_class.name}'" +
|
||||||
" AND #{join_alias}.customized_id = #{self.class.customized_class.table_name}.id" +
|
" AND #{join_alias}.customized_id = #{self.class.customized_class.table_name}.id" +
|
||||||
" AND #{join_alias}.custom_field_id = #{id}" +
|
" AND #{join_alias}.custom_field_id = #{id}" +
|
||||||
|
" AND (#{visibility_by_project_condition})" +
|
||||||
" AND #{join_alias}.id = (SELECT max(#{join_alias}_2.id) FROM #{CustomValue.table_name} #{join_alias}_2" +
|
" AND #{join_alias}.id = (SELECT max(#{join_alias}_2.id) FROM #{CustomValue.table_name} #{join_alias}_2" +
|
||||||
" WHERE #{join_alias}_2.customized_type = #{join_alias}.customized_type" +
|
" WHERE #{join_alias}_2.customized_type = #{join_alias}.customized_type" +
|
||||||
" AND #{join_alias}_2.customized_id = #{join_alias}.customized_id" +
|
" AND #{join_alias}_2.customized_id = #{join_alias}.customized_id" +
|
||||||
|
@ -254,6 +277,33 @@ class CustomField < ActiveRecord::Base
|
||||||
join_alias + "_" + field_format
|
join_alias + "_" + field_format
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def visibility_by_project_condition(project_key=nil, user=User.current)
|
||||||
|
if visible? || user.admin?
|
||||||
|
"1=1"
|
||||||
|
elsif user.anonymous?
|
||||||
|
"1=0"
|
||||||
|
else
|
||||||
|
project_key ||= "#{self.class.customized_class.table_name}.project_id"
|
||||||
|
"#{project_key} IN (SELECT DISTINCT m.project_id FROM #{Member.table_name} m" +
|
||||||
|
" INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
|
||||||
|
" INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
|
||||||
|
" WHERE m.user_id = #{user.id} AND cfr.custom_field_id = #{id})"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.visibility_condition
|
||||||
|
if user.admin?
|
||||||
|
"1=1"
|
||||||
|
elsif user.anonymous?
|
||||||
|
"#{table_name}.visible"
|
||||||
|
else
|
||||||
|
"#{project_key} IN (SELECT DISTINCT m.project_id FROM #{Member.table_name} m" +
|
||||||
|
" INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
|
||||||
|
" INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
|
||||||
|
" WHERE m.user_id = #{user.id} AND cfr.custom_field_id = #{id})"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def <=>(field)
|
def <=>(field)
|
||||||
position <=> field.position
|
position <=> field.position
|
||||||
end
|
end
|
||||||
|
|
|
@ -198,6 +198,13 @@ class Issue < ActiveRecord::Base
|
||||||
(project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
|
(project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def visible_custom_field_values(user=nil)
|
||||||
|
user_real = user || User.current
|
||||||
|
custom_field_values.select do |value|
|
||||||
|
value.custom_field.visible_by?(project, user_real)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Copies attributes from another issue, arg can be an id or an Issue
|
# Copies attributes from another issue, arg can be an id or an Issue
|
||||||
def copy_from(arg, options={})
|
def copy_from(arg, options={})
|
||||||
issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
|
issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
|
||||||
|
@ -445,11 +452,13 @@ class Issue < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
|
|
||||||
if attrs['custom_field_values'].present?
|
if attrs['custom_field_values'].present?
|
||||||
attrs['custom_field_values'] = attrs['custom_field_values'].reject {|k, v| read_only_attribute_names(user).include? k.to_s}
|
editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
|
||||||
|
attrs['custom_field_values'] = attrs['custom_field_values'].select {|k, v| editable_custom_field_ids.include? k.to_s}
|
||||||
end
|
end
|
||||||
|
|
||||||
if attrs['custom_fields'].present?
|
if attrs['custom_fields'].present?
|
||||||
attrs['custom_fields'] = attrs['custom_fields'].reject {|c| read_only_attribute_names(user).include? c['id'].to_s}
|
editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
|
||||||
|
attrs['custom_fields'] = attrs['custom_fields'].select {|c| editable_custom_field_ids.include? c['id'].to_s}
|
||||||
end
|
end
|
||||||
|
|
||||||
# mass-assignment security bypass
|
# mass-assignment security bypass
|
||||||
|
@ -462,7 +471,7 @@ class Issue < ActiveRecord::Base
|
||||||
|
|
||||||
# Returns the custom_field_values that can be edited by the given user
|
# Returns the custom_field_values that can be edited by the given user
|
||||||
def editable_custom_field_values(user=nil)
|
def editable_custom_field_values(user=nil)
|
||||||
custom_field_values.reject do |value|
|
visible_custom_field_values(user).reject do |value|
|
||||||
read_only_attribute_names(user).include?(value.custom_field_id.to_s)
|
read_only_attribute_names(user).include?(value.custom_field_id.to_s)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -790,6 +799,21 @@ class Issue < ActiveRecord::Base
|
||||||
notified_users.collect(&:mail)
|
notified_users.collect(&:mail)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def each_notification(users, &block)
|
||||||
|
if users.any?
|
||||||
|
if custom_field_values.detect {|value| !value.custom_field.visible?}
|
||||||
|
users_by_custom_field_visibility = users.group_by do |user|
|
||||||
|
visible_custom_field_values(user).map(&:custom_field_id).sort
|
||||||
|
end
|
||||||
|
users_by_custom_field_visibility.values.each do |users|
|
||||||
|
yield(users)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
yield(users)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Returns the number of hours spent on this issue
|
# Returns the number of hours spent on this issue
|
||||||
def spent_hours
|
def spent_hours
|
||||||
@spent_hours ||= time_entries.sum(:hours) || 0
|
@spent_hours ||= time_entries.sum(:hours) || 0
|
||||||
|
|
|
@ -23,5 +23,13 @@ class IssueCustomField < CustomField
|
||||||
def type_name
|
def type_name
|
||||||
:label_issue_plural
|
:label_issue_plural
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
|
def visible_by?(project, user=User.current)
|
||||||
|
visible? || user.admin? || (roles & user.roles_for_project(project)).present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_custom_field
|
||||||
|
super
|
||||||
|
errors.add(:base, l(:label_role_plural) + ' ' + l('activerecord.errors.messages.blank')) unless visible? || roles.present?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
|
@ -17,6 +17,6 @@
|
||||||
|
|
||||||
class IssueObserver < ActiveRecord::Observer
|
class IssueObserver < ActiveRecord::Observer
|
||||||
def after_create(issue)
|
def after_create(issue)
|
||||||
Mailer.issue_add(issue).deliver if Setting.notified_events.include?('issue_added')
|
Mailer.deliver_issue_add(issue) if Setting.notified_events.include?('issue_added')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -225,8 +225,8 @@ class IssueQuery < Query
|
||||||
@available_columns = self.class.available_columns.dup
|
@available_columns = self.class.available_columns.dup
|
||||||
@available_columns += (project ?
|
@available_columns += (project ?
|
||||||
project.all_issue_custom_fields :
|
project.all_issue_custom_fields :
|
||||||
IssueCustomField.all
|
IssueCustomField
|
||||||
).collect {|cf| QueryCustomFieldColumn.new(cf) }
|
).visible.collect {|cf| QueryCustomFieldColumn.new(cf) }
|
||||||
|
|
||||||
if User.current.allowed_to?(:view_time_entries, project, :global => true)
|
if User.current.allowed_to?(:view_time_entries, project, :global => true)
|
||||||
index = nil
|
index = nil
|
||||||
|
|
|
@ -53,6 +53,18 @@ class Journal < ActiveRecord::Base
|
||||||
(details.empty? && notes.blank?) ? false : super
|
(details.empty? && notes.blank?) ? false : super
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def visible_details(user=User.current)
|
||||||
|
details.select do |detail|
|
||||||
|
if detail.property == 'cf'
|
||||||
|
field_id = detail.prop_key
|
||||||
|
field = CustomField.find_by_id(field_id)
|
||||||
|
field && field.visible_by?(project, user)
|
||||||
|
else
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Returns the new status if the journal contains a status change, otherwise nil
|
# Returns the new status if the journal contains a status change, otherwise nil
|
||||||
def new_status
|
def new_status
|
||||||
c = details.detect {|detail| detail.prop_key == 'status_id'}
|
c = details.detect {|detail| detail.prop_key == 'status_id'}
|
||||||
|
@ -93,20 +105,28 @@ class Journal < ActiveRecord::Base
|
||||||
@notify = arg
|
@notify = arg
|
||||||
end
|
end
|
||||||
|
|
||||||
def recipients
|
def notified_users
|
||||||
notified = journalized.notified_users
|
notified = journalized.notified_users
|
||||||
if private_notes?
|
if private_notes?
|
||||||
notified = notified.select {|user| user.allowed_to?(:view_private_notes, journalized.project)}
|
notified = notified.select {|user| user.allowed_to?(:view_private_notes, journalized.project)}
|
||||||
end
|
end
|
||||||
notified.map(&:mail)
|
notified
|
||||||
end
|
end
|
||||||
|
|
||||||
def watcher_recipients
|
def recipients
|
||||||
|
notified_users.map(&:mail)
|
||||||
|
end
|
||||||
|
|
||||||
|
def notified_watchers
|
||||||
notified = journalized.notified_watchers
|
notified = journalized.notified_watchers
|
||||||
if private_notes?
|
if private_notes?
|
||||||
notified = notified.select {|user| user.allowed_to?(:view_private_notes, journalized.project)}
|
notified = notified.select {|user| user.allowed_to?(:view_private_notes, journalized.project)}
|
||||||
end
|
end
|
||||||
notified.map(&:mail)
|
notified
|
||||||
|
end
|
||||||
|
|
||||||
|
def watcher_recipients
|
||||||
|
notified_watchers.map(&:mail)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -23,7 +23,7 @@ class JournalObserver < ActiveRecord::Observer
|
||||||
(Setting.notified_events.include?('issue_status_updated') && journal.new_status.present?) ||
|
(Setting.notified_events.include?('issue_status_updated') && journal.new_status.present?) ||
|
||||||
(Setting.notified_events.include?('issue_priority_updated') && journal.new_value_for('priority_id').present?)
|
(Setting.notified_events.include?('issue_priority_updated') && journal.new_value_for('priority_id').present?)
|
||||||
)
|
)
|
||||||
Mailer.issue_edit(journal).deliver
|
Mailer.deliver_issue_edit(journal)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -136,7 +136,7 @@ class MailHandler < ActionMailer::Base
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
MESSAGE_ID_RE = %r{^<?redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
|
MESSAGE_ID_RE = %r{^<?redmine\.([a-z0-9_]+)\-(\d+)\.\d+(\.[a-f0-9]+)?@}
|
||||||
ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
|
ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
|
||||||
MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
|
MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
|
||||||
|
|
||||||
|
|
|
@ -27,34 +27,35 @@ class Mailer < ActionMailer::Base
|
||||||
{ :host => Setting.host_name, :protocol => Setting.protocol }
|
{ :host => Setting.host_name, :protocol => Setting.protocol }
|
||||||
end
|
end
|
||||||
|
|
||||||
# Builds a Mail::Message object used to email recipients of the added issue.
|
# Builds a mail for notifying to_users and cc_users about a new issue
|
||||||
#
|
def issue_add(issue, to_users, cc_users)
|
||||||
# Example:
|
|
||||||
# issue_add(issue) => Mail::Message object
|
|
||||||
# Mailer.issue_add(issue).deliver => sends an email to issue recipients
|
|
||||||
def issue_add(issue)
|
|
||||||
redmine_headers 'Project' => issue.project.identifier,
|
redmine_headers 'Project' => issue.project.identifier,
|
||||||
'Issue-Id' => issue.id,
|
'Issue-Id' => issue.id,
|
||||||
'Issue-Author' => issue.author.login
|
'Issue-Author' => issue.author.login
|
||||||
redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
|
redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
|
||||||
message_id issue
|
message_id issue
|
||||||
|
references issue
|
||||||
@author = issue.author
|
@author = issue.author
|
||||||
@issue = issue
|
@issue = issue
|
||||||
|
@users = to_users + cc_users
|
||||||
@issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue)
|
@issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue)
|
||||||
recipients = issue.recipients
|
mail :to => to_users.map(&:mail),
|
||||||
cc = issue.watcher_recipients - recipients
|
:cc => cc_users.map(&:mail),
|
||||||
mail :to => recipients,
|
|
||||||
:cc => cc,
|
|
||||||
:subject => "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
|
:subject => "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
|
||||||
end
|
end
|
||||||
|
|
||||||
# Builds a Mail::Message object used to email recipients of the edited issue.
|
# Notifies users about a new issue
|
||||||
#
|
def self.deliver_issue_add(issue)
|
||||||
# Example:
|
to = issue.notified_users
|
||||||
# issue_edit(journal) => Mail::Message object
|
cc = issue.notified_watchers - to
|
||||||
# Mailer.issue_edit(journal).deliver => sends an email to issue recipients
|
issue.each_notification(to + cc) do |users|
|
||||||
def issue_edit(journal)
|
Mailer.issue_add(issue, to & users, cc & users).deliver
|
||||||
issue = journal.journalized.reload
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Builds a mail for notifying to_users and cc_users about an issue update
|
||||||
|
def issue_edit(journal, to_users, cc_users)
|
||||||
|
issue = journal.journalized
|
||||||
redmine_headers 'Project' => issue.project.identifier,
|
redmine_headers 'Project' => issue.project.identifier,
|
||||||
'Issue-Id' => issue.id,
|
'Issue-Id' => issue.id,
|
||||||
'Issue-Author' => issue.author.login
|
'Issue-Author' => issue.author.login
|
||||||
|
@ -62,20 +63,30 @@ class Mailer < ActionMailer::Base
|
||||||
message_id journal
|
message_id journal
|
||||||
references issue
|
references issue
|
||||||
@author = journal.user
|
@author = journal.user
|
||||||
recipients = journal.recipients
|
|
||||||
# Watchers in cc
|
|
||||||
cc = journal.watcher_recipients - recipients
|
|
||||||
s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] "
|
s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] "
|
||||||
s << "(#{issue.status.name}) " if journal.new_value_for('status_id')
|
s << "(#{issue.status.name}) " if journal.new_value_for('status_id')
|
||||||
s << issue.subject
|
s << issue.subject
|
||||||
@issue = issue
|
@issue = issue
|
||||||
|
@users = to_users + cc_users
|
||||||
@journal = journal
|
@journal = journal
|
||||||
|
@journal_details = journal.visible_details(@users.first)
|
||||||
@issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue, :anchor => "change-#{journal.id}")
|
@issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue, :anchor => "change-#{journal.id}")
|
||||||
mail :to => recipients,
|
mail :to => to_users.map(&:mail),
|
||||||
:cc => cc,
|
:cc => cc_users.map(&:mail),
|
||||||
:subject => s
|
:subject => s
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Notifies users about an issue update
|
||||||
|
def self.deliver_issue_edit(journal)
|
||||||
|
issue = journal.journalized.reload
|
||||||
|
to = journal.notified_users
|
||||||
|
cc = journal.notified_watchers
|
||||||
|
issue.each_notification(to + cc) do |users|
|
||||||
|
next unless journal.notes? || journal.visible_details(users.first).any?
|
||||||
|
Mailer.issue_edit(journal, to & users, cc & users).deliver
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def reminder(user, issues, days)
|
def reminder(user, issues, days)
|
||||||
set_language_if_valid user.language
|
set_language_if_valid user.language
|
||||||
@issues = issues
|
@issues = issues
|
||||||
|
@ -142,6 +153,7 @@ class Mailer < ActionMailer::Base
|
||||||
redmine_headers 'Project' => news.project.identifier
|
redmine_headers 'Project' => news.project.identifier
|
||||||
@author = news.author
|
@author = news.author
|
||||||
message_id news
|
message_id news
|
||||||
|
references news
|
||||||
@news = news
|
@news = news
|
||||||
@news_url = url_for(:controller => 'news', :action => 'show', :id => news)
|
@news_url = url_for(:controller => 'news', :action => 'show', :id => news)
|
||||||
mail :to => news.recipients,
|
mail :to => news.recipients,
|
||||||
|
@ -158,6 +170,7 @@ class Mailer < ActionMailer::Base
|
||||||
redmine_headers 'Project' => news.project.identifier
|
redmine_headers 'Project' => news.project.identifier
|
||||||
@author = comment.author
|
@author = comment.author
|
||||||
message_id comment
|
message_id comment
|
||||||
|
references news
|
||||||
@news = news
|
@news = news
|
||||||
@comment = comment
|
@comment = comment
|
||||||
@news_url = url_for(:controller => 'news', :action => 'show', :id => news)
|
@news_url = url_for(:controller => 'news', :action => 'show', :id => news)
|
||||||
|
@ -176,7 +189,7 @@ class Mailer < ActionMailer::Base
|
||||||
'Topic-Id' => (message.parent_id || message.id)
|
'Topic-Id' => (message.parent_id || message.id)
|
||||||
@author = message.author
|
@author = message.author
|
||||||
message_id message
|
message_id message
|
||||||
references message.parent unless message.parent.nil?
|
references message.root
|
||||||
recipients = message.recipients
|
recipients = message.recipients
|
||||||
cc = ((message.root.watcher_recipients + message.board.watcher_recipients).uniq - recipients)
|
cc = ((message.root.watcher_recipients + message.board.watcher_recipients).uniq - recipients)
|
||||||
@message = message
|
@message = message
|
||||||
|
@ -386,7 +399,7 @@ class Mailer < ActionMailer::Base
|
||||||
headers[:message_id] = "<#{self.class.message_id_for(@message_id_object)}>"
|
headers[:message_id] = "<#{self.class.message_id_for(@message_id_object)}>"
|
||||||
end
|
end
|
||||||
if @references_objects
|
if @references_objects
|
||||||
headers[:references] = @references_objects.collect {|o| "<#{self.class.message_id_for(o)}>"}.join(' ')
|
headers[:references] = @references_objects.collect {|o| "<#{self.class.references_for(o)}>"}.join(' ')
|
||||||
end
|
end
|
||||||
|
|
||||||
super headers do |format|
|
super headers do |format|
|
||||||
|
@ -434,15 +447,30 @@ class Mailer < ActionMailer::Base
|
||||||
h.each { |k,v| headers["X-Redmine-#{k}"] = v.to_s }
|
h.each { |k,v| headers["X-Redmine-#{k}"] = v.to_s }
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns a predictable Message-Id for the given object
|
def self.token_for(object, rand=true)
|
||||||
def self.message_id_for(object)
|
|
||||||
# id + timestamp should reduce the odds of a collision
|
|
||||||
# as far as we don't send multiple emails for the same object
|
|
||||||
timestamp = object.send(object.respond_to?(:created_on) ? :created_on : :updated_on)
|
timestamp = object.send(object.respond_to?(:created_on) ? :created_on : :updated_on)
|
||||||
hash = "redmine.#{object.class.name.demodulize.underscore}-#{object.id}.#{timestamp.strftime("%Y%m%d%H%M%S")}"
|
hash = [
|
||||||
|
"redmine",
|
||||||
|
"#{object.class.name.demodulize.underscore}-#{object.id}",
|
||||||
|
timestamp.strftime("%Y%m%d%H%M%S")
|
||||||
|
]
|
||||||
|
if rand
|
||||||
|
hash << Redmine::Utils.random_hex(8)
|
||||||
|
end
|
||||||
host = Setting.mail_from.to_s.gsub(%r{^.*@}, '')
|
host = Setting.mail_from.to_s.gsub(%r{^.*@}, '')
|
||||||
host = "#{::Socket.gethostname}.redmine" if host.empty?
|
host = "#{::Socket.gethostname}.redmine" if host.empty?
|
||||||
"#{hash}@#{host}"
|
"#{hash.join('.')}@#{host}"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns a Message-Id for the given object
|
||||||
|
def self.message_id_for(object)
|
||||||
|
token_for(object, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns a uniq token for a given object referenced by all notifications
|
||||||
|
# related to this object
|
||||||
|
def self.references_for(object)
|
||||||
|
token_for(object, false)
|
||||||
end
|
end
|
||||||
|
|
||||||
def message_id(object)
|
def message_id(object)
|
||||||
|
|
|
@ -81,8 +81,12 @@ class QueryCustomFieldColumn < QueryColumn
|
||||||
end
|
end
|
||||||
|
|
||||||
def value(object)
|
def value(object)
|
||||||
cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
|
if custom_field.visible_by?(object.project, User.current)
|
||||||
cv.size > 1 ? cv.sort {|a,b| a.to_s <=> b.to_s} : cv.first
|
cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
|
||||||
|
cv.size > 1 ? cv.sort {|a,b| a.to_s <=> b.to_s} : cv.first
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def css_classes
|
def css_classes
|
||||||
|
@ -560,6 +564,11 @@ class Query < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
end if filters and valid?
|
end if filters and valid?
|
||||||
|
|
||||||
|
if (c = group_by_column) && c.is_a?(QueryCustomFieldColumn)
|
||||||
|
# Excludes results for which the grouped custom field is not visible
|
||||||
|
filters_clauses << c.custom_field.visibility_by_project_condition
|
||||||
|
end
|
||||||
|
|
||||||
filters_clauses << project_statement
|
filters_clauses << project_statement
|
||||||
filters_clauses.reject!(&:blank?)
|
filters_clauses.reject!(&:blank?)
|
||||||
|
|
||||||
|
@ -596,7 +605,10 @@ class Query < ActiveRecord::Base
|
||||||
if operator =~ /[<>]/
|
if operator =~ /[<>]/
|
||||||
where = "(#{where}) AND #{db_table}.#{db_field} <> ''"
|
where = "(#{where}) AND #{db_table}.#{db_field} <> ''"
|
||||||
end
|
end
|
||||||
"#{queried_table_name}.#{customized_key} #{not_in} IN (SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='#{customized_class}' AND #{db_table}.customized_id=#{customized_class.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id} WHERE #{where})"
|
"#{queried_table_name}.#{customized_key} #{not_in} IN (" +
|
||||||
|
"SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name}" +
|
||||||
|
" LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='#{customized_class}' AND #{db_table}.customized_id=#{customized_class.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id}" +
|
||||||
|
" WHERE (#{where}) AND (#{filter[:field].visibility_by_project_condition}))"
|
||||||
end
|
end
|
||||||
|
|
||||||
# Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
|
# Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
|
||||||
|
@ -785,14 +797,14 @@ class Query < ActiveRecord::Base
|
||||||
|
|
||||||
# Adds filters for the given custom fields scope
|
# Adds filters for the given custom fields scope
|
||||||
def add_custom_fields_filters(scope, assoc=nil)
|
def add_custom_fields_filters(scope, assoc=nil)
|
||||||
scope.where(:is_filter => true).sorted.each do |field|
|
scope.visible.where(:is_filter => true).sorted.each do |field|
|
||||||
add_custom_field_filter(field, assoc)
|
add_custom_field_filter(field, assoc)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Adds filters for the given associations custom fields
|
# Adds filters for the given associations custom fields
|
||||||
def add_associations_custom_fields_filters(*associations)
|
def add_associations_custom_fields_filters(*associations)
|
||||||
fields_by_class = CustomField.where(:is_filter => true).group_by(&:class)
|
fields_by_class = CustomField.visible.where(:is_filter => true).group_by(&:class)
|
||||||
associations.each do |assoc|
|
associations.each do |assoc|
|
||||||
association_klass = queried_class.reflect_on_association(assoc).klass
|
association_klass = queried_class.reflect_on_association(assoc).klass
|
||||||
fields_by_class.each do |field_class, fields|
|
fields_by_class.each do |field_class, fields|
|
||||||
|
|
|
@ -52,6 +52,7 @@ class Role < ActiveRecord::Base
|
||||||
WorkflowRule.copy(nil, source_role, nil, proxy_association.owner)
|
WorkflowRule.copy(nil, source_role, nil, proxy_association.owner)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
has_and_belongs_to_many :custom_fields, :join_table => "#{table_name_prefix}custom_fields_roles#{table_name_suffix}", :foreign_key => "role_id"
|
||||||
|
|
||||||
has_many :member_roles, :dependent => :destroy
|
has_many :member_roles, :dependent => :destroy
|
||||||
has_many :members, :through => :member_roles
|
has_many :members, :through => :member_roles
|
||||||
|
|
|
@ -91,8 +91,8 @@ class TimeEntryQuery < Query
|
||||||
def available_columns
|
def available_columns
|
||||||
return @available_columns if @available_columns
|
return @available_columns if @available_columns
|
||||||
@available_columns = self.class.available_columns.dup
|
@available_columns = self.class.available_columns.dup
|
||||||
@available_columns += TimeEntryCustomField.all.map {|cf| QueryCustomFieldColumn.new(cf) }
|
@available_columns += TimeEntryCustomField.visible.all.map {|cf| QueryCustomFieldColumn.new(cf) }
|
||||||
@available_columns += IssueCustomField.all.map {|cf| QueryAssociationCustomFieldColumn.new(:issue, cf) }
|
@available_columns += IssueCustomField.visible.all.map {|cf| QueryAssociationCustomFieldColumn.new(:issue, cf) }
|
||||||
@available_columns
|
@available_columns
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -64,6 +64,24 @@ when "IssueCustomField" %>
|
||||||
<p><%= f.check_box :is_for_all %></p>
|
<p><%= f.check_box :is_for_all %></p>
|
||||||
<p><%= f.check_box :is_filter %></p>
|
<p><%= f.check_box :is_filter %></p>
|
||||||
<p><%= f.check_box :searchable %></p>
|
<p><%= f.check_box :searchable %></p>
|
||||||
|
<p>
|
||||||
|
<label><%= l(:field_visible) %></label>
|
||||||
|
<label class="block">
|
||||||
|
<%= radio_button_tag 'custom_field[visible]', 1, @custom_field.visible?, :id => 'custom_field_visible_on' %>
|
||||||
|
<%= l(:label_visibility_public) %>
|
||||||
|
</label>
|
||||||
|
<label class="block">
|
||||||
|
<%= radio_button_tag 'custom_field[visible]', 0, !@custom_field.visible?, :id => 'custom_field_visible_off' %>
|
||||||
|
<%= l(:label_visibility_roles) %>:
|
||||||
|
</label>
|
||||||
|
<% Role.givable.sorted.each do |role| %>
|
||||||
|
<label class="block custom_field_role" style="padding-left:2em;">
|
||||||
|
<%= check_box_tag 'custom_field[role_ids][]', role.id, @custom_field.roles.include?(role) %>
|
||||||
|
<%= role.name %>
|
||||||
|
</label>
|
||||||
|
<% end %>
|
||||||
|
<%= hidden_field_tag 'custom_field[role_ids][]', '' %>
|
||||||
|
</p>
|
||||||
|
|
||||||
<% when "UserCustomField" %>
|
<% when "UserCustomField" %>
|
||||||
<p><%= f.check_box :is_required %></p>
|
<p><%= f.check_box :is_required %></p>
|
||||||
|
@ -97,3 +115,12 @@ when "IssueCustomField" %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% include_calendar_headers_tags %>
|
<% include_calendar_headers_tags %>
|
||||||
|
|
||||||
|
<%= javascript_tag do %>
|
||||||
|
function toggleCustomFieldRoles(){
|
||||||
|
var checked = $("#custom_field_visible_on").is(':checked');
|
||||||
|
$('.custom_field_role input').attr('disabled', checked);
|
||||||
|
}
|
||||||
|
$("#custom_field_visible_on, #custom_field_visible_off").change(toggleCustomFieldRoles);
|
||||||
|
$(document).ready(toggleCustomFieldRoles);
|
||||||
|
<% end %>
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
<% if journal.details.any? %>
|
<% if journal.details.any? %>
|
||||||
<ul class="details">
|
<ul class="details">
|
||||||
<% details_to_strings(journal.details).each do |string| %>
|
<% details_to_strings(journal.visible_details).each do |string| %>
|
||||||
<li><%= string %></li>
|
<li><%= string %></li>
|
||||||
<% end %>
|
<% end %>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -19,7 +19,7 @@ api.array :issues, api_meta(:total_count => @issue_count, :offset => @offset, :l
|
||||||
api.done_ratio issue.done_ratio
|
api.done_ratio issue.done_ratio
|
||||||
api.estimated_hours issue.estimated_hours
|
api.estimated_hours issue.estimated_hours
|
||||||
|
|
||||||
render_api_custom_values issue.custom_field_values, api
|
render_api_custom_values issue.visible_custom_field_values, api
|
||||||
|
|
||||||
api.created_on issue.created_on
|
api.created_on issue.created_on
|
||||||
api.updated_on issue.updated_on
|
api.updated_on issue.updated_on
|
||||||
|
|
|
@ -18,7 +18,7 @@ api.issue do
|
||||||
api.estimated_hours @issue.estimated_hours
|
api.estimated_hours @issue.estimated_hours
|
||||||
api.spent_hours(@issue.spent_hours) if User.current.allowed_to?(:view_time_entries, @project)
|
api.spent_hours(@issue.spent_hours) if User.current.allowed_to?(:view_time_entries, @project)
|
||||||
|
|
||||||
render_api_custom_values @issue.custom_field_values, api
|
render_api_custom_values @issue.visible_custom_field_values, api
|
||||||
|
|
||||||
api.created_on @issue.created_on
|
api.created_on @issue.created_on
|
||||||
api.updated_on @issue.updated_on
|
api.updated_on @issue.updated_on
|
||||||
|
@ -55,7 +55,7 @@ api.issue do
|
||||||
api.notes journal.notes
|
api.notes journal.notes
|
||||||
api.created_on journal.created_on
|
api.created_on journal.created_on
|
||||||
api.array :details do
|
api.array :details do
|
||||||
journal.details.each do |detail|
|
journal.visible_details.each do |detail|
|
||||||
api.detail :property => detail.property, :name => detail.prop_key do
|
api.detail :property => detail.property, :name => detail.prop_key do
|
||||||
api.old_value detail.old_value
|
api.old_value detail.old_value
|
||||||
api.new_value detail.value
|
api.new_value detail.value
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<li><%=l(:field_assigned_to)%>: <%=h issue.assigned_to %></li>
|
<li><%=l(:field_assigned_to)%>: <%=h issue.assigned_to %></li>
|
||||||
<li><%=l(:field_category)%>: <%=h issue.category %></li>
|
<li><%=l(:field_category)%>: <%=h issue.category %></li>
|
||||||
<li><%=l(:field_fixed_version)%>: <%=h issue.fixed_version %></li>
|
<li><%=l(:field_fixed_version)%>: <%=h issue.fixed_version %></li>
|
||||||
<% issue.custom_field_values.each do |c| %>
|
<% issue.visible_custom_field_values(users.first).each do |c| %>
|
||||||
<li><%=h c.custom_field.name %>: <%=h show_value(c) %></li>
|
<li><%=h c.custom_field.name %>: <%=h show_value(c) %></li>
|
||||||
<% end %>
|
<% end %>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
* <%=l(:field_assigned_to)%>: <%= issue.assigned_to %>
|
* <%=l(:field_assigned_to)%>: <%= issue.assigned_to %>
|
||||||
* <%=l(:field_category)%>: <%= issue.category %>
|
* <%=l(:field_category)%>: <%= issue.category %>
|
||||||
* <%=l(:field_fixed_version)%>: <%= issue.fixed_version %>
|
* <%=l(:field_fixed_version)%>: <%= issue.fixed_version %>
|
||||||
<% issue.custom_field_values.each do |c| %>* <%= c.custom_field.name %>: <%= show_value(c) %>
|
<% issue.visible_custom_field_values(users.first).each do |c| %>* <%= c.custom_field.name %>: <%= show_value(c) %>
|
||||||
<% end -%>
|
<% end -%>
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
<%= issue.description %>
|
<%= issue.description %>
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
<%= l(:text_issue_added, :id => "##{@issue.id}", :author => h(@issue.author)) %>
|
<%= l(:text_issue_added, :id => "##{@issue.id}", :author => h(@issue.author)) %>
|
||||||
<hr />
|
<hr />
|
||||||
<%= render :partial => 'issue', :formats => [:html], :locals => { :issue => @issue, :issue_url => @issue_url } %>
|
<%= render :partial => 'issue', :formats => [:html], :locals => { :issue => @issue, :users => @users, :issue_url => @issue_url } %>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<%= l(:text_issue_added, :id => "##{@issue.id}", :author => @issue.author) %>
|
<%= l(:text_issue_added, :id => "##{@issue.id}", :author => @issue.author) %>
|
||||||
|
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
<%= render :partial => 'issue', :formats => [:text], :locals => { :issue => @issue, :issue_url => @issue_url } %>
|
<%= render :partial => 'issue', :formats => [:text], :locals => { :issue => @issue, :users => @users, :issue_url => @issue_url } %>
|
||||||
|
|
|
@ -4,11 +4,11 @@
|
||||||
<%= l(:text_issue_updated, :id => "##{@issue.id}", :author => h(@journal.user)) %>
|
<%= l(:text_issue_updated, :id => "##{@issue.id}", :author => h(@journal.user)) %>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<% details_to_strings(@journal.details, false, :only_path => false).each do |string| %>
|
<% details_to_strings(@journal_details, false, :only_path => false).each do |string| %>
|
||||||
<li><%= string %></li>
|
<li><%= string %></li>
|
||||||
<% end %>
|
<% end %>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<%= textilizable(@journal, :notes, :only_path => false) %>
|
<%= textilizable(@journal, :notes, :only_path => false) %>
|
||||||
<hr />
|
<hr />
|
||||||
<%= render :partial => 'issue', :formats => [:html], :locals => { :issue => @issue, :issue_url => @issue_url } %>
|
<%= render :partial => 'issue', :formats => [:html], :locals => { :issue => @issue, :users => @users, :issue_url => @issue_url } %>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<%= "(#{l(:field_private_notes)}) " if @journal.private_notes? -%><%= l(:text_issue_updated, :id => "##{@issue.id}", :author => @journal.user) %>
|
<%= "(#{l(:field_private_notes)}) " if @journal.private_notes? -%><%= l(:text_issue_updated, :id => "##{@issue.id}", :author => @journal.user) %>
|
||||||
|
|
||||||
<% details_to_strings(@journal.details, true).each do |string| -%>
|
<% details_to_strings(@journal_details, true).each do |string| -%>
|
||||||
<%= string %>
|
<%= string %>
|
||||||
<% end -%>
|
<% end -%>
|
||||||
|
|
||||||
|
@ -9,4 +9,4 @@
|
||||||
|
|
||||||
<% end -%>
|
<% end -%>
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
<%= render :partial => 'issue', :formats => [:text], :locals => { :issue => @issue, :issue_url => @issue_url } %>
|
<%= render :partial => 'issue', :formats => [:text], :locals => { :issue => @issue, :users => @users, :issue_url => @issue_url } %>
|
||||||
|
|
|
@ -62,7 +62,7 @@
|
||||||
</td>
|
</td>
|
||||||
<% for status in @statuses -%>
|
<% for status in @statuses -%>
|
||||||
<td align="center" class="<%= @permissions[status.id][field] %>">
|
<td align="center" class="<%= @permissions[status.id][field] %>">
|
||||||
<%= field_permission_tag(@permissions, status, field) %>
|
<%= field_permission_tag(@permissions, status, field, @role) %>
|
||||||
<% unless status == @statuses.last %><a href="#" class="repeat-value">»</a><% end %>
|
<% unless status == @statuses.last %><a href="#" class="repeat-value">»</a><% end %>
|
||||||
</td>
|
</td>
|
||||||
<% end -%>
|
<% end -%>
|
||||||
|
@ -82,7 +82,7 @@
|
||||||
</td>
|
</td>
|
||||||
<% for status in @statuses -%>
|
<% for status in @statuses -%>
|
||||||
<td align="center" class="<%= @permissions[status.id][field.id.to_s] %>">
|
<td align="center" class="<%= @permissions[status.id][field.id.to_s] %>">
|
||||||
<%= field_permission_tag(@permissions, status, field) %>
|
<%= field_permission_tag(@permissions, status, field, @role) %>
|
||||||
<% unless status == @statuses.last %><a href="#" class="repeat-value">»</a><% end %>
|
<% unless status == @statuses.last %><a href="#" class="repeat-value">»</a><% end %>
|
||||||
</td>
|
</td>
|
||||||
<% end -%>
|
<% end -%>
|
||||||
|
|
|
@ -885,6 +885,7 @@ en:
|
||||||
label_fields_permissions: Fields permissions
|
label_fields_permissions: Fields permissions
|
||||||
label_readonly: Read-only
|
label_readonly: Read-only
|
||||||
label_required: Required
|
label_required: Required
|
||||||
|
label_hidden: Hidden
|
||||||
label_attribute_of_project: "Project's %{name}"
|
label_attribute_of_project: "Project's %{name}"
|
||||||
label_attribute_of_issue: "Issue's %{name}"
|
label_attribute_of_issue: "Issue's %{name}"
|
||||||
label_attribute_of_author: "Author's %{name}"
|
label_attribute_of_author: "Author's %{name}"
|
||||||
|
|
|
@ -861,6 +861,7 @@ fr:
|
||||||
label_fields_permissions: Permissions sur les champs
|
label_fields_permissions: Permissions sur les champs
|
||||||
label_readonly: Lecture
|
label_readonly: Lecture
|
||||||
label_required: Obligatoire
|
label_required: Obligatoire
|
||||||
|
label_hidden: Caché
|
||||||
label_attribute_of_project: "%{name} du projet"
|
label_attribute_of_project: "%{name} du projet"
|
||||||
label_attribute_of_issue: "%{name} de la demande"
|
label_attribute_of_issue: "%{name} de la demande"
|
||||||
label_attribute_of_author: "%{name} de l'auteur"
|
label_attribute_of_author: "%{name} de l'auteur"
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
class CreateCustomFieldsRoles < ActiveRecord::Migration
|
||||||
|
def self.up
|
||||||
|
create_table :custom_fields_roles, :id => false do |t|
|
||||||
|
t.column :custom_field_id, :integer, :null => false
|
||||||
|
t.column :role_id, :integer, :null => false
|
||||||
|
end
|
||||||
|
add_index :custom_fields_roles, [:custom_field_id, :role_id], :unique => true, :name => :custom_fields_roles_ids
|
||||||
|
CustomField.update_all({:visible => true}, {:type => 'IssueCustomField'})
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.down
|
||||||
|
drop_table :custom_fields_roles
|
||||||
|
end
|
||||||
|
end
|
|
@ -81,12 +81,13 @@ module Redmine
|
||||||
token_clauses = columns.collect {|column| "(LOWER(#{column}) LIKE ?)"}
|
token_clauses = columns.collect {|column| "(LOWER(#{column}) LIKE ?)"}
|
||||||
|
|
||||||
if !options[:titles_only] && searchable_options[:search_custom_fields]
|
if !options[:titles_only] && searchable_options[:search_custom_fields]
|
||||||
searchable_custom_field_ids = CustomField.where(:type => "#{self.name}CustomField", :searchable => true).pluck(:id)
|
searchable_custom_fields = CustomField.where(:type => "#{self.name}CustomField", :searchable => true)
|
||||||
if searchable_custom_field_ids.any?
|
searchable_custom_fields.each do |field|
|
||||||
custom_field_sql = "#{table_name}.id IN (SELECT customized_id FROM #{CustomValue.table_name}" +
|
sql = "#{table_name}.id IN (SELECT customized_id FROM #{CustomValue.table_name}" +
|
||||||
" WHERE customized_type='#{self.name}' AND customized_id=#{table_name}.id AND LOWER(value) LIKE ?" +
|
" WHERE customized_type='#{self.name}' AND customized_id=#{table_name}.id AND LOWER(value) LIKE ?" +
|
||||||
" AND #{CustomValue.table_name}.custom_field_id IN (#{searchable_custom_field_ids.join(',')}))"
|
" AND #{CustomValue.table_name}.custom_field_id = #{field.id})" +
|
||||||
token_clauses << custom_field_sql
|
" AND #{field.visibility_by_project_condition(searchable_options[:project_key], user)}"
|
||||||
|
token_clauses << sql
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -256,7 +256,7 @@ module Redmine
|
||||||
def fetch_row_values(issue, query, level)
|
def fetch_row_values(issue, query, level)
|
||||||
query.inline_columns.collect do |column|
|
query.inline_columns.collect do |column|
|
||||||
s = if column.is_a?(QueryCustomFieldColumn)
|
s = if column.is_a?(QueryCustomFieldColumn)
|
||||||
cv = issue.custom_field_values.detect {|v| v.custom_field_id == column.custom_field.id}
|
cv = issue.visible_custom_field_values.detect {|v| v.custom_field_id == column.custom_field.id}
|
||||||
show_value(cv)
|
show_value(cv)
|
||||||
else
|
else
|
||||||
value = issue.send(column.name)
|
value = issue.send(column.name)
|
||||||
|
@ -571,8 +571,8 @@ module Redmine
|
||||||
right << nil
|
right << nil
|
||||||
end
|
end
|
||||||
|
|
||||||
half = (issue.custom_field_values.size / 2.0).ceil
|
half = (issue.visible_custom_field_values.size / 2.0).ceil
|
||||||
issue.custom_field_values.each_with_index do |custom_value, i|
|
issue.visible_custom_field_values.each_with_index do |custom_value, i|
|
||||||
(i < half ? left : right) << [custom_value.custom_field.name, show_value(custom_value)]
|
(i < half ? left : right) << [custom_value.custom_field.name, show_value(custom_value)]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -683,7 +683,7 @@ module Redmine
|
||||||
pdf.RDMCell(190,5, title)
|
pdf.RDMCell(190,5, title)
|
||||||
pdf.Ln
|
pdf.Ln
|
||||||
pdf.SetFontStyle('I',8)
|
pdf.SetFontStyle('I',8)
|
||||||
details_to_strings(journal.details, true).each do |string|
|
details_to_strings(journal.visible_details, true).each do |string|
|
||||||
pdf.RDMMultiCell(190,5, "- " + string)
|
pdf.RDMMultiCell(190,5, "- " + string)
|
||||||
end
|
end
|
||||||
if journal.notes?
|
if journal.notes?
|
||||||
|
|
|
@ -0,0 +1,322 @@
|
||||||
|
# Redmine - project management software
|
||||||
|
# Copyright (C) 2006-2013 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.
|
||||||
|
|
||||||
|
require File.expand_path('../../test_helper', __FILE__)
|
||||||
|
|
||||||
|
class IssuesCustomFieldsVisibilityTest < ActionController::TestCase
|
||||||
|
tests IssuesController
|
||||||
|
fixtures :projects,
|
||||||
|
:users,
|
||||||
|
:roles,
|
||||||
|
:members,
|
||||||
|
:member_roles,
|
||||||
|
:issue_statuses,
|
||||||
|
:trackers,
|
||||||
|
:projects_trackers,
|
||||||
|
:enabled_modules,
|
||||||
|
:enumerations,
|
||||||
|
:workflows
|
||||||
|
|
||||||
|
def setup
|
||||||
|
CustomField.delete_all
|
||||||
|
Issue.delete_all
|
||||||
|
field_attributes = {:field_format => 'string', :is_for_all => true, :is_filter => true, :trackers => Tracker.all}
|
||||||
|
@fields = []
|
||||||
|
@fields << (@field1 = IssueCustomField.create!(field_attributes.merge(:name => 'Field 1', :visible => true)))
|
||||||
|
@fields << (@field2 = IssueCustomField.create!(field_attributes.merge(:name => 'Field 2', :visible => false, :role_ids => [1, 2])))
|
||||||
|
@fields << (@field3 = IssueCustomField.create!(field_attributes.merge(:name => 'Field 3', :visible => false, :role_ids => [1, 3])))
|
||||||
|
@issue = Issue.generate!(
|
||||||
|
:author_id => 1,
|
||||||
|
:project_id => 1,
|
||||||
|
:tracker_id => 1,
|
||||||
|
:custom_field_values => {@field1.id => 'Value0', @field2.id => 'Value1', @field3.id => 'Value2'}
|
||||||
|
)
|
||||||
|
|
||||||
|
@user_with_role_on_other_project = User.generate!
|
||||||
|
User.add_to_project(@user_with_role_on_other_project, Project.find(2), Role.find(3))
|
||||||
|
|
||||||
|
@users_to_test = {
|
||||||
|
User.find(1) => [@field1, @field2, @field3],
|
||||||
|
User.find(3) => [@field1, @field2],
|
||||||
|
@user_with_role_on_other_project => [@field1], # should see field1 only on Project 1
|
||||||
|
User.generate! => [@field1],
|
||||||
|
User.anonymous => [@field1]
|
||||||
|
}
|
||||||
|
|
||||||
|
Member.where(:project_id => 1).each do |member|
|
||||||
|
member.destroy unless @users_to_test.keys.include?(member.principal)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_show_should_show_visible_custom_fields_only
|
||||||
|
@users_to_test.each do |user, fields|
|
||||||
|
@request.session[:user_id] = user.id
|
||||||
|
get :show, :id => @issue.id
|
||||||
|
@fields.each_with_index do |field, i|
|
||||||
|
if fields.include?(field)
|
||||||
|
assert_select 'td', {:text => "Value#{i}", :count => 1}, "User #{user.id} was not able to view #{field.name}"
|
||||||
|
else
|
||||||
|
assert_select 'td', {:text => "Value#{i}", :count => 0}, "User #{user.id} was able to view #{field.name}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_show_should_show_visible_custom_fields_only_in_api
|
||||||
|
@users_to_test.each do |user, fields|
|
||||||
|
with_settings :rest_api_enabled => '1' do
|
||||||
|
get :show, :id => @issue.id, :format => 'xml', :include => 'custom_fields', :key => user.api_key
|
||||||
|
end
|
||||||
|
@fields.each_with_index do |field, i|
|
||||||
|
if fields.include?(field)
|
||||||
|
assert_select "custom_field[id=#{field.id}] value", {:text => "Value#{i}", :count => 1}, "User #{user.id} was not able to view #{field.name} in API"
|
||||||
|
else
|
||||||
|
assert_select "custom_field[id=#{field.id}] value", {:text => "Value#{i}", :count => 0}, "User #{user.id} was not able to view #{field.name} in API"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_show_should_show_visible_custom_fields_only_in_history
|
||||||
|
@issue.init_journal(User.find(1))
|
||||||
|
@issue.custom_field_values = {@field1.id => 'NewValue0', @field2.id => 'NewValue1', @field3.id => 'NewValue2'}
|
||||||
|
@issue.save!
|
||||||
|
|
||||||
|
@users_to_test.each do |user, fields|
|
||||||
|
@request.session[:user_id] = user.id
|
||||||
|
get :show, :id => @issue.id
|
||||||
|
@fields.each_with_index do |field, i|
|
||||||
|
if fields.include?(field)
|
||||||
|
assert_select 'ul.details i', {:text => "Value#{i}", :count => 1}, "User #{user.id} was not able to view #{field.name} change"
|
||||||
|
else
|
||||||
|
assert_select 'ul.details i', {:text => "Value#{i}", :count => 0}, "User #{user.id} was able to view #{field.name} change"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_show_should_show_visible_custom_fields_only_in_history_api
|
||||||
|
@issue.init_journal(User.find(1))
|
||||||
|
@issue.custom_field_values = {@field1.id => 'NewValue0', @field2.id => 'NewValue1', @field3.id => 'NewValue2'}
|
||||||
|
@issue.save!
|
||||||
|
|
||||||
|
@users_to_test.each do |user, fields|
|
||||||
|
with_settings :rest_api_enabled => '1' do
|
||||||
|
get :show, :id => @issue.id, :format => 'xml', :include => 'journals', :key => user.api_key
|
||||||
|
end
|
||||||
|
@fields.each_with_index do |field, i|
|
||||||
|
if fields.include?(field)
|
||||||
|
assert_select 'details old_value', {:text => "Value#{i}", :count => 1}, "User #{user.id} was not able to view #{field.name} change in API"
|
||||||
|
else
|
||||||
|
assert_select 'details old_value', {:text => "Value#{i}", :count => 0}, "User #{user.id} was able to view #{field.name} change in API"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_edit_should_show_visible_custom_fields_only
|
||||||
|
Role.anonymous.add_permission! :edit_issues
|
||||||
|
|
||||||
|
@users_to_test.each do |user, fields|
|
||||||
|
@request.session[:user_id] = user.id
|
||||||
|
get :edit, :id => @issue.id
|
||||||
|
@fields.each_with_index do |field, i|
|
||||||
|
if fields.include?(field)
|
||||||
|
assert_select 'input[value=?]', "Value#{i}", 1, "User #{user.id} was not able to edit #{field.name}"
|
||||||
|
else
|
||||||
|
assert_select 'input[value=?]', "Value#{i}", 0, "User #{user.id} was able to edit #{field.name}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_update_should_update_visible_custom_fields_only
|
||||||
|
Role.anonymous.add_permission! :edit_issues
|
||||||
|
|
||||||
|
@users_to_test.each do |user, fields|
|
||||||
|
@request.session[:user_id] = user.id
|
||||||
|
put :update, :id => @issue.id,
|
||||||
|
:issue => {:custom_field_values => {
|
||||||
|
@field1.id.to_s => "User#{user.id}Value0",
|
||||||
|
@field2.id.to_s => "User#{user.id}Value1",
|
||||||
|
@field3.id.to_s => "User#{user.id}Value2",
|
||||||
|
}}
|
||||||
|
@issue.reload
|
||||||
|
@fields.each_with_index do |field, i|
|
||||||
|
if fields.include?(field)
|
||||||
|
assert_equal "User#{user.id}Value#{i}", @issue.custom_field_value(field), "User #{user.id} was not able to update #{field.name}"
|
||||||
|
else
|
||||||
|
assert_not_equal "User#{user.id}Value#{i}", @issue.custom_field_value(field), "User #{user.id} was able to update #{field.name}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_index_should_show_visible_custom_fields_only
|
||||||
|
@users_to_test.each do |user, fields|
|
||||||
|
@request.session[:user_id] = user.id
|
||||||
|
get :index, :c => (["subject"] + @fields.map{|f| "cf_#{f.id}"})
|
||||||
|
@fields.each_with_index do |field, i|
|
||||||
|
if fields.include?(field)
|
||||||
|
assert_select 'td', {:text => "Value#{i}", :count => 1}, "User #{user.id} was not able to view #{field.name}"
|
||||||
|
else
|
||||||
|
assert_select 'td', {:text => "Value#{i}", :count => 0}, "User #{user.id} was able to view #{field.name}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_index_as_csv_should_show_visible_custom_fields_only
|
||||||
|
@users_to_test.each do |user, fields|
|
||||||
|
@request.session[:user_id] = user.id
|
||||||
|
get :index, :c => (["subject"] + @fields.map{|f| "cf_#{f.id}"}), :format => 'csv'
|
||||||
|
@fields.each_with_index do |field, i|
|
||||||
|
if fields.include?(field)
|
||||||
|
assert_include "Value#{i}", response.body, "User #{user.id} was not able to view #{field.name} in CSV"
|
||||||
|
else
|
||||||
|
assert_not_include "Value#{i}", response.body, "User #{user.id} was able to view #{field.name} in CSV"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_index_with_partial_custom_field_visibility
|
||||||
|
Issue.delete_all
|
||||||
|
p1 = Project.generate!
|
||||||
|
p2 = Project.generate!
|
||||||
|
user = User.generate!
|
||||||
|
User.add_to_project(user, p1, Role.find_all_by_id(1,3))
|
||||||
|
User.add_to_project(user, p2, Role.find_all_by_id(3))
|
||||||
|
Issue.generate!(:project => p1, :tracker_id => 1, :custom_field_values => {@field2.id => 'ValueA'})
|
||||||
|
Issue.generate!(:project => p2, :tracker_id => 1, :custom_field_values => {@field2.id => 'ValueB'})
|
||||||
|
Issue.generate!(:project => p1, :tracker_id => 1, :custom_field_values => {@field2.id => 'ValueC'})
|
||||||
|
|
||||||
|
@request.session[:user_id] = user.id
|
||||||
|
get :index, :c => ["subject", "cf_#{@field2.id}"]
|
||||||
|
assert_select 'td', :text => 'ValueA'
|
||||||
|
assert_select 'td', :text => 'ValueB', :count => 0
|
||||||
|
assert_select 'td', :text => 'ValueC'
|
||||||
|
|
||||||
|
get :index, :sort => "cf_#{@field2.id}"
|
||||||
|
# ValueB is not visible to user and ignored while sorting
|
||||||
|
assert_equal %w(ValueB ValueA ValueC), assigns(:issues).map{|i| i.custom_field_value(@field2)}
|
||||||
|
|
||||||
|
get :index, :set_filter => '1', "cf_#{@field2.id}" => '*'
|
||||||
|
assert_equal %w(ValueA ValueC), assigns(:issues).map{|i| i.custom_field_value(@field2)}
|
||||||
|
|
||||||
|
CustomField.update_all(:field_format => 'list')
|
||||||
|
get :index, :group => "cf_#{@field2.id}"
|
||||||
|
assert_equal %w(ValueA ValueC), assigns(:issues).map{|i| i.custom_field_value(@field2)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_create_should_send_notifications_according_custom_fields_visibility
|
||||||
|
# anonymous user is never notified
|
||||||
|
users_to_test = @users_to_test.reject {|k,v| k.anonymous?}
|
||||||
|
|
||||||
|
ActionMailer::Base.deliveries.clear
|
||||||
|
@request.session[:user_id] = 1
|
||||||
|
with_settings :bcc_recipients => '1' do
|
||||||
|
assert_difference 'Issue.count' do
|
||||||
|
post :create,
|
||||||
|
:project_id => 1,
|
||||||
|
:issue => {
|
||||||
|
:tracker_id => 1,
|
||||||
|
:status_id => 1,
|
||||||
|
:subject => 'New issue',
|
||||||
|
:priority_id => 5,
|
||||||
|
:custom_field_values => {@field1.id.to_s => 'Value0', @field2.id.to_s => 'Value1', @field3.id.to_s => 'Value2'},
|
||||||
|
:watcher_user_ids => users_to_test.keys.map(&:id)
|
||||||
|
}
|
||||||
|
assert_response 302
|
||||||
|
end
|
||||||
|
end
|
||||||
|
assert_equal users_to_test.values.uniq.size, ActionMailer::Base.deliveries.size
|
||||||
|
# tests that each user receives 1 email with the custom fields he is allowed to see only
|
||||||
|
users_to_test.each do |user, fields|
|
||||||
|
mails = ActionMailer::Base.deliveries.select {|m| m.bcc.include? user.mail}
|
||||||
|
assert_equal 1, mails.size
|
||||||
|
mail = mails.first
|
||||||
|
@fields.each_with_index do |field, i|
|
||||||
|
if fields.include?(field)
|
||||||
|
assert_mail_body_match "Value#{i}", mail, "User #{user.id} was not able to view #{field.name} in notification"
|
||||||
|
else
|
||||||
|
assert_mail_body_no_match "Value#{i}", mail, "User #{user.id} was able to view #{field.name} in notification"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_update_should_send_notifications_according_custom_fields_visibility
|
||||||
|
# anonymous user is never notified
|
||||||
|
users_to_test = @users_to_test.reject {|k,v| k.anonymous?}
|
||||||
|
|
||||||
|
users_to_test.keys.each do |user|
|
||||||
|
Watcher.create!(:user => user, :watchable => @issue)
|
||||||
|
end
|
||||||
|
ActionMailer::Base.deliveries.clear
|
||||||
|
@request.session[:user_id] = 1
|
||||||
|
with_settings :bcc_recipients => '1' do
|
||||||
|
put :update,
|
||||||
|
:id => @issue.id,
|
||||||
|
:issue => {
|
||||||
|
:custom_field_values => {@field1.id.to_s => 'NewValue0', @field2.id.to_s => 'NewValue1', @field3.id.to_s => 'NewValue2'}
|
||||||
|
}
|
||||||
|
assert_response 302
|
||||||
|
end
|
||||||
|
assert_equal users_to_test.values.uniq.size, ActionMailer::Base.deliveries.size
|
||||||
|
# tests that each user receives 1 email with the custom fields he is allowed to see only
|
||||||
|
users_to_test.each do |user, fields|
|
||||||
|
mails = ActionMailer::Base.deliveries.select {|m| m.bcc.include? user.mail}
|
||||||
|
assert_equal 1, mails.size
|
||||||
|
mail = mails.first
|
||||||
|
@fields.each_with_index do |field, i|
|
||||||
|
if fields.include?(field)
|
||||||
|
assert_mail_body_match "Value#{i}", mail, "User #{user.id} was not able to view #{field.name} in notification"
|
||||||
|
else
|
||||||
|
assert_mail_body_no_match "Value#{i}", mail, "User #{user.id} was able to view #{field.name} in notification"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_updating_hidden_custom_fields_only_should_not_notifiy_user
|
||||||
|
# anonymous user is never notified
|
||||||
|
users_to_test = @users_to_test.reject {|k,v| k.anonymous?}
|
||||||
|
|
||||||
|
users_to_test.keys.each do |user|
|
||||||
|
Watcher.create!(:user => user, :watchable => @issue)
|
||||||
|
end
|
||||||
|
ActionMailer::Base.deliveries.clear
|
||||||
|
@request.session[:user_id] = 1
|
||||||
|
with_settings :bcc_recipients => '1' do
|
||||||
|
put :update,
|
||||||
|
:id => @issue.id,
|
||||||
|
:issue => {
|
||||||
|
:custom_field_values => {@field2.id.to_s => 'NewValue1', @field3.id.to_s => 'NewValue2'}
|
||||||
|
}
|
||||||
|
assert_response 302
|
||||||
|
end
|
||||||
|
users_to_test.each do |user, fields|
|
||||||
|
mails = ActionMailer::Base.deliveries.select {|m| m.bcc.include? user.mail}
|
||||||
|
if (fields & [@field2, @field3]).any?
|
||||||
|
assert_equal 1, mails.size, "User #{user.id} was not notified"
|
||||||
|
else
|
||||||
|
assert_equal 0, mails.size, "User #{user.id} was notified"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,78 @@
|
||||||
|
# Redmine - project management software
|
||||||
|
# Copyright (C) 2006-2013 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.
|
||||||
|
|
||||||
|
require File.expand_path('../../test_helper', __FILE__)
|
||||||
|
|
||||||
|
class SearchCustomFieldsVisibilityTest < ActionController::TestCase
|
||||||
|
tests SearchController
|
||||||
|
fixtures :projects,
|
||||||
|
:users,
|
||||||
|
:roles,
|
||||||
|
:members,
|
||||||
|
:member_roles,
|
||||||
|
:issue_statuses,
|
||||||
|
:trackers,
|
||||||
|
:projects_trackers,
|
||||||
|
:enabled_modules,
|
||||||
|
:enumerations,
|
||||||
|
:workflows
|
||||||
|
|
||||||
|
def setup
|
||||||
|
field_attributes = {:field_format => 'string', :is_for_all => true, :is_filter => true, :searchable => true, :trackers => Tracker.all}
|
||||||
|
@fields = []
|
||||||
|
@fields << (@field1 = IssueCustomField.create!(field_attributes.merge(:name => 'Field 1', :visible => true)))
|
||||||
|
@fields << (@field2 = IssueCustomField.create!(field_attributes.merge(:name => 'Field 2', :visible => false, :role_ids => [1, 2])))
|
||||||
|
@fields << (@field3 = IssueCustomField.create!(field_attributes.merge(:name => 'Field 3', :visible => false, :role_ids => [1, 3])))
|
||||||
|
@issue = Issue.generate!(
|
||||||
|
:author_id => 1,
|
||||||
|
:project_id => 1,
|
||||||
|
:tracker_id => 1,
|
||||||
|
:custom_field_values => {@field1.id => 'Value0', @field2.id => 'Value1', @field3.id => 'Value2'}
|
||||||
|
)
|
||||||
|
|
||||||
|
@user_with_role_on_other_project = User.generate!
|
||||||
|
User.add_to_project(@user_with_role_on_other_project, Project.find(2), Role.find(3))
|
||||||
|
|
||||||
|
@users_to_test = {
|
||||||
|
User.find(1) => [@field1, @field2, @field3],
|
||||||
|
User.find(3) => [@field1, @field2],
|
||||||
|
@user_with_role_on_other_project => [@field1], # should see field1 only on Project 1
|
||||||
|
User.generate! => [@field1],
|
||||||
|
User.anonymous => [@field1]
|
||||||
|
}
|
||||||
|
|
||||||
|
Member.where(:project_id => 1).each do |member|
|
||||||
|
member.destroy unless @users_to_test.keys.include?(member.principal)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_search_should_search_visible_custom_fields_only
|
||||||
|
@users_to_test.each do |user, fields|
|
||||||
|
@request.session[:user_id] = user.id
|
||||||
|
@fields.each_with_index do |field, i|
|
||||||
|
get :index, :q => "value#{i}"
|
||||||
|
assert_response :success
|
||||||
|
# we should get a result only if the custom field is visible
|
||||||
|
if fields.include?(field)
|
||||||
|
assert_equal 1, assigns(:results).size
|
||||||
|
else
|
||||||
|
assert_equal 0, assigns(:results).size
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,113 @@
|
||||||
|
# Redmine - project management software
|
||||||
|
# Copyright (C) 2006-2013 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.
|
||||||
|
|
||||||
|
require File.expand_path('../../test_helper', __FILE__)
|
||||||
|
|
||||||
|
class TimelogCustomFieldsVisibilityTest < ActionController::TestCase
|
||||||
|
tests TimelogController
|
||||||
|
fixtures :projects,
|
||||||
|
:users,
|
||||||
|
:roles,
|
||||||
|
:members,
|
||||||
|
:member_roles,
|
||||||
|
:issue_statuses,
|
||||||
|
:trackers,
|
||||||
|
:projects_trackers,
|
||||||
|
:enabled_modules,
|
||||||
|
:enumerations,
|
||||||
|
:workflows
|
||||||
|
|
||||||
|
def setup
|
||||||
|
field_attributes = {:field_format => 'string', :is_for_all => true, :is_filter => true, :trackers => Tracker.all}
|
||||||
|
@fields = []
|
||||||
|
@fields << (@field1 = IssueCustomField.create!(field_attributes.merge(:name => 'Field 1', :visible => true)))
|
||||||
|
@fields << (@field2 = IssueCustomField.create!(field_attributes.merge(:name => 'Field 2', :visible => false, :role_ids => [1, 2])))
|
||||||
|
@fields << (@field3 = IssueCustomField.create!(field_attributes.merge(:name => 'Field 3', :visible => false, :role_ids => [1, 3])))
|
||||||
|
@issue = Issue.generate!(
|
||||||
|
:author_id => 1,
|
||||||
|
:project_id => 1,
|
||||||
|
:tracker_id => 1,
|
||||||
|
:custom_field_values => {@field1.id => 'Value0', @field2.id => 'Value1', @field3.id => 'Value2'}
|
||||||
|
)
|
||||||
|
TimeEntry.generate!(:issue => @issue)
|
||||||
|
|
||||||
|
@user_with_role_on_other_project = User.generate!
|
||||||
|
User.add_to_project(@user_with_role_on_other_project, Project.find(2), Role.find(3))
|
||||||
|
|
||||||
|
@users_to_test = {
|
||||||
|
User.find(1) => [@field1, @field2, @field3],
|
||||||
|
User.find(3) => [@field1, @field2],
|
||||||
|
@user_with_role_on_other_project => [@field1], # should see field1 only on Project 1
|
||||||
|
User.generate! => [@field1],
|
||||||
|
User.anonymous => [@field1]
|
||||||
|
}
|
||||||
|
|
||||||
|
Member.where(:project_id => 1).each do |member|
|
||||||
|
member.destroy unless @users_to_test.keys.include?(member.principal)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_index_should_show_visible_custom_fields_only
|
||||||
|
@users_to_test.each do |user, fields|
|
||||||
|
@request.session[:user_id] = user.id
|
||||||
|
get :index, :project_id => 1, :issue_id => @issue.id, :c => (['hours'] + @fields.map{|f| "issue.cf_#{f.id}"})
|
||||||
|
@fields.each_with_index do |field, i|
|
||||||
|
if fields.include?(field)
|
||||||
|
assert_select 'td', {:text => "Value#{i}", :count => 1}, "User #{user.id} was not able to view #{field.name}"
|
||||||
|
else
|
||||||
|
assert_select 'td', {:text => "Value#{i}", :count => 0}, "User #{user.id} was able to view #{field.name}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_index_as_csv_should_show_visible_custom_fields_only
|
||||||
|
@users_to_test.each do |user, fields|
|
||||||
|
@request.session[:user_id] = user.id
|
||||||
|
get :index, :project_id => 1, :issue_id => @issue.id, :c => (['hours'] + @fields.map{|f| "issue.cf_#{f.id}"}), :format => 'csv'
|
||||||
|
@fields.each_with_index do |field, i|
|
||||||
|
if fields.include?(field)
|
||||||
|
assert_include "Value#{i}", response.body, "User #{user.id} was not able to view #{field.name} in CSV"
|
||||||
|
else
|
||||||
|
assert_not_include "Value#{i}", response.body, "User #{user.id} was able to view #{field.name} in CSV"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_index_with_partial_custom_field_visibility_should_show_visible_custom_fields_only
|
||||||
|
Issue.delete_all
|
||||||
|
TimeEntry.delete_all
|
||||||
|
p1 = Project.generate!
|
||||||
|
p2 = Project.generate!
|
||||||
|
user = User.generate!
|
||||||
|
User.add_to_project(user, p1, Role.find_all_by_id(1,3))
|
||||||
|
User.add_to_project(user, p2, Role.find_all_by_id(3))
|
||||||
|
TimeEntry.generate!(:issue => Issue.generate!(:project => p1, :tracker_id => 1, :custom_field_values => {@field2.id => 'ValueA'}))
|
||||||
|
TimeEntry.generate!(:issue => Issue.generate!(:project => p2, :tracker_id => 1, :custom_field_values => {@field2.id => 'ValueB'}))
|
||||||
|
TimeEntry.generate!(:issue => Issue.generate!(:project => p1, :tracker_id => 1, :custom_field_values => {@field2.id => 'ValueC'}))
|
||||||
|
|
||||||
|
@request.session[:user_id] = user.id
|
||||||
|
get :index, :c => ["hours", "issue.cf_#{@field2.id}"]
|
||||||
|
assert_select 'td', :text => 'ValueA'
|
||||||
|
assert_select 'td', :text => 'ValueB', :count => 0
|
||||||
|
assert_select 'td', :text => 'ValueC'
|
||||||
|
|
||||||
|
get :index, :set_filter => '1', "issue.cf_#{@field2.id}" => '*'
|
||||||
|
assert_equal %w(ValueA ValueC), assigns(:entries).map{|i| i.issue.custom_field_value(@field2)}.sort
|
||||||
|
end
|
||||||
|
end
|
|
@ -200,6 +200,23 @@ class WorkflowsControllerTest < ActionController::TestCase
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_get_permissions_should_disable_hidden_custom_fields
|
||||||
|
cf1 = IssueCustomField.generate!(:tracker_ids => [1], :visible => true)
|
||||||
|
cf2 = IssueCustomField.generate!(:tracker_ids => [1], :visible => false, :role_ids => [1])
|
||||||
|
cf3 = IssueCustomField.generate!(:tracker_ids => [1], :visible => false, :role_ids => [1, 2])
|
||||||
|
|
||||||
|
get :permissions, :role_id => 2, :tracker_id => 1
|
||||||
|
assert_response :success
|
||||||
|
assert_template 'permissions'
|
||||||
|
|
||||||
|
assert_select 'select[name=?]:not(.disabled)', "permissions[#{cf1.id}][1]"
|
||||||
|
assert_select 'select[name=?]:not(.disabled)', "permissions[#{cf3.id}][1]"
|
||||||
|
|
||||||
|
assert_select 'select[name=?][disabled=disabled]', "permissions[#{cf2.id}][1]" do
|
||||||
|
assert_select 'option[value=][selected=selected]', :text => 'Hidden'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def test_get_permissions_with_role_and_tracker_and_all_statuses
|
def test_get_permissions_with_role_and_tracker_and_all_statuses
|
||||||
WorkflowTransition.delete_all
|
WorkflowTransition.delete_all
|
||||||
|
|
||||||
|
|
|
@ -169,8 +169,8 @@ class ActiveSupport::TestCase
|
||||||
assert s.include?(expected), (message || "\"#{expected}\" not found in \"#{s}\"")
|
assert s.include?(expected), (message || "\"#{expected}\" not found in \"#{s}\"")
|
||||||
end
|
end
|
||||||
|
|
||||||
def assert_not_include(expected, s)
|
def assert_not_include(expected, s, message=nil)
|
||||||
assert !s.include?(expected), "\"#{expected}\" found in \"#{s}\""
|
assert !s.include?(expected), (message || "\"#{expected}\" found in \"#{s}\"")
|
||||||
end
|
end
|
||||||
|
|
||||||
def assert_select_in(text, *args, &block)
|
def assert_select_in(text, *args, &block)
|
||||||
|
@ -178,19 +178,19 @@ class ActiveSupport::TestCase
|
||||||
assert_select(d, *args, &block)
|
assert_select(d, *args, &block)
|
||||||
end
|
end
|
||||||
|
|
||||||
def assert_mail_body_match(expected, mail)
|
def assert_mail_body_match(expected, mail, message=nil)
|
||||||
if expected.is_a?(String)
|
if expected.is_a?(String)
|
||||||
assert_include expected, mail_body(mail)
|
assert_include expected, mail_body(mail), message
|
||||||
else
|
else
|
||||||
assert_match expected, mail_body(mail)
|
assert_match expected, mail_body(mail), message
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def assert_mail_body_no_match(expected, mail)
|
def assert_mail_body_no_match(expected, mail, message=nil)
|
||||||
if expected.is_a?(String)
|
if expected.is_a?(String)
|
||||||
assert_not_include expected, mail_body(mail)
|
assert_not_include expected, mail_body(mail), message
|
||||||
else
|
else
|
||||||
assert_no_match expected, mail_body(mail)
|
assert_no_match expected, mail_body(mail), message
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -241,4 +241,42 @@ class CustomFieldTest < ActiveSupport::TestCase
|
||||||
field = CustomField.find(1)
|
field = CustomField.find(1)
|
||||||
assert_equal 'PostgreSQL', field.value_from_keyword('postgresql', Issue.find(1))
|
assert_equal 'PostgreSQL', field.value_from_keyword('postgresql', Issue.find(1))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_visibile_scope_with_admin_should_return_all_custom_fields
|
||||||
|
CustomField.delete_all
|
||||||
|
fields = [
|
||||||
|
CustomField.generate!(:visible => true),
|
||||||
|
CustomField.generate!(:visible => false),
|
||||||
|
CustomField.generate!(:visible => false, :role_ids => [1, 3]),
|
||||||
|
CustomField.generate!(:visible => false, :role_ids => [1, 2]),
|
||||||
|
]
|
||||||
|
|
||||||
|
assert_equal 4, CustomField.visible(User.find(1)).count
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_visibile_scope_with_non_admin_user_should_return_visible_custom_fields
|
||||||
|
CustomField.delete_all
|
||||||
|
fields = [
|
||||||
|
CustomField.generate!(:visible => true),
|
||||||
|
CustomField.generate!(:visible => false),
|
||||||
|
CustomField.generate!(:visible => false, :role_ids => [1, 3]),
|
||||||
|
CustomField.generate!(:visible => false, :role_ids => [1, 2]),
|
||||||
|
]
|
||||||
|
user = User.generate!
|
||||||
|
User.add_to_project(user, Project.first, Role.find(3))
|
||||||
|
|
||||||
|
assert_equal [fields[0], fields[2]], CustomField.visible(user).order("id").to_a
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_visibile_scope_with_anonymous_user_should_return_visible_custom_fields
|
||||||
|
CustomField.delete_all
|
||||||
|
fields = [
|
||||||
|
CustomField.generate!(:visible => true),
|
||||||
|
CustomField.generate!(:visible => false),
|
||||||
|
CustomField.generate!(:visible => false, :role_ids => [1, 3]),
|
||||||
|
CustomField.generate!(:visible => false, :role_ids => [1, 2]),
|
||||||
|
]
|
||||||
|
|
||||||
|
assert_equal [fields[0]], CustomField.visible(User.anonymous).order("id").to_a
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
# Redmine - project management software
|
||||||
|
# Copyright (C) 2006-2013 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.
|
||||||
|
|
||||||
|
require File.expand_path('../../test_helper', __FILE__)
|
||||||
|
|
||||||
|
class IssueCustomFieldTest < ActiveSupport::TestCase
|
||||||
|
include Redmine::I18n
|
||||||
|
|
||||||
|
fixtures :roles
|
||||||
|
|
||||||
|
def test_custom_field_with_visible_set_to_false_should_validate_roles
|
||||||
|
set_language_if_valid 'en'
|
||||||
|
field = IssueCustomField.new(:name => 'Field', :field_format => 'string', :visible => false)
|
||||||
|
assert !field.save
|
||||||
|
assert_include "Roles can't be blank", field.errors.full_messages
|
||||||
|
field.role_ids = [1, 2]
|
||||||
|
assert field.save
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_changing_visible_to_true_should_clear_roles
|
||||||
|
field = IssueCustomField.create!(:name => 'Field', :field_format => 'string', :visible => false, :role_ids => [1, 2])
|
||||||
|
assert_equal 2, field.roles.count
|
||||||
|
|
||||||
|
field.visible = true
|
||||||
|
field.save!
|
||||||
|
assert_equal 0, field.roles.count
|
||||||
|
end
|
||||||
|
end
|
|
@ -154,14 +154,14 @@ class Redmine::Hook::ManagerTest < ActionView::TestCase
|
||||||
issue = Issue.find(1)
|
issue = Issue.find(1)
|
||||||
|
|
||||||
ActionMailer::Base.deliveries.clear
|
ActionMailer::Base.deliveries.clear
|
||||||
Mailer.issue_add(issue).deliver
|
Mailer.deliver_issue_add(issue)
|
||||||
mail = ActionMailer::Base.deliveries.last
|
mail = ActionMailer::Base.deliveries.last
|
||||||
|
|
||||||
@hook_module.add_listener(TestLinkToHook)
|
@hook_module.add_listener(TestLinkToHook)
|
||||||
hook_helper.call_hook(:view_layouts_base_html_head)
|
hook_helper.call_hook(:view_layouts_base_html_head)
|
||||||
|
|
||||||
ActionMailer::Base.deliveries.clear
|
ActionMailer::Base.deliveries.clear
|
||||||
Mailer.issue_add(issue).deliver
|
Mailer.deliver_issue_add(issue)
|
||||||
mail2 = ActionMailer::Base.deliveries.last
|
mail2 = ActionMailer::Base.deliveries.last
|
||||||
|
|
||||||
assert_equal mail_body(mail), mail_body(mail2)
|
assert_equal mail_body(mail), mail_body(mail2)
|
||||||
|
|
|
@ -42,7 +42,7 @@ class MailerTest < ActiveSupport::TestCase
|
||||||
Setting.protocol = 'https'
|
Setting.protocol = 'https'
|
||||||
|
|
||||||
journal = Journal.find(3)
|
journal = Journal.find(3)
|
||||||
assert Mailer.issue_edit(journal).deliver
|
assert Mailer.deliver_issue_edit(journal)
|
||||||
|
|
||||||
mail = last_email
|
mail = last_email
|
||||||
assert_not_nil mail
|
assert_not_nil mail
|
||||||
|
@ -81,7 +81,7 @@ class MailerTest < ActiveSupport::TestCase
|
||||||
Setting.protocol = 'http'
|
Setting.protocol = 'http'
|
||||||
|
|
||||||
journal = Journal.find(3)
|
journal = Journal.find(3)
|
||||||
assert Mailer.issue_edit(journal).deliver
|
assert Mailer.deliver_issue_edit(journal)
|
||||||
|
|
||||||
mail = last_email
|
mail = last_email
|
||||||
assert_not_nil mail
|
assert_not_nil mail
|
||||||
|
@ -121,7 +121,7 @@ class MailerTest < ActiveSupport::TestCase
|
||||||
Redmine::Utils.relative_url_root = nil
|
Redmine::Utils.relative_url_root = nil
|
||||||
|
|
||||||
journal = Journal.find(3)
|
journal = Journal.find(3)
|
||||||
assert Mailer.issue_edit(journal).deliver
|
assert Mailer.deliver_issue_edit(journal)
|
||||||
|
|
||||||
mail = last_email
|
mail = last_email
|
||||||
assert_not_nil mail
|
assert_not_nil mail
|
||||||
|
@ -158,7 +158,7 @@ class MailerTest < ActiveSupport::TestCase
|
||||||
|
|
||||||
def test_email_headers
|
def test_email_headers
|
||||||
issue = Issue.find(1)
|
issue = Issue.find(1)
|
||||||
Mailer.issue_add(issue).deliver
|
Mailer.deliver_issue_add(issue)
|
||||||
mail = last_email
|
mail = last_email
|
||||||
assert_not_nil mail
|
assert_not_nil mail
|
||||||
assert_equal 'OOF', mail.header['X-Auto-Response-Suppress'].to_s
|
assert_equal 'OOF', mail.header['X-Auto-Response-Suppress'].to_s
|
||||||
|
@ -168,7 +168,7 @@ class MailerTest < ActiveSupport::TestCase
|
||||||
|
|
||||||
def test_email_headers_should_include_sender
|
def test_email_headers_should_include_sender
|
||||||
issue = Issue.find(1)
|
issue = Issue.find(1)
|
||||||
Mailer.issue_add(issue).deliver
|
Mailer.deliver_issue_add(issue)
|
||||||
mail = last_email
|
mail = last_email
|
||||||
assert_equal issue.author.login, mail.header['X-Redmine-Sender'].to_s
|
assert_equal issue.author.login, mail.header['X-Redmine-Sender'].to_s
|
||||||
end
|
end
|
||||||
|
@ -176,7 +176,7 @@ class MailerTest < ActiveSupport::TestCase
|
||||||
def test_plain_text_mail
|
def test_plain_text_mail
|
||||||
Setting.plain_text_mail = 1
|
Setting.plain_text_mail = 1
|
||||||
journal = Journal.find(2)
|
journal = Journal.find(2)
|
||||||
Mailer.issue_edit(journal).deliver
|
Mailer.deliver_issue_edit(journal)
|
||||||
mail = last_email
|
mail = last_email
|
||||||
assert_equal "text/plain; charset=UTF-8", mail.content_type
|
assert_equal "text/plain; charset=UTF-8", mail.content_type
|
||||||
assert_equal 0, mail.parts.size
|
assert_equal 0, mail.parts.size
|
||||||
|
@ -186,7 +186,7 @@ class MailerTest < ActiveSupport::TestCase
|
||||||
def test_html_mail
|
def test_html_mail
|
||||||
Setting.plain_text_mail = 0
|
Setting.plain_text_mail = 0
|
||||||
journal = Journal.find(2)
|
journal = Journal.find(2)
|
||||||
Mailer.issue_edit(journal).deliver
|
Mailer.deliver_issue_edit(journal)
|
||||||
mail = last_email
|
mail = last_email
|
||||||
assert_equal 2, mail.parts.size
|
assert_equal 2, mail.parts.size
|
||||||
assert mail.encoded.include?('href')
|
assert mail.encoded.include?('href')
|
||||||
|
@ -231,19 +231,21 @@ class MailerTest < ActiveSupport::TestCase
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_issue_add_message_id
|
def test_issue_add_message_id
|
||||||
issue = Issue.find(1)
|
issue = Issue.find(2)
|
||||||
Mailer.issue_add(issue).deliver
|
Mailer.deliver_issue_add(issue)
|
||||||
mail = last_email
|
mail = last_email
|
||||||
assert_equal Mailer.message_id_for(issue), mail.message_id
|
assert_match /^redmine\.issue-2\.20060719190421\.[a-f0-9]+@example\.net/, mail.message_id
|
||||||
assert_nil mail.references
|
assert_include "redmine.issue-2.20060719190421@example.net", mail.references
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_issue_edit_message_id
|
def test_issue_edit_message_id
|
||||||
journal = Journal.find(1)
|
journal = Journal.find(3)
|
||||||
Mailer.issue_edit(journal).deliver
|
journal.issue = Issue.find(2)
|
||||||
|
|
||||||
|
Mailer.deliver_issue_edit(journal)
|
||||||
mail = last_email
|
mail = last_email
|
||||||
assert_equal Mailer.message_id_for(journal), mail.message_id
|
assert_match /^redmine\.journal-3\.\d+\.[a-f0-9]+@example\.net/, mail.message_id
|
||||||
assert_include Mailer.message_id_for(journal.issue), mail.references
|
assert_include "redmine.issue-2.20060719190421@example.net", mail.references
|
||||||
assert_select_email do
|
assert_select_email do
|
||||||
# link to the update
|
# link to the update
|
||||||
assert_select "a[href=?]",
|
assert_select "a[href=?]",
|
||||||
|
@ -255,8 +257,8 @@ class MailerTest < ActiveSupport::TestCase
|
||||||
message = Message.find(1)
|
message = Message.find(1)
|
||||||
Mailer.message_posted(message).deliver
|
Mailer.message_posted(message).deliver
|
||||||
mail = last_email
|
mail = last_email
|
||||||
assert_equal Mailer.message_id_for(message), mail.message_id
|
assert_match /^redmine\.message-1\.\d+\.[a-f0-9]+@example\.net/, mail.message_id
|
||||||
assert_nil mail.references
|
assert_include "redmine.message-1.20070512151532@example.net", mail.references
|
||||||
assert_select_email do
|
assert_select_email do
|
||||||
# link to the message
|
# link to the message
|
||||||
assert_select "a[href=?]",
|
assert_select "a[href=?]",
|
||||||
|
@ -269,8 +271,8 @@ class MailerTest < ActiveSupport::TestCase
|
||||||
message = Message.find(3)
|
message = Message.find(3)
|
||||||
Mailer.message_posted(message).deliver
|
Mailer.message_posted(message).deliver
|
||||||
mail = last_email
|
mail = last_email
|
||||||
assert_equal Mailer.message_id_for(message), mail.message_id
|
assert_match /^redmine\.message-3\.\d+\.[a-f0-9]+@example\.net/, mail.message_id
|
||||||
assert_include Mailer.message_id_for(message.parent), mail.references
|
assert_include "redmine.message-1.20070512151532@example.net", mail.references
|
||||||
assert_select_email do
|
assert_select_email do
|
||||||
# link to the reply
|
# link to the reply
|
||||||
assert_select "a[href=?]",
|
assert_select "a[href=?]",
|
||||||
|
@ -281,14 +283,14 @@ class MailerTest < ActiveSupport::TestCase
|
||||||
|
|
||||||
test "#issue_add should notify project members" do
|
test "#issue_add should notify project members" do
|
||||||
issue = Issue.find(1)
|
issue = Issue.find(1)
|
||||||
assert Mailer.issue_add(issue).deliver
|
assert Mailer.deliver_issue_add(issue)
|
||||||
assert last_email.bcc.include?('dlopper@somenet.foo')
|
assert last_email.bcc.include?('dlopper@somenet.foo')
|
||||||
end
|
end
|
||||||
|
|
||||||
test "#issue_add should not notify project members that are not allow to view the issue" do
|
test "#issue_add should not notify project members that are not allow to view the issue" do
|
||||||
issue = Issue.find(1)
|
issue = Issue.find(1)
|
||||||
Role.find(2).remove_permission!(:view_issues)
|
Role.find(2).remove_permission!(:view_issues)
|
||||||
assert Mailer.issue_add(issue).deliver
|
assert Mailer.deliver_issue_add(issue)
|
||||||
assert !last_email.bcc.include?('dlopper@somenet.foo')
|
assert !last_email.bcc.include?('dlopper@somenet.foo')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -302,7 +304,7 @@ class MailerTest < ActiveSupport::TestCase
|
||||||
user.save
|
user.save
|
||||||
|
|
||||||
Watcher.create!(:watchable => issue, :user => user)
|
Watcher.create!(:watchable => issue, :user => user)
|
||||||
assert Mailer.issue_add(issue).deliver
|
assert Mailer.deliver_issue_add(issue)
|
||||||
assert last_email.bcc.include?(user.mail)
|
assert last_email.bcc.include?(user.mail)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -311,7 +313,7 @@ class MailerTest < ActiveSupport::TestCase
|
||||||
user = User.find(9)
|
user = User.find(9)
|
||||||
Watcher.create!(:watchable => issue, :user => user)
|
Watcher.create!(:watchable => issue, :user => user)
|
||||||
Role.non_member.remove_permission!(:view_issues)
|
Role.non_member.remove_permission!(:view_issues)
|
||||||
assert Mailer.issue_add(issue).deliver
|
assert Mailer.deliver_issue_add(issue)
|
||||||
assert !last_email.bcc.include?(user.mail)
|
assert !last_email.bcc.include?(user.mail)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -320,7 +322,7 @@ class MailerTest < ActiveSupport::TestCase
|
||||||
issue = Issue.find(1)
|
issue = Issue.find(1)
|
||||||
valid_languages.each do |lang|
|
valid_languages.each do |lang|
|
||||||
Setting.default_language = lang.to_s
|
Setting.default_language = lang.to_s
|
||||||
assert Mailer.issue_add(issue).deliver
|
assert Mailer.deliver_issue_add(issue)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -328,7 +330,7 @@ class MailerTest < ActiveSupport::TestCase
|
||||||
journal = Journal.find(1)
|
journal = Journal.find(1)
|
||||||
valid_languages.each do |lang|
|
valid_languages.each do |lang|
|
||||||
Setting.default_language = lang.to_s
|
Setting.default_language = lang.to_s
|
||||||
assert Mailer.issue_edit(journal).deliver
|
assert Mailer.deliver_issue_edit(journal)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -338,11 +340,11 @@ class MailerTest < ActiveSupport::TestCase
|
||||||
journal.save!
|
journal.save!
|
||||||
|
|
||||||
Role.find(2).add_permission! :view_private_notes
|
Role.find(2).add_permission! :view_private_notes
|
||||||
Mailer.issue_edit(journal).deliver
|
Mailer.deliver_issue_edit(journal)
|
||||||
assert_equal %w(dlopper@somenet.foo jsmith@somenet.foo), ActionMailer::Base.deliveries.last.bcc.sort
|
assert_equal %w(dlopper@somenet.foo jsmith@somenet.foo), ActionMailer::Base.deliveries.last.bcc.sort
|
||||||
|
|
||||||
Role.find(2).remove_permission! :view_private_notes
|
Role.find(2).remove_permission! :view_private_notes
|
||||||
Mailer.issue_edit(journal).deliver
|
Mailer.deliver_issue_edit(journal)
|
||||||
assert_equal %w(jsmith@somenet.foo), ActionMailer::Base.deliveries.last.bcc.sort
|
assert_equal %w(jsmith@somenet.foo), ActionMailer::Base.deliveries.last.bcc.sort
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -353,11 +355,11 @@ class MailerTest < ActiveSupport::TestCase
|
||||||
journal.save!
|
journal.save!
|
||||||
|
|
||||||
Role.non_member.add_permission! :view_private_notes
|
Role.non_member.add_permission! :view_private_notes
|
||||||
Mailer.issue_edit(journal).deliver
|
Mailer.deliver_issue_edit(journal)
|
||||||
assert_include 'someone@foo.bar', ActionMailer::Base.deliveries.last.bcc.sort
|
assert_include 'someone@foo.bar', ActionMailer::Base.deliveries.last.bcc.sort
|
||||||
|
|
||||||
Role.non_member.remove_permission! :view_private_notes
|
Role.non_member.remove_permission! :view_private_notes
|
||||||
Mailer.issue_edit(journal).deliver
|
Mailer.deliver_issue_edit(journal)
|
||||||
assert_not_include 'someone@foo.bar', ActionMailer::Base.deliveries.last.bcc.sort
|
assert_not_include 'someone@foo.bar', ActionMailer::Base.deliveries.last.bcc.sort
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -367,7 +369,7 @@ class MailerTest < ActiveSupport::TestCase
|
||||||
journal.save!
|
journal.save!
|
||||||
|
|
||||||
with_settings :default_language => 'en' do
|
with_settings :default_language => 'en' do
|
||||||
Mailer.issue_edit(journal).deliver
|
Mailer.deliver_issue_edit(journal)
|
||||||
end
|
end
|
||||||
assert_mail_body_match '(Private notes)', last_email
|
assert_mail_body_match '(Private notes)', last_email
|
||||||
end
|
end
|
||||||
|
|
|
@ -1201,6 +1201,28 @@ class QueryTest < ActiveSupport::TestCase
|
||||||
assert ! query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5'])
|
assert ! query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5'])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_available_filters_should_include_custom_field_according_to_user_visibility
|
||||||
|
visible_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => true)
|
||||||
|
hidden_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => false, :role_ids => [1])
|
||||||
|
|
||||||
|
with_current_user User.find(3) do
|
||||||
|
query = IssueQuery.new
|
||||||
|
assert_include "cf_#{visible_field.id}", query.available_filters.keys
|
||||||
|
assert_not_include "cf_#{hidden_field.id}", query.available_filters.keys
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_available_columns_should_include_custom_field_according_to_user_visibility
|
||||||
|
visible_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => true)
|
||||||
|
hidden_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => false, :role_ids => [1])
|
||||||
|
|
||||||
|
with_current_user User.find(3) do
|
||||||
|
query = IssueQuery.new
|
||||||
|
assert_include :"cf_#{visible_field.id}", query.available_columns.map(&:name)
|
||||||
|
assert_not_include :"cf_#{hidden_field.id}", query.available_columns.map(&:name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context "#statement" do
|
context "#statement" do
|
||||||
context "with 'member_of_group' filter" do
|
context "with 'member_of_group' filter" do
|
||||||
setup do
|
setup do
|
||||||
|
|
Loading…
Reference in New Issue