Project

General

Profile

Feature #1767 » 0004-Make-spent-time-project-custom-fields-configurable-s.patch

Yuichi HARADA, 2021-09-08 03:44

View differences:

app/controllers/custom_fields_controller.rb
31 31
      format.html do
32 32
        @custom_fields_by_type = CustomField.all.group_by {|f| f.class.name}
33 33
        @custom_fields_projects_count =
34
          IssueCustomField.where(is_for_all: false).joins(:projects).group(:custom_field_id).count
34
          [IssueCustomField, TimeEntryCustomField, ProjectCustomField].each_with_object({}) do |klass, _|
35
            _.merge!(
36
              klass.where(is_for_all: false).joins(:projects).group(:custom_field_id).count
37
            )
38
          end
35 39
      end
36 40
      format.api do
37 41
        @custom_fields = CustomField.all
app/controllers/project_enumerations_controller.rb
22 22
  before_action :authorize
23 23

  
24 24
  def update
25
    if @project.update_or_create_time_entry_activities(update_params)
26
      flash[:notice] = l(:notice_successful_update)
25
    Project.transaction do
26
      if params[:project]
27
        @project.safe_attributes = params[:project]
28
        raise ActiveRecord::Rollback unless @project.save
29
      end
30
      if @project.update_or_create_time_entry_activities(update_params)
31
        flash[:notice] = l(:notice_successful_update)
32
      else
33
        raise ActiveRecord::Rollback
34
      end
27 35
    end
28 36

  
29 37
    redirect_to settings_project_path(@project, :tab => 'activities')
app/controllers/projects_controller.rb
95 95

  
96 96
  def new
97 97
    @issue_custom_fields = IssueCustomField.sorted.to_a
98
    @project_custom_fields = ProjectCustomField.sorted.to_a
98 99
    @trackers = Tracker.sorted.to_a
99 100
    @project = Project.new
100 101
    @project.safe_attributes = params[:project]
......
102 103

  
103 104
  def create
104 105
    @issue_custom_fields = IssueCustomField.sorted.to_a
106
    @project_custom_fields = ProjectCustomField.sorted.to_a
105 107
    @trackers = Tracker.sorted.to_a
106 108
    @project = Project.new
107 109
    @project.safe_attributes = params[:project]
......
139 141

  
140 142
  def copy
141 143
    @issue_custom_fields = IssueCustomField.sorted.to_a
144
    @project_custom_fields = ProjectCustomField.sorted.to_a
145
    @time_entry_custom_fields = TimeEntryCustomField.sorted.to_a
142 146
    @trackers = Tracker.sorted.to_a
143 147
    @source_project = Project.find(params[:id])
144 148
    if request.get?
......
198 202

  
199 203
  def settings
200 204
    @issue_custom_fields = IssueCustomField.sorted.to_a
205
    @project_custom_fields = ProjectCustomField.sorted.to_a
206
    @time_entry_custom_fields = TimeEntryCustomField.sorted.to_a
201 207
    @issue_category ||= IssueCategory.new
202 208
    @member ||= @project.members.new
203 209
    @trackers = Tracker.sorted.to_a
app/models/custom_field.rb
117 117
    end
118 118
    if self.is_a?(IssueCustomField)
119 119
      self.tracker_ids = custom_field.tracker_ids.dup
120
    end
121
    if %w(IssueCustomField TimeEntryCustomField ProjectCustomField).include?(self.class.name)
120 122
      self.project_ids = custom_field.project_ids.dup
121 123
    end
122 124
    self
app/models/project.rb
58 58
                          :class_name => 'IssueCustomField',
59 59
                          :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
60 60
                          :association_foreign_key => 'custom_field_id'
61
  has_and_belongs_to_many :project_custom_fields,
62
                          lambda {order(:position)},
63
                          :class_name => 'ProjectCustomField',
64
                          :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
65
                          :association_foreign_key => 'custom_field_id'
66
  has_and_belongs_to_many :time_entry_custom_fields,
67
                          lambda {order(:position)},
68
                          :class_name => 'TimeEntryCustomField',
