From 673c83926defc85029df4b5e0c1180ad41cdf776 Mon Sep 17 00:00:00 2001 From: Yuichi HARADA Date: Tue, 17 Aug 2021 09:38:47 +0900 Subject: [PATCH 4/5] Make spent time & project custom fields configurable/switchable per project --- app/controllers/custom_fields_controller.rb | 6 ++- .../project_enumerations_controller.rb | 12 ++++- app/controllers/projects_controller.rb | 6 +++ app/models/custom_field.rb | 2 + app/models/project.rb | 50 +++++++++++++++++++ app/models/project_custom_field.rb | 11 +++- app/models/time_entry.rb | 6 +++ app/models/time_entry_custom_field.rb | 13 +++++ app/views/custom_fields/_form.html.erb | 2 + app/views/custom_fields/_index.html.erb | 6 +-- app/views/projects/_form.html.erb | 32 ++++++++++++ app/views/projects/copy.html.erb | 4 ++ .../projects/settings/_activities.html.erb | 18 +++++++ 13 files changed, 161 insertions(+), 7 deletions(-) diff --git a/app/controllers/custom_fields_controller.rb b/app/controllers/custom_fields_controller.rb index c65109e119..df23dc3337 100644 --- a/app/controllers/custom_fields_controller.rb +++ b/app/controllers/custom_fields_controller.rb @@ -31,7 +31,11 @@ class CustomFieldsController < ApplicationController format.html do @custom_fields_by_type = CustomField.all.group_by {|f| f.class.name} @custom_fields_projects_count = - IssueCustomField.where(is_for_all: false).joins(:projects).group(:custom_field_id).count + [IssueCustomField, TimeEntryCustomField, ProjectCustomField].each_with_object({}) do |klass, _| + _.merge!( + klass.where(is_for_all: false).joins(:projects).group(:custom_field_id).count + ) + end end format.api do @custom_fields = CustomField.all diff --git a/app/controllers/project_enumerations_controller.rb b/app/controllers/project_enumerations_controller.rb index 53dbd95b61..de4ab012d3 100644 --- a/app/controllers/project_enumerations_controller.rb +++ b/app/controllers/project_enumerations_controller.rb @@ -22,8 +22,16 @@ class ProjectEnumerationsController < ApplicationController before_action :authorize def update - if @project.update_or_create_time_entry_activities(update_params) - flash[:notice] = l(:notice_successful_update) + Project.transaction do + if params[:project] + @project.safe_attributes = params[:project] + raise ActiveRecord::Rollback unless @project.save + end + if @project.update_or_create_time_entry_activities(update_params) + flash[:notice] = l(:notice_successful_update) + else + raise ActiveRecord::Rollback + end end redirect_to settings_project_path(@project, :tab => 'activities') diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 445ff840f3..0cc6263f56 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -95,6 +95,7 @@ class ProjectsController < ApplicationController def new @issue_custom_fields = IssueCustomField.sorted.to_a + @project_custom_fields = ProjectCustomField.sorted.to_a @trackers = Tracker.sorted.to_a @project = Project.new @project.safe_attributes = params[:project] @@ -102,6 +103,7 @@ class ProjectsController < ApplicationController def create @issue_custom_fields = IssueCustomField.sorted.to_a + @project_custom_fields = ProjectCustomField.sorted.to_a @trackers = Tracker.sorted.to_a @project = Project.new @project.safe_attributes = params[:project] @@ -139,6 +141,8 @@ class ProjectsController < ApplicationController def copy @issue_custom_fields = IssueCustomField.sorted.to_a + @project_custom_fields = ProjectCustomField.sorted.to_a + @time_entry_custom_fields = TimeEntryCustomField.sorted.to_a @trackers = Tracker.sorted.to_a @source_project = Project.find(params[:id]) if request.get? @@ -198,6 +202,8 @@ class ProjectsController < ApplicationController def settings @issue_custom_fields = IssueCustomField.sorted.to_a + @project_custom_fields = ProjectCustomField.sorted.to_a + @time_entry_custom_fields = TimeEntryCustomField.sorted.to_a @issue_category ||= IssueCategory.new @member ||= @project.members.new @trackers = Tracker.sorted.to_a diff --git a/app/models/custom_field.rb b/app/models/custom_field.rb index 9787b2ee4d..5d57fa9cb1 100644 --- a/app/models/custom_field.rb +++ b/app/models/custom_field.rb @@ -117,6 +117,8 @@ class CustomField < ActiveRecord::Base end if self.is_a?(IssueCustomField) self.tracker_ids = custom_field.tracker_ids.dup + end + if %w(IssueCustomField TimeEntryCustomField ProjectCustomField).include?(self.class.name) self.project_ids = custom_field.project_ids.dup end self diff --git a/app/models/project.rb b/app/models/project.rb index 4d5e3c0513..065253eb43 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -58,6 +58,16 @@ class Project < ActiveRecord::Base :class_name => 'IssueCustomField', :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", :association_foreign_key => 'custom_field_id' + has_and_belongs_to_many :project_custom_fields, + lambda {order(:position)}, + :class_name => 'ProjectCustomField', + :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", + :association_foreign_key => 'custom_field_id' + has_and_belongs_to_many :time_entry_custom_fields, + lambda {order(:position)}, + :class_name => 'TimeEntryCustomField', + :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", + :association_foreign_key => 'custom_field_id' # Default Custom Query belongs_to :default_issue_query, :class_name => 'IssueQuery' @@ -366,6 +376,7 @@ class Project < ActiveRecord::Base @rolled_up_statuses = nil @rolled_up_custom_fields = nil @all_issue_custom_fields = nil + @all_project_custom_fields = nil @all_time_entry_custom_fields = nil @to_param = nil @allowed_parents = nil @@ -643,6 +654,37 @@ class Project < ActiveRecord::Base end end + # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields + def available_custom_fields + all_project_custom_fields + end + + def all_project_custom_fields + @all_project_custom_fields ||= + if new_record? + ProjectCustomField.sorted. + where("is_for_all = ? OR id IN (?)", true, project_custom_field_ids) + else + ProjectCustomField.sorted. + where("is_for_all = ? OR id IN (SELECT DISTINCT cfp.custom_field_id" + + " FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} cfp" + + " WHERE cfp.project_id = ?)", true, id) + end + end + + def all_time_entry_custom_fields + @all_time_entry_custom_fields ||= + if new_record? + TimeEntryCustomField.sorted. + where("is_for_all = ? OR id IN (?)", true, time_entry_custom_field_ids) + else + TimeEntryCustomField.sorted. + where("is_for_all = ? OR id IN (SELECT DISTINCT cfp.custom_field_id" + + " FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} cfp" + + " WHERE cfp.project_id = ?)", true, id) + end + end + # Returns a scope of all custom fields enabled for issues of the project # and its subprojects def rolled_up_custom_fields @@ -824,6 +866,8 @@ class Project < ActiveRecord::Base 'custom_fields', 'tracker_ids', 'issue_custom_field_ids', + 'project_custom_field_ids', + 'time_entry_custom_field_ids', 'parent_id', 'default_version_id', 'default_issue_query_id', @@ -869,6 +913,10 @@ class Project < ActiveRecord::Base end end + if project_custom_field_ids = attrs.delete('project_custom_field_ids') + super({'project_custom_field_ids' => project_custom_field_ids}, user) + end + # Reject custom fields values not visible by the user if attrs['custom_field_values'].present? editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s} @@ -944,6 +992,8 @@ class Project < ActiveRecord::Base copy.trackers = project.trackers copy.custom_values = project.custom_values.collect {|v| v.clone} copy.issue_custom_fields = project.issue_custom_fields + copy.project_custom_fields = project.project_custom_fields + copy.time_entry_custom_fields = project.time_entry_custom_fields copy end diff --git a/app/models/project_custom_field.rb b/app/models/project_custom_field.rb index aebce3ef4a..231b23b31d 100644 --- a/app/models/project_custom_field.rb +++ b/app/models/project_custom_field.rb @@ -18,6 +18,10 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class ProjectCustomField < CustomField + has_and_belongs_to_many :projects, :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", :foreign_key => "custom_field_id", :autosave => true + + safe_attributes 'project_ids' + def type_name :label_project_plural end @@ -28,6 +32,11 @@ class ProjectCustomField < CustomField def visibility_by_project_condition(project_key=nil, user=User.current, id_column=nil) project_key ||= "#{Project.table_name}.id" - super(project_key, user, id_column) + id_column ||= id + sql = super(project_key, user, id_column) + 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})" + + " 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})" + + "((#{sql}) AND (#{project_condition}))" end end diff --git a/app/models/time_entry.rb b/app/models/time_entry.rb index b5926c6129..864275cc38 100644 --- a/app/models/time_entry.rb +++ b/app/models/time_entry.rb @@ -228,6 +228,12 @@ class TimeEntry < ActiveRecord::Base end end + # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields + def available_custom_fields + p = issue&.project || project + p ? p.all_time_entry_custom_fields : super + end + def assignable_users users = [] if project diff --git a/app/models/time_entry_custom_field.rb b/app/models/time_entry_custom_field.rb index aa22944d77..5a1bfd79d3 100644 --- a/app/models/time_entry_custom_field.rb +++ b/app/models/time_entry_custom_field.rb @@ -18,6 +18,10 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class TimeEntryCustomField < CustomField + has_and_belongs_to_many :projects, :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", :foreign_key => "custom_field_id", :autosave => true + + safe_attributes 'project_ids' + def type_name :label_spent_time end @@ -26,6 +30,15 @@ class TimeEntryCustomField < CustomField super || (roles & user.roles_for_project(project)).present? end + def visibility_by_project_condition(project_key=nil, user=User.current, id_column=nil) + id_column ||= id + sql = super(project_key, user, id_column) + 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})" + + " 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})" + + "((#{sql}) AND (#{project_condition}))" + end + def validate_custom_field super errors.add(:base, l(:label_role_plural) + ' ' + l('activerecord.errors.messages.blank')) unless visible? || roles.present? diff --git a/app/views/custom_fields/_form.html.erb b/app/views/custom_fields/_form.html.erb index a7af54a070..2c22132058 100644 --- a/app/views/custom_fields/_form.html.erb +++ b/app/views/custom_fields/_form.html.erb @@ -59,7 +59,9 @@ <% if @custom_field.is_a?(IssueCustomField) %> <%= render :partial => 'visibility_by_tracker_selector', :locals => { :f => f } %> + <% end %> + <% if %w(IssueCustomField TimeEntryCustomField ProjectCustomField).include?(@custom_field.class.name) %> <%= render :partial => 'visibility_by_project_selector', :locals => { :f => f } %> <% end %> diff --git a/app/views/custom_fields/_index.html.erb b/app/views/custom_fields/_index.html.erb index 81fe214043..0354e30286 100644 --- a/app/views/custom_fields/_index.html.erb +++ b/app/views/custom_fields/_index.html.erb @@ -3,7 +3,7 @@ <%=l(:field_name)%> <%=l(:field_field_format)%> <%=l(:field_is_required)%> - <% if tab[:name] == 'IssueCustomField' %> + <% if %w(IssueCustomField TimeEntryCustomField ProjectCustomField).include?(tab[:name]) %> <%=l(:field_is_for_all)%> <%=l(:label_used_by)%> <% end %> @@ -16,9 +16,9 @@ <%= link_to custom_field.name, edit_custom_field_path(custom_field) %> <%= l(custom_field.format.label) %> <%= checked_image custom_field.is_required? %> - <% if tab[:name] == 'IssueCustomField' %> + <% if %w(IssueCustomField TimeEntryCustomField ProjectCustomField).include?(tab[:name]) %> <%= checked_image custom_field.is_for_all? %> - <%= 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? %> + <%= 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? %> <% end %> <%= reorder_handle(custom_field, :url => custom_field_path(custom_field), :param => 'custom_field') %> diff --git a/app/views/projects/_form.html.erb b/app/views/projects/_form.html.erb index 7c988fb0e2..3956a50572 100644 --- a/app/views/projects/_form.html.erb +++ b/app/views/projects/_form.html.erb @@ -31,6 +31,38 @@ <%= call_hook(:view_projects_form, :project => @project, :form => f) %> +<% unless @project_custom_fields.empty? %> +
<%= toggle_checkboxes_link('#project_project_custom_fields input[type=checkbox]:enabled') %><%=l(:label_custom_field_plural)%> + <% if User.current.admin? %> +
<%= link_to l(:label_administration), custom_fields_path(tab: ProjectCustomField.name), class: "icon icon-settings" %>
+ <% end %> + <% all_project_custom_fields = @project.all_project_custom_fields %> + <% @project_custom_fields.each do |custom_field| %> + + <% end %> +<%= hidden_field_tag 'project[project_custom_field_ids][]', '', id: nil %> +
+<%= javascript_tag do %> + $(document).ready(function(){ + var custom_field_toggle_disabled = function(custom_field_id){ + var custom_field_value = $('#project_custom_field_values_' + $(custom_field_id).attr('value')); + custom_field_value.prop('disabled', !$(custom_field_id).prop('checked')); + }; + $('#project_project_custom_fields input[type=checkbox]').change(function(){ + custom_field_toggle_disabled(this); + }); + $('#project_project_custom_fields input[type=checkbox]').each(function(){ + custom_field_toggle_disabled(this); + }); + }); +<% end %> +<% end %> + <% if @project.safe_attribute?('enabled_module_names') %>
<%= toggle_checkboxes_link('#project_modules input[type="checkbox"]') %><%= l(:label_module_plural) %> <% Redmine::AccessControl.available_project_modules.each do |m| %> diff --git a/app/views/projects/copy.html.erb b/app/views/projects/copy.html.erb index 8a7805ef09..61dd4fd212 100644 --- a/app/views/projects/copy.html.erb +++ b/app/views/projects/copy.html.erb @@ -26,5 +26,9 @@ <%= hidden_field_tag 'project[issue_custom_field_ids][]', issue_custom_field_id %> <% end %> +<% @project.time_entry_custom_field_ids.each do |time_entry_custom_field_id| %> + <%= hidden_field_tag 'project[time_entry_custom_field_ids][]', time_entry_custom_field_id %> +<% end %> + <%= submit_tag l(:button_copy) %> <% end %> diff --git a/app/views/projects/settings/_activities.html.erb b/app/views/projects/settings/_activities.html.erb index dce68df2c6..bdcbdb24db 100644 --- a/app/views/projects/settings/_activities.html.erb +++ b/app/views/projects/settings/_activities.html.erb @@ -41,5 +41,23 @@ <% end %> +<% unless @time_entry_custom_fields.empty? %> +
<%= toggle_checkboxes_link('#project_time_entry_custom_fields input[type=checkbox]:enabled') %><%=l(:label_custom_field_plural)%> +<% if User.current.admin? %> +
<%= link_to l(:label_administration), custom_fields_path(tab: TimeEntryCustomField.name), class: "icon icon-settings" %>
+<% end %> +<% all_time_entry_custom_fields = @project.all_time_entry_custom_fields %> +<% @time_entry_custom_fields.each do |custom_field| %> + +<% end %> +<%= hidden_field_tag 'project[time_entry_custom_field_ids][]', '', id: nil %> +
+<% end %> + <%= submit_tag l(:button_save) %> <% end %> -- 2.33.0