Quantcast
Channel: Experiencing Adobe Experience Manager (AEM, CQ)
Viewing all articles
Browse latest Browse all 525

AEM 65 - Setup a Review Env for Preview before Publishing to Live

$
0
0

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



Viewing all articles
Browse latest Browse all 525

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>