Skip to content

Commit 014db54

Browse files
committed
Keep the times when an issue is copied
Redmine's issue copy carries attributes but not associations, so a copy of an issue with times silently became all-day, although the design doc promised otherwise. A copied issue now inherits the original's sidecar row as the source for the sync, so the times of day survive and are re-anchored on the copy's (possibly changed) dates (#17). Also drop the dead `return true` callback idiom; callbacks halt only via throw :abort (#20).
1 parent 04c9037 commit 014db54

2 files changed

Lines changed: 82 additions & 13 deletions

File tree

lib/redmine_issue_datetime/issue_extension.rb

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ def due_time=(value)
2626
@due_time_input = value
2727
end
2828

29-
# "All day" is the absence of times rather than a stored flag, so the setter
30-
# is sugar for clearing both. It is applied after the time setters regardless
31-
# of parameter order, so ticking the box always wins over whatever the
32-
# (disabled, and therefore possibly stale) time inputs submitted.
29+
# "All day" means "no times"; there is no stored flag. Setting it to
30+
# true clears both times. The flag is applied during the save, not
31+
# here, so it wins regardless of the order the attributes were
32+
# assigned in.
3333
def all_day=(value)
3434
@all_day_input = ActiveRecord::Type::Boolean.new.cast(value)
3535
end
@@ -53,24 +53,29 @@ def due_time
5353

5454
def prepare_issue_datetime_sync
5555
@issue_datetime_pending = nil
56-
return true unless RedmineIssueDatetime.enabled_for?(tracker_id)
56+
return unless RedmineIssueDatetime.enabled_for?(tracker_id)
5757

58-
# Ticking "all day" clears both times whatever the (disabled) time inputs
59-
# submitted, so it cannot be defeated by a stale value in the form.
58+
# Ticking "all day" clears both times, even if the (disabled) time
59+
# inputs still submitted stale values.
6060
if @all_day_input
6161
@start_time_input = ''
6262
@due_time_input = ''
6363
end
6464

65-
record = issue_datetime
66-
return true if record.nil? && @start_time_input.blank? && @due_time_input.blank?
65+
# A copied issue has no sidecar row of its own yet, so use the
66+
# original's row as the source. This is what makes a copy keep its
67+
# times. Core exposes the original only as the @copied_from ivar
68+
# (its public API is just copy?).
69+
source = issue_datetime
70+
source ||= @copied_from.issue_datetime if new_record? && copy?
71+
return if source.nil? && @start_time_input.blank? && @due_time_input.blank?
6772

6873
touched = !@start_time_input.nil? || !@due_time_input.nil? ||
6974
will_save_change_to_start_date? || will_save_change_to_due_date?
70-
return true unless touched
75+
return unless touched
7176

72-
old_starts = record&.starts_at
73-
old_ends = record&.ends_at
77+
old_starts = source&.starts_at
78+
old_ends = source&.ends_at
7479
new_starts = RedmineIssueDatetime.combine(start_date, @start_time_input, old_starts)
7580
new_ends = RedmineIssueDatetime.combine(due_date, @due_time_input, old_ends)
7681

@@ -83,7 +88,6 @@ def prepare_issue_datetime_sync
8388
journalize_issue_datetime('due_time', old_ends, new_ends)
8489

8590
@issue_datetime_pending = {starts_at: new_starts, ends_at: new_ends}
86-
true
8791
end
8892

8993
def persist_issue_datetime

test/unit/issue_copy_test.rb

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
require File.expand_path('../test_helper', __dir__)
2+
3+
class IssueCopyTest < ActiveSupport::TestCase
4+
include IssueDatetimeTestHelper
5+
6+
fixtures :projects, :users, :email_addresses, :trackers, :projects_trackers,
7+
:issue_statuses, :issues, :enumerations, :enabled_modules,
8+
:members, :member_roles, :roles
9+
10+
def setup
11+
@issue = Issue.find(1)
12+
enable_issue_datetime(@issue.tracker_id)
13+
@issue.update!(start_date: Date.new(2026, 8, 3), start_time: '09:15',
14+
due_date: Date.new(2026, 8, 4), due_time: '17:00')
15+
@issue.reload
16+
end
17+
18+
# The sidecar row is an association, so Redmine's issue copy does not carry
19+
# it over by itself; the copy support in IssueExtension does.
20+
test 'copying an issue keeps its times' do
21+
copy = @issue.copy
22+
assert copy.save
23+
24+
record = copy.reload.issue_datetime
25+
assert_not_nil record, 'the copy must get its own sidecar row'
26+
assert_equal Time.utc(2026, 8, 3, 9, 15), record.starts_at
27+
assert_equal Time.utc(2026, 8, 4, 17, 0), record.ends_at
28+
end
29+
30+
test 'a copy with changed dates keeps the times of day' do
31+
copy = @issue.copy
32+
copy.start_date = Date.new(2026, 9, 1)
33+
copy.due_date = Date.new(2026, 9, 2)
34+
assert copy.save
35+
36+
record = copy.reload.issue_datetime
37+
assert_equal Time.utc(2026, 9, 1, 9, 15), record.starts_at
38+
assert_equal Time.utc(2026, 9, 2, 17, 0), record.ends_at
39+
end
40+
41+
test 'copying an all-day issue stays all day' do
42+
@issue.all_day = '1'
43+
assert @issue.save
44+
45+
copy = @issue.reload.copy
46+
assert copy.save
47+
assert_nil copy.reload.issue_datetime
48+
end
49+
50+
test 'times set while copying win over the inherited ones' do
51+
copy = @issue.copy
52+
copy.start_time = '10:30'
53+
assert copy.save
54+
55+
assert_equal Time.utc(2026, 8, 3, 10, 30), copy.reload.issue_datetime.starts_at
56+
end
57+
58+
test 'the original keeps its own row untouched' do
59+
copy = @issue.copy
60+
assert copy.save
61+
62+
assert_not_equal @issue.reload.issue_datetime.id, copy.reload.issue_datetime.id
63+
assert_equal Time.utc(2026, 8, 3, 9, 15), @issue.issue_datetime.starts_at
64+
end
65+
end

0 commit comments

Comments
 (0)