From 89e33c480f92aae7afe8cd7c84e6f97c8bd4463d Mon Sep 17 00:00:00 2001 From: Sofien Haj Chedhli Date: Tue, 19 Nov 2024 09:45:50 +0100 Subject: [PATCH] feat: Implement Notes/Content insert image option - EXO-74754 - Meeds-io/MIPs#145 (#312) Implement notes/content insert image option. --- .../ArticlePageVersionAttachmentPlugin.java | 89 ++++++++++++++++ .../news/service/impl/NewsServiceImpl.java | 19 +++- .../plugin/ArticleAttachmentPluginTest.java | 100 ++++++++++++++++++ .../service/impl/NewsServiceImplTest.java | 7 +- .../components/ContentRichEditor.vue | 20 +++- 5 files changed, 228 insertions(+), 7 deletions(-) create mode 100644 content-service/src/main/java/io/meeds/news/plugin/ArticlePageVersionAttachmentPlugin.java create mode 100644 content-service/src/test/java/io/meeds/news/plugin/ArticleAttachmentPluginTest.java diff --git a/content-service/src/main/java/io/meeds/news/plugin/ArticlePageVersionAttachmentPlugin.java b/content-service/src/main/java/io/meeds/news/plugin/ArticlePageVersionAttachmentPlugin.java new file mode 100644 index 000000000..8e23691db --- /dev/null +++ b/content-service/src/main/java/io/meeds/news/plugin/ArticlePageVersionAttachmentPlugin.java @@ -0,0 +1,89 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package io.meeds.news.plugin; + +import io.meeds.news.model.News; +import io.meeds.news.service.NewsService; +import jakarta.annotation.PostConstruct; +import org.exoplatform.commons.exception.ObjectNotFoundException; +import org.exoplatform.services.security.Identity; +import org.exoplatform.social.attachment.AttachmentPlugin; +import org.exoplatform.social.attachment.AttachmentService; +import org.exoplatform.wiki.model.PageVersion; +import org.exoplatform.wiki.service.NoteService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import static io.meeds.news.utils.NewsUtils.NewsObjectType.ARTICLE; + +@Component +public class ArticlePageVersionAttachmentPlugin extends AttachmentPlugin { + + @Autowired + private AttachmentService attachmentService; + + @Autowired + private NewsService newsService; + + @Autowired + private NoteService noteService; + + public static final String OBJECT_TYPE = "articlePageVersion"; + + @PostConstruct + public void init() { + attachmentService.addPlugin(this); + } + + @Override + public String getObjectType() { + return OBJECT_TYPE; + } + + @Override + public boolean hasAccessPermission(Identity identity, String articleVersionId) throws ObjectNotFoundException { + PageVersion pageVersion = noteService.getPageVersionById(Long.parseLong(articleVersionId)); + News news = newsService.getNewsArticleById(pageVersion.getParent().getId()); + return news != null && newsService.canViewNews(news, identity.getUserId()); + } + + @Override + public boolean hasEditPermission(Identity identity, String articleVersionId) throws ObjectNotFoundException { + News news = null; + try { + PageVersion pageVersion = noteService.getPageVersionById(Long.parseLong(articleVersionId)); + news = newsService.getNewsById(pageVersion.getParent().getId(), identity, false, ARTICLE.name()); + } catch (IllegalAccessException e) { + return false; + } + return news != null && news.isCanEdit(); + } + + @Override + public long getAudienceId(String s) throws ObjectNotFoundException { + return 0; + } + + @Override + public long getSpaceId(String articleVersionId) throws ObjectNotFoundException { + PageVersion pageVersion = noteService.getPageVersionById(Long.parseLong(articleVersionId)); + News news = newsService.getNewsArticleById(pageVersion.getParent().getId()); + return news != null ? Long.parseLong(news.getSpaceId()) : 0; + } +} diff --git a/content-service/src/main/java/io/meeds/news/service/impl/NewsServiceImpl.java b/content-service/src/main/java/io/meeds/news/service/impl/NewsServiceImpl.java index 2fa078bb8..27662e39b 100644 --- a/content-service/src/main/java/io/meeds/news/service/impl/NewsServiceImpl.java +++ b/content-service/src/main/java/io/meeds/news/service/impl/NewsServiceImpl.java @@ -39,6 +39,7 @@ import java.util.regex.Matcher; import java.util.stream.Stream; +import io.meeds.news.plugin.ArticlePageVersionAttachmentPlugin; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.StringUtils; @@ -941,6 +942,7 @@ public News createDraftArticleForNewPage(News draftArticle, draftArticle.setId(draftArticlePage.getId()); draftArticle.setCreationDate(draftArticlePage.getCreatedDate()); draftArticle.setUpdateDate(draftArticlePage.getUpdatedDate()); + draftArticle.setBody(draftArticlePage.getContent()); Space draftArticleSpace = spaceService.getSpaceByGroupId(pageOwnerId); draftArticle.setSpaceId(draftArticleSpace.getId()); NewsDraftObject draftArticleMetaDataObject = new NewsDraftObject(NEWS_METADATA_DRAFT_OBJECT_TYPE, @@ -1020,10 +1022,12 @@ public News createNewsArticlePage(News newsArticle, String newsArticleCreator) t newsArticlePage.setProperties(new NotePageProperties(Long.parseLong(draftNewsId), null, null, false, false, true)); } newsArticlePage = noteService.createNote(wiki, newsArticlesRootNotePage.getName(), newsArticlePage, poster); + noteService.createVersionOfNote(newsArticlePage, poster.getUserId(), ArticlePageVersionAttachmentPlugin.OBJECT_TYPE, draftNewsId); if (newsArticlePage != null) { PageVersion pageVersion = noteService.getPublishedVersionByPageIdAndLang(Long.parseLong(newsArticlePage.getId()), null); // set properties newsArticle.setId(newsArticlePage.getId()); + newsArticle.setBody(pageVersion.getContent()); newsArticle.setLang(newsArticlePage.getLang()); newsArticle.setCreationDate(pageVersion.getCreatedDate()); newsArticle.setProperties(newsArticlePage.getProperties()); @@ -1065,6 +1069,7 @@ public News createDraftForExistingPage(News draftArticle, draftArticle.setTargetPageId(draftArticlePage.getTargetPageId()); draftArticle.setProperties(draftArticlePage.getProperties()); draftArticle.setId(draftArticlePage.getId()); + draftArticle.setBody(draftArticlePage.getContent()); NewsLatestDraftObject latestDraftObject = new NewsLatestDraftObject(NEWS_METADATA_LATEST_DRAFT_OBJECT_TYPE, draftArticlePage.getId(), targetArticlePage.getId(), @@ -1218,6 +1223,7 @@ private News updateDraftArticleForNewPage(News draftArticle, String draftArticle Long.parseLong(identityManager.getOrCreateUserIdentity(draftArticleUpdater) .getId())); draftArticle.setProperties(draftPage.getProperties()); + draftArticle.setBody(draftPage.getContent()); draftArticle.setIllustrationURL(NewsUtils.buildIllustrationUrl(draftPage.getProperties(), draftArticle.getLang())); // Update content permissions @@ -1849,8 +1855,10 @@ private News updateArticle(News news, Identity updater, String newsUpdateType) t // create the version if (newsUpdateType.equalsIgnoreCase(CONTENT_AND_TITLE.name())) { - noteService.createVersionOfNote(existingPage, updater.getUserId()); - news.setLatestVersionId(noteService.getPublishedVersionByPageIdAndLang(Long.valueOf(news.getId()), news.getLang()).getId()); + noteService.createVersionOfNote(existingPage, updater.getUserId(), ArticlePageVersionAttachmentPlugin.OBJECT_TYPE, null); + PageVersion pageVersion = noteService.getPublishedVersionByPageIdAndLang(Long.valueOf(news.getId()), news.getLang()); + news.setLatestVersionId(pageVersion.getId()); + news.setBody(pageVersion.getContent()); // remove the draft DraftPage draftPage = noteService.getLatestDraftPageByUserAndTargetPageAndLang(Long.parseLong(existingPage.getId()), updater.getUserId(), @@ -1974,6 +1982,7 @@ private News updateDraftArticleForExistingPage(News news, String updater, Page p news.setDraftUpdater(draftPage.getAuthor()); news.setTargetPageId(draftPage.getTargetPageId()); news.setProperties(draftPage.getProperties()); + news.setBody(draftPage.getContent()); news.setIllustrationURL(NewsUtils.buildIllustrationUrl(draftPage.getProperties(), news.getLang())); NewsLatestDraftObject latestDraftObject = new NewsLatestDraftObject(NEWS_METADATA_LATEST_DRAFT_OBJECT_TYPE, @@ -2137,8 +2146,10 @@ private News addNewArticleVersionWithLang(News news, Identity versionCreator, Sp properties.setDraft(false); } existingPage.setProperties(properties); - noteService.createVersionOfNote(existingPage, versionCreator.getUserId()); - news.setLatestVersionId(noteService.getPublishedVersionByPageIdAndLang(Long.valueOf(newsId), news.getLang()).getId()); + noteService.createVersionOfNote(existingPage, versionCreator.getUserId(), ArticlePageVersionAttachmentPlugin.OBJECT_TYPE, null); + PageVersion pageVersion = noteService.getPublishedVersionByPageIdAndLang(Long.valueOf(newsId), news.getLang()); + news.setLatestVersionId(pageVersion.getId()); + news.setBody(pageVersion.getContent()); news.setIllustrationURL(NewsUtils.buildIllustrationUrl(news.getProperties(), news.getLang())); DraftPage draftPage = noteService.getLatestDraftPageByTargetPageAndLang(Long.parseLong(newsId), news.getLang()); if (draftPage != null) { diff --git a/content-service/src/test/java/io/meeds/news/plugin/ArticleAttachmentPluginTest.java b/content-service/src/test/java/io/meeds/news/plugin/ArticleAttachmentPluginTest.java new file mode 100644 index 000000000..9f3b61f38 --- /dev/null +++ b/content-service/src/test/java/io/meeds/news/plugin/ArticleAttachmentPluginTest.java @@ -0,0 +1,100 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package io.meeds.news.plugin; + +import io.meeds.news.model.News; +import io.meeds.news.service.NewsService; +import org.exoplatform.social.attachment.AttachmentService; +import org.exoplatform.wiki.model.Page; +import org.exoplatform.wiki.model.PageVersion; +import org.exoplatform.wiki.service.NoteService; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static io.meeds.news.utils.NewsUtils.NewsObjectType.ARTICLE; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class ArticleAttachmentPluginTest { + + @Mock + private NoteService noteService; + + @Mock + private NewsService newsService; + + @Mock + private AttachmentService attachmentService; + + @InjectMocks + private ArticlePageVersionAttachmentPlugin plugin; + + @Before + public void setUp() { + plugin.init(); + } + + @Test + public void testGetObjectType() { + Assert.assertEquals("articlePageVersion", plugin.getObjectType()); + } + + @Test + public void testHasAccessPermission() throws Exception { + org.exoplatform.services.security.Identity userIdentity = mock(org.exoplatform.services.security.Identity.class); + PageVersion pageVersion = mock(PageVersion.class); + Page page = mock(Page.class); + News news = mock(News.class); + + when(userIdentity.getUserId()).thenReturn("user123"); + when(noteService.getPageVersionById(anyLong())).thenReturn(pageVersion); + when(pageVersion.getParent()).thenReturn(page); + when(page.getId()).thenReturn("1"); + when(newsService.getNewsArticleById(anyString())).thenReturn(news); + when(newsService.canViewNews(news, userIdentity.getUserId())).thenReturn(true); + + assertTrue(plugin.hasAccessPermission(userIdentity, "1")); + } + + @Test + public void testHasEditPermission() throws Exception { + org.exoplatform.services.security.Identity userIdentity = mock(org.exoplatform.services.security.Identity.class); + PageVersion pageVersion = mock(PageVersion.class); + Page page = mock(Page.class); + News news = mock(News.class); + + when(noteService.getPageVersionById(anyLong())).thenReturn(pageVersion); + when(pageVersion.getParent()).thenReturn(page); + when(page.getId()).thenReturn("1"); + when(newsService.getNewsById("1", userIdentity, false, ARTICLE.name())).thenReturn(news); + when(news.isCanEdit()).thenReturn(true); + + assertTrue(plugin.hasEditPermission(userIdentity, "1")); + } + +} diff --git a/content-service/src/test/java/io/meeds/news/service/impl/NewsServiceImplTest.java b/content-service/src/test/java/io/meeds/news/service/impl/NewsServiceImplTest.java index fe779cebd..b0ad0d7fe 100644 --- a/content-service/src/test/java/io/meeds/news/service/impl/NewsServiceImplTest.java +++ b/content-service/src/test/java/io/meeds/news/service/impl/NewsServiceImplTest.java @@ -44,6 +44,7 @@ import java.util.List; import java.util.Map; +import io.meeds.news.plugin.ArticlePageVersionAttachmentPlugin; import io.meeds.news.search.NewsSearchConnector; import io.meeds.news.search.NewsESSearchResult; import io.meeds.notes.model.NoteFeaturedImage; @@ -622,6 +623,7 @@ public void testCreateDraftArticleForExistingPage() throws Exception { when(draftPage.getCreatedDate()).thenReturn(new Date()); when(draftPage.getAuthor()).thenReturn("john"); when(draftPage.getId()).thenReturn("1"); + when(draftPage.getContent()).thenReturn("content"); when(noteService.createDraftForExistPage(any(DraftPage.class), any(Page.class), nullable(String.class), @@ -783,7 +785,7 @@ public void testUpdateNewsArticle() throws Exception { // Then verify(noteService, times(1)).updateNote(any(Page.class), any(), any()); - verify(noteService, times(1)).createVersionOfNote(existingPage, identity.getUserId()); + verify(noteService, times(1)).createVersionOfNote(existingPage, identity.getUserId(), ArticlePageVersionAttachmentPlugin.OBJECT_TYPE, null); verify(noteService, times(2)).getPublishedVersionByPageIdAndLang(1L, null); } @@ -1015,6 +1017,7 @@ public void testAddNewsArticleTranslation() throws Exception { when(pageVersion.getAuthor()).thenReturn("john"); when(pageVersion.getUpdatedDate()).thenReturn(new Date()); when(pageVersion.getAuthorFullName()).thenReturn("full name"); + when(pageVersion.getContent()).thenReturn("content"); News news = new News(); news.setAuthor("john"); @@ -1043,7 +1046,7 @@ public void testAddNewsArticleTranslation() throws Exception { // Then verify(noteService, times(1)).updateNote(any(Page.class), any(), any()); - verify(noteService, times(1)).createVersionOfNote(existingPage, identity.getUserId()); + verify(noteService, times(1)).createVersionOfNote(existingPage, identity.getUserId(), ArticlePageVersionAttachmentPlugin.OBJECT_TYPE, null); verify(noteService, times(2)).getPublishedVersionByPageIdAndLang(1L, null); NEWS_UTILS.verify(() -> NewsUtils.broadcastEvent(eq(NewsUtils.ADD_ARTICLE_TRANSLATION), anyObject(), anyObject()), times(1)); } diff --git a/content-webapp/src/main/webapp/vue-app/news-activity-composer-app/components/ContentRichEditor.vue b/content-webapp/src/main/webapp/vue-app/news-activity-composer-app/components/ContentRichEditor.vue index 5d747d34f..85ffc8eea 100644 --- a/content-webapp/src/main/webapp/vue-app/news-activity-composer-app/components/ContentRichEditor.vue +++ b/content-webapp/src/main/webapp/vue-app/news-activity-composer-app/components/ContentRichEditor.vue @@ -134,7 +134,8 @@ export default { spacePrettyName: null, editorExtensions: null, autosaveProcessedFromEditorExtension: false, - allowedTargets: [] + allowedTargets: [], + draftObjectType: 'wikiDraft', }; }, watch: { @@ -329,6 +330,8 @@ export default { if (updatedArticle?.properties) { updatedArticle.properties.draft = true; } + updatedArticle.body = this.$noteUtils.sanitizeSrcImageTags(updatedArticle.body); + const setEditorDataMutely = this.$noteUtils.isHasImagesToBeProcessed(updatedArticle.body, this.draftObjectType); updatedArticle.publicationState = 'draft'; return this.$newsServices.updateNews(updatedArticle, false, this.articleType).then((createdArticle) => { this.spaceUrl = createdArticle.spaceUrl; @@ -342,10 +345,14 @@ export default { this.article.schedulePostDate = createdArticle.schedulePostDate; this.article.publicationState = createdArticle.publicationState; this.article.scheduleUnpublishDate = createdArticle.scheduleUnpublishDate; + this.article.body = createdArticle.body; this.article.lang = createdArticle.lang; if (this.article.body !== createdArticle.body) { this.imagesURLs = this.extractImagesURLsDiffs(this.article.body, createdArticle.body); } + if (setEditorDataMutely) { + this.$refs?.editor?.setEditorDataMutely?.(createdArticle.body); + } }).then(() => this.$emit('draftUpdated')) .then(() => this.draftSavingStatus = this.$t('news.composer.draft.savedDraftStatus')) .finally(() => { @@ -374,6 +381,7 @@ export default { this.imagesURLs = this.extractImagesURLsDiffs(this.article.body, createdArticle.body); } this.fillArticle(createdArticle.id, false, createdArticle.lang); + this.$refs?.editor?.setEditorDataMutely?.(createdArticle.body); this.displayAlert({ message: this.$t('news.save.success.message'), type: 'success', @@ -420,6 +428,8 @@ export default { properties: properties, draftPage: true }; + article.body = this.$noteUtils.sanitizeSrcImageTags(article.body); + const setEditorDataMutely = this.$noteUtils.isHasImagesToBeProcessed(article.body, this.draftObjectType); if (this.article.id) { if (this.article.title || this.article.body) { article.id = this.article.id; @@ -431,6 +441,9 @@ export default { this.article.draftPage = true; this.article.id = updatedArticle.id; this.article.properties = updatedArticle?.properties; + if (setEditorDataMutely) { + this.$refs?.editor?.setEditorDataMutely?.(updatedArticle.body); + } }) .then(() => this.$emit('draftUpdated')) .then(() => { @@ -457,6 +470,9 @@ export default { if (!this.articleId) { this.articleId = createdArticle.id; } + if (setEditorDataMutely) { + this.$refs?.editor?.setEditorDataMutely?.(createdArticle.body); + } this.$emit('draftCreated'); this.savingDraft = false; if (this.autosaveProcessedFromEditorExtension) { @@ -523,6 +539,7 @@ export default { if (article.publicationState ==='staged') { this.$newsServices.scheduleNews(article, this.articleType).then((scheduleArticle) => { this.articleType = 'latest_draft'; + this.$refs?.editor?.setEditorDataMutely?.(scheduleArticle.body); this.fillArticle(scheduleArticle.id, false, null).then(() => { this.updateUrl(); this.initDataPropertiesFromUrl(); @@ -538,6 +555,7 @@ export default { } else { this.$newsServices.saveNews(article).then((createdArticle) => { this.articleType = 'latest_draft'; + this.$refs?.editor?.setEditorDataMutely?.(createdArticle.body); this.fillArticle(createdArticle.id, false, createdArticle.lang || this.selectedLanguage).then(() => { this.updateUrl(); this.initDataPropertiesFromUrl();