Project

General

Profile

Feature #12005 » workflow_hidden_fields_redmine2.4.4_v1.01.patch

Patch file for Redmine v2.4.4 (r12953) - David Robinson, 2014-03-14 17:22

View differences:

app/helpers/issues_helper.rb (working copy)
234 234
  def email_issue_attributes(issue, user)
235 235
    items = []
236 236
    %w(author status priority assigned_to category fixed_version).each do |attribute|
237
      unless issue.disabled_core_fields.include?(attribute+"_id")
237
      unless issue.disabled_core_fields.include?(attribute+"_id") or issue.hidden_attribute_names(user).include?(attribute+"_id")
238 238
        items << "#{l("field_#{attribute}")}: #{issue.send attribute}"
239 239
      end
240 240
    end
241 241
    issue.visible_custom_field_values(user).each do |value|
242 242
      items << "#{value.custom_field.name}: #{show_value(value)}"
243 243
    end
244
    
244 245
    items
245 246
  end
246 247

  
......
260 261
    strings = []
261 262
    values_by_field = {}
262 263
    details.each do |detail|
263
      if detail.property == 'cf'
264
        field = detail.custom_field
265
        if field && field.multiple?
266
          values_by_field[field] ||= {:added => [], :deleted => []}
267
          if detail.old_value
268
            values_by_field[field][:deleted] << detail.old_value
264
      unless (detail.property == 'cf' || detail.property == 'attr') &&  detail.journal.issue.hidden_attribute?(detail.prop_key, options[:user])
265
        if detail.property == 'cf'
266
          field = detail.custom_field
267
          if field && field.multiple?
268
            values_by_field[field] ||= {:added => [], :deleted => []}
269
            if detail.old_value
270
              values_by_field[field][:deleted] << detail.old_value
271
            end
272
            if detail.value
273
              values_by_field[field][:added] << detail.value
274
            end
275
            next
269 276
          end
270
          if detail.value
271
            values_by_field[field][:added] << detail.value
272
          end
273
          next
274 277
        end
278
        strings << show_detail(detail, no_html, options)
275 279
      end
276
      strings << show_detail(detail, no_html, options)
277 280
    end
278 281
    values_by_field.each do |field, changes|
279 282
      detail = JournalDetail.new(:property => 'cf', :prop_key => field.id.to_s)
280
      detail.instance_variable_set "@custom_field", field
281
      if changes[:added].any?
282
        detail.value = changes[:added]
283
        strings << show_detail(detail, no_html, options)
284
      elsif changes[:deleted].any?
285
        detail.old_value = changes[:deleted]
286
        strings << show_detail(detail, no_html, options)
283
      unless detail.journal.issue.hidden_attribute?(detail.prop_key, options[:user])
284
        detail.instance_variable_set "@custom_field", field
285
        if changes[:added].any?
286
          detail.value = changes[:added]
287
          strings << show_detail(detail, no_html, options)
288
        elsif changes[:deleted].any?
289
          detail.old_value = changes[:deleted]
290
          strings << show_detail(detail, no_html, options)
291
        end
287 292
      end
288 293
    end
289 294
    strings
app/helpers/workflows_helper.rb (working copy)
25 25
  def field_permission_tag(permissions, status, field, role)
26 26
    name = field.is_a?(CustomField) ? field.id.to_s : field
27 27
    options = [["", ""], [l(:label_readonly), "readonly"]]
28
    options << [l(:label_hidden), "hidden"]
28 29
    options << [l(:label_required), "required"] unless field_required?(field)
29 30
    html_options = {}
30 31
    selected = permissions[status.id][name]
app/models/issue.rb (working copy)
205 205

  
206 206
  def visible_custom_field_values(user=nil)
207 207
    user_real = user || User.current
208
    custom_field_values.select do |value|
208
    fields = custom_field_values.select do |value|
209 209
      value.custom_field.visible_by?(project, user_real)
210 210
    end
211
    fields = fields & viewable_custom_field_values(user_real)
212
    fields
211 213
  end
212 214

  
213 215
  # Copies attributes from another issue, arg can be an id or an Issue
......
482 484
      read_only_attribute_names(user).include?(value.custom_field_id.to_s)
483 485
    end
484 486
  end
487
  
488
  # Returns the custom_field_values that can be viewed by the given user
489
  # For now: just exclude Fix Info and RNs, as it is printed seperately below description.
