From 9fd754aeca0733dd8fea6d02906120fc18f2a7b7 Mon Sep 17 00:00:00 2001
From: cryptogopher
1.0 - 1.4: Redmine 3.4.5, Ruby 2.3.8p459, Rails 4.2.11|
@@ -157,7 +157,7 @@ Upgrade steps should work for downgrade also, given that you do them in reverse
Database downgrade (```VERSION``` number ```
1.0 - 1.4: Redmine 3.4.5, Ruby 2.3.8p459, Rails 4.2.11|
+ |Redmine | Compatible plugin versions |Tested with |
+ |--------|----------------------------|-------------------------------------------------------------------------------------------------------------------|
+ |5.0 | 1.7 - |Redmine 5.0.2, Ruby 2.7.6p219, Rails 6.1.6 |
+ |4.2 | 1.7 - |Redmine 4.2.7, Ruby 2.7.6p219, Rails 5.2.8 |
+ |4.0 | 1.2 - 1.6 |Redmine 4.0.4, Ruby 2.4.6p354, Rails 5.2.3 |
+ |3.4 | 1.0 - 1.6 |1.5 - 1.6: Redmine 3.4.5, Ruby 2.4.7p357, Rails 4.2.11.1
1.0 - 1.4: Redmine 3.4.5, Ruby 2.3.8p459, Rails 4.2.11|
You may try and find this plugin working on other versions too. The best is to
run test suite and if it passes without errors, everything will most
diff --git a/app/models/issue_recurrence.rb b/app/models/issue_recurrence.rb
index 61ec435..38935d5 100644
--- a/app/models/issue_recurrence.rb
+++ b/app/models/issue_recurrence.rb
@@ -422,6 +422,9 @@ def create(dates)
log(:warning_author, id: new_issue.id, login: author_login)
end
+ relation_types = Array(Setting.plugin_issue_recurring[:copy_relation_types])
+ copy_issue_relations(ref_issue, new_issue, relation_types)
+
if self.include_subtasks
target_label = self.anchor_to_start ? :start : :due
new_issue.children.each do |child|
@@ -670,10 +673,74 @@ def self.renew_all(quiet=false)
private
+ def copy_issue_relations(ref_issue, new_issue, relation_types)
+ relation_types = Array(relation_types).map(&:to_s)
+
+ (new_issue.relations_from.to_a + new_issue.relations_to.to_a).uniq.each do |relation|
+ type_for_issue = relation_type_for_issue(relation, new_issue)
+ next if relation_types.include?(type_for_issue)
+
+ relation.destroy
+ end
+
+ return if relation_types.blank?
+
+ ref_issue.relations.each do |relation|
+ type_for_issue = relation_type_for_issue(relation, ref_issue)
+ next unless relation_types.include?(type_for_issue)
+
+ issue_from = relation.issue_from == ref_issue ? new_issue : relation.issue_from
+ issue_to = relation.issue_to == ref_issue ? new_issue : relation.issue_to
+
+ next if issue_from == issue_to
+
+ existing = IssueRelation
+ .where(relation_type: relation.relation_type)
+ .where(
+ "(issue_from_id = :from_id AND issue_to_id = :to_id) OR " \
+ "(issue_from_id = :to_id AND issue_to_id = :from_id)",
+ from_id: issue_from.id,
+ to_id: issue_to.id
+ )
+ .exists?
+ next if existing
+
+ new_relation = IssueRelation.new(relation_type: relation.relation_type,
+ delay: relation.delay)
+ new_relation.issue_from = issue_from
+ new_relation.issue_to = issue_to
+
+ next if new_relation.save
+
+ log(:warning_copy_relation,
+ id: new_issue.id,
+ relation: relation.relation_type,
+ errors: new_relation.errors.full_messages.to_sentence)
+ end
+ end
+
def log(label, **args)
@journal_notes << "#{l(label, args)}\r\n"
end
+ INVERSE_RELATION_TYPES = {
+ 'relates' => 'relates',
+ 'duplicates' => 'duplicated',
+ 'duplicated' => 'duplicates',
+ 'blocks' => 'blocked',
+ 'blocked' => 'blocks',
+ 'precedes' => 'follows',
+ 'follows' => 'precedes',
+ 'copied_to' => 'copied_from',
+ 'copied_from' => 'copied_to'
+ }.freeze
+
+ def relation_type_for_issue(relation, issue)
+ return relation.relation_type if relation.issue_from_id == issue.id
+
+ INVERSE_RELATION_TYPES.fetch(relation.relation_type, relation.relation_type)
+ end
+
class Date < ::Date
def self.today
# Due to its nature, Date.today may sometimes be equal to Date.yesterday/tomorrow.
diff --git a/app/views/settings/_issue_recurrences.html.erb b/app/views/settings/_issue_recurrences.html.erb
index 4c40000..2be0059 100644
--- a/app/views/settings/_issue_recurrences.html.erb
+++ b/app/views/settings/_issue_recurrences.html.erb
@@ -18,6 +18,11 @@
<%= t('.copy_recurrences_hint') %>
+<%= label_tag :settings_copy_relation_types, t('.copy_relation_types') %> +<%= relation_copy_options(@settings[:copy_relation_types]) %> +<%= t('.copy_relation_types_hint') %> +
+<%= label_tag :settings_renew_ahead, t('.renew_ahead') %> <%= number_field_tag 'settings[ahead_multiplier]', @settings[:ahead_multiplier], min: 0, step: 1, size: 3 %> diff --git a/config/locales/bg.yml b/config/locales/bg.yml index cb9196a..4ca4bce 100644 --- a/config/locales/bg.yml +++ b/config/locales/bg.yml @@ -51,6 +51,8 @@ bg: warning_author: "*Warning!* Nonexistent user %{login} can't be set as the author of newly recurred issue #%{id}. Keeping author from reference issue. Please select existing user in plugin settings for future recurrences." + warning_copy_relation: "*Warning!* Couldn't copy relation %{relation} to issue #%{id}: + %{errors}." settings: issue_recurrences: author: 'Авторът на новата задача да бъде' @@ -68,6 +70,9 @@ bg: copy_recurrences: 'Копиране на цикличностите при копиране на задачата' copy_recurrences_hint: 'прилага се независимо дали задачите се копират директно или като резултат от копиране на проект' + copy_relation_types: 'Копиране на връзките при създаване на повторения' + copy_relation_types_hint: 'изберете типовете връзки, които да се копират от отправната + задача към всяко повторение' renew_ahead: 'Renew fixed recurrences ahead for' renew_ahead_hint: 'ensures that the last created recurrence is at least that far into the future (recurrences based on close date are not affected)' diff --git a/config/locales/en.yml b/config/locales/en.yml index 81d4111..9928af2 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -39,6 +39,8 @@ en: warning_author: "*Warning!* Nonexistent user %{login} can't be set as the author of newly recurred issue #%{id}. Keeping author from reference issue. Please select existing user in plugin settings for future recurrences." + warning_copy_relation: "*Warning!* Couldn't copy relation %{relation} to issue #%{id}: + %{errors}." settings: issue_recurrences: author: 'Set author of new recurrence to' @@ -54,6 +56,9 @@ en: copy_recurrences: 'Copy recurrences on issue copy' copy_recurrences_hint: 'applies regardless of whether issues are copied directly or as a result of project copy' + copy_relation_types: 'Copy relations when creating recurrences' + copy_relation_types_hint: 'select relation types to copy from the reference issue into + each recurrence' renew_ahead: 'Renew fixed recurrences ahead for' renew_ahead_hint: 'ensures that the last created recurrence is at least that far into the future (recurrences based on close date are not affected)' diff --git a/config/locales/es.yml b/config/locales/es.yml index 9c76905..e52c31e 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -47,6 +47,8 @@ es: warning_author: "*Warning!* Nonexistent user %{login} can't be set as the author of newly recurred issue #%{id}. Keeping author from reference issue. Please select existing user in plugin settings for future recurrences." + warning_copy_relation: "*Warning!* Couldn't copy relation %{relation} to issue #%{id}: + %{errors}." settings: issue_recurrences: author: 'Establecer autor para nueva recurrencia a' @@ -64,6 +66,9 @@ es: copy_recurrences: 'Copy recurrences on issue copy' copy_recurrences_hint: 'applies regardless of whether issues are copied directly or as a result of project copy' + copy_relation_types: 'Copy relations when creating recurrences' + copy_relation_types_hint: 'select relation types to copy from the reference issue into + each recurrence' renew_ahead: 'Renew fixed recurrences ahead for' renew_ahead_hint: 'ensures that the last created recurrence is at least that far into the future (recurrences based on close date are not affected)' diff --git a/init.rb b/init.rb index 7e526c3..17f4f5e 100644 --- a/init.rb +++ b/init.rb @@ -46,7 +46,7 @@ def load_patches name 'Issue recurring plugin' author 'cryptogopher' description 'Schedule Redmine issue recurrence based on multiple conditions' - version '1.7.1' + version '1.7.2' url 'https://github.com/cryptogopher/issue_recurring' author_url 'https://github.com/cryptogopher' @@ -68,6 +68,7 @@ def load_patches keep_assignee: false, journal_mode: :never, copy_recurrences: false, + copy_relation_types: [], ahead_multiplier: 0, ahead_mode: :days }, partial: 'settings/issue_recurrences' diff --git a/lib/issue_recurring/settings_controller_patch.rb b/lib/issue_recurring/settings_controller_patch.rb index e5a653c..dae4960 100644 --- a/lib/issue_recurring/settings_controller_patch.rb +++ b/lib/issue_recurring/settings_controller_patch.rb @@ -25,6 +25,10 @@ def save_issue_recurring_settings settings[:copy_recurrences] = params[:settings][:copy_recurrences] == 'true' ? true : false + relation_types = Array(params[:settings][:copy_relation_types]).map(&:to_s) + settings[:copy_relation_types] = + relation_types & IssueRelation::TYPES.keys + settings[:ahead_multiplier] = params[:settings][:ahead_multiplier].to_i.abs ahead_mode = params[:settings][:ahead_mode].to_sym settings[:ahead_mode] = IssueRecurrence::AHEAD_MODES.include?(ahead_mode) ? diff --git a/lib/issue_recurring/settings_helper_patch.rb b/lib/issue_recurring/settings_helper_patch.rb index 3d8d90e..21eaaa7 100644 --- a/lib/issue_recurring/settings_helper_patch.rb +++ b/lib/issue_recurring/settings_helper_patch.rb @@ -18,6 +18,24 @@ def ahead_mode_options(default) modes.map { |am| [t("issues.recurrences.form.delay_modes.#{am}"), am] }, default ) end + + def relation_copy_options(selected) + selected = Array(selected).map(&:to_s) + types = IssueRelation::TYPES.keys + types = types.sort_by { |type| l(IssueRelation::TYPES[type][:name]) } + types.map do |type| + label = l(IssueRelation::TYPES[type][:name]) + content_tag(:label, class: 'block') do + safe_join([ + check_box_tag('settings[copy_relation_types][]', type, + selected.include?(type), + id: "settings_copy_relation_types_#{type}"), + ' ', + h(label) + ]) + end + end.join.html_safe + end end end diff --git a/test/integration/issue_recurrences_test.rb b/test/integration/issue_recurrences_test.rb index b205f72..34ef436 100644 --- a/test/integration/issue_recurrences_test.rb +++ b/test/integration/issue_recurrences_test.rb @@ -2506,6 +2506,115 @@ def test_renew_creation_mode_copy_last assert_equal rel2.issue_to, r2 end + def test_renew_copies_selected_relations + logout_user + log_user 'admin', 'foo' + update_plugin_settings(copy_relation_types: ['relates']) + logout_user + log_user 'alice', 'foo' + + @issue1.update!(start_date: Date.new(2018,9,15), due_date: Date.new(2018,9,20)) + IssueRelation.create!(issue_from: @issue1, issue_to: @issue2, relation_type: 'relates') + + create_recurrence + travel_to(@issue1.start_date) + new_issue = renew_all(1) + + relation = IssueRelation.find_by(issue_from: new_issue, issue_to: @issue2, + relation_type: 'relates') + relation ||= IssueRelation.find_by(issue_from: @issue2, issue_to: new_issue, + relation_type: 'relates') + assert relation + ensure + logout_user + log_user 'admin', 'foo' + update_plugin_settings(copy_relation_types: []) + logout_user + log_user 'alice', 'foo' + end + + def test_renew_copies_only_selected_blocked_relations + logout_user + log_user 'admin', 'foo' + update_plugin_settings(copy_relation_types: ['blocked']) + logout_user + log_user 'alice', 'foo' + + @issue1.update!(start_date: Date.new(2018,9,15), due_date: Date.new(2018,9,20)) + IssueRelation.create!(issue_from: @issue3, issue_to: @issue1, relation_type: 'blocks') + + create_recurrence + travel_to(@issue1.start_date) + new_issue = renew_all(1) + + relation = IssueRelation.find_by(issue_from: @issue3, issue_to: new_issue, + relation_type: 'blocks') + assert relation + + refute IssueRelation.exists?(issue_from: new_issue, relation_type: 'blocks') + ensure + logout_user + log_user 'admin', 'foo' + update_plugin_settings(copy_relation_types: []) + logout_user + log_user 'alice', 'foo' + end + + def test_renew_copies_only_selected_blocks_relations + logout_user + log_user 'admin', 'foo' + update_plugin_settings(copy_relation_types: ['blocks']) + logout_user + log_user 'alice', 'foo' + + @issue1.update!(start_date: Date.new(2018,9,15), due_date: Date.new(2018,9,20)) + IssueRelation.create!(issue_from: @issue1, issue_to: @issue3, relation_type: 'blocks') + + create_recurrence + travel_to(@issue1.start_date) + new_issue = renew_all(1) + + relation = IssueRelation.find_by(issue_from: new_issue, issue_to: @issue3, + relation_type: 'blocks') + assert relation + + refute IssueRelation.exists?(issue_to: new_issue, relation_type: 'blocks') + ensure + logout_user + log_user 'admin', 'foo' + update_plugin_settings(copy_relation_types: []) + logout_user + log_user 'alice', 'foo' + end + + def test_renew_copied_to_relations_skip_self_reference + logout_user + log_user 'admin', 'foo' + update_plugin_settings(copy_relation_types: ['copied_to']) + logout_user + log_user 'alice', 'foo' + + @issue1.update!(start_date: Date.new(2018,9,15), due_date: Date.new(2018,9,20)) + IssueRelation.create!(issue_from: @issue1, issue_to: @issue3, relation_type: 'copied_to') + + create_recurrence + travel_to(@issue1.start_date) + new_issue = renew_all(1) + + assert_nil IssueRelation.find_by(issue_from: new_issue, issue_to: new_issue, + relation_type: 'copied_to') + + relation = IssueRelation.find_by(issue_from: new_issue, issue_to: @issue3, + relation_type: 'copied_to') + assert relation + ensure + logout_user + log_user 'admin', 'foo' + update_plugin_settings(copy_relation_types: []) + logout_user + log_user 'alice', 'foo' + end + def test_renew_creation_mode_reopen @issue1.update!(start_date: Date.new(2018,9,15), due_date: Date.new(2018,9,20)) diff --git a/test/unit/issue_recurrence_test.rb b/test/unit/issue_recurrence_test.rb index d062c97..16f9c89 100644 --- a/test/unit/issue_recurrence_test.rb +++ b/test/unit/issue_recurrence_test.rb @@ -9,6 +9,8 @@ class IssueRecurrenceTest < ActiveSupport::TestCase def setup @issue1 = issues(:issue_01) + @issue2 = issues(:issue_02) + @issue3 = issues(:issue_03) User.current = users(:alice) end @@ -19,4 +21,104 @@ def test_new assert ir ir.save! end + + def test_copy_issue_relations_removes_unselected_types + recurrence = IssueRecurrence.new(issue: @issue1) + stray = IssueRelation.create!(issue_from: @issue2, issue_to: @issue3, + relation_type: 'blocks') + + recurrence.send(:copy_issue_relations, @issue1, @issue2, []) + + assert_not IssueRelation.exists?(stray.id) + ensure + stray.destroy if stray&.persisted? + end + + def test_copy_issue_relations_removes_unselected_incoming_types + recurrence = IssueRecurrence.new(issue: @issue1) + stray = IssueRelation.create!(issue_from: @issue3, issue_to: @issue2, + relation_type: 'blocks') + + recurrence.send(:copy_issue_relations, @issue1, @issue2, []) + + assert_not IssueRelation.exists?(stray.id) + ensure + stray.destroy if stray&.persisted? + end + + def test_copy_issue_relations_skips_duplicate_relations + recurrence = IssueRecurrence.new(issue: @issue1) + source = IssueRelation.create!(issue_from: @issue1, issue_to: @issue3, + relation_type: 'relates') + duplicate = IssueRelation.create!(issue_from: @issue2, issue_to: @issue3, + relation_type: 'relates') + + assert_no_difference 'IssueRelation.count' do + recurrence.send(:copy_issue_relations, @issue1, @issue2, ['relates']) + end + assert_equal '', recurrence.journal_notes + ensure + source.destroy if source&.persisted? + duplicate.destroy if duplicate&.persisted? + end + + def test_copy_issue_relations_copies_only_selected_outgoing_type + recurrence = IssueRecurrence.new(issue: @issue1) + source = IssueRelation.create!(issue_from: @issue1, issue_to: @issue3, + relation_type: 'blocks') + + recurrence.send(:copy_issue_relations, @issue1, @issue2, ['blocks']) + + copied = IssueRelation.find_by(issue_from: @issue2, issue_to: @issue3, + relation_type: 'blocks') + assert copied + assert_equal 1, IssueRelation.where(issue_from: @issue2, issue_to: @issue3, + relation_type: 'blocks').count + ensure + source.destroy if source&.persisted? + copied&.destroy if copied&.persisted? + end + + def test_copy_issue_relations_copies_only_selected_incoming_type + recurrence = IssueRecurrence.new(issue: @issue1) + source = IssueRelation.create!(issue_from: @issue3, issue_to: @issue1, + relation_type: 'blocks') + + recurrence.send(:copy_issue_relations, @issue1, @issue2, ['blocked']) + + copied = IssueRelation.find_by(issue_from: @issue3, issue_to: @issue2, + relation_type: 'blocks') + assert copied + assert_equal 1, IssueRelation.where(issue_from: @issue3, issue_to: @issue2, + relation_type: 'blocks').count + ensure + source.destroy if source&.persisted? + copied&.destroy if copied&.persisted? + end + + def test_copy_issue_relations_skips_unselected_outgoing_type + recurrence = IssueRecurrence.new(issue: @issue1) + source = IssueRelation.create!(issue_from: @issue1, issue_to: @issue3, + relation_type: 'blocks') + + recurrence.send(:copy_issue_relations, @issue1, @issue2, ['blocked']) + + refute IssueRelation.exists?(issue_from: @issue2, issue_to: @issue3, + relation_type: 'blocks') + ensure + source.destroy if source&.persisted? + end + + def test_copy_issue_relations_skips_unselected_incoming_type + recurrence = IssueRecurrence.new(issue: @issue1) + source = IssueRelation.create!(issue_from: @issue3, issue_to: @issue1, + relation_type: 'blocks') + + recurrence.send(:copy_issue_relations, @issue1, @issue2, ['blocks']) + + refute IssueRelation.exists?(issue_from: @issue3, issue_to: @issue2, + relation_type: 'blocks') + ensure + source.destroy if source&.persisted? + end end