69
                          :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
70
                          :association_foreign_key => 'custom_field_id'
61 71
  # Default Custom Query
62 72
  belongs_to :default_issue_query, :class_name => 'IssueQuery'
63 73

  
......
366 376
    @rolled_up_statuses = nil
367 377
    @rolled_up_custom_fields = nil
368 378
    @all_issue_custom_fields = nil
379
    @all_project_custom_fields = nil
369 380
    @all_time_entry_custom_fields = nil
370 381
    @to_param = nil
371 382
    @allowed_parents = nil
......
643 654
    end
644 655
  end
645 656

  
657
  # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
658
  def available_custom_fields
659
    all_project_custom_fields
660
  end
661

  
662
  def all_project_custom_fields
663
    @all_project_custom_fields ||=
664
      if new_record?
665
        ProjectCustomField.sorted.
666
          where("is_for_all = ? OR id IN (?)", true, project_custom_field_ids)
667
      else
668
        ProjectCustomField.sorted.
669
          where("is_for_all = ? OR id IN (SELECT DISTINCT cfp.custom_field_id" +
670
            " FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} cfp" +
671
            " WHERE cfp.project_id = ?)", true, id)
672
      end
673
  end
674

  
675
  def all_time_entry_custom_fields
676
    @all_time_entry_custom_fields ||=
677
      if new_record?
678
        TimeEntryCustomField.sorted.
679
          where("is_for_all = ? OR id IN (?)", true, time_entry_custom_field_ids)
680
      else
681
        TimeEntryCustomField.sorted.
682
          where("is_for_all = ? OR id IN (SELECT DISTINCT cfp.custom_field_id" +
683
            " FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} cfp" +
684
            " WHERE cfp.project_id = ?)", true, id)
685
      end
686
  end
687

  
646 688
  # Returns a scope of all custom fields enabled for issues of the project
647 689
  # and its subprojects
648 690
  def rolled_up_custom_fields
......
824 866
    'custom_fields',
825 867
    'tracker_ids',
826 868
    'issue_custom_field_ids',
869
    'project_custom_field_ids',
870
    'time_entry_custom_field_ids',
827 871
    'parent_id',
828 872
    'default_version_id',
829 873
    'default_issue_query_id',
......
869 913
      end
870 914
    end
871 915

  
916
    if project_custom_field_ids = attrs.delete('project_custom_field_ids')
917
      super({'project_custom_field_ids' => project_custom_field_ids}, user)
918
    end
919

  
872 920
    # Reject custom fields values not visible by the user
873 921
    if attrs['custom_field_values'].present?
874 922
      editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
......
944 992
    copy.trackers = project.trackers
945 993
    copy.custom_values = project.custom_values.collect {|v| v.clone}
946 994
    copy.issue_custom_fields = project.issue_custom_fields
995
    copy.project_custom_fields = project.project_custom_fields
996
    copy.time_entry_custom_fields = project.time_entry_custom_fields
947 997
    copy
948 998
  end
949 999

  
app/models/project_custom_field.rb
18 18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19 19

  
20 20
class ProjectCustomField < CustomField
21
  has_and_belongs_to_many :projects, :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", :foreign_key => "custom_field_id", :autosave => true
22

  
23
  safe_attributes 'project_ids'
24

  
21 25
  def type_name
22 26
    :label_project_plural
23 27
  end
......
28 32

  
29 33
  def visibility_by_project_condition(project_key=nil, user=User.current, id_column=nil)
30 34
    project_key ||= "#{Project.table_name}.id"
31
    super(project_key, user, id_column)
35
    id_column ||= id
36
    sql = super(project_key, user, id_column)
37
    project_condition = "EXISTS (SELECT 1 FROM #{CustomField.table_name} ifa WHERE ifa.is_for_all = #{self.class.connection.quoted_true} AND ifa.id = #{id_column})" +
38
      " OR #{Project.table_name}.id IN (SELECT project_id FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} WHERE custom_field_id = #{id_column})"
39

  
40
    "((#{sql}) AND (#{project_condition}))"
32 41
  end