490
  def viewable_custom_field_values(user=nil)
491
    custom_field_values.reject do |value|
492
      hidden_attribute_names(user).include?(value.custom_field_id.to_s)
493
    end
494
  end  
485 495

  
486 496
  # Returns the names of attributes that are read-only for user or the current user
487 497
  # For users with multiple roles, the read-only fields are the intersection of
......
492 502
  #   issue.read_only_attribute_names # => ['due_date', '2']
493 503
  #   issue.read_only_attribute_names(user) # => []
494 504
  def read_only_attribute_names(user=nil)
495
    workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
505
    workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly' and rule != 'hidden'}.keys
496 506
  end
507
  
508
  # Same as above, but for hidden fields
509
  def hidden_attribute_names(user=nil)
510
    workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'hidden'}.keys
511
  end
497 512

  
498 513
  # Returns the names of required attributes for user or the current user
499 514
  # For users with multiple roles, the required fields are the intersection of
......
506 521
  def required_attribute_names(user=nil)
507 522
    workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
508 523
  end
524
  
525
  # Returns true if the attribute should be hidden for user
526
  def hidden_attribute?(name, user=nil)
527
    hidden_attribute_names(user).include?(name.to_s)
528
  end  
509 529

  
510 530
  # Returns true if the attribute is required for user
511 531
  def required_attribute?(name, user=nil)
......
813 833
    notified_users.collect(&:mail)
814 834
  end
815 835

  
816
  def each_notification(users, &block)
836
  def each_notification(users, &block)   
817 837
    if users.any?
818
      if custom_field_values.detect {|value| !value.custom_field.visible?}
819
        users_by_custom_field_visibility = users.group_by do |user|
820
          visible_custom_field_values(user).map(&:custom_field_id).sort
821
        end
822
        users_by_custom_field_visibility.values.each do |users|
823
          yield(users)
824
        end
825
      else
826
        yield(users)
838
      variations = users.collect {
839
        |user| (hidden_attribute_names(user) + (custom_field_values - visible_custom_field_values(user))).uniq
840
      }.uniq
841
      recipient_groups = Array.new(variations.count) { Array.new }
842
      users.each do |user|
843
        recipient_groups[variations.index(hidden_attribute_names(user))] << user
827 844
      end
828
    end
845

  
846
      recipient_groups.each do |group|
847
        yield(group)
848
      end
849
    end    
829 850
  end
830 851

  
831 852
  # Returns the number of hours spent on this issue
app/models/issue_query.rb (working copy)
248 256
  def available_columns
249 257
    return @available_columns if @available_columns
250 258
    @available_columns = self.class.available_columns.dup
259
    
260
    if project == nil
261
      hidden_fields = []
262
      all_projects.each { |prj| 
263
        if prj.visible? and User.current.roles_for_project(prj).count > 0
264
          hidden_fields = hidden_fields == [] ? prj.completely_hidden_attribute_names : hidden_fields & prj.completely_hidden_attribute_names
265
        end
266
      }
267
    else
268
      hidden_fields = project.completely_hidden_attribute_names
269
    end
270
    hidden_fields.map! {|field| field.sub(/_id$/, '')}       
271
    
251 272
    @available_columns += (project ?
252 273
                            project.all_issue_custom_fields :
253 274
                            IssueCustomField
254
                           ).visible.collect {|cf| QueryCustomFieldColumn.new(cf) }
275
                           ).visible.collect {|cf| QueryCustomFieldColumn.new(cf) }.reject{|column| hidden_fields.include?(column.custom_field.id.to_s) }
255 276

  
256 277
    if User.current.allowed_to?(:view_time_entries, project, :global => true)
257 278
      index = nil
......
274 295
    @available_columns.reject! {|column|
275 296
      disabled_fields.include?(column.name.to_s)
276 297
    }
298
    
299
    @available_columns.reject! {|column|
300
      hidden_fields.include?(column.name.to_s)
301
    }    
277 302

  
278 303
    @available_columns
279 304
  end
app/models/journal.rb (working copy)
57 57
  # Returns journal details that are visible to user
58 58
  def visible_details(user=User.current)
59 59
    details.select do |detail|
60
      if detail.property == 'cf'
61
        detail.custom_field && detail.custom_field.visible_by?(project, user)
60
      if detail.property == 'attr'
