]> source.dussan.org Git - redmine.git/commitdiff
Role-based issue custom field visibility (#5037).
authorJean-Philippe Lang <jp_lang@yahoo.fr>
Sat, 13 Jul 2013 09:20:11 +0000 (09:20 +0000)
committerJean-Philippe Lang <jp_lang@yahoo.fr>
Sat, 13 Jul 2013 09:20:11 +0000 (09:20 +0000)
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@12012 e93f8b46-1217-0410-a6f0-8f06a7374b81

41 files changed:
app/controllers/issues_controller.rb
app/helpers/issues_helper.rb
app/helpers/workflows_helper.rb
app/models/custom_field.rb
app/models/issue.rb
app/models/issue_custom_field.rb
app/models/issue_observer.rb
app/models/issue_query.rb
app/models/journal.rb
app/models/journal_observer.rb
app/models/mail_handler.rb
app/models/mailer.rb
app/models/query.rb
app/models/role.rb
app/models/time_entry_query.rb
app/views/custom_fields/_form.html.erb
app/views/issues/_history.html.erb
app/views/issues/index.api.rsb
app/views/issues/show.api.rsb
app/views/mailer/_issue.html.erb
app/views/mailer/_issue.text.erb
app/views/mailer/issue_add.html.erb
app/views/mailer/issue_add.text.erb
app/views/mailer/issue_edit.html.erb
app/views/mailer/issue_edit.text.erb
app/views/workflows/permissions.html.erb
config/locales/en.yml
config/locales/fr.yml
db/migrate/20130713104233_create_custom_fields_roles.rb [new file with mode: 0644]
lib/plugins/acts_as_searchable/lib/acts_as_searchable.rb
lib/redmine/export/pdf.rb
test/functional/issues_custom_fields_visibility_test.rb [new file with mode: 0644]
test/functional/search_custom_fields_visibility_test.rb [new file with mode: 0644]
test/functional/timelog_custom_fields_visibility_test.rb [new file with mode: 0644]
test/functional/workflows_controller_test.rb
test/test_helper.rb
test/unit/custom_field_test.rb
test/unit/issue_custom_field_test.rb [new file with mode: 0644]
test/unit/lib/redmine/hook_test.rb
test/unit/mailer_test.rb
test/unit/query_test.rb

index 0546db36b8ea4e291078dcc67649ebefdfe1202b..b06e635e07da8be49f3dd50f35547625593a9959 100644 (file)
@@ -103,6 +103,7 @@ class IssuesController < ApplicationController
     @journals = @issue.journals.includes(:user, :details).reorder("#{Journal.table_name}.id ASC").all
     @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.select! {|journal| journal.notes? || journal.visible_details.any?}
     @journals.reverse! if User.current.wants_comments_in_reverse_order?
 
     @changesets = @issue.changesets.visible.all
@@ -230,7 +231,7 @@ class IssuesController < ApplicationController
     else
       @available_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
     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(:&)
     @trackers = target_projects.map(&:trackers).reduce(:&)
     @versions = target_projects.map {|p| p.shared_versions.open}.reduce(:&)
index f88a61e218295211b7cc98c033d1e547b0534dbf..2e45d2e976ec4fee9970107e6c238222754d7fe4 100644 (file)
@@ -160,12 +160,13 @@ module IssuesHelper
   end
 
   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 = []
-    half = (issue.custom_field_values.size / 2.0).ceil
+    half = (values.size / 2.0).ceil
     half.times do |i|
-      ordered_values << issue.custom_field_values[i]
-      ordered_values << issue.custom_field_values[i + half]
+      ordered_values << values[i]
+      ordered_values << values[i + half]
     end
     s = "<tr>\n"
     n = 0
index 1cec67a8852c72212229e71c3dbd3a5c89d9899c..7ef6e994209b72c62b286a00fe60f6885752bf6e 100644 (file)
@@ -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)
   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
     options = [["", ""], [l(:label_readonly), "readonly"]]
     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
index c15192388ec212b0f996fb364f525fb459be1a42..c2b717d0ee5c090857407e8a143e95b64447af1c 100644 (file)
@@ -19,6 +19,7 @@ class CustomField < ActiveRecord::Base
   include Redmine::SubclassFactory
 
   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}\''
   serialize :possible_values
 
