Showing posts with label print experience manager. Show all posts
Showing posts with label print experience manager. Show all posts

Monday, 4 April 2016

Sitecore PXM and ODG project (part 4 – customising email)

  1. Introduction
  2. Non-editable fields
  3. Architecture
  4. Customising ODG email
  5. PXM InDesign custom task

In the next couple of posts I'd like to focus on some of the customisations that can be made to improve or build upon the functionality provided by PXM and ODG out of the box.

Let's start with something simple: renaming the PDF file that's output from ODG.  Fortunately for us, Sitecore has made life a little easier by checking whether the name has already been set before setting its own, which means all we have to do is insert our "naming" processor before the others. If we look at Sitecore.PrintStudio.config, we can see in the <printPreview>, <printToDashboard> and <printToInDesign> pipelines it's the Sitecore.PrintStudio.PublishingEngine.Pipelines.RenderProjectXmlargs.PrintOptions.ResultFileName, so let's insert our processor before this one. In this case I only care about the <printToDashboard> pipeline, but the others will be the same.
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <printToDashboard>
        <processor
          patch:before="processor[@type='Sitecore.PrintStudio.PublishingEngine.Pipelines.RenderProjectXml, Sitecore.PrintStudio.PublishingEngine']"
          type="Custom.Pipelines.NameOutputFiles, Custom"/>
      </printToDashboard>
    </pipelines>
  </sitecore>
</configuration>

And the processor to set the name
public class NameOutputFiles : Sitecore.PrintStudio.PublishingEngine.Pipelines.IPrintProcessor
{
  public void Process(PrintPipelineArgs args)
  {
    args.PrintOptions.ResultFileName = Utils.GenerateFilename(Context.User, args.ProcessorItem.InnerItem.ID.Guid) + args.PrintOptions.ResultExtension;
  }

  public static string GenerateFilename(User user, Guid itemId)
  {
    return string.Concat(user.LocalName, "_", itemId, "_", DateTime.UtcNow.ToString("yyyy-MM-dd-HH-mm-ss-fff"));
  }
}

In this case I've separated the code generating the name into a separate method in case we need to re-use the same naming method elsewhere in the code (for example to generate a link to the document).

Now let's look at how we can add additional recipients to the email that ODG sends out once the document has been exported.  By default it sends an email to notify the user (who generated the document) that their document is ready, with a link to the document.  What if we want this to be CCd or BCCd to for example their boss, or a support email?

For our example, let's add a support email as a BCC to all emails sent.

The code which handles sending the email is in the Dashboard Service, however the code which tells the Dashboard Service which email(s) to use is in the printToDashboard pipeline, in the SendToDashboard class, so let's extend that and switch it out in a patch:
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <printToDashboard>
        <processor 
          patch:instead="processor[@type='Sitecore.PrintStudio.PublishingEngine.Pipelines.Dashboard.SendToDashboard, Sitecore.PrintStudio.PublishingEngine']"
          type="Custom.Pipelines.SendToDashboard, Custom"/>
      </printToDashboard>
    </pipelines>
  </sitecore>
</configuration>

So taking a look at SendToDashboard in the Send method, we can see the message is being built by SoapHelper.SendSoapMessage() call, and taking a look in SoapHelper we find that the CreateGroupNode method takes a BCC field, to which SendSoapMessage is just passing a blank string.

Unfortunately here's where things get a bit messy. We need to get BCC into SendSoapMessage, so ideally we would extend SoapHelper and add another override of SendSoapMessage which takes BCC as a parameter (with pretty much exactly the same code), however a lot of the methods that are used in SoapHelper are marked private (not protected) so we can't re-use the existing code!  At the end of the day I found it easiest to just copy the SendSoapMessage code directly into the new SendToDashboard class and use reflection to call CreateGroupNode() and CreateJobNode().  It would be great if Sitecore made these protected in the future, so we can extend the SoapHelper class (or expose the CC and BCC fields).

So now in our SendToDashboard class we have our new Send() method which calls the new SendSoapMessage() method with our BCC field.  Let's take things a little further and say we want to pass new info into this email that's being sent.  What if we want to pass the name of the user's document into the email so that they know which one it was (sounds pretty standard, right)?