33 42
end
app/models/time_entry.rb
228 228
    end
229 229
  end
230 230

  
231
  # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
232
  def available_custom_fields
233
    p = issue&.project || project
234
    p ? p.all_time_entry_custom_fields : super
235
  end
236

  
231 237
  def assignable_users
232 238
    users = []
233 239
    if project
app/models/time_entry_custom_field.rb
18 18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19 19

  
20 20
class TimeEntryCustomField < CustomField
21
  has_and_belongs_to_many :projects, :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", :foreign_key => "custom_field_id", :autosave => true
22

  
23
  safe_attributes 'project_ids'
24

  
21 25
  def type_name
22 26
    :label_spent_time
23 27
  end
......
26 30
    super || (roles & user.roles_for_project(project)).present?
27 31
  end
28 32

  
33
  def visibility_by_project_condition(project_key=nil, user=User.current, id_column=nil)
34
    id_column ||= id
35
    sql = super(project_key, user, id_column)
36
    project_condition = "EXISTS (SELECT 1 FROM #{CustomField.table_name} ifa WHERE ifa.is_for_all = #{self.class.connection.quoted_true} AND ifa.id = #{id_column})" +
37
      " OR #{TimeEntry.table_name}.project_id IN (SELECT project_id FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} WHERE custom_field_id = #{id_column})"
38

  
39
    "((#{sql}) AND (#{project_condition}))"
40
  end
41

  
29 42
  def validate_custom_field
30 43
    super
31 44
    errors.add(:base, l(:label_role_plural) + ' ' + l('activerecord.errors.messages.blank')) unless visible? || roles.present?
app/views/custom_fields/_form.html.erb
59 59

  
60 60
  <% if @custom_field.is_a?(IssueCustomField) %>
61 61
    <%= render :partial => 'visibility_by_tracker_selector', :locals => { :f => f } %>
62
  <% end %>
62 63

  
64
  <% if %w(IssueCustomField TimeEntryCustomField ProjectCustomField).include?(@custom_field.class.name) %>
63 65
    <%= render :partial => 'visibility_by_project_selector', :locals => { :f => f } %>
64 66
  <% end %>
65 67
</div>
app/views/custom_fields/_index.html.erb
3 3
    <th><%=l(:field_name)%></th>
4 4
    <th><%=l(:field_field_format)%></th>
5 5
    <th><%=l(:field_is_required)%></th>
6
    <% if tab[:name] == 'IssueCustomField' %>
6
    <% if %w(IssueCustomField TimeEntryCustomField ProjectCustomField).include?(tab[:name]) %>
7 7
      <th><%=l(:field_is_for_all)%></th>
8 8
      <th><%=l(:label_used_by)%></th>
9 9
    <% end %>
......
16 16
      <td class="name"><%= link_to custom_field.name, edit_custom_field_path(custom_field) %></td>
17 17
      <td><%= l(custom_field.format.label) %></td>
18 18
      <td><%= checked_image custom_field.is_required? %></td>
19
      <% if tab[:name] == 'IssueCustomField' %>
19
      <% if %w(IssueCustomField TimeEntryCustomField ProjectCustomField).include?(tab[:name]) %>
20 20
      <td><%= checked_image custom_field.is_for_all? %></td>
21
      <td><%= l(:label_x_projects, :count => @custom_fields_projects_count[custom_field.id] || 0) if custom_field.is_a? IssueCustomField and !custom_field.is_for_all? %></td>
21
      <td><%= l(:label_x_projects, :count => @custom_fields_projects_count[custom_field.id] || 0) if [IssueCustomField, TimeEntryCustomField, ProjectCustomField].include?(custom_field.class) and !custom_field.is_for_all? %></td>
22 22
      <% end %>
23 23
      <td class="buttons">
24 24
        <%= reorder_handle(custom_field, :url => custom_field_path(custom_field), :param => 'custom_field') %>
