feat(timeline): add description field to TimelineEntryDTO (REQ-001)
Surfaces the existing TimelineEvent.description through the list DTO so curated event notes reach the read view. Null for letters and derived entries; populated from ev.getDescription() in mapEvent() only. Refs #844 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,11 @@ import java.util.UUID;
|
|||||||
* entries. Deliberately NOT {@code @Schema(requiredMode = REQUIRED)} so the generated TypeScript
|
* entries. Deliberately NOT {@code @Schema(requiredMode = REQUIRED)} so the generated TypeScript
|
||||||
* type stays optional.
|
* type stays optional.
|
||||||
*
|
*
|
||||||
|
* <p><b>Event description ({@code description}):</b> curator-authored context note for a curated
|
||||||
|
* {@link Kind#EVENT} entry (#844). Populated from {@link TimelineEvent#getDescription()} — null
|
||||||
|
* for {@link Kind#LETTER} and derived entries. Deliberately NOT
|
||||||
|
* {@code @Schema(requiredMode = REQUIRED)} so the generated TypeScript type stays optional.
|
||||||
|
*
|
||||||
* <p>Callers of {@code TimelineEventService.assembleDerivedEvents()} must independently enforce
|
* <p>Callers of {@code TimelineEventService.assembleDerivedEvents()} must independently enforce
|
||||||
* {@code READ_ALL} authorization before invoking that method (see ADR-043).
|
* {@code READ_ALL} authorization before invoking that method (see ADR-043).
|
||||||
*/
|
*/
|
||||||
@@ -55,6 +60,7 @@ public record TimelineEntryDTO(
|
|||||||
UUID rootTagId,
|
UUID rootTagId,
|
||||||
String rootTagName,
|
String rootTagName,
|
||||||
String rootTagColor,
|
String rootTagColor,
|
||||||
UUID linkedEventId
|
UUID linkedEventId,
|
||||||
|
String description
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -267,7 +267,7 @@ public class TimelineEventService {
|
|||||||
p.getBirthDate(), null,
|
p.getBirthDate(), null,
|
||||||
p.getDisplayName(), EventType.PERSONAL,
|
p.getDisplayName(), EventType.PERSONAL,
|
||||||
null, null, List.of(p.getId()), DerivedEventType.BIRTH,
|
null, null, List.of(p.getId()), DerivedEventType.BIRTH,
|
||||||
null, null, null, null))
|
null, null, null, null, null))
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,7 +279,7 @@ public class TimelineEventService {
|
|||||||
p.getDeathDate(), null,
|
p.getDeathDate(), null,
|
||||||
p.getDisplayName(), EventType.PERSONAL,
|
p.getDisplayName(), EventType.PERSONAL,
|
||||||
null, null, List.of(p.getId()), DerivedEventType.DEATH,
|
null, null, List.of(p.getId()), DerivedEventType.DEATH,
|
||||||
null, null, null, null))
|
null, null, null, null, null))
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,7 +304,7 @@ public class TimelineEventService {
|
|||||||
null, null,
|
null, null,
|
||||||
List.of(r.getPerson().getId(), r.getRelatedPerson().getId()),
|
List.of(r.getPerson().getId(), r.getRelatedPerson().getId()),
|
||||||
DerivedEventType.MARRIAGE,
|
DerivedEventType.MARRIAGE,
|
||||||
null, null, null, null));
|
null, null, null, null, null));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -238,7 +238,8 @@ public class TimelineService {
|
|||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null
|
null,
|
||||||
|
ev.getDescription()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,7 +263,8 @@ public class TimelineService {
|
|||||||
root == null ? null : root.id(),
|
root == null ? null : root.id(),
|
||||||
root == null ? null : root.name(),
|
root == null ? null : root.name(),
|
||||||
root == null ? null : root.color(),
|
root == null ? null : root.color(),
|
||||||
eventByDocId.get(doc.getId())
|
eventByDocId.get(doc.getId()),
|
||||||
|
null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,28 @@ class TimelineControllerTest {
|
|||||||
.andExpect(jsonPath("$.undated").isArray());
|
.andExpect(jsonPath("$.undated").isArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── REQ-001: description field serialised ───────────────────────────────
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@org.springframework.security.test.context.support.WithMockUser(authorities = "READ_ALL")
|
||||||
|
void timelineIncludesEventDescription() throws Exception {
|
||||||
|
// REQ-001 (controller slice): a curated event entry with description "Kontext" is
|
||||||
|
// serialised into the timeline response at the correct JSON path.
|
||||||
|
var entry = new TimelineEntryDTO(Kind.EVENT, org.raddatz.familienarchiv.document.DatePrecision.DAY,
|
||||||
|
false, "", "",
|
||||||
|
java.time.LocalDate.of(1914, 8, 1), null, "Kriegsbeginn",
|
||||||
|
EventType.HISTORICAL, UUID.randomUUID(), null, List.of(), null,
|
||||||
|
null, null, null, null, "Kontext");
|
||||||
|
when(timelineService.assemble(any()))
|
||||||
|
.thenReturn(new TimelineDTO(
|
||||||
|
List.of(new TimelineYearDTO(1914, List.of(entry))),
|
||||||
|
List.of()));
|
||||||
|
|
||||||
|
mockMvc.perform(get("/api/timeline"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.years[0].entries[0].description", is("Kontext")));
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Parameter binding ────────────────────────────────────────────────────
|
// ─── Parameter binding ────────────────────────────────────────────────────
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -69,10 +69,10 @@ class TimelineServiceTest {
|
|||||||
UUID id2 = UUID.fromString("00000000-0000-0000-0000-000000000002");
|
UUID id2 = UUID.fromString("00000000-0000-0000-0000-000000000002");
|
||||||
var e1 = new TimelineEntryDTO(Kind.LETTER, DatePrecision.DAY, false, "", "",
|
var e1 = new TimelineEntryDTO(Kind.LETTER, DatePrecision.DAY, false, "", "",
|
||||||
LocalDate.of(1914, 7, 28), null, "Same Title", null, null, id1, List.of(), null,
|
LocalDate.of(1914, 7, 28), null, "Same Title", null, null, id1, List.of(), null,
|
||||||
null, null, null, null);
|
null, null, null, null, null);
|
||||||
var e2 = new TimelineEntryDTO(Kind.LETTER, DatePrecision.DAY, false, "", "",
|
var e2 = new TimelineEntryDTO(Kind.LETTER, DatePrecision.DAY, false, "", "",
|
||||||
LocalDate.of(1914, 7, 28), null, "Same Title", null, null, id2, List.of(), null,
|
LocalDate.of(1914, 7, 28), null, "Same Title", null, null, id2, List.of(), null,
|
||||||
null, null, null, null);
|
null, null, null, null, null);
|
||||||
|
|
||||||
var sorted = List.of(e2, e1).stream()
|
var sorted = List.of(e2, e1).stream()
|
||||||
.sorted(TimelineService.WITHIN_BAND_ORDER)
|
.sorted(TimelineService.WITHIN_BAND_ORDER)
|
||||||
@@ -511,6 +511,53 @@ class TimelineServiceTest {
|
|||||||
verify(tagService, times(1)).resolveRootTags(anyList());
|
verify(tagService, times(1)).resolveRootTags(anyList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── event description (#844, REQ-001) ───────────────────────────────────
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void mapEvent_populates_description_from_event() {
|
||||||
|
// REQ-001: a curated event with a description surfaces it on the assembled entry.
|
||||||
|
TimelineEvent ev = TimelineEvent.builder().id(UUID.randomUUID())
|
||||||
|
.title("Kriegsbeginn").type(EventType.HISTORICAL)
|
||||||
|
.eventDate(LocalDate.of(1914, 8, 1)).precision(DatePrecision.DAY)
|
||||||
|
.description("Kontext")
|
||||||
|
.build();
|
||||||
|
when(eventRepository.findAll()).thenReturn(List.of(ev));
|
||||||
|
when(timelineEventService.assembleDerivedEvents()).thenReturn(List.of());
|
||||||
|
when(documentService.getAllForTimeline()).thenReturn(List.of());
|
||||||
|
|
||||||
|
TimelineDTO result = timelineService.assemble(noFilters());
|
||||||
|
|
||||||
|
TimelineEntryDTO entry = result.years().get(0).entries().get(0);
|
||||||
|
assertThat(entry.description()).isEqualTo("Kontext");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void mapEvent_leaves_description_null_when_event_has_none() {
|
||||||
|
// REQ-001: an event without a description → null on the entry.
|
||||||
|
TimelineEvent ev = TimelineEvent.builder().id(UUID.randomUUID())
|
||||||
|
.title("Ereignis").type(EventType.PERSONAL)
|
||||||
|
.eventDate(LocalDate.of(1920, 1, 1)).precision(DatePrecision.YEAR)
|
||||||
|
.build();
|
||||||
|
when(eventRepository.findAll()).thenReturn(List.of(ev));
|
||||||
|
when(timelineEventService.assembleDerivedEvents()).thenReturn(List.of());
|
||||||
|
when(documentService.getAllForTimeline()).thenReturn(List.of());
|
||||||
|
|
||||||
|
TimelineEntryDTO entry = timelineService.assemble(noFilters()).years().get(0).entries().get(0);
|
||||||
|
assertThat(entry.description()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void mapDocument_leaves_description_null_for_letter() {
|
||||||
|
// REQ-001: LETTER entries carry null description, regardless of any document fields.
|
||||||
|
Document doc = docWithDate(LocalDate.of(1916, 3, 1), DatePrecision.MONTH);
|
||||||
|
when(eventRepository.findAll()).thenReturn(List.of());
|
||||||
|
when(timelineEventService.assembleDerivedEvents()).thenReturn(List.of());
|
||||||
|
when(documentService.getAllForTimeline()).thenReturn(List.of(doc));
|
||||||
|
|
||||||
|
TimelineEntryDTO entry = onlyLetterEntry(timelineService.assemble(noFilters()));
|
||||||
|
assertThat(entry.description()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
// ─── letter→event link (#850, REQ-009) ───────────────────────────────────
|
// ─── letter→event link (#850, REQ-009) ───────────────────────────────────
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -623,7 +670,7 @@ class TimelineServiceTest {
|
|||||||
private static TimelineEntryDTO letter(LocalDate date, DatePrecision precision, String title) {
|
private static TimelineEntryDTO letter(LocalDate date, DatePrecision precision, String title) {
|
||||||
return new TimelineEntryDTO(Kind.LETTER, precision, false, "", "",
|
return new TimelineEntryDTO(Kind.LETTER, precision, false, "", "",
|
||||||
date, null, title, null, null, UUID.randomUUID(), List.of(), null,
|
date, null, title, null, null, UUID.randomUUID(), List.of(), null,
|
||||||
null, null, null, null);
|
null, null, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Document docWithDate(LocalDate date, DatePrecision precision) {
|
private static Document docWithDate(LocalDate date, DatePrecision precision) {
|
||||||
|
|||||||
Reference in New Issue
Block a user