Skip to content

Commit a2c0385

Browse files
committed
Address review: range validation, strict ISO offsets, gating and guard fixes
- Abort issue save with an error when the computed due time would be before the start time (previously the sidecar save failed silently) - persist with save! so unexpected sidecar failures surface loudly - Require an explicit Z/offset in API timestamps - Skip sync work entirely for date-only edits on issues without times - Gate the issue-view hook by tracker enablement like the form hook - Explicit returns after render_403 in controller filters
1 parent b9016ef commit a2c0385

5 files changed

Lines changed: 37 additions & 6 deletions

File tree

app/controllers/issue_datetimes_controller.rb

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,13 @@ def index
7676

7777
def find_issue
7878
@issue = Issue.find(params[:issue_id])
79-
render_403 unless @issue.visible?
79+
return render_403 unless @issue.visible?
8080
rescue ActiveRecord::RecordNotFound
8181
render_404
8282
end
8383

8484
def require_edit_permission
85-
render_403 unless @issue.editable?
85+
return render_403 unless @issue.editable?
8686
end
8787

8888
def find_project
@@ -91,8 +91,13 @@ def find_project
9191
render_404
9292
end
9393

94+
# Requires an explicit offset (Z or +hh:mm) so API writes are
95+
# unambiguous regardless of the server's local time zone.
9496
def parse_timestamp(value)
95-
Time.iso8601(value.to_s)
97+
text = value.to_s
98+
return nil unless /(Z|[+-]\d{2}:?\d{2})\z/i.match?(text)
99+
100+
Time.iso8601(text)
96101
rescue ArgumentError
97102
nil
98103
end

app/views/hooks/_issue_datetime_show.html.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<% record = issue && issue.issue_datetime %>
1+
<% record = issue && RedmineIssueDatetime.enabled_for?(issue.tracker_id) ? issue.issue_datetime : nil %>
22
<% if record && !record.blank_times? %>
33
<div class="issue-datetime">
44
<span class="issue-datetime-start">

lib/redmine_issue_datetime/issue_extension.rb

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,22 @@ def prepare_issue_datetime_sync
4141
return true unless RedmineIssueDatetime.enabled_for?(tracker_id)
4242

4343
record = issue_datetime
44+
return true if record.nil? && @start_time_input.blank? && @due_time_input.blank?
45+
4446
touched = !@start_time_input.nil? || !@due_time_input.nil? ||
4547
will_save_change_to_start_date? || will_save_change_to_due_date?
46-
return true unless touched || record
48+
return true unless touched
4749

4850
old_starts = record&.starts_at
4951
old_ends = record&.ends_at
5052
new_starts = RedmineIssueDatetime.combine(start_date, @start_time_input, old_starts)
5153
new_ends = RedmineIssueDatetime.combine(due_date, @due_time_input, old_ends)
5254

55+
if new_starts && new_ends && new_ends < new_starts
56+
errors.add(:due_date, :greater_than_start_date)
57+
throw :abort
58+
end
59+
5360
journalize_issue_datetime('start_time', old_starts, new_starts)
5461
journalize_issue_datetime('due_time', old_ends, new_ends)
5562

@@ -70,7 +77,7 @@ def persist_issue_datetime
7077
record.destroy unless record.new_record?
7178
association(:issue_datetime).reset
7279
elsif record.changed?
73-
record.save
80+
record.save!
7481
end
7582
end
7683

test/integration/issue_datetimes_api_test.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@ def setup
4949
assert_equal Time.utc(2026, 8, 3, 17, 0), @issue.issue_datetime.ends_at
5050
end
5151

52+
test 'PUT rejects timestamps without an explicit offset' do
53+
put "/issues/#{@issue.id}/datetime.json",
54+
params: {starts_at: '2026-08-03T09:15:00'}.to_json,
55+
headers: {'Content-Type' => 'application/json'}.merge(credentials('jsmith'))
56+
57+
assert_response :unprocessable_entity
58+
end
59+
5260
test 'PUT rejects malformed timestamps' do
5361
put "/issues/#{@issue.id}/datetime.json",
5462
params: {starts_at: 'tomorrow-ish'}.to_json,

test/unit/issue_datetime_sync_test.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,17 @@ def setup
9595
assert_equal Time.utc(2026, 8, 3, 9, 15), @issue.reload.issue_datetime.starts_at
9696
end
9797

98+
test 'due time before start time on the same date blocks the save' do
99+
@issue.start_date = Date.new(2026, 8, 3)
100+
@issue.due_date = Date.new(2026, 8, 3)
101+
@issue.start_time = '10:00'
102+
@issue.due_time = '09:00'
103+
104+
assert_not @issue.save
105+
assert @issue.errors[:due_date].present?
106+
assert_nil @issue.reload.issue_datetime
107+
end
108+
98109
test 'destroying the issue destroys the sidecar row' do
99110
@issue.update!(start_date: Date.new(2026, 8, 3), start_time: '09:15')
100111
record_id = @issue.reload.issue_datetime.id

0 commit comments

Comments
 (0)