Looking into Trac
Trac
I know it's not .Net, but that's not always a bad thing...
Well I tend to keep my eye out for things which take my fancy, or more importantly, free things that take my fancy...Trac is one of those things, a project management and bug database web app written in Python, written by edgewall software... it's actually pretty cool... for RoR enthusiasts they're probably aware of it's existence already (considering RoR use this for manging their bugs and patches)
First off, you can run it on windows... though a *nix is generally better, and it's not too hard to get going - took me about half an hour... second thing, it's a lot better once you install a few hacks especially:
Now, at this point what makes it good?
- SVN Integration
- Wiki Integration
- Extensibility and community support
secondary to that is the very good subversion integration, a must considering that's the only source control system I use.
Creating tickets via XML-RPC in C#
Now the last hack I suggested exposes plenty of functionality in the system via XML RPC... at which point we can start logging bugs remotely, from C#... to do this we'll need a copy of the xmlrpc.net libraryI'm not conducting a tutorial here ;o) but if Trac takes your fancy you might find this code handy... first off this is our interface for logging tickets in Trac - incidentally this is only a small subset of the funcionality exposed.
[XmlRpcUrl("http://localhost/trac/login/xmlrpc")]
public interface ITicket : IXmlRpcProxy
{
[XmlRpcMethod("ticket.query")]
int[] Query(string qstr);[XmlRpcMethod("ticket.getRecentChanges")]
int[] GetRecentChanges(DateTime since);[XmlRpcMethod("ticket.getAvailableActions")]
string[] GetAvailableActions(int id);[XmlRpcMethod("ticket.getTicketFields")]
TicketField[] GetTicketFields();[XmlRpcMethod("ticket.create")]
int Create(string summary, string description, XmlRpcStruct attributes);[XmlRpcMethod("ticket.get")]
object[] GetTicket(int id);[XmlRpcMethod("ticket.delete")]
void Delete(int id);[XmlRpcMethod("ticket.update")]
object[] Update(int id, string comment, XmlRpcStruct attributes);[XmlRpcMethod("ticket.type.getAll")]
string[] GetAllTypes();[XmlRpcMethod("ticket.resolution.getAll")]
string[] GetAllResolutions();[XmlRpcMethod("ticket.priority.getAll")]
string[] GetAllPriorities();[XmlRpcMethod("ticket.component.getAll")]
string[] GetAllComponents();[XmlRpcMethod("ticket.version.getAll")]
string[] GetAllVersions();[XmlRpcMethod("ticket.severity.getAll")]
string[] GetAllSeverities();[XmlRpcMethod("ticket.milestone.getAll")]
string[] GetAllMilestones();
}
Now, tickets have a bunch of attributes associated with them...
public static class TicketAttributes
{
public const string Cc = "cc";
public const string Keywords = "keywords";
public const string Status = "status";
public const string Type = "type";
public const string Owner = "owner";
public const string Version = "version";
public const string Resolution = "resolution";
public const string Reporter = "reporter";
public const string Milestone = "milestone";
public const string Component = "component";
public const string Summary = "summary";
public const string Description = "description";
public const string Priority = "priority";
}
And we can make ticket information easier to get a hold of with a simple class:
///
/// represents the information for a ticket
///
public class TicketInfo
{
private int _ticketId;
private DateTime _created;
private DateTime _lastModified;
private XmlRpcStruct _attributes;public TicketInfo()
{
}public TicketInfo(object[] values)
{
Update(values);
}internal void Update(object[] values)
{
if (values == null) throw new ArgumentNullException("values");
if (values.Length != 4) throw new ArgumentException("values should have 4 elements");_ticketId = (int)values[0];
_created = DateHelper.ParseUnixTimestamp((int)values[1]);
_lastModified = DateHelper.ParseUnixTimestamp((int)values[2]);
_attributes = (XmlRpcStruct)values[3];
}///
/// The identifier for this ticket
///
public int TicketId
{
get { return _ticketId; }
set { _ticketId = value; }
}///
/// date and time the ticket was created
///
public DateTime Created
{
get { return _created; }
set { _created = value; }
}///
/// date and time the ticket was last modified
///
public DateTime LastModified
{
get { return _lastModified; }
set { _lastModified = value; }
}///
/// The attributes for this ticket, this will include any additional fields
/// that aren't defined explicitly as members of this class.
///
public XmlRpcStruct Attributes
{
get
{
if (_attributes == null) _attributes = new XmlRpcStruct();
return _attributes;
}
set { _attributes = value; }
}public string Cc
{
get { return GetAttribute(TicketAttributes.Cc); }
set { SetAttribute(TicketAttributes.Cc, value); }
}public string Keywords
{
get { return GetAttribute(TicketAttributes.Keywords); }
set { SetAttribute(TicketAttributes.Keywords, value); }
}public string Status
{
get { return GetAttribute(TicketAttributes.Status); }
set { SetAttribute(TicketAttributes.Status, value); }
}public string Type
{
get { return GetAttribute(TicketAttributes.Type); }
set { SetAttribute(TicketAttributes.Type, value); }
}public string Owner
{
get { return GetAttribute(TicketAttributes.Owner); }
set { SetAttribute(TicketAttributes.Owner, value); }
}public string Version
{
get { return GetAttribute(TicketAttributes.Version); }
set { SetAttribute(TicketAttributes.Version, value); }
}public string Resolution
{
get { return GetAttribute(TicketAttributes.Resolution); }
set { SetAttribute(TicketAttributes.Resolution, value); }
}public string Reporter
{
get { return GetAttribute(TicketAttributes.Reporter); }
set { SetAttribute(TicketAttributes.Reporter, value); }
}public string Milestone
{
get { return GetAttribute(TicketAttributes.Milestone); }
set { SetAttribute(TicketAttributes.Milestone, value); }
}public string Component
{
get { return GetAttribute(TicketAttributes.Component); }
set { SetAttribute(TicketAttributes.Component, value); }
}public string Summary
{
get { return GetAttribute(TicketAttributes.Summary); }
set { SetAttribute(TicketAttributes.Summary, value); }
}public string Description
{
get { return GetAttribute(TicketAttributes.Description); }
set { SetAttribute(TicketAttributes.Description, value); }
}public string Priority
{
get { return GetAttribute(TicketAttributes.Priority); }
set { SetAttribute(TicketAttributes.Priority, value); }
}#region Support methods
private string GetAttribute(string name)
{
if (Attributes.Contains(name))
{
return Convert.ToString(Attributes[name]);
}
return null;
}private void SetAttribute(string name, string value)
{
if (Attributes.Contains(name))
{
Attributes[name] = value;
}
else
{
Attributes.Add(name, value);
}
}#endregion
}
And last of all we have a class for managing tickets.. by the way this isn't complete, I haven't finished writing it because this is just one of my back burner projects...
public class TicketManager
{
private ITicket _ticket;public void Connect(string url, string userName, string password)
{
_ticket = XmlRpcProxyGen.Create();
_ticket.Url = url;
_ticket.PreAuthenticate = true;
_ticket.Credentials = new NetworkCredential(userName, password);
}public string[] GetAvailableActions(int id)
{
return _ticket.GetAvailableActions(id);
}public string[] GetAvailableActions(TicketInfo ticket)
{
ValidateTicket(ticket);
return _ticket.GetAvailableActions(ticket.TicketId);
}public int[] GetRecentChanges(DateTime since)
{
return _ticket.GetRecentChanges(since);
}public void DeleteTicket(int ticketId)
{
_ticket.Delete(ticketId);
}public void DeleteTicket(TicketInfo ticket)
{
ValidateTicket(ticket);
DeleteTicket(ticket.TicketId);
}public void UpdateTicket(TicketInfo ticket, string comment)
{
ValidateTicket(ticket);
object[] values = _ticket.Update(ticket.TicketId, comment, ticket.Attributes);
ticket.Update(values);
}public void CreateTicket(TicketInfo ticket)
{
if (string.IsNullOrEmpty(ticket.Summary)) throw new ArgumentNullException("ticket.Summary");
if (string.IsNullOrEmpty(ticket.Description)) throw new ArgumentNullException("ticket.Description");
if (string.IsNullOrEmpty(ticket.Type)) throw new ArgumentNullException("ticket.Type");
if (string.IsNullOrEmpty(ticket.Priority)) throw new ArgumentNullException("ticket.Priority");
if (string.IsNullOrEmpty(ticket.Component)) throw new ArgumentNullException("ticket.Component");XmlRpcStruct tempAttributes = new XmlRpcStruct();
foreach (object key in ticket.Attributes.Keys)
{
if ((((string)key) != TicketAttributes.Description) && (((string)key) != TicketAttributes.Summary))
{
tempAttributes.Add(key, ticket.Attributes[key]);
}
}int id = _ticket.Create(ticket.Summary, ticket.Description, ticket.Attributes);
ticket.TicketId = id;
}private void ValidateTicket(TicketInfo ticket)
{
if (ticket == null) throw new ArgumentNullException("ticket");
if (ticket.TicketId <= 0)="" throw="" new="" argumentexception("ticketid="" must="" be="" greater="" then="">=>
}
}
About the only trick here is that some of the date values get returned as unix time stamps so you need to convert them...
public static DateTime ParseUnixTimestamp(double timestamp)
{
return new DateTime(1970, 1, 1, 0, 0, 0, 0).AddSeconds(timestamp);
}
Because the Syzmk Rich Media Processor product I work on is a clearing house for simple messages among other things... It becomes a small jump from here (about 30 lines of C# code, 5 for a quick and dirty "IronPython" script) to start submitting tasks and bugs via email or mobile... who knows, it could be handy if you get a call while your out of the office and need to log a bug or task.