@@ -26,12 +27,31 @@ class CustomField < ActiveRecord::Base
   validates_uniqueness_of :name, :scope => :type
   validates_length_of :name, :maximum => 30
   validates_inclusion_of :field_format, :in => Redmine::CustomFieldFormat.available_formats
-
   validate :validate_custom_field
+
   before_validation :set_searchable
   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 :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 = [
     {: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}'" +
           " AND #{join_alias}.customized_id = #{self.class.customized_class.table_name}.id" +
           " AND #{join_alias}.custom_field_id = #{id}" +
+          " AND (#{visibility_by_project_condition})" +
           " AND #{join_alias}.value <> ''" +
           " 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" +
@@ -227,6 +248,7 @@ class CustomField < ActiveRecord::Base
           " 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}.custom_field_id = #{id}" +
+          " AND (#{visibility_by_project_condition})" +
           " AND #{join_alias}.value <> ''" +
           " 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" +
@@ -237,6 +259,7 @@ class CustomField < ActiveRecord::Base
           " 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}.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" +
             " WHERE #{join_alias}_2.customized_type = #{join_alias}.customized_type" +
             " AND #{join_alias}_2.customized_id = #{join_alias}.customized_id" +
@@ -254,6 +277,33 @@ class CustomField < ActiveRecord::Base
     join_alias + "_" + field_format
   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)
     position <=> field.position
   end
index ccedcfd4c5b77a2325e858bbfaf4d8c9a9349eda..69ace9f3131acc791f1e3efc6704f4cf57df1ee8 100644 (file)
@@ -198,6 +198,13 @@ class Issue < ActiveRecord::Base
     (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
   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
   def copy_from(arg, options={})
     issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
@@ -445,11 +452,13 @@ class Issue < ActiveRecord::Base
     end
 
     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
 
     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
 
     # 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
   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)
     end
   end
@@ -790,6 +799,21 @@ class Issue < ActiveRecord::Base
     notified_users.collect(&:mail)
   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
   def spent_hours
     @spent_hours ||= time_entries.sum(:hours) || 0
index e7ea9c0b3bddcc2cecff7cbe6589f0e37cd2aad1..9ddedc88250416fbbdf3c1fef0ff8a6b4bff11b0 100644 (file)
@@ -23,5 +23,13 @@ class IssueCustomField < CustomField
   def type_name
     :label_issue_plural
   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
index a7519428648f6540791e38af7554537e399f934c..58b636031ab9f9b7862c593c12a847651f02ca89 100644 (file)
@@ -17,6 +17,6 @@
 
 class IssueObserver < ActiveRecord::Observer
   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
index b2e470d7a6403b0e61f7abbbe910c56035621be5..323e46f4c4b5a8b5d6b6e7da21de2afa46bdaf12 100644 (file)
@@ -225,8 +225,8 @@ class IssueQuery < Query
     @available_columns = self.class.available_columns.dup
     @available_columns += (project ?
                             project.all_issue_custom_fields :
-                            IssueCustomField.all
-                           ).collect {|cf| QueryCustomFieldColumn.new(cf) }
+                            IssueCustomField
+                           ).visible.collect {|cf| QueryCustomFieldColumn.new(cf) }
 
     if User.current.allowed_to?(:view_time_entries, project, :global => true)
       index = nil
index a75c112db97dfac4e1015a30707b0b71140bdef2..c14051f8392a90828c4499dcb96890fcf3123662 100644 (file)
@@ -53,6 +53,18 @@ class Journal < ActiveRecord::Base
     (details.empty? && notes.blank?) ? false : super
   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
   def new_status
     c = details.detect {|detail| detail.prop_key == 'status_id'}
@@ -93,20 +105,28 @@ class Journal < ActiveRecord::Base
     @notify = arg
   end
 
-  def recipients
+  def notified_users
     notified = journalized.notified_users
     if private_notes?
       notified = notified.select {|user| user.allowed_to?(:view_private_notes, journalized.project)}
     end
-    notified.map(&:mail)
+    notified
   end
 
-  def watcher_recipients
+  def recipients
+    notified_users.map(&:mail)
+  end
+
+  def notified_watchers
     notified = journalized.notified_watchers
     if private_notes?
       notified = notified.select {|user| user.allowed_to?(:view_private_notes, journalized.project)}
     end
