PageRenderTime 84ms CodeModel.GetById 26ms RepoModel.GetById 1ms app.codeStats 0ms

/app/assets/javascripts/models/story.js

https://bitbucket.org/sqctest01/fulcrum
JavaScript | 273 lines | 216 code | 45 blank | 12 comment | 30 complexity | 6511799d8e44fc95659ef699c58868e9 MD5 | raw file
  1. if (typeof Fulcrum == 'undefined') {
  2. Fulcrum = {};
  3. }
  4. Fulcrum.Story = Backbone.Model.extend({
  5. name: 'story',
  6. timestampFormat: 'd mmm yyyy, h:MMtt',
  7. initialize: function(args) {
  8. this.bind('change:state', this.changeState);
  9. this.bind('change:notes', this.populateNotes);
  10. // FIXME Call super()?
  11. this.maybeUnwrap(args);
  12. this.initNotes();
  13. this.setColumn();
  14. },
  15. changeState: function(model, new_value) {
  16. if (new_value == "started") {
  17. model.set({owned_by_id: model.collection.project.current_user.id}, true);
  18. }
  19. model.setAcceptedAt();
  20. model.setColumn();
  21. },
  22. moveBetween: function(before, after) {
  23. var beforeStory = this.collection.get(before);
  24. var afterStory = this.collection.get(after);
  25. var difference = (afterStory.position() - beforeStory.position()) / 2;
  26. var newPosition = difference + beforeStory.position();
  27. this.set({position: newPosition});
  28. this.collection.sort();
  29. return this;
  30. },
  31. moveAfter: function(beforeId) {
  32. var before = this.collection.get(beforeId);
  33. var after = this.collection.next(before);
  34. if (typeof after == 'undefined') {
  35. afterPosition = before.position() + 2;
  36. } else {
  37. afterPosition = after.position();
  38. }
  39. var difference = (afterPosition - before.position()) / 2;
  40. var newPosition = difference + before.position();
  41. this.set({position: newPosition});
  42. return this;
  43. },
  44. moveBefore: function(afterId) {
  45. var after = this.collection.get(afterId);
  46. var before = this.collection.previous(after);
  47. if (typeof before == 'undefined') {
  48. beforePosition = 0.0;
  49. } else {
  50. beforePosition = before.position();
  51. }
  52. var difference = (after.position() - beforePosition) / 2;
  53. var newPosition = difference + beforePosition;
  54. this.set({position: newPosition});
  55. this.collection.sort({silent: true});
  56. return this;
  57. },
  58. defaults: {
  59. events: [],
  60. state: "unscheduled",
  61. story_type: "feature"
  62. },
  63. setColumn: function() {
  64. var column = '#in_progress';
  65. switch(this.get('state')) {
  66. case 'unscheduled':
  67. column = '#chilly_bin';
  68. break;
  69. case 'unstarted':
  70. column = '#backlog';
  71. break;
  72. case 'accepted':
  73. // Accepted stories remain in the in progress column if they were
  74. // completed within the current iteration.
  75. if (this.collection.project.currentIterationNumber() === this.iterationNumber()) {
  76. column = '#in_progress';
  77. } else {
  78. column = '#done';
  79. }
  80. break;
  81. }
  82. this.column = column;
  83. },
  84. clear: function() {
  85. if (!this.isNew()) {
  86. this.destroy();
  87. }
  88. this.view.remove();
  89. },
  90. estimable: function() {
  91. if (this.get('story_type') === 'feature') {
  92. return !this.estimated();
  93. } else {
  94. return false;
  95. }
  96. },
  97. estimated: function() {
  98. var estimate = this.get('estimate');
  99. return !(estimate === undefined || estimate === null);
  100. },
  101. point_values: function() {
  102. return this.collection.project.get('point_values');
  103. },
  104. // List available state transitions for this story
  105. events: function() {
  106. switch (this.get('state')) {
  107. case 'started':
  108. return ["finish"];
  109. break;
  110. case 'finished':
  111. return ["deliver"];
  112. break;
  113. case 'delivered':
  114. return ["accept", "reject"];
  115. break;
  116. case 'rejected':
  117. return ["restart"];
  118. break;
  119. case 'accepted':
  120. return [];
  121. break;
  122. default:
  123. return ["start"];
  124. }
  125. },
  126. // State machine transitions
  127. start: function() {
  128. this.set({state: "started"});
  129. },
  130. finish: function() {
  131. this.set({state: "finished"});
  132. },
  133. deliver: function() {
  134. this.set({state: "delivered"});
  135. },
  136. accept: function() {
  137. this.set({state: "accepted"});
  138. },
  139. reject: function() {
  140. this.set({state: "rejected"});
  141. },
  142. restart: function() {
  143. this.set({state: "started"});
  144. },
  145. position: function() {
  146. return parseFloat(this.get('position'));
  147. },
  148. className: function() {
  149. var className = 'story ' + this.get('story_type');
  150. if (this.estimable() && !this.estimated()) {
  151. className += ' unestimated';
  152. }
  153. return className;
  154. },
  155. hasErrors: function() {
  156. return (typeof this.get('errors') != "undefined");
  157. },
  158. errorsOn: function(field) {
  159. if (!this.hasErrors()) {
  160. return false;
  161. }
  162. return (typeof this.get('errors')[field] != "undefined");
  163. },
  164. errorMessages: function() {
  165. return _.map(this.get('errors'), function(errors, field) {
  166. return _.map(errors, function(error) {
  167. return field + " " + error;
  168. }).join(', ');
  169. }).join(', ');
  170. },
  171. // Returns the user that owns this Story, or undefined if no user owns
  172. // the Story
  173. owned_by: function() {
  174. return this.collection.project.users.get(this.get('owned_by_id'));
  175. },
  176. requested_by: function() {
  177. return this.collection.project.users.get(this.get('requested_by_id'));
  178. },
  179. created_at: function() {
  180. var d = new Date(this.get('created_at'));
  181. return d.format(this.timestampFormat);
  182. },
  183. hasDetails: function() {
  184. return (typeof this.get('description') == "string" || this.hasNotes());
  185. },
  186. iterationNumber: function() {
  187. if (this.get('state') === "accepted") {
  188. return this.collection.project.getIterationNumberForDate(new Date(this.get("accepted_at")));
  189. }
  190. },
  191. // If the story state is 'accepted', and the 'accepted_at' attribute is not
  192. // set, set it to today's date.
  193. setAcceptedAt: function() {
  194. if (this.get('state') === "accepted" && !this.get('accepted_at')) {
  195. var today = new Date();
  196. today.setHours(0);
  197. today.setMinutes(0);
  198. today.setSeconds(0);
  199. today.setMilliseconds(0);
  200. this.set({accepted_at: today});
  201. }
  202. },
  203. labels: function() {
  204. if (typeof this.get('labels') != 'string') {
  205. return [];
  206. }
  207. return _.map(this.get('labels').split(','), function(label) {
  208. return $.trim(label);
  209. });
  210. },
  211. // Initialize the notes collection on this story, and populate if necessary
  212. initNotes: function() {
  213. this.notes = new Fulcrum.NoteCollection();
  214. this.notes.story = this;
  215. this.populateNotes();
  216. },
  217. // Resets this stories notes collection
  218. populateNotes: function() {
  219. var notes = this.get("notes") || [];
  220. this.notes.reset(notes);
  221. },
  222. // Returns true if any of the story has any saved notes.
  223. hasNotes: function() {
  224. return this.notes.any(function(note) {
  225. return !note.isNew();
  226. });
  227. }
  228. });