houston-roadmaps 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (110) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/Gemfile +24 -0
  4. data/Gemfile.lock +358 -0
  5. data/MIT-LICENSE +20 -0
  6. data/README.md +31 -0
  7. data/Rakefile +25 -0
  8. data/app/assets/images/houston/roadmaps/.gitkeep +0 -0
  9. data/app/assets/javascripts/houston/roadmaps/app.coffee +2 -0
  10. data/app/assets/javascripts/houston/roadmaps/application.js +14 -0
  11. data/app/assets/javascripts/houston/roadmaps/handlebars_helpers.coffee +22 -0
  12. data/app/assets/javascripts/houston/roadmaps/jquery_extensions.coffee +4 -0
  13. data/app/assets/javascripts/houston/roadmaps/models/goal.coffee +11 -0
  14. data/app/assets/javascripts/houston/roadmaps/models/milestone.coffee +124 -0
  15. data/app/assets/javascripts/houston/roadmaps/models/roadmap.coffee +16 -0
  16. data/app/assets/javascripts/houston/roadmaps/models/viewport.coffee +3 -0
  17. data/app/assets/javascripts/houston/roadmaps/resize_listener.js +67 -0
  18. data/app/assets/javascripts/houston/roadmaps/views/_gantt_chart.coffee +224 -0
  19. data/app/assets/javascripts/houston/roadmaps/views/_show_milestone_view.coffee +204 -0
  20. data/app/assets/javascripts/houston/roadmaps/views/edit_gantt_chart.coffee +405 -0
  21. data/app/assets/javascripts/houston/roadmaps/views/edit_milestone_view.coffee +97 -0
  22. data/app/assets/javascripts/houston/roadmaps/views/edit_roadmap_view.coffee +80 -0
  23. data/app/assets/javascripts/houston/roadmaps/views/gantt_thumbnail_view.coffee +173 -0
  24. data/app/assets/javascripts/houston/roadmaps/views/goal_view.coffee +18 -0
  25. data/app/assets/javascripts/houston/roadmaps/views/project_goals_view.coffee +30 -0
  26. data/app/assets/javascripts/houston/roadmaps/views/roadmap_history_view.coffee +67 -0
  27. data/app/assets/javascripts/houston/roadmaps/views/roadmap_view.coffee +12 -0
  28. data/app/assets/javascripts/houston/roadmaps/views/roadmaps_view.coffee +29 -0
  29. data/app/assets/stylesheets/houston/roadmaps/application.css +13 -0
  30. data/app/assets/stylesheets/houston/roadmaps/colors.scss.erb +41 -0
  31. data/app/assets/stylesheets/houston/roadmaps/goals.scss +20 -0
  32. data/app/assets/stylesheets/houston/roadmaps/milestone.scss +58 -0
  33. data/app/assets/stylesheets/houston/roadmaps/roadmap.scss +276 -0
  34. data/app/assets/stylesheets/houston/roadmaps/roadmap_history.scss +42 -0
  35. data/app/assets/stylesheets/houston/roadmaps/roadmaps.scss +89 -0
  36. data/app/assets/templates/houston/roadmaps/goals/edit.hbs +6 -0
  37. data/app/assets/templates/houston/roadmaps/goals/index.hbs +9 -0
  38. data/app/assets/templates/houston/roadmaps/goals/new.hbs +27 -0
  39. data/app/assets/templates/houston/roadmaps/goals/show.hbs +6 -0
  40. data/app/assets/templates/houston/roadmaps/milestone/edit.hbs +67 -0
  41. data/app/assets/templates/houston/roadmaps/milestone/show.hbs +53 -0
  42. data/app/assets/templates/houston/roadmaps/milestone/ticket.hbs +28 -0
  43. data/app/assets/templates/houston/roadmaps/roadmap/goal.hbs +6 -0
  44. data/app/assets/templates/houston/roadmaps/roadmap/history.hbs +22 -0
  45. data/app/assets/templates/houston/roadmaps/roadmap/show.hbs +12 -0
  46. data/app/assets/templates/houston/roadmaps/roadmaps/index.hbs +2 -0
  47. data/app/assets/templates/houston/roadmaps/roadmaps/show.hbs +11 -0
  48. data/app/controllers/houston/roadmaps/api/v1/roadmap_controller.rb +20 -0
  49. data/app/controllers/houston/roadmaps/dashboard_controller.rb +34 -0
  50. data/app/controllers/houston/roadmaps/milestones_controller.rb +118 -0
  51. data/app/controllers/houston/roadmaps/project_goals_controller.rb +24 -0
  52. data/app/controllers/houston/roadmaps/roadmap_milestones_controller.rb +36 -0
  53. data/app/controllers/houston/roadmaps/roadmaps_controller.rb +77 -0
  54. data/app/helpers/houston/roadmaps/application_helper.rb +4 -0
  55. data/app/models/roadmap.rb +9 -0
  56. data/app/models/roadmap_commit.rb +35 -0
  57. data/app/models/roadmap_milestone.rb +29 -0
  58. data/app/models/roadmap_milestone_version.rb +8 -0
  59. data/app/presenters/houston/roadmaps/milestone_api_presenter.rb +27 -0
  60. data/app/presenters/houston/roadmaps/milestone_presenter.rb +74 -0
  61. data/app/presenters/houston/roadmaps/roadmap_milestone_presenter.rb +70 -0
  62. data/app/presenters/houston/roadmaps/roadmap_presenter.rb +37 -0
  63. data/app/presenters/houston/roadmaps/ticket_presenter.rb +20 -0
  64. data/app/views/houston/roadmaps/dashboard/show.html.erb +42 -0
  65. data/app/views/houston/roadmaps/milestones/show.html.erb +40 -0
  66. data/app/views/houston/roadmaps/project_goals/index.html.erb +42 -0
  67. data/app/views/houston/roadmaps/roadmaps/_form.html.erb +29 -0
  68. data/app/views/houston/roadmaps/roadmaps/edit.html.erb +8 -0
  69. data/app/views/houston/roadmaps/roadmaps/history.html.erb +41 -0
  70. data/app/views/houston/roadmaps/roadmaps/index.html.erb +24 -0
  71. data/app/views/houston/roadmaps/roadmaps/new.html.erb +7 -0
  72. data/app/views/houston/roadmaps/roadmaps/show.html.erb +27 -0
  73. data/app/views/houston/roadmaps/roadmaps/show_editable.html.erb +35 -0
  74. data/app/views/layouts/houston/roadmaps/application.html.erb +10 -0
  75. data/app/views/layouts/houston/roadmaps/dashboard.html.erb +9 -0
  76. data/bin/rails +8 -0
  77. data/config/database.yml +13 -0
  78. data/config/initializers/add_navigation_renderer.rb +13 -0
  79. data/config/routes.rb +37 -0
  80. data/db/.keep +0 -0
  81. data/db/migrate/20140831210254_add_band_to_milestones.rb +5 -0
  82. data/db/migrate/20140907212311_add_end_date_to_milestones.rb +19 -0
  83. data/db/migrate/20140916230539_add_locked_to_milestone.rb +5 -0
  84. data/db/migrate/20140927154728_add_closed_ticket_count_to_milestones.rb +11 -0
  85. data/db/migrate/20140929024130_create_roadmap_commits.rb +29 -0
  86. data/db/migrate/20141012023628_add_user_id_to_milestone_versions.rb +11 -0
  87. data/db/migrate/20150102192805_add_lanes_to_milestones.rb +5 -0
  88. data/db/migrate/20150119155145_add_goal_and_feedback_query_to_milestones.rb +6 -0
  89. data/db/migrate/20150524203903_add_project_id_to_roadmap_commits.rb +17 -0
  90. data/db/migrate/20150603203744_add_timestamps_to_roadmap_commits.rb +20 -0
  91. data/db/migrate/20160206214746_rename_roadmap_feature_to_milestones.rb +25 -0
  92. data/db/migrate/20160207154530_create_roadmaps.rb +125 -0
  93. data/db/structure.sql +2557 -0
  94. data/houston-roadmaps.gemspec +29 -0
  95. data/lib/houston-roadmaps.rb +1 -0
  96. data/lib/houston/roadmaps.rb +15 -0
  97. data/lib/houston/roadmaps/configuration.rb +14 -0
  98. data/lib/houston/roadmaps/engine.rb +27 -0
  99. data/lib/houston/roadmaps/milestone_ext.rb +14 -0
  100. data/lib/houston/roadmaps/project_ext.rb +20 -0
  101. data/lib/houston/roadmaps/railtie.rb +17 -0
  102. data/lib/houston/roadmaps/version.rb +5 -0
  103. data/lib/tasks/roadmap_tasks.rake +4 -0
  104. data/test/acceptance/houston_dummy_test.rb +17 -0
  105. data/test/dummy/houston.rb +23 -0
  106. data/test/fixtures/projects.yml +3 -0
  107. data/test/fixtures/users.yml +10 -0
  108. data/test/test_helper.rb +43 -0
  109. data/test/unit/fixtures_test.rb +11 -0
  110. metadata +227 -0
