Brian McKeiver's Blog

A Helpful Kentico Scheduled Task for Team Development


Introduction

One thing that I have learned over the years is that large web site implementation projects require good team development methodologies. Object versioning in Kentico allows for groups of developers to make sure they don't step on each other's toes. It's actually one of the Kentico best practices for team development that I have spoken on before. There is, however, one little problem that checked-out objects such as page templates or css stylesheets can cause when it comes to deployment time and it has to do with content staging. In this post I am going to show you how to avoid this pitfall and hopefully save your development team some heartache. 

 

Background

The Kentico A TeamAh yes, content staging, if you are like me than you most likely have a love hate relationship with the staging module. Most of the time the tool works as advertised. You simply select a set of tasks and they are deployed from your test site to your live site and your production website is updated in the matter of a few seconds. But there are occurrences where it does not work. In some situations the fix is simple, like upping a setting in the web.config file. In other situations it is not as clear. Sometimes you will see a synchronization failed error message, but in rare cases you will see no error message, but your production page template is not updated. This last case is what I want to talk about today.

If you are a Kentico developer and have ever run into the situation where your staging task successfully finished from the source server, but the target server does not have your changes upon further inspection, there could be a simple reason why. It could be the fact that the page template, transformation, or other object is checked-out on the target server. That's right, if you push a task through content staging to a target server where that very same object is checked out the staging UI will report that the task successfully went through, but in fact it actually didn't.

This has actually happened on a few team development environments that we manage in joint efforts with some of our clients. To combat this issue I have created a custom scheduled task that iterates through all of the objects that support check-in / check-out. The task will send an email to the developer who has them checked out, if that object has been checked out for more than a few hours. You can use this task on your environments where you are staging changes and should have objects permanently checked out. The code is below for the custom scheduled task. It is written against the Kentico 8.2 API, but it should work in 8.0, 8.1 and future versions as well.
 

 

The Solution

using System;
using System.Collections.Generic;
using System.Linq;

using CMS;
using CMS.DataEngine;
using CMS.EmailEngine;
using CMS.EventLog;
using CMS.Helpers;
using CMS.Membership;
using CMS.Scheduler;
using CMS.SiteProvider;
using CMS.Synchronization;


/// 
/// Custom Scheduled Task for Kentico 8 to remind developers if they leave an object checked out
/// 
[assembly: RegisterCustomClass("CheckOutObjectsTask", typeof(CheckOutObjectsTask))]
public class CheckOutObjectsTask : ITask
{
    //Default amount of hours to allow an Object to be checked out, can be overridden by TaskData param of scheduled task
    private const int _defaultLockWindowTime = 4;

    public CheckOutObjectsTask() { }

    /// 
    /// Return a list of all of the checked out objects in the system that support 
    /// 
    /// ObjectQuery
    private ObjectQuery FindAllLockedObjects()
    {
        var lockableTypes = ObjectTypeManager.AllObjectTypes.Where(t => ModuleManager.GetReadOnlyObject(t).TypeInfo.SupportsLocking);

        var lockableTypesFormatted = lockableTypes.Select(t => string.Format("N'{0}'", t)).Join(",");
            
        var objects = ObjectSettingsInfoProvider.GetObjectSettings()
            .Where(string.Format("ObjectCheckedOutByUserID > 0 AND ObjectSettingsObjectType IN ({0})", lockableTypesFormatted));

        return objects;
    }

    /// 
    /// Sends Email via Email Queue
    /// 
    /// plain text email body
    /// email address to send to
    private void SendNotification(string MessageBody, string EmailTo)
    {
        EmailMessage em = new EmailMessage();
        em.From = "no-reply@mcbeev.com";
        em.Recipients = EmailTo;
        em.Subject = string.Format("Objects checked out on {0}", SiteContext.CurrentSiteName);
        em.Body = MessageBody;

        EmailSender.SendEmail(em);
    }

    /// 
    /// Executes the task.
    /// 
    /// TaskInfo object representing the scheduled task
    public string Execute(TaskInfo ti)
    {
        //Do nothing if check-in / check-out is not turned on for objects
        if (!SynchronizationHelper.UseCheckinCheckout)
        {
            return null;
        }

        //determine lock window
        int lockWindowTimeInHours;
        lockWindowTimeInHours = ValidationHelper.GetInteger(ti.TaskData, _defaultLockWindowTime);

        //Iterate through all objects in the system that support locking and are checked out
        foreach (var os in FindAllLockedObjects())
        {
            //Now see if any checkouts are older than the allowed lock window
            TimeSpan diff = DateTime.Now - os.ObjectCheckedOutWhen;
            if (diff.Hours > lockWindowTimeInHours)
            {
                //Get the lazy developer's username and email
                var lazyDev = UserInfoProvider.GetUserInfo(os.ObjectCheckedOutByUserID);

                //Gets the Info object of the correct type by its actual Provider
                var oso = BaseAbstractInfoProvider.GetInfosByIds(os.ObjectSettingsObjectType, new List<int> {os.ObjectSettingsObjectID});
                
                string message = string.Format("Object: {0} checked out for more than {1} hours",
                    oso[os.ObjectSettingsObjectID], 
                    lockWindowTimeInHours);

                string log = string.Format("{0} sending email to: {1} <{2}>",
                    message,
                    lazyDev.UserName,
                    lazyDev.Email);
      
                EventLogProvider.LogInformation("CheckOutObjectsTask", "LOCKFOUND", log);

                SendNotification(message, lazyDev.Email);
            }
        }
    
        return null;
    }
}



Once you have the code in a class file you can create a quick custom scheduled task in the interface. It's pretty simple. You can simply create a new scheduled task by clicking new task in the scheduled tasks application. Then you just have to configure the scheduled task's property to use your custom class that you registered. I figured that running the task once a day was good enough, but you can run it more than that if you would like.



Kentico Custom Scheduled Task

 

The Result

Once everything is setup you should start receiving notification emails if a developer forgets to check in an object in your Kentico installation. One other thing to mention too, this is not something I would recommend running on a development or test environment, it's more geared for a staging or production environment that you don't want things to remain checked out on.


Kentico Email Message

I thought this was pretty cool and useful for our team at BizStream. As always if you find this useful or have a different approach in your team, feel free to let me know via comments on this blog post. Plus I got to work in a picture of the A Team in this blog post. That just makes me smile. Best. Show. Ever. Thanks!