Unfortunately this is just as messy.  In this case it's the CreateSoapMessageDetails() method we need to override to take the document name and pass it to SoapMessageDetails(), which should pass it into the string.Format() which builds the "Success" or "Failed" nodes.  Again none of these are protected, so we need to pretty  duplicate the code all the way through with our new parameter.  Don't forget to update the email template to make use of this new parameter!
public class SendToDashboard : Sitecore.PrintStudio.PublishingEngine.Pipelines.IPrintProcessor
{
  public void Process(PrintPipelineArgs args)
  {
    if (!string.IsNullOrEmpty(args.XmlResultFile))
    {
      Item processingJobItem = args.ProcessorItem.Database.GetItem(args.PrintJobId);
      this.Send(args.XmlResultFile, Path.Combine(args.PrintOptions.ResultFolder, args.PrintOptions.ResultFileName), args.PrintOptions.ResultFolder, processingJobItem, args.ProcessorItem.InnerItem.Name);
    }
    else
    {
      Logger.Info("Empty args.XmlResultFile");
      args.AddMessage("Empty args.XmlResultFile", PipelineMessageType.Error);
      args.AbortPipeline();
    }
  }
  private void Send(string resultXmlFileName, string pdfFileName, string absoluteFilePath, Item processingJobItem, string docName)
  {
    if (string.IsNullOrEmpty(resultXmlFileName))
    {
      return;
    }
    string dbServerIpAddress = WebConfigHandler.PrintStudioEngineSettings.DbServerIpAddress;
    string dbServerPort = WebConfigHandler.PrintStudioEngineSettings.DbServerPort;
    Assert.IsNotNullOrEmpty(dbServerIpAddress, "Missing PrintStudio.DBServer.IPAddress configuration");
    Assert.IsNotNullOrEmpty(dbServerPort, "Missing PrintStudio.DBServer.Port configuration");
    Language userLanguage = SitecoreHelper.GetUserLanguage(Context.User, processingJobItem.Database);
    XmlDocument resultDocument = new XmlDocument();
    Logger.Info("Sending to Dashboard: " + Context.User.Name + " (" + Context.User.Profile.Email + ") file " + resultXmlFileName);
    SendSoapMessage(Context.User.Name,
      Context.User.Name,
      Context.User.Profile.Email,
      resultXmlFileName,
      WebConfigHandler.PrintStudioEngineSettings.ResponseType,
      WebConfigHandler.PrintStudioEngineSettings.DashboardQueueName,
      WebConfigHandler.PrintStudioEngineSettings.DashboardServiceMethod,
      SitecoreHelper.GetMessage(processingJobItem, userLanguage, "Mail Subject") + " " + TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, TimeZoneInfo.FindSystemTimeZoneById(Constants.General.Timezone)).ToString("g"),
 string.Empty,
     CreateSoapMessageDetails(processingJobItem, resultDocument, userLanguage, docName).ToString(),
     SitecoreHelper.GetPrintFilePath(processingJobItem, "relative filename path"),
     absoluteFilePath,
     pdfFileName,
     $"{dbServerIpAddress}:{dbServerPort}",
     processingJobItem.Database.GetItem(new ID(Constants.Items.SiteSettings))?.Fields[Constants.Fields.SupportEmail]?.Value);
  }
  private static string SendSoapMessage(string userName, string loginName, string email, string jobXml, string responseType, string serviceType, string serviceMethod, string subject, string body, string messageBrands, string relativePath, string absolutePath, string fileName, string serviceUrl, string bcc)
  {
     string startTime = DateTime.UtcNow.ToString("s", DateTimeFormatInfo.InvariantInfo);
     XmlDataDocument xmlDataDocument = new XmlDataDocument();
     XmlNode groupNode = (XmlNode)typeof(SoapHelper).GetMethod("CreateGroupNode", BindingFlags.Static | BindingFlags.NonPublic).Invoke(null, new object[] { xmlDataDocument, userName, userName, loginName, startTime, responseType, email, body, subject, string.Empty, bcc, string.Empty, "HTML" });
     XmlNode jobNode = (XmlNode)typeof(SoapHelper).GetMethod("CreateJobNode", BindingFlags.Static | BindingFlags.NonPublic).Invoke(null, new object[] { xmlDataDocument, userName, "1", serviceType, serviceMethod, string.Empty, fileName, relativePath, absolutePath, string.Empty, string.Empty, jobXml, string.Empty, serviceUrl });
     typeof(SoapHelper).GetMethod("AddJobNode", BindingFlags.Static | BindingFlags.NonPublic).Invoke(null, new object[] { xmlDataDocument, groupNode, jobNode });
     if (!string.IsNullOrEmpty(messageBrands))
     {
       XmlNode xmlNode = groupNode.SelectSingleNode("//Jobs/Job");
       if (xmlNode != null && groupNode.OwnerDocument != null)
       {
         XmlElement element = groupNode.OwnerDocument.CreateElement("ResultMail");
         element.InnerXml = messageBrands;
         xmlNode.AppendChild(element);
       }
    }
    return SoapHelper.DashBoardWebService.Send("<?xml version=\"1.0\" encoding=\"utf-16\" ?>" + groupNode.OuterXml); // returns "Success"
  }
  private static StringBuilder CreateSoapMessageDetails(Item processingJobItem, XmlDocument resultDocument, Language language, string jobName)
  {
    string message1 = SitecoreHelper.GetMessage(processingJobItem, language, "Mail Header");
    string message2 = SitecoreHelper.GetMessage(processingJobItem, language, "Mail Footer");
    string message3 = SitecoreHelper.GetMessage(processingJobItem, language, "Ready Message");
    string message4 = SitecoreHelper.GetMessage(processingJobItem, language, "Rejected Message");
    return SoapMessageDetails(resultDocument, message3, message4, message1, message2, jobName);
  }
  private static StringBuilder SoapMessageDetails(XmlDocument resultDocument, string jobSuccess, string jobFailure, string mailHeader, string mailFooter, string jobName)
  {
    StringBuilder stringBuilder = new StringBuilder();
    XmlNode xmlNode1 = resultDocument.CreateElement("Header");
    xmlNode1.AppendChild(resultDocument.CreateCDataSection(mailHeader));
    stringBuilder.Append(xmlNode1.OuterXml);
    XmlNode xmlNode2 = resultDocument.CreateElement("Footer");
    xmlNode2.AppendChild(resultDocument.CreateCDataSection(mailFooter));
    stringBuilder.Append(xmlNode2.OuterXml);
    XmlNode xmlNode3 = resultDocument.CreateElement("Success");
    xmlNode3.AppendChild(resultDocument.CreateCDataSection(string.Format(jobSuccess, Context.User.Profile.FullName, Context.User.LocalName, jobName)));
    stringBuilder.Append(xmlNode3.OuterXml);
    XmlNode xmlNode4 = resultDocument.CreateElement("Failed");
    xmlNode4.AppendChild(resultDocument.CreateCDataSection(string.Format(jobFailure, Context.User.Profile.FullName, Context.User.LocalName, jobName)));
    stringBuilder.Append(xmlNode4.OuterXml);
    return stringBuilder;
  }
}