-    notified.map(&:mail)
+    notified
+  end
+
+  def watcher_recipients
+    notified_watchers.map(&:mail)
   end
 
   private
index fe937de070d34a9e49581988df9731f2ed60b01e..ff4b898374cd980a6705c9527f6f2771e3d6be16 100644 (file)
@@ -23,7 +23,7 @@ class JournalObserver < ActiveRecord::Observer
           (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?)
         )
-      Mailer.issue_edit(journal).deliver
+      Mailer.deliver_issue_edit(journal)
     end
   end
 end
index 90850dffbd487a3a181d64b59794613c818afe91..fb30374bd7c9f437d6aeb3b52972bfbe1a56273c 100644 (file)
@@ -136,7 +136,7 @@ class MailHandler < ActionMailer::Base
 
   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+)\]}
   MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
 
index 6e07d61f77a630c81e6f53edbaad863c8c6e0dd6..6a8aded13d9df624a864d90fa3a1a2036195d784 100644 (file)
@@ -27,34 +27,35 @@ class Mailer < ActionMailer::Base
     { :host => Setting.host_name, :protocol => Setting.protocol }
   end
 
-  # Builds a Mail::Message object used to email recipients of the added issue.
-  #
-  # Example:
-  #   issue_add(issue) => Mail::Message object
-  #   Mailer.issue_add(issue).deliver => sends an email to issue recipients
-  def issue_add(issue)
+  # Builds a mail for notifying to_users and cc_users about a new issue
+  def issue_add(issue, to_users, cc_users)
     redmine_headers 'Project' => issue.project.identifier,
                     'Issue-Id' => issue.id,
                     'Issue-Author' => issue.author.login
     redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
     message_id issue
+    references issue
     @author = issue.author
     @issue = issue
+    @users = to_users + cc_users
     @issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue)
-    recipients = issue.recipients
-    cc = issue.watcher_recipients - recipients
-    mail :to => recipients,
-      :cc => cc,
+    mail :to => to_users.map(&:mail),
+      :cc => cc_users.map(&:mail),
       :subject => "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
   end
 
-  # Builds a Mail::Message object used to email recipients of the edited issue.
-  #
-  # Example:
-  #   issue_edit(journal) => Mail::Message object
-  #   Mailer.issue_edit(journal).deliver => sends an email to issue recipients
-  def issue_edit(journal)
-    issue = journal.journalized.reload
+  # Notifies users about a new issue
+  def self.deliver_issue_add(issue)
+    to = issue.notified_users
+    cc = issue.notified_watchers - to
+    issue.each_notification(to + cc) do |users|
+      Mailer.issue_add(issue, to & users, cc & users).deliver
+    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,
                     'Issue-Id' => issue.id,
                     'Issue-Author' => issue.author.login
@@ -62,20 +63,30 @@ class Mailer < ActionMailer::Base
     message_id journal
     references issue
     @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.status.name}) " if journal.new_value_for('status_id')
     s << issue.subject
     @issue = issue
+    @users = to_users + cc_users
     @journal = journal
+    @journal_details = journal.visible_details(@users.first)
     @issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue, :anchor => "change-#{journal.id}")
-    mail :to => recipients,
-      :cc => cc,
+    mail :to => to_users.map(&:mail),
+      :cc => cc_users.map(&:mail),
       :subject => s
   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)
     set_language_if_valid user.language
     @issues = issues
@@ -142,6 +153,7 @@ class Mailer < ActionMailer::Base
     redmine_headers 'Project' => news.project.identifier
     @author = news.author
     message_id news
+    references news
     @news = news
     @news_url = url_for(:controller => 'news', :action => 'show', :id => news)
     mail :to => news.recipients,
@@ -158,6 +170,7 @@ class Mailer < ActionMailer::Base
     redmine_headers 'Project' => news.project.identifier
     @author = comment.author
     message_id comment
+    references news
     @news = news
     @comment = comment
     @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)
     @author = message.author
     message_id message
-    references message.parent unless message.parent.nil?
+    references message.root
     recipients = message.recipients
     cc = ((message.root.watcher_recipients + message.board.watcher_recipients).uniq - recipients)
     @message = message
@@ -386,7 +399,7 @@ class Mailer < ActionMailer::Base
       headers[:message_id] = "<#{self.class.message_id_for(@message_id_object)}>"
     end
     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
 
     super headers do |format|
