diff --git a/webapp-java/build.gradle b/webapp-java/build.gradle index 115ed51ba..ca6f90d57 100644 --- a/webapp-java/build.gradle +++ b/webapp-java/build.gradle @@ -35,7 +35,8 @@ task buildModules(type: MavenBuildTask) { "dxa-module-googleanalytics", "dxa-module-mediamanager", "dxa-module-search", - "dxa-module-51degrees" + "dxa-module-51degrees", + "dxa-module-ugc" ], ["dxa-module-test"] ] diff --git a/webapp-java/dxa-module-51degrees/pom.xml b/webapp-java/dxa-module-51degrees/pom.xml index 565321b4e..f0d74fe81 100644 --- a/webapp-java/dxa-module-51degrees/pom.xml +++ b/webapp-java/dxa-module-51degrees/pom.xml @@ -7,7 +7,7 @@ com.sdl.dxa dxa-oss-parent - 2.0.0 + 2.1.0-SNAPSHOT com.sdl.dxa.modules diff --git a/webapp-java/dxa-module-audience-manager/pom.xml b/webapp-java/dxa-module-audience-manager/pom.xml index bb730e246..b95fc37b4 100644 --- a/webapp-java/dxa-module-audience-manager/pom.xml +++ b/webapp-java/dxa-module-audience-manager/pom.xml @@ -5,7 +5,7 @@ com.sdl.dxa dxa-oss-parent - 2.0.0 + 2.1.0-SNAPSHOT com.sdl.dxa.modules diff --git a/webapp-java/dxa-module-cid/pom.xml b/webapp-java/dxa-module-cid/pom.xml index 428cbcf45..76fc09ae8 100644 --- a/webapp-java/dxa-module-cid/pom.xml +++ b/webapp-java/dxa-module-cid/pom.xml @@ -6,7 +6,7 @@ com.sdl.dxa dxa-oss-parent - 2.0.0 + 2.1.0-SNAPSHOT com.sdl.dxa.modules diff --git a/webapp-java/dxa-module-cid/src/main/java/com/sdl/dxa/modules/cid/CidWebInitializer.java b/webapp-java/dxa-module-cid/src/main/java/com/sdl/dxa/modules/cid/CidWebInitializer.java index 6cc7d2afd..767711d94 100644 --- a/webapp-java/dxa-module-cid/src/main/java/com/sdl/dxa/modules/cid/CidWebInitializer.java +++ b/webapp-java/dxa-module-cid/src/main/java/com/sdl/dxa/modules/cid/CidWebInitializer.java @@ -7,13 +7,12 @@ import javax.servlet.Filter; import javax.servlet.Servlet; import javax.servlet.ServletContext; -import javax.servlet.ServletException; import java.util.Properties; @Slf4j public class CidWebInitializer implements WebApplicationInitializer { @Override - public void onStartup(ServletContext servletContext) throws ServletException { + public void onStartup(ServletContext servletContext) { log.debug("Trying to initialize servlet for CID module"); Properties properties = InitializationUtils.loadDxaProperties(); @@ -34,8 +33,6 @@ public void onStartup(ServletContext servletContext) throws ServletException { log.warn("Class {} for CID module is expected to be either Filter of Servlet but is", className); } - String sessionIdName = InitializationUtils.loadDxaProperties().getProperty("dxa.modules.cid.sessionid.name"); - servletContext.getSessionCookieConfig().setName(sessionIdName); - log.info("Set default SESSIONID to {}", sessionIdName); + // session id rename has moved to DXA Web Initialization } } diff --git a/webapp-java/dxa-module-cid/src/main/resources/dxa.modules.cid.properties b/webapp-java/dxa-module-cid/src/main/resources/dxa.modules.cid.properties index 8d6b1b5bf..9d01fbcc1 100644 --- a/webapp-java/dxa-module-cid/src/main/resources/dxa.modules.cid.properties +++ b/webapp-java/dxa-module-cid/src/main/resources/dxa.modules.cid.properties @@ -1,4 +1,5 @@ dxa.modules.cid.mapping=/cid/* -dxa.modules.cid.sessionid.name=DXA-SESSIONID +# need to rename it in DXA because of CID bug +dxa.web.sessionid.name=DXA-SESSIONID #dxa.modules.cid.className=should be configured it in dxa.properties file because if version-specific #dxa.modules.cid.appHostMapping=external path to the server which runs DXA for back-mapping for CID Service \ No newline at end of file diff --git a/webapp-java/dxa-module-context-expressions/pom.xml b/webapp-java/dxa-module-context-expressions/pom.xml index 0682fdacc..86eb5aeac 100644 --- a/webapp-java/dxa-module-context-expressions/pom.xml +++ b/webapp-java/dxa-module-context-expressions/pom.xml @@ -7,7 +7,7 @@ com.sdl.dxa dxa-oss-parent - 2.0.0 + 2.1.0-SNAPSHOT com.sdl.dxa.modules diff --git a/webapp-java/dxa-module-core/pom.xml b/webapp-java/dxa-module-core/pom.xml index 3e95d1fcd..233a46bf6 100644 --- a/webapp-java/dxa-module-core/pom.xml +++ b/webapp-java/dxa-module-core/pom.xml @@ -6,7 +6,7 @@ com.sdl.dxa dxa-oss-parent - 2.0.0 + 2.1.0-SNAPSHOT com.sdl.dxa.modules diff --git a/webapp-java/dxa-module-googleanalytics/pom.xml b/webapp-java/dxa-module-googleanalytics/pom.xml index 1a64c1ed3..af702c917 100644 --- a/webapp-java/dxa-module-googleanalytics/pom.xml +++ b/webapp-java/dxa-module-googleanalytics/pom.xml @@ -5,7 +5,7 @@ com.sdl.dxa dxa-oss-parent - 2.0.0 + 2.1.0-SNAPSHOT com.sdl.dxa.modules diff --git a/webapp-java/dxa-module-mediamanager/pom.xml b/webapp-java/dxa-module-mediamanager/pom.xml index 22bbda770..2a03d141c 100644 --- a/webapp-java/dxa-module-mediamanager/pom.xml +++ b/webapp-java/dxa-module-mediamanager/pom.xml @@ -6,7 +6,7 @@ com.sdl.dxa dxa-oss-parent - 2.0.0 + 2.1.0-SNAPSHOT com.sdl.dxa.modules diff --git a/webapp-java/dxa-module-search/pom.xml b/webapp-java/dxa-module-search/pom.xml index d667ce9be..5e76a46f7 100644 --- a/webapp-java/dxa-module-search/pom.xml +++ b/webapp-java/dxa-module-search/pom.xml @@ -5,7 +5,7 @@ com.sdl.dxa dxa-oss-parent - 2.0.0 + 2.1.0-SNAPSHOT com.sdl.dxa.modules diff --git a/webapp-java/dxa-module-smarttarget/pom.xml b/webapp-java/dxa-module-smarttarget/pom.xml index c5bbad9a0..649c9c6f1 100644 --- a/webapp-java/dxa-module-smarttarget/pom.xml +++ b/webapp-java/dxa-module-smarttarget/pom.xml @@ -6,7 +6,7 @@ com.sdl.dxa dxa-oss-parent - 2.0.0 + 2.1.0-SNAPSHOT com.sdl.dxa.modules diff --git a/webapp-java/dxa-module-smarttarget/src/test/java/com/sdl/dxa/modules/smarttarget/mapping/SmartTargetPageBuilderTest.java b/webapp-java/dxa-module-smarttarget/src/test/java/com/sdl/dxa/modules/smarttarget/mapping/SmartTargetPageBuilderTest.java index 767b1937a..ceb2b4dfe 100644 --- a/webapp-java/dxa-module-smarttarget/src/test/java/com/sdl/dxa/modules/smarttarget/mapping/SmartTargetPageBuilderTest.java +++ b/webapp-java/dxa-module-smarttarget/src/test/java/com/sdl/dxa/modules/smarttarget/mapping/SmartTargetPageBuilderTest.java @@ -101,7 +101,7 @@ public void shouldHavePositiveePriority() { public void shouldReturnNullIfPageModelIsNull() { //when, then //noinspection ConstantConditions - assertNull(pageBuilder.buildPageModel(null, new PageModelData("", null, null, null, null, null, null))); + assertNull(pageBuilder.buildPageModel(null, new PageModelData("", "tcm", null, null, null, null, null, null))); } @Test @@ -112,7 +112,7 @@ public void shouldNotChangePageModelWithoutSTRegionsOnPage() throws DxaException PageModel expected = createPageModel(new RegionModelImpl("test")); //when - PageModel page2 = pageBuilder.buildPageModel(pageModel, new PageModelData("", null, null, null, null, null, null)); + PageModel page2 = pageBuilder.buildPageModel(pageModel, new PageModelData("", "tcm", null, null, null, null, null, null)); //then Assert.assertEquals(expected, pageModel); @@ -134,7 +134,7 @@ public void shouldNotChangePageModelWithoutRegionsMetadata() throws DxaException PageModel expected = createPageModel(new SmartTargetRegion("test")); //when - PageModel pageR2 = pageBuilder.buildPageModel(pageModel, new PageModelData("", null, null, null, null, null, "")); + PageModel pageR2 = pageBuilder.buildPageModel(pageModel, new PageModelData("", "tcm", null, null, null, null, null, "")); //then Assert.assertEquals(expected, pageModel); @@ -148,7 +148,7 @@ private void shouldCallSubclassForSmartTargetAndProcessMetadata_R2(String maxIte RegionModelData regionModelData = RegionModelData.builder().name("test").metadata(new ContentModelData() {{ put("maxItems", maxItemsValue); }}).build(); - PageModelData pageModelData = new PageModelData("id", null, Collections.emptyMap(), null, "title", Lists.newArrayList(regionModelData), ""); + PageModelData pageModelData = new PageModelData("id", "tcm", null, Collections.emptyMap(), null, "title", Lists.newArrayList(regionModelData), ""); SmartTargetPageModel stPageModel = Mockito.mock(SmartTargetPageModel.class); Mockito.when(stPageModel.setAllowDuplicates(Matchers.anyBoolean())).thenReturn(stPageModel); diff --git a/webapp-java/dxa-module-test/pom.xml b/webapp-java/dxa-module-test/pom.xml index 87a1dfcf9..c3693bf32 100644 --- a/webapp-java/dxa-module-test/pom.xml +++ b/webapp-java/dxa-module-test/pom.xml @@ -7,7 +7,7 @@ com.sdl.dxa dxa-oss-parent - 2.0.0 + 2.1.0-SNAPSHOT com.sdl.dxa.modules diff --git a/webapp-java/dxa-module-ugc/pom.xml b/webapp-java/dxa-module-ugc/pom.xml new file mode 100644 index 000000000..c5736a733 --- /dev/null +++ b/webapp-java/dxa-module-ugc/pom.xml @@ -0,0 +1,88 @@ + + + 4.0.0 + + com.sdl.dxa + dxa-oss-parent + 2.1.0-SNAPSHOT + + + com.sdl.dxa.modules + dxa-module-ugc + + DXA Modules - UGC + Implementation of DXA UGC module + + + https://github.com/sdl/dxa-modules + scm:git:git@github.com:sdl/dxa-modules.git + scm:git:git@github.com:sdl/dxa-modules.git + HEAD + + + 11.0.0-SNAPSHOT + + + + + com.sdl.dxa + dxa-common-api + + + com.sdl.dxa + dxa-tridion-provider + + + + com.sdl.delivery + udp-cil-api + ${ugc-cil.version} + + + com.sdl.delivery + udp-ugc-cil-api + ${ugc-cil.version} + + + + + javax.servlet + javax.servlet-api + + + javax.servlet + jstl + + + + + org.springframework + spring-test + + + + junit + junit + + + org.mockito + mockito-all + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + org.apache.maven.plugins + maven-jar-plugin + + + + + \ No newline at end of file diff --git a/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/UgcInitializer.java b/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/UgcInitializer.java new file mode 100644 index 000000000..7260ee996 --- /dev/null +++ b/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/UgcInitializer.java @@ -0,0 +1,50 @@ +package com.sdl.dxa.modules.ugc; + +import com.sdl.delivery.ugc.client.comment.UgcCommentApi; +import com.sdl.delivery.ugc.client.comment.impl.DefaultUgcCommentApi; +import com.sdl.dxa.modules.ugc.model.entity.UgcComments; +import com.sdl.dxa.modules.ugc.model.entity.UgcPostCommentForm; +import com.sdl.dxa.modules.ugc.model.entity.UgcRegion; +import com.sdl.webapp.common.api.mapping.views.AbstractModuleInitializer; +import com.sdl.webapp.common.api.mapping.views.ModuleInfo; +import com.sdl.webapp.common.api.mapping.views.RegisteredViewModel; +import com.sdl.webapp.common.api.mapping.views.RegisteredViewModels; +import com.sdl.webapp.common.api.model.page.DefaultPageModel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; + +/** + *

Ugc Module initializer which initializes + * views registration in modules with {@link RegisteredViewModel} and {@link RegisteredViewModels}. + *

+ */ +@Slf4j +@Configuration +@ComponentScan("com.sdl.dxa.modules.ugc") +public class UgcInitializer { + + @Bean + public UgcCommentApi ugcCommentApi() { + return new DefaultUgcCommentApi(); + } + + //Todo: use UgcVoteCommentApi implementation when it becomes available + + @Component + @RegisteredViewModels({ + @RegisteredViewModel(viewName = "Comments", modelClass = UgcRegion.class), + @RegisteredViewModel(viewName = "UgcComments", modelClass = UgcComments.class), + @RegisteredViewModel(viewName = "UgcPostCommentForm", modelClass = UgcPostCommentForm.class, controllerName = "Ugc"), + @RegisteredViewModel(viewName = "GeneralPage", modelClass = DefaultPageModel.class) + }) + @ModuleInfo(name = "UGC module", areaName = "Ugc", description = "Support for UGC views") + public static class UgcViewModuleInitializer extends AbstractModuleInitializer { + @Override + protected String getAreaName() { + return "Ugc"; + } + } +} diff --git a/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/UgcService.java b/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/UgcService.java new file mode 100644 index 000000000..efba18abb --- /dev/null +++ b/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/UgcService.java @@ -0,0 +1,142 @@ +package com.sdl.dxa.modules.ugc; + +import com.sdl.delivery.ugc.client.comment.UgcCommentApi; +import com.sdl.delivery.ugc.client.comment.impl.SimpleCommentsFilter; +import com.sdl.dxa.modules.ugc.data.Comment; +import com.sdl.dxa.modules.ugc.data.User; +import com.sdl.web.ugc.Status; +import com.sdl.webapp.common.util.TcmUtils; +import com.tridion.ambientdata.AmbientDataContext; +import com.tridion.ambientdata.claimstore.ClaimStore; +import lombok.extern.slf4j.Slf4j; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.net.URI; +import java.net.URISyntaxException; +import java.time.ZonedDateTime; +import java.util.*; + +/** + *

Service providing methods to create and retrieve comments

+ */ +@Service +@Slf4j +public class UgcService { + + private static final int maximumThreadsDepth = -1; + + @Autowired + private UgcCommentApi ugcCommentApi; + + //Todo: use UgcVoteCommentApi implementation when it becomes available + + @Autowired + public UgcService() { + } + + /** + * retrieves a list of {@link Comment} items for a given page + * + * @param publicationId Publication Id + * @param pageId Page Id + * @param descending Order + * @param statuses Limit to specific statuses + * @param top maximum number of comments to show + * @param skip number of comments to skip + * @return List of {@link Comment} + */ + public List getComments(int publicationId, int pageId, boolean descending, Integer[] statuses, int top, int skip) { + final List statusStatuses = new ArrayList<>(); + Arrays.stream(statuses).forEach(status -> statusStatuses.add(Status.getStatusForId(status))); + final SimpleCommentsFilter filter = new SimpleCommentsFilter() + .withTop(top) + .withSkip(skip) + .withDepth(maximumThreadsDepth) + .withStatuses(statusStatuses); + + return convert(ugcCommentApi.retrieveThreadedComments(TcmUtils.buildPageTcmUri(publicationId, pageId), filter, descending, true)); + } + + /** + * Post {@link Comment} for a given page + * + * @param publicationId Publication Id + * @param pageId Page Id + * @param username User name + * @param email Email address + * @param content Post content + * @param parentId parent + * @param metadata Meta data + * @return {@link Comment} + */ + public Comment postComment(int publicationId, int pageId, String username, String email, String content, + int parentId, Map metadata) { + + try { + final ClaimStore claimStore = AmbientDataContext.getCurrentClaimStore(); + if (claimStore != null) { + claimStore.put(new URI("taf:claim:contentdelivery:webservice:user"), username); + claimStore.put(new URI("taf:claim:contentdelivery:webservice:post:allowed"), true); + } + } catch (URISyntaxException e) { + log.error("Error while Storing Claims", e); + } + return convert( + ugcCommentApi.postComment(TcmUtils.buildPageTcmUri(publicationId, pageId), username, email, content, parentId, metadata)); + } + + private List convert(List comments) { + final List convertedComments = new ArrayList<>(); + comments.forEach(comment -> convertedComments.add(convert(comment))); + + return convertedComments; + } + + private Comment convert(com.sdl.delivery.ugc.client.odata.edm.Comment comment) { + if (comment == null) { + return null; + } + final Comment c = new Comment(); + c.setId(comment.getIdLong()); + if (comment.getParent() != null) { + c.setParentId(comment.getParent().getIdLong()); + } + c.setItemId(comment.getItemId()); + c.setItemType(comment.getItemType()); + c.setItemPublicationId(comment.getItemPublicationId()); + c.setContent(comment.getContent()); + c.setRating(comment.getScore()); + c.setMetadata(comment.getMetadata()); + if (comment.getUser() != null) { + c.setUser(convert(comment.getUser())); + } + if (comment.getCreationDate() != null) { + c.setCreationDate(convert(comment.getCreationDate())); + } + if (comment.getLastModifiedDate() != null) { + c.setLastModifiedDate(convert(comment.getLastModifiedDate())); + } + c.setChildren(convert(comment.getChildren())); + + return c; + } + + private DateTime convert(ZonedDateTime zonedDateTime) { + return new DateTime( + zonedDateTime.toInstant().toEpochMilli(), + DateTimeZone.forTimeZone(TimeZone.getTimeZone(zonedDateTime.getZone()))); + } + + private User convert(com.sdl.delivery.ugc.client.odata.edm.User user) { + final User u = new User(); + u.setId(user.getId()); + u.setExternalId(user.getExternalId()); + u.setName(user.getName()); + u.setEmailAddress(user.getEmailAddress()); + return u; + } + +} diff --git a/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/controllers/UgcApiController.java b/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/controllers/UgcApiController.java new file mode 100644 index 000000000..7860075a9 --- /dev/null +++ b/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/controllers/UgcApiController.java @@ -0,0 +1,114 @@ +package com.sdl.dxa.modules.ugc.controllers; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.sdl.dxa.modules.ugc.UgcService; +import com.sdl.dxa.modules.ugc.data.Comment; +import com.sdl.dxa.modules.ugc.data.PostedComment; +import com.sdl.dxa.modules.ugc.data.PubIdTitleLang; +import com.sdl.webapp.common.controller.BaseController; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.springframework.web.bind.annotation.RequestMethod.GET; +import static org.springframework.web.bind.annotation.RequestMethod.POST; + + +/** + *

Ugc API controller that handles requests to /api/comments/.

+ */ +@Controller +@RequestMapping(value = {"/api/comments", "/{path}/api/comments"}) +@Slf4j +public class UgcApiController extends BaseController { + + @Autowired + private UgcService ugcService; + + @Autowired + public UgcApiController() { + } + + /** + *

handles get request

+ *

listens to basepath/{publicationId}/{pageId}

+ * + * @param publicationId Publication Id + * @param pageId Page Id + * @param descending Sort descending + * @param status limit results to comments with a specific status + * @param top maximum number of comments to show + * @param skip number of comments to skip + * @return List of {@link Comment} + */ + @RequestMapping(method = GET, value = "/{publicationId}/{pageId}", + produces = MediaType.APPLICATION_JSON_VALUE) + @ResponseBody + public List getComments(@PathVariable("publicationId") Integer publicationId, + @PathVariable("pageId") Integer pageId, + @RequestParam(value = "descending", + required = false, + defaultValue = "false") Boolean descending, + @RequestParam(value = "status[]", + required = false, + defaultValue = "0") Integer[] status, + @RequestParam(value = "top", + required = false, + defaultValue = "0") Integer top, + @RequestParam(value = "skip", + required = false, + defaultValue = "0") Integer skip) { + return ugcService.getComments(publicationId, pageId, descending, status, top, skip); + } + + /** + *

handles post request

+ *

listens to basepath/add

+ * + * @param input {@link PostedComment} + * @return {@link Comment} + */ + @RequestMapping(method = POST, value = "/add", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + @ResponseBody + public Comment postComment(@RequestBody PostedComment input) { + Map metadata = new HashMap<>(); + metadata.put("publicationTitle", input.getPublicationTitle()); + metadata.put("publicationUrl", input.getPublicationUrl()); + metadata.put("itemTitle", input.getPageTitle()); + metadata.put("itemUrl", input.getPageUrl()); + metadata.put("language", input.getLanguage()); + metadata.put("status", "0"); + + addPubIdTitleLangToCommentMetadata(input, metadata); + + return ugcService.postComment(input.getPublicationId(), + input.getPageId(), + input.getUsername(), + input.getEmail(), + input.getContent(), + input.getParentId(), + metadata); + } + + private void addPubIdTitleLangToCommentMetadata(@RequestBody PostedComment input, Map metadata) { + PubIdTitleLang pubIdTitleLang = new PubIdTitleLang(); + pubIdTitleLang.setId(input.getPublicationId()); + pubIdTitleLang.setLang(input.getLanguage()); + pubIdTitleLang.setTitle(input.getPublicationTitle()); + + Gson gSon = new GsonBuilder().create(); + String pubIdTitleLangJson = gSon.toJson(pubIdTitleLang); + + metadata.put("pubIdTitleLang", pubIdTitleLangJson); + } + +} diff --git a/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/controllers/UgcController.java b/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/controllers/UgcController.java new file mode 100644 index 000000000..cca78417a --- /dev/null +++ b/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/controllers/UgcController.java @@ -0,0 +1,81 @@ +package com.sdl.dxa.modules.ugc.controllers; + +import com.sdl.dxa.modules.ugc.UgcService; +import com.sdl.dxa.modules.ugc.data.Comment; +import com.sdl.dxa.modules.ugc.model.entity.UgcComment; +import com.sdl.dxa.modules.ugc.model.entity.UgcComments; +import com.sdl.dxa.modules.ugc.model.entity.UgcPostCommentForm; +import com.sdl.webapp.common.api.content.ContentProviderException; +import com.sdl.webapp.common.api.model.EntityModel; +import com.sdl.webapp.common.api.model.ViewModel; +import com.sdl.webapp.common.controller.EntityController; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; + +import javax.servlet.http.HttpServletRequest; +import java.util.ArrayList; +import java.util.List; + +/** + *

Ugc entity controller that handles include requests to /system/mvc/Framework/Ugc/{entityId}.

+ */ +@Controller +@RequestMapping({"/system/mvc/Framework/Ugc"}) +public class UgcController extends EntityController { + + @Autowired + private UgcService ugcService; + + private static List createEntities(List comments) { + + final List ugcComments = new ArrayList<>(); + comments.forEach((Comment comment) -> ugcComments.add(createEntity(comment))); + return ugcComments; + } + + private static UgcComment createEntity(Comment comment) { + final UgcComment ugcComment = new UgcComment(); + + ugcComment.setComments(createEntities(comment.getChildren())); + ugcComment.setCommentData(comment); + return ugcComment; + } + + @Override + protected ViewModel enrichModel(ViewModel model, HttpServletRequest request) throws Exception { + + if (model instanceof UgcComments) { + final ViewModel enrichedModel = super.enrichModel(model, request); + final UgcComments ugcComments = (UgcComments) (enrichedModel instanceof EntityModel ? enrichedModel : model); + final List comments = ugcService.getComments(ugcComments.getTarget().getPublicationId(), + ugcComments.getTarget().getItemId(), false, new Integer[0], 0, 0); + ugcComments.setComments(createEntities(comments)); + return ugcComments; + } + + if (model instanceof UgcPostCommentForm) { + final ViewModel enrichedModel = super.enrichModel(model, request); + final UgcPostCommentForm postForm = (UgcPostCommentForm) (enrichedModel instanceof EntityModel ? enrichedModel : model); + + postForm.setFormUrl(context.getPage().getUrl()); + } + return model; + } + + /** + * Handles a request for an entity. + * + * @param request The request. + * @param entityId The entity id. + * @return The name of the entity view that should be rendered for this request. + * @throws ContentProviderException If an error occurs so that the entity cannot not be retrieved. + * @throws java.lang.Exception if any. + */ + @RequestMapping({"Entity/{entityId}"}) + public String handleGetEntity(HttpServletRequest request, @PathVariable String entityId) throws Exception { + return this.handleEntityRequest(request, entityId); + } + +} diff --git a/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/controllers/UgcPostFormController.java b/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/controllers/UgcPostFormController.java new file mode 100644 index 000000000..9dc05f54e --- /dev/null +++ b/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/controllers/UgcPostFormController.java @@ -0,0 +1,62 @@ +package com.sdl.dxa.modules.ugc.controllers; + +import com.sdl.dxa.modules.ugc.UgcService; +import com.sdl.dxa.modules.ugc.model.entity.UgcPostCommentForm; +import com.sdl.dxa.modules.ugc.model.validator.UgcPostCommentFormValidator; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; + +/** + *

Ugc API controller that handles requests to /api/ugc/postcomment.

+ */ +@Controller +@RequestMapping(value = {"/api/ugc", "/{path}/api/ugc"}) +@Slf4j +public class UgcPostFormController { + + private static final String REDIRECT_PREFIX = "redirect:"; + + private final UgcPostCommentFormValidator ugcPostCommentFormValidator; + + @Autowired + private UgcService ugcService; + + @Autowired + public UgcPostFormController(UgcPostCommentFormValidator ugcPostCommentFormValidator) { + this.ugcPostCommentFormValidator = ugcPostCommentFormValidator; + } + + /** + *

handles post comment request

+ *

listens to /api/ugc/postcomment.

+ * + * @param form Posted Comment form + * @param bindingResult represents binding results + * @param redirectAttributes Redirect attributes + * @return redirect url + */ + @RequestMapping(value = "/postcomment", method = RequestMethod.POST) + public String postComment(@ModelAttribute("entity") UgcPostCommentForm form, BindingResult bindingResult, RedirectAttributes redirectAttributes) { + ugcPostCommentFormValidator.validate(form, bindingResult); + + if (bindingResult.hasErrors()) { + log.trace("Comment Form for {} has {} errors", form.getTarget(), bindingResult.getErrorCount()); + redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors()); + return REDIRECT_PREFIX + form.getFormUrl(); + } + log.trace("Comment Form for {} complete and valid",form.getTarget()); + + ugcService.postComment(form.getTarget().getPublicationId(), form.getTarget().getItemId(), form.getUserName(), + form.getEmailAddress(), form.getContent(), form.getParentId(), form.getMetadata()); + + log.trace("Comment on {} by {} posted succesful", form.getTarget(),form.getUserName()); + return REDIRECT_PREFIX + form.getFormUrl(); + } + +} diff --git a/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/data/Comment.java b/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/data/Comment.java new file mode 100644 index 000000000..4a906934f --- /dev/null +++ b/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/data/Comment.java @@ -0,0 +1,46 @@ +package com.sdl.dxa.modules.ugc.data; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; +import org.joda.time.DateTime; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + *

Ugc comment

+ */ + +@Data +public class Comment { + private long id; + + @JsonIgnore + private long parentId; + + private int itemPublicationId; + + private int itemId; + + private int itemType; + +// private CommentDate creationDate; + private DateTime creationDate; + +// private CommentDate lastModifiedDate; + private DateTime lastModifiedDate; + + private String content; + + private User user; + + private List children; + + @JsonIgnore + private int rating = 0; + + @JsonIgnore + private Map metadata = new HashMap<>(); + +} diff --git a/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/data/PostedComment.java b/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/data/PostedComment.java new file mode 100644 index 000000000..1b0eb7f09 --- /dev/null +++ b/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/data/PostedComment.java @@ -0,0 +1,32 @@ +package com.sdl.dxa.modules.ugc.data; + +import lombok.Data; + +/** + *

Ugc posted comment

+ */ + +@Data +public class PostedComment { + private int publicationId; + + private int pageId; + + private String username; + + private String email; + + private String content; + + private int parentId = 0; + + private String publicationTitle; + + private String publicationUrl; + + private String pageTitle; + + private String pageUrl; + + private String language; +} diff --git a/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/data/PubIdTitleLang.java b/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/data/PubIdTitleLang.java new file mode 100644 index 000000000..c8962cbf6 --- /dev/null +++ b/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/data/PubIdTitleLang.java @@ -0,0 +1,17 @@ +package com.sdl.dxa.modules.ugc.data; + +import lombok.Data; + +/** + *

Publication Title and Language

+ */ + +@Data +public class PubIdTitleLang { + + private int id; + + private String title; + + private String lang; +} diff --git a/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/data/User.java b/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/data/User.java new file mode 100644 index 000000000..4782462d5 --- /dev/null +++ b/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/data/User.java @@ -0,0 +1,17 @@ +package com.sdl.dxa.modules.ugc.data; + +import lombok.Data; + +/** + *

Ugc User information

+ */ +@Data +public class User { + private String id; + + private String name; + + private String emailAddress; + + private String externalId; +} diff --git a/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/mapping/UgcModelBuilder.java b/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/mapping/UgcModelBuilder.java new file mode 100644 index 000000000..9434f51ca --- /dev/null +++ b/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/mapping/UgcModelBuilder.java @@ -0,0 +1,283 @@ +package com.sdl.dxa.modules.ugc.mapping; + +import com.sdl.dxa.api.datamodel.model.ContentModelData; +import com.sdl.dxa.api.datamodel.model.EntityModelData; +import com.sdl.dxa.api.datamodel.model.PageModelData; +import com.sdl.dxa.modules.ugc.model.entity.UgcComments; +import com.sdl.dxa.modules.ugc.model.entity.UgcPostCommentForm; +import com.sdl.dxa.modules.ugc.model.entity.UgcRegion; +import com.sdl.dxa.tridion.mapping.EntityModelBuilder; +import com.sdl.dxa.tridion.mapping.PageModelBuilder; +import com.sdl.webapp.common.api.WebRequestContext; +import com.sdl.webapp.common.api.localization.Localization; +import com.sdl.webapp.common.api.model.*; +import com.sdl.webapp.common.api.model.mvcdata.DefaultsMvcData; +import com.sdl.webapp.common.api.model.mvcdata.MvcDataCreator; +import com.sdl.webapp.common.exceptions.DxaException; +import com.sdl.webapp.common.util.TcmUtils; +import com.tridion.util.TCMURI; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.text.ParseException; +import java.util.ArrayList; +import java.util.List; + +/** + *

Builds {@linkplain PageModel Page Model} ando{@linkplain EntityModel Entity Model}.

+ */ +@Slf4j +@Component +public class UgcModelBuilder implements PageModelBuilder, EntityModelBuilder { + + private static final String COMMENTS_CONFIG = "ugcConfig"; + private static final String POST_FORM_CONFIG = "postFormConfig"; + + private static final String COMMENTS_REGION_KEY = "commentsRegion"; + private static final String SHOW_COMMENTS_KEY = "showComments"; + private static final String ALLOW_POST_KEY = "allowPost"; + + private static final String COMMENTS_REGION = "Comments"; + private static final String COMMENTS_AREA = "Ugc"; + + private static final String SHOW_COMMENTS_EXT_DATA = "UgcShowComments"; + private static final String POST_COMMENTS_EXT_DATA = "UgcPostComments"; + private static final String COMMENTS_ENTITY_REGION_EXT_DATA = "CommentsEntityRegion"; + + private static final String COMMENTS_QUALIFIED_NAME = "Ugc:Ugc:UgcComments"; + private static final String POST_FORM_QUALIFIED_NAME = "Ugc:Ugc:UgcPostCommentForm"; + + private final WebRequestContext webRequestContext; + + @Autowired + public UgcModelBuilder(WebRequestContext webRequestContext) { + this.webRequestContext = webRequestContext; + } + + private RegionModel findRegion(RegionModelSet regionModelSet, String regionName) { + for (RegionModel region : regionModelSet) { + if (region.getName().equals(regionName)) { + return region; + } + RegionModel childRegion = findRegion(region.getRegions(), regionName); + if (childRegion != null) { + return childRegion; + } + } + return null; + } + + private UgcRegion createRegion(PageModel pageModel, String areaName, String regionName) { + UgcRegion ugcRegion = null; + if (!pageModel.getRegions().containsClass(UgcRegion.class)) { + try { + ugcRegion = new UgcRegion(regionName); + MvcData mvcData = MvcDataCreator.creator() + .fromQualifiedName(String.format("%s:%s", areaName, regionName)) + .defaults(DefaultsMvcData.REGION) + .create(); + ugcRegion.setMvcData(mvcData); + pageModel.getRegions().add(ugcRegion); + } catch (DxaException e) { + log.error("Creation of RegionModel {} failed", regionName, e); + } + } else { + ugcRegion = pageModel.getRegions().stream() + .filter(regionModel -> regionModel instanceof UgcRegion) + .map(regionModel -> (UgcRegion) regionModel) + .findFirst().orElse(null); + } + return ugcRegion; + } + + private void addCommentsViews(PageModel pageModel, RegionModel region, Localization localization, RegionModel ugcRegion) { + final List regionEntities = new ArrayList<>(); + for (EntityModel entity : region.getEntities()) { + if (entity.getExtensionData() == null) { + continue; + } + List entities = ugcRegion != null ? ugcRegion.getEntities() : regionEntities; + if (entity.getExtensionData().containsKey(COMMENTS_ENTITY_REGION_EXT_DATA)) { + // comment region specified for this entity so lets find it and use that + RegionModel targetRegion = findRegion(pageModel.getRegions(), (String) entity.getExtensionData().get(COMMENTS_ENTITY_REGION_EXT_DATA)); + if (targetRegion != null && targetRegion != region) { + entities = targetRegion.getEntities(); + } else if (targetRegion == null || targetRegion == region) { + entities = regionEntities; + } + + if (entity.getExtensionData().containsKey(SHOW_COMMENTS_EXT_DATA) && + (Boolean) entity.getExtensionData().get(SHOW_COMMENTS_EXT_DATA)) { + entities.add(createUgcCommentsEntity(localization, entity.getId(), TcmUtils.COMPONENT_ITEM_TYPE)); + } + if (entity.getExtensionData().containsKey(POST_COMMENTS_EXT_DATA) && entity.getExtensionData().get(POST_COMMENTS_EXT_DATA) != null) { + entities.add(createUgcPostCommentEntity(localization, entity.getId(), TcmUtils.COMPONENT_ITEM_TYPE, (ContentModelData) entity.getExtensionData().get(POST_COMMENTS_EXT_DATA))); + } + } + } + // Add our ugc views to either the same region as the entity we have comments enabled for or the ugc "Comments" region if available + regionEntities.forEach(entity -> region.getEntities().add(entity)); + region.getRegions().forEach(childRegion -> addCommentsViews(pageModel, childRegion, localization, ugcRegion)); + } + + private UgcComments createUgcCommentsEntity(Localization localization, String id, int itemType) { + final MvcData mvcData = MvcDataCreator.creator() + .fromQualifiedName(COMMENTS_QUALIFIED_NAME) + .defaults(DefaultsMvcData.ENTITY) + .create(); + final UgcComments model = new UgcComments(); + try { + model.setTarget(new TCMURI(TcmUtils.buildTcmUri(localization.getId(), id, itemType))); + } catch (ParseException e) { + log.error("Unable to process TCMURI '{}'.", TcmUtils.buildTcmUri(localization.getId(), id, itemType)); + } + model.setMvcData(mvcData); + return model; + } + + private UgcPostCommentForm createUgcPostCommentEntity(Localization localization, String id, int itemType, ContentModelData postFormConfig) { + final MvcData mvcData = MvcDataCreator.creator() + .fromQualifiedName(POST_FORM_QUALIFIED_NAME) + .defaults(DefaultsMvcData.ENTITY) + .create(); + final UgcPostCommentForm model = new UgcPostCommentForm(); + try { + model.setTarget(new TCMURI(TcmUtils.buildTcmUri(localization.getId(), id, itemType))); + } catch (ParseException e) { + log.error("Unable to process TCMURI '{}'.", TcmUtils.buildTcmUri(localization.getId(), id, itemType)); + } + model.setMvcData(mvcData); + model.setUserNameLabel(getValue(postFormConfig, "userNameLabel", String.class)); + model.setEmailAddressLabel(getValue(postFormConfig, "emailAddressLabel", String.class)); + model.setContentLabel(getValue(postFormConfig, "contentLabel", String.class)); + model.setSubmitButtonLabel(getValue(postFormConfig, "submitButtonLabel", String.class)); + model.setNoContentMessage(getValue(postFormConfig, "noContentMessage", String.class)); + model.setNoEmailAddressMessage(getValue(postFormConfig, "noEmailAddressMessage", String.class)); + model.setNoUserNameMessage(getValue(postFormConfig, "noUserNameMessage", String.class)); + + return model; + } + + private ContentModelData ugcMetadata(ContentModelData metadata) { + return metadata == null ? null : metadata.getAndCast(COMMENTS_CONFIG, ContentModelData.class); + } + + private ContentModelData ugcPostFormMetadata(ContentModelData ugcMetadata) { + return ugcMetadata == null ? null : ugcMetadata.getAndCast(POST_FORM_CONFIG, ContentModelData.class); + } + + private Boolean showComments(ContentModelData ugcMetadata) { + return getValue(ugcMetadata, SHOW_COMMENTS_KEY, Boolean.class); + } + + private Boolean postComments(ContentModelData ugcMetadata) { + return getValue(ugcMetadata, ALLOW_POST_KEY, Boolean.class); + } + + private T getValue(ContentModelData metadata, String name, Class type) { + if (metadata == null || !metadata.containsKey(name)) { + if (type == Boolean.class) { + //noinspection unchecked + return (T) (Boolean.valueOf("No")); + } + return null; + } + Object v = metadata.get(name); + if (v == null) { + if (type == Boolean.class) { + //noinspection unchecked + return (T) Boolean.FALSE; + } + return null; + } + if (type == Boolean.class) { + //noinspection unchecked + return (T) (v.toString().equalsIgnoreCase("yes") ? Boolean.TRUE : Boolean.FALSE); + } + //noinspection unchecked + return (T) v; + } + + /** + *

Extends the pagemodel with a UGC region and entities

+ * + * @param originalPageModel the strongly typed Page Model to build + * @param modelData the DXA R2 Data Model + */ + @Nullable + @Override + public PageModel buildPageModel(@Nullable PageModel originalPageModel, @NotNull PageModelData modelData) { + if (originalPageModel == null) { + log.error("Original page model is null"); + return null; + } + + final ContentModelData metadata = modelData.getPageTemplate() == null ? null : modelData.getPageTemplate().getMetadata(); + final ContentModelData ugcMetadata = ugcMetadata(metadata); + final Localization localization = webRequestContext.getLocalization(); + + String regionName = getValue(ugcMetadata, COMMENTS_REGION_KEY, String.class); + final RegionModel ugcRegion; + RegionModel ugcRegionModel; + if (StringUtils.isEmpty(regionName)) { + ugcRegionModel = findRegion(originalPageModel.getRegions(), COMMENTS_REGION); + if (ugcRegionModel == null) { + ugcRegionModel = createRegion(originalPageModel, COMMENTS_AREA, COMMENTS_REGION); + } + } else { + ugcRegionModel = findRegion(originalPageModel.getRegions(), regionName); + if (ugcRegionModel == null) { + log.error("Unable to locate region for comments '{}'.", regionName); + } + } + + ugcRegion = ugcRegionModel; + originalPageModel.getRegions().forEach(region -> addCommentsViews(originalPageModel, region, localization, ugcRegion)); + + if (ugcRegion != null) { + if (showComments(ugcMetadata)) { + ugcRegion.getEntities().add(createUgcCommentsEntity(localization, originalPageModel.getId(), TcmUtils.PAGE_ITEM_TYPE)); + } + if (postComments(ugcMetadata)) { + ugcRegion.getEntities().add(createUgcPostCommentEntity(localization, originalPageModel.getId(), TcmUtils.PAGE_ITEM_TYPE, + ugcPostFormMetadata(ugcMetadata))); + } + } + return originalPageModel; + } + + /** + *

Extends the Entity Model with Ugc extension data. Never returns null.

+ * + * @param originalEntityModel the strongly typed Entity Model to build. Is null for the first Entity Model Builder in the ModelBuilderPipelineImpl + * @param modelData the DXA R2 Data Model + * @param expectedClass required class of entity model, gets the priority if modelData contains MVC data + * @return the strongly typed Entity Model + */ + @Override + public T buildEntityModel(@Nullable T originalEntityModel, EntityModelData modelData, @Nullable Class expectedClass) { + if (modelData != null && modelData.getComponentTemplate() != null ) { + final ContentModelData ugcMetadata = ugcMetadata(modelData.getComponentTemplate().getMetadata()); + if (ugcMetadata != null) { + originalEntityModel.addExtensionData(SHOW_COMMENTS_EXT_DATA, showComments(ugcMetadata)); + originalEntityModel.addExtensionData(POST_COMMENTS_EXT_DATA, (postComments(ugcMetadata) ? ugcPostFormMetadata(ugcMetadata) : null)); + originalEntityModel.addExtensionData(COMMENTS_ENTITY_REGION_EXT_DATA, getValue(ugcMetadata, COMMENTS_REGION_KEY, String.class)); + } + } + return originalEntityModel; + } + + /** + * {@inheritDoc} + */ + @Override + public int getOrder() { + return 0; + } + + +} diff --git a/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/model/entity/UgcComment.java b/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/model/entity/UgcComment.java new file mode 100644 index 000000000..566553500 --- /dev/null +++ b/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/model/entity/UgcComment.java @@ -0,0 +1,27 @@ +package com.sdl.dxa.modules.ugc.model.entity; + +import com.sdl.dxa.modules.ugc.data.Comment; +import com.sdl.webapp.common.api.model.entity.AbstractEntityModel; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/** + *

EntityModel for a single Ugc comment

+ */ +@Data +@EqualsAndHashCode(callSuper = true) +public class UgcComment extends AbstractEntityModel { + + /** + * Comment Data + **/ + private Comment commentData; + + /** + * List of related comments + **/ + private List comments; + +} diff --git a/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/model/entity/UgcComments.java b/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/model/entity/UgcComments.java new file mode 100644 index 000000000..9d7a1a2c5 --- /dev/null +++ b/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/model/entity/UgcComments.java @@ -0,0 +1,29 @@ +package com.sdl.dxa.modules.ugc.model.entity; + +import com.sdl.webapp.common.api.mapping.semantic.annotations.SemanticMappingIgnore; +import com.sdl.webapp.common.api.model.entity.AbstractEntityModel; +import com.tridion.util.CMURI; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/** + *

EntityModel containing all comments related to a given page or component

+ */ +@Data +@EqualsAndHashCode(callSuper = true) +public class UgcComments extends AbstractEntityModel { + + /** + * Target CmUri for comments + **/ + @SemanticMappingIgnore + private CMURI target; + + /** + * List of comments + **/ + @SemanticMappingIgnore + private List comments; +} diff --git a/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/model/entity/UgcPostCommentForm.java b/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/model/entity/UgcPostCommentForm.java new file mode 100644 index 000000000..2458b4244 --- /dev/null +++ b/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/model/entity/UgcPostCommentForm.java @@ -0,0 +1,129 @@ +package com.sdl.dxa.modules.ugc.model.entity; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.sdl.dxa.caching.NeverCached; +import com.sdl.webapp.common.api.mapping.semantic.annotations.SemanticMappingIgnore; +import com.sdl.webapp.common.api.model.entity.AbstractEntityModel; +import com.sdl.webapp.common.api.model.validation.DynamicCodeResolver; +import com.sdl.webapp.common.api.model.validation.DynamicValidationMessage; +import com.tridion.util.CMURI; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Map; + +/** + *

EntityModel for Posted Comment model

+ */ +@Data +@EqualsAndHashCode(callSuper = true) +@NeverCached(qualifier = "UgcPostCommentForm") +public class UgcPostCommentForm extends AbstractEntityModel { + + /** + * Holds the forms url, for redirecting purposes + */ + @SemanticMappingIgnore + @JsonIgnore + private String formUrl; + + /** + * Holds the form control value for username + */ + @SemanticMappingIgnore + private String userName; + + /** + * Holds the form control value for email address + **/ + @SemanticMappingIgnore + private String emailAddress; + + + + /** + * Holds the form control value for email address + **/ + @SemanticMappingIgnore + private String content; + + /** + * Metadata of comment to post + **/ + @SemanticMappingIgnore + private Map metadata; + + /** + * Parent id of comment to post + **/ + @SemanticMappingIgnore + public int parentId = 0; + + /** + * Label text for username input control on view + **/ + private String userNameLabel; + + /** + * Label text for email address input control on view + **/ + private String emailAddressLabel; + + /** + * Label text for content input control on view + **/ + private String contentLabel; + + /** + * Label text for submit button on view + **/ + private String submitButtonLabel; + + /** + * User name not specified message + **/ + private String noUserNameMessage; + + /** + * Email not specified message + **/ + private String noEmailAddressMessage; + + + /** + * Content not specified message + **/ + private String noContentMessage; + + /** + * Target CmUri for comments + **/ + @SemanticMappingIgnore + private CMURI target; + + @DynamicValidationMessage(errorCode = "userName.empty") + public String getNoUserNameMessage() { + return noUserNameMessage; + } + + @DynamicValidationMessage(errorCode = "emailAddress.empty") + public String getNoEmailAddressMessage() { + return noEmailAddressMessage; + } + + @DynamicValidationMessage(errorCode = "content.empty") + public String getNoContentMessage() { + return noContentMessage; + } + + /** + * Resolves error code using {@link DynamicCodeResolver}. + * + * @param code code to resolve + * @return resolved message + */ + public String resolveErrorCode(String code) { + return DynamicCodeResolver.resolveCode(code, this); + } + +} diff --git a/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/model/entity/UgcRegion.java b/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/model/entity/UgcRegion.java new file mode 100644 index 000000000..d78f7d598 --- /dev/null +++ b/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/model/entity/UgcRegion.java @@ -0,0 +1,47 @@ +package com.sdl.dxa.modules.ugc.model.entity; + +import com.sdl.webapp.common.api.model.MvcData; +import com.sdl.webapp.common.api.model.RegionModel; +import com.sdl.webapp.common.api.model.region.RegionModelImpl; +import com.sdl.webapp.common.exceptions.DxaException; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + *

RegionModel for the Ugc Comment region

+ */ +@EqualsAndHashCode(callSuper = true) +@Data +public class UgcRegion extends RegionModelImpl { + /** + * @param other other Region + */ + public UgcRegion(RegionModel other) { + super(other); + } + + /** + * @param name Region name + * @throws DxaException if name is empty or null + */ + public UgcRegion(String name) throws DxaException { + super(name); + } + + /** + * @param name region name + * @param qualifiedViewName view name + * @throws DxaException if name is empty or null + */ + public UgcRegion(String name, String qualifiedViewName) throws DxaException { + super(name, qualifiedViewName); + } + + /** + * @param mvcData {@link MvcData} + * @throws DxaException if name is empty or null + */ + public UgcRegion(MvcData mvcData) throws DxaException { + super(mvcData); + } +} diff --git a/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/model/validator/UgcPostCommentFormValidator.java b/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/model/validator/UgcPostCommentFormValidator.java new file mode 100644 index 000000000..bbd8a3f70 --- /dev/null +++ b/webapp-java/dxa-module-ugc/src/main/java/com/sdl/dxa/modules/ugc/model/validator/UgcPostCommentFormValidator.java @@ -0,0 +1,38 @@ +package com.sdl.dxa.modules.ugc.model.validator; + +import com.sdl.dxa.modules.ugc.model.entity.UgcPostCommentForm; +import org.springframework.stereotype.Component; +import org.springframework.validation.Errors; +import org.springframework.validation.ValidationUtils; +import org.springframework.validation.Validator; + +/** + *

Validator, validating Ugc comment Posts

+ */ +@Component +public class UgcPostCommentFormValidator implements Validator { + + /** + * @param clazz supported form model class + * @return True/False + */ + @Override + public boolean supports(Class clazz) { + return UgcPostCommentForm.class.isAssignableFrom(clazz); + } + + /** + *

Validates posted form

+ * + * @param target form to validate + * @param errors errors found during validation + */ + @Override + public void validate(Object target, Errors errors) { + final UgcPostCommentForm form = (UgcPostCommentForm) target; + + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "userName", "userName.empty", form.getNoUserNameMessage()); + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "emailAddress", "emailAddress.empty", form.getNoEmailAddressMessage()); + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "content", "content.empty", form.getNoContentMessage()); + } +} diff --git a/webapp-java/dxa-module-ugc/src/main/resources/META-INF/resources/WEB-INF/Views/Ugc/Entity/UgcComments.jsp b/webapp-java/dxa-module-ugc/src/main/resources/META-INF/resources/WEB-INF/Views/Ugc/Entity/UgcComments.jsp new file mode 100644 index 000000000..aec04868b --- /dev/null +++ b/webapp-java/dxa-module-ugc/src/main/resources/META-INF/resources/WEB-INF/Views/Ugc/Entity/UgcComments.jsp @@ -0,0 +1,33 @@ +<%@ taglib prefix="dxa" uri="http://www.sdl.com/tridion-dxa" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + +
+

Comments (${entity.comments.size()})

+
+
+ +
+
+ +
+
+
+ ${comment.commentData.user.name} + - + +
+
+

${comment.commentData.content}

+
+
+
+
+
+
+
+
+ + diff --git a/webapp-java/dxa-module-ugc/src/main/resources/META-INF/resources/WEB-INF/Views/Ugc/Entity/UgcPostCommentForm.jsp b/webapp-java/dxa-module-ugc/src/main/resources/META-INF/resources/WEB-INF/Views/Ugc/Entity/UgcPostCommentForm.jsp new file mode 100644 index 000000000..7aaf86b7c --- /dev/null +++ b/webapp-java/dxa-module-ugc/src/main/resources/META-INF/resources/WEB-INF/Views/Ugc/Entity/UgcPostCommentForm.jsp @@ -0,0 +1,41 @@ +<%@ taglib prefix="dxa" uri="http://www.sdl.com/tridion-dxa" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> + + + + +
+ + + <%--@elvariable id="errors" type="java.util.ArrayList"--%> + +
+
+
    + +
  • ${entity.resolveErrorCode(error.code)}
  • +
    +
+
+
+
+
+ +
+
+ +
+
+ +
+ + + +
+ + +
+
+
\ No newline at end of file diff --git a/webapp-java/dxa-module-ugc/src/main/resources/META-INF/resources/WEB-INF/Views/Ugc/Page/GeneralPage.jsp b/webapp-java/dxa-module-ugc/src/main/resources/META-INF/resources/WEB-INF/Views/Ugc/Page/GeneralPage.jsp new file mode 100644 index 000000000..b79ad48fa --- /dev/null +++ b/webapp-java/dxa-module-ugc/src/main/resources/META-INF/resources/WEB-INF/Views/Ugc/Page/GeneralPage.jsp @@ -0,0 +1,67 @@ + +<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="dxa" uri="http://www.sdl.com/tridion-dxa" %> +<%@ taglib prefix="xpm" uri="http://www.sdl.com/tridion-xpm" %> + + + + + + + + + + + + + ${pageModel.title} + + + + + + + + + + + + +
+ +
+ + +
+ + + +
+ + +
+
+
+ +
+
+ +
+
+
+
+ + + + + + + + + + diff --git a/webapp-java/dxa-module-ugc/src/main/resources/META-INF/resources/WEB-INF/Views/Ugc/Region/Comments.jsp b/webapp-java/dxa-module-ugc/src/main/resources/META-INF/resources/WEB-INF/Views/Ugc/Region/Comments.jsp new file mode 100644 index 000000000..747dbf96e --- /dev/null +++ b/webapp-java/dxa-module-ugc/src/main/resources/META-INF/resources/WEB-INF/Views/Ugc/Region/Comments.jsp @@ -0,0 +1,8 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> +<%@ taglib prefix="dxa" uri="http://www.sdl.com/tridion-dxa" %> + + + +
+ +
diff --git a/webapp-java/dxa-module-ugc/src/test/java/com/sdl/dxa/modules/ugc/UgcServiceTest.java b/webapp-java/dxa-module-ugc/src/test/java/com/sdl/dxa/modules/ugc/UgcServiceTest.java new file mode 100644 index 000000000..1ae01ce7c --- /dev/null +++ b/webapp-java/dxa-module-ugc/src/test/java/com/sdl/dxa/modules/ugc/UgcServiceTest.java @@ -0,0 +1,58 @@ +package com.sdl.dxa.modules.ugc; + +import com.sdl.delivery.ugc.client.comment.UgcCommentApi; +import com.sdl.dxa.modules.ugc.data.Comment; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyMapOf; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class UgcServiceTest { + + + @Mock + private UgcCommentApi ugcCommentApi; + + @Mock + private Comment comment; + + @Mock + private UgcService ugcService; + + @Test + public void shouldProcessGetComments() { + //given + List comments = new ArrayList<>(); + comments.add(comment); + when(ugcService.getComments(any(int.class), any(int.class), any(boolean.class), any(Integer[].class), any(int.class), any(int.class))).thenReturn(comments); + + //when + List result = ugcService.getComments(1,1,false,new Integer[]{},0,0); + + //then + Assert.assertEquals(result,comments); + + } + + @Test + public void shouldProcessPostComment(){ + //given + when(ugcService.postComment(any(int.class), any(int.class), any(String.class), any(String.class), any(String.class), any(int.class), anyMapOf(String.class, String.class))).thenReturn(comment); + + //when + Comment result = ugcService.postComment(1,1,"userName","test@test.com","message",0, new HashMap<>()); + + //then + Assert.assertEquals(result,comment); + } +} \ No newline at end of file diff --git a/webapp-java/dxa-module-ugc/src/test/java/com/sdl/dxa/modules/ugc/controllers/UgControllerTest.java b/webapp-java/dxa-module-ugc/src/test/java/com/sdl/dxa/modules/ugc/controllers/UgControllerTest.java new file mode 100644 index 000000000..ff591bf53 --- /dev/null +++ b/webapp-java/dxa-module-ugc/src/test/java/com/sdl/dxa/modules/ugc/controllers/UgControllerTest.java @@ -0,0 +1,61 @@ +package com.sdl.dxa.modules.ugc.controllers; + +import com.sdl.dxa.modules.ugc.UgcService; +import com.sdl.dxa.modules.ugc.model.entity.UgcComment; +import com.sdl.dxa.modules.ugc.model.entity.UgcComments; +import com.sdl.webapp.common.api.model.ViewModel; +import com.tridion.util.TCMURI; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.runners.MockitoJUnitRunner; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class UgControllerTest { + + @Mock + private UgcService ugcService; + + @Mock + private List ugcComments; + + @InjectMocks + @Spy + private UgcController controller; + + @Test + public void shouldIgnoreModelIfItIsNotUgcComments() throws Exception { + //given + ViewModel model = mock(ViewModel.class); + + //when + controller.enrichModel(model, null); + + //then + verifyZeroInteractions(model); + } + + @Test + public void shouldProcessModelIfItIsUgcComments() throws Exception { + //given + UgcComments model = mock(UgcComments.class); + ugcComments = new ArrayList<>(); + when(model.getTarget()).thenReturn(new TCMURI("tcm:1-2-16")); + when(model.getComments()).thenReturn(ugcComments); + when(ugcService.getComments(any(int.class), any(int.class), any(boolean.class), any(Integer[].class), any(int.class), any(int.class))).thenReturn(new ArrayList<>()); + + //when + ViewModel result = controller.enrichModel(model, null); + + //then + assertEquals(model.getClass(),result.getClass()); + } +} \ No newline at end of file diff --git a/webapp-java/dxa-module-ugc/src/test/java/com/sdl/dxa/modules/ugc/controllers/UgcApiControllerTest.java b/webapp-java/dxa-module-ugc/src/test/java/com/sdl/dxa/modules/ugc/controllers/UgcApiControllerTest.java new file mode 100644 index 000000000..a446abed8 --- /dev/null +++ b/webapp-java/dxa-module-ugc/src/test/java/com/sdl/dxa/modules/ugc/controllers/UgcApiControllerTest.java @@ -0,0 +1,101 @@ +package com.sdl.dxa.modules.ugc.controllers; + +import com.sdl.dxa.modules.ugc.UgcService; +import com.sdl.dxa.modules.ugc.data.Comment; +import com.sdl.dxa.modules.ugc.data.PostedComment; +import com.sdl.dxa.modules.ugc.data.User; +import com.sdl.webapp.common.api.WebRequestContext; +import com.sdl.webapp.common.api.localization.Localization; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.test.context.ActiveProfiles; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyMapOf; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +@ActiveProfiles("test") +public class UgcApiControllerTest { + + private static final int PAGE_ID = 12451; + private static final int PUBLICATION_ID = 4574; + private static final String COMMENT_TEXT = "comment"; + private static final String USER_EMAIL = "email"; + private static final String USER_NAME = "name"; + + @Mock + private WebRequestContext webRequestContext; + + @Mock + private Localization localization; + + @Mock + private List comments; + + @Mock + private Comment comment; + + @Mock + private UgcService ugcService; + + @InjectMocks + @Spy + private UgcApiController ugcApiController; + + @Before + public void init() { + comments = new ArrayList<>(); + when(webRequestContext.getLocalization()).thenReturn(localization); + when(localization.getPath()).thenReturn("path"); + when(ugcService.getComments(any(int.class), any(int.class), any(boolean.class), any(Integer[].class), any(int.class), any(int.class))).thenReturn(comments); + when(ugcService.postComment(any(int.class), any(int.class), any(String.class), any(String.class), + any(String.class), any(int.class), anyMapOf(String.class,String.class))).thenReturn(comment); + } + + /** + * + */ + @Test + public void shouldReturnComments() { + //given + //when + List retrievedComments = ugcApiController.getComments(PUBLICATION_ID, PAGE_ID, false, null, 0, 0); + + //then + assertEquals(comments, retrievedComments); + } + + @Test + public void testPostComment() { + //given + PostedComment postedComment = mock(PostedComment.class); + User user= mock(User.class); + + when(postedComment.getUsername()).thenReturn(USER_NAME); + when(postedComment.getEmail()).thenReturn(USER_EMAIL); + when(postedComment.getContent()).thenReturn(COMMENT_TEXT); + when(comment.getUser()).thenReturn(user); + when(user.getName()).thenReturn(USER_NAME); + when(user.getEmailAddress()).thenReturn(USER_EMAIL); + when(comment.getContent()).thenReturn(COMMENT_TEXT); + + + //when + Comment result = ugcApiController.postComment(postedComment); + //then + assertEquals(USER_NAME, result.getUser().getName()); + assertEquals(USER_EMAIL, result.getUser().getEmailAddress()); + + } +} \ No newline at end of file diff --git a/webapp-java/dxa-module-ugc/src/test/java/com/sdl/dxa/modules/ugc/mapping/UgcModelBuilderTest.java b/webapp-java/dxa-module-ugc/src/test/java/com/sdl/dxa/modules/ugc/mapping/UgcModelBuilderTest.java new file mode 100644 index 000000000..3b14f858b --- /dev/null +++ b/webapp-java/dxa-module-ugc/src/test/java/com/sdl/dxa/modules/ugc/mapping/UgcModelBuilderTest.java @@ -0,0 +1,158 @@ +package com.sdl.dxa.modules.ugc.mapping; + +import com.sdl.dxa.api.datamodel.model.*; +import com.sdl.webapp.common.api.WebRequestContext; +import com.sdl.webapp.common.api.localization.Localization; +import com.sdl.webapp.common.api.model.EntityModel; +import com.sdl.webapp.common.api.model.PageModel; +import com.sdl.webapp.common.api.model.RegionModel; +import com.sdl.webapp.common.api.model.RegionModelSet; +import com.sdl.webapp.common.api.model.entity.AbstractEntityModel; +import com.sdl.webapp.common.api.model.region.RegionModelImpl; +import com.sdl.webapp.common.api.model.region.RegionModelSetImpl; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import java.util.ArrayList; +import java.util.List; + +import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.mock; + +@RunWith(MockitoJUnitRunner.class) +public class UgcModelBuilderTest { + + private static final String COMMENTS_CONFIG = "ugcConfig"; + private static final String POST_FORM_CONFIG = "postFormConfig"; + private static final String COMMENTS_REGION_KEY = "commentsRegion"; + private static final String SHOW_COMMENTS_KEY = "showComments"; + private static final String ALLOW_POST_KEY = "allowPost"; + private static final String SHOW_COMMENTS_EXT_DATA = "UgcShowComments"; + private static final String POST_COMMENTS_EXT_DATA = "UgcPostComments"; + private static final String COMMENTS_ENTITY_REGION_EXT_DATA = "CommentsEntityRegion"; + + @Mock + private WebRequestContext webRequestContext; + + @Mock + private Localization localization; + + @InjectMocks + private UgcModelBuilder builder; + + @Test + public void shouldNotChangeEntityBecauseTemplateExtensionDataIsNullOrHasNoUgcMetadata() { + //given + TestEntity testEntity = new TestEntity(); + EntityModelData entityModelData = new EntityModelData(); + ComponentTemplateData componentTemplate = new ComponentTemplateData(); + ContentModelData metadata = new ContentModelData(); + entityModelData.setComponentTemplate(componentTemplate); + + //when + EntityModel entityR2 = builder.buildEntityModel(testEntity, entityModelData, null); + + //then + Assert.assertSame(testEntity, entityR2); + + //when + componentTemplate.setMetadata(metadata); + entityR2 = builder.buildEntityModel(testEntity, entityModelData, null); + Assert.assertNull(entityR2.getExtensionData()); + + //then + Assert.assertSame(testEntity, entityR2); + Assert.assertNull(entityR2.getExtensionData()); + } + + /** + * + */ + @Test + public void shouldChangeEntity() { + //given + TestEntity testEntity = new TestEntity(); + EntityModelData entityModelData = getUgcEntityModelData(); + + //when + EntityModel entityR2 = builder.buildEntityModel(testEntity, entityModelData, null); + + //then + Assert.assertEquals(entityR2.getExtensionData().get(SHOW_COMMENTS_EXT_DATA), true); + Assert.assertEquals(entityR2.getExtensionData().get(COMMENTS_ENTITY_REGION_EXT_DATA), "comment"); + Assert.assertEquals(entityR2.getExtensionData().get(POST_COMMENTS_EXT_DATA), new ContentModelData()); + } + + @Test + public void shouldNotChangePageModel() { + //given + PageModel testPage = mock(PageModel.class); + + PageModelData pageModelData = new PageModelData(); + PageTemplateData pageTemplate = new PageTemplateData(); + ContentModelData metadata = new ContentModelData(); + ContentModelData ugcMetadata = new ContentModelData(); + ugcMetadata.put(COMMENTS_REGION_KEY, "comment"); + metadata.put(COMMENTS_CONFIG, ugcMetadata); + + RegionModel regionmodel = mock(RegionModelImpl.class); + RegionModelSet regionModelSet = new RegionModelSetImpl(); + regionModelSet.add(regionmodel); + + TestEntity testEntity = new TestEntity(); + EntityModel entityR2 = builder.buildEntityModel(testEntity, getUgcEntityModelData(), null); + ((TestEntity) entityR2).setId("2"); + + List regionEntities = new ArrayList<>(); + regionEntities.add(entityR2); + + pageTemplate.setMetadata(metadata); + pageModelData.setPageTemplate(pageTemplate); + + when(webRequestContext.getLocalization()).thenReturn(localization); + when(localization.getPath()).thenReturn("path"); + when(localization.getId()).thenReturn("1"); + when(testPage.getRegions()).thenReturn(regionModelSet); + when(regionmodel.getEntities()).thenReturn(regionEntities); + when(regionmodel.getRegions()).thenReturn(new RegionModelSetImpl()); + when(regionmodel.getName()).thenReturn("comment"); + + + //when + PageModel pageR2 = builder.buildPageModel(testPage, pageModelData); + + //then + Assert.assertSame(testPage, pageR2); + Assert.assertEquals(((RegionModel)testPage.getRegions().get(regionmodel.getClass()).toArray()[0]).getEntities().size(),3); + + + } + + private EntityModelData getUgcEntityModelData() { + EntityModelData entityModelData = new EntityModelData(); + ComponentTemplateData componentTemplate = new ComponentTemplateData(); + ContentModelData metadata = new ContentModelData(); + ContentModelData ugcMetadata = new ContentModelData(); + ContentModelData ugcPostMetadata = new ContentModelData(); + + ugcMetadata.put(SHOW_COMMENTS_KEY, "yes"); + ugcMetadata.put(ALLOW_POST_KEY, "yes"); + ugcMetadata.put(COMMENTS_REGION_KEY, "comment"); + ugcMetadata.put(POST_FORM_CONFIG, ugcPostMetadata); + + metadata.put(COMMENTS_CONFIG, ugcMetadata); + + componentTemplate.setMetadata(metadata); + entityModelData.setComponentTemplate(componentTemplate); + return entityModelData; + } + + private static class TestEntity extends AbstractEntityModel { + + } + +} \ No newline at end of file diff --git a/webapp-java/dxa-module-ugc/src/test/java/com/sdl/dxa/modules/ugc/model/entity/UgcCommentTest.java b/webapp-java/dxa-module-ugc/src/test/java/com/sdl/dxa/modules/ugc/model/entity/UgcCommentTest.java new file mode 100644 index 000000000..ef2e8352a --- /dev/null +++ b/webapp-java/dxa-module-ugc/src/test/java/com/sdl/dxa/modules/ugc/model/entity/UgcCommentTest.java @@ -0,0 +1,117 @@ +package com.sdl.dxa.modules.ugc.model.entity; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sdl.dxa.DxaSpringInitialization; +import com.sdl.dxa.modules.ugc.data.Comment; +import com.sdl.dxa.modules.ugc.data.User; +import com.sdl.webapp.common.util.ApplicationContextHolder; +import com.sdl.webapp.common.util.TcmUtils; +import org.joda.time.DateTime; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.AnnotationConfigContextLoader; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(loader = AnnotationConfigContextLoader.class) +@ActiveProfiles("test") +public class UgcCommentTest { + private static UgcComment ugcComment = new UgcComment(); + private static Comment commentData = new Comment(); + private static List comments = new ArrayList<>(); + private static UgcComment otherComment = new UgcComment(); + private static DateTime time = DateTime.now(); + private static User user = new User(); + + static { + + user.setEmailAddress("test@test.test"); + user.setName("Tester"); + user.setExternalId("ExternalId"); + user.setId("Id"); + + commentData.setCreationDate(time); + commentData.setLastModifiedDate(time); + commentData.setContent("Comment"); + commentData.setId(1); + commentData.setItemId(2); + commentData.setItemPublicationId(3); + commentData.setParentId(4); + commentData.setItemType(TcmUtils.COMPONENT_ITEM_TYPE); + commentData.setRating(5); + commentData.setUser(user); + + otherComment.setCommentData(commentData); + otherComment.setComments(comments); + + ugcComment.setCommentData(commentData); + ugcComment.setComments(comments); + } + + @Test + public void shouldReturnCommentData() { + //given + + //when + + //then + assertEquals(commentData, ugcComment.getCommentData()); + } + + @Test + public void shouldReturnUserName() { + //given + + //when + + //then + assertEquals("Tester", ugcComment.getCommentData().getUser().getName()); + } + + @Test + public void shouldReturnComments() { + //given + + //when + + //then + assertEquals(comments, ugcComment.getComments()); + } + + @Test + public void shouldReturnCreationDate() { + //given + + //when + + //then + assertEquals(time, ugcComment.getCommentData().getCreationDate()); + } + + + @Configuration + @Profile("test") + static class SpringContext { + + @Bean + public ApplicationContextHolder applicationContextHolder() { + return new ApplicationContextHolder(); + } + + @Bean + public ObjectMapper objectMapper() { + return new DxaSpringInitialization().objectMapper(); + } + } + +} \ No newline at end of file diff --git a/webapp-java/dxa-module-ugc/src/test/java/com/sdl/dxa/modules/ugc/model/entity/UgcCommentsTest.java b/webapp-java/dxa-module-ugc/src/test/java/com/sdl/dxa/modules/ugc/model/entity/UgcCommentsTest.java new file mode 100644 index 000000000..48a28779c --- /dev/null +++ b/webapp-java/dxa-module-ugc/src/test/java/com/sdl/dxa/modules/ugc/model/entity/UgcCommentsTest.java @@ -0,0 +1,74 @@ +package com.sdl.dxa.modules.ugc.model.entity; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sdl.dxa.DxaSpringInitialization; +import com.sdl.webapp.common.util.ApplicationContextHolder; +import com.sdl.webapp.common.util.TcmUtils; +import com.tridion.util.CMURI; +import com.tridion.util.TCMURI; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.AnnotationConfigContextLoader; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(loader = AnnotationConfigContextLoader.class) +@ActiveProfiles("test") +public class UgcCommentsTest { + + private static UgcComments ugcComments = new UgcComments(); + private static UgcComment ugcComment = new UgcComment(); + private static List comments = new ArrayList<>(); + private static CMURI target; + + static { + } + + @Test + public void shouldReturnComments() { + //given + comments.add(ugcComment); + ugcComments.setComments(comments); + //when + + //then + assertEquals(ugcComment, ugcComments.getComments().get(0)); + } + + @Test + public void setTarget() throws Exception { + //given + target = new TCMURI(TcmUtils.buildTcmUri(1, 2, TcmUtils.COMPONENT_ITEM_TYPE)); + ugcComments.setTarget(target); + //when + + //then + assertEquals(target, ugcComments.getTarget()); + } + + @Configuration + @Profile("test") + static class SpringContext { + + @Bean + public ApplicationContextHolder applicationContextHolder() { + return new ApplicationContextHolder(); + } + + @Bean + public ObjectMapper objectMapper() { + return new DxaSpringInitialization().objectMapper(); + } + } + +} \ No newline at end of file diff --git a/webapp-java/dxa-module-ugc/src/test/java/com/sdl/dxa/modules/ugc/model/validator/UgcPostCommentFormValidatorTest.java b/webapp-java/dxa-module-ugc/src/test/java/com/sdl/dxa/modules/ugc/model/validator/UgcPostCommentFormValidatorTest.java new file mode 100644 index 000000000..5e36185c1 --- /dev/null +++ b/webapp-java/dxa-module-ugc/src/test/java/com/sdl/dxa/modules/ugc/model/validator/UgcPostCommentFormValidatorTest.java @@ -0,0 +1,71 @@ +package com.sdl.dxa.modules.ugc.model.validator; + +import com.sdl.dxa.modules.ugc.model.entity.UgcPostCommentForm; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.validation.BindingResult; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class UgcPostCommentFormValidatorTest { + + @Mock + private UgcPostCommentFormValidator ugcPostCommentFormValidator; + + @Test + public void shouldSupportUgcPostCommentFormClass() { + //given + ugcPostCommentFormValidator = new UgcPostCommentFormValidator(); + //when + boolean result = ugcPostCommentFormValidator.supports(UgcPostCommentForm.class); + //then + Assert.assertTrue(result); + } + + @Test + public void shouldNotSupportOtherClass() { + //given + ugcPostCommentFormValidator = new UgcPostCommentFormValidator(); + //when + boolean result = ugcPostCommentFormValidator.supports(Object.class); + //then + Assert.assertFalse(result); + } + + @Test + public void shouldFailOnValidation() { + //given + ugcPostCommentFormValidator = new UgcPostCommentFormValidator(); + UgcPostCommentForm ugcPostCommentForm = mock(UgcPostCommentForm.class); + BindingResult bindingResult = new BeanPropertyBindingResult(ugcPostCommentForm, "Test"); + + //when + ugcPostCommentFormValidator.validate(ugcPostCommentForm, bindingResult); + //then + Assert.assertEquals(bindingResult.getErrorCount(), 3); + } + + @Test + public void shouldSucceedOnValidation() { + //given + ugcPostCommentFormValidator = new UgcPostCommentFormValidator(); + UgcPostCommentForm ugcPostCommentForm = mock(UgcPostCommentForm.class); + BindingResult bindingResult = new BeanPropertyBindingResult(ugcPostCommentForm, "Test"); + when(ugcPostCommentForm.getUserName()).thenReturn("userName"); + when(ugcPostCommentForm.getEmailAddress()).thenReturn("test@test.com"); + when(ugcPostCommentForm.getContent()).thenReturn("message"); + + //when + ugcPostCommentFormValidator.validate(ugcPostCommentForm, bindingResult); + + //then + Assert.assertEquals(bindingResult.getErrorCount(), 0); + } + +} \ No newline at end of file diff --git a/webapp-java/gradle.properties b/webapp-java/gradle.properties index e997a9af9..431ee3726 100644 --- a/webapp-java/gradle.properties +++ b/webapp-java/gradle.properties @@ -1 +1 @@ -version=2.0.0 \ No newline at end of file +version=2.1.0-SNAPSHOT \ No newline at end of file diff --git a/webapp-net/51Degrees/Degrees51ContextClaimsProvider.cs b/webapp-net/51Degrees/Degrees51ContextClaimsProvider.cs index f3918fd15..74c48ab98 100644 --- a/webapp-net/51Degrees/Degrees51ContextClaimsProvider.cs +++ b/webapp-net/51Degrees/Degrees51ContextClaimsProvider.cs @@ -4,7 +4,6 @@ using System.Threading; using System.Web; using FiftyOne.Foundation.Mobile.Detection; -using Sdl.Web.Common.Configuration; using Sdl.Web.Common.Interfaces; using Sdl.Web.Common.Logging; using Sdl.Web.Context.Api.Types; @@ -131,7 +130,7 @@ protected bool PerformUpdateRequest(string key) return false; } - public IDictionary GetContextClaims(string aspectName, Localization localization) + public IDictionary GetContextClaims(string aspectName, ILocalization localization) { try { diff --git a/webapp-net/51Degrees/Sdl.Web.Modules.Degrees51.csproj b/webapp-net/51Degrees/Sdl.Web.Modules.Degrees51.csproj index b0aebe8d2..e9cb46579 100644 --- a/webapp-net/51Degrees/Sdl.Web.Modules.Degrees51.csproj +++ b/webapp-net/51Degrees/Sdl.Web.Modules.Degrees51.csproj @@ -1,6 +1,9 @@  + + + Debug AnyCPU @@ -13,8 +16,6 @@ 512 - 2.0.0 - 10.1.0 true diff --git a/webapp-net/51Degrees/packages.config b/webapp-net/51Degrees/packages.config index 282195bcd..5652a8fd7 100644 --- a/webapp-net/51Degrees/packages.config +++ b/webapp-net/51Degrees/packages.config @@ -3,5 +3,5 @@ - + \ No newline at end of file diff --git a/webapp-net/AudienceManager/Sdl.Web.Modules.AudienceManager.csproj b/webapp-net/AudienceManager/Sdl.Web.Modules.AudienceManager.csproj index 697b08522..fbd09f1a3 100644 --- a/webapp-net/AudienceManager/Sdl.Web.Modules.AudienceManager.csproj +++ b/webapp-net/AudienceManager/Sdl.Web.Modules.AudienceManager.csproj @@ -1,6 +1,7 @@  + Debug AnyCPU @@ -18,9 +19,6 @@ 12.0 - 2.0.0 - 10.1.0 - 8.5.0 true @@ -84,7 +82,7 @@ ..\packages\Sdl.Web.Delivery.$(SdlDeliveryVersion)\lib\net452\Tridion.ContentDelivery.AmbientData.dll - false + True ..\packages\Sdl.AudienceManager.ContentDelivery.$(AudienceManagerVersion)\lib\net452\Tridion.OutboundEmail.ContentDelivery.dll diff --git a/webapp-net/AudienceManager/UserProfileFactory.cs b/webapp-net/AudienceManager/UserProfileFactory.cs index 4e30d818b..6683d225c 100644 --- a/webapp-net/AudienceManager/UserProfileFactory.cs +++ b/webapp-net/AudienceManager/UserProfileFactory.cs @@ -1,6 +1,7 @@ using System; using System.Web; using Sdl.Web.Common.Configuration; +using Sdl.Web.Common.Interfaces; using Sdl.Web.Common.Logging; using Sdl.Web.Mvc.Configuration; using Tridion.ContentDelivery.AmbientData; @@ -57,7 +58,7 @@ public static UserProfile GetUserProfile(string identificationKey) return null; } - Localization localization = WebRequestContext.Localization; + ILocalization localization = WebRequestContext.Localization; // Audience Manager reads the context Publication ID from ADF: AmbientDataContext.CurrentClaimStore.Put(new Uri("taf:claim:publication:id"), localization.Id); diff --git a/webapp-net/AudienceManager/packages.config b/webapp-net/AudienceManager/packages.config index aa2944298..c313affea 100644 --- a/webapp-net/AudienceManager/packages.config +++ b/webapp-net/AudienceManager/packages.config @@ -5,7 +5,7 @@ - - - + + + \ No newline at end of file diff --git a/webapp-net/AzureWebApp/AzureUnknownLocalizationHandler.cs b/webapp-net/AzureWebApp/AzureUnknownLocalizationHandler.cs index 0b17b966d..5690c35bd 100644 --- a/webapp-net/AzureWebApp/AzureUnknownLocalizationHandler.cs +++ b/webapp-net/AzureWebApp/AzureUnknownLocalizationHandler.cs @@ -24,7 +24,7 @@ public class AzureUnknownLocalizationHandler : IUnknownLocalizationHandler /// the response headers and body should be set and should be called to terminate the HTTP processing pipeline. /// /// May return a instance if the handler manages to resolve the Localization. If null is returned, default error handling will be applied. - public Localization HandleUnknownLocalization(DxaUnknownLocalizationException exception, HttpRequest request, HttpResponse response) + public ILocalization HandleUnknownLocalization(DxaUnknownLocalizationException exception, HttpRequest request, HttpResponse response) { using (new Tracer(exception, request, response)) { diff --git a/webapp-net/AzureWebApp/Sdl.Web.Modules.AzureWebApp.csproj b/webapp-net/AzureWebApp/Sdl.Web.Modules.AzureWebApp.csproj index 03b0a3ee8..6d29c20d4 100644 --- a/webapp-net/AzureWebApp/Sdl.Web.Modules.AzureWebApp.csproj +++ b/webapp-net/AzureWebApp/Sdl.Web.Modules.AzureWebApp.csproj @@ -1,6 +1,9 @@  + + + Debug AnyCPU @@ -13,7 +16,6 @@ 512 - 2.0.0 true diff --git a/webapp-net/AzureWebApp/Web.config b/webapp-net/AzureWebApp/Web.config index 38665117c..742f0ae97 100644 --- a/webapp-net/AzureWebApp/Web.config +++ b/webapp-net/AzureWebApp/Web.config @@ -164,48 +164,48 @@ - + - + - + - + - + - + - + - + - + - + - + diff --git a/webapp-net/AzureWebApp/packages.config b/webapp-net/AzureWebApp/packages.config index fd5cb3cfd..0a739b6de 100644 --- a/webapp-net/AzureWebApp/packages.config +++ b/webapp-net/AzureWebApp/packages.config @@ -15,5 +15,5 @@ - + \ No newline at end of file diff --git a/webapp-net/ContextExpressions/ContextExpressionEvaluator.cs b/webapp-net/ContextExpressions/ContextExpressionEvaluator.cs index 05883d9fc..7b392ac7c 100644 --- a/webapp-net/ContextExpressions/ContextExpressionEvaluator.cs +++ b/webapp-net/ContextExpressions/ContextExpressionEvaluator.cs @@ -20,7 +20,7 @@ public class ContextExpressionEvaluator : IConditionalEntityEvaluator /// The Entity Model to be evaluated. /// The context Localization /// true if the Entity should be included. - public bool IncludeEntity(EntityModel entity, Localization localization) + public bool IncludeEntity(EntityModel entity, ILocalization localization) { using (new Tracer(entity)) { @@ -51,7 +51,7 @@ public bool IncludeEntity(EntityModel entity, Localization localization) } #endregion - private static IDictionary GetCachedContextClaims(Localization localization) + private static IDictionary GetCachedContextClaims(ILocalization localization) { // TODO TSI-110: This is a temporary measure to cache the Context Claims per request IDictionary result = null; diff --git a/webapp-net/ContextExpressions/ContextExpressionModelBuilder.cs b/webapp-net/ContextExpressions/ContextExpressionModelBuilder.cs index c5801b0b4..d627816f9 100644 --- a/webapp-net/ContextExpressions/ContextExpressionModelBuilder.cs +++ b/webapp-net/ContextExpressions/ContextExpressionModelBuilder.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; using DD4T.ContentModel; -using Sdl.Web.Common.Configuration; +using Sdl.Web.Common.Interfaces; using Sdl.Web.Common.Logging; using Sdl.Web.Common.Models; using Sdl.Web.DataModel; @@ -21,12 +21,12 @@ namespace Sdl.Web.Modules.ContextExpressions public class ContextExpressionModelBuilder : IModelBuilder, IEntityModelBuilder { #region IModelBuilder members - public void BuildPageModel(ref PageModel pageModel, IPage page, IEnumerable includes, Localization localization) + public void BuildPageModel(ref PageModel pageModel, IPage page, IEnumerable includes, ILocalization localization) { // Nothing to do here } - public void BuildEntityModel(ref EntityModel entityModel, IComponentPresentation cp, Localization localization) + public void BuildEntityModel(ref EntityModel entityModel, IComponentPresentation cp, ILocalization localization) { using (new Tracer(entityModel, cp, localization)) { @@ -53,7 +53,7 @@ public void BuildEntityModel(ref EntityModel entityModel, IComponentPresentation } } - public void BuildEntityModel(ref EntityModel entityModel, IComponent component, Type baseModelType, Localization localization) + public void BuildEntityModel(ref EntityModel entityModel, IComponent component, Type baseModelType, ILocalization localization) { // Nothing to do here } @@ -66,8 +66,8 @@ public void BuildEntityModel(ref EntityModel entityModel, IComponent component, /// The strongly typed Entity Model to build. Is null for the first Entity Model Builder in the pipeline. /// The DXA R2 Data Model. /// The base type for the Entity Model to build. - /// The context . - public void BuildEntityModel(ref EntityModel entityModel, EntityModelData entityModelData, Type baseModelType, Localization localization) + /// The context . + public void BuildEntityModel(ref EntityModel entityModel, EntityModelData entityModelData, Type baseModelType, ILocalization localization) { using (new Tracer(entityModel, entityModelData, baseModelType, localization)) { diff --git a/webapp-net/ContextExpressions/Sdl.Web.Modules.ContextExpressions.csproj b/webapp-net/ContextExpressions/Sdl.Web.Modules.ContextExpressions.csproj index 19afedb5b..c4bf826f2 100644 --- a/webapp-net/ContextExpressions/Sdl.Web.Modules.ContextExpressions.csproj +++ b/webapp-net/ContextExpressions/Sdl.Web.Modules.ContextExpressions.csproj @@ -1,6 +1,9 @@  + + + Debug AnyCPU @@ -13,8 +16,6 @@ 512 - 2.0.0 - 2.0.0 true @@ -39,7 +40,7 @@ False - ..\packages\Sdl.Dxa.DataModel.$(SdlDxaDataModelPackageVersion)\lib\net452\Sdl.Web.DataModel.dll + ..\packages\Sdl.Dxa.DataModel.$(DxaDataModelVersion)\lib\net452\Sdl.Web.DataModel.dll False diff --git a/webapp-net/ContextExpressions/packages.config b/webapp-net/ContextExpressions/packages.config index 82a2a8389..1abcd5f29 100644 --- a/webapp-net/ContextExpressions/packages.config +++ b/webapp-net/ContextExpressions/packages.config @@ -1,6 +1,6 @@  - - + + \ No newline at end of file diff --git a/webapp-net/Core/Areas/Core/Views/Entity/LanguageSelector.cshtml b/webapp-net/Core/Areas/Core/Views/Entity/LanguageSelector.cshtml index 27c285db6..fc5079098 100644 --- a/webapp-net/Core/Areas/Core/Views/Entity/LanguageSelector.cshtml +++ b/webapp-net/Core/Areas/Core/Views/Entity/LanguageSelector.cshtml @@ -1,7 +1,9 @@ -@model Configuration +@using System.Collections.Generic +@using Sdl.Web.Common.Interfaces +@model Configuration @{ var siteLocalizations = WebRequestContext.Localization.SiteLocalizations; - var filteredLocalizations = new List(); + var filteredLocalizations = new List(); if (siteLocalizations.Count > 1) { var excludedLocalizations = new List(); diff --git a/webapp-net/Core/Models/Entity/Article.cs b/webapp-net/Core/Models/Entity/Article.cs index 1d78c0d17..d7d7820a3 100644 --- a/webapp-net/Core/Models/Entity/Article.cs +++ b/webapp-net/Core/Models/Entity/Article.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.ServiceModel.Syndication; -using Sdl.Web.Common.Configuration; +using Sdl.Web.Common.Interfaces; namespace Sdl.Web.Modules.Core.Models { @@ -27,7 +27,7 @@ public class Article : EntityModel, ISyndicationFeedItemProvider /// /// The context . /// A single syndication feed item containing information extracted from this . - public IEnumerable ExtractSyndicationFeedItems(Localization localization) + public IEnumerable ExtractSyndicationFeedItems(ILocalization localization) { return new[] { CreateSyndicationItem(Headline, Description, null, Date, localization) }; } diff --git a/webapp-net/Core/Models/Entity/ContentList.cs b/webapp-net/Core/Models/Entity/ContentList.cs index e7b8e1a0e..0358ca8e7 100644 --- a/webapp-net/Core/Models/Entity/ContentList.cs +++ b/webapp-net/Core/Models/Entity/ContentList.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; +using Sdl.Web.Common.Interfaces; using Sdl.Web.Common.Logging; namespace Sdl.Web.Modules.Core.Models @@ -40,7 +41,7 @@ public int CurrentPage } } - public override Query GetQuery(Localization localization) + public override Query GetQuery(ILocalization localization) { return new SimpleBrokerQuery { @@ -53,7 +54,7 @@ public override Query GetQuery(Localization localization) }; } - protected int MapSchema(Localization localization) + protected int MapSchema(ILocalization localization) { if (ContentType == null) { diff --git a/webapp-net/Core/Models/Entity/Download.cs b/webapp-net/Core/Models/Entity/Download.cs index 582f84a24..88e0f88ed 100644 --- a/webapp-net/Core/Models/Entity/Download.cs +++ b/webapp-net/Core/Models/Entity/Download.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; using System.ServiceModel.Syndication; -using Sdl.Web.Common.Configuration; using Sdl.Web.Common.Models; using System; +using Sdl.Web.Common.Interfaces; namespace Sdl.Web.Modules.Core.Models { @@ -50,7 +50,7 @@ public override string ToHtml(string widthFactor, double aspect = 0, string cssC /// /// This makes it possible possible to render "embedded" Download Models using the Html.DxaEntity method. /// - public override MvcData GetDefaultView(Localization localization) + public override MvcData GetDefaultView(ILocalization localization) { return new MvcData("Core:Download"); } @@ -61,7 +61,7 @@ public override MvcData GetDefaultView(Localization localization) /// /// The context . /// A single syndication feed item containing information extracted from this . - public IEnumerable ExtractSyndicationFeedItems(Localization localization) + public IEnumerable ExtractSyndicationFeedItems(ILocalization localization) { Link downloadLink = new Link {Url = Url}; return new[] { CreateSyndicationItem(FileName, Description, downloadLink, null, localization) }; diff --git a/webapp-net/Core/Models/Entity/Image.cs b/webapp-net/Core/Models/Entity/Image.cs index 94ec1efbe..afa082ca3 100644 --- a/webapp-net/Core/Models/Entity/Image.cs +++ b/webapp-net/Core/Models/Entity/Image.cs @@ -2,6 +2,7 @@ using System.Globalization; using System.Xml; using Sdl.Web.Common.Configuration; +using Sdl.Web.Common.Interfaces; using Sdl.Web.Common.Models; namespace Sdl.Web.Modules.Core.Models @@ -65,7 +66,7 @@ public override void ReadFromXhtmlElement(XmlElement xhtmlElement) /// /// This makes it possible possible to render "embedded" Image Models using the Html.DxaEntity method. /// - public override MvcData GetDefaultView(Localization localization) + public override MvcData GetDefaultView(ILocalization localization) { return new MvcData("Core:Image"); } diff --git a/webapp-net/Core/Models/Entity/ItemList.cs b/webapp-net/Core/Models/Entity/ItemList.cs index fedb9e80c..3f83b7cb7 100644 --- a/webapp-net/Core/Models/Entity/ItemList.cs +++ b/webapp-net/Core/Models/Entity/ItemList.cs @@ -1,8 +1,8 @@ using Sdl.Web.Common.Models; using System.Collections.Generic; using System.ServiceModel.Syndication; -using Sdl.Web.Common.Configuration; using System; +using Sdl.Web.Common.Interfaces; namespace Sdl.Web.Modules.Core.Models { @@ -21,7 +21,7 @@ public class ItemList : EntityModel, ISyndicationFeedItemProvider /// /// The context . /// The extracted syndication feed items; a concatentation of syndication feed items provided by (if any). - public virtual IEnumerable ExtractSyndicationFeedItems(Localization localization) + public virtual IEnumerable ExtractSyndicationFeedItems(ILocalization localization) { return ConcatenateSyndicationFeedItems(ItemListElements, localization); } diff --git a/webapp-net/Core/Models/Entity/Teaser.cs b/webapp-net/Core/Models/Entity/Teaser.cs index 846c579e3..6d8023961 100644 --- a/webapp-net/Core/Models/Entity/Teaser.cs +++ b/webapp-net/Core/Models/Entity/Teaser.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.ServiceModel.Syndication; -using Sdl.Web.Common.Configuration; +using Sdl.Web.Common.Interfaces; namespace Sdl.Web.Modules.Core.Models { @@ -94,7 +94,7 @@ public void SetFormatOption(string key, string value) /// /// The context . /// A single syndication feed item containing information extracted from this . - public IEnumerable ExtractSyndicationFeedItems(Localization localization) + public IEnumerable ExtractSyndicationFeedItems(ILocalization localization) { Link link = Link; if (link == null && Media != null) diff --git a/webapp-net/Core/Models/Entity/YouTubeVideo.cs b/webapp-net/Core/Models/Entity/YouTubeVideo.cs index b34e1b565..3b53b462c 100644 --- a/webapp-net/Core/Models/Entity/YouTubeVideo.cs +++ b/webapp-net/Core/Models/Entity/YouTubeVideo.cs @@ -2,6 +2,7 @@ using System.Xml; using Newtonsoft.Json; using Sdl.Web.Common.Configuration; +using Sdl.Web.Common.Interfaces; using Sdl.Web.Common.Models; namespace Sdl.Web.Modules.Core.Models @@ -72,7 +73,7 @@ public override void ReadFromXhtmlElement(XmlElement xhtmlElement) /// /// This makes it possible possible to render "embedded" YouTubeVideo Models using the Html.DxaEntity method. /// - public override MvcData GetDefaultView(Localization localization) + public override MvcData GetDefaultView(ILocalization localization) { return new MvcData("Core:YouTubeVideo"); } diff --git a/webapp-net/Core/Sdl.Web.Modules.Core.csproj b/webapp-net/Core/Sdl.Web.Modules.Core.csproj index 467468878..5db9f9719 100644 --- a/webapp-net/Core/Sdl.Web.Modules.Core.csproj +++ b/webapp-net/Core/Sdl.Web.Modules.Core.csproj @@ -1,6 +1,9 @@  + + + Debug AnyCPU @@ -27,7 +30,6 @@ true - 2.0.0 true diff --git a/webapp-net/Core/packages.config b/webapp-net/Core/packages.config index 4d08071a9..6015e2506 100644 --- a/webapp-net/Core/packages.config +++ b/webapp-net/Core/packages.config @@ -5,5 +5,5 @@ - + \ No newline at end of file diff --git a/webapp-net/DxaModules.sln b/webapp-net/DxaModules.sln index fe81fc341..2a0d03705 100644 --- a/webapp-net/DxaModules.sln +++ b/webapp-net/DxaModules.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.24720.0 +VisualStudioVersion = 14.0.25420.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sdl.Web.Modules.Search", "Search\Sdl.Web.Modules.Search.csproj", "{8FEFFF4C-0AE2-452F-89B1-36F7D2944032}" EndProject @@ -32,6 +32,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sdl.Web.Modules.Core", "Cor EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sdl.Web.Modules.AudienceManager", "AudienceManager\Sdl.Web.Modules.AudienceManager.csproj", "{83EC18A4-831F-4939-9912-D8A93FA3998D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sdl.Web.Modules.Ugc", "Ugc\Sdl.Web.Modules.Ugc.csproj", "{54DDEEE6-1F97-4E53-8018-5918F7D90A8B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sdl.Web.Modules.TridionDocs", "TridionDocs\Sdl.Web.Modules.TridionDocs.csproj", "{EE523D3E-4A5D-4E01-9FB2-7BF4352F6CE5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -86,6 +90,14 @@ Global {83EC18A4-831F-4939-9912-D8A93FA3998D}.Debug|Any CPU.Build.0 = Debug|Any CPU {83EC18A4-831F-4939-9912-D8A93FA3998D}.Release|Any CPU.ActiveCfg = Release|Any CPU {83EC18A4-831F-4939-9912-D8A93FA3998D}.Release|Any CPU.Build.0 = Release|Any CPU + {54DDEEE6-1F97-4E53-8018-5918F7D90A8B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {54DDEEE6-1F97-4E53-8018-5918F7D90A8B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {54DDEEE6-1F97-4E53-8018-5918F7D90A8B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {54DDEEE6-1F97-4E53-8018-5918F7D90A8B}.Release|Any CPU.Build.0 = Release|Any CPU + {EE523D3E-4A5D-4E01-9FB2-7BF4352F6CE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EE523D3E-4A5D-4E01-9FB2-7BF4352F6CE5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EE523D3E-4A5D-4E01-9FB2-7BF4352F6CE5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EE523D3E-4A5D-4E01-9FB2-7BF4352F6CE5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/webapp-net/DxaModulesCommon.Props b/webapp-net/DxaModulesCommon.Props new file mode 100644 index 000000000..3075e41be --- /dev/null +++ b/webapp-net/DxaModulesCommon.Props @@ -0,0 +1,20 @@ + + + + + + 2.1.0-beta-201804241437 + 2.1.0-beta-201805111302 + 11.0.0-beta-201805161005 + 9.0.0-beta-201804031026 + 8.5.0 + 10.1.0 + + + \ No newline at end of file diff --git a/webapp-net/ExperienceOptimization/Mapping/SmartTargetModelBuilder.cs b/webapp-net/ExperienceOptimization/Mapping/SmartTargetModelBuilder.cs index 2580b563a..050a75050 100644 --- a/webapp-net/ExperienceOptimization/Mapping/SmartTargetModelBuilder.cs +++ b/webapp-net/ExperienceOptimization/Mapping/SmartTargetModelBuilder.cs @@ -20,6 +20,7 @@ using MvcData = Sdl.Web.Common.Models.MvcData; using TcmUri = Tridion.SmartTarget.Utils.TcmUri; using System.Globalization; +using Sdl.Web.Common.Interfaces; namespace Sdl.Web.Modules.SmartTarget.Mapping { @@ -44,7 +45,7 @@ public class SmartTargetModelBuilder : IModelBuilder, IPageModelBuilder /// This implementation relies on the already having constructed Region Models of type . /// We "upgrade" the Page Model to type and populate the ST Regions Entities. /// - public void BuildPageModel(ref PageModel pageModel, IPage page, IEnumerable includes, Localization localization) + public void BuildPageModel(ref PageModel pageModel, IPage page, IEnumerable includes, ILocalization localization) { using (new Tracer(pageModel, page, includes, localization)) { @@ -99,12 +100,12 @@ public void BuildPageModel(ref PageModel pageModel, IPage page, IEnumerableThe DXA R2 Data Model. /// Indicates whether Include Page Regions should be included. /// The context . - public void BuildPageModel(ref PageModel pageModel, PageModelData pageModelData, bool includePageRegions, Localization localization) + public void BuildPageModel(ref PageModel pageModel, PageModelData pageModelData, bool includePageRegions, ILocalization localization) { using (new Tracer(pageModel, pageModelData, includePageRegions, localization)) { @@ -155,7 +156,7 @@ public void BuildPageModel(ref PageModel pageModel, PageModelData pageModelData, } #endregion - private void PopulateSmartTargetRegions(SmartTargetPageModel smartTargetPageModel, Localization localization) + private void PopulateSmartTargetRegions(SmartTargetPageModel smartTargetPageModel, ILocalization localization) { // Execute a ST Query for all SmartTargetRegions on the Page. ResultSet resultSet = ExecuteSmartTargetQuery(smartTargetPageModel, localization); @@ -232,7 +233,7 @@ private void PopulateSmartTargetRegions(SmartTargetPageModel smartTargetPageMode } } - protected virtual SmartTargetPromotion CreatePromotionEntity(Promotion promotion, string viewName, string regionName, Localization localization, ExperimentDimensions experimentDimensions) + protected virtual SmartTargetPromotion CreatePromotionEntity(Promotion promotion, string viewName, string regionName, ILocalization localization, ExperimentDimensions experimentDimensions) { // In ST 2014 SP1 the ResultSet.FilterPromotions API represents Experiments as type Promotion, so we're not testing on type Experiment here. SmartTargetPromotion result = (experimentDimensions != null) ? new SmartTargetExperiment(experimentDimensions) : new SmartTargetPromotion(); @@ -251,13 +252,13 @@ protected virtual SmartTargetPromotion CreatePromotionEntity(Promotion promotion return result; } - protected virtual SmartTargetItem CreateSmartTargetItem(Item item, Localization localization) + protected virtual SmartTargetItem CreateSmartTargetItem(Item item, ILocalization localization) { string entityId = $"{item.ComponentUri.ItemId}-{item.TemplateUri.ItemId}"; return new SmartTargetItem(entityId, localization); } - private static ResultSet ExecuteSmartTargetQuery(SmartTargetPageModel smartTargetPageModel, Localization localization) + private static ResultSet ExecuteSmartTargetQuery(SmartTargetPageModel smartTargetPageModel, ILocalization localization) { using (new Tracer(smartTargetPageModel, localization)) { @@ -282,7 +283,7 @@ private static ResultSet ExecuteSmartTargetQuery(SmartTargetPageModel smartTarge } } - private static bool GetAllowDuplicatesFromConfig(Localization localization) + private static bool GetAllowDuplicatesFromConfig(ILocalization localization) { string configValue = localization.GetConfigValue("smarttarget.allowDuplicationOnSamePageConfig"); return Convert.ToBoolean(configValue); @@ -294,7 +295,7 @@ private static bool GetAllowDuplicatesFromConfig(Localization localization) /// /// Used in R2 Model Builder Pipeline. /// - private static bool GetAllowDuplicatesOnSamePage(PageModelData pageModelData, Localization localization) + private static bool GetAllowDuplicatesOnSamePage(PageModelData pageModelData, ILocalization localization) { object allowDups; if ((pageModelData.Metadata == null) || !pageModelData.Metadata.TryGetValue(AllowDuplicatesFieldName, out allowDups)) @@ -318,7 +319,7 @@ private static bool GetAllowDuplicatesOnSamePage(PageModelData pageModelData, Lo /// /// Used in Legacy Model Builder Pipeline (DD4T-based) /// - private static bool GetAllowDuplicatesOnSamePage(IPageTemplate pageTemplate, Localization localization) + private static bool GetAllowDuplicatesOnSamePage(IPageTemplate pageTemplate, ILocalization localization) { string allowDuplicates = null; if ((pageTemplate?.MetadataFields != null) && pageTemplate.MetadataFields.ContainsKey(AllowDuplicatesFieldName)) diff --git a/webapp-net/ExperienceOptimization/Models/SmartTargetItem.cs b/webapp-net/ExperienceOptimization/Models/SmartTargetItem.cs index 79f9e1a4f..bb22553fc 100644 --- a/webapp-net/ExperienceOptimization/Models/SmartTargetItem.cs +++ b/webapp-net/ExperienceOptimization/Models/SmartTargetItem.cs @@ -1,13 +1,14 @@ using Sdl.Web.Common.Configuration; using Sdl.Web.Common.Models; using System; +using Sdl.Web.Common.Interfaces; namespace Sdl.Web.Modules.SmartTarget.Models { [Serializable] public class SmartTargetItem { - private readonly Localization _localization; + private readonly ILocalization _localization; private EntityModel _entity; public string EntityId { get; private set; } @@ -20,7 +21,7 @@ public class SmartTargetItem /// public EntityModel Entity => _entity ?? (_entity = SiteConfiguration.ContentProvider.GetEntityModel(EntityId, _localization)); - public SmartTargetItem(string entityId, Localization localization) + public SmartTargetItem(string entityId, ILocalization localization) { EntityId = entityId; _localization = localization; diff --git a/webapp-net/ExperienceOptimization/Models/SmartTargetPromotion.cs b/webapp-net/ExperienceOptimization/Models/SmartTargetPromotion.cs index 7d5eed7a1..b74da737e 100644 --- a/webapp-net/ExperienceOptimization/Models/SmartTargetPromotion.cs +++ b/webapp-net/ExperienceOptimization/Models/SmartTargetPromotion.cs @@ -1,8 +1,8 @@ using System; -using Sdl.Web.Common.Configuration; using Sdl.Web.Common.Models; using System.Collections.Generic; using Sdl.Web.Common; +using Sdl.Web.Common.Interfaces; namespace Sdl.Web.Modules.SmartTarget.Models { @@ -19,7 +19,7 @@ public class SmartTargetPromotion : EntityModel public List Items { get; set; } - public override string GetXpmMarkup(Localization localization) + public override string GetXpmMarkup(ILocalization localization) { return (XpmMetadata == null) ? string.Empty : string.Format(XpmMarkupFormat, XpmMetadata["PromotionID"], XpmMetadata["RegionID"]); } diff --git a/webapp-net/ExperienceOptimization/Models/SmartTargetRegion.cs b/webapp-net/ExperienceOptimization/Models/SmartTargetRegion.cs index 16dc10baf..19a8ef044 100644 --- a/webapp-net/ExperienceOptimization/Models/SmartTargetRegion.cs +++ b/webapp-net/ExperienceOptimization/Models/SmartTargetRegion.cs @@ -1,5 +1,5 @@ using System; -using Sdl.Web.Common.Configuration; +using Sdl.Web.Common.Interfaces; using Sdl.Web.Common.Models; namespace Sdl.Web.Modules.SmartTarget.Models @@ -41,7 +41,7 @@ public SmartTargetRegion(string name, string qualifiedViewName) /// public string GetStartQueryXpmMarkup() { - return (XpmMetadata == null) ? String.Empty : (string) XpmMetadata["Query"]; + return (XpmMetadata == null) ? string.Empty : (string) XpmMetadata["Query"]; } /// @@ -49,7 +49,7 @@ public string GetStartQueryXpmMarkup() /// /// The context Localization. /// The XPM markup. - public override string GetXpmMarkup(Localization localization) + public override string GetXpmMarkup(ILocalization localization) { return $""; } diff --git a/webapp-net/ExperienceOptimization/SDL.Web.Modules.SmartTarget.csproj b/webapp-net/ExperienceOptimization/SDL.Web.Modules.SmartTarget.csproj index 75438fedb..cba404461 100644 --- a/webapp-net/ExperienceOptimization/SDL.Web.Modules.SmartTarget.csproj +++ b/webapp-net/ExperienceOptimization/SDL.Web.Modules.SmartTarget.csproj @@ -1,6 +1,9 @@  + + + Debug AnyCPU @@ -14,10 +17,6 @@ - 10.1.0 - 2.0.0 - 2.0.0 - 8.5.0 true @@ -46,7 +45,7 @@ False - ..\packages\Sdl.Dxa.DataModel.$(SdlDxaDataModelPackageVersion)\lib\net452\Sdl.Web.DataModel.dll + ..\packages\Sdl.Dxa.DataModel.$(DxaDataModelVersion)\lib\net452\Sdl.Web.DataModel.dll False diff --git a/webapp-net/ExperienceOptimization/packages.config b/webapp-net/ExperienceOptimization/packages.config index 882483bf7..663cead72 100644 --- a/webapp-net/ExperienceOptimization/packages.config +++ b/webapp-net/ExperienceOptimization/packages.config @@ -6,8 +6,8 @@ - - + + - + \ No newline at end of file diff --git a/webapp-net/GoogleAnalytics/Sdl.Web.Modules.GoogleAnalytics.csproj b/webapp-net/GoogleAnalytics/Sdl.Web.Modules.GoogleAnalytics.csproj index c2dee1dad..296ac14a9 100644 --- a/webapp-net/GoogleAnalytics/Sdl.Web.Modules.GoogleAnalytics.csproj +++ b/webapp-net/GoogleAnalytics/Sdl.Web.Modules.GoogleAnalytics.csproj @@ -1,6 +1,9 @@  + + + Debug AnyCPU @@ -14,7 +17,6 @@ - 2.0.0 true diff --git a/webapp-net/GoogleAnalytics/packages.config b/webapp-net/GoogleAnalytics/packages.config index 2a6363325..8cefb486f 100644 --- a/webapp-net/GoogleAnalytics/packages.config +++ b/webapp-net/GoogleAnalytics/packages.config @@ -5,5 +5,5 @@ - + \ No newline at end of file diff --git a/webapp-net/Impress/Sdl.Web.Modules.Impress.csproj b/webapp-net/Impress/Sdl.Web.Modules.Impress.csproj index 86aa2aa7e..5e5fe76f9 100644 --- a/webapp-net/Impress/Sdl.Web.Modules.Impress.csproj +++ b/webapp-net/Impress/Sdl.Web.Modules.Impress.csproj @@ -1,6 +1,9 @@  + + + Debug AnyCPU @@ -14,7 +17,6 @@ - 2.0.0 true diff --git a/webapp-net/Impress/packages.config b/webapp-net/Impress/packages.config index 2a6363325..8cefb486f 100644 --- a/webapp-net/Impress/packages.config +++ b/webapp-net/Impress/packages.config @@ -5,5 +5,5 @@ - + \ No newline at end of file diff --git a/webapp-net/MediaManager/Models/MediaManagerDistribution.cs b/webapp-net/MediaManager/Models/MediaManagerDistribution.cs index adb0d83e2..3a25c0e35 100644 --- a/webapp-net/MediaManager/Models/MediaManagerDistribution.cs +++ b/webapp-net/MediaManager/Models/MediaManagerDistribution.cs @@ -2,7 +2,7 @@ using System.Globalization; using System.Xml; using Newtonsoft.Json; -using Sdl.Web.Common.Configuration; +using Sdl.Web.Common.Interfaces; using Sdl.Web.Common.Models; namespace Sdl.Web.Modules.MediaManager.Models @@ -134,7 +134,7 @@ public bool IsCustomVideoControls /// /// This makes it possible possible to render "embedded" MediaManagerDistribution Models using the Html.DxaEntity method. /// - public override MvcData GetDefaultView(Localization localization) + public override MvcData GetDefaultView(ILocalization localization) { return new MvcData("MediaManager:" + EclDisplayTypeId); } diff --git a/webapp-net/MediaManager/Sdl.Web.Modules.MediaManager.csproj b/webapp-net/MediaManager/Sdl.Web.Modules.MediaManager.csproj index ec7ea85ef..05866c5fb 100644 --- a/webapp-net/MediaManager/Sdl.Web.Modules.MediaManager.csproj +++ b/webapp-net/MediaManager/Sdl.Web.Modules.MediaManager.csproj @@ -1,6 +1,9 @@  + + + Debug AnyCPU @@ -13,8 +16,7 @@ 512 - - 2.0.0 + true diff --git a/webapp-net/MediaManager/packages.config b/webapp-net/MediaManager/packages.config index 2a6363325..8cefb486f 100644 --- a/webapp-net/MediaManager/packages.config +++ b/webapp-net/MediaManager/packages.config @@ -5,5 +5,5 @@ - + \ No newline at end of file diff --git a/webapp-net/Search/Controllers/TridionDocsSearchController.cs b/webapp-net/Search/Controllers/TridionDocsSearchController.cs new file mode 100644 index 000000000..801b6bcab --- /dev/null +++ b/webapp-net/Search/Controllers/TridionDocsSearchController.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Web.Mvc; +using Newtonsoft.Json; +using Sdl.Web.Delivery.IQQuery.API; +using Sdl.Web.Delivery.IQQuery.Client; +using Sdl.Web.Delivery.IQQuery.Model.Field; +using Sdl.Web.Delivery.IQQuery.Model.Result; +using Sdl.Web.Modules.Search.Data; +using Sdl.Web.Mvc.Controllers; +using System.Text.RegularExpressions; + +namespace Sdl.Web.Modules.Search.Controllers +{ + public class TridionDocsSearchController : BaseController + { + private static readonly Regex RegexpDoubleQuotes = new Regex("^\"(.*)\"$", RegexOptions.Compiled); + + [Route("~/api/search")] + [HttpPost] + public virtual ActionResult Search() + { + try + { + Stream req = Request.InputStream; + req.Seek(0, System.IO.SeekOrigin.Begin); + string json = new StreamReader(req).ReadToEnd(); + + SearchParameters searchParams = JsonConvert.DeserializeObject(json); + + IQSearchClient search = + new IQSearchClient(); + search.WithResultFilter(new SearchResultFilter + { + StartOfRange = searchParams.StartIndex, + EndOfRange = searchParams.StartIndex + searchParams.Count, + IsHighlightingEnabled = true + }); + var fields = new List(); + var values = new List(); + if (searchParams.PublicationId != null) + { + fields.Add("publicationId"); + values.Add(new DefaultTermValue(searchParams.PublicationId.Value.ToString())); + } + fields.Add("dynamic.FISHDITADLVRREMOTESTATUS.lng.element"); + fields.Add($"content.{CultureInfo.GetCultureInfo(searchParams.Language).EnglishName.ToLower()}"); + values.Add(new DefaultTermValue("VDITADLVRREMOTESTATUSONLINE")); + + string searchQuery = searchParams.SearchQuery; + Match match = RegexpDoubleQuotes.Match(searchQuery); + if (match.Success) + { + searchQuery = match.Groups[1].Value; + } + + values.Add(new DefaultTermValue(searchQuery, TermTypes.Exact)); + var results = search.WithResultFilter(new SearchResultFilter + { + StartOfRange = searchParams.StartIndex, + EndOfRange = searchParams.StartIndex + searchParams.Count, + IsHighlightingEnabled = true + }).Search(new Delivery.IQQuery.Model.Search.SearchQuery().GroupedAnd(fields, values).Compile()); + var resultSet = new SearchResultSetWrapped(results) + { + Hits = results.Hits, + Count = searchParams.Count.Value, + StartIndex = searchParams.StartIndex.Value + }; + return Json(resultSet); + } + catch (Exception) + { + Response.StatusCode = 405; + return new EmptyResult(); + } + } + } +} diff --git a/webapp-net/Search/Data/SearchParameters.cs b/webapp-net/Search/Data/SearchParameters.cs new file mode 100644 index 000000000..816b466a7 --- /dev/null +++ b/webapp-net/Search/Data/SearchParameters.cs @@ -0,0 +1,20 @@ +namespace Sdl.Web.Modules.Search.Data +{ + public class SearchParameters + { + private static readonly string DefaultLanguage = "en"; + private static readonly int DefaultStartIndex = 0; + private static readonly int DefaultResultCount = 10; + private static readonly string DefaultSearchQuery = ""; + + public int? PublicationId { get; set; } + + public string Language { get; set; } = DefaultLanguage; + + public string SearchQuery { get; set; } = DefaultSearchQuery; + + public int? StartIndex { get; set; } = DefaultStartIndex; + + public int? Count { get; set; } = DefaultResultCount; + } +} diff --git a/webapp-net/Search/Data/SearchResult.cs b/webapp-net/Search/Data/SearchResult.cs new file mode 100644 index 000000000..4c84a0a7f --- /dev/null +++ b/webapp-net/Search/Data/SearchResult.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using Sdl.Web.Delivery.IQQuery.API; + +namespace Sdl.Web.Modules.Search.Data +{ + public class SearchResult : IQueryResult + { + public string Id { get; set; } + + public Dictionary Fields { get; set; } + + public string Locale { get; set; } = "en"; + + public Dictionary> Highlighted { get; set; } + + public string Content { get; set; } + + public string CreatedDate { get; set; } + + public string ModifiedDate { get; set; } + + public int PublicationId { get; set; } + + public string PublicationTitle { get; set; } + + public string ProductFamilyName { get; set; } + + public string ProductReleaseName { get; set; } + } + + public class SearchResultWrapped : IQueryResult + { + private readonly SearchResult _wrapped; + + public SearchResultWrapped(SearchResult searchResult) + { + _wrapped = searchResult; + } + + public string Id => _wrapped.Id; + + public Dictionary Meta => _wrapped.Fields; + + public string Locale => _wrapped.Locale; + + public Dictionary> Highlighted => _wrapped.Highlighted; + + public string Content => _wrapped.Content; + + public string CreatedDate => _wrapped.CreatedDate; + + public string ModifiedDate => _wrapped.ModifiedDate; + + public int PublicationId => _wrapped.PublicationId; + + public string PublicationTitle => _wrapped.PublicationTitle; + + public string ProductFamilyName => _wrapped.ProductFamilyName; + + public string ProductReleaseName => _wrapped.ProductFamilyName; + } +} diff --git a/webapp-net/Search/Data/SearchResultSet.cs b/webapp-net/Search/Data/SearchResultSet.cs new file mode 100644 index 000000000..105961c82 --- /dev/null +++ b/webapp-net/Search/Data/SearchResultSet.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using Sdl.Web.Delivery.IQQuery.API; + +namespace Sdl.Web.Modules.Search.Data +{ + public class SearchResultSet : IQueryResultData + { + public int Hits { get; set; } + + public int Count { get; set; } + + public int StartIndex { get; set; } + + public IList QueryResults { get; set; } + } + + public class SearchResultSetWrapped : IQueryResultData + { + private readonly List _results; + public SearchResultSetWrapped(SearchResultSet searchResultSet) + { + _results = new List(); + foreach (var x in searchResultSet.QueryResults) + { + _results.Add(new SearchResultWrapped(x)); + } + } + + public int Hits { get; set; } + public int Count { get; set; } + public int StartIndex { get; set; } + + public IList QueryResults + { + get { return _results; } + set { } + } + } +} diff --git a/webapp-net/Search/Providers/AwsCloudSearchProvider.cs b/webapp-net/Search/Providers/AwsCloudSearchProvider.cs index cf6805371..0fce2c5ee 100644 --- a/webapp-net/Search/Providers/AwsCloudSearchProvider.cs +++ b/webapp-net/Search/Providers/AwsCloudSearchProvider.cs @@ -1,6 +1,6 @@ using System.Collections.Specialized; using System.Linq; -using Sdl.Web.Common.Configuration; +using Sdl.Web.Common.Interfaces; using Sdl.Web.Common.Logging; using Sdl.Web.Modules.Search.Models; using SI4T.Query.Models; @@ -9,7 +9,7 @@ namespace Sdl.Web.Modules.Search.Providers { public class AwsCloudSearchProvider : SI4TSearchProvider { - protected override NameValueCollection SetupParameters(SearchQuery searchQuery, Localization localization) + protected override NameValueCollection SetupParameters(SearchQuery searchQuery, ILocalization localization) { NameValueCollection result = base.SetupParameters(searchQuery, localization); if (!result.AllKeys.Contains("q.options")) diff --git a/webapp-net/Search/Providers/IQSearchProvider.cs b/webapp-net/Search/Providers/IQSearchProvider.cs new file mode 100644 index 000000000..5fd1d3851 --- /dev/null +++ b/webapp-net/Search/Providers/IQSearchProvider.cs @@ -0,0 +1,14 @@ +using System; +using Sdl.Web.Common.Interfaces; +using Sdl.Web.Modules.Search.Models; + +namespace Sdl.Web.Modules.Search.Providers +{ + public class IQSearchProvider : ISearchProvider + { + public void ExecuteQuery(SearchQuery searchQuery, Type resultType, ILocalization localization) + { + throw new NotImplementedException(); + } + } +} diff --git a/webapp-net/Search/Providers/ISearchProvider.cs b/webapp-net/Search/Providers/ISearchProvider.cs index 0a1ba5405..d7b60c847 100644 --- a/webapp-net/Search/Providers/ISearchProvider.cs +++ b/webapp-net/Search/Providers/ISearchProvider.cs @@ -1,11 +1,11 @@ using System; -using Sdl.Web.Common.Configuration; +using Sdl.Web.Common.Interfaces; using Sdl.Web.Modules.Search.Models; namespace Sdl.Web.Modules.Search.Providers { public interface ISearchProvider { - void ExecuteQuery(SearchQuery searchQuery, Type resultType, Localization localization); + void ExecuteQuery(SearchQuery searchQuery, Type resultType, ILocalization localization); } } diff --git a/webapp-net/Search/Providers/SI4TSearchProvider.cs b/webapp-net/Search/Providers/SI4TSearchProvider.cs index 2c08e2637..b848e6f1a 100644 --- a/webapp-net/Search/Providers/SI4TSearchProvider.cs +++ b/webapp-net/Search/Providers/SI4TSearchProvider.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Specialized; using System.Globalization; -using Sdl.Web.Common.Configuration; +using Sdl.Web.Common.Interfaces; using Sdl.Web.Common.Logging; using Sdl.Web.Common.Models; using Sdl.Web.Modules.Search.Models; @@ -15,7 +15,7 @@ namespace Sdl.Web.Modules.Search.Providers public abstract class SI4TSearchProvider : ISearchProvider { #region ISearchProvider members - public void ExecuteQuery(SearchQuery searchQuery, Type resultType, Localization localization) + public void ExecuteQuery(SearchQuery searchQuery, Type resultType, ILocalization localization) { using (new Tracer(searchQuery, resultType, localization)) { @@ -40,7 +40,7 @@ public void ExecuteQuery(SearchQuery searchQuery, Type resultType, Localization } #endregion - protected virtual string GetSearchIndexUrl(Localization localization) + protected virtual string GetSearchIndexUrl(ILocalization localization) { // First try the new search.queryURL setting provided by DXA 1.3 TBBs if the Search Query URL can be obtained from Topology Manager. string result = localization.GetConfigValue("search.queryURL"); @@ -66,7 +66,7 @@ protected virtual SearchItem MapResult(SearchResult result, Type modelType, stri return searchItem; } - protected virtual NameValueCollection SetupParameters(SearchQuery searchQuery, Localization localization) + protected virtual NameValueCollection SetupParameters(SearchQuery searchQuery, ILocalization localization) { NameValueCollection result = new NameValueCollection(searchQuery.QueryStringParameters); result["fq"] = "publicationid:" + localization.Id; // TODO: What about CM URI scheme? diff --git a/webapp-net/Search/Providers/SolrProvider.cs b/webapp-net/Search/Providers/SolrProvider.cs index a43a5b176..5d35bd80c 100644 --- a/webapp-net/Search/Providers/SolrProvider.cs +++ b/webapp-net/Search/Providers/SolrProvider.cs @@ -1,5 +1,5 @@ using System.Collections.Specialized; -using Sdl.Web.Common.Configuration; +using Sdl.Web.Common.Interfaces; using Sdl.Web.Common.Logging; using Sdl.Web.Modules.Search.Models; using SI4T.Query.Models; @@ -8,7 +8,7 @@ namespace Sdl.Web.Modules.Search.Providers { public class SolrProvider : SI4TSearchProvider { - protected override NameValueCollection SetupParameters(SearchQuery searchQuery, Localization localization) + protected override NameValueCollection SetupParameters(SearchQuery searchQuery, ILocalization localization) { NameValueCollection parameters = base.SetupParameters(searchQuery, localization); // We use the highlighting feature to autogenerate a Summary if no Summary is present in the search index. diff --git a/webapp-net/Search/SI4T.Query.CloudSearch/packages.config b/webapp-net/Search/SI4T.Query.CloudSearch/packages.config index ba5a8f787..9a8bfc3c8 100644 --- a/webapp-net/Search/SI4T.Query.CloudSearch/packages.config +++ b/webapp-net/Search/SI4T.Query.CloudSearch/packages.config @@ -2,5 +2,5 @@ - + \ No newline at end of file diff --git a/webapp-net/Search/Sdl.Web.Modules.Search.csproj b/webapp-net/Search/Sdl.Web.Modules.Search.csproj index a960b8013..e74f2d8ce 100644 --- a/webapp-net/Search/Sdl.Web.Modules.Search.csproj +++ b/webapp-net/Search/Sdl.Web.Modules.Search.csproj @@ -1,6 +1,7 @@  + Debug AnyCPU @@ -14,7 +15,6 @@ - 2.0.0 true @@ -42,6 +42,9 @@ ..\packages\Sdl.Dxa.Framework.Web8.$(DxaFrameworkVersion)\lib\net452\Sdl.Web.Common.dll False + + ..\packages\Sdl.Web.Delivery.$(SdlDeliveryVersion)\lib\net452\Sdl.Web.Delivery.IQQuery.dll + ..\packages\Sdl.Dxa.Framework.Web8.$(DxaFrameworkVersion)\lib\net452\Sdl.Web.Mvc.dll False @@ -59,8 +62,13 @@ + + + + + diff --git a/webapp-net/Search/packages.config b/webapp-net/Search/packages.config index 58f450302..9d2df9523 100644 --- a/webapp-net/Search/packages.config +++ b/webapp-net/Search/packages.config @@ -6,5 +6,5 @@ - + \ No newline at end of file diff --git a/webapp-net/Test/App.config b/webapp-net/Test/App.config index f780a1d05..c7c9fd1fc 100644 --- a/webapp-net/Test/App.config +++ b/webapp-net/Test/App.config @@ -46,48 +46,48 @@ - + - + - + - + - + - + - + - + - + - + - + diff --git a/webapp-net/Test/Models/TestFlickrImageModel.cs b/webapp-net/Test/Models/TestFlickrImageModel.cs index 8ec95a96d..4015f91d7 100644 --- a/webapp-net/Test/Models/TestFlickrImageModel.cs +++ b/webapp-net/Test/Models/TestFlickrImageModel.cs @@ -1,7 +1,7 @@ using System; using Sdl.Web.Common.Models; using System.Globalization; -using Sdl.Web.Common.Configuration; +using Sdl.Web.Common.Interfaces; namespace Sdl.Web.Modules.Test.Models { @@ -36,7 +36,7 @@ public string TestProperty4 set; } - public override MvcData GetDefaultView(Localization localization) + public override MvcData GetDefaultView(ILocalization localization) { return new MvcData("Test:TestFlickrImage"); } diff --git a/webapp-net/Test/Models/Tsi1758TestEntity.cs b/webapp-net/Test/Models/Tsi1758TestEntity.cs index 3bbf937a7..757c67d06 100644 --- a/webapp-net/Test/Models/Tsi1758TestEntity.cs +++ b/webapp-net/Test/Models/Tsi1758TestEntity.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Sdl.Web.Common.Configuration; +using Sdl.Web.Common.Interfaces; using Sdl.Web.Common.Models; namespace Sdl.Web.Modules.Test.Models @@ -18,7 +18,7 @@ public class Tsi1758TestEmbeddedEntity : EntityModel public Link EmbedField1 { get; set; } public Tsi1758TestEmbedded2Entity EmbedField2 { get; set; } - public override MvcData GetDefaultView(Localization localization) + public override MvcData GetDefaultView(ILocalization localization) { return new MvcData("Test:TSI1758TestEmbedded"); } @@ -30,7 +30,7 @@ public class Tsi1758TestEmbedded2Entity : EntityModel public string TextField { get; set; } public Link EmbedField2 { get; set; } - public override MvcData GetDefaultView(Localization localization) + public override MvcData GetDefaultView(ILocalization localization) { return new MvcData("Test:TSI1758TestEmbedded2"); } diff --git a/webapp-net/Test/Sdl.Web.Modules.Test.csproj b/webapp-net/Test/Sdl.Web.Modules.Test.csproj index 473e8e3ff..2afa70997 100644 --- a/webapp-net/Test/Sdl.Web.Modules.Test.csproj +++ b/webapp-net/Test/Sdl.Web.Modules.Test.csproj @@ -1,6 +1,9 @@  + + + Debug AnyCPU @@ -15,7 +18,6 @@ - 2.0.0 true diff --git a/webapp-net/Test/packages.config b/webapp-net/Test/packages.config index 2a6363325..8cefb486f 100644 --- a/webapp-net/Test/packages.config +++ b/webapp-net/Test/packages.config @@ -5,5 +5,5 @@ - + \ No newline at end of file diff --git a/webapp-net/TridionDocs/Areas/TridionDocs/Views/Page/ErrorPage.cshtml b/webapp-net/TridionDocs/Areas/TridionDocs/Views/Page/ErrorPage.cshtml new file mode 100644 index 000000000..64887740d --- /dev/null +++ b/webapp-net/TridionDocs/Areas/TridionDocs/Views/Page/ErrorPage.cshtml @@ -0,0 +1,29 @@ + + + + + + SDL Documentation Error + + + + + + + + + + +
+ + + + + + + + + diff --git a/webapp-net/TridionDocs/Areas/TridionDocs/Views/TridionDocsApi/ErrorPage.cshtml b/webapp-net/TridionDocs/Areas/TridionDocs/Views/TridionDocsApi/ErrorPage.cshtml new file mode 100644 index 000000000..8274961ab --- /dev/null +++ b/webapp-net/TridionDocs/Areas/TridionDocs/Views/TridionDocsApi/ErrorPage.cshtml @@ -0,0 +1,29 @@ + + + + + + SDL Documentation Error + + + + + + + + + + +
+ + + + + + + + + diff --git a/webapp-net/TridionDocs/Areas/TridionDocs/Views/TridionDocsApi/GeneralPage.cshtml b/webapp-net/TridionDocs/Areas/TridionDocs/Views/TridionDocsApi/GeneralPage.cshtml new file mode 100644 index 000000000..7309a6233 --- /dev/null +++ b/webapp-net/TridionDocs/Areas/TridionDocs/Views/TridionDocsApi/GeneralPage.cshtml @@ -0,0 +1,31 @@ + + + + + + SDL Documentation + + + + + + + + + + +
+ + + + + + + + + diff --git a/webapp-net/TridionDocs/Areas/TridionDocs/Views/TridionDocsPage/ErrorPage.cshtml b/webapp-net/TridionDocs/Areas/TridionDocs/Views/TridionDocsPage/ErrorPage.cshtml new file mode 100644 index 000000000..8274961ab --- /dev/null +++ b/webapp-net/TridionDocs/Areas/TridionDocs/Views/TridionDocsPage/ErrorPage.cshtml @@ -0,0 +1,29 @@ + + + + + + SDL Documentation Error + + + + + + + + + + +
+ + + + + + + + + diff --git a/webapp-net/TridionDocs/Areas/TridionDocs/Views/TridionDocsPage/GeneralPage.cshtml b/webapp-net/TridionDocs/Areas/TridionDocs/Views/TridionDocsPage/GeneralPage.cshtml new file mode 100644 index 000000000..7309a6233 --- /dev/null +++ b/webapp-net/TridionDocs/Areas/TridionDocs/Views/TridionDocsPage/GeneralPage.cshtml @@ -0,0 +1,31 @@ + + + + + + SDL Documentation + + + + + + + + + + +
+ + + + + + + + + diff --git a/webapp-net/TridionDocs/Areas/TridionDocs/Views/web.config b/webapp-net/TridionDocs/Areas/TridionDocs/Views/web.config new file mode 100644 index 000000000..c17eb43b6 --- /dev/null +++ b/webapp-net/TridionDocs/Areas/TridionDocs/Views/web.config @@ -0,0 +1,37 @@ + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/webapp-net/TridionDocs/Controllers/BaseController.cs b/webapp-net/TridionDocs/Controllers/BaseController.cs new file mode 100644 index 000000000..876813411 --- /dev/null +++ b/webapp-net/TridionDocs/Controllers/BaseController.cs @@ -0,0 +1,100 @@ +using System.Collections.Generic; +using System.Web.Mvc; +using Sdl.Web.Common.Configuration; +using Sdl.Web.Common.Interfaces; +using Sdl.Web.Common.Models; +using Sdl.Web.Modules.TridionDocs.Exceptions; +using Sdl.Web.Modules.TridionDocs.Navigation; +using Sdl.Web.Modules.TridionDocs.Providers; +using Sdl.Web.Mvc.Configuration; +using Tridion.ContentDelivery.Meta; + +namespace Sdl.Web.Modules.TridionDocs.Controllers +{ + [RouteArea("TridionDocs")] + public class BaseController : Mvc.Controllers.PageController + { + private static readonly string TocNaventriesMeta = "tocnaventries.generated.value"; + private static readonly string PageConditionsUsedMeta = "conditionsused.generated.value"; + private static readonly string PageLogicalRefObjectId = "ishlogicalref.object.id"; + + private TridionDocsContentProvider _contentProvider; + + protected ILocalization SetupLocalization(int publicationId) + { + PublicationProvider provider = new PublicationProvider(); + provider.CheckPublicationOnline(publicationId); + ILocalization localization = WebRequestContext.Localization; + localization.Id = publicationId.ToString(); + return localization; + } + + protected override ViewModel EnrichModel(ViewModel model) + { + PageModel pageModel = model as PageModel; + if (pageModel == null) + { + return model; + } + ILocalization localization = WebRequestContext.Localization; + PageMetaFactory pageMetaFactory = new PageMetaFactory(localization.Id); + var pageMeta = pageMetaFactory.GetMeta(pageModel.Id); + if (pageMeta != null) + { + var customMeta = pageMeta.CustomMeta; + + if (customMeta.GetFirstValue(TocNaventriesMeta) != null) + { + // Take the generated product family name from the metadata + NameValuePair tocNavEntries = (NameValuePair)customMeta.NameValues[TocNaventriesMeta]; + List values = (List) tocNavEntries?.MultipleValues; + if (values != null) + { + pageModel.Meta.Add(TocNaventriesMeta, string.Join(", ", values)); + } + } + + // Put the information about used conditions form page metadata + if (customMeta.GetFirstValue(PageConditionsUsedMeta) != null) + { + pageModel.Meta.Add(PageConditionsUsedMeta, (string)customMeta.GetFirstValue(PageConditionsUsedMeta)); + } + + // Add logical Ref ID information + if (customMeta.GetFirstValue(PageLogicalRefObjectId) != null) + { + pageModel.Meta.Add(PageLogicalRefObjectId, (string)customMeta.GetFirstValue(PageLogicalRefObjectId)); + } + } + return model; + } + + protected TridionDocsContentProvider TridionDocsContentProvider + { + get + { + if (!(ContentProvider is TridionDocsContentProvider)) + { + throw new TridionDocsApiException( + "TridionDocsContentProvider not configured. Please make sure you have the TridionDocsContentProvider specified in your Unity.config"); + } + return ContentProvider as TridionDocsContentProvider; + } + } + + protected TridionDocsNavigationProvider TridionDocsNavigationProvider + { + get + { + INavigationProvider navProvider = SiteConfiguration.NavigationProvider; + if (!(navProvider is TridionDocsNavigationProvider)) + { + throw new TridionDocsApiException( + "TridionDocsNavigationProvider not configured. Please make sure you have the TridionDocsNavigationProvider specified in your Unity.config"); + + } + return navProvider as TridionDocsNavigationProvider; + } + } + } +} diff --git a/webapp-net/TridionDocs/Controllers/TridionDocsApiController.cs b/webapp-net/TridionDocs/Controllers/TridionDocsApiController.cs new file mode 100644 index 000000000..fc0f6adac --- /dev/null +++ b/webapp-net/TridionDocs/Controllers/TridionDocsApiController.cs @@ -0,0 +1,220 @@ +using System; +using System.Web.Mvc; +using Sdl.Web.Common; +using Sdl.Web.Common.Models; +using Sdl.Web.Delivery.ServicesCore.ClaimStore; +using Sdl.Web.Modules.TridionDocs.Providers; +using Sdl.Web.Mvc.Configuration; +using Sdl.Web.Mvc.Formats; +using System.Text; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Sdl.Web.Common.Logging; +using Sdl.Web.Modules.TridionDocs.Exceptions; + +namespace Sdl.Web.Modules.TridionDocs.Controllers +{ + /// + /// TridionDocs Api Controller + /// + public class TridionDocsApiController : BaseController + { + private static readonly Uri UserConditionsUri = new Uri("taf:ish:userconditions"); + + [Route("~/api/page/{publicationId:int}/{pageId:int}")] + [HttpGet] + public virtual ActionResult Page(int publicationId, int pageId) + { + try + { + PageModel model = TridionDocsContentProvider.GetPageModel(pageId, SetupLocalization(publicationId)); + WebRequestContext.PageModel = model; + return Json(model); + } + catch(Exception ex) + { + Log.Error(ex); + return ServerError(new TridionDocsApiException($"Page not found: [{publicationId}] {pageId}/index.html")); + } + } + + [Route("~/api/page/{publicationId}/{pageId}")] + [HttpGet] + public virtual ActionResult Page(string publicationId, string pageId) + { + return ServerError(new TridionDocsApiException($"Page not found: [{publicationId}] {pageId}/index.html"), 400); + } + + [Route("~/api/page/{publicationId:int}/{pageId:int}/{*content}")] + [HttpPost] + public virtual ActionResult Page(int publicationId, int pageId, string content) + { + try + { + string conditions = Request.QueryString["conditions"]; + if (!string.IsNullOrEmpty(conditions)) + { + AmbientDataContext.CurrentClaimStore.Put(UserConditionsUri, conditions); + } + PageModel model = TridionDocsContentProvider.GetPageModel(pageId, SetupLocalization(publicationId)); + WebRequestContext.PageModel = model; + return Json(model); + } + catch (Exception ex) + { + Log.Error(ex); + return ServerError(ex); + } + } + + [Route("~/binary/{publicationId:int}/{binaryId:int}/{*content}")] + [Route("~/api/binary/{publicationId:int}/{binaryId:int}/{*content}")] + [HttpGet] + [FormatData] + public virtual ActionResult Binary(int publicationId, int binaryId) + { + try + { + StaticContentItem content = TridionDocsContentProvider.GetStaticContentItem(binaryId, + SetupLocalization(publicationId)); + return new FileStreamResult(content.GetContentStream(), content.ContentType); + } + catch (Exception ex) + { + return ServerError(ex); + } + } + + [Route("~/binary/{publicationId}/{binaryId}")] + [Route("~/api/binary/{publicationId}/{binaryId}")] + [HttpGet] + [FormatData] + public virtual ActionResult Binary(string publicationId, string binaryId) + { + return ServerError(null, 400); + } + + [Route("~/api/publications")] + [HttpGet] + public virtual ActionResult Publications() + { + try + { + PublicationProvider provider = new PublicationProvider(); + return JsonResult(provider.PublicationList); + } + catch (Exception ex) + { + return ServerError(ex); + } + } + + [Route("~/api/conditions/{publicationId:int}")] + public virtual ActionResult Conditions(int publicationId) + { + try + { + return new ContentResult + { + ContentType = "application/json", + Content = new ConditionProvider().GetConditions(publicationId), + ContentEncoding = Encoding.UTF8 + }; + } + catch (Exception ex) + { + return ServerError(ex); + } + } + + [Route("~/api/sitemap.xml")] + public virtual ActionResult SitemapXml() + { + // Use the common SiteMapXml view for rendering out the xml of all the sitemap items. + return View("SiteMapXml", TridionDocsNavigationProvider.SiteMap); + } + + [Route("~/api/toc/{publicationId:int}")] + public virtual ActionResult RootToc(int publicationId, string conditions = "") + { + try + { + SetupLocalization(publicationId); + if (!string.IsNullOrEmpty(conditions)) + { + AmbientDataContext.CurrentClaimStore.Put(UserConditionsUri, conditions); + } + TocProvider tocProvider = new TocProvider(); + return Json(tocProvider.GetToc(publicationId)); + } + catch (Exception ex) + { + return ServerError(ex); + } + } + + [Route("~/api/toc/{publicationId:int}/{sitemapItemId}")] + public virtual ActionResult Toc(int publicationId, string sitemapItemId, string conditions = "", + bool includeAncestors = false) + { + try + { + SetupLocalization(publicationId); + if (!string.IsNullOrEmpty(conditions)) + { + AmbientDataContext.CurrentClaimStore.Put(UserConditionsUri, conditions); + } + TocProvider tocProvider = new TocProvider(); + var sitemapItems = tocProvider.GetToc(publicationId, sitemapItemId, includeAncestors); + return Json(sitemapItems); + } + catch (Exception ex) + { + return ServerError(ex); + } + } + + [Route("~/api/toc/{publicationId}/{sitemapItemId}")] + public virtual ActionResult Toc(string publicationId, string sitemapItemId) + { + return ServerError(null, 400); + } + + [Route("~/api/pageIdByReference/{publicationId:int}/{ishFieldValue}")] + public virtual ActionResult TopicIdInTargetPublication(int publicationId, string ishFieldValue) + { + try + { + SetupLocalization(publicationId); + if (!string.IsNullOrEmpty(ishFieldValue)) + { + throw new DxaItemNotFoundException( + "Unable to use empty 'ishlogicalref.object.id' value as a search criteria."); + } + return Json(TridionDocsContentProvider.GetPageIdByIshLogicalReference(publicationId, ishFieldValue)); + } + catch (Exception ex) + { + return ServerError(ex); + } + } + + public ActionResult ServerError(Exception ex, int statusCode = 404) + { + Response.StatusCode = statusCode; + if(ex == null) return new EmptyResult(); + if (ex.InnerException != null) ex = ex.InnerException; + return Content("{ \"Message\": \"" + ex.Message + "\" }", "application/json"); + } + + private ContentResult JsonResult(object result) + { + return new ContentResult + { + ContentType = "application/json", + Content = JsonConvert.SerializeObject(result, new IsoDateTimeConverter() { DateTimeFormat = "yyyy-MM-ddThh:mm:ssZ" }), + ContentEncoding = Encoding.UTF8 + }; + } + } +} diff --git a/webapp-net/TridionDocs/Controllers/TridionDocsPageController.cs b/webapp-net/TridionDocs/Controllers/TridionDocsPageController.cs new file mode 100644 index 000000000..ac73e1ace --- /dev/null +++ b/webapp-net/TridionDocs/Controllers/TridionDocsPageController.cs @@ -0,0 +1,89 @@ +using System; +using System.Web.Mvc; +using Sdl.Web.Common; +using Sdl.Web.Common.Interfaces; +using Sdl.Web.Common.Logging; +using Sdl.Web.Common.Models; +using Sdl.Web.Mvc.Configuration; + +namespace Sdl.Web.Modules.TridionDocs.Controllers +{ + /// + /// TridionDocs Page Controller + /// + public class TridionDocsPageController : BaseController + { + [Route("~/")] + [Route("~/home")] + [Route("~/publications/{*content}")] + public ActionResult Home() + { + return View("GeneralPage"); + } + + [Route("~/{publicationId:int}")] + public virtual ActionResult Page(int publicationId) + { + return GetPage(publicationId); + } + + [Route("~/{publicationId:int}/{pageId:int}")] + [Route("~/{publicationId:int}/{pageId:int}/{*path}")] + public virtual ActionResult Page(int publicationId, int pageId, string path = "") + { + return GetPage(publicationId, pageId); + } + + protected ActionResult GetPage(int publicationId) + { + SetupLocalization(publicationId); + return View("GeneralPage"); + } + + protected ActionResult GetPage(int publicationId, int pageId) + { + using (new Tracer(publicationId, pageId)) + { + try + { + ILocalization localization = SetupLocalization(publicationId); + + PageModel pageModel; + try + { + pageModel = TridionDocsContentProvider.GetPageModel(pageId, localization); + } + catch (DxaItemNotFoundException ex) + { + Log.Info(ex.Message); + return NotFound(); + } + + PageModelWithHttpResponseData pageModelWithHttpResponseData = + pageModel as PageModelWithHttpResponseData; + pageModelWithHttpResponseData?.SetHttpResponseData(System.Web.HttpContext.Current.Response); + SetupViewData(pageModel); + PageModel model = (EnrichModel(pageModel) as PageModel) ?? pageModel; + WebRequestContext.PageModel = model; + return View(model.MvcData.ViewName, model); + } + catch (Exception ex) + { + Log.Error(ex); + return ServerError(); + } + } + } + + public ActionResult ServerError() + { + using (new Tracer()) + { + Response.StatusCode = 404; + ViewResult r = View("ErrorPage"); + r.ViewData.Add("statusCode", Response.StatusCode); + return r; + } + } + } +} diff --git a/webapp-net/TridionDocs/Exceptions/TridionDocsApiException.cs b/webapp-net/TridionDocs/Exceptions/TridionDocsApiException.cs new file mode 100644 index 000000000..f79a2cbd3 --- /dev/null +++ b/webapp-net/TridionDocs/Exceptions/TridionDocsApiException.cs @@ -0,0 +1,22 @@ +using System; + +namespace Sdl.Web.Modules.TridionDocs.Exceptions +{ + /// + /// TridionDocs Api Exception + /// + public class TridionDocsApiException : Exception + { + public TridionDocsApiException() + { + } + + public TridionDocsApiException(string msg) : base(msg) + { + } + + public TridionDocsApiException(string msg, Exception innerException) : base(msg, innerException) + { + } + } +} diff --git a/webapp-net/TridionDocs/Localization/TridionDocsLocalization.cs b/webapp-net/TridionDocs/Localization/TridionDocsLocalization.cs new file mode 100644 index 000000000..8993e6e74 --- /dev/null +++ b/webapp-net/TridionDocs/Localization/TridionDocsLocalization.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using Sdl.Web.Common.Configuration; +using Sdl.Web.Common.Logging; + +namespace Sdl.Web.Modules.TridionDocs.Localization +{ + /// + /// TridionDocs Localization Implementation + /// + public class TridionDocsLocalization : Common.Configuration.Localization + { + public override string Path { get; set; } = ""; // content path + + public override string CmUriScheme { get; } = "ish"; + + public override bool IsXpmEnabled { get; set; } = false; // no xpm on dd-webapp + + public override string BinaryCacheFolder => $"{SiteConfiguration.StaticsFolder}\\TridionDocs"; + + protected override void Load() + { + using (new Tracer(this)) + { + LastRefresh = DateTime.Now; + } + } + + public override IDictionary GetResources(string sectionName = null) + => new Hashtable(); // no resources so return empty hash to avoid default impl + + public override bool IsStaticContentUrl(string urlPath) + { + List mediaPatterns = new List(); + mediaPatterns.Add("^/favicon.ico"); + mediaPatterns.Add($"^{Path}/{SiteConfiguration.SystemFolder}/assets/.*"); + mediaPatterns.Add($"^{Path}/{SiteConfiguration.SystemFolder}/.*\\.json$"); + StaticContentUrlPattern = string.Join("|", mediaPatterns); + Regex staticContentUrlRegex = new Regex(StaticContentUrlPattern, RegexOptions.IgnoreCase | RegexOptions.Compiled); + return staticContentUrlRegex.IsMatch(urlPath); + } + } +} \ No newline at end of file diff --git a/webapp-net/TridionDocs/Localization/TridionDocsLocalizationResolver.cs b/webapp-net/TridionDocs/Localization/TridionDocsLocalizationResolver.cs new file mode 100644 index 000000000..e9681ef45 --- /dev/null +++ b/webapp-net/TridionDocs/Localization/TridionDocsLocalizationResolver.cs @@ -0,0 +1,25 @@ +using System; +using Sdl.Web.Common.Interfaces; +using Sdl.Web.Tridion; + +namespace Sdl.Web.Modules.TridionDocs.Localization +{ + /// + /// TridionDocs Localization Resolver + /// + public class TridionDocsLocalizationResolver : LocalizationResolver + { + private readonly ILocalization _localization; + + public TridionDocsLocalizationResolver() + { + _localization = new TridionDocsLocalization(); + _localization.EnsureInitialized(); + } + + public override ILocalization ResolveLocalization(Uri url) + { + return _localization; + } + } +} diff --git a/webapp-net/TridionDocs/Mapping/TridionDocsModelBuilder.cs b/webapp-net/TridionDocs/Mapping/TridionDocsModelBuilder.cs new file mode 100644 index 000000000..0cd547608 --- /dev/null +++ b/webapp-net/TridionDocs/Mapping/TridionDocsModelBuilder.cs @@ -0,0 +1,46 @@ +using System; +using Sdl.Web.Common.Interfaces; +using Sdl.Web.Common.Models; +using Sdl.Web.DataModel; +using Sdl.Web.Tridion.Mapping; + +namespace Sdl.Web.Modules.TridionDocs.Mapping +{ + /// + /// Tridion Docs Model Builder + /// + /// Remaps 'Ish' mvc area to TridionDocs + /// + /// Add to Web.Config: + /// + /// + /// + /// ... + /// + /// + public class TridionDocsModelBuilder : IPageModelBuilder, IEntityModelBuilder + { + public void BuildPageModel(ref PageModel pageModel, PageModelData pageModelData, bool includePageRegions, + ILocalization localization) + { + DataModel.MvcData mvcData = pageModelData.MvcData; + RemapMvcAreaName(ref mvcData); + } + + public void BuildEntityModel(ref EntityModel entityModel, EntityModelData entityModelData, Type baseModelType, + ILocalization localization) + { + DataModel.MvcData mvcData = entityModelData.MvcData; + RemapMvcAreaName(ref mvcData); + } + + private void RemapMvcAreaName(ref DataModel.MvcData mvcData) + { + if (mvcData != null && mvcData.AreaName != null && + mvcData.AreaName.Equals("Ish")) + { + mvcData.AreaName = TridionDocsModuleAreaRegistration.AREA_NAME; + } + } + } +} diff --git a/webapp-net/TridionDocs/Models/Publication.cs b/webapp-net/TridionDocs/Models/Publication.cs new file mode 100644 index 000000000..6b09b8a3a --- /dev/null +++ b/webapp-net/TridionDocs/Models/Publication.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; + +namespace Sdl.Web.Modules.TridionDocs.Models +{ + /// + /// Publication + /// + public class Publication + { + public string Id { get; set; } + public string Title { get; set; } + public List ProductFamily { get; set; } + public List ProductReleaseVersion { get; set; } + public string VersionRef { get; set; } + public string Language { get; set; } + public DateTime CreatedOn { get; set; } + public string Version { get; set; } + public string LogicalId { get; set; } + } +} diff --git a/webapp-net/TridionDocs/Models/PublicationSiteMap.cs b/webapp-net/TridionDocs/Models/PublicationSiteMap.cs new file mode 100644 index 000000000..aa58b8655 --- /dev/null +++ b/webapp-net/TridionDocs/Models/PublicationSiteMap.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Sdl.Web.Common.Models; + +namespace Sdl.Web.Modules.TridionDocs.Models +{ + public class PublicationSiteMap : EntityModel + { + public int PublicationId { get; set; } + public int NamespaceId { get; set; } + public List Urls { get; set; } = new List(); + } +} diff --git a/webapp-net/TridionDocs/Models/SiteMapUrlEntry.cs b/webapp-net/TridionDocs/Models/SiteMapUrlEntry.cs new file mode 100644 index 000000000..1de08fe71 --- /dev/null +++ b/webapp-net/TridionDocs/Models/SiteMapUrlEntry.cs @@ -0,0 +1,10 @@ +using System; + +namespace Sdl.Web.Modules.TridionDocs.Models +{ + public class SiteMapUrlEntry + { + public string Url { get; set; } + public DateTime LastModifiedDate { get; set; } + } +} diff --git a/webapp-net/TridionDocs/Models/Topic.cs b/webapp-net/TridionDocs/Models/Topic.cs new file mode 100644 index 000000000..55868dab2 --- /dev/null +++ b/webapp-net/TridionDocs/Models/Topic.cs @@ -0,0 +1,22 @@ +using System; +using Newtonsoft.Json; +using Sdl.Web.Common.Models; + +namespace Sdl.Web.Modules.TridionDocs.Models +{ + /// + /// Topic Entity + /// + [SemanticEntity(Vocab = SchemaOrgVocabulary, EntityName = "Topic", Prefix = "s", Public = true)] + [Serializable] + public class Topic : EntityModel + { + [SemanticProperty("topicBody")] + [JsonProperty(PropertyName = "topicBody")] + public RichText TopicBody { get; set; } + + [SemanticProperty("topicTitle")] + [JsonProperty(PropertyName = "topicTitle")] + public string TopicTitle { get; set; } + } +} diff --git a/webapp-net/TridionDocs/Navigation/TridionDocsNavigationProvider.cs b/webapp-net/TridionDocs/Navigation/TridionDocsNavigationProvider.cs new file mode 100644 index 000000000..692481f30 --- /dev/null +++ b/webapp-net/TridionDocs/Navigation/TridionDocsNavigationProvider.cs @@ -0,0 +1,91 @@ +using System.Collections.Generic; +using System.Linq; +using System.Web; +using Newtonsoft.Json; +using Sdl.Web.Common.Interfaces; +using Sdl.Web.Common.Models; +using Sdl.Web.Common.Models.Navigation; +using Sdl.Web.Modules.TridionDocs.Models; +using Sdl.Web.Tridion.ContentManager; +using Tridion.ContentDelivery.Meta; +using Tridion.ContentDelivery.Taxonomies; +using System.Text.RegularExpressions; +using Sdl.Web.Modules.TridionDocs.Exceptions; + +namespace Sdl.Web.Modules.TridionDocs.Navigation +{ + /// + /// Tridion Docs Navigation Provider + /// + public class TridionDocsNavigationProvider : Tridion.Navigation.CILImpl.DynamicNavigationProvider + { + private static readonly Regex RegEx = new Regex("^(?:\\w)(\\d+)(?:-\\w)(\\d+)", RegexOptions.Compiled); + + public string GetBaseUrl() + { + var request = HttpContext.Current.Request; + var appUrl = HttpRuntime.AppDomainAppVirtualPath; + + if (appUrl != "/") + appUrl = "/" + appUrl; + + var baseUrl = $"{request.Url.Scheme}://{request.Url.Authority}{appUrl}"; + + return baseUrl; + } + + protected override List SortTaxonomyNodes(IList taxonomyNodes) + // Sort by topic id since the base impl sorts alphabetically using the title + => taxonomyNodes.OrderBy(x => int.Parse(RegEx.Match(x.Id).Groups[1].Value)). + ThenBy(x => int.Parse(RegEx.Match(x.Id).Groups[2].Value)).ToList(); + + protected override TaxonomyNode CreateTaxonomyNode(Keyword keyword, int expandLevels, NavigationFilter filter, ILocalization localization) + { + TaxonomyNode node = base.CreateTaxonomyNode(keyword, expandLevels, filter, localization); + string ishRefUri = (string)keyword.KeywordMeta.GetFirstValue("ish.ref.uri"); + if (ishRefUri != null) + { + var ish = CmUri.FromString(ishRefUri); + node.Url = $"/{ish.PublicationId}/{ish.ItemId}"; + } + node.Visible = true; + return node; + } + + protected override IEnumerable ExpandDescendants(string keywordUri, string taxonomyUri, + NavigationFilter filter, ILocalization localization) + { + TaxonomyFactory taxonomyFactory = new TaxonomyFactory(); + TaxonomyFilter taxonomyFilter = new DepthFilter(filter.DescendantLevels, DepthFilter.FilterDown); + Keyword contextKeyword = taxonomyFactory.GetTaxonomyKeywords(taxonomyUri, taxonomyFilter, keywordUri); + if (contextKeyword == null) + { + throw new TridionDocsApiException(); + } + + TaxonomyNode contextTaxonomyNode = CreateTaxonomyNode(contextKeyword, filter.DescendantLevels, filter, + localization); + return contextTaxonomyNode.Items; + } + + protected override SitemapItem[] ExpandClassifiedPages(Keyword keyword, string taxonomyId, + ILocalization localization) + => new SitemapItem[] {}; + + public SitemapItem SiteMap + { + get + { + SitemapItem root = new SitemapItem(); + PublicationMetaFactory factory = new PublicationMetaFactory(); + string json = factory.GetSiteMapForPublication(-1); + List siteMap = JsonConvert.DeserializeObject>(json); + foreach (var url in siteMap.SelectMany(x => x.Urls)) + { + root.Items.Add(new SitemapItem {Type = "Page", Url = $"/{url.Url.TrimStart('/')}", PublishedDate = url.LastModifiedDate}); + } + return root; + } + } + } +} diff --git a/webapp-net/TridionDocs/Properties/AssemblyInfo.cs b/webapp-net/TridionDocs/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..8d767c18b --- /dev/null +++ b/webapp-net/TridionDocs/Properties/AssemblyInfo.cs @@ -0,0 +1,5 @@ +using System.Reflection; + +[assembly: AssemblyTitle("SDL DXA DD Web App Module")] + +// NOTE: Common Assembly Info is generated by the build in CommonAssemblyInfo.cs diff --git a/webapp-net/TridionDocs/Providers/ConditionProvider.cs b/webapp-net/TridionDocs/Providers/ConditionProvider.cs new file mode 100644 index 000000000..64d366fa4 --- /dev/null +++ b/webapp-net/TridionDocs/Providers/ConditionProvider.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using Sdl.Web.Modules.TridionDocs.Exceptions; +using Tridion.ContentDelivery.Meta; + +namespace Sdl.Web.Modules.TridionDocs.Providers +{ + /// + /// Condition Provider + /// + public class ConditionProvider + { + private static readonly string ConditionUsed = "conditionsused.generated.value"; + private static readonly string ConditionMetadata = "conditionmetadata.generated.value"; + private static readonly string ConditionValues = "values"; + + private class Condition + { + [JsonProperty("datatype")] + public string Datatype { get; set; } + [JsonProperty("range")] + public bool Range { get; set; } + [JsonProperty("values")] + public string[] Values { get; set; } + } + + public string GetConditions(int publicationId) + { + var conditionUsed = GetMetadata(publicationId, ConditionUsed); + var conditionMetadata = GetMetadata(publicationId, ConditionMetadata); + Dictionary d1 = + JsonConvert.DeserializeObject>(conditionUsed); + Dictionary d2 = + JsonConvert.DeserializeObject>(conditionMetadata); + foreach (var v in d1) + { + d2[v.Key].Values = v.Value; + } + return JsonConvert.SerializeObject(d2); + } + + private string GetMetadata(int publicationId, string metadataName) + { + try + { + PublicationMetaFactory factory = new PublicationMetaFactory(); + PublicationMeta meta = factory.GetMeta(publicationId); + if (meta?.CustomMeta == null) + { + throw new TridionDocsApiException( + $"Metadata '{metadataName}' is not found for publication {publicationId}."); + } + + object metadata = meta.CustomMeta.GetFirstValue(metadataName); + string metadataString = metadata != null ? (string) metadata : "{}"; + return metadataString; + } + catch (Exception) + { + throw new TridionDocsApiException( + $"Metadata '{metadataName}' is not found for publication {publicationId}."); + } + } + } +} diff --git a/webapp-net/TridionDocs/Providers/PublicationProvider.cs b/webapp-net/TridionDocs/Providers/PublicationProvider.cs new file mode 100644 index 000000000..45ab44b77 --- /dev/null +++ b/webapp-net/TridionDocs/Providers/PublicationProvider.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Sdl.Web.Common.Logging; +using Sdl.Web.Modules.TridionDocs.Models; +using Tridion.ContentDelivery.Meta; +using Sdl.Web.Modules.TridionDocs.Exceptions; + +namespace Sdl.Web.Modules.TridionDocs.Providers +{ + /// + /// Publication Provider + /// + public class PublicationProvider + { + private static readonly string PublicationTitleMeta = "publicationtitle.generated.value"; + private static readonly string PublicationProductfamilynameMeta = "FISHPRODUCTFAMILYNAME.logical.value"; + private static readonly string PublicationProductreleasenameMeta = "FISHPRODUCTRELEASENAME.version.value"; + private static readonly string PublicationVersionrefMeta = "ishversionref.object.id"; + private static readonly string PublicationLangMeta = "FISHPUBLNGCOMBINATION.lng.value"; + private static readonly string PublicationOnlineStatusMeta = "FISHDITADLVRREMOTESTATUS.lng.element"; + private static readonly string PublicationOnlineValue = "VDITADLVRREMOTESTATUSONLINE"; + private static readonly string PublicationCratedonMeta = "CREATED-ON.version.value"; + private static readonly string PublicationVersionMeta = "VERSION.version.value"; + private static readonly string PublicationLogicalId = "ishref.object.value"; + + public List PublicationList + { + get + { + PublicationMetaFactory factory = new PublicationMetaFactory(); + List result = new List(); + try + { + PublicationMeta[] publicationMetas = factory.GetAllMeta(new List + { + PublicationTitleMeta, + PublicationProductfamilynameMeta, + PublicationProductreleasenameMeta, + PublicationVersionrefMeta, + PublicationLangMeta, + PublicationOnlineStatusMeta, + PublicationCratedonMeta, + PublicationVersionMeta, + PublicationLogicalId + }); + + result.AddRange(from meta in publicationMetas where IsPublicationOnline(meta) select BuildPublicationFrom(meta)); + return result; + } + catch (Exception e) + { + throw new TridionDocsApiException("Unable to fetch list of publications.", e); + } + } + } + + public void CheckPublicationOnline(int publicationId) + { + PublicationMeta meta = null; + PublicationMetaFactory factory = new PublicationMetaFactory(); + try + { + meta = factory.GetMeta(publicationId); + } + catch (Exception) + { + Log.Error("Couldn't find publication metadata for id: " + publicationId); + } + if (meta == null || !IsPublicationOnline(meta)) + { + throw new TridionDocsApiException($"Unable to find publication {publicationId}"); + } + } + + public bool IsPublicationOnline(PublicationMeta publicationMeta) + { + var customMeta = publicationMeta.CustomMeta; + if (customMeta == null) return false; + try + { + var status = customMeta.GetFirstValue(PublicationOnlineStatusMeta); + return status != null && PublicationOnlineValue.Equals(status.ToString()); + } + catch (Exception) + { + return false; + } + } + + private Publication BuildPublicationFrom(PublicationMeta publicationMeta) + { + Publication publication = new Publication(); + publication.Id = publicationMeta.Id.ToString(); + var customMeta = publicationMeta.CustomMeta; + if (customMeta == null) return publication; + if (customMeta.GetFirstValue(PublicationTitleMeta) != null) + { + publication.Title = (string)customMeta.GetFirstValue(PublicationTitleMeta); + } + else + { + publication.Title = publicationMeta.Title; + } + + if (customMeta.GetFirstValue(PublicationProductfamilynameMeta) != null) + { + // Take the generated product family name from the metadata + NameValuePair pair = (NameValuePair)customMeta.NameValues[PublicationProductfamilynameMeta]; + publication.ProductFamily = new List(); + foreach (var value in pair.MultipleValues) + { + publication.ProductFamily.Add(value?.ToString()); + } + } + + if (customMeta.GetFirstValue(PublicationProductreleasenameMeta) != null) + { + // Take the generated product release name from the metadata + NameValuePair pair = (NameValuePair)customMeta.NameValues[PublicationProductreleasenameMeta]; + publication.ProductReleaseVersion = new List(); + foreach (var value in pair.MultipleValues) + { + publication.ProductReleaseVersion.Add(value?.ToString()); + } + } + + if (customMeta.GetFirstValue(PublicationVersionrefMeta) != null) + { + string versionRef = (string)customMeta.GetFirstValue(PublicationVersionrefMeta); + // The value is stored as float on Content Service, so we need to get rid of fractional part + publication.VersionRef = versionRef.Split('\"', '[', '.', ']')[0]; + } + + if (customMeta.GetFirstValue(PublicationLangMeta) != null) + { + publication.Language = (string)customMeta.GetFirstValue(PublicationLangMeta); + } + + if (customMeta.GetFirstValue(PublicationCratedonMeta) != null) + { + publication.CreatedOn = DateTime.Parse((string)customMeta.GetFirstValue(PublicationCratedonMeta)); + } + + if (customMeta.GetFirstValue(PublicationVersionMeta) != null) + { + publication.Version = (string)customMeta.GetFirstValue(PublicationVersionMeta); + } + + if (customMeta.GetFirstValue(PublicationLogicalId) != null) + { + publication.LogicalId = (string)customMeta.GetFirstValue(PublicationLogicalId); + } + return publication; + } + } +} diff --git a/webapp-net/TridionDocs/Providers/TocProvider.cs b/webapp-net/TridionDocs/Providers/TocProvider.cs new file mode 100644 index 000000000..10579f2e3 --- /dev/null +++ b/webapp-net/TridionDocs/Providers/TocProvider.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using Sdl.Web.Common.Configuration; +using Sdl.Web.Common.Interfaces; +using Sdl.Web.Common.Models; +using Sdl.Web.Common.Models.Navigation; +using Sdl.Web.Delivery.Service; +using Sdl.Web.Mvc.Configuration; + +namespace Sdl.Web.Modules.TridionDocs.Providers +{ + /// + /// Table of Contents (TOC) Provider + /// + public class TocProvider + { + public IEnumerable GetToc(int publicationId) + => GetToc(publicationId, null, false, 1); + + public IEnumerable GetToc(int publicationId, string sitemapItemId) + => GetToc(publicationId, sitemapItemId, false, 1); + + public IEnumerable GetToc(int publicationId, string sitemapItemId, bool includeAncestors) + => GetToc(publicationId, sitemapItemId, includeAncestors, 1); + + public IEnumerable GetToc(int publicationId, string sitemapItemId, bool includeAncestors, + int descendantLevels) + { + bool caching = ServiceCacheProvider.Instance.DisableCaching; + ServiceCacheProvider.Instance.DisableCaching = true; + IOnDemandNavigationProvider onDemandNavigationProvider = SiteConfiguration.NavigationProvider as IOnDemandNavigationProvider; + NavigationFilter navigationFilter = new NavigationFilter + { + DescendantLevels = descendantLevels, + IncludeAncestors = includeAncestors + }; + + ILocalization localization = WebRequestContext.Localization; + localization.Id = publicationId.ToString(); + + var result = onDemandNavigationProvider.GetNavigationSubtree(sitemapItemId, navigationFilter, localization); + ServiceCacheProvider.Instance.DisableCaching = caching; + return result; + } + } +} diff --git a/webapp-net/TridionDocs/Providers/TridionDocsContentProvider.cs b/webapp-net/TridionDocs/Providers/TridionDocsContentProvider.cs new file mode 100644 index 000000000..0101b94c4 --- /dev/null +++ b/webapp-net/TridionDocs/Providers/TridionDocsContentProvider.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections; +using System.Web; +using Sdl.Web.Common; +using Sdl.Web.Common.Interfaces; +using Sdl.Web.Common.Models; +using Sdl.Web.Delivery.Service; +using Sdl.Web.Modules.TridionDocs.Exceptions; +using Sdl.Web.Tridion.Mapping; +using Tridion.ContentDelivery.DynamicContent.Query; +using Tridion.ContentDelivery.Meta; +using Sdl.Web.Tridion.ContentManager; +using Query = Tridion.ContentDelivery.DynamicContent.Query.Query; + +namespace Sdl.Web.Modules.TridionDocs.Providers +{ + /// + /// TridionDocs Content Provider + /// + public class TridionDocsContentProvider : DefaultContentProvider + { + private static readonly string RefFieldName = "ishlogicalref.object.id"; + private static readonly string DefaultPublishData = "1900-01-01 00:00:00.000"; + private static readonly string TocNaventriesMeta = "tocnaventries.generated.value"; + private static readonly string PageConditionsUsedMeta = "conditionsused.generated.value"; + + public class ItemImpl : IItem + { + public void Dispose() + { + } + + public Category[] GetCategories() + { + return null; + } + + public int Id { get; } + public string Title { get; } + public int MinorVersion { get; } + public int MajorVersion { get; } + public DateTime ModificationDate { get; } + public DateTime InitialPublicationDate { get; } + public DateTime LastPublicationDate { get; } + public DateTime CreationDate { get; } + public int PublicationId { get; } + public int OwningPublicationId { get; } + } + + public override PageModel GetPageModel(int pageId, ILocalization localization, bool addIncludes = true) + { + bool caching = ServiceCacheProvider.Instance.DisableCaching; + ServiceCacheProvider.Instance.DisableCaching = true; + + PageModel pageModel = base.GetPageModel(pageId, localization, addIncludes); + if (pageModel != null) + { + // Enhance the page model with custom metadata + PageMetaFactory pageMetaFactory = new PageMetaFactory(int.Parse(localization.Id)); + string cmId = $"ish:{localization.Id}-{pageId}-16"; + var pageMeta = pageMetaFactory.GetMeta(cmId); + if (pageMeta != null) + { + var customMeta = pageMeta.CustomMeta; + + // Put the information about the toc entries on the metadata + // This is required by the UI so it knows the location of the page in the Toc + if (customMeta.GetFirstValue(TocNaventriesMeta) != null) + { + NameValuePair pair = (NameValuePair)customMeta.NameValues[TocNaventriesMeta]; + var values = (ArrayList) pair.MultipleValues; + if (values != null) + { + pageModel.Meta.Add(TocNaventriesMeta, string.Join(", ", values.ToArray())); + } + } + + // Put the information about used conditions form page metadata + if (customMeta.GetFirstValue(PageConditionsUsedMeta) != null) + { + pageModel.Meta.Add(PageConditionsUsedMeta, (string)customMeta.GetFirstValue(PageConditionsUsedMeta)); + } + + // Add logical Ref ID information + if (customMeta.GetFirstValue(RefFieldName) != null) + { + pageModel.Meta.Add(RefFieldName, (string)customMeta.GetFirstValue(RefFieldName)); + } + } + } + + ServiceCacheProvider.Instance.DisableCaching = caching; + return pageModel; + } + + public override PageModel GetPageModel(string urlPath, ILocalization localization, bool addIncludes = true) + { + HttpContext.Current.Response.StatusCode = 404; + return new PageModel + { + MvcData = new MvcData { ViewName = "ErrorPage", AreaName = "TridionDocs"} + }; + } + + public IItem GetPageIdByIshLogicalReference(int publicationId, string ishLogicalRefValue) + { + try + { + Criteria dateCriteria = new ItemLastPublishedDateCriteria(DefaultPublishData, Criteria.GreaterThanOrEqual); + CustomMetaKeyCriteria metaKeyCriteria = new CustomMetaKeyCriteria(RefFieldName); + Criteria refCriteria = new CustomMetaValueCriteria(metaKeyCriteria, ishLogicalRefValue); + Criteria pubCriteria = new PublicationCriteria(publicationId); + Criteria itemType = new ItemTypeCriteria((int)ItemType.Page); + Criteria composite = new AndCriteria(new[] { dateCriteria, refCriteria, itemType, pubCriteria}); + + Query query = new Query(composite); + IItem[] items = query.ExecuteEntityQuery(); + if (items == null || items.Length == 0) + { + return new ItemImpl(); + } + + if (items.Length > 1) + { + throw new TridionDocsApiException($"Too many page Ids found in publication with logical ref value {ishLogicalRefValue}"); + } + + return items[0]; + } + catch (Exception) + { + throw new DxaItemNotFoundException($"Page reference by ishlogicalref.object.id = {ishLogicalRefValue} not found in publication {publicationId}."); + } + } + } +} diff --git a/webapp-net/TridionDocs/Sdl.Web.Modules.TridionDocs.csproj b/webapp-net/TridionDocs/Sdl.Web.Modules.TridionDocs.csproj new file mode 100644 index 000000000..8bd3207e0 --- /dev/null +++ b/webapp-net/TridionDocs/Sdl.Web.Modules.TridionDocs.csproj @@ -0,0 +1,133 @@ + + + + + + Debug + AnyCPU + {EE523D3E-4A5D-4E01-9FB2-7BF4352F6CE5} + Library + Properties + Sdl.Web.Modules.TridionDocs + Sdl.Web.Modules.TridionDocs + v4.5.2 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll + True + + + ..\packages\Newtonsoft.Json.10.0.1\lib\net45\Newtonsoft.Json.dll + True + + + False + ..\packages\Sdl.Dxa.Framework.Web8.$(DxaFrameworkVersion)\lib\net452\Sdl.Web.Common.dll + + + False + ..\packages\Sdl.Dxa.Framework.Web8.$(DxaFrameworkVersion)\\lib\net452\Sdl.Web.DataModel.dll + + + False + ..\packages\Sdl.Web.Delivery.$(SdlDeliveryVersion)\lib\net452\Sdl.Web.Delivery.Service.dll + + + False + ..\packages\Sdl.Web.Delivery.$(SdlDeliveryVersion)\lib\net452\Sdl.Web.Delivery.ServicesCore.dll + + + False + ..\packages\Sdl.Dxa.Framework.Web8.$(DxaFrameworkVersion)\lib\net452\Sdl.Web.Mvc.dll + + + False + ..\packages\Sdl.Dxa.Framework.Web8.$(DxaFrameworkVersion)\lib\net452\Sdl.Web.Tridion.dll + + + + + + ..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.Helpers.dll + True + + + ..\packages\Microsoft.AspNet.Mvc.5.2.3\lib\net45\System.Web.Mvc.dll + True + + + ..\packages\Microsoft.AspNet.Razor.3.2.3\lib\net45\System.Web.Razor.dll + True + + + ..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.dll + True + + + + + + + + + ..\packages\Sdl.Web.Delivery.$(SdlDeliveryVersion)\lib\net452\Tridion.ContentDelivery.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/webapp-net/TridionDocs/TridionDocsModuleAreaRegistration.cs b/webapp-net/TridionDocs/TridionDocsModuleAreaRegistration.cs new file mode 100644 index 000000000..7afa386fc --- /dev/null +++ b/webapp-net/TridionDocs/TridionDocsModuleAreaRegistration.cs @@ -0,0 +1,24 @@ +using Sdl.Web.Common.Models; +using Sdl.Web.Modules.TridionDocs.Models; +using Sdl.Web.Mvc.Configuration; + +namespace Sdl.Web.Modules.TridionDocs +{ + /// + /// Tridion Docs module area registration + /// + public class TridionDocsModuleAreaRegistration : BaseAreaRegistration + { + public static string AREA_NAME = "TridionDocs"; + public override string AreaName => AREA_NAME; + protected override void RegisterAllViewModels() + { + // Entity Views + RegisterViewModel("Topic", typeof(Topic)); + + // Page Views + RegisterViewModel("GeneralPage", typeof(PageModel)); + RegisterViewModel("ErrorPage", typeof(PageModel)); + } + } +} diff --git a/webapp-net/TridionDocs/packages.config b/webapp-net/TridionDocs/packages.config new file mode 100644 index 000000000..da2616e98 --- /dev/null +++ b/webapp-net/TridionDocs/packages.config @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/webapp-net/Ugc/Areas/Ugc/Views/Page/GeneralPage.cshtml b/webapp-net/Ugc/Areas/Ugc/Views/Page/GeneralPage.cshtml new file mode 100644 index 000000000..69360da37 --- /dev/null +++ b/webapp-net/Ugc/Areas/Ugc/Views/Page/GeneralPage.cshtml @@ -0,0 +1,29 @@ +@model PageModel +@{ + bool hasLeftBar = Model.Regions.ContainsKey("Left Navigation") || Model.Regions.ContainsKey("Left"); + int mainContainerSize = hasLeftBar ? 9 : 12; +} +@Html.DxaRegion("Header") +
+ +
+ @Html.DxaRegion("Hero") + @Html.DxaRegion("Content Tools") +
+ @if (hasLeftBar) + { +
+ @Html.DxaRegion("Left Navigation", containerSize: 3) + @Html.DxaRegion("Left", containerSize: 3) +
+ } +
+ @Html.DxaRegions(exclude: "Header,Footer,Hero,Content Tools,Left Navigation,Left,Comments", containerSize: mainContainerSize) +
+
+ @Html.DxaRegion("Comments", containerSize: mainContainerSize) +
+
+
+
+@Html.DxaRegion("Footer") diff --git a/webapp-net/Ugc/Areas/Ugc/Views/Page/_ViewStart.cshtml b/webapp-net/Ugc/Areas/Ugc/Views/Page/_ViewStart.cshtml new file mode 100644 index 000000000..efda124b1 --- /dev/null +++ b/webapp-net/Ugc/Areas/Ugc/Views/Page/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "~/Views/Shared/_Layout.cshtml"; +} \ No newline at end of file diff --git a/webapp-net/Ugc/Areas/Ugc/Views/Region/Comments.cshtml b/webapp-net/Ugc/Areas/Ugc/Views/Region/Comments.cshtml new file mode 100644 index 000000000..20fd0f0e9 --- /dev/null +++ b/webapp-net/Ugc/Areas/Ugc/Views/Region/Comments.cshtml @@ -0,0 +1,6 @@ +@model UgcRegion +
+
+ @Html.DxaEntities() +
+
\ No newline at end of file diff --git a/webapp-net/Ugc/Areas/Ugc/Views/Ugc/Partials/UgcComment.cshtml b/webapp-net/Ugc/Areas/Ugc/Views/Ugc/Partials/UgcComment.cshtml new file mode 100644 index 000000000..56401e279 --- /dev/null +++ b/webapp-net/Ugc/Areas/Ugc/Views/Ugc/Partials/UgcComment.cshtml @@ -0,0 +1,39 @@ +@model UgcComment +
+
+ +
+
+ +
+
+
+ @Model.CommentData.User.Name + - + +
+
+

@Model.CommentData.Content

+
+
+ + @Model.CommentData.Rating + + +
+
+
+ + + @foreach (var comment in Model.Comments) + { +
    +
  • + @Html.Partial("Partials/UgcComment", comment) +
  • +
+ } +
+
+ + diff --git a/webapp-net/Ugc/Areas/Ugc/Views/Ugc/UgcComments.cshtml b/webapp-net/Ugc/Areas/Ugc/Views/Ugc/UgcComments.cshtml new file mode 100644 index 000000000..6cf611202 --- /dev/null +++ b/webapp-net/Ugc/Areas/Ugc/Views/Ugc/UgcComments.cshtml @@ -0,0 +1,15 @@ +@model UgcComments +
+

Comments (@Model.Comments.Count)

+
+
+ @foreach (var comment in Model.Comments) + { +
+ @Html.Partial("Partials/UgcComment", comment) +
+ } +
+
+ + diff --git a/webapp-net/Ugc/Areas/Ugc/Views/Ugc/UgcPostCommentForm.cshtml b/webapp-net/Ugc/Areas/Ugc/Views/Ugc/UgcPostCommentForm.cshtml new file mode 100644 index 000000000..d50ad3d5f --- /dev/null +++ b/webapp-net/Ugc/Areas/Ugc/Views/Ugc/UgcPostCommentForm.cshtml @@ -0,0 +1,32 @@ +@model UgcPostCommentForm + +
+ + @using (Html.BeginForm()) + { + if (!WebRequestContext.IsPreview) + { + @* If we put in an Anti Forgery Token, the Page can't be edited in XPM anymore *@ + @Html.AntiForgeryToken() + } + if (!Html.ViewData.ModelState.IsValid) + { +
+ @Html.ValidationSummary(excludePropertyErrors: false) +
+ } +
+ @Html.TextBoxFor(m => m.UserName, new {@class = "form-control", placeholder = @Model.UserNameLabel}) +
+
+ @Html.TextBoxFor(m => m.EmailAddress, new {@class = "form-control", placeholder = @Model.EmailAddressLabel}) +
+
+ @Html.TextAreaFor(m => m.Content, new {@class = "form-control", placeholder = @Model.ContentLabel}) +
+
+ + +
+ } +
\ No newline at end of file diff --git a/webapp-net/Ugc/Areas/Ugc/Views/web.config b/webapp-net/Ugc/Areas/Ugc/Views/web.config new file mode 100644 index 000000000..5c5324b79 --- /dev/null +++ b/webapp-net/Ugc/Areas/Ugc/Views/web.config @@ -0,0 +1,37 @@ + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/webapp-net/Ugc/Controllers/UgcApiController.cs b/webapp-net/Ugc/Controllers/UgcApiController.cs new file mode 100644 index 000000000..bc4b3c52a --- /dev/null +++ b/webapp-net/Ugc/Controllers/UgcApiController.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; +using Sdl.Web.Mvc.Controllers; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Web.Mvc; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using Sdl.Web.Modules.Ugc.Data; +using Sdl.Web.Mvc.Configuration; + +namespace Sdl.Web.Modules.Ugc.Controllers +{ + /// + /// Ugc Api Controller + /// + public class UgcApiController : BaseController + { + [Route("{localization}/api/comments/{pageId:int}")] + [Route("~/api/comments/{publicationId:int}/{pageId:int}")] + [HttpGet] + public ActionResult GetComments(int? publicationId, int pageId, bool descending = false, int[] status = null, + int top = 0, int skip = 0) + { + UgcService ugc = new UgcService(); + var comments = ugc.GetComments( + publicationId ?? int.Parse(WebRequestContext.Localization.Id), + pageId, descending, status ?? new int[] {}, top, skip); + + if(comments == null) + return ServerError(null); + return new ContentResult + { + ContentType = "application/json", + Content = + JsonConvert.SerializeObject(comments, + new JsonSerializerSettings {ContractResolver = new CamelCasePropertyNamesContractResolver()}), + ContentEncoding = Encoding.UTF8 + }; + } + + [Route("~/api/comments/add")] + [Route("{localization}/api/comments/add")] + [HttpPost] + public ActionResult PostComment(int? publicationId, int? pageId, bool descending = false, int[] status = null, + int top = 0, int skip = 0) + { + try + { + if (pageId == null || publicationId == null) + { + Response.StatusCode = 200; + return new EmptyResult(); + } + UgcService ugc = new UgcService(); + Stream req = Request.InputStream; + req.Seek(0, SeekOrigin.Begin); + string json = new StreamReader(req).ReadToEnd(); + PostedComment posted = JsonConvert.DeserializeObject(json); + Dictionary metadata = CreateMetadata(posted, true); + + string userId = posted.Username; + if (string.IsNullOrEmpty(userId)) + { + userId = "Anonymous"; + } + + Comment result = ugc.PostComment(posted.PublicationId, + posted.PageId.Value, + userId, + posted.Email, + posted.Content, + posted.ParentId ?? 0, + metadata); + + if(result == null) + return ServerError(null); + result.Metadata = CreateMetadata(posted, false); + return new ContentResult + { + ContentType = "application/json", + Content = + JsonConvert.SerializeObject(result, + new JsonSerializerSettings {ContractResolver = new CamelCasePropertyNamesContractResolver()}), + ContentEncoding = Encoding.UTF8 + }; + } + catch (Exception ex) + { + return ServerError(ex); + } + } + + [Route("~/api/comments/upvote")] + [Route("{localization}/api/comments/upvote")] + public async Task UpVoteComment(int commentId) + { + UgcService ugc = new UgcService(); + await ugc.UpVoteComment(commentId); + return Redirect(Request.UrlReferrer?.AbsolutePath); + } + + [Route("~/api/comments/downvote")] + [Route("{localization}/api/comments/downvote")] + public async Task DownVoteComment(int commentId) + { + UgcService ugc = new UgcService(); + await ugc.DownVoteComment(commentId); + return Redirect(Request.UrlReferrer?.AbsolutePath); + } + + [Route("~/api/comments/remove")] + [Route("{localization}/api/comments/remove")] + public async Task RemoveComment(int commentId) + { + UgcService ugc = new UgcService(); + await ugc.RemoveComment(commentId); + return Redirect(Request.UrlReferrer?.AbsolutePath); + } + + private static Dictionary CreateMetadata(PostedComment posted, bool escape) + { + string pubTitle = posted.PublicationTitle; + string pubUrl = posted.PublicationUrl; + string itemTitle = posted.PageTitle; + string pageUrl = posted.PageUrl; + string lang = posted.Language; + + if (escape) + { + pubTitle = $"\"{Regex.Escape(pubTitle)}\""; + pubUrl = $"\"{pubUrl}\""; + pageUrl = $"\"{pageUrl}\""; + itemTitle = $"\"{itemTitle}\""; + lang = $"\"{lang}\""; + } + + var metadata = new Dictionary + { + {"publicationTitle", pubTitle}, + {"publicationUrl", pubUrl}, + {"itemTitle", itemTitle}, + {"itemUrl", pageUrl}, + {"language", lang}, + {"status", "0"} + }; + metadata.Add("pubIdTitleLang", $"{{\"id\":{posted.PublicationId},\"title\":\"{posted.PublicationTitle}\",\"lang\":\"{posted.Language}\"}}"); + return metadata; + } + + public ActionResult ServerError(Exception ex) + { + Response.StatusCode = 405; + if (ex == null) + { + return new EmptyResult(); + } + if (ex.InnerException != null) ex = ex.InnerException; + return Content("{ \"Message\": \"" + ex.Message + "\" }", "application/json"); + } + } +} diff --git a/webapp-net/Ugc/Controllers/UgcController.cs b/webapp-net/Ugc/Controllers/UgcController.cs new file mode 100644 index 000000000..dabb8e859 --- /dev/null +++ b/webapp-net/Ugc/Controllers/UgcController.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Linq; +using Sdl.Web.Mvc.Controllers; +using Sdl.Web.Common.Models; +using Sdl.Web.Modules.Ugc.Data; +using Sdl.Web.Modules.Ugc.Models; +using Sdl.Web.Mvc.Configuration; + +namespace Sdl.Web.Modules.Ugc.Controllers +{ + /// + /// Ugc Controller + /// + public class UgcController : EntityController + { + protected override ViewModel EnrichModel(ViewModel sourceModel) + { + UgcComments model = base.EnrichModel(sourceModel) as UgcComments; + if (model != null) + { + var ugcService = new UgcService(); + var comments = ugcService.GetComments(model.Target.PublicationId, model.Target.ItemId, false, + new int[] {}, 0, 0); + model.Comments = CreateEntities(comments); + return model; + } + + UgcPostCommentForm postForm = base.EnrichModel(sourceModel) as UgcPostCommentForm; + if (postForm != null && MapRequestFormData(postForm) && ModelState.IsValid) + { + var ugcService = new UgcService(); + ugcService.PostComment(postForm.Target.PublicationId, postForm.Target.ItemId, postForm.UserName, postForm.EmailAddress, postForm.Content, postForm.ParentId, postForm.Metadata); + return new RedirectModel(WebRequestContext.RequestUrl); + } + + return sourceModel; + } + + private static List CreateEntities(List comments) => comments.Select(CreateEntity).ToList(); + + private static UgcComment CreateEntity(Comment comment) => new UgcComment {Comments = CreateEntities(comment.Children), CommentData = comment}; + } +} diff --git a/webapp-net/Ugc/Data/Comment.cs b/webapp-net/Ugc/Data/Comment.cs new file mode 100644 index 000000000..54c6b7367 --- /dev/null +++ b/webapp-net/Ugc/Data/Comment.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Sdl.Web.Modules.Ugc.Data +{ + public class Comment + { + public long Id { get; set; } + + [JsonIgnore] + public long ParentId { get; set; } + + public int ItemPublicationId { get; set; } + public int ItemId { get; set; } + public int ItemType { get; set; } + public CommentDate CreationDate { get; set; } + public CommentDate LastModifiedDate { get; set; } + public string Content { get; set; } + public User User { get; set; } + public List Children { get; set; } + + [JsonIgnore] + public int Rating { get; set; } = 0; + + public Dictionary Metadata { get; set; } = new Dictionary(); + } + + public class CommentDate + { + [JsonIgnore] + public DateTime DateTime { get; set; } + + public int DayOfMonth { get; set; } + public string DayOfWeek { get; set; } + public int DayOfYear { get; set; } + public int Hour { get; set; } + public int Minute { get; set; } + public string Month { get; set; } + public int MonthValue { get; set; } + public int Nano { get; set; } + public int Second { get; set; } + public int Year { get; set; } + } +} diff --git a/webapp-net/Ugc/Data/PostedComment.cs b/webapp-net/Ugc/Data/PostedComment.cs new file mode 100644 index 000000000..c1fcf4a51 --- /dev/null +++ b/webapp-net/Ugc/Data/PostedComment.cs @@ -0,0 +1,30 @@ +namespace Sdl.Web.Modules.Ugc.Data +{ + /// + /// Posted Comment + /// + public class PostedComment + { + public int PublicationId { get; set; } + + public int? PageId { get; set; } + + public string Username { get; set; } + + public string Email { get; set; } + + public string Content { get; set; } + + public int? ParentId { get; set; } = 0; + + public string PublicationTitle { get; set; } + + public string PublicationUrl { get; set; } + + public string PageTitle { get; set; } + + public string PageUrl { get; set; } + + public string Language { get; set; } + } +} diff --git a/webapp-net/Ugc/Data/PubIdTitleLang.cs b/webapp-net/Ugc/Data/PubIdTitleLang.cs new file mode 100644 index 000000000..77c35d2c1 --- /dev/null +++ b/webapp-net/Ugc/Data/PubIdTitleLang.cs @@ -0,0 +1,9 @@ +namespace Sdl.Web.Modules.Ugc.Data +{ + public class PubIdTitleLang + { + public string Id { get; set; } + public string Title { get; set; } + public string Lang { get; set; } + } +} diff --git a/webapp-net/Ugc/Data/User.cs b/webapp-net/Ugc/Data/User.cs new file mode 100644 index 000000000..c5bbdc193 --- /dev/null +++ b/webapp-net/Ugc/Data/User.cs @@ -0,0 +1,10 @@ +namespace Sdl.Web.Modules.Ugc.Data +{ + public class User + { + public string Id { get; set; } + public string Name { get; set; } + public string EmailAddress { get; set; } + public string ExternalId { get; set; } + } +} diff --git a/webapp-net/Ugc/Mapping/UgcModelBuilder.cs b/webapp-net/Ugc/Mapping/UgcModelBuilder.cs new file mode 100644 index 000000000..d018f0d4a --- /dev/null +++ b/webapp-net/Ugc/Mapping/UgcModelBuilder.cs @@ -0,0 +1,215 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Sdl.Web.Common.Interfaces; +using Sdl.Web.Common.Logging; +using Sdl.Web.Common.Models; +using Sdl.Web.DataModel; +using Sdl.Web.Modules.Ugc.Models; +using Sdl.Web.Tridion.ContentManager; +using Sdl.Web.Tridion.Mapping; + +namespace Sdl.Web.Modules.Ugc.Mapping +{ + /// + /// Ugc Model Builder + /// + public class UgcModelBuilder : IPageModelBuilder, IEntityModelBuilder + { + private static readonly string ShowCommentsExtData = "UgcShowComments"; + private static readonly string PostCommentsExtData = "UgcPostComments"; + private static readonly string CommentsEntityRegionExt = "CommentsEntityRegion"; + + public void BuildPageModel(ref PageModel pageModel, PageModelData pageModelData, bool includePageRegions, + ILocalization localization) + { + using (new Tracer(pageModel, pageModelData, localization)) + { + var ugcMetadata = UgcMetadata(pageModelData.PageTemplate?.Metadata); + + string regionName = GetValue(ugcMetadata, "commentsRegion"); + string areaName = pageModel.MvcData.AreaName; + RegionModel ugcRegion; + if (string.IsNullOrEmpty(regionName)) + { + areaName = "Ugc"; + regionName = "Comments"; + ugcRegion = FindRegion(pageModel.Regions, "Comments") ?? + CreateRegion(pageModel, areaName, regionName); + } + else + { + ugcRegion = FindRegion(pageModel.Regions, regionName); + if (ugcRegion == null) + { + Log.Error("Unable to locate region for comments '" + regionName + "'."); + } + } + + // Entity Comments + foreach (var region in pageModel.Regions) + { + AddCommentsViews(pageModel, region, localization, ugcRegion); + } + + if (ugcRegion != null) + { + // Page Comments + if (ShowComments(ugcMetadata)) + { + ugcRegion.Entities.Add(CreateUgcCommentsEntity(localization, pageModel.Id, ItemType.Page)); + } + if (PostComments(ugcMetadata)) + { + ugcRegion.Entities.Add(CreateUgcPostCommentEntity(localization, pageModel.Id, ItemType.Page, + UgcPostFormMetadata(ugcMetadata))); + } + } + } + } + + private static void AddCommentsViews(PageModel pageModel, RegionModel region, ILocalization localization, RegionModel ugcRegion) + { + List regionEntities = new List(); + + foreach (var entity in region.Entities.Where(e => e.ExtensionData != null)) + { + if (entity.ExtensionData == null) continue; + + // comments get added to the ugcRegion if it exists else we place in same region as entity + IList entities = ugcRegion != null ? ugcRegion.Entities : regionEntities; + if (entity.ExtensionData.ContainsKey(CommentsEntityRegionExt)) + { + // comment region specified for this entity so lets find it and use that + var targetRegion = FindRegion(pageModel.Regions, (string) entity.ExtensionData[CommentsEntityRegionExt]); + if (targetRegion != null && targetRegion != region) + { + entities = targetRegion.Entities; + } + else if (targetRegion == null || targetRegion == region) + { + entities = regionEntities; + } + } + + if (entity.ExtensionData.ContainsKey(ShowCommentsExtData) && + (bool) entity.ExtensionData[ShowCommentsExtData]) + { + entities.Add(CreateUgcCommentsEntity(localization, entity.Id, ItemType.Component)); + } + if (entity.ExtensionData.ContainsKey(PostCommentsExtData) && entity.ExtensionData[PostCommentsExtData] != null) + { + entities.Add(CreateUgcPostCommentEntity(localization, entity.Id, ItemType.Component, (ContentModelData)entity.ExtensionData[PostCommentsExtData])); + } + } + + // Add our ugc views to either the same region as the entity we have comments enabled for or the ugc "Comments" region if available + foreach (var x in regionEntities) + { + region.Entities.Add(x); + } + + foreach (var childRegion in region.Regions) + { + AddCommentsViews(pageModel, childRegion, localization, ugcRegion); + } + } + + public void BuildEntityModel(ref EntityModel entityModel, EntityModelData entityModelData, Type baseModelType, + ILocalization localization) + { + var ugcMetadata = UgcMetadata(entityModelData.ComponentTemplate?.Metadata); + entityModel.SetExtensionData(ShowCommentsExtData, ShowComments(ugcMetadata)); + entityModel.SetExtensionData(PostCommentsExtData, PostComments(ugcMetadata) ? UgcPostFormMetadata(ugcMetadata) : null); + entityModel.SetExtensionData(CommentsEntityRegionExt, GetCommentsRegion(ugcMetadata)); + } + + private static RegionModel FindRegion(RegionModelSet regionModelSet, string regionName) + { + foreach (var region in regionModelSet) + { + if (region.Name.Equals(regionName)) return region; + RegionModel childRegion = FindRegion(region.Regions, regionName); + if (childRegion != null) return childRegion; + } + + return null; + } + + private static UgcRegion CreateRegion(PageModel pageModel, string areaName, string regionName) + { + UgcRegion ugcRegion; + if (!pageModel.Regions.OfType().Any()) + { + ugcRegion = new UgcRegion(regionName); + ugcRegion.MvcData = new Common.Models.MvcData($"{areaName}:{regionName}"); + pageModel.Regions.Add(ugcRegion); + } + else + { + ugcRegion = pageModel.Regions.OfType().First(); + } + return ugcRegion; + } + + private static UgcComments CreateUgcCommentsEntity(ILocalization localization, string modelId, ItemType itemType) + { + var mvcData = new Common.Models.MvcData("Ugc:Ugc:UgcComments"); + mvcData.ControllerAreaName = "Ugc"; + return new UgcComments + { + Target = + CmUri.FromString( + $"{localization.CmUriScheme}:{localization.Id}-{modelId}-{(int)itemType}"), + MvcData = mvcData, + IsVolatile = true + }; + } + + private static UgcPostCommentForm CreateUgcPostCommentEntity(ILocalization localization, string modelId, ItemType itemType, ContentModelData postFormConfig) + { + var mvcData = new Common.Models.MvcData("Ugc:Ugc:UgcPostCommentForm"); + mvcData.ControllerAreaName = "Ugc"; + return new UgcPostCommentForm + { + Target = + CmUri.FromString( + $"{localization.CmUriScheme}:{localization.Id}-{modelId}-{(int)itemType}"), + MvcData = mvcData, + UserNameLabel = GetValue(postFormConfig, "userNameLabel"), + EmailAddressLabel = GetValue(postFormConfig, "emailAddressLabel"), + ContentLabel = GetValue(postFormConfig, "contentLabel"), + SubmitButtonLabel = GetValue(postFormConfig, "submitButtonLabel"), + NoContentMessage = GetValue(postFormConfig, "noContentMessage"), + NoEmailAddressMessage = GetValue(postFormConfig, "noEmailAddressMessage"), + NoUserNameMessage = GetValue(postFormConfig, "noUserNameMessage"), + IsVolatile = true + }; + } + + private static string GetCommentsRegion(ContentModelData metadata) => GetValue(metadata, "commentsRegion"); + + private static ContentModelData UgcMetadata(ContentModelData metadata) => metadata != null && metadata.ContainsKey("ugcConfig") + ? (ContentModelData) metadata["ugcConfig"] + : null; + + private static ContentModelData UgcPostFormMetadata(ContentModelData metadata) => metadata != null && metadata.ContainsKey("postFormConfig") + ? (ContentModelData)metadata["postFormConfig"] + : null; + + private static T GetValue(ContentModelData metadata, string name) + { + if (metadata == null || !metadata.ContainsKey(name)) return default(T); + var v = metadata[name]; + if (v == null) return default(T); + if (typeof(T) == typeof(bool)) + { + return (T) Convert.ChangeType(v.Equals("Yes"), typeof (T)); + } + return (T) Convert.ChangeType(v, typeof (T)); + } + + private static bool ShowComments(ContentModelData metadata) => GetValue(metadata, "showComments"); + private static bool PostComments(ContentModelData metadata) => GetValue(metadata, "allowPost"); + } +} diff --git a/webapp-net/Ugc/Models/UgcComment.cs b/webapp-net/Ugc/Models/UgcComment.cs new file mode 100644 index 000000000..1184dc33f --- /dev/null +++ b/webapp-net/Ugc/Models/UgcComment.cs @@ -0,0 +1,13 @@ +using Sdl.Web.Common.Models; +using Sdl.Web.Modules.Ugc.Data; +using System.Collections.Generic; + +namespace Sdl.Web.Modules.Ugc.Models +{ + public class UgcComment : EntityModel + { + public Comment CommentData { get; set; } + + public List Comments { get; set; } + } +} diff --git a/webapp-net/Ugc/Models/UgcComments.cs b/webapp-net/Ugc/Models/UgcComments.cs new file mode 100644 index 000000000..0a4921bb9 --- /dev/null +++ b/webapp-net/Ugc/Models/UgcComments.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using Sdl.Web.Common.Models; +using Sdl.Web.Modules.Ugc.Data; +using Sdl.Web.Tridion.ContentManager; + +namespace Sdl.Web.Modules.Ugc.Models +{ + public class UgcComments : EntityModel + { + [SemanticProperty(IgnoreMapping = true)] + public CmUri Target { get; set; } + + [SemanticProperty(IgnoreMapping = true)] + public List Comments { get; set; } + } +} diff --git a/webapp-net/Ugc/Models/UgcPostCommentForm.cs b/webapp-net/Ugc/Models/UgcPostCommentForm.cs new file mode 100644 index 000000000..a71fcb723 --- /dev/null +++ b/webapp-net/Ugc/Models/UgcPostCommentForm.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Sdl.Web.Common; +using Sdl.Web.Common.Models; +using Sdl.Web.Tridion.ContentManager; + +namespace Sdl.Web.Modules.Ugc.Models +{ + [Serializable] + [DxaNoOutputCache] + public class UgcPostCommentForm : EntityModel + { + /// + /// Holds the form control value for username + /// + [Required(ErrorMessage = "@Model.NoUserNameMessage")] + [StringLength(80)] + [SemanticProperty(IgnoreMapping = true)] + public string UserName { get; set; } + + /// + /// Holds the form control value for email address + /// + [Required(ErrorMessage = "@Model.NoEmailAddressMessage")] + [StringLength(255)] + [SemanticProperty(IgnoreMapping = true)] + public string EmailAddress { get; set; } + + /// + /// Holds the form control value for email address + /// + [Required(ErrorMessage = "@Model.NoContentMessage")] + [SemanticProperty(IgnoreMapping = true)] + public string Content { get; set; } + + /// + /// Metadata of comment to post + /// + [SemanticProperty(IgnoreMapping = true)] + public Dictionary Metadata { get; set; } + + /// + /// Parent id of comment to post + /// + [SemanticProperty(IgnoreMapping = true)] + public int ParentId { get; set; } = 0; + + /// + /// Label text for username input control on view + /// + public string UserNameLabel { get; set; } + + /// + /// Label text for email address input control on view + /// + public string EmailAddressLabel { get; set; } + + /// + /// Label text for content input control on view + /// + public string ContentLabel { get; set; } + + /// + /// Label text for submit botton on view + /// + public string SubmitButtonLabel { get; set; } + + /// + /// User name not specified message + /// + public string NoUserNameMessage { get; set; } + + /// + /// Email not specified message + /// + public string NoEmailAddressMessage { get; set; } + + /// + /// Content not specified message + /// + public string NoContentMessage { get; set; } + + /// + /// Target CmUri for comments + /// + [SemanticProperty(IgnoreMapping = true)] + public CmUri Target { get; set; } + } +} diff --git a/webapp-net/Ugc/Models/UgcRegion.cs b/webapp-net/Ugc/Models/UgcRegion.cs new file mode 100644 index 000000000..b0db27e46 --- /dev/null +++ b/webapp-net/Ugc/Models/UgcRegion.cs @@ -0,0 +1,19 @@ +using System; +using Sdl.Web.Common.Models; + +namespace Sdl.Web.Modules.Ugc.Models +{ + [Serializable] + public class UgcRegion : RegionModel + { + public UgcRegion(string name) + : base(name) + { + } + + public UgcRegion(string name, string qualifiedViewName) + : base(name, qualifiedViewName) + { + } + } +} diff --git a/webapp-net/Ugc/Properties/AssemblyInfo.cs b/webapp-net/Ugc/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..aa7156741 --- /dev/null +++ b/webapp-net/Ugc/Properties/AssemblyInfo.cs @@ -0,0 +1,5 @@ +using System.Reflection; + +[assembly: AssemblyTitle("SDL DXA Ugc Module")] + +// NOTE: Common Assembly Info is generated by the build in CommonAssemblyInfo.cs diff --git a/webapp-net/Ugc/Sdl.Web.Modules.Ugc.csproj b/webapp-net/Ugc/Sdl.Web.Modules.Ugc.csproj new file mode 100644 index 000000000..6ba7d8d5a --- /dev/null +++ b/webapp-net/Ugc/Sdl.Web.Modules.Ugc.csproj @@ -0,0 +1,131 @@ + + + + + + Debug + AnyCPU + {54DDEEE6-1F97-4E53-8018-5918F7D90A8B} + Library + Properties + Sdl.Web.Modules.Ugc + Sdl.Web.Modules.Ugc + v4.5.2 + true + 512 + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + ..\packages\Newtonsoft.Json.10.0.1\lib\net45\Newtonsoft.Json.dll + + + False + ..\packages\Sdl.Dxa.Framework.Web8.$(DxaFrameworkVersion)\lib\net452\Sdl.Web.Common.dll + + + False + ..\packages\Sdl.Dxa.Framework.Web8.$(DxaFrameworkVersion)\lib\net452\Sdl.Web.DataModel.dll + + + False + ..\packages\Sdl.Web.Delivery.$(SdlDeliveryVersion)\lib\net452\Sdl.Web.Delivery.UGC.dll + + + ..\packages\Sdl.Dxa.Framework.Web8.$(DxaFrameworkVersion)\lib\net452\Sdl.Web.Mvc.dll + + + ..\packages\Sdl.Dxa.Framework.Web8.$(DxaFrameworkVersion)\lib\net452\Sdl.Web.Tridion.dll + + + False + ..\packages\Microsoft.AspNet.Mvc.5.2.3\lib\net45\System.Web.Mvc.dll + $(CopyLocal) + + + False + ..\packages\Microsoft.AspNet.Razor.3.2.3\lib\net45\System.Web.Razor.dll + $(CopyLocal) + + + False + ..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.dll + $(CopyLocal) + + + ..\packages\Sdl.Web.Delivery.$(SdlDeliveryVersion)\lib\net452\Tridion.ContentDelivery.AmbientData.dll + + + False + ..\packages\Microsoft.AspNet.WebPages.Data.3.2.3\lib\net45\WebMatrix.Data.dll + + + False + ..\packages\Microsoft.AspNet.WebPages.WebData.3.2.3\lib\net45\WebMatrix.WebData.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Designer + + + + + \ No newline at end of file diff --git a/webapp-net/Ugc/UgcAreaRegistration.cs b/webapp-net/Ugc/UgcAreaRegistration.cs new file mode 100644 index 000000000..a7992d380 --- /dev/null +++ b/webapp-net/Ugc/UgcAreaRegistration.cs @@ -0,0 +1,24 @@ +using Sdl.Web.Common.Models; +using Sdl.Web.Modules.Ugc.Models; +using Sdl.Web.Mvc.Configuration; + +namespace Sdl.Web.Modules.Ugc +{ + public class UgcAreaRegistration : BaseAreaRegistration + { + public override string AreaName => "Ugc"; + + protected override void RegisterAllViewModels() + { + // Region + RegisterViewModel("Comments", typeof(UgcRegion)); + + // Entity + RegisterViewModel("UgcComments", typeof(UgcComments), "Ugc"); + RegisterViewModel("UgcPostCommentForm", typeof(UgcPostCommentForm), "Ugc"); + + // Page + RegisterViewModel("GeneralPage", typeof(PageModel)); + } + } +} diff --git a/webapp-net/Ugc/UgcService.cs b/webapp-net/Ugc/UgcService.cs new file mode 100644 index 000000000..2816c75b1 --- /dev/null +++ b/webapp-net/Ugc/UgcService.cs @@ -0,0 +1,148 @@ +using System; +using Sdl.Web.Tridion.ContentManager; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using Sdl.Web.Common.Interfaces; +using Sdl.Web.Delivery.UGC; +using Sdl.Web.Delivery.UGC.Model; +using Sdl.Web.Modules.Ugc.Data; +using Sdl.Web.Mvc.Configuration; +using Tridion.ContentDelivery.AmbientData; + +namespace Sdl.Web.Modules.Ugc +{ + /// + /// Ugc Service + /// + public class UgcService + { + private static readonly int MaximumThreadsDepth = -1; + private readonly IUgcCommentsApi _api; + private readonly IUgcVoteCommentApi _votingApi; + + public UgcService() + { + _api = UgcInstanceProvider.Instance.UgcCommunityClient(); + _votingApi = (IUgcVoteCommentApi) _api; + } + + public List GetComments(int publicationId, int pageId, bool descending, int[] status, int top, int skip) + { + SimpleCommentsFilter filter = new SimpleCommentsFilter + { + Top = top, + Skip = skip, + Depth = MaximumThreadsDepth, + Statuses = new List(new List(status).Select(x => (Status) x)) + }; + return Convert(_api.RetrieveThreadedComments(CreateUri(publicationId, pageId), filter, descending, true)); + } + + public Comment PostComment(int publicationId, int pageId, string username, string email, string content, + int parentId, Dictionary metadata) + { + var claimStore = AmbientDataContext.CurrentClaimStore; + if (claimStore != null) + { + claimStore.Put(new Uri("taf:claim:contentdelivery:webservice:user"), username); + claimStore.Put(new Uri("taf:claim:contentdelivery:webservice:post:allowed"), true); + } + return Convert( + _api.PostComment(CreateUri(publicationId, pageId), username, email, content, parentId, metadata).Result); + } + + public async Task UpVoteComment(long commentId) + { + await _votingApi.VoteCommentUp(commentId); + } + + public async Task DownVoteComment(long commentId) + { + await _votingApi.VoteCommentDown(commentId); + } + + public async Task RemoveComment(long commentId) + { + return await _api.RemoveComment(commentId); + } + + private static CmUri CreateUri(int publicationId, int pageId) + { + ILocalization localization = WebRequestContext.Localization; + return CmUri.Create(localization.CmUriScheme, publicationId, pageId, ItemType.Page); + } + + private static List Convert(IEnumerable comments) + => comments?.Select(Convert).ToList(); + + private static Comment Convert(IComment comment) + { + if (comment == null) return null; + Comment c = new Comment + { + Id = comment.Id, + ParentId = comment.ParentId, + ItemId = comment.ItemId, + ItemType = comment.ItemType, + ItemPublicationId = comment.ItemPublicationId, + Content = comment.Content, + Rating = comment.Score, + Metadata = Convert(comment.Metadata) + }; + + if (comment.User != null) + { + c.User = Convert(comment.User); + } + + if (comment.CreationDate.HasValue) + { + c.CreationDate = Convert(comment.CreationDate.Value); + } + + if (comment.LastModifiedDate.HasValue) + { + c.LastModifiedDate = Convert(comment.LastModifiedDate.Value); + } + + c.Children = Convert(comment.Children); + return c; + } + + private static Dictionary Convert(List meta) + { + var metadata = new Dictionary(); + if (meta == null) return metadata; + foreach (var m in meta.Where(m => !metadata.ContainsKey(m.KeyName))) + { + metadata.Add(m.KeyName, m.KeyValue); + } + return metadata; + } + + private static User Convert(IUser user) => new User + { + Id = user.Id, + ExternalId = user.ExternalId, + Name = user.Name, + EmailAddress = user.EmailAddress, + }; + + private static CommentDate Convert(DateTime dt) => new CommentDate + { + DateTime = dt, + DayOfMonth = dt.Month, + DayOfWeek = dt.DayOfWeek.ToString(), + DayOfYear = dt.DayOfYear, + Month = new DateTimeFormatInfo().GetMonthName(dt.Month), + MonthValue = dt.Month, + Year = dt.Year, + Hour = dt.Hour, + Minute = dt.Minute, + Second = dt.Second, + Nano = dt.Millisecond + }; + } +} diff --git a/webapp-net/Ugc/packages.config b/webapp-net/Ugc/packages.config new file mode 100644 index 000000000..d2496af24 --- /dev/null +++ b/webapp-net/Ugc/packages.config @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/webapp-net/_tools/NuGet.exe b/webapp-net/_tools/NuGet.exe deleted file mode 100644 index be85ec2c4..000000000 Binary files a/webapp-net/_tools/NuGet.exe and /dev/null differ diff --git a/webapp-net/ciBuild.proj b/webapp-net/ciBuild.proj index 521e5baf8..92bd06ec4 100644 --- a/webapp-net/ciBuild.proj +++ b/webapp-net/ciBuild.proj @@ -27,7 +27,7 @@ $(JenkinsUrl)/job/DXA%20$(DxaBuildType)%20CI%20Web%20App%20.NET/lastSuccessfulBuild/artifact/Site/bin - "$(ProjectDirectory)\_tools\NuGet.exe" + NuGet.exe C:\Program Files (x86)\MSBuild\12.0\Bin\MSBuild.exe C:\Windows\Sysnative\WindowsPowerShell\v1.0\powershell.exe C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe