PageRenderTime 558ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/app/assets/javascripts/models/project.js

https://bitbucket.org/sqctest01/fulcrum
JavaScript | 342 lines | 213 code | 67 blank | 62 comment | 23 complexity | 576dee65667dd15734cd963d9dd02106 MD5 | raw file
  1. if (typeof Fulcrum == 'undefined') {
  2. Fulcrum = {};
  3. }
  4. Fulcrum.Project = Backbone.Model.extend({
  5. name: 'project',
  6. initialize: function(args) {
  7. this.maybeUnwrap(args);
  8. this.bind('change:last_changeset_id', this.updateChangesets);
  9. this.stories = new Fulcrum.StoryCollection();
  10. this.stories.url = this.url() + '/stories';
  11. this.stories.project = this;
  12. this.users = new Fulcrum.UserCollection();
  13. this.users.url = this.url() + '/users';
  14. this.users.project = this;
  15. this.iterations = [];
  16. },
  17. defaults: {
  18. default_velocity: 10
  19. },
  20. url: function() {
  21. return '/projects/' + this.id;
  22. },
  23. // The ids of the columns, in the order that they appear by story weight
  24. columnIds: ['#done', '#in_progress', '#backlog', '#chilly_bin'],
  25. // Return an array of the columns that appear after column, or an empty
  26. // array if the column is the last
  27. columnsAfter: function(column) {
  28. var index = _.indexOf(this.columnIds, column);
  29. if (index === -1) {
  30. // column was not found in the array
  31. throw column.toString() + ' is not a valid column';
  32. }
  33. return this.columnIds.slice(index + 1);
  34. },
  35. // Return an array of the columns that appear before column, or an empty
  36. // array if the column is the first
  37. columnsBefore: function(column) {
  38. var index = _.indexOf(this.columnIds, column);
  39. if (index === -1) {
  40. // column was not found in the array
  41. throw column.toString() + ' is not a valid column';
  42. }
  43. return this.columnIds.slice(0, index);
  44. },
  45. // This method is triggered when the last_changeset_id attribute is changed,
  46. // which indicates there are changed or new stories on the server which need
  47. // to be loaded.
  48. updateChangesets: function() {
  49. var from = this.previous('last_changeset_id');
  50. if (from === null) {
  51. from = 0;
  52. }
  53. var to = this.get('last_changeset_id');
  54. var model = this;
  55. var options = {
  56. type: 'GET',
  57. dataType: 'json',
  58. success: function(resp, status, xhr) {
  59. model.handleChangesets(resp);
  60. },
  61. data: {from: from, to: to},
  62. url: this.url() + '/changesets'
  63. };
  64. $.ajax(options);
  65. },
  66. // (Re)load each of the stories described in the provided changesets.
  67. handleChangesets: function(changesets) {
  68. var that = this;
  69. var story_ids = _.map(changesets, function(changeset) {
  70. return changeset.changeset.story_id;
  71. });
  72. story_ids = _.uniq(story_ids);
  73. _.each(story_ids, function(story_id) {
  74. // FIXME - Feature envy on stories collection
  75. var story = that.stories.get(story_id);
  76. if (story) {
  77. // This is an existing story on the collection, just reload it
  78. story.fetch();
  79. } else {
  80. // This is a new story, which is present on the server but we don't
  81. // have it locally yet.
  82. that.stories.add({id: story_id});
  83. story = that.stories.get(story_id);
  84. story.fetch();
  85. }
  86. });
  87. },
  88. milliseconds_in_a_day: 1000 * 60 * 60 * 24,
  89. // Return the correct iteration number for a given date.
  90. getIterationNumberForDate: function(compare_date) {
  91. //var start_date = new Date(this.get('start_date'));
  92. var start_date = this.startDate();
  93. var difference = Math.abs(compare_date.getTime() - start_date.getTime());
  94. var days_apart = Math.round(difference / this.milliseconds_in_a_day);
  95. return Math.floor((days_apart / (this.get('iteration_length') * 7)) + 1);
  96. },
  97. getDateForIterationNumber: function(iteration_number) {
  98. // The difference betweeen the start date in days. Iteration length is
  99. // in weeks.
  100. var difference = (7 * this.get('iteration_length')) * (iteration_number - 1);
  101. var start_date = this.startDate();
  102. var iteration_date = new Date(start_date);
  103. iteration_date.setDate(start_date.getDate() + difference);
  104. return iteration_date;
  105. },
  106. currentIterationNumber: function() {
  107. return this.getIterationNumberForDate(new Date());
  108. },
  109. startDate: function() {
  110. var start_date;
  111. if (this.get('start_date')) {
  112. // Parse the date string into an array of [YYYY, MM, DD] to
  113. // ensure identical date behaviour across browsers.
  114. var dateArray = this.get('start_date').split('/');
  115. var year = parseInt(dateArray[0], 10);
  116. // Month is zero indexed
  117. var month = parseInt(dateArray[1], 10) - 1;
  118. var day = parseInt(dateArray[2], 10);
  119. start_date = new Date(year, month, day);
  120. } else {
  121. start_date = new Date();
  122. }
  123. // Is the specified project start date the same week day as the iteration
  124. // start day?
  125. if (start_date.getDay() === this.get('iteration_start_day')) {
  126. return start_date;
  127. } else {
  128. // Calculate the date of the nearest prior iteration start day to the
  129. // specified project start date. So if the iteration start day is
  130. // set to Monday, but the project start date is set to a specific
  131. // Thursday, return the Monday before the Thursday. A greater
  132. // mathemtician than I could probably do this with the modulo.
  133. var day_difference = start_date.getDay() - this.get('iteration_start_day');
  134. // The iteration start day is after the project start date, in terms of
  135. // day number
  136. if (day_difference < 0) {
  137. day_difference = day_difference + 7;
  138. }
  139. return new Date(start_date - day_difference * this.milliseconds_in_a_day);
  140. }
  141. },
  142. // Override the calculated velocity with a user defined value. If this
  143. // value is different to the calculated velocity, the velocityIsFake
  144. // attribute will be set to true.
  145. velocity: function(userVelocity) {
  146. if(userVelocity !== undefined) {
  147. if(userVelocity < 1) {
  148. userVelocity = 1;
  149. }
  150. if(userVelocity === this.calculateVelocity()) {
  151. this.unset('userVelocity');
  152. } else {
  153. this.set({userVelocity: userVelocity});
  154. }
  155. }
  156. if(this.get('userVelocity')) {
  157. return this.get('userVelocity');
  158. } else {
  159. return this.calculateVelocity();
  160. }
  161. },
  162. velocityIsFake: function() {
  163. return (this.get('userVelocity') !== undefined);
  164. },
  165. calculateVelocity: function() {
  166. if (this.doneIterations().length === 0) {
  167. return this.get('default_velocity');
  168. } else {
  169. // TODO Make number of iterations configurable
  170. var numIterations = 3;
  171. var iterations = this.doneIterations();
  172. // Take a maximum of numIterations from the end of the array
  173. if (iterations.length > numIterations) {
  174. iterations = iterations.slice(iterations.length - numIterations);
  175. }
  176. var pointsArray = _.invoke(iterations, 'points');
  177. var sum = _.reduce(pointsArray, function(memo, points) {
  178. return memo + points;
  179. }, 0);
  180. var velocity = Math.floor(sum / pointsArray.length);
  181. return velocity < 1 ? 1 : velocity;
  182. }
  183. },
  184. revertVelocity: function() {
  185. this.unset('userVelocity');
  186. },
  187. doneIterations: function() {
  188. return _.select(this.iterations, function(iteration) {
  189. return iteration.get('column') === "#done";
  190. });
  191. },
  192. rebuildIterations: function() {
  193. //
  194. // Done column
  195. //
  196. var that = this;
  197. // Clear the project iterations
  198. this.iterations = [];
  199. // Reset all story column values. Required as the story.column values
  200. // may have been changed from their default values by a prior run of
  201. // this method.
  202. this.stories.invoke('setColumn');
  203. var doneIterations = _.groupBy(this.stories.column('#done'),
  204. function(story) {
  205. return story.iterationNumber();
  206. });
  207. // groupBy() returns an object with keys of the iteration number
  208. // and values of the stories array. Ensure the keys are sorted
  209. // in numeric order.
  210. var doneNumbers = _.keys(doneIterations).sort(function(left, right) {
  211. return (left - right);
  212. });
  213. _.each(doneNumbers, function(iterationNumber) {
  214. var stories = doneIterations[iterationNumber];
  215. var iteration = new Fulcrum.Iteration({
  216. 'number': iterationNumber, 'stories': stories, column: '#done'
  217. });
  218. that.appendIteration(iteration, '#done');
  219. });
  220. var currentIteration = new Fulcrum.Iteration({
  221. 'number': this.currentIterationNumber(),
  222. 'stories': this.stories.column('#in_progress'),
  223. 'maximum_points': this.velocity(), 'column': '#in_progress'
  224. });
  225. this.appendIteration(currentIteration, '#done');
  226. //
  227. // Backlog column
  228. //
  229. var backlogIteration = new Fulcrum.Iteration({
  230. 'number': currentIteration.get('number') + 1,
  231. 'column': '#backlog', 'maximum_points': this.velocity()
  232. });
  233. this.appendIteration(backlogIteration, '#backlog');
  234. _.each(this.stories.column('#backlog'), function(story) {
  235. // The in progress iteration usually needs to be filled with the first
  236. // few stories from the backlog, unless the points total of the stories
  237. // in progress already equal or exceed the project velocity
  238. if (currentIteration.canTakeStory(story)) {
  239. // On initialisation, a stories column is determined based on the
  240. // story state. For unstarted stories this defaults to #backlog.
  241. // Stories matched here need this value overridden to #in_progress
  242. story.column = '#in_progress';
  243. currentIteration.get('stories').push(story);
  244. return;
  245. }
  246. if (!backlogIteration.canTakeStory(story)) {
  247. // Iterations sometimes 'overflow', i.e. an iteration may contain a
  248. // 5 point story but the project velocity is 1. In this case, the
  249. // next iteration that can have a story added is the current + 4.
  250. var nextNumber = backlogIteration.get('number') + 1 + Math.ceil(backlogIteration.overflowsBy() / that.velocity());
  251. backlogIteration = new Fulcrum.Iteration({
  252. 'number': nextNumber, 'column': '#backlog',
  253. 'maximum_points': that.velocity()
  254. });
  255. that.appendIteration(backlogIteration, '#backlog');
  256. }
  257. backlogIteration.get('stories').push(story);
  258. });
  259. _.each(this.iterations, function(iteration) {
  260. iteration.project = that;
  261. });
  262. this.trigger('rebuilt-iterations');
  263. },
  264. // Adds an iteration to the project. Creates empty iterations to fill any
  265. // gaps between the iteration number and the last iteration number added.
  266. appendIteration: function(iteration, columnForMissingIterations) {
  267. var lastIteration = _.last(this.iterations);
  268. // If there is a gap between the last iteration and this one, fill
  269. // the gap with empty iterations
  270. this.iterations = this.iterations.concat(
  271. Fulcrum.Iteration.createMissingIterations(columnForMissingIterations, lastIteration, iteration)
  272. );
  273. this.iterations.push(iteration);
  274. }
  275. });