diff --git a/datashare-api/src/main/java/org/icij/datashare/Repository.java b/datashare-api/src/main/java/org/icij/datashare/Repository.java index 71758fc97..ccc964417 100644 --- a/datashare-api/src/main/java/org/icij/datashare/Repository.java +++ b/datashare-api/src/main/java/org/icij/datashare/Repository.java @@ -19,6 +19,9 @@ public interface Repository { boolean star(User user, String documentId); boolean unstar(User user, String documentId); List getStarredDocuments(User user); + boolean markRead(User user, String documentId); + boolean unmarkRead(User user, String documentId); + List getMarkedReadDocumentUsers(String documentId); // project related List getDocumentsNotTaggedWithPipeline(Project project, Pipeline.Type type); @@ -26,6 +29,9 @@ public interface Repository { int star(Project project, User user, List documentIds); int unstar(Project project, User user, List documentIds); List getStarredDocuments(Project project, User user); + int markRead(Project project, User user, List documentIds); + int unmarkRead(Project project, User user, List documentIds); + List getMarkedReadDocumentUsers(Project project, String documentId); boolean tag(Project prj, String documentId, Tag... tags); boolean untag(Project prj, String documentId, Tag... tags); diff --git a/datashare-app/src/main/java/org/icij/datashare/web/DocumentResource.java b/datashare-app/src/main/java/org/icij/datashare/web/DocumentResource.java index a031169d5..d3999b8d1 100644 --- a/datashare-app/src/main/java/org/icij/datashare/web/DocumentResource.java +++ b/datashare-app/src/main/java/org/icij/datashare/web/DocumentResource.java @@ -298,6 +298,75 @@ public Payload unstarDocument(final String docId, Context context) { return repository.unstar((HashMapUser)context.currentUser(), docId) ? Payload.created(): Payload.ok(); } + /** + * Gets all the tags from a document with the user and timestamp. + * @param projectId + * @param docId + * @return 200 and the list of tags + * + * Example : + * $(curl http://localhost:8080/api/apigen-datashare/documents/tags/bd2ef02d39043cc5cd8c5050e81f6e73c608cafde339c9b7ed68b2919482e8dc7da92e33aea9cafec2419c97375f684f) + */ + @Get("/:project/documents/:docId/markedRead") + public List getMarkedReadDocumentUsers(final String projectId,final String docId) { + return repository.getMarkedReadDocumentUsers(project(projectId),docId); + } + + /** + * Group star the documents. The id list is passed in the request body as a json list. + * + * It answers 200 if the change as been done and the number of documents updated in the response body. + * @param projectId + * @param docIds as json + * @return 200 and the number of documents updated + * + * Example : + * $(curl -i -XPOST -H "Content-Type: application/json" localhost:8080/api/apigen-datashare/documents/batchUpdate/star -d '["bd2ef02d39043cc5cd8c5050e81f6e73c608cafde339c9b7ed68b2919482e8dc7da92e33aea9cafec2419c97375f684f"]') + */ + @Post("/:projectId/documents/batchUpdate/markRead") + public int groupMarkReadProject(final String projectId, final List docIds, Context context) { + return repository.markRead(project(projectId), (HashMapUser)context.currentUser(), docIds); + } + + /** + * Group unstar the documents. The id list is passed in the request body as a json list. + * + * It answers 200 if the change as been done and the number of documents updated in the response body. + * + * @param projectId + * @param docIds as json in body + * @return 200 and the number of documents unstarred + * + * Example : + * $(curl -i -XPOST -H "Content-Type: application/json" localhost:8080/api/apigen-datashare/documents/batchUpdate/unstar -d '["bd2ef02d39043cc5cd8c5050e81f6e73c608cafde339c9b7ed68b2919482e8dc7da92e33aea9cafec2419c97375f684f", "unknownId"]') + */ + @Post("/:project/documents/batchUpdate/unmarkRead") + public int groupUnmarkReadProject(final String projectId, final List docIds, Context context) { + return repository.unmarkRead(project(projectId), (HashMapUser)context.currentUser(), docIds); + } + + /** + * Mark the document read by the user with id docId + * @param docId + * @return 201 if the document has been marked else 200 + * $(curl localhost:8080/api/document/markRead/bd2ef02d39043cc5cd8c5050e81f6e73c608cafde339c9b7ed68b2919482e8dc7da92e33aea9cafec2419c97375f684f) + */ + @Post("/documents/markRead/:docId") + public Payload markReadDocument(final String docId, Context context) { + return repository.markRead((HashMapUser)context.currentUser(), docId) ? Payload.created(): Payload.ok(); + } + + /** + * Unmark the document read by the user with id docId + * @param docId + * @return 201 if the document has been unmarked else 200 + * $(curl localhost:8080/api/document/markRead/bd2ef02d39043cc5cd8c5050e81f6e73c608cafde339c9b7ed68b2919482e8dc7da92e33aea9cafec2419c97375f684f) + */ + @Post("/documents/unmarkRead/:docId") + public Payload unmarkReadDocument(final String docId, Context context) { + return repository.unmarkRead((HashMapUser)context.currentUser(), docId) ? Payload.created(): Payload.ok(); + } + @NotNull private Payload getPayload(Document doc, String index, boolean inline) throws IOException { try (InputStream from = new SourceExtractor().getSource(project(index), doc)) { diff --git a/datashare-app/src/test/java/org/icij/datashare/web/DocumentResourceTest.java b/datashare-app/src/test/java/org/icij/datashare/web/DocumentResourceTest.java index dbf127c18..ed2cae29f 100644 --- a/datashare-app/src/test/java/org/icij/datashare/web/DocumentResourceTest.java +++ b/datashare-app/src/test/java/org/icij/datashare/web/DocumentResourceTest.java @@ -128,6 +128,38 @@ public void testGroupUnstarDocumentWithProject() { post("/api/prj1/documents/batchUpdate/unstar", "[\"id1\", \"id2\"]").should().respond(200); } + @Test + public void testMarkReadDocument() { + when(repository.markRead(any(),any())).thenReturn(true).thenReturn(false); + post("/api/documents/markRead/doc_id").should().respond(201); + post("/api/documents/markRead/doc_id").should().respond(200); + } + + @Test + public void testUnmarkReadDocument() { + when(repository.unmarkRead(any(),any())).thenReturn(true).thenReturn(false); + post("/api/documents/unmarkRead/doc_id").should().respond(201); + post("/api/documents/unmarkRead/doc_id").should().respond(200); + } + + @Test + public void testGroupMarkReadDocumentWithProject() { + when(repository.markRead(project("prj1"), User.local(), asList("id1", "id2"))).thenReturn(2); + post("/api/prj1/documents/batchUpdate/markRead", "[\"id1\", \"id2\"]").should().respond(200); + } + + @Test + public void testGroupUnmarkReadDocumentWithProject() { + when(repository.unmarkRead(project("prj1"), User.local(), asList("id1", "id2"))).thenReturn(2); + post("/api/prj1/documents/batchUpdate/unmarkRead", "[\"id1\", \"id2\"]").should().respond(200); + } + + @Test + public void testGetMarkedReadDocumentUsers() { + when(repository.getMarkedReadDocumentUsers(eq(project("prj")), eq("docId"))).thenReturn(asList("user1", "user2")); + get("/api/prj/documents/docId/markedRead").should().respond(200).contain("user1").contain("user2"); + } + @Test public void testTagDocumentWithProject() throws Exception { when(repository.tag(eq(project("prj1")), anyString(), eq(tag("tag1")), eq(tag("tag2")))).thenReturn(true).thenReturn(false); diff --git a/datashare-db/src/main/java/org/icij/datashare/db/JooqRepository.java b/datashare-db/src/main/java/org/icij/datashare/db/JooqRepository.java index ad6c0a255..f40ac9ad5 100644 --- a/datashare-db/src/main/java/org/icij/datashare/db/JooqRepository.java +++ b/datashare-db/src/main/java/org/icij/datashare/db/JooqRepository.java @@ -27,6 +27,7 @@ import static org.icij.datashare.db.tables.NamedEntity.NAMED_ENTITY; import static org.icij.datashare.db.tables.Note.NOTE; import static org.icij.datashare.db.tables.Project.PROJECT; +import static org.icij.datashare.db.tables.DocumentUserMarkRead.DOCUMENT_USER_MARK_READ; import static org.icij.datashare.json.JsonObjectMapper.MAPPER; import static org.icij.datashare.text.Document.Status.fromCode; import static org.icij.datashare.text.Language.parse; @@ -122,6 +123,32 @@ public List getStarredDocuments(User user) { where(DOCUMENT_USER_STAR.USER_ID.eq(user.id)).fetch().stream().map(this::createDocumentFrom).collect(toList()); } + @Override + public boolean markRead(User user, String documentId) { + DSLContext create = DSL.using(connectionProvider, dialect);; + Result> existResult = create.selectCount().from(DOCUMENT_USER_MARK_READ). + where(DOCUMENT_USER_MARK_READ.USER_ID.eq(user.id), DOCUMENT_USER_MARK_READ.DOC_ID.eq(documentId)).fetch(); + if (existResult.get(0).value1() == 0) { + return create.insertInto(DOCUMENT_USER_MARK_READ, DOCUMENT_USER_MARK_READ.DOC_ID, DOCUMENT_USER_MARK_READ.USER_ID). + values(documentId, user.id).execute() > 0; + } else { + return false; + } + } + + @Override + public boolean unmarkRead(User user, String documentId) { + return DSL.using(connectionProvider, dialect).deleteFrom(DOCUMENT_USER_MARK_READ). + where(DOCUMENT_USER_MARK_READ.DOC_ID.eq(documentId), DOCUMENT_USER_MARK_READ.USER_ID.eq(user.id)).execute() > 0; + } + + @Override + public List getMarkedReadDocumentUsers(String documentId) { + DSLContext create = DSL.using(connectionProvider, dialect); + return create.select(DOCUMENT_USER_MARK_READ.USER_ID).from(DOCUMENT_USER_MARK_READ). + where(DOCUMENT_USER_MARK_READ.DOC_ID.eq(documentId)).fetch().getValues(DOCUMENT_USER_MARK_READ.USER_ID); + } + // ------------- functions that don't need document migration/indexing // they can use just the DOCUMENT_USER_STAR table thus denormalizing project information // this could be removed later @@ -150,6 +177,31 @@ public List getStarredDocuments(Project project, User user) { fetch().getValues(DOCUMENT_USER_STAR.DOC_ID); } + @Override + public int markRead(Project project, User user, List documentIds) { + InsertValuesStep3 query = using(connectionProvider, dialect). + insertInto(DOCUMENT_USER_MARK_READ, DOCUMENT_USER_MARK_READ.DOC_ID, DOCUMENT_USER_MARK_READ.USER_ID, DOCUMENT_USER_MARK_READ.PRJ_ID); + documentIds.forEach(t -> query.values(t, user.id, project.getId())); + return query.execute(); + } + + @Override + public int unmarkRead(Project project, User user, List documentIds) { + return DSL.using(connectionProvider, dialect).deleteFrom(DOCUMENT_USER_MARK_READ). + where(DOCUMENT_USER_MARK_READ.DOC_ID.in(documentIds), + DOCUMENT_USER_MARK_READ.USER_ID.eq(user.id), + DOCUMENT_USER_MARK_READ.PRJ_ID.eq(project.getId())).execute(); + } + + @Override + public List getMarkedReadDocumentUsers(Project project, String documentId) { + DSLContext create = DSL.using(connectionProvider, dialect); + return create.select(DOCUMENT_USER_MARK_READ.USER_ID).from(DOCUMENT_USER_MARK_READ). + where(DOCUMENT_USER_MARK_READ.DOC_ID.eq(documentId)). + and(DOCUMENT_USER_MARK_READ.PRJ_ID.eq(project.getId())). + fetch().getValues(DOCUMENT_USER_MARK_READ.USER_ID); + } + @Override public boolean tag(Project prj, String documentId, Tag... tags) { InsertValuesStep5 query = using(connectionProvider, dialect).insertInto( diff --git a/datashare-db/src/main/resources/liquibase/changelog/changes/015-markRead.yml b/datashare-db/src/main/resources/liquibase/changelog/changes/015-markRead.yml new file mode 100644 index 000000000..db6fced12 --- /dev/null +++ b/datashare-db/src/main/resources/liquibase/changelog/changes/015-markRead.yml @@ -0,0 +1,45 @@ +databaseChangeLog: + - changeSet: + id: 25 + author: mvanza + changes: + - createTable: + tableName: document_user_mark_read + columns: + - column: + name: doc_id + type: varchar(96) + constraints: + nullable: false + - column: + name: user_id + type: varchar(96) + constraints: + nullable: false + - column: + name: prj_id + type: varchar(96) + + - createIndex: + indexName: document_user_mark_read_doc_id + tableName: document_user_mark_read + columns: + - column: + name: doc_id + type: varchar(96) + + - createIndex: + indexName: document_user_mark_read_project_id + tableName: document_user_mark_read + columns: + - column: + name: prj_id + type: varchar(96) + + - createIndex: + indexName: document_user_mark_read_user_id + tableName: document_user_mark_read + columns: + - column: + name: user_id + type: varchar(96) diff --git a/datashare-db/src/main/resources/liquibase/changelog/db.changelog.yml b/datashare-db/src/main/resources/liquibase/changelog/db.changelog.yml index 5a0e250d8..b94b7a823 100644 --- a/datashare-db/src/main/resources/liquibase/changelog/db.changelog.yml +++ b/datashare-db/src/main/resources/liquibase/changelog/db.changelog.yml @@ -44,3 +44,6 @@ databaseChangeLog: - include: file: changes/014-note.yml relativeToChangelogFile: true + - include: + file: changes/015-markRead.yml + relativeToChangelogFile: true diff --git a/datashare-db/src/test/java/org/icij/datashare/db/DbSetupRule.java b/datashare-db/src/test/java/org/icij/datashare/db/DbSetupRule.java index ab5c845c8..3330323d0 100644 --- a/datashare-db/src/test/java/org/icij/datashare/db/DbSetupRule.java +++ b/datashare-db/src/test/java/org/icij/datashare/db/DbSetupRule.java @@ -19,7 +19,7 @@ public class DbSetupRule extends ExternalResource { private final String dataSourceUrl; private static final Operation DELETE_ALL = deleteAllFrom( "document", "named_entity", "document_user_star", "document_tag", "batch_search", - "batch_search_query", "batch_search_result", "project", "note"); + "batch_search_query", "batch_search_result", "project", "note","document_user_mark_read"); DbSetupRule(String dataSourceUrl) { this.dataSource = createDatasource(dataSourceUrl); diff --git a/datashare-db/src/test/java/org/icij/datashare/db/JooqRepositoryTest.java b/datashare-db/src/test/java/org/icij/datashare/db/JooqRepositoryTest.java index 1eddff50b..a99bd049e 100644 --- a/datashare-db/src/test/java/org/icij/datashare/db/JooqRepositoryTest.java +++ b/datashare-db/src/test/java/org/icij/datashare/db/JooqRepositoryTest.java @@ -120,6 +120,23 @@ public void test_star_unstar_a_document_with_join() { assertThat(repository.unstar(user, doc.getId())).isFalse(); } + @Test + public void test_markRead_unmarkRead_a_document_with_join() { + // we consider that this one is unmarked and become marked + Document doc = new Document("id", project("prj"), Paths.get("/path/to/docId"), "my doc", + FRENCH, Charset.defaultCharset(), + "text/plain", new HashMap<>(), + Document.Status.INDEXED, Pipeline.set(CORENLP, OPENNLP), 432L); + repository.create(doc); + User user = new User("userid"); + + assertThat(repository.markRead(user, doc.getId())).isTrue(); + assertThat(repository.markRead(user, doc.getId())).isFalse(); + + assertThat(repository.unmarkRead(user, doc.getId())).isTrue(); + assertThat(repository.unmarkRead(user, doc.getId())).isFalse(); + } + @Test public void test_save_read_project() { assertThat(repository.save(new Project("prj", Paths.get("/source"), "10.0.*.*"))).isTrue(); @@ -134,7 +151,7 @@ public void test_get_unknown_project() { } @Test - public void test_group_star_unstar_a_document_without_documents() { + public void test_group_unstar_a_document_without_documents() { User user = new User("userid"); assertThat(repository.star(project("prj"), user, asList("id1", "id2", "id3"))).isEqualTo(3); @@ -146,6 +163,16 @@ public void test_group_star_unstar_a_document_without_documents() { assertThat(repository.getStarredDocuments(project("prj"), user)).isEmpty(); } + @Test + public void test_group_markRead_unmarkRead_a_document_without_documents() { + User user = new User("userid"); + + assertThat(repository.markRead(project("prj"), user, asList("id1", "id2", "id3"))).isEqualTo(3); + + assertThat(repository.unmarkRead(project("prj"), user,asList("id1", "id2"))).isEqualTo(2); + assertThat(repository.unmarkRead(project("prj"), user, singletonList("id3"))).isEqualTo(1); + } + @Test public void test_tag_untag_a_document() { assertThat(repository.tag(project("prj"), "doc_id", tag("tag1"), tag("tag2"))).isTrue();