Skip to content

Commit 55c2877

Browse files
committed
Rewrite the README as user documentation
The README still described shipped features as planned and had no installation, configuration, API, or operations documentation. It now covers the full user surface. The design doc is updated to the plugin as built: the one-clock time zone decision, the before_save/after_save sync, issue copy, and the actual test matrix (#19).
1 parent c7de312 commit 55c2877

2 files changed

Lines changed: 158 additions & 33 deletions

File tree

README.md

Lines changed: 132 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,143 @@
11
# redmine_issue_datetime
22

3-
A Redmine plugin that adds time of day to issue start and due dates,
3+
A Redmine plugin that adds a time of day to issue start and due dates,
44
without changing the Redmine core schema.
55

6-
Redmine stores `start_date` and `due_date` as plain dates. The ability to
7-
set a time as well has been requested for a long time
8-
([redmine.org #5458](https://www.redmine.org/issues/5458)). This plugin
9-
solves it with parallel storage: times live in a plugin-owned table and
10-
the date part is mirrored into the core columns, so filters, calendar,
11-
Gantt, exports, and the REST API keep working exactly as before.
6+
Redmine stores `start_date` and `due_date` as plain dates. Adding a time
7+
has been requested for a long time
8+
([redmine.org #5458](https://www.redmine.org/issues/5458), open since
9+
2010). This plugin solves it with parallel storage: times live in a
10+
plugin-owned table, and the date part is mirrored into the core columns.
11+
Filters, calendar, Gantt, exports, and the REST API keep working exactly
12+
as before, and an issue without times behaves exactly like stock Redmine.
1213

13-
## Status
14+
## Features
1415

15-
Early development, no release yet. The core pieces (sidecar storage,
16-
date mirroring, issue form integration, JSON API) are implemented and
17-
covered by tests. See [docs/design.md](docs/design.md) for the full
18-
design document.
19-
20-
## Planned highlights
21-
22-
- Time inputs next to the start and due date fields, with a configurable
23-
step (15 minutes by default)
24-
- Per-tracker enablement; issues without times behave exactly like stock
25-
Redmine
26-
- Times journalized in the issue history
16+
- Time inputs next to the start and due date fields on the issue form,
17+
with a configurable step (15 minutes by default) and an "All day"
18+
toggle
19+
- Enabled per tracker; issues of other trackers are untouched
20+
- Time changes appear in the issue history (journal)
21+
- Optional "Start time" / "Due time" columns on the issue list
22+
- A "Date and time" custom field format, offered alongside the built-in
23+
"Date" format
2724
- JSON API for reading and writing times, including a bulk endpoint for
2825
external consumers such as schedulers and sync clients
26+
- Consistency check and repair tasks for operators
27+
28+
## Requirements
29+
30+
- Redmine 6.0 or later (tested against Redmine 6.1 and 7.0, on Ruby 3.4
31+
and 4.0)
32+
- No other plugins and no GIS stack required
33+
34+
## Installation
35+
36+
```bash
37+
cd /path/to/redmine/plugins
38+
git clone https://github.com/gtt-project/redmine_issue_datetime.git
39+
cd /path/to/redmine
40+
bundle exec rake redmine:plugins:migrate RAILS_ENV=production
41+
```
42+
43+
Restart Redmine, then open
44+
**Administration → Plugins → Redmine Issue Datetime → Configure** and
45+
enable the trackers that should have time fields.
46+
47+
To uninstall, revert the migration and remove the plugin directory:
48+
49+
```bash
50+
bundle exec rake redmine:plugins:migrate NAME=redmine_issue_datetime VERSION=0 RAILS_ENV=production
51+
rm -rf plugins/redmine_issue_datetime
52+
```
53+
54+
Uninstalling drops only the plugin's own table; core issue data is not
55+
affected.
56+
57+
## Configuration
58+
59+
| Setting | Meaning | Default |
60+
| --- | --- | --- |
61+
| Enabled for trackers | Trackers whose issues get time fields | none |
62+
| Time input step | Interval of the time picker; typed values are snapped to it | 15 min |
63+
| Reference time zone | The clock all times are entered and shown in (see below) | application default |
64+
65+
## How times are shown: one clock for everyone
66+
67+
Times are stored as instants (UTC in the database) but always entered
68+
and displayed in one time zone, the configured reference zone. They are
69+
deliberately **not** converted to each viewer's personal time zone: for
70+
work that happens at a physical place, a dispatcher and a worker must
71+
mean the same wall-clock time by "09:15". The zone is labelled in the UI
72+
so this is never ambiguous.
73+
74+
The reference zone also decides which date is mirrored into
75+
`start_date`/`due_date`. Changing it after times have been stored can
76+
make existing rows inconsistent; run the check task below afterwards.
77+
78+
## Custom field format "Date and time"
79+
80+
The plugin also registers a `datetime` custom field format next to the
81+
built-in `date` one. Values are stored as naive local timestamps in the
82+
reference zone (for example `2026-08-03T09:15`) and support filtering and
83+
grouping. Unlike the start/due times, custom field values are not
84+
snapped to the step: an arbitrary datetime field may legitimately record
85+
an off-grid moment. Currently offered for issue custom fields
86+
([#15](https://github.com/gtt-project/redmine_issue_datetime/issues/15)
87+
tracks widening the scope).
88+
89+
## REST API
90+
91+
All endpoints are JSON and use Redmine's regular API authentication.
92+
Reading requires issue visibility; writing requires permission to edit
93+
issue attributes.
94+
95+
```
96+
GET /issues/:issue_id/datetime.json
97+
PUT /issues/:issue_id/datetime.json
98+
DELETE /issues/:issue_id/datetime.json
99+
GET /projects/:project_id/issue_datetimes.json?updated_since=<iso8601>
100+
```
101+
102+
Write timestamps must carry an explicit offset (`Z` or `+09:00`), so a
103+
request means the same thing regardless of the server's local zone:
104+
105+
```bash
106+
curl -X PUT -H 'Content-Type: application/json' \
107+
-H 'X-Redmine-API-Key: <key>' \
108+
-d '{"starts_at": "2026-08-03T09:15:00+09:00", "ends_at": null}' \
109+
https://redmine.example.org/issues/123/datetime.json
110+
```
111+
112+
A timestamp sets both the core date and the time; `null` clears the time
113+
and keeps the date; omitted keys are left untouched. `DELETE` clears all
114+
times (back to "all day") and keeps the dates.
115+
116+
## Consistency tasks
117+
118+
The date mirror is maintained by ActiveRecord callbacks. Anything that
119+
bypasses them (raw SQL, `update_column`, a restored database) can put
120+
the stored times and the core dates out of step:
121+
122+
```bash
123+
# Report disagreements; exits non-zero when any are found (cron/monitoring friendly)
124+
bundle exec rake redmine_issue_datetime:check RAILS_ENV=production
125+
126+
# Repair them: the core date is re-derived from the stored timestamp
127+
bundle exec rake redmine_issue_datetime:repair RAILS_ENV=production
128+
```
129+
130+
## Development
131+
132+
Tests run inside a Redmine checkout, like any Redmine plugin:
133+
134+
```bash
135+
bundle exec rails test plugins/redmine_issue_datetime/test RAILS_ENV=test
136+
```
137+
138+
CI covers Redmine 6.1/7.0 on Ruby 3.4/4.0 with PostgreSQL, plus a
139+
`zeitwerk:check` eager-loading gate. See
140+
[docs/design.md](docs/design.md) for the design document.
29141

30142
## Related projects
31143

docs/design.md

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# redmine_issue_datetime: Design Document
22

3-
Status: Draft for review
3+
Status: As built (kept in step with the implementation)
44
Scope: Standalone Redmine plugin adding time of day to issue start and due dates
55

66
## 1. Problem
@@ -61,10 +61,14 @@ Rules:
6161
default for every existing and new issue.
6262
- `starts_at` / `ends_at` may each be null independently (time set only on
6363
one side).
64-
- Times are stored in UTC and rendered in the user's Redmine time zone
65-
preference. Since core dates are zone-naive, the mirror into
66-
`start_date` / `due_date` uses a per-instance reference zone (plugin
67-
setting, default: the application default time zone).
64+
- Times are stored in UTC but always entered and displayed in one
65+
per-instance reference zone (plugin setting, default: the application
66+
default time zone), with a visible zone label. They are deliberately
67+
not converted to each viewer's personal time zone: for work that
68+
happens at a physical place, everyone must mean the same wall-clock
69+
time by "09:15". The same reference zone anchors the date mirror into
70+
`start_date` / `due_date`, so the date a user sees always matches the
71+
time they typed.
6872
- Duration is intentionally not stored here. Redmine's existing
6973
`estimated_hours` already expresses effort/service duration and stays
7074
the single source for that.
@@ -76,8 +80,12 @@ Rules:
7680
- Setting a time creates or updates the sidecar row and mirrors the date
7781
part into the core column in the same save.
7882
- Changing only the core date (via stock UI, bulk edit, or API) keeps the
79-
time of day and shifts the timestamp to the new date. The sync runs in an
80-
`after_save` patch on `Issue` (module prepend, no alias chaining).
83+
time of day and shifts the timestamp to the new date. The sync is an
84+
`ActiveSupport::Concern` included into `Issue` and runs in two steps:
85+
the new timestamps are computed and journalized in `before_save`
86+
(journal details must exist before core's own `create_journal`
87+
after_save callback writes the journal), and persisted to the sidecar
88+
row in `after_save`.
8189
- Clearing the date clears the corresponding timestamp.
8290
- Clearing the time (back to "all day") deletes or nulls the sidecar value;
8391
the core date stays.
@@ -89,8 +97,11 @@ keys), so issue history reflects them like any other field change.
8997

9098
### Copy / move
9199

92-
Issue copy duplicates the sidecar row. This must be explicit
93-
(`after copy` hook); it does not come for free.
100+
Issue copy duplicates the times: the copy keeps the same times of day,
101+
re-anchored on its (possibly changed) dates. This is explicit in the
102+
sync callback (a copied issue inherits the original's sidecar row as its
103+
source); it does not come for free from Redmine's copy, which only
104+
carries attributes.
94105

95106
### Parent/child
96107

@@ -157,10 +168,12 @@ code updates dates without going through ActiveRecord callbacks (raw SQL,
157168
## 9. Testing
158169

159170
- Model tests for the mirror invariant in both directions, null handling,
160-
time zone edges (date boundary around midnight in non-UTC zones).
161-
- Controller tests for the API endpoints and permissions.
162-
- Integration test for form submit through the hook.
163-
- Target matrix: current Redmine 6.x and RedMica, PostgreSQL and MySQL.
171+
time zone edges (date boundary around midnight in non-UTC zones), issue
172+
copy, and the drift check/repair.
173+
- API tests for the endpoints, parsing rules, and permissions (including
174+
the note-only-permission case, which must not be allowed to write).
175+
- CI matrix: Redmine 6.1 (Ruby 3.4) and 7.0 (Ruby 3.4 and 4.0) on
176+
PostgreSQL, plus a `zeitwerk:check` eager-loading gate.
164177

165178
## 10. Delivery
166179

0 commit comments

Comments
 (0)