61
        !issue.hidden_attribute?(detail.prop_key, user)
62
      elsif detail.property == 'cf'
63
        detail.custom_field && detail.custom_field.visible_by?(project, user) && !issue.hidden_attribute?(detail.prop_key, user)
62 64
      elsif detail.property == 'relation'
63 65
        Issue.find_by_id(detail.value || detail.old_value).try(:visible?, user)
64 66
      else
app/models/project.rb (working copy)
151 151
  def visible?(user=User.current)
152 152
    user.allowed_to?(:view_project, self)
153 153
  end
154
  
155
  # Returns list of attributes that are hidden on all statuses of all trackers for +user+ or the current user.
156
  def completely_hidden_attribute_names(user=nil)
157
    user_real = user || User.current
158
    roles = user_real.admin ? Role.all : user_real.roles_for_project(self)
159
    return {} if roles.empty?
154 160

  
161
    result = {}
162
    workflow_permissions = WorkflowPermission.where(:tracker_id => trackers.map(&:id), :old_status_id => IssueStatus.all.map(&:id), :role_id => roles.map(&:id), :rule => 'hidden').all
163

  
164
    if workflow_permissions.any?
165
      workflow_rules = workflow_permissions.inject({}) do |h, wp|
166
        h[wp.field_name] ||= []
167
        h[wp.field_name] << wp.rule
168
        h
169
      end
170
      workflow_rules.each do |attr, rules|
171
        next if rules.size < (roles.size * trackers.size * IssueStatus.all.size)
172
        uniq_rules = rules.uniq
173
        if uniq_rules.size == 1
174
          result[attr] = uniq_rules.first
175
        else
176
          result[attr] = 'required'
177
        end
178
      end
179
    end
180

  
181
    result.keys    
182
  end  
183

  
155 184
  # Returns a SQL conditions string used to find all projects visible by the specified user.
156 185
  #
157 186
  # Examples:
app/models/query.rb (working copy)
54 54
  end
55 55

  
56 56
  def value(object)
57
    object.send name
57
    if object.respond_to?(:hidden_attribute_names)
58
      hidden_fields = object.hidden_attribute_names.map {|field| field.sub(/_id$/, '')}
59
      if hidden_fields.include?(name.to_s)
60
        ""
61
      else
62
        object.send name
63
      end
64
    else
65
      object.send name
66
    end    
58 67
  end
59 68

  
60 69
  def css_classes
......
331 340
        options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
332 341
      end
333 342
    end
343
    
344
    hidden_fields.each {|field|
345
      @available_filters.delete field
346
    }    
347
    
334 348
    @available_filters
335 349
  end
336 350

  
......
762 776
    return sql
763 777
  end
764 778

  
779
  def hidden_fields
780
    return @hidden_fields if @hidden_fields
781

  
782
    if project != nil
783
      @hidden_fields = project.completely_hidden_attribute_names
784
    else
785
      @hidden_fields = []
786
      usr = User.current;
787
      first = true
788
      all_projects.each { |prj|
789
        if prj.visible? and usr.roles_for_project(prj).count > 0
790
          if first
791
            @hidden_fields = prj.completely_hidden_attribute_names(usr)
792
          else
793
            @hidden_fields &= prj.completely_hidden_attribute_names(usr)
794
          end
795
          return @hidden_fields if @hidden_fields.empty?
796
        end
797
        first = false
798
      }
799
    end
800
    return @hidden_fields
801
  end
802

  
765 803
  # Adds a filter for the given custom field
766 804
  def add_custom_field_filter(field, assoc=nil)
767
    case field.field_format
768
    when "text"
769
      options = { :type => :text }
770
    when "list"
771
      options = { :type => :list_optional, :values => field.possible_values }
772
    when "date"
773
      options = { :type => :date }
774
    when "bool"
775
      options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]] }
776
    when "int"
777
      options = { :type => :integer }
778
    when "float"
779
      options = { :type => :float }
780
    when "user", "version"
781
      return unless project
782
      values = field.possible_values_options(project)
783
      if User.current.logged? && field.field_format == 'user'
784
        values.unshift ["<< #{l(:label_me)} >>", "me"]
805
    unless hidden_fields.include?(field.id.to_s)
806
      case field.field_format
807
      when "text"
808
        options = { :type => :text }
809
      when "list"
810
        options = { :type => :list_optional, :values => field.possible_values }