This was a bit of a messy one. Hopefully with newer releases of PXM and ODG Sitecore makes things a little more extensible!

Wednesday, 16 March 2016

Sitecore PXM and ODG project (part 3 - architecture)

There are a LOT of components to set up when you're using PXM, ODG, and exporting your documents to PDF.  Adding even more complexity is the fact that this solution should really be running on a content delivery server accessible by the clients (or potentially multiple servers, in Azure IaaS/PaaS) whereas ODG natively runs in the Sitecore admin interface which would be on a content authoring server. Here's a list of things you'll need for the full setup:

Sitecore PXM ODG architecture
PXM + ODG architecture in authoring / delivery setup

Note: Unfortunately this setup can run multiple delivery servers (IaaS/PaaS), but will only really support one authoring server (IaaS) since there isn't really any way to balance the connections from delivery to the Dashboard/Processing/InDesign server side of things.  InDesign Server comes in a single or multi-instance licence, and if you need to scale then the multi-instance option on one high-spec'd authoring server would probably be best.

Firstly it's important to set up and ensure that Sitecore on content authoring is working correctly with PXM, ODG, and InDesign Server.  This functionality is "out of the box", but requires a lot of configuration which you can follow on the PXM youtube video series.  There has been an updated release of some of the PXM connectors to support CC 2015 so ensure you're using the correct software version and connector version for your OS.  The video series refers to the folder holding the exports / config / logs as PXMPublishing so that's what I will refer to it as here.