app/views/projects/_form.html.erb
31 31
<%= call_hook(:view_projects_form, :project => @project, :form => f) %>
32 32
</div>
33 33

  
34
<% unless @project_custom_fields.empty? %>
35
<fieldset class="box tabular" id="project_project_custom_fields"><legend><%= toggle_checkboxes_link('#project_project_custom_fields input[type=checkbox]:enabled') %><%=l(:label_custom_field_plural)%></legend>
36
  <% if User.current.admin? %>
37
  <div class="contextual"><%= link_to l(:label_administration), custom_fields_path(tab: ProjectCustomField.name), class: "icon icon-settings" %></div>
38
  <% end %>
39
  <% all_project_custom_fields = @project.all_project_custom_fields %>
40
  <% @project_custom_fields.each do |custom_field| %>
41
  <label class="floating">
42
    <%= check_box_tag 'project[project_custom_field_ids][]', custom_field.id, all_project_custom_fields.include?(custom_field),
43
        disabled: (custom_field.is_for_all? ? "disabled" : nil),
44
        id: nil %>
45
    <%= custom_field_name_tag(custom_field) %>
46
  </label>
47
  <% end %>
48
<%= hidden_field_tag 'project[project_custom_field_ids][]', '', id: nil %>
49
</fieldset>
50
<%= javascript_tag do %>
51
  $(document).ready(function(){
52
    var custom_field_toggle_disabled = function(custom_field_id){
53
      var custom_field_value = $('#project_custom_field_values_' + $(custom_field_id).attr('value'));
54
      custom_field_value.prop('disabled', !$(custom_field_id).prop('checked'));
55
    };
56
    $('#project_project_custom_fields input[type=checkbox]').change(function(){
57
      custom_field_toggle_disabled(this);
58
    });
59
    $('#project_project_custom_fields input[type=checkbox]').each(function(){
60
      custom_field_toggle_disabled(this);
61
    });
62
  });
63
<% end %>
64
<% end %>
65

  
34 66
<% if @project.safe_attribute?('enabled_module_names') %>
35 67
<fieldset class="box tabular" id="project_modules"><legend><%= toggle_checkboxes_link('#project_modules input[type="checkbox"]') %><%= l(:label_module_plural) %></legend>
36 68
<% Redmine::AccessControl.available_project_modules.each do |m| %>
app/views/projects/copy.html.erb
26 26
  <%= hidden_field_tag 'project[issue_custom_field_ids][]', issue_custom_field_id %>
27 27
<% end %>
28 28

  
29
<% @project.time_entry_custom_field_ids.each do |time_entry_custom_field_id| %>
30
  <%= hidden_field_tag 'project[time_entry_custom_field_ids][]', time_entry_custom_field_id %>
31
<% end %>
32

  
29 33
<%= submit_tag l(:button_copy) %>
30 34
<% end %>
app/views/projects/settings/_activities.html.erb
41 41
  <% end %>
42 42
</table>
43 43

  
44
<% unless @time_entry_custom_fields.empty? %>
45
<fieldset class="box tabular" id="project_time_entry_custom_fields"><legend><%= toggle_checkboxes_link('#project_time_entry_custom_fields input[type=checkbox]:enabled') %><%=l(:label_custom_field_plural)%></legend>
46
<% if User.current.admin? %>
47
  <div class="contextual"><%= link_to l(:label_administration), custom_fields_path(tab: TimeEntryCustomField.name), class: "icon icon-settings" %></div>
48
<% end %>
49
<% all_time_entry_custom_fields = @project.all_time_entry_custom_fields %>
50
<% @time_entry_custom_fields.each do |custom_field| %>
51
  <label class="floating">
52
    <%= check_box_tag 'project[time_entry_custom_field_ids][]', custom_field.id, all_time_entry_custom_fields.include?(custom_field),
53
        disabled: (custom_field.is_for_all? ? "disabled" : nil),
54
        id: nil %>
55
    <%= custom_field_name_tag(custom_field) %>
56
  </label>
57
<% end %>
58
<%= hidden_field_tag 'project[time_entry_custom_field_ids][]', '', id: nil %>
59
</fieldset>
60
<% end %>
61

  
44 62
<%= submit_tag l(:button_save) %>
45 63
<% end %>
(4-4/11)