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
|
||||
* 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
|
||||
* {@code READ_ALL} authorization before invoking that method (see ADR-043).
|
||||
*/
|
||||
@@ -55,6 +60,7 @@ public record TimelineEntryDTO(
|
||||
UUID rootTagId,
|
||||
String rootTagName,
|
||||
String rootTagColor,
|
||||
UUID linkedEventId
|
||||
UUID linkedEventId,
|
||||
String description
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -267,7 +267,7 @@ public class TimelineEventService {
|
||||
p.getBirthDate(), null,
|
||||
p.getDisplayName(), EventType.PERSONAL,
|
||||
null, null, List.of(p.getId()), DerivedEventType.BIRTH,
|
||||
null, null, null, null))
|
||||
null, null, null, null, null))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@@ -279,7 +279,7 @@ public class TimelineEventService {
|
||||
p.getDeathDate(), null,
|
||||
p.getDisplayName(), EventType.PERSONAL,
|
||||
null, null, List.of(p.getId()), DerivedEventType.DEATH,
|
||||
null, null, null, null))
|
||||
null, null, null, null, null))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@@ -304,7 +304,7 @@ public class TimelineEventService {
|
||||
null, null,
|
||||
List.of(r.getPerson().getId(), r.getRelatedPerson().getId()),
|
||||
DerivedEventType.MARRIAGE,
|
||||
null, null, null, null));
|
||||
null, null, null, null, null));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
||||
@@ -238,7 +238,8 @@ public class TimelineService {
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
ev.getDescription()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -262,7 +263,8 @@ public class TimelineService {
|
||||
root == null ? null : root.id(),
|
||||
root == null ? null : root.name(),
|
||||
root == null ? null : root.color(),
|
||||
eventByDocId.get(doc.getId())
|
||||
eventByDocId.get(doc.getId()),
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -74,6 +74,28 @@ class TimelineControllerTest {
|
||||
.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 ────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
|
||||
@@ -69,10 +69,10 @@ class TimelineServiceTest {
|
||||
UUID id2 = UUID.fromString("00000000-0000-0000-0000-000000000002");
|
||||
var e1 = new TimelineEntryDTO(Kind.LETTER, DatePrecision.DAY, false, "", "",
|
||||
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, "", "",
|
||||
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()
|
||||
.sorted(TimelineService.WITHIN_BAND_ORDER)
|
||||
@@ -511,6 +511,53 @@ class TimelineServiceTest {
|
||||
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) ───────────────────────────────────
|
||||
|
||||
@Test
|
||||
@@ -623,7 +670,7 @@ class TimelineServiceTest {
|
||||
private static TimelineEntryDTO letter(LocalDate date, DatePrecision precision, String title) {
|
||||
return new TimelineEntryDTO(Kind.LETTER, precision, false, "", "",
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user