811
      when "date"
812
        options = { :type => :date }
813
      when "bool"
814
        options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]] }
815
      when "int"
816
        options = { :type => :integer }
817
      when "float"
818
        options = { :type => :float }
819
      when "user", "version"
820
        return unless project
821
        values = field.possible_values_options(project)
822
        if User.current.logged? && field.field_format == 'user'
823
          values.unshift ["<< #{l(:label_me)} >>", "me"]
824
        end
825
        options = { :type => :list_optional, :values => values }
826
      else
827
        options = { :type => :string }
785 828
      end
786
      options = { :type => :list_optional, :values => values }
787
    else
788
      options = { :type => :string }
829
      filter_id = "cf_#{field.id}"
830
      filter_name = field.name
831
      if assoc.present?
832
        filter_id = "#{assoc}.#{filter_id}"
833
        filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
834
      end
835
      add_available_filter filter_id, options.merge({
836
        :name => filter_name,
837
        :format => field.field_format,
838
        :field => field
839
      })
789 840
    end
790
    filter_id = "cf_#{field.id}"
791
    filter_name = field.name
792
    if assoc.present?
793
      filter_id = "#{assoc}.#{filter_id}"
794
      filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
795
    end
796
    add_available_filter filter_id, options.merge({
797
      :name => filter_name,
798
      :format => field.field_format,
799
      :field => field
800
    })
801 841
  end
802 842

  
803 843
  # Adds filters for the given custom fields scope
app/models/workflow_permission.rb (working copy)
16 16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17 17

  
18 18
class WorkflowPermission < WorkflowRule
19
  validates_inclusion_of :rule, :in => %w(readonly required)
19
  validates_inclusion_of :rule, :in => %w(readonly required hidden)
20 20
  validate :validate_field_name
21 21

  
22 22
  # Replaces the workflow permissions for the given tracker and role
23 23
  #
24 24
  # Example:
25
  #   WorkflowPermission.replace_permissions role, tracker, {'due_date' => {'1' => 'readonly', '2' => 'required'}}
25
  #   WorkflowPermission.replace_permissions role, tracker, {'due_date' => {'1' => 'readonly', '2' => 'required', '3' => 'hidden'}}
26 26
  def self.replace_permissions(tracker, role, permissions)
27 27
    destroy_all(:tracker_id => tracker.id, :role_id => role.id)
28 28

  
app/views/issues/_history.html.erb (working copy)
1 1
<% reply_links = authorize_for('issues', 'edit') -%>
2 2
<% for journal in journals %>
3
  <div id="change-<%= journal.id %>" class="<%= journal.css_classes %>">
4
    <div id="note-<%= journal.indice %>">
5
    <h4><%= link_to "##{journal.indice}", {:anchor => "note-#{journal.indice}"}, :class => "journal-link" %>
6
    <%= avatar(journal.user, :size => "24") %>
7
    <%= authoring journal.created_on, journal.user, :label => :label_updated_time_by %></h4>
8

  
9
    <% if journal.details.any? %>
10
    <ul class="details">
11
      <% details_to_strings(journal.visible_details).each do |string| %>
12
       <li><%= string %></li>
3
  <% if details_to_strings(journal.details).any? || journal.notes.blank? == false %>
4
    <div id="change-<%= journal.id %>" class="<%= journal.css_classes %>">
5
      <div id="note-<%= journal.indice %>">
6
      <h4><%= link_to "##{journal.indice}", {:anchor => "note-#{journal.indice}"}, :class => "journal-link" %>
7
      <%= avatar(journal.user, :size => "24") %>
8
      <%= authoring journal.created_on, journal.user, :label => :label_updated_time_by %></h4>
9
  
10
      <% if journal.details.any? %>
11
      <ul class="details">
12
        <% details_to_strings(journal.visible_details).each do |string| %>
13
         <li><%= string %></li>
14
        <% end %>
15
      </ul>
13 16
      <% end %>
14
    </ul>
15
    <% end %>
16
    <%= render_notes(issue, journal, :reply_links => reply_links) unless journal.notes.blank? %>
17
      <%= render_notes(issue, journal, :reply_links => reply_links) unless journal.notes.blank? %>
18
      </div>
17 19
    </div>
18
  </div>
19
  <%= call_hook(:view_issues_history_journal_bottom, { :journal => journal }) %>
