mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-22 15:06:32 +00:00
fix(tours): save_tour_change says nothing was saved and answers a proposal saved once already
A save refused by the rules of the tour now says "Nothing was saved and nothing changed." instead of the preparation's wording: lay() takes what the changes are laid for, and refusalText() leads with that action. A save sent again after it committed (a retry after a lost answer) used to meet the conflict of a tour that moved on, which tells the assistant to prepare the change anew, so an add could be saved twice. The save now looks up the step its proposal was saved as from the revision it was prepared on, before the stale check and once more inside the write, and answers already_saved with that step instead, writing nothing.
This commit is contained in:
@@ -87,6 +87,25 @@ export class TourChangeStepsRepository extends TrekRepository<TourChangeSteps> {
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* The step a proposal of this tour was saved as, from the revision it was
|
||||
* prepared on; undefined while it is not saved. A proposal is saved once, so
|
||||
* a save sent again finds the step its first one recorded.
|
||||
*/
|
||||
async findSavedProposal(
|
||||
place_id: number,
|
||||
proposal_id: string,
|
||||
base_revision: number,
|
||||
): Promise<TourChangeStepRow | undefined> {
|
||||
return await this.db()
|
||||
.selectFrom('tour_change_steps')
|
||||
.select(STEP_COLUMNS)
|
||||
.where('place_id', '=', place_id)
|
||||
.where('proposal_id', '=', proposal_id)
|
||||
.where('base_revision', '=', base_revision)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a step taken back, inside the caller's transaction. Only a step that
|
||||
* still stands is marked, so it is taken back once: false when it was already.
|
||||
|
||||
@@ -369,7 +369,10 @@ function refusalLine(refusal: ChangeRefusal): string {
|
||||
return `${about}: ${REFUSAL_TEXT[refusal.reason]}${days}.`;
|
||||
}
|
||||
|
||||
/** Every refusal of a proposal, one per line, under a line saying nothing was prepared or saved. */
|
||||
export function refusalText(refused: readonly ChangeRefusal[]): string {
|
||||
return ['Nothing was prepared and nothing changed.', ...refused.map(refusalLine)].join('\n');
|
||||
/** What a refused proposal was asked for: prepared for review, or saved once the traveller confirmed it. */
|
||||
export type RefusedAction = 'prepared' | 'saved';
|
||||
|
||||
/** Every refusal of a proposal, one per line, under a line saying it was not prepared, or not saved. */
|
||||
export function refusalText(refused: readonly ChangeRefusal[], action: RefusedAction): string {
|
||||
return [`Nothing was ${action} and nothing changed.`, ...refused.map(refusalLine)].join('\n');
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { TourStationVisitsRepository } from '../../db/repositories/TourStat
|
||||
import type { TourWaypointsRepository } from '../../db/repositories/TourWaypoints.repository';
|
||||
import { DomainError } from '../common/domain-error';
|
||||
import { CityWalkFootWayService } from './city-walk-foot-way.service';
|
||||
import { applyChanges, refusalText, type StationDone } from './tour-change-apply';
|
||||
import { applyChanges, refusalText, type RefusedAction, type StationDone } from './tour-change-apply';
|
||||
import { describeChange } from './tour-change-effects';
|
||||
import { proposalIdOf, staleProposal } from './tour-change-proposal';
|
||||
import { ToursService } from './tours.service';
|
||||
@@ -15,6 +15,12 @@ import type { TourChange, TourChangeRequest } from '@trek/shared';
|
||||
/** The refusal of a proposal the rules of its tour do not allow; its text names every change and why. */
|
||||
export const TOUR_CHANGE_REFUSED = 'tour_change_refused';
|
||||
|
||||
/**
|
||||
* What the changes are laid for: a preparation, or the save of a proposal
|
||||
* prepared on a revision, whose refusal then says nothing was saved.
|
||||
*/
|
||||
export type LayFor = { action: Extract<RefusedAction, 'prepared'> } | { action: 'saved'; preparedOn: number };
|
||||
|
||||
/** The places of the trip the new stations of a proposal name. */
|
||||
function linkedIds(changes: readonly TourChange[]): number[] {
|
||||
return changes.flatMap((change) => {
|
||||
@@ -55,15 +61,15 @@ export class TourChangePreviewService {
|
||||
/**
|
||||
* The changes laid onto the tour as it is saved now, with the visits of all
|
||||
* its days and the places the new stations name, or the refusal naming
|
||||
* every change the rules of the tour do not allow. A save lays them again
|
||||
* on the tour it is about to write, so it is held to the same rules, and
|
||||
* names the revision they were prepared on: a tour that moved past it is a
|
||||
* conflict before any rule is asked.
|
||||
* every change the rules of the tour do not allow, saying it was not
|
||||
* `layFor.action`. A save lays them again on the tour it is about to write, so
|
||||
* it is held to the same rules, and names the revision they were prepared
|
||||
* on: a tour that moved past it is a conflict before any rule is asked.
|
||||
*/
|
||||
async lay(tripId: number, placeId: number, changes: readonly TourChange[], preparedOn?: number) {
|
||||
async lay(tripId: number, placeId: number, changes: readonly TourChange[], layFor: LayFor) {
|
||||
const { tour, waypoints } = await this.tours.getTour(String(tripId), String(placeId));
|
||||
if (preparedOn !== undefined && tour.revision !== preparedOn) {
|
||||
throw staleProposal(preparedOn, tour.revision ?? null);
|
||||
if (layFor.action === 'saved' && tour.revision !== layFor.preparedOn) {
|
||||
throw staleProposal(layFor.preparedOn, tour.revision ?? null);
|
||||
}
|
||||
const [visits, linkable] = await Promise.all([
|
||||
this.visitsRepo.listForPlace(tripId, placeId),
|
||||
@@ -80,7 +86,7 @@ export class TourChangePreviewService {
|
||||
changes,
|
||||
);
|
||||
if (outcome.kind === 'refused') {
|
||||
throw new DomainError(422, refusalText(outcome.refused), {
|
||||
throw new DomainError(422, refusalText(outcome.refused, layFor.action), {
|
||||
code: TOUR_CHANGE_REFUSED,
|
||||
details: { refused: outcome.refused },
|
||||
});
|
||||
@@ -89,7 +95,7 @@ export class TourChangePreviewService {
|
||||
}
|
||||
|
||||
async prepare(tripId: number, placeId: number, userId: number, { changes }: TourChangeRequest) {
|
||||
const { tour, waypoints, outcome } = await this.lay(tripId, placeId, changes);
|
||||
const { tour, waypoints, outcome } = await this.lay(tripId, placeId, changes, { action: 'prepared' });
|
||||
const [wayBefore, wayAfter] = await Promise.all([
|
||||
this.footWays.plan(userId, tripId, waypoints),
|
||||
this.footWays.plan(userId, tripId, outcome.points),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TourChangeSteps } from '../../db/entities/TourChangeSteps.entity';
|
||||
import { TOUR_CHANGE_STEP_BEFORE, TOUR_CHANGE_STEP_CHANGES } from '../../db/json-columns';
|
||||
import type { TourChangeStepsRepository } from '../../db/repositories/TourChangeSteps.repository';
|
||||
import type { TourChangeStepRow, TourChangeStepsRepository } from '../../db/repositories/TourChangeSteps.repository';
|
||||
import { encodeJson } from '../../utils/json-column';
|
||||
import { DomainError } from '../common/domain-error';
|
||||
import { UnitOfWork } from '../database/unit-of-work';
|
||||
@@ -63,6 +63,29 @@ function savedAnswer(detail: TourDetailResponse, step: SavedStep, { way, prepare
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The answer to a save of a proposal already saved, a save sent again after
|
||||
* its answer was lost, say: it writes nothing and points at the step the
|
||||
* first save recorded, so the assistant neither prepares the same changes
|
||||
* again on the new revision nor saves them twice.
|
||||
*/
|
||||
function alreadySavedAnswer(step: TourChangeStepRow) {
|
||||
const undone =
|
||||
step.undone_revision === null
|
||||
? ''
|
||||
: ` It was taken back since, at revision ${step.undone_revision}: ` +
|
||||
'prepare it anew only if the traveller asks for it again.';
|
||||
return {
|
||||
saved: true as const,
|
||||
already_saved: true as const,
|
||||
step: { id: step.id, base_revision: step.base_revision, revision: step.revision },
|
||||
undone_revision: step.undone_revision,
|
||||
message:
|
||||
`This very proposal was already saved as step ${step.id}, raising the tour to revision ${step.revision}, ` +
|
||||
`and this save wrote nothing. Do not prepare or save it again: read the tour with get_tour_plan.${undone}`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* MCP-T03: a prepared change the traveller confirmed, saved whole or not at
|
||||
* all through the write the planner's save uses. The save names the revision
|
||||
@@ -72,6 +95,8 @@ function savedAnswer(detail: TourDetailResponse, step: SavedStep, { way, prepare
|
||||
* the same rules: first to plan the way on foot, then once more inside the
|
||||
* write, so a station visited or a place gone in between refuses the save
|
||||
* instead of being written over, and a failure leaves the tour as it was.
|
||||
* A proposal is saved once: sent again once saved, it answers the step it
|
||||
* was saved as instead of the conflict of a tour that moved on.
|
||||
*
|
||||
* The write records the step: the proposal, both revisions, the changes and
|
||||
* the tour as it was before them (its line, its points, its measures), so it
|
||||
@@ -98,13 +123,18 @@ export class TourChangeSaveService {
|
||||
{ code: TOUR_CHANGE_NOT_PREPARED },
|
||||
);
|
||||
}
|
||||
const laid = await this.preview.lay(tripId, placeId, changes, preparedOn);
|
||||
const earlier = await this.stepsRepo.findSavedProposal(placeId, proposalId, preparedOn);
|
||||
if (earlier) return alreadySavedAnswer(earlier);
|
||||
const laid = await this.preview.lay(tripId, placeId, changes, { action: 'saved', preparedOn });
|
||||
const way = await this.footWays.plan(userId, tripId, laid.outcome.points);
|
||||
const changed: ChangedTour = { tour: laid.tour, points: laid.outcome.points, way, preparedOn };
|
||||
const write = await this.tours.routeWrite(writeOf(changed));
|
||||
const saved = await this.uow.transactional(async () => {
|
||||
// The same proposal saved by a save sent alongside this one while the way was planned.
|
||||
const savedMeanwhile = await this.stepsRepo.findSavedProposal(placeId, proposalId, preparedOn);
|
||||
if (savedMeanwhile) return { earlier: savedMeanwhile };
|
||||
// Laid once more on the tour as it is now: a tour moved on, a station done or a place gone refuses here.
|
||||
const now = await this.preview.lay(tripId, placeId, changes, preparedOn);
|
||||
const now = await this.preview.lay(tripId, placeId, changes, { action: 'saved', preparedOn });
|
||||
const before = await this.beforeState(tripId, placeId, now);
|
||||
const { revision, visitsDropped } = await this.tours.replaceRoute(tripId, placeId, write);
|
||||
const id = await this.stepsRepo.record({
|
||||
@@ -118,6 +148,7 @@ export class TourChangeSaveService {
|
||||
});
|
||||
return { step: { id, revision }, visitsDropped };
|
||||
});
|
||||
if ('earlier' in saved) return alreadySavedAnswer(saved.earlier);
|
||||
const detail = await this.tours.announceRouteUpdate(String(tripId), placeId, saved.visitsDropped);
|
||||
return savedAnswer(detail, saved.step, changed);
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ export class TourChangeMcp {
|
||||
@Tool({
|
||||
name: 'save_tour_change',
|
||||
description:
|
||||
"Save a change to a saved tour that prepare_tour_change prepared and the traveller confirmed, whole or not at all. Call it only after the traveller said yes to the proposal exactly as it was shown to them, with confirmed true, the same changes, expected_revision set to base.revision and the proposal_id of that proposal; anything else is refused and nothing is saved. Before anything is written the trip permission and the tour are checked again: a tour changed since the proposal was prepared is refused as a conflict (prepare the change again from get_tour_plan, never retry the old one), and a change the rules no longer allow (a station visited or skipped meanwhile, a place of the trip gone) is refused with every reason. The save writes the stations, their keys and the way on foot as the planner's save does, planned by the same router as the proposal: where the router gives no way the walk keeps its stations only (route_pending true, distance and walking time null) until the planner plans it. Stations kept keep their keys and their visits. The answer: saved, step (its id, base_revision and the revision the save raised the tour to; the step keeps the changes and the tour as it was before, so it can be taken back with prepare_tour_undo and undo_tour_change while it is the latest change, and nothing of the conversation), the tour as now saved (revision, distance_km, walking_minutes, route_pending), foot_way (planned, or why not) and the stations in order with their keys. Read the result with get_tour_plan. A new walk has no revision yet: create it with create_city_walk instead. Needs place editing permission on the trip.",
|
||||
"Save a change to a saved tour that prepare_tour_change prepared and the traveller confirmed, whole or not at all. Call it only after the traveller said yes to the proposal exactly as it was shown to them, with confirmed true, the same changes, expected_revision set to base.revision and the proposal_id of that proposal; anything else is refused and nothing is saved. Before anything is written the trip permission and the tour are checked again: a tour changed since the proposal was prepared is refused as a conflict (prepare the change again from get_tour_plan, never retry the old one), a proposal already saved, a save sent again after its answer was lost, writes nothing and answers already_saved with the step it was saved as (never prepare or save it again), and a change the rules no longer allow (a station visited or skipped meanwhile, a place of the trip gone) is refused with every reason. The save writes the stations, their keys and the way on foot as the planner's save does, planned by the same router as the proposal: where the router gives no way the walk keeps its stations only (route_pending true, distance and walking time null) until the planner plans it. Stations kept keep their keys and their visits. The answer: saved, step (its id, base_revision and the revision the save raised the tour to; the step keeps the changes and the tour as it was before, so it can be taken back with prepare_tour_undo and undo_tour_change while it is the latest change, and nothing of the conversation), the tour as now saved (revision, distance_km, walking_minutes, route_pending), foot_way (planned, or why not) and the stations in order with their keys. Read the result with get_tour_plan. A new walk has no revision yet: create it with create_city_walk instead. Needs place editing permission on the trip.",
|
||||
inputSchema: {
|
||||
tripId: idSchema,
|
||||
placeId: idSchema.describe('The place id of the tour, as list_tours gives it'),
|
||||
|
||||
@@ -120,8 +120,12 @@ describe('a change laid onto a saved walk (MCP-T02)', () => {
|
||||
],
|
||||
});
|
||||
if (outcome.kind !== 'refused') return;
|
||||
const text = refusalText(outcome.refused);
|
||||
const text = refusalText(outcome.refused, 'prepared');
|
||||
expect(text.split('\n')[0]).toBe('Nothing was prepared and nothing changed.');
|
||||
// A save refused by the same rules says nothing was saved, over the same reasons.
|
||||
const saved = refusalText(outcome.refused, 'saved').split('\n');
|
||||
expect(saved[0]).toBe('Nothing was saved and nothing changed.');
|
||||
expect(saved.slice(1)).toEqual(text.split('\n').slice(1));
|
||||
expect(text).toContain('Change 2 (move stop-end): the travellers already visited or skipped');
|
||||
expect(text).toContain('day ids 7 visited.');
|
||||
});
|
||||
|
||||
@@ -312,7 +312,7 @@ describe('TourChangeMcp.saveTourChange (MCP-T03)', () => {
|
||||
});
|
||||
const during = await refusal(saveAs(f.trip.id, f.walk.id, request, f.user.id));
|
||||
expect([during.getStatus(), during.code]).toEqual([422, TOUR_CHANGE_REFUSED]);
|
||||
expect(during.publicMessage).toMatch(/^Nothing was prepared and nothing changed\.\nChange 1 \(remove stop-park\)/);
|
||||
expect(during.publicMessage).toMatch(/^Nothing was saved and nothing changed\.\nChange 1 \(remove stop-park\)/);
|
||||
expect((await findRow(orm(), Tours, { place: f.walk.id }))?.revision).toBe(3);
|
||||
expect(await countRows(orm(), TourWaypoints, { place: f.walk.id })).toBe(4);
|
||||
expect(await countRows(orm(), TourChangeSteps, {})).toBe(0);
|
||||
@@ -321,6 +321,7 @@ describe('TourChangeMcp.saveTourChange (MCP-T03)', () => {
|
||||
route.mockClear();
|
||||
const after = await refusal(saveAs(f.trip.id, f.walk.id, request, f.user.id));
|
||||
expect(after.code).toBe(TOUR_CHANGE_REFUSED);
|
||||
expect(after.publicMessage).toMatch(/^Nothing was saved and nothing changed\./);
|
||||
expect(route).not.toHaveBeenCalled();
|
||||
expect(t.realtime.broadcastMock).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -394,7 +395,15 @@ describe('TourChangeMcp.saveTourChange (MCP-T03)', () => {
|
||||
false,
|
||||
{ group: 'trips', mode: 'write' },
|
||||
]);
|
||||
const terms = ['confirmed', 'expected_revision', 'proposal_id', 'conflict', 'get_tour_plan', 'create_city_walk'];
|
||||
const terms = [
|
||||
'confirmed',
|
||||
'expected_revision',
|
||||
'proposal_id',
|
||||
'conflict',
|
||||
'already_saved',
|
||||
'get_tour_plan',
|
||||
'create_city_walk',
|
||||
];
|
||||
for (const term of terms) expect(options.description).toContain(term);
|
||||
// The fields are zod schemas from @trek/shared, read here by what they answer.
|
||||
const shape = options.inputSchema as Record<string, { safeParse(value: unknown): { success: boolean } }>;
|
||||
@@ -406,4 +415,58 @@ describe('TourChangeMcp.saveTourChange (MCP-T03)', () => {
|
||||
expect(await options.when?.(ctx(1), { addons: { isAddonEnabled } })).toBe(true);
|
||||
expect(isAddonEnabled).toHaveBeenCalledWith('tours');
|
||||
});
|
||||
|
||||
it('TCHG-SAVE-010: a save sent again once saved writes nothing and answers the step it was saved as', async () => {
|
||||
const f = await fixture();
|
||||
const { request } = await prepared(f.trip.id, f.walk.id, stopAndSwap(f.bakery.id), f.user.id);
|
||||
const first = read(await saveAs(f.trip.id, f.walk.id, request, f.user.id));
|
||||
const before = await untouched(f.walk.id);
|
||||
route.mockClear();
|
||||
t.realtime.broadcastMock.mockClear();
|
||||
|
||||
const again = read(await saveAs(f.trip.id, f.walk.id, request, f.user.id));
|
||||
|
||||
expect(again).toEqual({
|
||||
saved: true,
|
||||
already_saved: true,
|
||||
step: { id: first.step.id, base_revision: 3, revision: 4 },
|
||||
undone_revision: null,
|
||||
message: expect.stringContaining(
|
||||
`This very proposal was already saved as step ${first.step.id}, raising the tour to revision 4`,
|
||||
),
|
||||
});
|
||||
expect(again.message).toContain('Do not prepare or save it again');
|
||||
// Not the conflict of a tour that moved on, which would send the assistant to prepare it anew.
|
||||
expect(again.message).not.toContain('prepare the change anew');
|
||||
expect(route).not.toHaveBeenCalled();
|
||||
expect(t.realtime.broadcastMock).not.toHaveBeenCalled();
|
||||
expect(await untouched(f.walk.id)).toEqual(before);
|
||||
|
||||
// Taken back since, it still is the step it was saved as, and says so.
|
||||
await updateRows(orm(), TourChangeSteps, { id: first.step.id }, { undone_revision: 5 });
|
||||
const undone = read(await saveAs(f.trip.id, f.walk.id, request, f.user.id));
|
||||
expect([undone.already_saved, undone.undone_revision]).toEqual([true, 5]);
|
||||
expect(undone.message).toContain('It was taken back since, at revision 5');
|
||||
expect(await countRows(orm(), TourChangeSteps, {})).toBe(1);
|
||||
});
|
||||
|
||||
it('TCHG-SAVE-011: of two saves of one proposal at once, the one that writes second writes nothing', async () => {
|
||||
const f = await fixture();
|
||||
const { request } = await prepared(f.trip.id, f.walk.id, stopAndSwap(f.bakery.id), f.user.id);
|
||||
const firstSave: { answer?: { saved: boolean; step: { id: number } } } = {};
|
||||
// The same save sent again lands while this one plans its way, and commits first.
|
||||
route.mockImplementationOnce(async (...args: Parameters<typeof routedOnFoot>) => {
|
||||
firstSave.answer = read(await saveAs(f.trip.id, f.walk.id, request, f.user.id));
|
||||
return await routedOnFoot(...args);
|
||||
});
|
||||
|
||||
const second = read(await saveAs(f.trip.id, f.walk.id, request, f.user.id));
|
||||
|
||||
expect(firstSave.answer).toMatchObject({ saved: true, step: { base_revision: 3, revision: 4 } });
|
||||
expect(second).toMatchObject({ already_saved: true, step: { id: firstSave.answer?.step.id, revision: 4 } });
|
||||
expect(await countRows(orm(), TourChangeSteps, {})).toBe(1);
|
||||
expect((await findRow(orm(), Tours, { place: f.walk.id }))?.revision).toBe(4);
|
||||
const points = await findRows(orm(), TourWaypoints, { place: f.walk.id }, { sequence: 'asc' });
|
||||
expect(points.map((point) => point.name)).toEqual(['Gate', 'Cathedral', 'Market', 'Bakery', 'Beer garden']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -249,7 +249,7 @@ The saved tours of a trip, as the Tours list and the tour detail show them, how
|
||||
| `propose_city_walk` | Proposes a city walk from a briefing without saving anything: start (and end), the time frame on a day, the themes (`culture`, `parks`, `food`, `mixed`), the stations the traveller wished for, the places of the trip it may use and fixed appointments. Places for the themes come from the same search as `search_tour_pois`, up to 8, each with its source, theme and category; nothing is made up. Answers the stations in order with the walking distance and time from the one before, the arrival and leave times, the stay where the traveller gave it (unknown otherwise, which makes later arrivals the earliest possible), and the opening hours checked against the arrival on that day. The order is chosen on estimates; the walk in that order is then planned on foot by the router TREK uses for road trips (the traveller's own routing server where they set one), whose legs give the distances and times. Where it gives no way, because the router cannot be reached or a station lies more than 300 m off every way, the times are estimates over the straight distance with an allowance for streets at 4.5 km/h, and the answer says so and why. Walking and stays are kept apart, a complete total only when every stay is known, with the minutes the frame leaves for the stays not known. Lists free time, the wishes and appointments that cannot be kept as asked, what was left out and why, and `stations_for_create` for `create_city_walk` | `trips:read` |
|
||||
| `create_city_walk` | Creates a city walk from stations the traveller confirmed (for instance `stations_for_create` of `propose_city_walk`), through the same creation the planner's save uses: a key for every station, its name, stay, importance, protection, opening hours and the place of the trip it stands for. The way on foot is planned by the same router as for the proposal, and the walk is saved with it, its distance and its walking time. Where the router gives no way, the walk is saved with its stations only (`route_pending`), says why, and has no line, distance or walking time until someone opens it in the planner, where TREK plans the way on foot and the walk can be saved with it. Needs `place_edit` on the trip | `trips:write` |
|
||||
| `prepare_tour_change` | Prepares a change to a saved city walk for the traveller to review, without saving anything: no tour, station, visit or day plan changes, and turning it down keeps the plan as it was. The changes apply in order and name the saved stations by their keys: `add` (a new station after a station; a spontaneous stop on the way is an add after the station the traveller is at), `remove`, `replace` and `move`. A protected station, or one visited or skipped on any day of the walk, is never removed, replaced or moved, nothing is put before the last station already done, and a leg drawn by hand stays as drawn, the closing leg of a loop included; such a change is refused, and with it the whole proposal, naming every change and why. Another type of tour, or a walk that is a recorded track without stations of its own, is refused as well: so far only a city walk takes prepared changes. The answer names the tour and the `revision` it was prepared on (a save has to name it, and a tour changed since is refused rather than overwritten), the stations after the change with what changed each and where it stood before, what was removed, the way on foot before and after planned by the same router TREK uses for city walks and its change in metres and minutes (the distance and walking time stored with the walk given apart), the stays as far as known before and after, every leg after the change with the new ones marked, `data_gaps` for what could not be worked out (no way from the router, stays not known, no arrival times, shaping points dropped because the leg they steered changed), a warning for a station of importance must removed or replaced, each new station that is not yet a place of the trip as `create_place` takes it to keep it as a place instead (a station linked to a place of the trip is left out), whether the traveller may change the trip's places at all (`can_save`) and the `proposal_id` a save of it names | `trips:read` |
|
||||
| `save_tour_change` | Saves a change `prepare_tour_change` prepared, once the traveller said yes to it exactly as shown: the same changes, the `revision` it was prepared on, its `proposal_id` and `confirmed` set to true. Anything else is refused and nothing is saved. The trip permission and the tour are checked again before anything is written: a tour changed since the proposal was prepared is refused as a conflict rather than overwritten (prepare the change again), and a change the rules no longer allow, a station visited or skipped meanwhile or a place of the trip gone, is refused with every reason. The change is saved whole or not at all, through the same write the planner's save uses: the stations in their order, the stations kept keep their keys and their visits, and the way on foot is planned by the same router as the proposal; where it gives no way the walk keeps its stations only (`route_pending`) until someone opens it in the planner. Every save is kept as a step of the tour with the revision it was prepared on and the one it raised the tour to, the changes and the tour as it was before them, so it can be followed and taken back with `undo_tour_change`; nothing of the conversation is kept. A new walk has no revision yet and is made with `create_city_walk`. Needs `place_edit` on the trip | `trips:write` |
|
||||
| `save_tour_change` | Saves a change `prepare_tour_change` prepared, once the traveller said yes to it exactly as shown: the same changes, the `revision` it was prepared on, its `proposal_id` and `confirmed` set to true. Anything else is refused and nothing is saved. The trip permission and the tour are checked again before anything is written: a tour changed since the proposal was prepared is refused as a conflict rather than overwritten (prepare the change again), a proposal already saved, sent again because its answer was lost, writes nothing and answers `already_saved` with the step it was saved as, and a change the rules no longer allow, a station visited or skipped meanwhile or a place of the trip gone, is refused with every reason. The change is saved whole or not at all, through the same write the planner's save uses: the stations in their order, the stations kept keep their keys and their visits, and the way on foot is planned by the same router as the proposal; where it gives no way the walk keeps its stations only (`route_pending`) until someone opens it in the planner. Every save is kept as a step of the tour with the revision it was prepared on and the one it raised the tour to, the changes and the tour as it was before them, so it can be followed and taken back with `undo_tour_change`; nothing of the conversation is kept. A new walk has no revision yet and is made with `create_city_walk`. Needs `place_edit` on the trip | `trips:write` |
|
||||
| `list_tour_change_steps` | Lists the changes saved to a tour with `save_tour_change`, the latest first: each step with when it was saved, the revision it was prepared on and the one it raised the tour to, and the changes as confirmed. `standing` says whether it can be taken back: `latest` (the tour's latest change), `undone` (taken back already, with when and the revision that raised the tour to), `later_change` (something was saved on top of it since, so taking it back would lose that) or `no_earlier_state` (its record of the tour before cannot be read). Edits saved in the planner are no steps, and a draft in the planner is never seen | `trips:read` |
|
||||
| `prepare_tour_undo` | Shows what taking back a saved step would do, without changing anything: the step and its changes, the tour now (distance and walking time as stored, each station with whether it was visited or skipped on a day) and the tour exactly as it was before the step (its stations, each kept or coming back with where it stands now, and the distance and walking time stored then), the stations taking back removes, and the `step_id` and `expected_revision` that `undo_tour_change` names. Only the tour's latest change can be taken back; a step something was saved on top of since is refused as a conflict, so no later edit is lost, and so is one that would remove a station visited or skipped since or bring back a station whose place of the trip is gone. Visits of stations that stay, the days and the tour's name and notes are not touched. Not the planner's undo: discarding a draft in the planner never changes a saved tour. Also says whether the traveller may change the trip's places (`can_undo`) | `trips:read` |
|
||||
| `undo_tour_change` | Takes back a saved step once the traveller said yes to the preview of exactly that step: `step_id` and `expected_revision` as `prepare_tour_undo` gave them, and `confirmed` set to true. Everything is checked again before anything is written, and a tour changed since the preview, a step no longer the latest change or taken back already, or a station visited or skipped meanwhile that would go, refuses it and leaves the tour as it is. Restores the tour whole or not at all, exactly as it was before the step: its line, its stations with their keys and the distance and walking time stored then. A step is taken back once, and taking it back raises the tour's revision like any other save. Needs `place_edit` on the trip | `trips:write` |
|
||||
|
||||
Reference in New Issue
Block a user