TFS 2013 leaderboard to provide friendly competition amongst co-workers

Dashboard.cs 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Diagnostics;
  4. using System.IO;
  5. using System.Linq;
  6. using Microsoft.TeamFoundation.VersionControl.Client;
  7. using Microsoft.TeamFoundation.WorkItemTracking.Client;
  8. namespace pulse
  9. {
  10. /// <summary>
  11. /// Dashboard.
  12. /// </summary>
  13. public class Dashboard
  14. {
  15. /// <summary>
  16. /// Gets or sets the date refresh.
  17. /// </summary>
  18. /// <value>
  19. /// The date refresh.
  20. /// </value>
  21. public DateTime? DateRefresh { get; set; }
  22. /// <summary>
  23. /// Gets or sets the selected item.
  24. /// </summary>
  25. /// <value>
  26. /// The selected item.
  27. /// </value>
  28. public int SelectedItem { get; set; }
  29. /// <summary>
  30. /// Gets or sets the server.
  31. /// </summary>
  32. /// <value>
  33. /// The server.
  34. /// </value>
  35. public Uri Server { get; set; }
  36. /// <summary>
  37. /// Gets the cache file.
  38. /// </summary>
  39. /// <value>
  40. /// The cache file.
  41. /// </value>
  42. private string CacheFile
  43. {
  44. get
  45. {
  46. if (this.Server == null)
  47. {
  48. return "$cache";
  49. }
  50. return string.Format(
  51. "{0}_{1}_{2}.cache",
  52. this.Server.Scheme,
  53. this.Server.Host,
  54. this.Server.Port);
  55. }
  56. }
  57. /// <summary>
  58. /// Gets or sets the commits.
  59. /// </summary>
  60. /// <value>
  61. /// The commits.
  62. /// </value>
  63. private int Commits { get; set; }
  64. /// <summary>
  65. /// Gets the height.
  66. /// </summary>
  67. /// <value>
  68. /// The height.
  69. /// </value>
  70. private int Height
  71. {
  72. get
  73. {
  74. return Console.WindowHeight - 2;
  75. }
  76. }
  77. /// <summary>
  78. /// Gets or sets the highlighted scores.
  79. /// </summary>
  80. /// <value>
  81. /// The highlighted scores.
  82. /// </value>
  83. private IList<string> HighlightedScores { get; set; }
  84. /// <summary>
  85. /// Gets or sets the points.
  86. /// </summary>
  87. /// <value>
  88. /// The points.
  89. /// </value>
  90. private IDictionary<string, double> Points { get; set; }
  91. /// <summary>
  92. /// Gets or sets the projects.
  93. /// </summary>
  94. /// <value>
  95. /// The projects.
  96. /// </value>
  97. private int Projects { get; set; }
  98. /// <summary>
  99. /// Gets or sets the scores.
  100. /// </summary>
  101. /// <value>
  102. /// The scores.
  103. /// </value>
  104. private IDictionary<string, double> Scores { get; set; }
  105. /// <summary>
  106. /// Gets the width.
  107. /// </summary>
  108. /// <value>
  109. /// The width.
  110. /// </value>
  111. private int Width
  112. {
  113. get
  114. {
  115. return Console.WindowWidth;
  116. }
  117. }
  118. /// <summary>
  119. /// Gets or sets the work items.
  120. /// </summary>
  121. /// <value>
  122. /// The work items.
  123. /// </value>
  124. private int WorkItems { get; set; }
  125. /// <summary>
  126. /// Initializes a new instance of the <see cref="Dashboard" /> class.
  127. /// </summary>
  128. /// <param name="server">The server.</param>
  129. public Dashboard(Uri server)
  130. {
  131. this.Commits = 0;
  132. this.Projects = 0;
  133. this.WorkItems = 0;
  134. this.DateRefresh = null;
  135. this.Server = server;
  136. this.SelectedItem = 1;
  137. this.Scores = new Dictionary<string, double>();
  138. this.HighlightedScores = new List<string>();
  139. this.Points = new Dictionary<string, double>()
  140. {
  141. { Penalties.LackingVerbosity, -0.2 },
  142. { Penalties.LackOfPurpose, -0.2 },
  143. { Rewards.BugReport, 0.7 },
  144. { Rewards.Collaboration, 0.7 },
  145. { Rewards.CreatedWork, 0.02 },
  146. { Rewards.DueDiligence, 0.4 },
  147. { Rewards.FinishWork, 0.5 },
  148. { Rewards.HouseCleaning, 0.1 },
  149. { Rewards.Participation, 0.3 },
  150. { Rewards.TakingOwnership, 0.1 },
  151. { Rewards.Verbosity, 0.8 }
  152. };
  153. var cacheFile = new FileInfo(this.CacheFile);
  154. if (cacheFile.Exists)
  155. {
  156. using (var reader = new StreamReader(cacheFile.Open(FileMode.Open, FileAccess.Read, FileShare.Read)))
  157. {
  158. while (!reader.EndOfStream)
  159. {
  160. var line = reader.ReadLine();
  161. var fields = line.Split(new char[] { ';' });
  162. switch (fields[0].ToLower())
  163. {
  164. case "refreshed":
  165. if (fields.Count() > 1 && !string.IsNullOrWhiteSpace(fields[1]))
  166. {
  167. this.DateRefresh = Convert.ToDateTime(fields[1]);
  168. }
  169. break;
  170. case "projects":
  171. this.Projects = Convert.ToInt32(fields[1]);
  172. break;
  173. case "work":
  174. this.WorkItems = Convert.ToInt32(fields[1]);
  175. break;
  176. case "commits":
  177. this.Commits = Convert.ToInt32(fields[1]);
  178. break;
  179. case "score":
  180. var points = Convert.ToDouble(fields[2]);
  181. if (!this.Scores.ContainsKey(fields[1]))
  182. {
  183. this.Scores.Add(fields[1], points);
  184. }
  185. else
  186. {
  187. this.Scores[fields[1]] = points;
  188. }
  189. break;
  190. case "point":
  191. var pointValue = Convert.ToDouble(fields[2]);
  192. if (!this.Points.ContainsKey(fields[1]))
  193. {
  194. this.Points.Add(fields[1], pointValue);
  195. }
  196. else
  197. {
  198. this.Points[fields[1]] = pointValue;
  199. }
  200. break;
  201. }
  202. }
  203. }
  204. }
  205. }
  206. /// <summary>
  207. /// Adds the commits.
  208. /// </summary>
  209. /// <param name="commits">The commits.</param>
  210. public void AddCommits(IEnumerable<Changeset> commits)
  211. {
  212. foreach (var commit in commits)
  213. {
  214. // Add to the overall tally.
  215. this.Commits++;
  216. var personOfInterest = commit.CommitterDisplayName;
  217. this.AddHighlightedScore(personOfInterest);
  218. if (!this.Scores.Any(x => x.Key.Equals(personOfInterest, StringComparison.InvariantCultureIgnoreCase)))
  219. {
  220. this.Scores.Add(personOfInterest, 0);
  221. }
  222. // Get a point for participation.
  223. this.Scores[personOfInterest] += this.GetPoints(Rewards.Participation);
  224. // Handle points for commits with comments.
  225. if (string.IsNullOrWhiteSpace(commit.Comment))
  226. {
  227. this.Scores[personOfInterest] += this.GetPoints(Penalties.LackingVerbosity);
  228. }
  229. else
  230. {
  231. this.Scores[personOfInterest] += this.GetPoints(Rewards.Verbosity);
  232. }
  233. // Handle points with associating commits with actual work.
  234. if (commit.AssociatedWorkItems.Count() == 0)
  235. {
  236. this.Scores[personOfInterest] += this.GetPoints(Penalties.LackOfPurpose);
  237. }
  238. else
  239. {
  240. this.Scores[personOfInterest] += this.GetPoints(Rewards.Collaboration);
  241. }
  242. }
  243. }
  244. /// <summary>
  245. /// Adds the projects.
  246. /// </summary>
  247. /// <param name="projects">The projects.</param>
  248. public void AddProjects(IEnumerable<object> projects)
  249. {
  250. this.Projects = projects.Count();
  251. }
  252. /// <summary>
  253. /// Adds the work items.
  254. /// </summary>
  255. /// <param name="workItems">The work items.</param>
  256. public void AddWorkItems(IEnumerable<WorkItem> workItems)
  257. {
  258. foreach (var item in workItems)
  259. {
  260. // Add to the overall tally.
  261. this.WorkItems++;
  262. if (!string.IsNullOrWhiteSpace(item.CreatedBy))
  263. {
  264. if (!this.Scores.Any(x => x.Key.Equals(item.CreatedBy, StringComparison.InvariantCultureIgnoreCase)))
  265. {
  266. this.Scores.Add(item.CreatedBy, 0);
  267. }
  268. }
  269. if (item.CreatedBy != item.ChangedBy)
  270. {
  271. if (!string.IsNullOrWhiteSpace(item.ChangedBy))
  272. {
  273. if (!this.Scores.Any(x => x.Key.Equals(item.ChangedBy, StringComparison.InvariantCultureIgnoreCase)))
  274. {
  275. this.Scores.Add(item.ChangedBy, 0);
  276. }
  277. // Add points for collaboration.
  278. this.Scores[item.ChangedBy] += this.GetPoints(Rewards.Collaboration);
  279. }
  280. }
  281. else
  282. {
  283. if (!string.IsNullOrWhiteSpace(item.CreatedBy))
  284. {
  285. // They must have updated something right?
  286. this.Scores[item.CreatedBy] += this.GetPoints(Rewards.Participation);
  287. }
  288. }
  289. var personOfInterest = item.CreatedDate < item.ChangedDate
  290. ? item.ChangedBy
  291. : item.CreatedBy;
  292. this.AddHighlightedScore(personOfInterest);
  293. if (!string.IsNullOrWhiteSpace(personOfInterest))
  294. {
  295. switch (item.Type.Name.ToLower())
  296. {
  297. case "product backlog item":
  298. this.Scores[personOfInterest] += this.GetPoints(Rewards.Participation);
  299. break;
  300. case "user story":
  301. this.Scores[personOfInterest] += this.GetPoints(Rewards.Participation);
  302. break;
  303. case "feature":
  304. this.Scores[personOfInterest] += this.GetPoints(Rewards.Participation);
  305. break;
  306. case "task":
  307. this.Scores[personOfInterest] += this.GetPoints(Rewards.CreatedWork);
  308. break;
  309. case "impediment":
  310. case "issue":
  311. case "bug":
  312. this.Scores[personOfInterest] += this.GetPoints(Rewards.BugReport);
  313. break;
  314. case "test case":
  315. case "shared steps":
  316. this.Scores[personOfInterest] += this.GetPoints(Rewards.CreatedWork);
  317. break;
  318. default:
  319. Debug.WriteLine("Did not handle work item type [" + item.Type.Name + "]");
  320. break;
  321. }
  322. switch (item.State.ToLower())
  323. {
  324. case "new":
  325. case "design":
  326. case "to do":
  327. case "open":
  328. case "approved":
  329. case "active":
  330. case "in progress":
  331. this.Scores[personOfInterest] += this.GetPoints(Rewards.TakingOwnership);
  332. break;
  333. case "done":
  334. case "committed":
  335. case "resolved":
  336. case "closed":
  337. this.Scores[personOfInterest] += this.GetPoints(Rewards.FinishWork);
  338. break;
  339. case "removed":
  340. this.Scores[personOfInterest] += this.GetPoints(Rewards.HouseCleaning);
  341. break;
  342. default:
  343. Debug.WriteLine("Did not handle work item state [" + item.State + "]");
  344. break;
  345. }
  346. }
  347. }
  348. }
  349. /// <summary>
  350. /// Caches this instance.
  351. /// </summary>
  352. public void Cache()
  353. {
  354. var cacheFile = new FileInfo(this.CacheFile);
  355. using (var writer = new StreamWriter(cacheFile.Open(FileMode.OpenOrCreate, FileAccess.Write, FileShare.None)))
  356. {
  357. writer.WriteLine(string.Format("{0};{1}", "refreshed", this.DateRefresh.HasValue ? this.DateRefresh.Value.ToString() : string.Empty));
  358. writer.WriteLine(string.Format("{0};{1}", "projects", this.Projects));
  359. writer.WriteLine(string.Format("{0};{1}", "work", this.WorkItems));
  360. writer.WriteLine(string.Format("{0};{1}", "commits", this.Commits));
  361. foreach (var score in this.Scores)
  362. {
  363. writer.WriteLine(string.Format("{0};{1};{2}", "score", score.Key, score.Value));
  364. }
  365. foreach (var point in this.Points)
  366. {
  367. writer.WriteLine(string.Format("{0};{1};{2}", "point", point.Key, point.Value));
  368. }
  369. }
  370. }
  371. /// <summary>
  372. /// Clears the highlights.
  373. /// </summary>
  374. public void ClearHighlights()
  375. {
  376. this.HighlightedScores.Clear();
  377. }
  378. /// <summary>
  379. /// Updates the specified refreshing.
  380. /// </summary>
  381. public void Update()
  382. {
  383. // Header
  384. var buffer = new List<string>();
  385. buffer.AddRange(this.Statistics());
  386. // Divider
  387. buffer.Add(Padding(this.Width, "-"));
  388. // Leaderboard
  389. var leaderBoard = this.Leadboard();
  390. var pageSize = this.Height - buffer.Count();
  391. // Reset the selected item if it exceeds the leader board limitations.
  392. if (this.SelectedItem > leaderBoard.Count())
  393. {
  394. this.SelectedItem = leaderBoard.Count();
  395. }
  396. // Figure out what to put in the page.
  397. if (this.SelectedItem > pageSize)
  398. {
  399. buffer.AddRange(leaderBoard.Skip(this.SelectedItem % pageSize).Take(pageSize));
  400. }
  401. else
  402. {
  403. buffer.AddRange(leaderBoard.Take(pageSize));
  404. }
  405. // Filler
  406. var heightRemaining = this.Height - buffer.Count();
  407. for (var i = 0; i < heightRemaining; i++)
  408. {
  409. buffer.Add(string.Empty);
  410. }
  411. Console.CursorVisible = false;
  412. Console.SetCursorPosition(0, 0);
  413. foreach (var line in buffer)
  414. {
  415. foreach (var highlight in this.HighlightedScores)
  416. {
  417. if (line.Contains(highlight))
  418. {
  419. Console.ForegroundColor = ConsoleColor.Green;
  420. break;
  421. }
  422. }
  423. if (line.StartsWith(string.Format(" {0,3:###}.", this.SelectedItem)))
  424. {
  425. Console.BackgroundColor = ConsoleColor.White;
  426. if (Console.ForegroundColor != ConsoleColor.Green)
  427. {
  428. Console.ForegroundColor = ConsoleColor.Black;
  429. }
  430. }
  431. if (line.Length >= this.Width)
  432. {
  433. Console.Write(line);
  434. }
  435. else
  436. {
  437. Console.WriteLine(line);
  438. }
  439. Console.ResetColor();
  440. }
  441. // Notify with a sound
  442. if (this.HighlightedScores.Count() > 0)
  443. {
  444. Sound.Queue.Enqueue(Sound.Notify);
  445. }
  446. }
  447. /// <summary>
  448. /// Adds the highlighted score.
  449. /// </summary>
  450. /// <param name="person">The person.</param>
  451. private void AddHighlightedScore(string person)
  452. {
  453. if (this.Scores.ContainsKey(person) && !this.HighlightedScores.Contains(person))
  454. {
  455. this.HighlightedScores.Add(person);
  456. }
  457. }
  458. /// <summary>
  459. /// Gets the points.
  460. /// </summary>
  461. /// <param name="action">The action.</param>
  462. /// <returns>Returns the point value for a specific action.</returns>
  463. private double GetPoints(string action)
  464. {
  465. if (this.Points.ContainsKey(action))
  466. {
  467. return this.Points[action];
  468. }
  469. return 0.0;
  470. }
  471. /// <summary>
  472. /// Statisticses this instance.
  473. /// </summary>
  474. /// <returns></returns>
  475. private IEnumerable<string> Statistics()
  476. {
  477. var lines = new List<string>();
  478. var title = "pulse";
  479. var titlePadding = Padding((this.Width - title.Length) / 2);
  480. lines.Add(string.Format("{0}{1}{0}", titlePadding, title));
  481. var stats = string.Format(
  482. "p: {0} | w: {1} | c: {2} | r: {3:MM/dd/yyyy HH:mm}",
  483. this.Projects,
  484. this.WorkItems,
  485. this.Commits,
  486. this.DateRefresh.HasValue ? this.DateRefresh.Value : DateTime.Now);
  487. var statsPadding = Padding((this.Width - stats.Length) / 2);
  488. lines.Add(string.Format("{0}{1}{0}", statsPadding, stats));
  489. return lines;
  490. }
  491. /// <summary>
  492. /// Leadboards this instance.
  493. /// </summary>
  494. /// <returns></returns>
  495. private IEnumerable<string> Leadboard()
  496. {
  497. if (this.Scores == null || this.Scores.Count() == 0)
  498. {
  499. return new List<string>();
  500. }
  501. var rank = 0;
  502. return this.Scores
  503. .OrderByDescending(x => x.Value)
  504. .Select(x =>
  505. {
  506. var player = x.Key;
  507. var points = (int)x.Value;
  508. if (points < 0)
  509. {
  510. points = 0;
  511. }
  512. var basic = string.Format(" {0,3:###}. {1}{{0}}{2}", ++rank, player, points);
  513. return basic.Replace("{0}", Padding(this.Width - basic.Length + 2, " "));
  514. })
  515. .ToList();
  516. }
  517. /// <summary>
  518. /// Paddings the specified length.
  519. /// </summary>
  520. /// <param name="length">The length.</param>
  521. /// <param name="text">The text.</param>
  522. /// <returns></returns>
  523. private static string Padding(int length, string text = " ")
  524. {
  525. var padding = string.Empty;
  526. for (var i = 0; i < length; i++)
  527. {
  528. padding += text;
  529. }
  530. return padding;
  531. }
  532. }
  533. }