20
    <%= call_hook(:view_issues_history_journal_bottom, { :journal => journal }) %>
21
  <% end %>
20 22
<% end %>
21 23

  
22 24
<% heads_for_wiki_formatter if User.current.allowed_to?(:edit_issue_notes, issue.project) || User.current.allowed_to?(:edit_own_issue_notes, issue.project) %>
app/views/issues/show.html.erb (working copy)
33 33

  
34 34
<table class="attributes">
35 35
<%= issue_fields_rows do |rows|
36
  rows.left l(:field_status), h(@issue.status.name), :class => 'status'
37
  rows.left l(:field_priority), h(@issue.priority.name), :class => 'priority'
36
  unless @issue.hidden_attribute?('status')
37
    rows.left l(:field_status), h(@issue.status.name), :class => 'status_id'
38
  end
39
  unless @issue.hidden_attribute?('priority_id')
40
    rows.left l(:field_priority), h(@issue.priority.name), :class => 'priority_id'
41
  end  
38 42

  
39
  unless @issue.disabled_core_fields.include?('assigned_to_id')
43
  unless @issue.disabled_core_fields.include?('assigned_to_id') || @issue.hidden_attribute?('assigned_to_id')
40 44
    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'
41 45
  end
42
  unless @issue.disabled_core_fields.include?('category_id')
46
  unless @issue.disabled_core_fields.include?('category_id') || @issue.hidden_attribute?('category_id')
43 47
    rows.left l(:field_category), h(@issue.category ? @issue.category.name : "-"), :class => 'category'
44 48
  end
45
  unless @issue.disabled_core_fields.include?('fixed_version_id')
49
  unless @issue.disabled_core_fields.include?('fixed_version_id') || @issue.hidden_attribute?('fixed_version_id')
46 50
    rows.left l(:field_fixed_version), (@issue.fixed_version ? link_to_version(@issue.fixed_version) : "-"), :class => 'fixed-version'
47 51
  end
48 52

  
49
  unless @issue.disabled_core_fields.include?('start_date')
53
  unless @issue.disabled_core_fields.include?('start_date') || @issue.hidden_attribute?('start_date')
50 54
    rows.right l(:field_start_date), format_date(@issue.start_date), :class => 'start-date'
51 55
  end
52
  unless @issue.disabled_core_fields.include?('due_date')
56
  unless @issue.disabled_core_fields.include?('due_date') || @issue.hidden_attribute?('due_date')
53 57
    rows.right l(:field_due_date), format_date(@issue.due_date), :class => 'due-date'
54 58
  end
55
  unless @issue.disabled_core_fields.include?('done_ratio')
59
  unless @issue.disabled_core_fields.include?('done_ratio') || @issue.hidden_attribute?('done_ratio')
56 60
    rows.right l(:field_done_ratio), progress_bar(@issue.done_ratio, :width => '80px', :legend => "#{@issue.done_ratio}%"), :class => 'progress'
57 61
  end
58
  unless @issue.disabled_core_fields.include?('estimated_hours')
62
  unless @issue.disabled_core_fields.include?('estimated_hours') || @issue.hidden_attribute?('estimated_hours')
59 63
    unless @issue.estimated_hours.nil?
60 64
      rows.right l(:field_estimated_hours), l_hours(@issue.estimated_hours), :class => 'estimated-hours'
61 65
    end
app/views/mailer/issue_edit.html.erb (working copy)
4 4
<%= l(:text_issue_updated, :id => "##{@issue.id}", :author => h(@journal.user)) %>
5 5

  
6 6
<ul>
7
<% details_to_strings(@journal_details, false, :only_path => false).each do |string| %>
7
<% details_to_strings(@journal_details, false, :only_path => false, :user => @auser).each do |string| %>
8 8
  <li><%= string %></li>
9 9
<% end %>
10 10
</ul>
app/views/mailer/issue_edit.text.erb (working copy)
1 1
<%= "(#{l(:field_private_notes)}) " if @journal.private_notes? -%><%= l(:text_issue_updated, :id => "##{@issue.id}", :author => @journal.user) %>
2 2

  
3
<% details_to_strings(@journal_details, true).each do |string| -%>
3
<% details_to_strings(@journal_details, true, :user => @auser).each do |string| -%>
4 4
<%= string %>
5 5
<% end -%>
6 6

  
(13-13/13)