TFS Timesheets

TFS Timesheets

В данном посте я хочу затронуть тему получения отчетов о затраченном времени из TFS.

При работе с TFS мы вручную увеличиваем значения поля Completed work на то количество часов, сколько было на него потрачено. Мы не указываем то, когда были потрачены эти N часов. Считается, что разработчики делают это по факту выполнения работы.

Что же делать, если мы хотим получить сводную таблицу с указанием сколько, куда и когда были потрачены эти часы? К сожалению, из коробки, такой возможности нет. Однако есть определенный набор API, который поможет нам написать утилиту, создающую желаемый отчет. И называется оно TFS Object Model. Именно этим мы и займемся. Напишем ее.

Итак, наше приложение будет консольным.

Пример его использования описан ниже:

TFS_Timesheet.exe /collection:http://server:8080/tfs/defaultcollection /from:2012-03-01 /to:2012-03-31

В качестве параметров передаются адрес TFS коллекции проектов и даты начала и конца для формирования отчета.

Создадим класс для хранения этих параметров:

public class RequestData
{
    public RequestData()
    {
        From = new DateTime(1900, 1, 1);
        To = new DateTime(2500, 1, 1);
    }

    public Uri Collection { get; set; }
    public DateTime From { get; set; }
    public DateTime To { get; set; }
}

При старте приложения инициализируем наш класс:

public static void Main(string[] args)
{
    var request = new RequestData();

    foreach (var arg in args)
    {
        if (arg.StartsWith("/collection:"))
        {
            request.Collection = new Uri(arg.Substring("/collection:".Length));
        }
        else if (arg.StartsWith("/from:"))
        {
            request.From = DateTime.Parse(arg.Substring("/from:".Length) + "T00:00:00.0000000");
        }
        else if (arg.StartsWith("/to:"))
        {
            request.To = DateTime.Parse(arg.Substring("/to:".Length) + "T00:00:00.0000000");
        }
    }
}

Чтение данных из TFS

Добавим еще один класс, который будет содержать в себе данные о выполнении некоторой работы.

public class TimeEntry
{
    public string Name { get; set; }
    public DateTime Date { get; set; }
    public int Id { get; set; }
    public string Title { get; set; }
    public double Hours { get; set; }
    public string Comments { get; set; }
}

Теперь нашей задачей является получение коллекции объектов этого класса. Т.е. получение списка проделанной работы за указанный период времени.

var timeEntries = new List<TimeEntry>();
var teamProjectCollection = new TfsTeamProjectCollection(request.Collection, true);
var workItemStore = teamProjectCollection.GetService<WorkItemStore>();
var workItems = workItemStore.Query(string.Format(
    @"SELECT [System.Id], [System.ChangedDate], [System.Title], [System.State]
      FROM WorkItems
      WHERE [System.ChangedDate] >= '{0}' AND [System.ChangedDate] < '{1}'
      ORDER BY [System.ChangedDate]", request.From.ToLongDateString(), request.To.AddDays(1).ToLongDateString()));

foreach (WorkItem workItem in workItems)
{
    if (workItem == null || !workItem.IsValid()) continue;

    foreach (Revision rev in workItem.Revisions)
    {
        foreach (Field field in rev.Fields)
        {
            if (field.Name == "Completed Work" &&
                field.OriginalValue != field.Value &&
                Convert.ToDateTime(rev.Fields[CoreField.ChangedDate].Value) >= request.From &&
                Convert.ToDateTime(rev.Fields[CoreField.ChangedDate].Value) < request.To.AddDays(1))
            {
                var entry = new TimeEntry();
                entry.Id = workItem.Id;
                entry.Date = Convert.ToDateTime(rev.Fields[CoreField.ChangedDate].Value);
                entry.Name = rev.Fields[CoreField.ChangedBy].Value.ToString();
                entry.Title = workItem.Title;
                entry.Hours = Convert.ToDouble(field.Value) - Convert.ToDouble(field.OriginalValue);
                if (entry.Hours > 0)
                {
                    timeEntries.Add(entry);
                }
            }
        }
    }
}

Формирование отчета

Завершающим этапом служит построение отчета на основании собранных данных.

private static void WriteTimesheet(List<TimeEntry> timeEntries)
{
    var workItems = timeEntries.Select(t => t.Id).Distinct().OrderBy(t => t).ToList();
    var dates = timeEntries.Select(t => t.Date.Date).Distinct().OrderBy(t => t).ToList();

    using(var timesheet = File.CreateText("timesheet_report.html"))
    {
        timesheet.Write("<html><head><title>TFS Timesheet Report</title><style>\n"+Properties.Resources.Css+"\n</style>");
        timesheet.Write("<meta content=\"text/html; charset=utf-8\" http-equiv=\"content-type\" />");
        timesheet.Write("<head><body><table><thead><tr><th class='id'>ID</th><th class='title'>Title</th><th>&nbsp;</th>");

        foreach (var date in dates)
        {
            timesheet.Write("<th>" + date.ToShortDateString() + "</th>");
        }

        timesheet.WriteLine("</tr></thead><tbody>");

        // Общеевремя.
        timesheet.Write("<tr><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td>");

        foreach (var date in dates)
        {
            var entries = timeEntries.Where(t => t.Date.Date == date).ToList();
            var hours = entries.Aggregate(0.0, (t, i) => t + i.Hours);

            timesheet.Write("<td class='total'>" + hours + "</td>");
        }

        timesheet.Write("</tr>");

        // Распределение времени по задачам.
        foreach (var wid in workItems)
        {
            timesheet.Write("<tr>");
            timesheet.Write("<td class='id'>" + wid + "</td>");

            var title = timeEntries.First(t => t.Id == wid).Title;
            timesheet.Write("<td class='title'>" + title + "</td>");

            var entries = timeEntries.Where(t => t.Id == wid).ToList();
            var hours = entries.Aggregate(0.0, (t, i) => t + i.Hours);
            timesheet.Write("<td class='total'>" + hours + "</td>");

            foreach (var date in dates)
            {
                entries = timeEntries.Where(t => t.Id == wid && t.Date.Date == date).ToList();
                hours = entries.Aggregate(0.0, (t, i) => t + i.Hours);

                timesheet.Write("<td>");
                if (hours > 0)
                {
                    timesheet.Write(hours);
                }
                else
                {
                    timesheet.Write("&nbsp;");
                }

                timesheet.Write("</td>");
            }

            timesheet.WriteLine("</tr>");
        }

        timesheet.WriteLine("</tbody></table></body></html>");
    }
}

Скачать все вместе можно здесь.