Publishing information, like who published what and when they published it, isn’t readily available in the Sitecore item information. To find this information we typically have to look into the log.* files. A client requested that publish date and publish author be added to the item information. To accomplish this I found 2 very helpful articles from Sitecore legends, Mike Reynolds and Alex Shyba, with their page links at the end of the post. These guys took a lot of the heavy lifting away with the information they provided.
For this site we are working with a very early implementation of the Ignition framework. Ignition is an open-source Sitecore framework and can be obtained from here.
I want every item to contain the publish information so I added the fields to the ItemBase template as followed:
The ‘Title’ field for each of these items was set to the same value without the underscores so that it would display nicely.
I also wanted the Publish Stats section to show up right after the Quick Info section in the Content Editor and to do that I set the Sortorder field for that section to -1. I originally wanted to include this information in the Quick Info section, but that turned out to be so tightly coupled that it wouldn’t be worth the additional effort to do that.
From a template standpoint this was all that needed. Next comes the related code updates.
The new fields need added to the Interface that the ItemBase maps to:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Xml.Serialization; using Glass.Mapper.Sc.Configuration; using Glass.Mapper.Sc.Configuration.Attributes; using Sitecore.ContentSearch; using Sitecore.ContentSearch.Converters; using Sitecore.Data; using Sitecore.Globalization; namespace Common.Core.Models { [SitecoreType(TemplateId = "{4C3CDC24-1610-4808-92A3-A221768AE3B2}", AutoMap = true)] public interface IModelBase : IComparable<IModelBase>, IEquatable<IModelBase> { [SitecoreId, IndexField("_group")] Guid Id { get; set; } [SitecoreInfo(SitecoreInfoType.Language), IndexField("_language")] Language Language { get; set; } [TypeConverter(typeof(IndexFieldItemUriValueConverter)), XmlIgnore, IndexField("_uniqueid")] ItemUri Uri { get; set; } [SitecoreInfo(SitecoreInfoType.DisplayName)] string DisplayName { get; set; } [SitecoreInfo(SitecoreInfoType.Version)] int Version { get; } [SitecoreInfo(SitecoreInfoType.Path), IndexField("_path")] string Path { get; set; } [IndexField("_fullpath")] string FullPath { get; set; } [SitecoreInfo(SitecoreInfoType.Name), IndexField("_name")] string Name { get; set; } [SitecoreInfo(SitecoreInfoType.Url, UrlOptions = SitecoreInfoUrlOptions.LanguageEmbeddingNever)] string Url { get; set; } [SitecoreInfo(SitecoreInfoType.Url, UrlOptions = SitecoreInfoUrlOptions.AlwaysIncludeServerUrl | SitecoreInfoUrlOptions.LanguageEmbeddingNever)] string FullUrl { get; set; } [SitecoreInfo(SitecoreInfoType.TemplateId), IndexField("_template")] Guid TemplateId { get; set; } [SitecoreInfo(SitecoreInfoType.TemplateName), IndexField("_templatename")] string TemplateName { get; set; } [SitecoreChildren(IsLazy = false, InferType = true)] IEnumerable<IModelBase> BaseChildren { get; set; } [SitecoreParent(InferType = true)] IModelBase Parent { get; set; } [SitecoreField("__Sortorder"), IndexField("__Sortorder")] string Sortorder { get; set; } [SitecoreField(FieldId = "{536799CE-C76C-4D13-B77B-7AA745821E98}")] DateTime Published { get; set; } [SitecoreField(FieldId = "{B3DD018F-E9F7-48B4-BD6C-77429A833C59}")] string PublishedBy { get; set; } } }
Since I’m using a version of the Ignition Framework, I also need to update the BaseTreeDecorator, NullModel, and NullParams classes to include the new Published fields.
using System; using System.Collections.Generic; using System.Linq; using Common.Core.Models; using Glass.Mapper; using Glass.Mapper.Sc; using Sitecore.Data; using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.Globalization; namespace Common.Core.Utils { public class BaseTreeDecorator : IModelBase { private readonly IModelBase _item; private readonly ISitecoreService _service; public BaseTreeDecorator(IModelBase item, ISitecoreService service) { if (item == null) throw new ArgumentNullException("item"); if (service == null) throw new ArgumentNullException("service"); _item = item; _service = service; } public IModelBase Parent { get { return _item.Parent; } set { throw new NotImplementedException(); } } #region Delegated Methods public Guid Id { get { return _item.Id; } set { _item.Id = value; } } public Language Language { get { return _item.Language; } set { _item.Language = value; } } public ItemUri Uri { get { return _item.Uri; } set { _item.Uri = value; } } public string DisplayName { get { return _item.DisplayName; } set { _item.DisplayName = value; } } public int Version { get { return _item.Version; } } public string Path { get { return _item.Path; } set { _item.Path = value; } } public string FullPath { get { return _item.FullPath; } set { _item.FullPath = value; } } public string Name { get { return _item.Name; } set { _item.Name = value; } } public string Url { get { return _item.Url; } set { _item.Url = value; } } public string FullUrl { get { return _item.FullUrl; } set { _item.FullUrl = value; } } public Guid TemplateId { get { return _item.TemplateId; } set { _item.TemplateId = value; } } public string TemplateName { get { return _item.TemplateName; } set { _item.TemplateName = value; } } public IEnumerable<IModelBase> BaseChildren { get { return _item.BaseChildren; } set { _item.BaseChildren = value; } } public string Sortorder { get { return _item.Sortorder; } set { _item.Sortorder = value; } } public DateTime Published { get { return _item.Published; } set { _item.Published = value; } } public string PublishedBy { get { return _item.PublishedBy; } set { _item.PublishedBy = value; } } #endregion #region Helper Methods public IEnumerable<T> GetSelectChildren<T>() where T : IModelBase { return BaseChildren.Where(a => a is T).Cast<T>(); } /// <remarks> /// WARNING: Uses Sitecore search Axes, so performance might not be optimal. Consider using search if perfomance /// is a concern /// </remarks> public IEnumerable<T> GetSelectDescendents<T>() where T : IModelBase { return _item.CastTo<Item>().Axes.GetDescendants().Where(a => _service.Cast<IModelBase>(a) is T) .Select(a => _service.Cast<IModelBase>(a, false, true)).Cast<T>(); } /// <remarks> /// WARNING: Uses Sitecore search Axes, so performance might not be optimal. Consider using search if perfomance /// is a concern /// </remarks> public IEnumerable<IModelBase> GetAllDescendents() { return _service.GetItem<Item>(_item.Id) .Axes.GetDescendants() .Where(a => a.TemplateName != "Bucket") .Select(a => _service.Cast<IModelBase>(a)); } #endregion #region Interface Methods public int CompareTo(IModelBase other) { Assert.IsNotNull(other, typeof(IModelBase)); return string.Compare(Name, other.Name, StringComparison.CurrentCulture); } public bool Equals(IModelBase other) { return Id.Equals(other.Id) && Version == other.Version && Language == other.Language; } public override bool Equals(object obj) { if (!(obj is IModelBase)) return false; return Equals((IModelBase)obj); } public override int GetHashCode() { return base.GetHashCode(); } public override string ToString() { return string.Format("{0}|{1}|{2}", DisplayName, TemplateName, Id); } #endregion } }
using System; using System.Collections.Generic; using Glass.Mapper.Sc.Configuration.Attributes; using Sitecore; using Sitecore.Data; using Sitecore.Globalization; namespace Common.Core.Models { [SitecoreType(AutoMap = false, TemplateId = "{00000000-0000-0000-0000-000000000000}")] public sealed class NullModel : IModelBase { public int CompareTo(IModelBase other) { return -0; } public bool Equals(IModelBase other) { return other == null || other.Id == Guid.Empty; } public Guid Id { get { return Guid.Empty; } set { throw new NotImplementedException(); } } public Language Language { get { return Language.Current; } set { throw new NotImplementedException(); } } public ItemUri Uri { get { return new ItemUri(new ID(Guid.Parse("{11111111-1111-1111-1111-111111111111}")), Context.Database); } set { throw new NotImplementedException(); } } public string DisplayName { get { return "Null Item"; } set { throw new NotImplementedException(); } } public int Version { get { return -1; } } public string Path { get { return "/"; } set { throw new NotImplementedException(); } } public string FullPath { get { return "/"; } set { throw new NotImplementedException(); } } public string Name { get { return "Null Item"; } set { throw new NotImplementedException(); } } public string Url { get { return string.Empty; } set { throw new NotImplementedException(); } } public string FullUrl { get { return string.Empty; } set { throw new NotImplementedException(); } } public Guid TemplateId { get { return Id; } set { throw new NotImplementedException(); } } public string TemplateName { get { return string.Empty; } set { throw new NotImplementedException(); } } public IEnumerable<IModelBase> BaseChildren { get { return new List<IModelBase>(); } set { throw new NotImplementedException(); } } public IModelBase Parent { get { return new NullModel(); } set { throw new NotImplementedException(); } } public string Sortorder { get { return "0"; } set { throw new NotImplementedException(); } } public DateTime Published { get { return DateTime.MinValue; } set { throw new NotImplementedException(); } } public string PublishedBy { get { return string.Empty; } set { throw new NotImplementedException(); } } } }
using System; using System.Collections.Generic; using Glass.Mapper.Sc.Configuration.Attributes; using Sitecore; using Sitecore.Data; using Sitecore.Globalization; namespace Common.Core.Models { [SitecoreType(AutoMap = false, TemplateId = "{00000000-0000-0000-0000-000000000000}")] public sealed class NullParams : IParamsBase { public int CompareTo(IModelBase other) { return 0; } public bool Equals(IModelBase other) { return other == null || other.Id == Guid.Empty; } public Guid Id { get { return Guid.Empty; } set { throw new NotImplementedException(); } } public Language Language { get { return Language.Current; } set { throw new NotImplementedException(); } } public ItemUri Uri { get { return new ItemUri(new ID(Guid.Parse("{11111111-1111-1111-1111-111111111111}")), Context.Database); } set { throw new NotImplementedException(); } } public string DisplayName { get { return "Null Item"; } set { throw new NotImplementedException(); } } public int Version { get { return -1; } } public string Path { get { return "/"; } set { throw new NotImplementedException(); } } public string FullPath { get { return "/"; } set { throw new NotImplementedException(); } } public string Name { get { return "Null Item"; } set { throw new NotImplementedException(); } } public string Url { get { return string.Empty; } set { throw new NotImplementedException(); } } public string FullUrl { get { return string.Empty; } set { throw new NotImplementedException(); } } public Guid TemplateId { get { return Id; } set { throw new NotImplementedException(); } } public string TemplateName { get { return string.Empty; } set { throw new NotImplementedException(); } } public IEnumerable<IModelBase> BaseChildren { get { return new List<IModelBase>(); } set { throw new NotImplementedException(); } } public IModelBase Parent { get { return new NullModel(); } set { throw new NotImplementedException(); } } public string Sortorder { get { return "0"; } set { throw new NotImplementedException(); } } public DateTime Published { get { return DateTime.MinValue; } set { throw new NotImplementedException(); } } public string PublishedBy { get { return string.Empty; } set { throw new NotImplementedException(); } } public string PlaceHolderName { get; set; } } }
With the back-end templates updated to support the new fields, I implemented a processor similar to the one Mike Reynolds used in his blog.
using Sitecore; using Sitecore.Data; using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.Publishing.Pipelines.PublishItem; using Sitecore.SecurityModel; using System; namespace Domain.Infrastructure.Processors { public class UpdatePublishingInfo : PublishItemProcessor { private const string PublishedFieldName = "__Published"; private const string PublishedByFieldName = "__Published By"; public override void Process(PublishItemContext context) { SetPublishingInfoFields(context); } private void SetPublishingInfoFields(PublishItemContext context) { Assert.ArgumentNotNull(context, "context"); Assert.ArgumentNotNull(context.PublishOptions, "context.PublishOptions"); Assert.ArgumentNotNull(context.PublishOptions.SourceDatabase, "context.PublishOptions.SourceDatabase"); Assert.ArgumentNotNull(context.PublishOptions.TargetDatabase, "context.PublishOptions.TargetDatabase"); Assert.ArgumentCondition(!ID.IsNullOrEmpty(context.ItemId), "context.ItemId", "context.ItemId must be set!"); Assert.ArgumentNotNull(context.User, "context.User"); SetPublishingInfoFields(context.PublishOptions.SourceDatabase, context.ItemId, context.User.Name); SetPublishingInfoFields(context.PublishOptions.TargetDatabase, context.ItemId, context.User.Name); } private void SetPublishingInfoFields(Database database, ID itemId, string userName) { Assert.ArgumentNotNull(database, "database"); var item = TryGetItem(database, itemId); if (item != null && HasPublishingInfoFields(item)) { SetPublishingInfoFields(item, DateUtil.IsoNow, userName); } } private void SetPublishingInfoFields(Item item, string isoDateTime, string userName) { Assert.ArgumentNotNull(item, "item"); Assert.ArgumentNotNullOrEmpty(isoDateTime, "isoDateTime"); Assert.ArgumentNotNullOrEmpty(userName, "userName"); using (new SecurityDisabler()) { item.Editing.BeginEdit(); item.Fields[PublishedFieldName].Value = DateUtil.IsoNow; item.Fields[PublishedByFieldName].Value = userName; item.Editing.EndEdit(); } } private Item TryGetItem(Database database, ID itemId) { try { return database.Items[itemId]; } catch (Exception ex) { Log.Error(ToString(), ex, this); } return null; } private static bool HasPublishingInfoFields(Item item) { Assert.ArgumentNotNull(item, "item"); return item.Fields[PublishedFieldName] != null && item.Fields[PublishedByFieldName] != null; } } }
With the processor in place all that is needed is to include it in the publishing pipeline. As Mike Reynolds mentioned, we are putting it in the pipeline after the UpdateStatistics processor so that we can pull the information from there.
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <pipelines> <publishItem> <processor type="Domain.Infrastructure.Processors.UpdatePublishingInfo, Domain.Infrastructure" patch:after="processor[@type='Sitecore.Publishing.Pipelines.PublishItem.UpdateStatistics, Sitecore.Kernel']" /> </publishItem> </pipelines> </sitecore> </configuration>
Now that everything is in place, we can see the date and author of the last publish for any item in the tree.
I combined information from the following articles to complete this task:
https://sitecorejunkie.com/2013/01/26/who-just-published-that-log-publishing-statistics-in-the-sitecore-client/
http://sitecoreblog.alexshyba.com/quick_tip_resolving_section_sort_order_problem_with_inheritance/
If you have any questions or suggestions for other topics please feel free to use the comments below or reach out to me on Twitter bill_cacy.