@@ -434,15 +447,30 @@ class Mailer < ActionMailer::Base
     h.each { |k,v| headers["X-Redmine-#{k}"] = v.to_s }
   end
 
-  # Returns a predictable Message-Id for the given object
-  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
+  def self.token_for(object, rand=true)
     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 = "#{::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
 
   def message_id(object)
index 2753c276887b9a1360fe60e973f17ab79d1eef4e..641a263c678b7fa99a13ac9a4547a06769be44be 100644 (file)
@@ -81,8 +81,12 @@ class QueryCustomFieldColumn < QueryColumn
   end
 
   def value(object)
-    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
+    if custom_field.visible_by?(object.project, User.current)
+      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
 
   def css_classes
@@ -560,6 +564,11 @@ class Query < ActiveRecord::Base
       end
     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.reject!(&:blank?)
 
@@ -596,7 +605,10 @@ class Query < ActiveRecord::Base
     if operator =~ /[<>]/
       where = "(#{where}) AND #{db_table}.#{db_field} <> ''"
     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
 
   # 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
   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)
     end
   end
 
   # Adds filters for the given associations custom fields
   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|
       association_klass = queried_class.reflect_on_association(assoc).klass
       fields_by_class.each do |field_class, fields|
index d24103663194dc9f691406c9f69518d20e83c0d4..751ec6eb4c27b0fa9ae2e87fcf59ac539f3289c8 100644 (file)
@@ -52,6 +52,7 @@ class Role < ActiveRecord::Base
       WorkflowRule.copy(nil, source_role, nil, proxy_association.owner)
     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 :members, :through => :member_roles
index dbccea03dce0c6dee37b62e47c07029f561bba66..3a6ab64ef16bebed8780102cb10d9c9d1ebc8657 100644 (file)
@@ -91,8 +91,8 @@ class TimeEntryQuery < Query
   def available_columns
     return @available_columns if @available_columns
     @available_columns = self.class.available_columns.dup
-    @available_columns += TimeEntryCustomField.all.map {|cf| QueryCustomFieldColumn.new(cf) }
-    @available_columns += IssueCustomField.all.map {|cf| QueryAssociationCustomFieldColumn.new(:issue, cf) }
+    @available_columns += TimeEntryCustomField.visible.all.map {|cf| QueryCustomFieldColumn.new(cf) }
+    @available_columns += IssueCustomField.visible.all.map {|cf| QueryAssociationCustomFieldColumn.new(:issue, cf) }
     @available_columns
   end
 
index b6f7ddb1dae3a97e75c11e251745953839ce9d18..4c0b702435ad1e1943f8ffcb7e9226ec29b88879 100644 (file)
@@ -64,6 +64,24 @@ when "IssueCustomField" %>
     <p><%= f.check_box :is_for_all %></p>
     <p><%= f.check_box :is_filter %></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" %>
     <p><%= f.check_box :is_required %></p>
@@ -97,3 +115,12 @@ when "IssueCustomField" %>
 </div>
 
 <% 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 %>
index 470a55acf1a780947af42855e50d6b1ac53f4635..94d98ccc7deb737fb13fe6b03fb4ac8b82bf4f17 100644 (file)
@@ -8,7 +8,7 @@
 
     <% if journal.details.any? %>
     <ul class="details">
-      <% details_to_strings(journal.details).each do |string| %>
+      <% details_to_strings(journal.visible_details).each do |string| %>
        <li><%= string %></li>
       <% end %>
     </ul>
