diff --git a/backend/src/main/java/org/raddatz/familienarchiv/repository/CompletionStatsRow.java b/backend/src/main/java/org/raddatz/familienarchiv/repository/CompletionStatsRow.java new file mode 100644 index 00000000..f1680df5 --- /dev/null +++ b/backend/src/main/java/org/raddatz/familienarchiv/repository/CompletionStatsRow.java @@ -0,0 +1,8 @@ +package org.raddatz.familienarchiv.repository; + +import java.util.UUID; + +public interface CompletionStatsRow { + UUID getDocumentId(); + int getCompletionPercentage(); +} diff --git a/backend/src/main/java/org/raddatz/familienarchiv/repository/TranscriptionBlockRepository.java b/backend/src/main/java/org/raddatz/familienarchiv/repository/TranscriptionBlockRepository.java index c88830ad..1bf2d108 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/repository/TranscriptionBlockRepository.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/repository/TranscriptionBlockRepository.java @@ -5,12 +5,24 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.UUID; public interface TranscriptionBlockRepository extends JpaRepository { + @Query(value = """ + SELECT + b.document_id AS documentId, + ROUND(COUNT(*) FILTER (WHERE b.reviewed = true) * 100.0 / COUNT(*))::int AS completionPercentage + FROM transcription_blocks b + WHERE b.document_id IN :documentIds + GROUP BY b.document_id + """, nativeQuery = true) + List findCompletionStatsForDocuments( + @Param("documentIds") Collection documentIds); + List findByDocumentIdOrderBySortOrderAsc(UUID documentId); Optional findByIdAndDocumentId(UUID id, UUID documentId); diff --git a/backend/src/test/java/org/raddatz/familienarchiv/repository/TranscriptionBlockRepositoryIntegrationTest.java b/backend/src/test/java/org/raddatz/familienarchiv/repository/TranscriptionBlockRepositoryIntegrationTest.java new file mode 100644 index 00000000..dde0c089 --- /dev/null +++ b/backend/src/test/java/org/raddatz/familienarchiv/repository/TranscriptionBlockRepositoryIntegrationTest.java @@ -0,0 +1,104 @@ +package org.raddatz.familienarchiv.repository; + +import org.junit.jupiter.api.Test; +import org.raddatz.familienarchiv.PostgresContainerConfig; +import org.raddatz.familienarchiv.config.FlywayConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest; +import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.jdbc.Sql; + +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@Import({PostgresContainerConfig.class, FlywayConfig.class}) +class TranscriptionBlockRepositoryIntegrationTest { + + static final UUID DOC_A = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"); + static final UUID DOC_B = UUID.fromString("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"); + static final UUID ANN_A = UUID.fromString("cccccccc-cccc-cccc-cccc-cccccccccccc"); + static final UUID ANN_B = UUID.fromString("dddddddd-dddd-dddd-dddd-dddddddddddd"); + + @Autowired TranscriptionBlockRepository repository; + + @Test + @Sql(statements = { + "INSERT INTO documents (id, title, original_filename, status) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'Doc A', 'a.pdf', 'PLACEHOLDER')", + "INSERT INTO document_annotations (id, document_id, page_number, x, y, width, height, color) VALUES ('cccccccc-cccc-cccc-cccc-cccccccccccc', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 1, 0, 0, 1, 1, '#fff')", + "INSERT INTO transcription_blocks (annotation_id, document_id, sort_order, reviewed) VALUES ('cccccccc-cccc-cccc-cccc-cccccccccccc', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 0, true)", + "INSERT INTO transcription_blocks (annotation_id, document_id, sort_order, reviewed) VALUES ('cccccccc-cccc-cccc-cccc-cccccccccccc', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 1, true)" + }) + void findCompletionStats_returns_100_when_all_blocks_reviewed() { + List rows = repository.findCompletionStatsForDocuments(List.of(DOC_A)); + + assertThat(rows).hasSize(1); + assertThat(rows.get(0).getDocumentId()).isEqualTo(DOC_A); + assertThat(rows.get(0).getCompletionPercentage()).isEqualTo(100); + } + + @Test + @Sql(statements = { + "INSERT INTO documents (id, title, original_filename, status) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'Doc A', 'a.pdf', 'PLACEHOLDER')", + "INSERT INTO document_annotations (id, document_id, page_number, x, y, width, height, color) VALUES ('cccccccc-cccc-cccc-cccc-cccccccccccc', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 1, 0, 0, 1, 1, '#fff')", + "INSERT INTO transcription_blocks (annotation_id, document_id, sort_order, reviewed) VALUES ('cccccccc-cccc-cccc-cccc-cccccccccccc', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 0, false)", + "INSERT INTO transcription_blocks (annotation_id, document_id, sort_order, reviewed) VALUES ('cccccccc-cccc-cccc-cccc-cccccccccccc', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 1, false)" + }) + void findCompletionStats_returns_0_when_no_blocks_reviewed() { + List rows = repository.findCompletionStatsForDocuments(List.of(DOC_A)); + + assertThat(rows).hasSize(1); + assertThat(rows.get(0).getCompletionPercentage()).isEqualTo(0); + } + + @Test + @Sql(statements = { + "INSERT INTO documents (id, title, original_filename, status) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'Doc A', 'a.pdf', 'PLACEHOLDER')" + }) + void findCompletionStats_returns_empty_when_document_has_no_blocks() { + List rows = repository.findCompletionStatsForDocuments(List.of(DOC_A)); + + assertThat(rows).isEmpty(); + } + + @Test + @Sql(statements = { + "INSERT INTO documents (id, title, original_filename, status) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'Doc A', 'a.pdf', 'PLACEHOLDER')", + "INSERT INTO document_annotations (id, document_id, page_number, x, y, width, height, color) VALUES ('cccccccc-cccc-cccc-cccc-cccccccccccc', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 1, 0, 0, 1, 1, '#fff')", + "INSERT INTO transcription_blocks (annotation_id, document_id, sort_order, reviewed) VALUES ('cccccccc-cccc-cccc-cccc-cccccccccccc', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 0, true)", + "INSERT INTO transcription_blocks (annotation_id, document_id, sort_order, reviewed) VALUES ('cccccccc-cccc-cccc-cccc-cccccccccccc', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 1, false)", + "INSERT INTO transcription_blocks (annotation_id, document_id, sort_order, reviewed) VALUES ('cccccccc-cccc-cccc-cccc-cccccccccccc', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 2, false)", + "INSERT INTO transcription_blocks (annotation_id, document_id, sort_order, reviewed) VALUES ('cccccccc-cccc-cccc-cccc-cccccccccccc', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 3, false)" + }) + void findCompletionStats_rounds_partial_completion_correctly() { + List rows = repository.findCompletionStatsForDocuments(List.of(DOC_A)); + + assertThat(rows).hasSize(1); + assertThat(rows.get(0).getCompletionPercentage()).isEqualTo(25); + } + + @Test + @Sql(statements = { + "INSERT INTO documents (id, title, original_filename, status) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'Doc A', 'a.pdf', 'PLACEHOLDER')", + "INSERT INTO documents (id, title, original_filename, status) VALUES ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'Doc B', 'b.pdf', 'PLACEHOLDER')", + "INSERT INTO document_annotations (id, document_id, page_number, x, y, width, height, color) VALUES ('cccccccc-cccc-cccc-cccc-cccccccccccc', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 1, 0, 0, 1, 1, '#fff')", + "INSERT INTO document_annotations (id, document_id, page_number, x, y, width, height, color) VALUES ('dddddddd-dddd-dddd-dddd-dddddddddddd', 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 1, 0, 0, 1, 1, '#fff')", + "INSERT INTO transcription_blocks (annotation_id, document_id, sort_order, reviewed) VALUES ('cccccccc-cccc-cccc-cccc-cccccccccccc', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 0, true)", + "INSERT INTO transcription_blocks (annotation_id, document_id, sort_order, reviewed) VALUES ('dddddddd-dddd-dddd-dddd-dddddddddddd', 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 0, false)" + }) + void findCompletionStats_handles_multiple_documents_in_one_call() { + List rows = repository.findCompletionStatsForDocuments(List.of(DOC_A, DOC_B)); + + Map byDoc = rows.stream() + .collect(Collectors.toMap(CompletionStatsRow::getDocumentId, CompletionStatsRow::getCompletionPercentage)); + + assertThat(byDoc).containsEntry(DOC_A, 100); + assertThat(byDoc).containsEntry(DOC_B, 0); + } +}