Once those pieces of software are setup, you should be able to:
  1. Import an InDesign document (through InDesign using the connector)
  2. Upload any media into the media library and replace the references in the PXM project so that they point to the media library rather than your hard drive (open the project through someone else's InDesign connector to verify)
  3. Create a document in ODG, replace various content / media, and preview it
  4. Export your document to PDF and preview the output PDF in the PXMPublishing/PublishFolder folder
There are various logs that can help you if you can't successfully complete all 4 steps:
  1. Sitecore logs
  2. PXM log (in with the rest of the Sitecore logs)
  3. Dashboard Server log (PXMPublishing/Logs/DashboardServer)
  4. Processing Server log (PXMPublishing/Logs/InDesignProcessingService)
  5. InDesign Server log (<InDesign Server directory>/Logs)
Once the content authoring server is running correctly, there are a few things that need to be setup so that everything works with the delivery server:
  • Ensure your PXMPublishing folder is accessible from both the authoring and delivery servers (through a file share or Azure File Storage works)
  • If you have a large amount of media (or very large media files) that you don't want to upload, ensure they're also in a shared location accessible from both servers (covered later in the blog)
  • Ensure the dashboard service port (by default 8070) on the CA server is open to the CD server (via both Azure and Windows firewall)
  • Ensure the InDesign server port (by default 8081) on the CA server is open to the CD server (via both Azure and Windows firewall)
  • Ensure the dashboard server in the web.config on the CD server is pointing to the URL / port of the dashboard server running on the CA server
  • Unfortunately a wrapper design controller will need to be created for the CD server to call the existing functionality. This wraps Sitecore.Odg.Controllers.DesignController.
  • If you're going to be allowing users to grab the output PDFs from the server, ensure the virtual folder is mapped in IIS on the CD server
Once that's done you should be able to call the GetCustomPreviewImage and ExportToPdf controller methods to get the preview image of your document, and export it to PDF, and view it via URL. For example:
$.ajax({
  dataType: 'json',
  url: '/api/sitecore/CustomDesign/GetCustomPreviewImage?&itemId=' + currentPageId + '&lang=en&forceNew=true&useHighRes=false&saveInPage=true',
  cache: false,
  success: function (response) {
    if (response.mediaUrl != null && response.mediaUrl != "") {
      updatePageThumbnail(response.thumbnailUrl);
      loadImageToCanvas(response.mediaUrl);
    }
  }
});
and
$.ajax('/api/sitecore/CustomDesign/ExportToPdf', {
    type: 'POST',
    data: {itemId: odgDocumentId, processingJobId: processingJobId, lang: language},
    success: function (result) {
        alert('Successfully sent for conversion to PDF')
    },
    error: function (result) {
        alert('Error saving.  Please try again or contact us if the issue reoccurs.');
    }
});
In the next blog we'll look into other customisations we can make to the process and pipelines to improve the user experience.

Monday, 7 March 2016

Sitecore PXM and ODG project (part 2 – non-editable fields)

  1. Introduction
  2. Non-editable fields
  3. Architecture
  4. Customising ODG email
  5. PXM InDesign custom task
Since we were using as much Sitecore-provided PXM and ODG functionality as possible (and ODG provides pretty much all of the functionality) the first task was to find a way to selectively make specific fields in the documents editable. ODG out of the box makes all text and image fields in the document editable, however images like the company logo, and text like terms and conditions should not be editable by the end user. There are (at least) two ways to go about this:
  1. Use security to restrict the user's read/write access to the field
    This is a little risky, down the line when we go to export the document the read/write access may be required to export the text/image field to PDF
  2. Create a new template which inherits from the default snippet
    Digging into the Sitecore SPEAK code*, we can see that in ODG the snippets are loaded by doing a query on the template type ('get all child items with the snippet template type'). By extending the existing snippet this query fill filter out new snippet type, so all the non-editable fields can be hidden in here, however when export to PDF is run it's still treated as a snippet and the text is shown. The downside to this is that the custom snippet is not supported by the InDesign connector so even though the snippet will appear in InDesign, making items non-editable (creating the non-editable snippet and dragging items into this snippet) will have to be done in the content editor.
We ended up going with option 2, though option 1 may still work, I did not get a chance to investigate.
So it's easy as finding the P_Snippet template (at /sitecore/templates/Print Studio Templates/Publishing Engine/P_Snippet), and making a custom template which contains that as a base template.

custom_snippet
Custom non-editable snippet made from P_Snippet

Sitecore PXM and ODG project (part 1 - introduction)

  1. Introduction
  2. Non-editable fields
  3. Architecture
  4. Customising ODG email
  5. PXM InDesign custom task
This series of posts will cover my discoveries while using Print Exprience Manager (PXM) and Online Document Generator (ODG) with Sitecore 8.1 initial release. It won't go into any detail on the setup of PXM or ODG as there are great resources and videos for getting these set up.  Unfortunately because it is an internal client project I can't post screenshots, but by the time the project went live it allowed the business to import InDesign documents (created by marketing) into Sitecore, which then displayed these to the end user for selection. Once a document was seelcted the user could then edit specified text and imagery in the document, save their document, export it as a PDF, and allow it to be sent to print. Although similar in functionality to out-of-the-box ODG, it was decided that the user should not be allowed access to the Sitecore admin interface to use ODG. We did, however, use as much existing functionality in PXM and ODG as possible; it was effectively a content-delivery based re-skin/wrapper of ODG.
While the majority of the documentation around PXM focuses on how to import Sitecore content into an InDesign document and format it, our project was the reverse: given a number of already-created InDesign documents, we were to import these into Sitecore (PXM) and allow them to be editable in ODG for export as high quality PDF to be used by the printing companies. Unfortunately our marketing team had little interest in learning Sitecore, and little time to explain the workings of InDesign. Coming from a Sitecore background (with minimal Photoshop knowledge and no InDesign knowledge) this meant that I certainly tought myself a lot in a very short time.

InDesign

Edit: just after I wrote this an excellent article on how to use the Sitecore connector with InDesign was posted
InDesign itself is based primarily on XML and Sitecore takes advantage of this using the connector (an InDesign plugin) to easily create or import the InDesign elements into Sitecore as items in various sections of the in the content tree. I'd highly recommend watching the series of PXM videos linked above (or at least the first few) to get an idea of how it all works together.   The elements in the InDesign document (shapes, images, text, etc.) are imported into PXM (sub-items in Print Studio projects); whereas the layers, character styles, paragraph styles, and object styles, are saved in the master document (in the media library) which is then referenced by the InDesign project(s). Images are not automatically imported into Sitecore; rather, a reference to the image location on your hard drive is saved in a field (meaning unless others have the image in the same location on the same drive letter, it will not appear for them). If you later choose to import the image into Sitecore, a second field (media reference) overrides this hard drive location reference.
Similar to those familiar with CSS, InDesign has what I'll call "inline" styles (like style="" in HTML), and then defined character and paragraph styles (saved groups of styles given a name, like a CSS class). If you simply select a bunch of text, and hit "bold" it will save it inline, or you can save your styles as a character/paragraph style and apply that to your text (some styles, like centering text, can only be applied to a paragraph). Unfortunately I found out the hard way that the Sitecore connector does not recognise these "inline" styles, but rather relies on character/paragraph styles having been created, which when imported are saved in the master document. I say unfortunate, because none of the InDesign documents we were provided had any character or paragraph styles applied. If you're importing an InDesign document you'll certainly want to ensure these exist first or your document is going to look very strange when you load it from Sitecore. We also discovered that you shouldn't use an ampersand (&) in the style name, as this is misinterpreted by InDesign (as least for TextFrames) and your TextFrame will not be positioned (or even appear) correctly.

Sitecore PXM and ODG


master_documents
Master documents (where styles are stored)

PXM project
PXM Projects (where shapes are stored)

ODG
ODG items
(where user documents are stored)
Installing ODG adds a couple of new icons to the dashboard (in this case the useful one is Document Publisher) and a new ODG section to the content tree.  The SPEAK applications are simply a pretty frontend for manipulating the items in the content tree, which can be done manually by more advanced users.  This new content tree section allows the creation of Collections, which were used to categorise our documents and are stored in the Design Collections bucket; Projects which are a clone of the original PXM project specific to the user who creates it and for some reason aren't stored in a bucket (the fact that they are a clone is useful as it means updates to the original PXM project are automatically applied to all ODG documents cloned from it, but also can be problematic if your user wants to keep a version of the original); Design Templates which specify which formats the PXM projects can be exported as; and Design Documents which link them all together and say "the user has created a document referencing this project (clone) belonging to this collection which can be exported as PDF/other". ODG The Document Publisher app allows the creation of Collections to group documents, as well as creating a user-specific version of a PXM project, edit the text and image fields (field types configurable in the config) in this document, and export it to specified format (through the processing job field which links to the PXM Publishing Settings) PDF high / low quality and Flash out of the box.  These correspond with InDesign Server settings which can be found in <install_directory>\Adobe InDesign CC Server 2014\Resources\Adobe PDF\settings\mul\*.joboptions

Part 2 will cover our first customisation for ODG: specifying which fields should be editable.