Goal
Business users with no access to AEM may want to review the page (content and layout) before authors activate the page to Publish (and make it Live). A typical review process may contains the following steps...
1) Replicate the page content to a Review environment
2) Preview the page in Review environment (pre production)
3) Approve the page (or provide feedback)
4) Activate the page to Publish
The following process provides a solution for the first two steps...
Demo | Package Install | Github
Publish to Review
Review in Progress - Card View
Review in Progress - List View
Review in Progress - Column View
Solution
1) Setup a publish environment behind the organization firewall and label it Review / Preview environment. Content is first pushed to this environment probably in a workflow process before replicating to Publish. Locally, for demo, its running on 4504
Author - http://localhost:4502
Publish - http://localhost:4503
Review - http://localhost:4504
2) Create a replication agent Publish to Review Agent(review_agent) to push content from Author to Review (Package Install has a sample replication agent /etc/replication/agents.author/review_agent)
3) Set the Transport URI, Transport Username and Password of the Review env in agent (adding admin user credentials is not recommended).
<?xml version="1.0" encoding="UTF-8"?> <jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0" jcr:primaryType="cq:Page"> <jcr:content cq:lastModified="{Date}2019-08-09T09:57:33.965-05:00" cq:lastModifiedBy="admin" cq:template="/libs/cq/replication/templates/agent" jcr:lastModified="{Date}2019-08-09T09:57:33.957-05:00" jcr:lastModifiedBy="admin" jcr:primaryType="nt:unstructured" jcr:title="Publish to Review Agent" sling:resourceType="cq/replication/components/agent" enabled="true" transportPassword="\{40033b099a3b0d7e8f360c8623e446e1fd2171f5c621d72a3d30ba07cdc00793}" transportUri="http://localhost:4504/bin/receive?sling:authRequestLogin=1" transportUser="admin" userId="admin"/> </jcr:root>
4) Test, make sure the agent is enabled and reachable
5) To simplify the process add a button in action bar Publish to Review (like Quick Publish) to push page (and references) to Review with the following code, in node /apps/eaem-publish-page-to-review-env/content/publish-toreview-toolbar-ext
<?xml version="1.0" encoding="UTF-8"?> <jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:granite="http://www.adobe.com/jcr/granite/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0" granite:rel="cq-damadmin-admin-actions-publish-to-review-activator" jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/collection/action" activeSelectionCount="multiple" icon="dimension" target=".cq-damadmin-admin-childpages" text="Publish to Review" variant="actionBar"> <data jcr:primaryType="nt:unstructured" text="Page published to review environment"/> </jcr:root>
6) In CRXDE Lite (http://localhost:4502/crx/de), create folder /apps/eaem-publish-page-to-review-env
7) Create node /apps/eaem-publish-page-to-review-env/clientlib of type cq:ClientLibraryFolder, add String property categories with value cq.common.wcm, String[] property dependencies with value lodash
8) Create file (nt:file) /apps/eaem-publish-page-to-review-env/clientlib/js.txt, add
add-publish-to-review-action.js
9) Create file (nt:file) /apps/eaem-publish-page-to-review-env/clientlib/add-publish-to-review-action.js, add the following code
(function ($, $document) { var BUTTON_URL = "/apps/eaem-publish-page-to-review-env/content/publish-toreview-toolbar-ext.html", QUICK_PUBLISH_ACTIVATOR = "cq-siteadmin-admin-actions-quickpublish-activator", REVIEW_STATUS_URL = "/bin/eaem/sites/review/status?parentPath=", PUBLISH_TO_REVIEW = "/bin/eaem/sites/review/publish?pagePaths=", F_CONTENT_LOADED = "foundation-contentloaded", F_MODE_CHANGE = "foundation-mode-change", F_SEL_CHANGE = "foundation-selections-change", F_COL_ITEM_ID = "foundationCollectionItemId", F_COL_ACTION = "foundationCollectionAction", FOUNDATION_COLLECTION_ID = "foundation-collection-id", LAYOUT_COL_VIEW = "column", LAYOUT_LIST_VIEW = "list", LAYOUT_CARD_VIEW = "card", COLUMN_VIEW = "coral-columnview", EVENT_COLUMNVIEW_CHANGE = "coral-columnview:change", FOUNDATION_COLLECTION_ITEM = ".foundation-collection-item", FOUNDATION_COLLECTION_ITEM_ID = "foundation-collection-item-id", CORAL_COLUMNVIEW_PREVIEW = "coral-columnview-preview", CORAL_COLUMNVIEW_PREVIEW_ASSET = "coral-columnview-preview-asset", EAEM_BANNER_CLASS = "eaem-banner", EAEM_BANNER = ".eaem-banner", FOUNDATION_COLLECTION_ITEM_TITLE = ".foundation-collection-item-title", SITE_ADMIN_CHILD_PAGES = ".cq-siteadmin-admin-childpages", NEW_BANNER = "New", colViewListenerAdded = false, reviewButtonAdded = false; $document.on(F_CONTENT_LOADED, removeNewBanner); $document.on(F_CONTENT_LOADED, checkReviewStatus); $document.on(F_SEL_CHANGE, function () { if(reviewButtonAdded){ return; } reviewButtonAdded = true; colViewListenerAdded = false; checkReviewStatus(); $.ajax(BUTTON_URL).done(addButton); }); function removeNewBanner(){ var $container = $(SITE_ADMIN_CHILD_PAGES), $label, $items = $container.find(FOUNDATION_COLLECTION_ITEM); _.each($items, function(item){ $label = $(item).find("coral-card-info coral-tag"); if(_.isEmpty($label) || $label.find("coral-tag-label").html().trim() != NEW_BANNER){ return; } $label.remove(); }); } function checkReviewStatus(){ var parentPath = $(SITE_ADMIN_CHILD_PAGES).data(FOUNDATION_COLLECTION_ID); if(_.isEmpty(parentPath)){ return; } $.ajax(REVIEW_STATUS_URL + parentPath).done(showBanners); } function showBanners(pathsObj){ if(isColumnView()){ handleColumnView(); } if(_.isEmpty(pathsObj)){ return; } if(isCardView()){ addCardViewBanner(pathsObj); }else if(isListView()){ addListViewBanner(pathsObj) } } function handleColumnView(){ var $columnView = $(COLUMN_VIEW); if(colViewListenerAdded){ return; } colViewListenerAdded = true; $columnView.on(EVENT_COLUMNVIEW_CHANGE, handleColumnItemSelection); } function handleColumnItemSelection(event){ var detail = event.originalEvent.detail, $page = $(detail.selection[0]), pagePath = $page.data(FOUNDATION_COLLECTION_ITEM_ID); if(_.isEmpty(pagePath)){ return; } $.ajax(REVIEW_STATUS_URL + pagePath).done(addColumnViewBanner); } function addColumnViewBanner(pageObj){ getUIWidget(CORAL_COLUMNVIEW_PREVIEW).then(handler); function handler($colPreview){ var $pagePreview = $colPreview.find(CORAL_COLUMNVIEW_PREVIEW_ASSET), pagePath = $colPreview.data("foundation-layout-columnview-columnid"), state = pageObj[pagePath]; if(_.isEmpty(state)){ return; } $pagePreview.find(EAEM_BANNER).remove(); $pagePreview.prepend(getBannerColumnView(state)); } } function getBannerColumnView(state){ var ct = getColorText(state); if(!ct.color){ return; } return "<coral-tag style='background-color: " + ct.color + ";z-index: 9999; width: 100%' class='" + EAEM_BANNER_CLASS + "'>" + "<i class='coral-Icon coral-Icon--bell coral-Icon--sizeXS' style='margin-right: 10px'></i>" + ct.text + "</coral-tag>"; } function getBannerHtml(state){ var ct = getColorText(state); if(!ct.color){ return; } return "<coral-alert style='background-color:" + ct.color + "' class='" + EAEM_BANNER_CLASS + "'>" + "<coral-alert-content>" + ct.text + "</coral-alert-content>" + "</coral-alert>"; } function getColorText(state){ var color, text; if(_.isEmpty(state)){ return } if(state == "IN_PROGRESS"){ color = "#ff7f7f"; text = "Review in progress" } return{ color: color, text: text } } function addListViewBanner(pathsObj){ var $container = $(SITE_ADMIN_CHILD_PAGES), $item, ct; _.each(pathsObj, function(state, pagePath){ $item = $container.find("[data-" + FOUNDATION_COLLECTION_ITEM_ID + "='" + pagePath + "']"); if(!_.isEmpty($item.find(EAEM_BANNER))){ return; } ct = getColorText(state); if(!ct.color){ return; } $item.find("td").css("background-color" , ct.color).addClass(EAEM_BANNER_CLASS); $item.find(FOUNDATION_COLLECTION_ITEM_TITLE).prepend(getListViewBannerHtml()); }); } function getListViewBannerHtml(){ return "<i class='coral-Icon coral-Icon--bell coral-Icon--sizeXS' style='margin-right: 10px'></i>"; } function addCardViewBanner(pathsObj){ var $container = $(SITE_ADMIN_CHILD_PAGES), $item; _.each(pathsObj, function(state, pagePath){ $item = $container.find("[data-" + FOUNDATION_COLLECTION_ITEM_ID + "='" + pagePath + "']"); if(_.isEmpty($item)){ return; } if(!_.isEmpty($item.find(EAEM_BANNER))){ return; } $item.find("coral-card-info").append(getBannerHtml(state)); }); } function isColumnView(){ return ( getAssetsConsoleLayout() === LAYOUT_COL_VIEW ); } function isListView(){ return ( getAssetsConsoleLayout() === LAYOUT_LIST_VIEW ); } function isCardView(){ return (getAssetsConsoleLayout() === LAYOUT_CARD_VIEW); } function getAssetsConsoleLayout(){ var $childPage = $(SITE_ADMIN_CHILD_PAGES), foundationLayout = $childPage.data("foundation-layout"); if(_.isEmpty(foundationLayout)){ return ""; } return foundationLayout.layoutId; } function getUIWidget(selector){ if(_.isEmpty(selector)){ return; } var deferred = $.Deferred(); var INTERVAL = setInterval(function(){ var $widget = $(selector); if(_.isEmpty($widget)){ return; } clearInterval(INTERVAL); deferred.resolve($widget); }, 250); return deferred.promise(); } function startsWith(val, start){ return val && start && (val.indexOf(start) === 0); } function addButton(html) { var $eActivator = $("." + QUICK_PUBLISH_ACTIVATOR); if ($eActivator.length == 0) { return; } var $convert = $(html).css("margin-left", "20px").insertBefore($eActivator); $convert.click(postPublishToReviewRequest); } function postPublishToReviewRequest(){ var actionConfig = ($(this)).data(F_COL_ACTION); var $items = $(".foundation-selections-item"), pagePaths = []; $items.each(function () { pagePaths.push($(this).data(F_COL_ITEM_ID)); }); $.ajax(PUBLISH_TO_REVIEW + pagePaths.join(",")).done(function(){ showAlert(actionConfig.data.text, "Publish"); }); } function showAlert(message, title, callback){ var fui = $(window).adaptTo("foundation-ui"), options = [{ id: "ok", text: "OK", primary: true }]; message = message || "Unknown Error"; title = title || "Error"; fui.prompt(title, message, "default", options, callback); } }(jQuery, jQuery(document)));
10) Create a servlet apps.experienceaem.sites.PublishToReviewServlet to collect references in page and publish to Review using replication agent review_agent
package apps.experienceaem.sites; import com.day.cq.commons.jcr.JcrConstants; import com.day.cq.replication.*; import com.day.cq.wcm.api.reference.ReferenceProvider; import org.apache.commons.lang3.StringUtils; import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.api.SlingHttpServletResponse; import org.apache.sling.api.resource.Resource; import org.apache.sling.api.resource.ResourceResolver; import org.apache.sling.api.servlets.SlingAllMethodsServlet; import org.apache.sling.servlets.post.JSONResponse; import org.json.JSONObject; import org.osgi.service.component.annotations.Component; import com.day.cq.wcm.api.reference.Reference; import org.osgi.service.component.annotations.ReferenceCardinality; import org.osgi.service.component.annotations.ReferencePolicy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.jcr.Node; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.security.AccessControlManager; import javax.jcr.security.Privilege; import javax.servlet.Servlet; import javax.servlet.ServletException; import java.io.IOException; import java.util.*; import java.util.concurrent.CopyOnWriteArrayList; @Component( name = "Experience AEM Publish to Review Servlet", immediate = true, service = Servlet.class, property = { "sling.servlet.methods=GET", "sling.servlet.paths=/bin/eaem/sites/review/status", "sling.servlet.paths=/bin/eaem/sites/review/publish" } ) public class PublishToReviewServlet extends SlingAllMethodsServlet { private static final Logger log = LoggerFactory.getLogger(PublishToReviewServlet.class); public static final String PUBLISH_TO_REVIEW_URL = "/bin/eaem/sites/review/publish"; public static final String STATUS_URL = "/bin/eaem/sites/review/status"; private static final String REVIEW_AGENT = "review_agent"; private static final String REVIEW_STATUS = "reviewStatus"; private static final String REVIEW_STATUS_IN_PROGRESS = "IN_PROGRESS"; @org.osgi.service.component.annotations.Reference Replicator replicator; @org.osgi.service.component.annotations.Reference( service = ReferenceProvider.class, cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) private final List<ReferenceProvider> referenceProviders = new CopyOnWriteArrayList<ReferenceProvider>(); protected void bindReferenceProviders(ReferenceProvider referenceProvider) { referenceProviders.add(referenceProvider); } protected void unbindReferenceProviders(ReferenceProvider referenceProvider) { referenceProviders.remove(referenceProvider); } @Override protected final void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws ServletException, IOException { try { addJSONHeaders(response); if(PUBLISH_TO_REVIEW_URL.equals(request.getRequestPathInfo().getResourcePath())){ handlePublish(request, response); }else{ handleStatus(request, response); } } catch (Exception e) { log.error("Error processing publish to review..."); response.setStatus(SlingHttpServletResponse.SC_INTERNAL_SERVER_ERROR); } } private void handleStatus(SlingHttpServletRequest request, SlingHttpServletResponse response) throws Exception { JSONObject jsonObject = new JSONObject(); String parentPath = request.getParameter("parentPath"); if(StringUtils.isEmpty(parentPath)){ jsonObject.put("error", "No parent path provided"); jsonObject.write(response.getWriter()); return; } ResourceResolver resolver = request.getResourceResolver(); Session session = resolver.adaptTo(Session.class); if ((session != null) && session.isLive() && !session.nodeExists(parentPath)) { log.debug("No such node {} ", parentPath); return; } jsonObject = getReviewInProgressPages(resolver.getResource(parentPath)); jsonObject.write(response.getWriter()); } private JSONObject getReviewInProgressPages(Resource resource) throws Exception{ final JSONObject pagePaths = new JSONObject(); final Iterator<Resource> childResItr = resource.listChildren(); Resource childRes, jcrContent; Node jcrContentNode; while (childResItr.hasNext()) { childRes = childResItr.next(); if(childRes.getName().equals("jcr:content")){ jcrContent = childRes; }else{ jcrContent = childRes.getChild("jcr:content"); } if(jcrContent == null){ continue; } jcrContentNode = jcrContent.adaptTo(Node.class); if (!jcrContentNode.hasProperty(REVIEW_STATUS) || !jcrContentNode.getProperty(REVIEW_STATUS).getString().equals(REVIEW_STATUS_IN_PROGRESS)) { continue; } if(childRes.getName().equals("jcr:content")){ pagePaths.put(childRes.getParent().getPath(), REVIEW_STATUS_IN_PROGRESS); }else{ pagePaths.put(childRes.getPath(), REVIEW_STATUS_IN_PROGRESS); } } return pagePaths; } private void handlePublish(SlingHttpServletRequest request, SlingHttpServletResponse response) throws Exception { JSONObject jsonResponse = new JSONObject(); List<String> publishPaths = new ArrayList<String>(); ResourceResolver resolver = request.getResourceResolver(); Session session = resolver.adaptTo(Session.class); String pagePaths = request.getParameter("pagePaths"); for(String pagePath : pagePaths.split(",")){ Resource page = resolver.getResource(pagePath); Resource jcrContent = resolver.getResource(page.getPath() + "/" + JcrConstants.JCR_CONTENT); Set<Reference> allReferences = new TreeSet<Reference>(new Comparator<Reference>() { public int compare(Reference o1, Reference o2) { return o1.getResource().getPath().compareTo(o2.getResource().getPath()); } }); for (ReferenceProvider referenceProvider : referenceProviders) { allReferences.addAll(referenceProvider.findReferences(jcrContent)); } for (Reference reference : allReferences) { Resource resource = reference.getResource(); if (resource == null) { continue; } boolean canReplicate = canReplicate(resource.getPath(), session); if(!canReplicate){ log.warn("Skipping, No replicate permission on - " + resource.getPath()); continue; } if(shouldReplicate(reference)){ publishPaths.add(resource.getPath()); } } jcrContent.adaptTo(Node.class).setProperty(REVIEW_STATUS, REVIEW_STATUS_IN_PROGRESS); publishPaths.add(pagePath); } session.save(); doReplicate(publishPaths, session); jsonResponse.put("success", "true"); response.getWriter().write(jsonResponse.toString()); } private static boolean canReplicate(String path, Session session) { try { AccessControlManager acMgr = session.getAccessControlManager(); return acMgr.hasPrivileges(path, new Privilege[]{ acMgr.privilegeFromName(Replicator.REPLICATE_PRIVILEGE) }); } catch (RepositoryException e) { return false; } } private boolean shouldReplicate(Reference reference){ Resource resource = reference.getResource(); ReplicationStatus replStatus = resource.adaptTo(ReplicationStatus.class); if (replStatus == null) { return true; } boolean doReplicate = false, published = false, outdated = false; long lastPublished = 0; published = replStatus.isActivated(); if (published) { lastPublished = replStatus.getLastPublished().getTimeInMillis(); outdated = lastPublished < reference.getLastModified(); } if (!published || outdated) { doReplicate = true; } return doReplicate; } private void doReplicate(List<String> paths, Session session) throws Exception{ ReplicationOptions opts = new ReplicationOptions(); opts.setFilter(new AgentFilter() { public boolean isIncluded(com.day.cq.replication.Agent agent) { return agent.getId().equalsIgnoreCase(REVIEW_AGENT); } }); for(String path : paths){ replicator.replicate(session, ReplicationActionType.ACTIVATE, path, opts); } } public static void addJSONHeaders(SlingHttpServletResponse response){ response.setContentType(JSONResponse.RESPONSE_CONTENT_TYPE); response.setHeader("Cache-Control", "nocache"); response.setCharacterEncoding("utf-8"); } }
11) The necessary packages with components and templates are pre installed in Review (Package Install has sample template /apps/eaem-basic-htl-page-template)
12) Page in Review environment
Looks like this works for content pages.
ReplyDeleteHow about Experience Fragment, any suggestions.