Index: app/helpers/issues_helper.rb =================================================================== --- app/helpers/issues_helper.rb (revision 12960) +++ app/helpers/issues_helper.rb (working copy) @@ -234,7 +234,7 @@ def email_issue_attributes(issue, user) items = [] %w(author status priority assigned_to category fixed_version).each do |attribute| - unless issue.disabled_core_fields.include?(attribute+"_id") + unless issue.disabled_core_fields.include?(attribute+"_id") or issue.hidden_attribute_names(user).include?(attribute+"_id") items << "#{l("field_#{attribute}")}: #{issue.send attribute}" end end @@ -241,6 +241,7 @@ issue.visible_custom_field_values(user).each do |value| items << "#{value.custom_field.name}: #{show_value(value)}" end + items end @@ -260,30 +261,34 @@ strings = [] values_by_field = {} details.each do |detail| - if detail.property == 'cf' - field = detail.custom_field - if field && field.multiple? - values_by_field[field] ||= {:added => [], :deleted => []} - if detail.old_value - values_by_field[field][:deleted] << detail.old_value + unless (detail.property == 'cf' || detail.property == 'attr') && detail.journal.issue.hidden_attribute?(detail.prop_key, options[:user]) + if detail.property == 'cf' + field = detail.custom_field + if field && field.multiple? + values_by_field[field] ||= {:added => [], :deleted => []} + if detail.old_value + values_by_field[field][:deleted] << detail.old_value + end + if detail.value + values_by_field[field][:added] << detail.value + end + next end - if detail.value - values_by_field[field][:added] << detail.value - end - next end + strings << show_detail(detail, no_html, options) end - strings << show_detail(detail, no_html, options) end values_by_field.each do |field, changes| detail = JournalDetail.new(:property => 'cf', :prop_key => field.id.to_s) - detail.instance_variable_set "@custom_field", field - if changes[:added].any? - detail.value = changes[:added] - strings << show_detail(detail, no_html, options) - elsif changes[:deleted].any? - detail.old_value = changes[:deleted] - strings << show_detail(detail, no_html, options) + unless detail.journal.issue.hidden_attribute?(detail.prop_key, options[:user]) + detail.instance_variable_set "@custom_field", field + if changes[:added].any? + detail.value = changes[:added] + strings << show_detail(detail, no_html, options) + elsif changes[:deleted].any? + detail.old_value = changes[:deleted] + strings << show_detail(detail, no_html, options) + end end end strings Index: app/helpers/workflows_helper.rb =================================================================== --- app/helpers/workflows_helper.rb (revision 12960) +++ app/helpers/workflows_helper.rb (working copy) @@ -25,6 +25,7 @@ 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_hidden), "hidden"] options << [l(:label_required), "required"] unless field_required?(field) html_options = {} selected = permissions[status.id][name] Index: app/models/issue.rb =================================================================== --- app/models/issue.rb (revision 12960) +++ app/models/issue.rb (working copy) @@ -205,9 +205,11 @@ def visible_custom_field_values(user=nil) user_real = user || User.current - custom_field_values.select do |value| + fields = custom_field_values.select do |value| value.custom_field.visible_by?(project, user_real) end + fields = fields & viewable_custom_field_values(user_real) + fields end # Copies attributes from another issue, arg can be an id or an Issue @@ -482,6 +484,14 @@ read_only_attribute_names(user).include?(value.custom_field_id.to_s) end end + + # Returns the custom_field_values that can be viewed by the given user + # For now: just exclude Fix Info and RNs, as it is printed seperately below description. + def viewable_custom_field_values(user=nil) + custom_field_values.reject do |value| + hidden_attribute_names(user).include?(value.custom_field_id.to_s) + end + end # Returns the names of attributes that are read-only for user or the current user # For users with multiple roles, the read-only fields are the intersection of @@ -492,8 +502,13 @@ # issue.read_only_attribute_names # => ['due_date', '2'] # issue.read_only_attribute_names(user) # => [] def read_only_attribute_names(user=nil) - workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys + workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly' and rule != 'hidden'}.keys end + + # Same as above, but for hidden fields + def hidden_attribute_names(user=nil) + workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'hidden'}.keys + end # Returns the names of required attributes for user or the current user # For users with multiple roles, the required fields are the intersection of @@ -506,6 +521,11 @@ def required_attribute_names(user=nil) workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys end + + # Returns true if the attribute should be hidden for user + def hidden_attribute?(name, user=nil) + hidden_attribute_names(user).include?(name.to_s) + end # Returns true if the attribute is required for user def required_attribute?(name, user=nil) @@ -813,19 +833,20 @@ notified_users.collect(&:mail) end - def each_notification(users, &block) + 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) + variations = users.collect { + |user| (hidden_attribute_names(user) + (custom_field_values - visible_custom_field_values(user))).uniq + }.uniq + recipient_groups = Array.new(variations.count) { Array.new } + users.each do |user| + recipient_groups[variations.index(hidden_attribute_names(user))] << user end - end + + recipient_groups.each do |group| + yield(group) + end + end end # Returns the number of hours spent on this issue Index: app/models/issue_query.rb =================================================================== --- app/models/issue_query.rb (revision 12960) +++ app/models/issue_query.rb (working copy) @@ -248,10 +256,23 @@ def available_columns return @available_columns if @available_columns @available_columns = self.class.available_columns.dup + + if project == nil + hidden_fields = [] + all_projects.each { |prj| + if prj.visible? and User.current.roles_for_project(prj).count > 0 + hidden_fields = hidden_fields == [] ? prj.completely_hidden_attribute_names : hidden_fields & prj.completely_hidden_attribute_names + end + } + else + hidden_fields = project.completely_hidden_attribute_names + end + hidden_fields.map! {|field| field.sub(/_id$/, '')} + @available_columns += (project ? project.all_issue_custom_fields : IssueCustomField - ).visible.collect {|cf| QueryCustomFieldColumn.new(cf) } + ).visible.collect {|cf| QueryCustomFieldColumn.new(cf) }.reject{|column| hidden_fields.include?(column.custom_field.id.to_s) } if User.current.allowed_to?(:view_time_entries, project, :global => true) index = nil @@ -274,6 +295,10 @@ @available_columns.reject! {|column| disabled_fields.include?(column.name.to_s) } + + @available_columns.reject! {|column| + hidden_fields.include?(column.name.to_s) + } @available_columns end Index: app/models/journal.rb =================================================================== --- app/models/journal.rb (revision 12960) +++ app/models/journal.rb (working copy) @@ -57,8 +57,10 @@ # Returns journal details that are visible to user def visible_details(user=User.current) details.select do |detail| - if detail.property == 'cf' - detail.custom_field && detail.custom_field.visible_by?(project, user) + if detail.property == 'attr' + !issue.hidden_attribute?(detail.prop_key, user) + elsif detail.property == 'cf' + detail.custom_field && detail.custom_field.visible_by?(project, user) && !issue.hidden_attribute?(detail.prop_key, user) elsif detail.property == 'relation' Issue.find_by_id(detail.value || detail.old_value).try(:visible?, user) else Index: app/models/project.rb =================================================================== --- app/models/project.rb (revision 12960) +++ app/models/project.rb (working copy) @@ -151,7 +151,36 @@ def visible?(user=User.current) user.allowed_to?(:view_project, self) end + + # Returns list of attributes that are hidden on all statuses of all trackers for +user+ or the current user. + def completely_hidden_attribute_names(user=nil) + user_real = user || User.current + roles = user_real.admin ? Role.all : user_real.roles_for_project(self) + return {} if roles.empty? + result = {} + workflow_permissions = WorkflowPermission.where(:tracker_id => trackers.map(&:id), :old_status_id => IssueStatus.all.map(&:id), :role_id => roles.map(&:id), :rule => 'hidden').all + + if workflow_permissions.any? + workflow_rules = workflow_permissions.inject({}) do |h, wp| + h[wp.field_name] ||= [] + h[wp.field_name] << wp.rule + h + end + workflow_rules.each do |attr, rules| + next if rules.size < (roles.size * trackers.size * IssueStatus.all.size) + uniq_rules = rules.uniq + if uniq_rules.size == 1 + result[attr] = uniq_rules.first + else + result[attr] = 'required' + end + end + end + + result.keys + end + # Returns a SQL conditions string used to find all projects visible by the specified user. # # Examples: Index: app/models/query.rb =================================================================== --- app/models/query.rb (revision 12960) +++ app/models/query.rb (working copy) @@ -54,7 +54,16 @@ end def value(object) - object.send name + if object.respond_to?(:hidden_attribute_names) + hidden_fields = object.hidden_attribute_names.map {|field| field.sub(/_id$/, '')} + if hidden_fields.include?(name.to_s) + "" + else + object.send name + end + else + object.send name + end end def css_classes @@ -331,6 +340,11 @@ options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, '')) end end + + hidden_fields.each {|field| + @available_filters.delete field + } + @available_filters end @@ -762,42 +776,68 @@ return sql end + def hidden_fields + return @hidden_fields if @hidden_fields + + if project != nil + @hidden_fields = project.completely_hidden_attribute_names + else + @hidden_fields = [] + usr = User.current; + first = true + all_projects.each { |prj| + if prj.visible? and usr.roles_for_project(prj).count > 0 + if first + @hidden_fields = prj.completely_hidden_attribute_names(usr) + else + @hidden_fields &= prj.completely_hidden_attribute_names(usr) + end + return @hidden_fields if @hidden_fields.empty? + end + first = false + } + end + return @hidden_fields + end + # Adds a filter for the given custom field def add_custom_field_filter(field, assoc=nil) - case field.field_format - when "text" - options = { :type => :text } - when "list" - options = { :type => :list_optional, :values => field.possible_values } - when "date" - options = { :type => :date } - when "bool" - options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]] } - when "int" - options = { :type => :integer } - when "float" - options = { :type => :float } - when "user", "version" - return unless project - values = field.possible_values_options(project) - if User.current.logged? && field.field_format == 'user' - values.unshift ["<< #{l(:label_me)} >>", "me"] + unless hidden_fields.include?(field.id.to_s) + case field.field_format + when "text" + options = { :type => :text } + when "list" + options = { :type => :list_optional, :values => field.possible_values } + when "date" + options = { :type => :date } + when "bool" + options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]] } + when "int" + options = { :type => :integer } + when "float" + options = { :type => :float } + when "user", "version" + return unless project + values = field.possible_values_options(project) + if User.current.logged? && field.field_format == 'user' + values.unshift ["<< #{l(:label_me)} >>", "me"] + end + options = { :type => :list_optional, :values => values } + else + options = { :type => :string } end - options = { :type => :list_optional, :values => values } - else - options = { :type => :string } + filter_id = "cf_#{field.id}" + filter_name = field.name + if assoc.present? + filter_id = "#{assoc}.#{filter_id}" + filter_name = l("label_attribute_of_#{assoc}", :name => filter_name) + end + add_available_filter filter_id, options.merge({ + :name => filter_name, + :format => field.field_format, + :field => field + }) end - filter_id = "cf_#{field.id}" - filter_name = field.name - if assoc.present? - filter_id = "#{assoc}.#{filter_id}" - filter_name = l("label_attribute_of_#{assoc}", :name => filter_name) - end - add_available_filter filter_id, options.merge({ - :name => filter_name, - :format => field.field_format, - :field => field - }) end # Adds filters for the given custom fields scope Index: app/models/workflow_permission.rb =================================================================== --- app/models/workflow_permission.rb (revision 12960) +++ app/models/workflow_permission.rb (working copy) @@ -16,13 +16,13 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class WorkflowPermission < WorkflowRule - validates_inclusion_of :rule, :in => %w(readonly required) + validates_inclusion_of :rule, :in => %w(readonly required hidden) validate :validate_field_name # Replaces the workflow permissions for the given tracker and role # # Example: - # WorkflowPermission.replace_permissions role, tracker, {'due_date' => {'1' => 'readonly', '2' => 'required'}} + # WorkflowPermission.replace_permissions role, tracker, {'due_date' => {'1' => 'readonly', '2' => 'required', '3' => 'hidden'}} def self.replace_permissions(tracker, role, permissions) destroy_all(:tracker_id => tracker.id, :role_id => role.id) Index: app/views/issues/_history.html.erb =================================================================== --- app/views/issues/_history.html.erb (revision 12960) +++ app/views/issues/_history.html.erb (working copy) @@ -1,22 +1,24 @@ <% reply_links = authorize_for('issues', 'edit') -%> <% for journal in journals %> -
-
-

