Tuesday, 22 April 2014

First and Last Publish Dates in Sitecore

Sitecore users and developers occasionally ask how to find out when an item was most recently published or when it was first published. Unfortunately, the answer isn't as simple as you might assume.


Looking through the standard fields in the content editor, there are a couple of promising candidates, but these turn out to be red herrings:

Publish and Unpublish Fields
These fields specify publishing restrictions. They represent a date range during which it is possible to publish the item, but they don't have anything to do with actual publishes.

Updated Field
Some people think this field shows the last time the item was published. That's partially true, but it's slightly misleading. When you publish an item, the Updated field does change to show the current date and time, but it also changes whenever you save or move the item too. Behind the scenes, Sitecore's Item Provider is calling the UpdateRevision method of the ItemStatistics class.

The web database should only update when a publish occurs. So by definition, the Updated field should contain the most recent publish date. That's useful for reporting off that field, but isn't very helpful for non-technical users who just want to view it in the Content Editor.

GetLastPublishDate Method
Looking at the API we find a method named GetLastPublishDate hidden away in the DataProperties class. Again, this is a dead end. The method returns the last publish that occurred on a specified database, regardless of which items were affected. It also doesn't refresh for Smart Publishes.

Creating Custom Fields

So with no simple way of obtaining the first and last publish dates for each item, I decided to tackle the problem myself. I created 2 new Datetime fields (__First publish and __Last publish), to be included on every template. Whenever a publish occurs, these fields will be updated.

For my experiment I simply added the fields to the to the Statistics section of the standard template, but I'm sure you wouldn't do anything quite so reckless.


With my new fields in place these are the steps I took:
  1. Wait until after the PerformAction processor is complete in the PublishItem pipeline.
  2. Check that the publishing context is reporting success.
  3. Ensure the new fields exist in the published item.
  4. Set the fields in both the source and target database to the current date and time.
public class SetPublishDates : PublishItemProcessor
{
    private const String firstPublishField = "__First publish";
    private const String lastPublishField = "__Last publish";

    public override void Process(PublishItemContext context)
    {
        if (!SuccessfulPublishState(context))
            return;

        ID itemId = context.ItemId;
        Item sourceItem = context.PublishHelper.GetSourceItem(itemId);
        Item targetItem = context.PublishHelper.GetTargetItem(itemId);

        if (!FieldsExist(sourceItem) || !FieldsExist(targetItem))
            return;

        DateTime now = DateTime.Now;

        SetDates(sourceItem, now);
        SetDates(targetItem, now);
    }

    private bool SuccessfulPublishState(PublishItemContext context)
    {
        if (context == null || context.Result == null)
            return false;

        var op = context.Result.Operation;

        if (op != PublishOperation.Created && op != PublishOperation.Updated)
            return false;

        if (context.Action != PublishAction.PublishVersion)
            return false;

        if (context.VersionToPublish == null)
            return false;

        return true;
    }

    private bool FieldsExist(Item item)
    {
        if (item.Fields[firstPublishField] == null)
            return false;

        if (item.Fields[lastPublishField] == null)
            return false;

        return true;
    }

    private void SetDates(Item item, DateTime dt)
    {
        using (new Sitecore.SecurityModel.SecurityDisabler())
        {
            using(new EditContext(item,false, false))
            {
                var isoDt = DateUtil.ToIsoDate(dt);

                if (item[firstPublishField] == "")
                    item[firstPublishField] = isoDt;

                item[lastPublishField] = isoDt;
            }
        }
    }
}
Finally, the following needs to be added the /App_Config/Include folder, so that the new processor runs at the correct point in the pipeline:
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">  
  <sitecore>  
    <pipelines>
      <publishItem>
        <processor patch:after="*[@type='Sitecore.Publishing.Pipelines.
PublishItem.PerformAction, Sitecore.Kernel']"
type="PublishDates.SetPublishDates, PublishDates"/>
      </publishItem>
    </pipelines>
  </sitecore>  
</configuration>
There are a few places in the publishing process that I could have intervened to achieve my goal. But they all seemed to involve some copy-pasting of existing Sitecore code. I settled on this approach because it's quite self contained. However, it's worth noting that if you publish in bulk, then my solution might be a bit slower.

One thing that caught me out when working on this was that the disposal of EditContext invokes the UpdateStatistics method that I mentioned earlier. As a result, the item will always be flagged as changed. To overcome this, I used the overloaded version of the EditContext constructor. Setting the second parameter to false instructs Sitecore to skip UpdateStatistics.

While writing this post I found an article by Mike Reynolds in which he discusses similar points, but takes things a bit further by logging publishing data:
Who Just Published That? Log Publishing Statistics in the Sitecore Client


Image by Calsidyrose