index 5009ffa7eb5a0bb85d5b304564954fc04c03159d..c3bcfd74b03a52fdd38e3d4422bd7b05a718e800 100644 (file)
@@ -19,7 +19,7 @@ api.array :issues, api_meta(:total_count => @issue_count, :offset => @offset, :l
       api.done_ratio  issue.done_ratio
       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.updated_on issue.updated_on
index ce788a72348c38ca01fe5bd7919b23a5de3675bc..3878e71bdebbad53e10118a7f1ee429dc1a8120e 100644 (file)
@@ -18,7 +18,7 @@ api.issue do
   api.estimated_hours @issue.estimated_hours
   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.updated_on @issue.updated_on
@@ -55,7 +55,7 @@ api.issue do
         api.notes journal.notes
         api.created_on journal.created_on
         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.old_value detail.old_value
               api.new_value detail.value
index 829f8d5768299aabe85c6cbc9bbb9bef089ad0a2..aee365e5681eb084609d7d36dd10005747bec543 100644 (file)
@@ -7,7 +7,7 @@
 <li><%=l(:field_assigned_to)%>: <%=h issue.assigned_to %></li>
 <li><%=l(:field_category)%>: <%=h issue.category %></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>
 <% end %>
 </ul>
index 55448804924a2c55cbbcca25d83194fe40705353..a2d5a41b2e382e3be3c497356a543b211c6b3eb7 100644 (file)
@@ -7,7 +7,7 @@
 * <%=l(:field_assigned_to)%>: <%= issue.assigned_to %>
 * <%=l(:field_category)%>: <%= issue.category %>
 * <%=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 -%>
 ----------------------------------------
 <%= issue.description %>
index fb4a2dab66b3747d018b1e6c1d60d8cc2164f3cd..99fd08d14050d16767fec435763616e5b306dcfd 100644 (file)
@@ -1,3 +1,3 @@
 <%= l(:text_issue_added, :id => "##{@issue.id}", :author => h(@issue.author)) %>
 <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 } %>
index e990ff0d2191f0ace9fa66a782f3694d920de71a..6e3b427259c8414cfc1a70542cf08a44c9843183 100644 (file)
@@ -1,4 +1,4 @@
 <%= 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 } %>
index 3222519126f330e413c9a7f2d636bcb5f53e27d4..e3b6f5c6c22eda234739720b458ccf7cae172037 100644 (file)
@@ -4,11 +4,11 @@
 <%= l(:text_issue_updated, :id => "##{@issue.id}", :author => h(@journal.user)) %>
 
 <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>
 <% end %>
 </ul>
 
 <%= textilizable(@journal, :notes, :only_path => false) %>
 <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 } %>
index 395f8f6264908718d4cdf985c260ae4217499cde..173d2c4fe73796288f3550f8b34ed0eed29f777c 100644 (file)
@@ -1,6 +1,6 @@
 <%= "(#{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 %>
 <% end -%>
 
@@ -9,4 +9,4 @@
 
 <% 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 } %>
index b646146b0f2add410629c411f46b024d6fb79784..6046201d31ca96bab5fbf7ec12beac6c1381d19b 100644 (file)
@@ -62,7 +62,7 @@
         </td>
         <% for status in @statuses -%>
         <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">&#187;</a><% end %>
         </td>
         <% end -%>
@@ -82,7 +82,7 @@
           </td>
           <% for status in @statuses -%>
           <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">&#187;</a><% end %>
           </td>
           <% end -%>
index 7099de8c4351559cd5ca402212628e133748d461..18221e8378be6b32b5b834013f164135e26a58d4 100644 (file)
@@ -885,6 +885,7 @@ en:
   label_fields_permissions: Fields permissions
   label_readonly: Read-only
   label_required: Required
+  label_hidden: Hidden
   label_attribute_of_project: "Project's %{name}"
   label_attribute_of_issue: "Issue's %{name}"
   label_attribute_of_author: "Author's %{name}"
index 510913a1778068c9235aecc4388a0b85c79a3956..13fcbf0f1908bfcf4b78b8c6551037aca486ec85 100644 (file)
@@ -861,6 +861,7 @@ fr:
   label_fields_permissions: Permissions sur les champs
   label_readonly: Lecture
   label_required: Obligatoire
+  label_hidden: Caché
   label_attribute_of_project: "%{name} du projet"
   label_attribute_of_issue: "%{name} de la demande"
   label_attribute_of_author: "%{name} de l'auteur"
diff --git a/db/migrate/20130713104233_create_custom_fields_roles.rb b/db/migrate/20130713104233_create_custom_fields_roles.rb
new file mode 100644 (file)
index 0000000..458bfb5
--- /dev/null
@@ -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
index 5a1f3675202686b326b986ceb4188572e3aa2a6d..3e91c72dc483e38d1a3dc2041e5e1d3b709d16de 100644 (file)
@@ -81,12 +81,13 @@ module Redmine
             token_clauses = columns.collect {|column| "(LOWER(#{column}) LIKE ?)"}
 
             if !options[:titles_only] && searchable_options[:search_custom_fields]
