From 77180ea0e7e68b6b6af2a04fbdf50a07e21d74c1 Mon Sep 17 00:00:00 2001 From: Burak Ikiler Date: Fri, 19 May 2023 20:02:59 +0300 Subject: [PATCH 1/3] Redmine 5 Compatibility: Autoloder fix and Rails 6 updates. --- .../concerns/issue_templates_common.rb | 210 +++++++++--------- .../concerns/project_templates_common.rb | 106 +++++---- .../global_issue_templates_controller.rb | 2 +- app/controllers/issue_templates_controller.rb | 4 +- app/controllers/note_templates_controller.rb | 2 +- app/models/concerns/issue_template/common.rb | 180 ++++++++------- app/models/global_issue_template.rb | 2 +- app/models/issue_template.rb | 2 +- init.rb | 6 +- 9 files changed, 254 insertions(+), 260 deletions(-) diff --git a/app/controllers/concerns/issue_templates_common.rb b/app/controllers/concerns/issue_templates_common.rb index e7a8fba..0182ddd 100644 --- a/app/controllers/concerns/issue_templates_common.rb +++ b/app/controllers/concerns/issue_templates_common.rb @@ -1,142 +1,140 @@ # frozen_string_literal: true -module Concerns - module IssueTemplatesCommon - extend ActiveSupport::Concern +module IssueTemplatesCommon + extend ActiveSupport::Concern - class InvalidTemplateFormatError < StandardError; end + class InvalidTemplateFormatError < StandardError; end - included do - before_action :log_action, only: [:destroy] + included do + before_action :log_action, only: [:destroy] - # logging action - def log_action - logger&.info "[#{self.class}] #{action_name} called by #{User.current.name}" - end - - def plugin_setting - Setting.plugin_redmine_issue_templates - end - - def apply_all_projects? - plugin_setting['apply_global_template_to_all_projects'].to_s == 'true' - end - - def apply_template_when_edit_issue? - plugin_setting['apply_template_when_edit_issue'].to_s == 'true' - end - - def builtin_fields_enabled? - plugin_setting['enable_builtin_fields'].to_s == 'true' - end + # logging action + def log_action + logger&.info "[#{self.class}] #{action_name} called by #{User.current.name}" end - def load_selectable_fields - tracker_id = params[:tracker_id] - project_id = params[:project_id] - render plain: {} && return if tracker_id.blank? - - custom_fields = core_fields_map_by_tracker_id(tracker_id: tracker_id, project_id: project_id).merge(custom_fields_map_by_tracker_id(tracker_id)) - render plain: { custom_fields: custom_fields }.to_json - end - - def orphaned_templates - render partial: 'common/orphaned', locals: { orphaned_templates: orphaned } + def plugin_setting + Setting.plugin_redmine_issue_templates end def apply_all_projects? plugin_setting['apply_global_template_to_all_projects'].to_s == 'true' end - def checklists - template_params[:checklists].presence || [] + def apply_template_when_edit_issue? + plugin_setting['apply_template_when_edit_issue'].to_s == 'true' end - def builtin_fields_json - value = template_params[:builtin_fields].blank? ? {} : JSON.parse(template_params[:builtin_fields]) - return value if value.is_a?(Hash) - - raise InvalidTemplateFormatError + def builtin_fields_enabled? + plugin_setting['enable_builtin_fields'].to_s == 'true' end + end - def checklist_enabled? - Redmine::Plugin.registered_plugins.key? :redmine_checklists - rescue StandardError - false - end + def load_selectable_fields + tracker_id = params[:tracker_id] + project_id = params[:project_id] + render plain: {} && return if tracker_id.blank? - def valid_params - # convert attribute name and data for checklist plugin supporting - attributes = template_params.except(:checklists, :builtin_fields) - attributes[:builtin_fields_json] = builtin_fields_json if builtin_fields_enabled? - attributes[:checklist_json] = checklists.to_json if checklist_enabled? - attributes - end + custom_fields = core_fields_map_by_tracker_id(tracker_id: tracker_id, project_id: project_id).merge(custom_fields_map_by_tracker_id(tracker_id)) + render plain: { custom_fields: custom_fields }.to_json + end - def destroy - raise NotImplementedError, "You must implement #{self.class}##{__method__}" - end + def orphaned_templates + render partial: 'common/orphaned', locals: { orphaned_templates: orphaned } + end - # - # TODO: Code should be refactored - # - def core_fields_map_by_tracker_id(tracker_id: nil, project_id: nil) - return {} unless builtin_fields_enabled? + def apply_all_projects? + plugin_setting['apply_global_template_to_all_projects'].to_s == 'true' + end - fields = %w[status_id priority_id] + def checklists + template_params[:checklists].presence || [] + end - # exclude "description" - tracker = Tracker.find_by(id: tracker_id) - fields += tracker.core_fields.reject { |field| field == 'description' } if tracker.present? - fields.reject! { |field| %w[category_id fixed_version_id].include?(field) } if project_id.blank? + def builtin_fields_json + value = template_params[:builtin_fields].blank? ? {} : JSON.parse(template_params[:builtin_fields]) + return value if value.is_a?(Hash) - map = {} + raise InvalidTemplateFormatError + end - fields.each do |field| - id = "issue_#{field}" - name = I18n.t('field_' + field.gsub(/_id$/, '')) - value = { name: name, core_field_id: id } - if field == 'priority_id' - value[:possible_values] = IssuePriority.active.pluck(:name) - value[:field_format] = 'list' - end + def checklist_enabled? + Redmine::Plugin.registered_plugins.key? :redmine_checklists + rescue StandardError + false + end + + def valid_params + # convert attribute name and data for checklist plugin supporting + attributes = template_params.except(:checklists, :builtin_fields) + attributes[:builtin_fields_json] = builtin_fields_json if builtin_fields_enabled? + attributes[:checklist_json] = checklists.to_json if checklist_enabled? + attributes + end + + def destroy + raise NotImplementedError, "You must implement #{self.class}##{__method__}" + end - if field == 'status_id' && tracker.present? - value[:possible_values] = tracker.issue_statuses.pluck(:name) - value[:field_format] = 'list' - end + # + # TODO: Code should be refactored + # + def core_fields_map_by_tracker_id(tracker_id: nil, project_id: nil) + return {} unless builtin_fields_enabled? - value[:field_format] = 'date' if %(start_date due_date).include?(field) + fields = %w[status_id priority_id] - value[:field_format] = 'ratio' if field == 'done_ratio' + # exclude "description" + tracker = Tracker.find_by(id: tracker_id) + fields += tracker.core_fields.reject { |field| field == 'description' } if tracker.present? + fields.reject! { |field| %w[category_id fixed_version_id].include?(field) } if project_id.blank? - map[id] = value + map = {} + + fields.each do |field| + id = "issue_#{field}" + name = I18n.t('field_' + field.gsub(/_id$/, '')) + value = { name: name, core_field_id: id } + if field == 'priority_id' + value[:possible_values] = IssuePriority.active.pluck(:name) + value[:field_format] = 'list' + end + + if field == 'status_id' && tracker.present? + value[:possible_values] = tracker.issue_statuses.pluck(:name) + value[:field_format] = 'list' end - map - rescue StandardError => e - logger&.info "core_fields_map_by_tracker_id failed due to this error: #{e.message}" - {} + + value[:field_format] = 'date' if %(start_date due_date).include?(field) + + value[:field_format] = 'ratio' if field == 'done_ratio' + + map[id] = value end + map + rescue StandardError => e + logger&.info "core_fields_map_by_tracker_id failed due to this error: #{e.message}" + {} + end - def custom_fields_map_by_tracker_id(tracker_id = nil) - return {} unless builtin_fields_enabled? - return {} if tracker_id.blank? + def custom_fields_map_by_tracker_id(tracker_id = nil) + return {} unless builtin_fields_enabled? + return {} if tracker_id.blank? - tracker = Tracker.find_by(id: tracker_id) - ids = tracker&.custom_field_ids || [] - fields = IssueCustomField.where(id: ids) - map = {} - fields.each do |field| - id = "issue_custom_field_values_#{field.id}" - attributes = field.attributes + tracker = Tracker.find_by(id: tracker_id) + ids = tracker&.custom_field_ids || [] + fields = IssueCustomField.where(id: ids) + map = {} + fields.each do |field| + id = "issue_custom_field_values_#{field.id}" + attributes = field.attributes - attributes = attributes.merge(possible_values: field.possible_values_options.map { |value| value[0] }) if field.format.name == 'bool' - map[id] = attributes - end - map - rescue StandardError => e - logger&.info "core_fields_map_by_tracker_id failed due to this error: #{e.message}" - {} + attributes = attributes.merge(possible_values: field.possible_values_options.map { |value| value[0] }) if field.format.name == 'bool' + map[id] = attributes end + map + rescue StandardError => e + logger&.info "core_fields_map_by_tracker_id failed due to this error: #{e.message}" + {} end end diff --git a/app/controllers/concerns/project_templates_common.rb b/app/controllers/concerns/project_templates_common.rb index 40800c3..e9e0547 100644 --- a/app/controllers/concerns/project_templates_common.rb +++ b/app/controllers/concerns/project_templates_common.rb @@ -1,74 +1,72 @@ # frozen_string_literal: true -module Concerns - module ProjectTemplatesCommon - extend ActiveSupport::Concern - included do - before_action :find_user, :find_project, :authorize, except: %i[preview load load_selectable_fields] - before_action :find_object, only: %i[show edit update destroy] - accept_api_auth :index, :list_templates, :load - end - - def show - render render_form_params - end +module ProjectTemplatesCommon + extend ActiveSupport::Concern + included do + before_action :find_user, :find_project, :authorize, except: %i[preview load load_selectable_fields] + before_action :find_object, only: %i[show edit update destroy] + accept_api_auth :index, :list_templates, :load + end - def destroy - unless template.destroy - flash[:error] = l(:enabled_template_cannot_destroy) - redirect_to action: :show, project_id: @project, id: template - return - end + def show + render render_form_params + end - flash[:notice] = l(:notice_successful_delete) - redirect_to action: 'index', project_id: @project + def destroy + unless template.destroy + flash[:error] = l(:enabled_template_cannot_destroy) + redirect_to action: :show, project_id: @project, id: template + return end - def save_and_flash(message, action_on_failure) - unless template.save - render render_form_params.merge(action: action_on_failure) - return - end + flash[:notice] = l(:notice_successful_delete) + redirect_to action: 'index', project_id: @project + end - respond_to do |format| - format.html do - flash[:notice] = l(message) - redirect_to action: 'show', id: template.id, project_id: @project - end - format.js { head 200 } - end - rescue NoteTemplate::NoteTemplateError => e - flash[:error] = e.message + def save_and_flash(message, action_on_failure) + unless template.save render render_form_params.merge(action: action_on_failure) nil end - def plugin_setting - Setting.plugin_redmine_issue_templates + respond_to do |format| + format.html do + flash[:notice] = l(message) + redirect_to action: 'show', id: template.id, project_id: @project + end + format.js { head 200 } end + rescue NoteTemplate::NoteTemplateError => e + flash[:error] = e.message + render render_form_params.merge(action: action_on_failure) + nil + end - def apply_all_projects? - plugin_setting['apply_global_template_to_all_projects'].to_s == 'true' - end + def plugin_setting + Setting.plugin_redmine_issue_templates + end - private + def apply_all_projects? + plugin_setting['apply_global_template_to_all_projects'].to_s == 'true' + end - def template - raise NotImplementedError, "You must implement #{self.class}##{__method__}" - end + private - def find_user - @user = User.current - end + def template + raise NotImplementedError, "You must implement #{self.class}##{__method__}" + end - def find_tracker - @tracker = Tracker.find(params[:issue_tracker_id]) - end + def find_user + @user = User.current + end - def find_project - @project = Project.find(params[:project_id]) - rescue ActiveRecord::RecordNotFound - render_404 - end + def find_tracker + @tracker = Tracker.find(params[:issue_tracker_id]) + end + + def find_project + @project = Project.find(params[:project_id]) + rescue ActiveRecord::RecordNotFound + render_404 end end diff --git a/app/controllers/global_issue_templates_controller.rb b/app/controllers/global_issue_templates_controller.rb index 68c39c3..0f4c79d 100644 --- a/app/controllers/global_issue_templates_controller.rb +++ b/app/controllers/global_issue_templates_controller.rb @@ -5,7 +5,7 @@ class GlobalIssueTemplatesController < ApplicationController layout 'base' helper :issues include IssueTemplatesHelper - include Concerns::IssueTemplatesCommon + include IssueTemplatesCommon menu_item :issues before_action :find_object, only: %i[show edit update destroy] before_action :find_project, only: %i[edit update] diff --git a/app/controllers/issue_templates_controller.rb b/app/controllers/issue_templates_controller.rb index f1e5832..eb0f5d0 100644 --- a/app/controllers/issue_templates_controller.rb +++ b/app/controllers/issue_templates_controller.rb @@ -4,8 +4,8 @@ class IssueTemplatesController < ApplicationController layout 'base' helper :issues - include Concerns::IssueTemplatesCommon - include Concerns::ProjectTemplatesCommon + include IssueTemplatesCommon + include ProjectTemplatesCommon menu_item :issues before_action :find_tracker, :find_templates, only: %i[set_pulldown list_templates] diff --git a/app/controllers/note_templates_controller.rb b/app/controllers/note_templates_controller.rb index 66222eb..c17783a 100644 --- a/app/controllers/note_templates_controller.rb +++ b/app/controllers/note_templates_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class NoteTemplatesController < ApplicationController - include Concerns::ProjectTemplatesCommon + include ProjectTemplatesCommon layout 'base' helper :issue_templates menu_item :issues diff --git a/app/models/concerns/issue_template/common.rb b/app/models/concerns/issue_template/common.rb index efee22f..8fb55b3 100644 --- a/app/models/concerns/issue_template/common.rb +++ b/app/models/concerns/issue_template/common.rb @@ -1,110 +1,106 @@ # frozen_string_literal: true -module Concerns - module IssueTemplate - module Common - extend ActiveSupport::Concern - - # - # Common scope both global and project scope template. - # - included do - belongs_to :author, class_name: 'User', foreign_key: 'author_id' - belongs_to :tracker - before_save :check_default - - before_destroy :confirm_disabled - - validates :title, presence: true - validates :tracker, presence: true - validates :related_link, format: { with: URI::DEFAULT_PARSER.make_regexp }, allow_blank: true - - scope :enabled, -> { where(enabled: true) } - scope :sorted, -> { order(:position) } - scope :search_by_tracker, lambda { |tracker_id| - where(tracker_id: tracker_id) if tracker_id.present? - } - - scope :is_default, -> { where(is_default: true) } - scope :not_default, -> { where(is_default: false) } - - scope :orphaned, lambda { |project_id = nil| - condition = all - if project_id.present? && try(:name) == 'IssueTemplate' - condition = condition.where(project_id: project_id) - ids = Tracker.joins(:projects).where(projects: { id: project_id }).pluck(:id) - else - ids = Tracker.pluck(:id) - end - condition.where.not(tracker_id: ids) - } - - after_destroy do |template| - logger.info("[Destroy] #{self.class}: #{template.inspect}") - end - - # ActiveRecord::SerializationTypeMismatch may be thrown if non hash object is assigned. - serialize :builtin_fields_json, Hash +module IssueTemplateCommon + extend ActiveSupport::Concern + + # + # Common scope both global and project scope template. + # + included do + belongs_to :author, class_name: 'User', foreign_key: 'author_id' + belongs_to :tracker + before_save :check_default + + before_destroy :confirm_disabled + + validates :title, presence: true + validates :tracker, presence: true + validates :related_link, format: { with: URI::DEFAULT_PARSER.make_regexp }, allow_blank: true + + scope :enabled, -> { where(enabled: true) } + scope :sorted, -> { order(:position) } + scope :search_by_tracker, lambda { |tracker_id| + where(tracker_id: tracker_id) if tracker_id.present? + } + + scope :is_default, -> { where(is_default: true) } + scope :not_default, -> { where(is_default: false) } + + scope :orphaned, lambda { |project_id = nil| + condition = all + if project_id.present? && try(:name) == 'IssueTemplate' + condition = condition.where(project_id: project_id) + ids = Tracker.joins(:projects).where(projects: { id: project_id }).pluck(:id) + else + ids = Tracker.pluck(:id) end + condition.where.not(tracker_id: ids) + } - # - # Common methods both global and project scope template. - # - def enabled? - enabled - end + after_destroy do |template| + logger.info("[Destroy] #{self.class}: #{template.inspect}") + end - def <=>(other) - position <=> other.position - end + # ActiveRecord::SerializationTypeMismatch may be thrown if non hash object is assigned. + serialize :builtin_fields_json, Hash + end - def checklist - return [] if checklist_json.blank? + # + # Common methods both global and project scope template. + # + def enabled? + enabled + end - begin - JSON.parse(checklist_json) - rescue StandardError - [] - end - end + def <=>(other) + position <=> other.position + end - def template_json(except: nil) - template = {} - template[self.class::Config::JSON_OBJECT_NAME] = generate_json - return template.to_json(root: true) if except.blank? + def checklist + return [] if checklist_json.blank? - template.to_json(root: true, except: [except]) - end + begin + JSON.parse(checklist_json) + rescue StandardError + [] + end + end - def builtin_fields - builtin_fields_json.to_json - end + def template_json(except: nil) + template = {} + template[self.class::Config::JSON_OBJECT_NAME] = generate_json + return template.to_json(root: true) if except.blank? - def generate_json - result = attributes - result[:link_title] = link_title.presence || I18n.t(:issue_template_related_link, default: 'Related Link') - result[:checklist] = checklist - result.except('checklist_json') - end + template.to_json(root: true, except: [except]) + end - def template_struct(option = {}) - Struct.new(:value, :name, :class, :selected).new(id, title, option[:class]) - end + def builtin_fields + builtin_fields_json.to_json + end - def log_destroy_action(template) - logger.info "[Destroy] #{self.class}: #{template.inspect}" if logger&.info - end + def generate_json + result = attributes + result[:link_title] = link_title.presence || I18n.t(:issue_template_related_link, default: 'Related Link') + result[:checklist] = checklist + result.except('checklist_json') + end - def confirm_disabled - return unless enabled? + def template_struct(option = {}) + Struct.new(:value, :name, :class, :selected).new(id, title, option[:class]) + end - errors.add :base, 'enabled_template_cannot_destroy' - throw :abort - end + def log_destroy_action(template) + logger.info "[Destroy] #{self.class}: #{template.inspect}" if logger&.info + end - def copy_title - "copy_of_#{title}" - end - end + def confirm_disabled + return unless enabled? + + errors.add :base, 'enabled_template_cannot_destroy' + throw :abort + end + + def copy_title + "copy_of_#{title}" end end diff --git a/app/models/global_issue_template.rb b/app/models/global_issue_template.rb index b32357f..ff5395a 100644 --- a/app/models/global_issue_template.rb +++ b/app/models/global_issue_template.rb @@ -2,7 +2,7 @@ class GlobalIssueTemplate < ActiveRecord::Base include Redmine::SafeAttributes - include Concerns::IssueTemplate::Common + include IssueTemplateCommon validates :title, uniqueness: { scope: :tracker_id } has_and_belongs_to_many :projects diff --git a/app/models/issue_template.rb b/app/models/issue_template.rb index a88be5b..8311b68 100644 --- a/app/models/issue_template.rb +++ b/app/models/issue_template.rb @@ -2,7 +2,7 @@ class IssueTemplate < ActiveRecord::Base include Redmine::SafeAttributes - include Concerns::IssueTemplate::Common + include IssueTemplateCommon belongs_to :project validates :project_id, presence: true validates :title, uniqueness: { scope: :project_id } diff --git a/init.rb b/init.rb index bbe89ed..d49985d 100644 --- a/init.rb +++ b/init.rb @@ -21,8 +21,10 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. require 'redmine' -require 'issue_templates/issues_hook' -require 'issue_templates/journals_hook' +Rails.configuration.to_prepare do + require 'issue_templates/issues_hook' + require 'issue_templates/journals_hook' +end # NOTE: Keep error message for a while to support Redmine3.x users. def issue_template_version_message(original_message = nil) From 09f7c92ff2ea00860bc25cb28045f469c0400b3a Mon Sep 17 00:00:00 2001 From: Burak Ikiler Date: Fri, 19 May 2023 20:09:30 +0300 Subject: [PATCH 2/3] Redmine 5 Compatibility: Autoloder fix and Rails 6 updates. --- .../{issue_template/common.rb => issue_template_common.rb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/models/concerns/{issue_template/common.rb => issue_template_common.rb} (100%) diff --git a/app/models/concerns/issue_template/common.rb b/app/models/concerns/issue_template_common.rb similarity index 100% rename from app/models/concerns/issue_template/common.rb rename to app/models/concerns/issue_template_common.rb From a227cdca7ddb410dcd9bc00fe2023fa087317096 Mon Sep 17 00:00:00 2001 From: Burak Ikiler <123000591+burak-ikiler-ew@users.noreply.github.com> Date: Wed, 22 May 2024 12:20:15 +0300 Subject: [PATCH 3/3] Update init.rb for version upgrade --- init.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/init.rb b/init.rb index d49985d..2fda8a6 100644 --- a/init.rb +++ b/init.rb @@ -49,7 +49,7 @@ def template_menu_allowed? name 'Redmine Issue Templates plugin' author 'Akiko Takano' description 'Plugin to generate and use issue templates for each project to assist issue creation.' - version '1.0.3' + version '1.0.4' author_url 'http://twitter.com/akiko_pusu' requires_redmine version_or_higher: '4.0' url 'https://github.com/akiko-pusu/redmine_issue_templates'