data/Rakefile ADDED
@@ -0,0 +1,25 @@
1
+ begin
2
+ require "bundler/setup"
3
+ rescue LoadError
4
+ puts "You must `gem install bundler` and `bundle install` to run rake tasks"
5
+ end
6
+
7
+
8
+ # `rake test` and `rake test:all` are loaded from Houston
9
+ require_relative "test/dummy/houston"
10
+ Houston::Application.load_tasks
11
+
12
+
13
+ require "rdoc/task"
14
+
15
+ RDoc::Task.new(:rdoc) do |rdoc|
16
+ rdoc.rdoc_dir = "rdoc"
17
+ rdoc.title = "Roadmap"
18
+ rdoc.options << "--line-numbers"
19
+ rdoc.rdoc_files.include("README.rdoc")
20
+ rdoc.rdoc_files.include("lib/**/*.rb")
21
+ end
22
+
23
+
24
+ Bundler::GemHelper.install_tasks
25
+
File without changes
@@ -0,0 +1,2 @@
1
+ window.Roadmaps = {}
2
+ window.Neat.template = HandlebarsTemplates
@@ -0,0 +1,14 @@
1
+ // This is a manifest file that'll be compiled into application.js, which will include all the files
2
+ // listed below.
3
+ //
4
+ // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5
+ // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
6
+ //
7
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
+ // the compiled file.
9
+ //
10
+ // WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD
11
+ // GO AFTER THE REQUIRES BELOW.
12
+ //
13
+ //= require_tree ../../../templates/houston/roadmaps
14
+ //= require_tree .
@@ -0,0 +1,22 @@
1
+ Handlebars.registerPartial 'milestoneTicket', (task)->
2
+ HandlebarsTemplates['houston/roadmaps/milestone/ticket'](task)
3
+
4
+ Handlebars.registerHelper 'durationOfMilestone', (milestone)->
5
+ return "" unless milestone.endDate and milestone.startDate
6
+ duration = milestone.endDate - milestone.startDate
7
+ weeks = Math.round(duration / Duration.WEEK)
8
+ "#{weeks} weeks"
9
+
10
+ Handlebars.registerHelper 'milestonePercentComplete', (milestone)->
11
+ return "" if !milestone.percentComplete?
12
+ App.formatPercent milestone.percentComplete
13
+
14
+ Handlebars.registerHelper 'ticketPercentComplete', (ticket)->
15
+ return "" unless ticket.estimatedEffort? and ticket.estimatedEffortCompleted?
16
+ percent = ticket.estimatedEffortCompleted / ticket.estimatedEffort
17
+ App.formatPercent percent
18
+
19
+ Handlebars.registerHelper 'linkToFeedbackQuery', (projectSlug, feedbackQuery)->
20
+ q = encodeURIComponent(feedbackQuery)
21
+ path = "/feedback/by_project/#{projectSlug}?q=#{q}"
22
+ "<a href=\"#{path}\">See comments</a>"
@@ -0,0 +1,4 @@
1
+ $.fn.extend
2
+
3
+ pluck: (attr)->
4
+ _.map @, (el)-> $(el).attr('data-id')
@@ -0,0 +1,11 @@
1
+ class Roadmaps.Goal extends Backbone.Model
2
+ urlRoot: '/roadmap/milestones'
3
+
4
+
5
+
6
+ class Roadmaps.Goals extends Backbone.Collection
7
+ model: Roadmaps.Goal
8
+
9
+ outsideOf: (milestones) ->
10
+ ids = _(milestones.pluck('milestoneId'))
11
+ new Roadmaps.Goals @reject (goal) -> ids.include(goal.id)
@@ -0,0 +1,124 @@
1
+ class Roadmaps.Milestone extends Backbone.Model
2
+ urlRoot: '/roadmap/milestones'
3
+
4
+ initialize: ->
5
+ super
6
+ _.bindAll(@, 'clearChangesSinceSave', 'revert')
7
+ @clearChangesSinceSave()
8
+
9
+ save: (attrs, options)->
10
+ success = options?.success
11
+ options.success = (response)=>
12
+ @trigger('save:success', @)
13
+ success(@, response) if success
14
+ @clearChangesSinceSave()
15
+ @trigger('save', @)
16
+ super
17
+
18
+ duration: ->
19
+ Math.floor((@get('endDate') - @get('startDate')) / Duration.DAY).days()
20
+
21
+ clearChangesSinceSave: ->
22
+ @_originalAttributes = _.clone @attributes
23
+ @_originalAttributes.removed = false
24
+
25
+ revert: ->
26
+ @set(@_originalAttributes) if @_originalAttributes
27
+
28
+ markRemoved: ->
29
+ @set(removed: true)
30
+
31
+ changesSinceSave: ->
32
+ changes = {}
33
+ for attribute, value of @attributes
34
+ originalValue = @_originalAttributes[attribute]
35
+ changes[attribute] = [originalValue, value] unless _.isEqual(originalValue, value)
36
+ changes
37
+
38
+ bands: ->
39
+ band = @get('band')
40
+ bands = [band]
41
+ for i in [1...@get('lanes')]
42
+ bands.push band + i
43
+ bands
44
+
45
+
46
+
47
+ parse: (milestone)->
48
+ milestone.startDate = App.serverDateFormat.parse(milestone.startDate) if milestone.startDate and !_.isDate(milestone.startDate)
49
+ milestone.endDate = App.serverDateFormat.parse(milestone.endDate) if milestone.endDate and !_.isDate(milestone.endDate)
50
+ milestone
51
+
52
+ toJSON: (options)->
53
+ json = super(options)
54
+ json.cid = @cid
55
+ if 'emulateHTTP' of (options || {})
56
+ json.start_date = App.serverDateFormat(json.startDate) if json.startDate
57
+ delete json.startDate
58
+ json.end_date = App.serverDateFormat(json.endDate) if json.endDate
59
+ delete json.endDate
60
+ json
61
+
62
+
63
+
64
+ class Roadmaps.Milestones extends Backbone.Collection
65
+ model: Roadmaps.Milestone
66
+ comparator: 'startDate'
67
+
68
+ start: -> _.min @pluck('startDate')
69
+ end: -> _.max @pluck('endDate')
70
+
71
+ revert: ->
72
+ i = 0
73
+ while i < @length
74
+ milestone = @models[i]
75
+ if milestone.get('id')
76
+ milestone.revert()
77
+ i++
78
+ else
79
+ @remove(milestone)
80
+
81
+ changes: ->
82
+ for milestone in @models when !milestone.id or _.keys(changes = milestone.changesSinceSave()).length > 0
83
+ if milestone.id
84
+ change = id: milestone.id
85
+ for attribute, [originalView, newValue] of changes
86
+ [attribute, newValue] = ['start_date', App.serverDateFormat(newValue)] if attribute is 'startDate'
87
+ [attribute, newValue] = ['end_date', App.serverDateFormat(newValue)] if attribute is 'endDate'
88
+ change[attribute] = newValue
89
+ change
90
+ else
91
+ milestone.toJSON(emulateHTTP: true)
92
+
93
+ overlappingBands: (bands)->
94
+ new Milestones @select (m)-> _.intersection(m.bands(), bands).length > 0
95
+
96
+ before: (milestone)->
97
+ startDate = milestone.get('startDate')
98
+ new Milestones @select (m)-> m.get('endDate') < startDate
99
+
100
+ after: (milestone)->
101
+ endDate = milestone.get('endDate')
102
+ new Milestones @select (m)-> m.get('startDate') > endDate
103
+
104
+ lastMilestoneBefore: (milestone)->
105
+ @before(milestone).sortBy('endDate').last()
106
+
107
+ firstMilestoneAfter: (milestone)->
108
+ @after(milestone).sortBy('startDate').first()
109
+
110
+ downstreamOf: (date, bands)->
111
+ milestones = []
112
+ @each (milestone)->
113
+ return if milestone.get('startDate') < date
114
+ newBands = milestone.bands()
115
+ return if _.intersection(newBands, bands).length is 0
116
+
117
+ # If this milestone being dragged can push this milestone,
118
+ # then it can also push any milestones this one can push
119
+ bands = _.union(bands, newBands)
120
+ milestones.push milestone
121
+ milestones
122
+
123
+ unremoved: ->
124
+ new Roadmaps.Milestones @where(removed: false)
@@ -0,0 +1,16 @@
1
+ class @Roadmaps.Roadmap extends Backbone.Model
2
+ urlRoot: "/roadmaps"
3
+
4
+ milestones: ->
5
+ @_milestones ||= new Roadmaps.Milestones(@get("milestones"), parse: true)
6
+
7
+ viewport: ->
8
+ new Roadmaps.Viewport
9
+ start: @milestones().start()
10
+ end: @milestones().end()
11
+
12
+
13
+
14
+ class @Roadmaps.Roadmaps extends Backbone.Collection
15
+ model: window.Roadmaps.Roadmap
16
+
@@ -0,0 +1,3 @@
1
+ class Roadmaps.Viewport extends Backbone.Model
2
+
3
+ domain: -> [@get('start'), @get('end')]
@@ -0,0 +1,67 @@
1
+ // This is kind of a hack that allows you to execute code
2
+ // when an element is resized. See:
3
+ // http://www.backalleycoder.com/2013/03/18/cross-browser-event-based-element-resize-detection/
4
+ (function(){
5
+ var attachEvent = document.attachEvent;
6
+ var isIE = navigator.userAgent.match(/Trident/);
7
+ var requestFrame = (function(){
8
+ var raf = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame ||
9
+ function(fn){ return window.setTimeout(fn, 20); };
10
+ return function(fn){ return raf(fn); };
11
+ })();
12
+
13
+ var cancelFrame = (function(){
14
+ var cancel = window.cancelAnimationFrame || window.mozCancelAnimationFrame || window.webkitCancelAnimationFrame ||
15
+ window.clearTimeout;
16
+ return function(id){ return cancel(id); };
17
+ })();
18
+
19
+ function resizeListener(e){
20
+ var win = e.target || e.srcElement;
21
+ if (win.__resizeRAF__) cancelFrame(win.__resizeRAF__);
22
+ win.__resizeRAF__ = requestFrame(function(){
23
+ var trigger = win.__resizeTrigger__;
24
+ trigger.__resizeListeners__.forEach(function(fn){
25
+ fn.call(trigger, e);
26
+ });
27
+ });
28
+ }
29
+
30
+ function objectLoad(e){
31
+ this.contentDocument.defaultView.__resizeTrigger__ = this.__resizeElement__;
32
+ this.contentDocument.defaultView.addEventListener('resize', resizeListener);
33
+ }
34
+
35
+ window.addResizeListener = function(element, fn){
36
+ if (!element.__resizeListeners__) {
37
+ element.__resizeListeners__ = [];
38
+ if (attachEvent) {
39
+ element.__resizeTrigger__ = element;
40
+ element.attachEvent('onresize', resizeListener);
41
+ }
42
+ else {
43
+ if (getComputedStyle(element).position == 'static') element.style.position = 'relative';
44
+ var obj = element.__resizeTrigger__ = document.createElement('object');
45
+ obj.setAttribute('style', 'display: block; position: absolute; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden; pointer-events: none; z-index: -1;');
46
+ obj.__resizeElement__ = element;
47
+ obj.onload = objectLoad;
48
+ obj.type = 'text/html';
49
+ if (isIE) element.appendChild(obj);
50
+ obj.data = 'about:blank';
51
+ if (!isIE) element.appendChild(obj);
52
+ }
53
+ }
54
+ element.__resizeListeners__.push(fn);
55
+ };
56
+
57
+ window.removeResizeListener = function(element, fn){
58
+ element.__resizeListeners__.splice(element.__resizeListeners__.indexOf(fn), 1);
59
+ if (!element.__resizeListeners__.length) {
60
+ if (attachEvent) element.detachEvent('onresize', resizeListener);
61
+ else {
62
+ element.__resizeTrigger__.contentDocument.defaultView.removeEventListener('resize', resizeListener);
63
+ element.__resizeTrigger__ = !element.removeChild(element.__resizeTrigger__);
64
+ }
65
+ }
66
+ }
67
+ })();
@@ -0,0 +1,224 @@
1
+ class Roadmaps.GanttChart
2
+ today: new Date()
3
+
4
+ constructor: (@milestones, options={})->
5
+ @el = options.el
6
+ @selector = options.selector ? '#roadmap'
7
+ @showToday = options.showToday ? true
8
+ @showThumbnail = options.showThumbnail ? true
9
+ @showWeekends = options.showWeekends ? false
10
+ @linkMilestones = options.linkMilestones ? false
11
+ @showProgress = options.showProgress ? false
12
+ @bandHeight = options.bandHeight ? 30
13
+ @bandMargin = options.bandMargin ? 8
14
+ @viewport = options.viewport ? @defaultViewport()
15
+ @viewport.bind 'change', @updateViewport, @
16
+ @height = 24
17
+ @markers = options.markers ? []
18
+ for marker in @markers
19
+ marker.date = App.serverDateFormat.parse(marker.date).endOfDay()
20
+ @milestones.bind 'add', @update, @
21
+ @milestones.bind 'change', @update, @
22
+ @milestones.bind 'remove', @update, @
23
+ @milestones.bind 'reset', @update, @
24
+ $(window).resize (e)=>
25
+ @updateWindow() if e.target is window
26
+
27
+ defaultViewport: ->
28
+ new Roadmaps.Viewport
29
+ start: 3.weeks().before(@today)
30
+ end: 6.months().after(3.weeks().before(@today))
31
+
32
+ render: ->
33
+ @el = $(@selector)[0] unless @el
34
+ @$el = $(@el)
35
+
36
+ if @showThumbnail
37
+ @thumbnail = new Roadmaps.ThumbnailGanttChart
38
+ milestones: @milestones
39
+ markers: @markers
40
+ showToday: @showToday
41
+ viewport: @viewport
42
+ parent: d3.select(@el)
43
+ @thumbnail.render()
44
+
45
+ @roadmap = d3.select(@el)
46
+ .append('div')
47
+ .attr('class', 'roadmap-bands')
48
+
49
+ svg = @roadmap.append('svg')
50
+ .attr('height', @height)
51
+ .attr('class', 'roadmap-axis')
52
+ .append('g')
53
+ .attr('transform', "translate(0,0)")
54
+ @xAxis = svg.append('g').attr('class', 'x axis')
55
+
56
+ @updateWindow()
57
+
58
+ updateWindow: ->
59
+ width = @$el.width() || 960
60
+ return if @width is width
61
+ @width = width
62
+
63
+ @roadmap.select('svg').transition(150).attr('width', @width)
64
+
65
+ @x = d3.time.scale()
66
+ .domain(@viewport.domain())
67
+ .range([0, @width])
68
+ timeline = d3.svg.axis()
69
+ .scale(@x)
70
+ .orient('bottom')
71
+ @xAxis.transition(150).call(timeline)
72
+
73
+ @update(transition: true)
74
+
75
+ updateViewport: ->
76
+ @x = d3.time.scale()
77
+ .domain(@viewport.domain())
78
+ .range([0, @width])
79
+ timeline = d3.svg.axis()
80
+ .scale(@x)
81
+ .orient('bottom')
82
+ @xAxis.call(timeline)
83
+
84
+ @update(transition: false)
85
+
86
+ update: (options)->
87
+ transition = options?.transition ? true
88
+ view = @
89
+ @today = new Date()
90
+
91
+ bands = @roadmap.selectAll('.roadmap-band')
92
+ .data(@groupMilestonesIntoBands(), (band)-> band.key)
93
+
94
+ bands.enter()
95
+ .append('div')
96
+ .attr('class', (band)-> "roadmap-band")
97
+ .attr('style', (date)=> "height: #{@bandHeight}px; margin: #{@bandMargin}px 0;")
98
+ .attr('data-band', (band)-> band.number)
99
+ .each -> view.initializeBand.apply(view, [@])
100
+
101
+ bands.exit().remove()
102
+
103
+
104
+
105
+ if @showWeekends
106
+ weeks = @roadmap.selectAll('.roadmap-weekend')
107
+ .data(d3.time.saturdays(@x.domain()...), (date)-> date)
108
+
109
+ weeks.enter().append('div')
110
+ .attr('class', 'roadmap-weekend')
111
+ .attr('style', (date)=> "left: #{@x(date)}px; width: #{@x(2.days().after(date)) - @x(date)}px;")
112
+
113
+ update = if transition then weeks.transition(150) else weeks
114
+ update
115
+ .attr('style', (date)=> "left: #{@x(date)}px; width: #{@x(2.days().after(date)) - @x(date)}px;")
116
+
117
+ weeks.exit().remove()
118
+
119
+
120
+
121
+ milestones = bands.selectAll('.roadmap-milestone')
122
+ .data(((band)-> band.milestones), (milestone)-> milestone.cid)
123
+
124
+ newMilestones = if @linkMilestones
125
+ milestones.enter().append('a')
126
+ .attr('href', (milestone)-> "/roadmap/milestones/#{milestone.milestoneId}")
127
+ else
128
+ milestones.enter().append('div')
129
+
130
+ if @showProgress
131
+ newMilestones.append('div')
132
+ .attr('class', 'roadmap-milestone-progress')
133
+
134
+ newMilestones
135
+ .attr 'style', (milestone)=>
136
+ [ "left: #{@x(milestone.startDate)}px",
137
+ "width: #{@x(milestone.endDate) - @x(milestone.startDate)}px",
138
+ "height: #{milestone.lanes * (@bandHeight + @bandMargin) - @bandMargin}px" ].join('; ')
139
+ .attr('class', 'roadmap-milestone')
140
+ .attr('data-cid', (milestone)-> milestone.cid)
141
+ .attr('data-milestone-id', (milestone)-> milestone.milestoneId)
142
+ .each -> view.initializeMilestone.apply(view, [@])
143
+
144
+ # Put the milestone name into a span so that Midori can render it correctly
145
+ .append('span')
146
+ .attr('class', 'roadmap-milestone-name')
147
+ .text((milestone)-> milestone.name)
148
+
149
+ update = if transition then milestones.transition(150) else milestones
150
+ update
151
+ .attr 'class', (milestone)=>
152
+ classes = ['roadmap-milestone', milestone.projectColor]
153
+ classes.push(if milestone.locked then 'locked' else 'unlocked')
154
+ classes.push(if milestone.completed then 'completed' else 'uncompleted')
155
+ classes.push('clickable') if @linkMilestones
156
+ if milestone.startDate > @today
157
+ classes.push 'upcoming'
158
+ else if milestone.endDate < @today
159
+ classes.push 'past'
160
+ else
161
+ classes.push 'active'
162
+ classes.join(' ')
163
+ .attr 'style', (milestone)=>
164
+ [ "left: #{@x(milestone.startDate)}px",
165
+ "width: #{@x(milestone.endDate) - @x(milestone.startDate)}px",
166
+ "height: #{milestone.lanes * (@bandHeight + @bandMargin) - @bandMargin}px" ].join('; ')
167
+ .select('.roadmap-milestone-progress')
168
+ .attr 'style', (milestone)->
169
+ return "width: 0" if milestone.tickets is 0
170
+ "width: #{milestone.percentComplete * 100}%"
171
+
172
+ update.select('.roadmap-milestone-name')
173
+ .text((milestone)-> milestone.name)
174
+
175
+ milestones.exit().remove()
176
+
177
+
178
+
179
+ if @showToday
180
+ todayLine = @roadmap.selectAll('.roadmap-today')
181
+ .data([@today])
182
+
183
+ todayLine.enter()
184
+ .append('div')
185
+ .attr('class', 'roadmap-today')
186
+ .attr('style', (date)=> "left: #{@x(date)}px;")
187
+
188
+ update = if transition then todayLine.transition(150) else todayLine
189
+ update
190
+ .attr('style', (date)=> "left: #{@x(date)}px;")
191
+
192
+
193
+
194
+ markers = @roadmap.selectAll('.roadmap-marker')
195
+ .data(@markers)
196
+
197
+ markers.enter()
198
+ .append('div')
199
+ .attr('class', 'roadmap-marker')
200
+ .attr('style', ({date})=> "left: #{@x(date)}px;")
201
+
202
+ update = if transition then markers.transition(150) else markers
203
+ update
204
+ .attr('style', ({date})=> "left: #{@x(date)}px;")
205
+
206
+ markers.exit().remove()
207
+
208
+ groupMilestonesIntoBands: ->
209
+ milestoneBands = {}
210
+ milestones = (@toJSON(milestone) for milestone in @milestones.models)
211
+ for milestone in milestones when milestone.startDate and milestone.endDate and !milestone.removed
212
+ (milestoneBands[milestone.band] ||=
213
+ key: milestone.band
214
+ number: milestone.band
215
+ milestones: []).milestones.push(milestone)
216
+ _.values(milestoneBands)
217
+
218
+ toJSON: (milestone)->
219
+ json = milestone.toJSON()
220
+ json.cid = milestone.id # Use `id` because the view is readonly
221
+ json
222
+
223
+ initializeBand: ->
224
+ initializeMilestone: ->