-              searchable_custom_field_ids = CustomField.where(:type => "#{self.name}CustomField", :searchable => true).pluck(:id)
-              if searchable_custom_field_ids.any?
-                custom_field_sql = "#{table_name}.id IN (SELECT customized_id FROM #{CustomValue.table_name}" +
+              searchable_custom_fields = CustomField.where(:type => "#{self.name}CustomField", :searchable => true)
+              searchable_custom_fields.each do |field|
+                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 ?" +
-                  " AND #{CustomValue.table_name}.custom_field_id IN (#{searchable_custom_field_ids.join(',')}))"
-                token_clauses << custom_field_sql
+                  " AND #{CustomValue.table_name}.custom_field_id = #{field.id})" +
+                  " AND #{field.visibility_by_project_condition(searchable_options[:project_key], user)}"
+                token_clauses << sql
               end
             end
 
index f720e662aada999ed25096f99935b3da3f9935ce..193319aa592c4bd5f2d57e1d972a32038f398575 100644 (file)
@@ -256,7 +256,7 @@ module Redmine
       def fetch_row_values(issue, query, level)
         query.inline_columns.collect do |column|
           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)
           else
             value = issue.send(column.name)
@@ -571,8 +571,8 @@ module Redmine
           right << nil
         end
 
-        half = (issue.custom_field_values.size / 2.0).ceil
-        issue.custom_field_values.each_with_index do |custom_value, i|
+        half = (issue.visible_custom_field_values.size / 2.0).ceil
+        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)]
         end
 
@@ -683,7 +683,7 @@ module Redmine
             pdf.RDMCell(190,5, title)
             pdf.Ln
             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)
             end
             if journal.notes?
diff --git a/test/functional/issues_custom_fields_visibility_test.rb b/test/functional/issues_custom_fields_visibility_test.rb
new file mode 100644 (file)
index 0000000..dfe6e07
--- /dev/null
@@ -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
diff --git a/test/functional/search_custom_fields_visibility_test.rb b/test/functional/search_custom_fields_visibility_test.rb
new file mode 100644 (file)
index 0000000..9b88aec
--- /dev/null
@@ -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
diff --git a/test/functional/timelog_custom_fields_visibility_test.rb b/test/functional/timelog_custom_fields_visibility_test.rb
new file mode 100644 (file)
index 0000000..c90eadc
--- /dev/null
@@ -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
index f5bf3910b95fb32588739b7f7fd0ae80810562aa..001cb1a4e3bc83c1b727bb7f15c0b07b07080244 100644 (file)
@@ -200,6 +200,23 @@ class WorkflowsControllerTest < ActionController::TestCase
     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
     WorkflowTransition.delete_all
 
index 39826bc2da4ce3ebace8fe141bccf9ad16fb8b50..725b1f596aad752e42eec82d1a9f5eddf934f0d9 100644 (file)
@@ -169,8 +169,8 @@ class ActiveSupport::TestCase
     assert s.include?(expected), (message || "\"#{expected}\" not found in \"#{s}\"")
   end
 
-  def assert_not_include(expected, s)
-    assert !s.include?(expected), "\"#{expected}\" found in \"#{s}\""
+  def assert_not_include(expected, s, message=nil)
+    assert !s.include?(expected), (message || "\"#{expected}\" found in \"#{s}\"")
   end
 
   def assert_select_in(text, *args, &block)
@@ -178,19 +178,19 @@ class ActiveSupport::TestCase
     assert_select(d, *args, &block)
   end
 
-  def assert_mail_body_match(expected, mail)
+  def assert_mail_body_match(expected, mail, message=nil)
     if expected.is_a?(String)
-      assert_include expected, mail_body(mail)
+      assert_include expected, mail_body(mail), message
     else
-      assert_match expected, mail_body(mail)
+      assert_match expected, mail_body(mail), message
     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)
-      assert_not_include expected, mail_body(mail)
+      assert_not_include expected, mail_body(mail), message
     else
-      assert_no_match expected, mail_body(mail)
+      assert_no_match expected, mail_body(mail), message
     end
   end
 
index 17a0041c08b245ad555e165d5f0e433071d44020..051853abc3eb3b3d0a8429352adc7ecaf91f8fd1 100644 (file)
@@ -241,4 +241,42 @@ class CustomFieldTest < ActiveSupport::TestCase
     field = CustomField.find(1)
     assert_equal 'PostgreSQL', field.value_from_keyword('postgresql', Issue.find(1))
   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