<%= link_to "##{journal.indice}", {:anchor => "note-#{journal.indice}"}, :class => "journal-link" %> - <%= avatar(journal.user, :size => "24") %> - <%= authoring journal.created_on, journal.user, :label => :label_updated_time_by %>

- - <% if journal.details.any? %> - - <% end %> - <%= render_notes(issue, journal, :reply_links => reply_links) unless journal.notes.blank? %> + <%= render_notes(issue, journal, :reply_links => reply_links) unless journal.notes.blank? %> +
- - <%= call_hook(:view_issues_history_journal_bottom, { :journal => journal }) %> + <%= call_hook(:view_issues_history_journal_bottom, { :journal => journal }) %> + <% end %> <% end %> <% heads_for_wiki_formatter if User.current.allowed_to?(:edit_issue_notes, issue.project) || User.current.allowed_to?(:edit_own_issue_notes, issue.project) %> Index: app/views/issues/show.html.erb =================================================================== --- app/views/issues/show.html.erb (revision 12960) +++ app/views/issues/show.html.erb (working copy) @@ -33,29 +33,33 @@ <%= issue_fields_rows do |rows| - rows.left l(:field_status), h(@issue.status.name), :class => 'status' - rows.left l(:field_priority), h(@issue.priority.name), :class => 'priority' + unless @issue.hidden_attribute?('status') + rows.left l(:field_status), h(@issue.status.name), :class => 'status_id' + end + unless @issue.hidden_attribute?('priority_id') + rows.left l(:field_priority), h(@issue.priority.name), :class => 'priority_id' + end - unless @issue.disabled_core_fields.include?('assigned_to_id') + unless @issue.disabled_core_fields.include?('assigned_to_id') || @issue.hidden_attribute?('assigned_to_id') rows.left l(:field_assigned_to), avatar(@issue.assigned_to, :size => "14").to_s.html_safe + (@issue.assigned_to ? link_to_user(@issue.assigned_to) : "-"), :class => 'assigned-to' end - unless @issue.disabled_core_fields.include?('category_id') + unless @issue.disabled_core_fields.include?('category_id') || @issue.hidden_attribute?('category_id') rows.left l(:field_category), h(@issue.category ? @issue.category.name : "-"), :class => 'category' end - unless @issue.disabled_core_fields.include?('fixed_version_id') + unless @issue.disabled_core_fields.include?('fixed_version_id') || @issue.hidden_attribute?('fixed_version_id') rows.left l(:field_fixed_version), (@issue.fixed_version ? link_to_version(@issue.fixed_version) : "-"), :class => 'fixed-version' end - unless @issue.disabled_core_fields.include?('start_date') + unless @issue.disabled_core_fields.include?('start_date') || @issue.hidden_attribute?('start_date') rows.right l(:field_start_date), format_date(@issue.start_date), :class => 'start-date' end - unless @issue.disabled_core_fields.include?('due_date') + unless @issue.disabled_core_fields.include?('due_date') || @issue.hidden_attribute?('due_date') rows.right l(:field_due_date), format_date(@issue.due_date), :class => 'due-date' end - unless @issue.disabled_core_fields.include?('done_ratio') + unless @issue.disabled_core_fields.include?('done_ratio') || @issue.hidden_attribute?('done_ratio') rows.right l(:field_done_ratio), progress_bar(@issue.done_ratio, :width => '80px', :legend => "#{@issue.done_ratio}%"), :class => 'progress' end - unless @issue.disabled_core_fields.include?('estimated_hours') + unless @issue.disabled_core_fields.include?('estimated_hours') || @issue.hidden_attribute?('estimated_hours') unless @issue.estimated_hours.nil? rows.right l(:field_estimated_hours), l_hours(@issue.estimated_hours), :class => 'estimated-hours' end Index: app/views/mailer/issue_edit.html.erb =================================================================== --- app/views/mailer/issue_edit.html.erb (revision 12960) +++ app/views/mailer/issue_edit.html.erb (working copy) @@ -4,7 +4,7 @@ <%= l(:text_issue_updated, :id => "##{@issue.id}", :author => h(@journal.user)) %> Index: app/views/mailer/issue_edit.text.erb =================================================================== --- app/views/mailer/issue_edit.text.erb (revision 12960) +++ app/views/mailer/issue_edit.text.erb (working copy) @@ -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, :user => @auser).each do |string| -%> <%= string %> <% end -%>