Dmgdotnet's Blog

Sitecore and stuff

Aggregated Personalisation – Part 1

Posted by dmgdotnet on October 30, 2012

A feature of Sitecore’s DMS that is often misunderstood is the personalisation rules around the visitors session. It is often assumed that the default behaviour is for DMS to accumulate scores for all visits and not just for the current one, this is simply not the case.  If you visit a site running personalisation and you exhibit behaviour as, for example, a landscape photographer, the site may respond by giving you personalised content targeted at your demographic. If you close the browser and return to the site, despite being recognised as a returning visitor, your personalisation experience will start again as though it is your first visit.

I am hoping to put together a series of posts, with hopefully some community help (:, outlining some approaches that can be taken to aggregate different kinds of personalisation.

Profiling

For the first post we will look into aggregating profiling scores.  Some quick disclaimers, at the time of writing:

  1. THIS HAS NOT BEEN LOAD TESTED
    Obviously the major concern with these customisations is the affect on performance. I have not had a chance to load test this AT ALL so please approach this with caution if you plan to use any aggregation approaches in your solution. I will hopefully have a chance to set up a testing environment in the near future to provide a better understanding of the impact of these changes.
  2. These code samples have been put together quickly so if you have a better idea or can see any issues, please point them out and I will update.

Onto the example; for this I have used Sitecore 6.5 which has a different API to the older OMS version meaning this will need modification to run on the OMS. My first step was to use dotPeek to take a look at the existing profile condition “Sitecore.Analytics.Rules.Conditions.ProfileCondition”.   The method we want to change is “GetProfileKeyValue” which is private so we will also need to override the “Execute” method in this case. I’ve added an additional property called “Visits” that will allow the author/developer to set how many visits we wish to include where zero means all visits (CAUTION!!).

        private double GetProfileKeyValue()
        {
            if (string.IsNullOrEmpty(ProfileKeyId))
                return 0.0;
            var obj = Tracker.DefinitionDatabase.GetItem(ProfileKeyId);
            if (obj == null)
                return 0.0;
            var parent = obj.Parent;
            if (parent == null)
                return 0.0;
            var name1 = obj.Name;
            var visits = int.Parse(Visits);

            var index = Tracker.Visitor.GetOrCreateCurrentVisit().VisitorVisitIndex;
            Tracker.Visitor.Load(new VisitorLoadOptions
                {
                    Start = visits == 0 || index - visits < 0 ? 0 : index - visits,
                    Count = visits == 0 || index + 1 < visits ? index + 1 : visits,
                    VisitLoadOptions = VisitLoadOptions.Profiles,
                    Options = VisitorOptions.None                 
                }
             );             
            return Tracker.CurrentVisit.Profiles.Sum(v => v.GetValue(name1));
        }

To get this working was a very small change in the end.  Firstly I had to modify the VisitorLoadOptions to return more than a single record.  We set ‘Start’ to the appropriate index based on the condition configuration and how many times the current user has visited the site.  We set the ‘Count’ to the number of rows to return and the rest of the options remain the same.  From here just need to sum the values for the given profile to get our aggregated profile score.

The only thing that is left is to create the content item that will reference our new class. It’s easiest just to copy the existing Profile Condition from ‘/sitecore/system/Settings/Rules/Conditional Renderings/Conditions/Profiles and Patterns/Profile Condition’ and update the Type field to your new class.

sitecore content tree

Location of new aggregated profile condition in the content tree

We also need to add in a token to allow users to set the value of our ‘Visits’ property in our class (outlined in red).

aggregated profile condition

With this in place we can set a personalisation rule that will aggregate profile scores across the specified number of visits for our current user.

rule editor

Posted in Sitecore | Tagged: , | Leave a Comment »

Link to item with no Read access

Posted by dmgdotnet on December 14, 2011

If you’ve ever restricted read access to content for authors in Sitecore you would have quickly noticed that these users can no longer view these items in any part of the system. This is to be expected however it can be a bit limiting.  The scenario I’ve recently come across is where users are trying to link to content they may not necessarily have the correct permissions to view.  In this case it was a show-stopper so we implemented what turned out to be a reasonably simple solution.

The 2 main locations users link to items are in General Link fields and the Rich Text Editor. There are many others of course however the requirements for this project were for these 2 only.  These areas of the application use xml controls located in ‘/sitecore/shell/applications/dialogs/internallink’  and ‘/sitecore/shell/Controls/Rich Text Editor/InsertLink’ respectively. Both of these files use the TreeviewEx class to render the content tree which in turn references the DataContext class that it uses to populate the tree.  It’s with this DataContext class that we will begin the customisation, first of all we need to point the DataContext to a custom DataView that we will create later.

In both files I changed this

<DataContext ID="InternalLinkDataContext" />

To this

<DataContext ID="InternalLinkDataContext" DataViewName="Link" Parameters="ignoresecurity=true"/>

Sitecore has another nice feature that I used here. There is an override folder found in /sitecore/shell/override that allows you to make modifications to these xml files without changing the actual Sitecore files. As long as the files are named the same, Sitecore will check the override folder first when initialising these controls in the client. This will allow you to make modifications without having to worry about losing the changes during an upgrade.

Next we need to create the DataView class that our DataContext is referencing. There are a few of these already defined in the <dataviews> section of our web.config. These views allow us to pass parameters, in this case I’m going to use the parameter we added to our DataContext above to determine if we should ignore security when fetching items for the view.

public class LinkDataView : MasterDataView
{
  private bool _ignoreSecurity;

  public override void Initialize(string parameters)
  {
    base.Initialize(parameters);
    var str = new UrlString(parameters);
    bool.TryParse(StringUtil.GetString(new[] { str["ignoresecurity"] }), out _ignoreSecurity);
  }

We then need to override 4 methods to check for our security bool, if true we want to ignore security.

protected override void GetChildItems(ItemCollection items, Item item)
{
  Error.AssertObject(items, "items");
  if (item == null) return;

  ChildList children;
  if (!Settings.ContentEditor.CheckSecurityOnTreeNodes || _ignoreSecurity)
  {
    children = item.GetChildren(ChildListOptions.IgnoreSecurity);
  }
  else
  {
    children = item.Children;
  }
  items.AddRange(children.ToArray());

}

protected override Item GetItemFromID(string id, Language language, Version version)
{
  if (_ignoreSecurity)
  {
    using (new Sitecore.SecurityModel.SecurityDisabler())
    {
      return base.GetItemFromID(id, language, version);
    }
  }

  return base.GetItemFromID(id, language, version);
}

public override bool HasChildren(Item item, string filter)
{
  Sitecore.SecurityModel.SecurityCheck enable;
  Assert.ArgumentNotNull(item, "item");
  Assert.ArgumentNotNull(filter, "filter");
  if (filter.Length != 0)
  {
    return (GetChildren(item, string.Empty, true, 0, 0, filter).Count > 0);
  }
  if (!Settings.ContentEditor.CheckHasChildrenOnTreeNodes)
  {
    return true;
  }
  if (Settings.ContentEditor.CheckSecurityOnTreeNodes && !_ignoreSecurity)
  {
    enable = Sitecore.SecurityModel.SecurityCheck.Enable;
  }
  else
  {
    enable = Sitecore.SecurityModel.SecurityCheck.Disable;
  }
  return ItemManager.HasChildren(item, enable);
}

protected override Item GetParentItem(Item item)

{
  if (item != null)
  {
    var enable = _ignoreSecurity ? Sitecore.SecurityModel.SecurityCheck.Disable : Sitecore.SecurityModel.SecurityCheck.Enable;
    return ItemManager.GetParent(item, enable);
  }
  return null;
}

We now need to get this class into our configuration. This is easily achieved using an include file in /app_config/include which again will help us in the future during upgrades.

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <dataviews>
      <dataview name="Link" assembly="Sitecore.Custom" type="Sitecore.Custom.Component.LinkDataView" Parameters="" />
    </dataviews>
  </sitecore>
</configuration>

The last piece to our puzzle is to do with the TreeviewEx control itself. After a bit of digging around I discovered that when you click the ‘+’ button to expand the tree, an Ajax call is made to a webform to retrieve the children of the section you are expanding. In this case it is a core Sitecore file that needs to be changed to get our module working; not ideal for upgrading but the change works. I created a new class to become the codebehind for this file:

public class TreeviewExPage : Page
{
  // Methods
  protected void Page_Load(object sender, EventArgs e)
  {
    Assert.ArgumentNotNull(sender, "sender");
    Assert.ArgumentNotNull(e, "e");
    var child = new TreeviewEx();
    Controls.Add(child);
    child.ID = WebUtil.GetQueryString("treeid");
    var queryString = WebUtil.GetQueryString("db", Client.ContentDatabase.Name);
    var database = Factory.GetDatabase(queryString);
    Assert.IsNotNull(database, queryString);
    var itemId = ShortID.DecodeID(WebUtil.GetQueryString("id"));
    Item item;
    using(new Sitecore.SecurityModel.SecurityDisabler())
    {
      item = database.GetItem(itemId);
    }
    if (item != null)
    {
      child.ParentItem = item;
    }
  }
}

To use this we just need to update the inherits attribute of our aspx page being used for the Ajax call found in /sitecore/shell/Controls/TreeviewEx/TreeviewEx.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="TreeviewEx.aspx.cs" Inherits="Sitecore.Custom.Component.TreeviewExPage" %>

Robert’s your fathers brother

Posted in Sitecore | Tagged: | Leave a Comment »

Remove old Sitecore installs

Posted by dmgdotnet on November 8, 2011

If you are like me and tend to do a fresh install of Sitecore everytime you begin a new module or feature then you will have a very large list of Sitecore sites in IIS.  The main reason I never clean these out is due to the work involved of Removing IIS site, detaching db’s, deleting files etc. etc.  To speed this up I thought I would give PowerShell a run at automating this for me.  The resulting script does a reasonable job of this however I am new to PowerShell and would welcome any pointers or improvements to what I came up with.

param($site)

if($site -eq $null){"No site paramater"; exit; }
import-module WebAdministration

if ( (Get-PSSnapin -Name SqlServerCmdletSnapin100 -ErrorAction SilentlyContinue) -eq $null )
{
    Add-PsSnapin SqlServerCmdletSnapin100
}
if ( (Get-PSSnapin -Name SqlServerProviderSnapin100 -ErrorAction SilentlyContinue) -eq $null )
{
    Add-PsSnapin SqlServerProviderSnapin100
}

$SqlServerConnectionTimeout = 8

function Match-Path
{
    param($dbFilePath, $dbdirs)
    foreach($dir in $dbdirs)
    {
      if([regex]::escape($dir.ToLower()) -eq [regex]::escape($dbFilePath.ToLower()))
      {
        return $true
      }
    }
    return $false
}

$website = Get-Website | Where {$_.Name -eq $site}
if($website -eq $null)
{
    "Website not found - Cannot Continue"
    exit
}
else
{
    "Website found: " + $website.Name
}

"Continue? 'y' to continue removal"
$cont = Read-Host
if($cont -ne 'y')
{
  exit
}

"Stopping Site: " + $website.Name
Stop-Website $website.Name
$website.Name + " Stopped"

$websitepath = $website.PhysicalPath
$apppath = $websitepath | Split-Path -parent
$appPath
$dbfiles = Get-ChildItem $apppath -recurse -include *.mdf
$dbdirectories = $dbfiles | Split-Path -parent | Get-Unique
"Found database directories: " + $dbdirectories

foreach($dbServer in gci SQLSERVER:\SQL\localhost)
{
    $arrService = Get-Service -Name ("MSSQL$" + $dbServer.DisplayName)

    if($arrService.Status -eq "Running")
    {
       $matches = $dbServer.databases | Where {Match-Path $_.PrimaryFilePath $dbdirectories} | foreach-object -process {$_.Name  }
       if($matches -ne $null)
       {
           "Found attached databases: " + $matches
            "Detaching Dbs"
            foreach($db in $matches)
            {
                $dbServer.KillAllprocesses($db.Name)
                "Detach: " + $db
                $dbServer.DetachDatabase($db, $false, $false)
            }

        } else
        {
            "No attached dbs found on " + $arrService.Name
        }
    }

}

gci IIS:\AppPools\ | Where {$_.Name -match $website.Name} | foreach{$_.Stop()}

"Deleting Site: " + $website.Name

Remove-Website $website.Name

if(($apppath | Split-Path -leaf) -eq "wwwroot" )
{
    "Deleting all files: " + $websitepath
    "Continue? 'y' to delete"
    $response = Read-Host
     if($response -eq "y")
    {
        "Deleting..."
        Remove-Item -Recurse -Force $websitepath
    }else{
        "Delete Skipped"
    }
}else
{
    "Deleting all files; " + $apppath
    "Continue? 'y' to delete"
    $response = Read-Host
    if($response -eq "y")
    {
        "Deleting..."
        Remove-Item -Recurse -Force $apppath
    }else{
        "Delete Skipped"
    }
}

gci IIS:\AppPools\ | Where {$_.Name -match $website.Name} | foreach{$_.Start()}

Get-ChildItem HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall | Where {$_.GetValue("InstallLocation") -match [regex]::escape($apppath) + "\\"} | foreach{ Remove-Item $_.Name.Replace("HKEY_LOCAL_MACHINE\", "HKLM:\") }
Get-ChildItem "HKLM:\SOFTWARE\Wow6432Node\Sitecore CMS" | Where {$_.GetValue("InstanceDirectory") -match [regex]::escape($apppath) + "\\"} | foreach{ Remove-Item $_.Name.Replace("HKEY_LOCAL_MACHINE\", "HKLM:\") }

EDIT:
Made some changes around cleaning up some registry entries and some other modifications. Starting to feel a little bit Hacky but hey, it does the job. Use with caution!

Posted in Sitecore | Leave a Comment »

Notifications

Posted by dmgdotnet on November 23, 2010

With the ever increasing average number of content authors in a standard Sitecore installation there comes the requirement of informing all these users of imminent events like server downtime, upcoming publish operation etc.  This in mind I thought this could be easily achieved by adding a Sitecore UI that allows you to select multiple roles or users and notify them via email with the single click of a button; result – Notifications Module.  If this sounds like something that could be utilised in your project, check out the module on the shared source library here.

Feel free to leave any feedback or suggestions for improvement in the comments and I’ll do my best to incorporate them in the next release.

Posted in Sitecore | Leave a Comment »

Tasker – The Business

Posted by dmgdotnet on November 11, 2010

For the Android lovers out there that haven’t discovered this awesome app yet get your hands on a copy ASAP.  It really lets you open up the capabilities of your device; do a quick google search to bring up a huge list of possiblities including things like automatically enabling your wireless when you are in a certain radius of your house (based off network or GPS location or both). If your like me home is typically the only place you connect to a wifi network so anytime I’m not there I turn off wifi on my phone to save the battery, with tasker this all happens automatically.

If you’re a vodafone user this is another profile you could setup.

Requirement: Enable voicemail during work hours and set ringer volume to a low level. Outside work hours turn off voicemail and turn up the volume on the ringer.
Profile: Create a profile for weekdays that starts at 9am and goes to 5pm.
Task: Automatically call 1211 to turn on voicemail for busy, unavaliable and no answer; set ringer volume to 3.
Exit Task: Automatically call 1210 to turn off voicemail; set ringer volume to 7.

Obviously you could achieve the same thing on other networks just as easily by replacing the number that is automatically dialled.

Posted in Android | Leave a Comment »

Sitecore Rich Text Editor – Setting background colour for resized images

Posted by dmgdotnet on May 14, 2010

If you’ve ever played around at resizing images in Sitecore’s RTE then you may have noticed the default background colour for ill-proportioned images comes through as black. There may be a number of ways to combat this but 1 QUICK  fix for this is to automatically append a parameter (bc) to the media items url when it is inserted into the RTE.  As I said this is a quick fix that may not be ideal for many situations as if you wish to change this colour in the future you will have to go through all of your RTE’s and update the images there.  

Step 1: Find our class that handles the insertion of images into the RTE.
After a bit of digging you can quickly find the InsertImage xml file here: sitecore\shell\Controls\Rich Text Editor\InsertImage
At the top of this file we see the code beside tag that points to the C# class for this interface.  Using Reflector we can grab the existing code for this dialog and make our (small) customisations to it. Before we do this however, it’s a good idea to create a configuration file for setting the colour we are going to use as no-one likes hardcoded values….  

As a best practice for configurations we want to add a new config file in the App_Config\Include directory similar to:  

  
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <settings>
      <!--
        background colour appended to querystring of images
        inserted into rich text fields
        when resizing an image without constraining
        proportions this colour is used to fill
        use html colour codes eg. #FFFFFF
        to turn off leave blank
      -->
      <setting name="DefaultImageBackgroundColour" value="#16527D">
    </settings>
  </sitecore>
</configuration>

 
Now all we need to do is check this setting when inserting our image and apply if necessary

 
...
MediaUrlOptions options2 = new MediaUrlOptions();
options2.UseItemPath = false;
options2.AbsolutePath = true;
//check for default colour setting and apply if available
if (!String.IsNullOrEmpty(Settings.GetSetting("DefaultImageBackgroundColour")))
{
  try
  {
    options2.BackgroundColor = ColorTranslator.FromHtml(Settings.GetSetting("DefaultImageBackgroundColour"));
  }
  catch
  {
    //Colour translator was unable to translate the string value
    SheerResponse.Alert("Background colour setting has an invalid value")
  }
}
MediaUrlOptions options = options2;
string text = !string.IsNullOrEmpty(HttpContext.Current.Request.Form["AlternateText"]) ? HttpContext.Current.Request.Form["AlternateText"] : item.Alt;
TagBuilder image = new TagBuilder("img");
...

And the very last step is to point our InsertImage.xml file to our new class and library. Now when we insert an image into the RTE it’s url when have the bc parameter set to our config colour.
Rick text editor image background colour

Posted in Sitecore | Leave a Comment »

Sitecore – Publish via Scheduled Task

Posted by dmgdotnet on March 15, 2010

While it may seem like a strange requirement, I recently came across the request to be able to set Content Items in Sitecore to be only published via a Sitecore Scheduled Task. While the client still wanted the flexibility of allowing their content authors to publish as often as they required, there were certain items in the content tree that would cause a relatively large amount of traffic during a publish.  While considering a solution to this I realised, while relatively simple, we would be customising some interesting parts of the system:

  • Publishing Agent – The out of the box scheduled task that runs the publisher to a schedule
  • Process Queue – The pipeline that handles the queued publishing request
  • Publisher – The class that processes publishing requests
  • item:publish – The command for publishing a single item in the content tree
  • item:publishnow – The command for immediate publishing of an item
  • Set Publishing Form – The dialog where we change publishing restrictions
  • Schedule publish editor warnings – The warning/information displayed at the top of the content window in the content editor
  • Gutter Publishing Warnings – The warning that displays as an icon in the gutter of the content tree

First things first, before we get started we need to setup a boolean value on our system publishing template in order to track the items we want to only publish with the scheduler. This data template can be found at /templates/system/templates/sections/publishing. Add a checkbox field to the Publishing section and call it __Publish Only in Schedule or anything else you like.  After creating the field you can also change the Title of it under the data section of the new field to be something more readable: Publish Only in Schedule

Publisher

The first place to start in terms of coding is customising the Publisher itself, namely adding some small changes to the Sitecore.Publishing.Publisher class that will allow us to identify when this is being run by the scheduler rather than as a manual publish.  One way to accomplish this is to take advantage of the JobManager classes ability to associate parameters with the function being called when the job is run.  Utilising the JobManager is default behaviour of the Publisher however in order to add parameters when initialising the JobOptions of the job being created we have to have a method statement that matches the parameters we are passing. The easiest way to achieve this is to inherit the Publisher and make some small modifications:

  ...
  //create a method where we can associate parameters for tracking the origin of a publish call
  public virtual void Publish(bool empty)
  {
    base.Publish();
  }

  public virtual Job PublishAsync(bool schedule)
  {
    this.AssertState();
    JobOptions options;
    if (!schedule)
    {
      //standard JobOptions declaration
      options = new JobOptions(this.GetJobName(), "publish", "publisher", this, "Publish");
    }
    else
    {
      //if call is from a schedule, add a boolean parameter that is set to true
      options = new JobOptions(this.GetJobName(), "publish", "publisher", this, "Publish", new object[]{true});
    }
    options.ContextUser = Context.User;
    options.AfterLife = TimeSpan.FromMinutes(1.0);
    options.AtomicExecution = true;
    options.CustomData = new PublishStatus();
    return JobManager.Start(options);
  }
  ...

Publishing Agent

Next step is to customise the publishing agent. This is the class that is run by the scheduler and is specified in the web.config. Using reflector we can copy the functionality of the original class and make a small change so that it is calling the new Publisher class we just created.


 private void StartPublish(Language language)
 {
   Assert.ArgumentNotNull(language, "language");
   Log.Info(string.Concat(new object[] { "PublishAgent started (source: ", this.SourceDatabase, ", target: ",
                          this.TargetDatabase, ", mode: ", this.Mode, ")" }), this);
   Database database = Factory.GetDatabase(this.SourceDatabase);
   Database database2 = Factory.GetDatabase(this.TargetDatabase);
   Assert.IsNotNull(database, "Unknown database: {0}", new object[] { this.SourceDatabase });
   Assert.IsNotNull(database2, "Unknown database: {0}", new object[] { this.TargetDatabase });
   PublishOptions options = new PublishOptions(database, database2, this.Mode, language, DateTime.Now);

   //use custom publisher
   CustomPublisher publisher = new CustomPublisher(options);
   bool willBeQueued = publisher.WillBeQueued;

   //pass true bool value to indicate call is coming from the scheduler
   publisher.PublishAsync(true);
   Log.Info("Asynchronous publishing " + (willBeQueued ? "queued" : "started") + ". Job name: " + publisher.GetJobName(), this);
   TaskCounters.Publishings.Increment();
 }

We also need to change the configuration in the web.config to point to this class when the schedule is run. The interval needs to be changed from zero in order for the schedule to run.


<!--<agent type="Sitecore.Tasks.PublishAgent" method="Run"  interval="00:00:00">-->
<agent type="Sitecore.Starterkit.Classes.CustomPublishingAgent" method="Run" interval="00:15:00">
  <param desc="source database">master</param>
  <param desc="target database">web</param>
  <param desc="mode (full or incremental)">incremental</param>
  <param desc="languages">en, da</param>
</agent>

Process Queue

Next is to customise the process queue, here we need to check for a bool parameter for the job that is being run in the pipeline to determine if the current request is an automatic publish or a manual one.  If it is an automatic publish we can skip the checking of the __Publish Only in Schedule field otherwise we need to check this field and skip the publish if it is set to true.

 protected virtual void ProcessEntries(IEnumerable<PublishingCandidate> entries, PublishContext context)
 {
   //get parameters from current job
   object[] obj = context.Job.Options.Parameters;
   bool schedule = false;
   if (obj != null && obj.Count() > 0)
   {
     try
     {
       //if the parameter is true we know that this job has come from the scheduler
       schedule = (bool)obj[0];
     }
     catch { }
   }

   foreach (PublishingCandidate candidate in entries)
   {
     PublishItemContext publishItem = this.CreateItemContext(candidate, context);

     if (schedule || !this.SkipScheduledOnly(publishItem, context))
     {
       PublishItemResult result = PublishItemPipeline.Run(publishItem);

       if (!this.SkipReferrers(result, context))
       {
         this.ProcessEntries(result.ReferredItems, context);
       }
       if (!this.SkipChildren(result, candidate, context))
       {
         this.ProcessEntries(candidate.ChildEntries, context);
       }
     }
   }
 }

 protected virtual bool SkipScheduledOnly(PublishItemContext result, PublishContext context)
 {
   Item itm  = context.PublishOptions.SourceDatabase.GetItem(result.ItemId);
   if (itm != null && itm.Fields["__Publish Only in Schedule"] != null && itm.Fields["__Publish Only in Schedule"].Value == "1") return true;
   return false;
 }

Again we need to update the web.config and change the publishing pipeline to point to our new queue processer.

<publish help="Processors should derive from Sitecore.Publishing.Pipelines.Publish.PublishProcessor">
  <processor type="Sitecore.Publishing.Pipelines.Publish.AddLanguagesToQueue, Sitecore.Kernel"/>
  <processor type="Sitecore.Publishing.Pipelines.Publish.AddItemsToQueue, Sitecore.Kernel"/>
  <!--<processor type="Sitecore.Publishing.Pipelines.Publish.ProcessQueue, Sitecore.Kernel"/>-->
  <processor type="Sitecore.Starterkit.ProcessQueue, Sitecore.Starterkit"/>
</publish>

Now all we have left to do is to make some customisations to the content editor to give the option for enabling this setting and to give the content editor some visual feedback when this restriction is in place.

item:publish; item:publishnow

Due to Sitecore’s flexibility customising the commands from the ribbon is an easy process.  These commands are all configured in the Commands.Config file located in the App_Config directory.  We need to change this command to give a visual alert when an editor tries to publish an item that has this restriction in place.  First some simple code that checks an item for the restriction that will be used in both commands:

public static bool CheckScheduleOnly(ClientPipelineArgs args, Item item)
 {
   Assert.ArgumentNotNull(args, "args");
   Assert.ArgumentNotNull(item, "item");
   if (item.Fields["__Publish Only in Schedule"] == null
       || string.IsNullOrEmpty(item.Fields["__Publish Only in Schedule"].Value.ToString())
       || item.Fields["__Publish Only in Schedule"].Value == "0") return true;
   SheerResponse.Alert(Translate.Text
             ("The current item \"{0}\" can only be published by the publishing scheduler. It will be published during next scheduled publish."
             , item.DisplayName));
   return false;
 }

Then we need to add the checking to the Run method of both commands.

item:publish


 new protected void Run(ClientPipelineArgs args)
 {
   Assert.ArgumentNotNull(args, "args");
   string str = args.Parameters["id"];
   string name = args.Parameters["language"];
   string str3 = args.Parameters["version"];
   if (SheerResponse.CheckModified())
   {
     Item item = Context.ContentDatabase.Items[str, Language.Parse(name), Sitecore.Data.Version.Parse(str3)];
     if (item == null)
     {
       SheerResponse.Alert("Item not found.", new string[0]);
     }
     //check for schedule only flag
     else if (CheckScheduleOnly(args, item) && CheckWorkflow(args, item))
     {
       Log.Audit(this, "Publish item: {0}", new string[] { AuditFormatter.FormatItem(item) });
       Items.Publish(item);
     }
   }
 }

item:publishnow


 new protected void Run(ClientPipelineArgs args)
 {
   Assert.ArgumentNotNull(args, "args");
   string str = args.Parameters["id"];
   string name = args.Parameters["language"];
   string str3 = args.Parameters["version"];
   Item item = Client.ContentDatabase.Items[str, Language.Parse(name), Sitecore.Data.Version.Parse(str3)];
   if (item == null)
   {
     SheerResponse.Alert("Item not found.", new string[0]);
   }
   //check for schedule only flag
   else if (CheckScheduleOnly(args, item) && CheckWorkflow(args, item) && SheerResponse.CheckModified())
   {
     if (args.IsPostBack)
     {
       if (args.Result == "yes")
       {
         Database[] targets = GetTargets();
         if (targets.Length == 0)
         {
           SheerResponse.Alert("No target databases were found for publishing.", new string[0]);
         }
         else
         {
           LanguageCollection languages = LanguageManager.GetLanguages(Context.ContentDatabase);
           if ((languages == null) || (languages.Count == 0))
           {
             SheerResponse.Alert("No languages were found for publishing.", new string[0]);
           }
           else
           {
             Log.Audit(this, "Publish item now: {0}", new string[] { AuditFormatter.FormatItem(item) });
             PublishManager.PublishItem(item, targets, languages.ToArray(), false, true);
             SheerResponse.Alert("The item is being published.", new string[0]);
           }
         }
       }
     }
     else
     {
       SheerResponse.Confirm(Translate.Text("Are you sure you want to publish \"{0}\"\nin every language to every publishing target?",
                             new object[] { item.DisplayName }));
       args.WaitForPostBack();
     }
   }
 }

And the 2 line changes to Commands.Config to point to our new commands


<!--<command name="item:publish" type="Sitecore.Shell.Framework.Commands.PublishItem,Sitecore.Kernel" />-->
 <command name="item:publish" type="Sitecore.Starterkit.Classes.CustomPublishItem,Sitecore.Starterkit" />
 <!--<command name="item:publishnow" type="Sitecore.Shell.Framework.Commands.PublishNow,Sitecore.Kernel" />-->
 <command name="item:publishnow" type="Sitecore.Starterkit.Classes.CustomPublishNow,Sitecore.Starterkit" />

And the result:
Warning Dialog for Schedule Only Items

Set Publishing Form

We also need a nice way of enabling this feature for items. To do this we will extend the set publishing dialogue form that is shown when the change publishing restrictions button is pressed.  The xml page that builds this form can be found in /sitecore/shell/Applications/Content Manager/Dialogs/Set Publishing/Set Publishing.xml. We also want to extend the CodeBeside class for this file to incorporate the logic for our new option.


<CodeBeside Type="Sitecore.Starterkit.Classes.CustomSetPublishingForm,Sitecore.Starterkit"/>
<!--<CodeBeside Type="Sitecore.Shell.Applications.ContentManager.Dialogs.SetPublishing.SetPublishingForm,Sitecore.Client"/>-->
...
<Border Padding="8px 8px 4px 8px">
  <Checkbox ID="SchedulePublish" Header="Publish to Schedule Only" />
</Border>

public class CustomSetPublishingForm : DialogForm
{
  ...
  protected override void OnOK(object sender, EventArgs args)
  {
    ...
    itemFromQueryString.Publishing.NeverPublish = !this.NeverPublish.Checked;
    if (itemFromQueryString.Fields["__Publish Only in Schedule"] != null)
        itemFromQueryString.Fields["__Publish Only in Schedule"].Value = this.SchedulePublish.Checked ? "1" : "0";
    ...
  }

  private void RenderItemTab(Item item)
  {
    Assert.ArgumentNotNull(item, "item");
    this.NeverPublish.Checked = !item.Publishing.NeverPublish;
    if (item.Fields["__Publish Only in Schedule"] == null)
    {
      SchedulePublish.Visible = false;
    }
    else
    {
      this.SchedulePublish.Checked = item.Fields["__Publish Only in Schedule"].Value == "1";
    }
    ...
  }

Result:
Publish to schedule settings

Publishing Warnings

So after doing all the hard work the only thing left is to display some nice info/warning messages in the content editor. The two places where publishing warnings occur are in the gutter and the warning panel and while the warnings themselves are very similar (if not identical) the area to customise these are in very different places.

Editor warnings

This is fairly simple to achieve by creating a class that is called in the getContentEditorWarnings pipeline. First of all make the simple class that will be called:

public class SchedulePublishEditorWarning
  {
  // Methods
  public void Process(GetContentEditorWarningsArgs args)
  {
    Item item = args.Item;
    if ((item != null) && item.Fields["__Publish Only in Schedule"] != null && item.Fields["__Publish Only in Schedule"].Value == "1")
    {
      args.Add(Translate.Text("This item can only be published by the publishing scheduler. It will be published during next scheduled publish."),
               Translate.Text("Only the scheduled publisher can publish this item"));
    }
  }
}

From here all we need to do is add this class into the pipeline


<getContentEditorWarnings>
  ...
  <processor type="Sitecore.Pipelines.GetContentEditorWarnings.NeverPublish, Sitecore.Kernel"/>
  <processor type="Sitecore.Pipelines.GetContentEditorWarnings.ItemPublishingRestricted, Sitecore.Kernel"/>
  <processor type="Sitecore.Pipelines.GetContentEditorWarnings.VersionPublishingRestricted, Sitecore.Kernel"/>
  <processor type="Sitecore.Starterkit.Classes.SchedulePublishEditorWarning, Sitecore.Starterkit" />
  ...
</getContentEditorWarnings>

This gives us a message in the content editor:

content editor warning for scheduled only publishing

Gutter Warnings

The last thing for us to do is to set the warning in the gutter when the editor has Publishing Warnings enabled in the gutter. To find this setting we need to switch to the core database on the desktop and open up the content editor. The relevant setting can be found at /sitecore/content/Applications/Content Editor/Gutters/Publishing Warnings. Here in the type field of the data section we can add in our own class for these warnings. So again using reflector we can copy the original class and add our own logic to it.

 private static string GetTooltip(Item item)
 {
   ...
   Item validVersion = item.Publishing.GetValidVersion(now, requireApproved);
   if ((item.Publishing.IsPublishable(now, false)
        && item.Publishing.IsValid(now, requireApproved))
        && ((validVersion != null) && (item.Version == validVersion.Version))
        && (item.Fields["__Publish Only in Schedule"] == null
            || string.IsNullOrEmpty(item.Fields["__Publish Only in Schedule"].Value)
            || item.Fields["__Publish Only in Schedule"].Value == "0"))
   {
     return null;
   }
   ...
   if (item.Fields["__Publish Only in Schedule"] != null && item.Fields["__Publish Only in Schedule"].Value == "1")
   {
     return "You cannot publish this item as it is restricted to scheduled publish only. It will be published by the publishing scheduler";
   }
   return "If you publish now, the latest version will not be visible on the web site as it is unpublishable.";
 }

Now all we need to do is change the application setting in the core database and we get the following:

gutter warning for scheduled publish

Hopefully that has covered all the bases for restricting publishing in such a way. While I agree that this is a very abstract requirement, most of these customisations will be handy in some other context along the way.

Posted in Sitecore | 1 Comment »