diff --git a/test/unit/issue_custom_field_test.rb b/test/unit/issue_custom_field_test.rb
new file mode 100644 (file)
index 0000000..26cc844
--- /dev/null
@@ -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
index 9f81b912decd8c8bf526730eb0318563f9a3663d..f5ee1179fa152eac1a4466b9694f9d3a99418bff 100644 (file)
@@ -154,14 +154,14 @@ class Redmine::Hook::ManagerTest < ActionView::TestCase
     issue = Issue.find(1)
 
     ActionMailer::Base.deliveries.clear
-    Mailer.issue_add(issue).deliver
+    Mailer.deliver_issue_add(issue)
     mail = ActionMailer::Base.deliveries.last
 
     @hook_module.add_listener(TestLinkToHook)
     hook_helper.call_hook(:view_layouts_base_html_head)
 
     ActionMailer::Base.deliveries.clear
-    Mailer.issue_add(issue).deliver
+    Mailer.deliver_issue_add(issue)
     mail2 = ActionMailer::Base.deliveries.last
 
     assert_equal mail_body(mail), mail_body(mail2)
index c4891678f2eb660312f317b176b55fafd7de1fea..93f4567d3614aa573d67d7c4f103ff6308db717c 100644 (file)
@@ -42,7 +42,7 @@ class MailerTest < ActiveSupport::TestCase
     Setting.protocol = 'https'
 
     journal = Journal.find(3)
-    assert Mailer.issue_edit(journal).deliver
+    assert Mailer.deliver_issue_edit(journal)
 
     mail = last_email
     assert_not_nil mail
@@ -81,7 +81,7 @@ class MailerTest < ActiveSupport::TestCase
     Setting.protocol = 'http'
 
     journal = Journal.find(3)
-    assert Mailer.issue_edit(journal).deliver
+    assert Mailer.deliver_issue_edit(journal)
 
     mail = last_email
     assert_not_nil mail
@@ -121,7 +121,7 @@ class MailerTest < ActiveSupport::TestCase
     Redmine::Utils.relative_url_root = nil
 
     journal = Journal.find(3)
-    assert Mailer.issue_edit(journal).deliver
+    assert Mailer.deliver_issue_edit(journal)
 
     mail = last_email
     assert_not_nil mail
@@ -158,7 +158,7 @@ class MailerTest < ActiveSupport::TestCase
 
   def test_email_headers
     issue = Issue.find(1)
-    Mailer.issue_add(issue).deliver
+    Mailer.deliver_issue_add(issue)
     mail = last_email
     assert_not_nil mail
     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
     issue = Issue.find(1)
-    Mailer.issue_add(issue).deliver
+    Mailer.deliver_issue_add(issue)
     mail = last_email
     assert_equal issue.author.login, mail.header['X-Redmine-Sender'].to_s
   end
@@ -176,7 +176,7 @@ class MailerTest < ActiveSupport::TestCase
   def test_plain_text_mail
     Setting.plain_text_mail = 1
     journal = Journal.find(2)
-    Mailer.issue_edit(journal).deliver
+    Mailer.deliver_issue_edit(journal)
     mail = last_email
     assert_equal "text/plain; charset=UTF-8", mail.content_type
     assert_equal 0, mail.parts.size
@@ -186,7 +186,7 @@ class MailerTest < ActiveSupport::TestCase
   def test_html_mail
     Setting.plain_text_mail = 0
     journal = Journal.find(2)
-    Mailer.issue_edit(journal).deliver
+    Mailer.deliver_issue_edit(journal)
     mail = last_email
     assert_equal 2, mail.parts.size
     assert mail.encoded.include?('href')
@@ -231,19 +231,21 @@ class MailerTest < ActiveSupport::TestCase
   end
 
   def test_issue_add_message_id
-    issue = Issue.find(1)
-    Mailer.issue_add(issue).deliver
+    issue = Issue.find(2)
+    Mailer.deliver_issue_add(issue)
     mail = last_email
-    assert_equal Mailer.message_id_for(issue), mail.message_id
-    assert_nil mail.references
+    assert_match /^redmine\.issue-2\.20060719190421\.[a-f0-9]+@example\.net/, mail.message_id
+    assert_include "redmine.issue-2.20060719190421@example.net", mail.references
   end
 
   def test_issue_edit_message_id
