Let an on-call day be handed to somebody else
Release / build (amd64, darwin) (push) Has been skipped
Release / build (arm64, darwin) (push) Has been skipped
Release / docker (push) Has been skipped
Release / release (push) Has been skipped
Release / test (push) Failing after 6s
Release / build (amd64, linux) (push) Has been skipped
Release / build (arm64, linux) (push) Has been skipped
Release / chart (push) Has been skipped
Release / build (amd64, darwin) (push) Has been skipped
Release / build (arm64, darwin) (push) Has been skipped
Release / docker (push) Has been skipped
Release / release (push) Has been skipped
Release / test (push) Failing after 6s
Release / build (amd64, linux) (push) Has been skipped
Release / build (arm64, linux) (push) Has been skipped
Release / chart (push) Has been skipped
A date is held by exactly one person and POST /api/schedule plain-inserts, so any date that was already taken came back 409. That made reassignment impossible through the API: the only route was to delete the entry first, and for a week that meant seven separate deletions. Worse, the reject is all-or-nothing across the request, so assigning a week where a single day happened to be taken failed entirely and placed none of the other six. The refusal itself is worth keeping. Moving a shift off the person expecting to be paged for it should not be something a plain call does by accident, so the fix is to make it possible to ask for rather than to remove the guard: "replace": true takes the dates anyway, and the flag defaults to off so every existing caller behaves exactly as before. The delete and the insert share the transaction that was already there. That matters more than the flag does — a week of free and taken days now lands as a unit, and a failure part way through leaves the rota as it was instead of with a shift deleted and nothing put back. A rota with a hole in it is worse than a rota that refused to change. One consequence worth naming: under replace a date repeated inside one request is idempotent rather than a conflict, because the second pass clears what the first wrote.
This commit is contained in:
@@ -17,6 +17,12 @@ func handleCreateSchedule(db *sql.DB) http.HandlerFunc {
|
||||
var req struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
Dates []string `json:"dates"`
|
||||
|
||||
// Replace takes dates that somebody else already holds. It defaults
|
||||
// to off so that the plain call cannot quietly move a shift off the
|
||||
// person expecting to be paged for it — reassigning has to be asked
|
||||
// for.
|
||||
Replace bool `json:"replace"`
|
||||
}
|
||||
if err := decodeJSON(r, &req); err != nil {
|
||||
respond(w, http.StatusBadRequest, errResp("invalid request body"))
|
||||
@@ -44,7 +50,10 @@ func handleCreateSchedule(db *sql.DB) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// All-or-nothing: if any date already has an assignment, reject the whole request.
|
||||
// All-or-nothing, in both directions: without replace, one taken date
|
||||
// rejects the whole request; with it, either every date moves or none
|
||||
// does. The rota must never be left with a hole where a shift used to
|
||||
// be, so the delete and the insert share one transaction.
|
||||
tx, err := db.BeginTx(r.Context(), nil)
|
||||
if err != nil {
|
||||
respond(w, http.StatusInternalServerError, errResp("internal error"))
|
||||
@@ -53,10 +62,18 @@ func handleCreateSchedule(db *sql.DB) http.HandlerFunc {
|
||||
defer tx.Rollback()
|
||||
|
||||
for _, d := range req.Dates {
|
||||
if req.Replace {
|
||||
if _, err := tx.ExecContext(r.Context(),
|
||||
"DELETE FROM schedule_entries WHERE date = ?", d); err != nil {
|
||||
respond(w, http.StatusInternalServerError, errResp("internal error"))
|
||||
return
|
||||
}
|
||||
}
|
||||
if _, err := tx.ExecContext(r.Context(),
|
||||
"INSERT INTO schedule_entries (user_id, date) VALUES (?, ?)", req.UserID, d); err != nil {
|
||||
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
|
||||
respond(w, http.StatusConflict, errResp("date already assigned: "+d))
|
||||
respond(w, http.StatusConflict,
|
||||
errResp("date already assigned: "+d+" (pass replace to take it)"))
|
||||
return
|
||||
}
|
||||
respond(w, http.StatusInternalServerError, errResp("internal error"))
|
||||
|
||||
Reference in New Issue
Block a user