From f517c637f9e689fd7b769ef3bc225a8a59d7e16f Mon Sep 17 00:00:00 2001 From: Sofien Haj Chedhli Date: Thu, 28 Nov 2024 14:58:03 +0100 Subject: [PATCH] feat: Implement Notes/Content insert image option - EXO-74754 - Meeds-io/MIPs#145 (#325) --- .../plugin/ArticlePageAttachmentPlugin.java | 81 ++++++++++++++++++ .../news/service/impl/NewsServiceImpl.java | 20 ++++- .../plugin/ArticleAttachmentPluginTest.java | 82 +++++++++++++++++++ .../service/impl/NewsServiceImplTest.java | 6 +- .../components/ContentRichEditor.vue | 22 +++++ 5 files changed, 209 insertions(+), 2 deletions(-) create mode 100644 content-service/src/main/java/io/meeds/news/plugin/ArticlePageAttachmentPlugin.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/ArticlePageAttachmentPlugin.java b/content-service/src/main/java/io/meeds/news/plugin/ArticlePageAttachmentPlugin.java new file mode 100644 index 000000000..defbf21ba --- /dev/null +++ b/content-service/src/main/java/io/meeds/news/plugin/ArticlePageAttachmentPlugin.java @@ -0,0 +1,81 @@ +/** + * 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.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import static io.meeds.news.utils.NewsUtils.NewsObjectType.ARTICLE; + +@Component +public class ArticlePageAttachmentPlugin extends AttachmentPlugin { + + @Autowired + private AttachmentService attachmentService; + + @Autowired + private NewsService newsService; + + public static final String OBJECT_TYPE = "articlePage"; + + @PostConstruct + public void init() { + attachmentService.addPlugin(this); + } + + @Override + public String getObjectType() { + return OBJECT_TYPE; + } + + @Override + public boolean hasAccessPermission(Identity identity, String articleId) throws ObjectNotFoundException { + News news = newsService.getNewsArticleById(articleId); + return news != null && newsService.canViewNews(news, identity.getUserId()); + } + + @Override + public boolean hasEditPermission(Identity identity, String articleId) throws ObjectNotFoundException { + News news = null; + try { + news = newsService.getNewsById(articleId, 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 articleId) throws ObjectNotFoundException { + News news = newsService.getNewsArticleById(articleId); + 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 726911914..554c7b5db 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 @@ -41,9 +41,11 @@ import java.util.regex.Matcher; import java.util.stream.Stream; +import io.meeds.news.plugin.ArticlePageAttachmentPlugin; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.StringUtils; +import org.exoplatform.wiki.service.plugin.WikiDraftPageAttachmentPlugin; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Service; @@ -925,6 +927,7 @@ public News createDraftArticleForNewPage(News draftArticle, draftArticlePage.setParentPageId(newsArticlesRootNotePage.getId()); draftArticlePage.setAuthor(draftArticle.getAuthor()); draftArticlePage.setProperties(draftArticle.getProperties()); + draftArticlePage.setAttachmentObjectType(WikiDraftPageAttachmentPlugin.OBJECT_TYPE); draftArticlePage = noteService.createDraftForNewPage(draftArticlePage, creationDate, @@ -936,6 +939,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, @@ -989,6 +993,7 @@ public News createNewsArticlePage(News newsArticle, String newsArticleCreator) t if (newsArticlesRootNotePage != null) { Page newsArticlePage = new Page(); + newsArticlePage.setAttachmentObjectType(ArticlePageAttachmentPlugin.OBJECT_TYPE); newsArticlePage.setName(newsArticle.getName()); newsArticlePage.setTitle(newsArticle.getTitle()); newsArticlePage.setContent(newsArticle.getBody()); @@ -1003,6 +1008,7 @@ public News createNewsArticlePage(News newsArticle, String newsArticleCreator) t 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()); @@ -1066,6 +1072,7 @@ public News createDraftForExistingPage(News draftArticle, long creationDate, Space space) throws Exception { DraftPage draftArticlePage = new DraftPage(); + draftArticlePage.setAttachmentObjectType(ArticlePageAttachmentPlugin.OBJECT_TYPE); draftArticlePage.setNewPage(false); draftArticlePage.setTargetPageId(targetArticlePage.getId()); draftArticlePage.setTitle(draftArticle.getTitle()); @@ -1082,6 +1089,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(), @@ -1177,6 +1185,7 @@ private News updateDraftArticleForNewPage(News draftArticle, String draftArticle draftArticlePage.setTitle(draftArticle.getTitle()); draftArticlePage.setContent(draftArticle.getBody()); draftArticlePage.setProperties(draftArticle.getProperties()); + draftArticlePage.setAttachmentObjectType(WikiDraftPageAttachmentPlugin.OBJECT_TYPE); // created and updated date set by default during the draft creation DraftPage draftPage = noteService.updateDraftForNewPage(draftArticlePage, @@ -1184,6 +1193,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 @@ -1787,6 +1797,7 @@ private News updateArticle(News news, Identity updater, String newsUpdateType) t existingPage.setContent(news.getBody()); } existingPage.setProperties(news.getProperties()); + existingPage.setAttachmentObjectType(ArticlePageAttachmentPlugin.OBJECT_TYPE); existingPage = noteService.updateNote(existingPage, PageUpdateType.EDIT_PAGE_CONTENT_AND_TITLE, updater); news.setUpdateDate(existingPage.getUpdatedDate()); news.setUpdater(existingPage.getAuthor()); @@ -1833,6 +1844,8 @@ 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()); + PageVersion pageVersion = noteService.getPublishedVersionByPageIdAndLang(Long.valueOf(news.getId()), news.getLang()); + news.setBody(pageVersion.getContent()); // remove the draft DraftPage draftPage = noteService.getLatestDraftPageByUserAndTargetPageAndLang(Long.parseLong(existingPage.getId()), updater.getUserId(), @@ -1957,6 +1970,7 @@ private News updateDraftArticleForExistingPage(News news, String updater, Page p draftPage.setTargetPageId(page.getId()); draftPage.setLang(news.getLang()); draftPage.setProperties(news.getProperties()); + draftPage.setAttachmentObjectType(ArticlePageAttachmentPlugin.OBJECT_TYPE); draftPage = noteService.updateDraftForExistPage(draftPage, page, null, System.currentTimeMillis(), updater); @@ -1964,6 +1978,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, @@ -2090,7 +2105,7 @@ private News addNewArticleVersionWithLang(News news, Identity versionCreator, Sp String newsId = news.getTargetPageId() != null ? news.getTargetPageId() : news.getId(); Page existingPage = noteService.getNoteById(newsId); if (existingPage != null) { - existingPage.setLang(news.getLang()); + existingPage.setAttachmentObjectType(ArticlePageAttachmentPlugin.OBJECT_TYPE); existingPage = noteService.updateNote(existingPage, PageUpdateType.EDIT_PAGE_CONTENT_AND_TITLE, versionCreator); news.setPublicationState(POSTED); // update the metadata item @@ -2113,12 +2128,15 @@ private News addNewArticleVersionWithLang(News news, Identity versionCreator, Sp } existingPage.setTitle(news.getTitle()); existingPage.setContent(news.getBody()); + existingPage.setLang(news.getLang()); NotePageProperties properties = news.getProperties(); if (properties != null) { properties.setDraft(false); } existingPage.setProperties(properties); noteService.createVersionOfNote(existingPage, versionCreator.getUserId()); + PageVersion pageVersion = noteService.getPublishedVersionByPageIdAndLang(Long.valueOf(newsId), news.getLang()); + 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..a6f2cd21a --- /dev/null +++ b/content-service/src/test/java/io/meeds/news/plugin/ArticleAttachmentPluginTest.java @@ -0,0 +1,82 @@ +/** + * 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.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.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class ArticleAttachmentPluginTest { + + @Mock + private NewsService newsService; + + @Mock + private AttachmentService attachmentService; + + @InjectMocks + private ArticlePageAttachmentPlugin plugin; + + @Before + public void setUp() { + plugin.init(); + } + + @Test + public void testGetObjectType() { + Assert.assertEquals("articlePage", plugin.getObjectType()); + } + + @Test + public void testHasAccessPermission() throws Exception { + org.exoplatform.services.security.Identity userIdentity = mock(org.exoplatform.services.security.Identity.class); + News news = mock(News.class); + + 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); + News news = mock(News.class); + + 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 3e41946a6..40f0d1821 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.ArticlePageAttachmentPlugin; import io.meeds.news.search.NewsSearchConnector; import io.meeds.news.search.NewsESSearchResult; import io.meeds.notes.model.NoteFeaturedImage; @@ -527,6 +528,7 @@ public void testPostNews() throws Exception { newsArticlePage.setParentPageId(rootPage.getId()); newsArticlePage.setAuthor(newsArticle.getAuthor()); newsArticlePage.setLang(null); + newsArticlePage.setAttachmentObjectType(ArticlePageAttachmentPlugin.OBJECT_TYPE); Page createdPage = mock(Page.class); when(createdPage.getId()).thenReturn("1"); @@ -609,6 +611,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), @@ -769,7 +772,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)).getPublishedVersionByPageIdAndLang(1L, null); + verify(noteService, times(2)).getPublishedVersionByPageIdAndLang(1L, null); } @Test @@ -997,6 +1000,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"); 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 5f177785d..49ac656a0 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 @@ -127,6 +127,7 @@ export default { isSpaceMember: false, spacePrettyName: null, editorExtensions: null, + draftObjectType: 'wikiDraft', }; }, watch: { @@ -291,10 +292,14 @@ export default { this.article.targetPageId = createdArticle.targetPageId; this.article.properties = createdArticle.properties; this.article.draftPage = true; + 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); } + document.dispatchEvent(new CustomEvent('update-processed-image-url', {detail: { + content: createdArticle.body + }})); }).then(() => this.$emit('draftUpdated')) .then(() => this.draftSavingStatus = this.$t('news.composer.draft.savedDraftStatus')) .finally(() => { @@ -319,6 +324,9 @@ export default { if (createdArticle.lang) { alertLink = `${alertLink}&lang=${createdArticle.lang}`; } + document.dispatchEvent(new CustomEvent('update-processed-image-url', {detail: { + content: updatedArticle.body + }})); this.displayAlert({ message: this.$t('news.save.success.message'), type: 'success', @@ -372,6 +380,9 @@ export default { } this.article.properties = updatedArticle?.properties; this.article.draftPage = true; + document.dispatchEvent(new CustomEvent('update-processed-image-url', {detail: { + content: updatedArticle.body + }})); }) .then(() => this.$emit('draftUpdated')) .then(() => { @@ -397,6 +408,9 @@ export default { if (!this.articleId) { this.articleId = createdArticle.id; } + document.dispatchEvent(new CustomEvent('update-processed-image-url', {detail: { + content: createdArticle.body + }})); this.$emit('draftCreated'); this.savingDraft = false; }); @@ -458,10 +472,18 @@ export default { history.replaceState(null,'',scheduleArticle.spaceUrl); window.location.href = scheduleArticle.url; } + document.dispatchEvent(new CustomEvent('update-processed-image-url', { + detail: { + content: scheduleArticle.body + } + })); }); } else { this.$newsServices.saveNews(article).then((createdArticle) => { this.articleType = 'latest_draft'; + document.dispatchEvent(new CustomEvent('update-processed-image-url', {detail: { + content: createdArticle.body + }})); this.fillArticle(createdArticle.id, false, createdArticle.lang || this.selectedLanguage).then(() => { this.updateUrl(); this.initDataPropertiesFromUrl();