-    journal = Journal.find(1)
-    Mailer.issue_edit(journal).deliver
+    journal = Journal.find(3)
+    journal.issue = Issue.find(2)
+
+    Mailer.deliver_issue_edit(journal)
     mail = last_email
-    assert_equal Mailer.message_id_for(journal), mail.message_id
-    assert_include Mailer.message_id_for(journal.issue), mail.references
+    assert_match /^redmine\.journal-3\.\d+\.[a-f0-9]+@example\.net/, mail.message_id
+    assert_include "redmine.issue-2.20060719190421@example.net", mail.references
     assert_select_email do
       # link to the update
       assert_select "a[href=?]",
@@ -255,8 +257,8 @@ class MailerTest < ActiveSupport::TestCase
     message = Message.find(1)
     Mailer.message_posted(message).deliver
     mail = last_email
-    assert_equal Mailer.message_id_for(message), mail.message_id
-    assert_nil mail.references
+    assert_match /^redmine\.message-1\.\d+\.[a-f0-9]+@example\.net/, mail.message_id
+    assert_include "redmine.message-1.20070512151532@example.net", mail.references
     assert_select_email do
       # link to the message
       assert_select "a[href=?]",
@@ -269,8 +271,8 @@ class MailerTest < ActiveSupport::TestCase
     message = Message.find(3)
     Mailer.message_posted(message).deliver
     mail = last_email
-    assert_equal Mailer.message_id_for(message), mail.message_id
-    assert_include Mailer.message_id_for(message.parent), mail.references
+    assert_match /^redmine\.message-3\.\d+\.[a-f0-9]+@example\.net/, mail.message_id
+    assert_include "redmine.message-1.20070512151532@example.net", mail.references
     assert_select_email do
       # link to the reply
       assert_select "a[href=?]",
@@ -281,14 +283,14 @@ class MailerTest < ActiveSupport::TestCase
 
   test "#issue_add should notify project members" do
     issue = Issue.find(1)
-    assert Mailer.issue_add(issue).deliver
+    assert Mailer.deliver_issue_add(issue)
     assert last_email.bcc.include?('dlopper@somenet.foo')
   end
 
   test "#issue_add should not notify project members that are not allow to view the issue" do
     issue = Issue.find(1)
     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')
   end
 
@@ -302,7 +304,7 @@ class MailerTest < ActiveSupport::TestCase
     user.save
 
     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)
   end
 
@@ -311,7 +313,7 @@ class MailerTest < ActiveSupport::TestCase
     user = User.find(9)
     Watcher.create!(:watchable => issue, :user => user)
     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)
   end
 
@@ -320,7 +322,7 @@ class MailerTest < ActiveSupport::TestCase
     issue = Issue.find(1)
     valid_languages.each do |lang|
       Setting.default_language = lang.to_s
-      assert Mailer.issue_add(issue).deliver
+      assert Mailer.deliver_issue_add(issue)
     end
   end
 
@@ -328,7 +330,7 @@ class MailerTest < ActiveSupport::TestCase
     journal = Journal.find(1)
     valid_languages.each do |lang|
       Setting.default_language = lang.to_s
-      assert Mailer.issue_edit(journal).deliver
+      assert Mailer.deliver_issue_edit(journal)
     end
   end
 
@@ -338,11 +340,11 @@ class MailerTest < ActiveSupport::TestCase
     journal.save!
 
     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
 
     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
   end
 
@@ -353,11 +355,11 @@ class MailerTest < ActiveSupport::TestCase
     journal.save!
 
     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
 
     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
   end
 
@@ -367,7 +369,7 @@ class MailerTest < ActiveSupport::TestCase
     journal.save!
 
     with_settings :default_language => 'en' do
-      Mailer.issue_edit(journal).deliver
+      Mailer.deliver_issue_edit(journal)
     end
     assert_mail_body_match '(Private notes)', last_email
   end
index 15f1cf21c0c79f576edc545fd781d4095223d659..4ec430cf743ccf6236e7aaeb15ab46d2138bc035 100644 (file)
@@ -1201,6 +1201,28 @@ class QueryTest < ActiveSupport::TestCase
     assert ! query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5'])
   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 "with 'member_of_group' filter" do
       setup do