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
@@ -0,0 +1,118 @@
1
+ module Houston
2
+ module Roadmaps
3
+ class MilestonesController < ApplicationController
4
+ attr_reader :milestone
5
+
6
+ layout "houston/roadmaps/application"
7
+
8
+ before_filter :find_milestone, only: [:show, :update, :close, :add_ticket, :create_ticket, :remove_ticket, :update_order]
9
+
10
+
11
+ def show
12
+ authorize! :read, milestone
13
+ @project = milestone.project
14
+ @title = "#{milestone.name} • #{@project.name}"
15
+ @tickets = milestone.tickets.includes(:project, :tasks, :reporter)
16
+ @open_tickets = @project.tickets.open.includes(:tasks, :reporter)
17
+ end
18
+
19
+
20
+ def create
21
+ project = Project.find(params[:projectId])
22
+ authorize! :create, project.milestones.build
23
+ milestone = project.create_milestone!(milestone_attributes)
24
+ if milestone.persisted?
25
+ render json: Houston::Roadmaps::MilestonePresenter.new(milestone), status: :created
26
+ else
27
+ render json: milestone.errors, status: :unprocessable_entity
28
+ end
29
+ end
30
+
31
+
32
+ def update
33
+ authorize! :update, milestone
34
+ milestone.updated_by = current_user if milestone.respond_to?(:updated_by=)
35
+ if milestone.update_attributes(milestone_attributes)
36
+ render json: Houston::Roadmaps::MilestonePresenter.new(milestone)
37
+ else
38
+ render json: milestone.errors, status: :unprocessable_entity
39
+ end
40
+ end
41
+
42
+
43
+ def close
44
+ authorize! :update, milestone
45
+ milestone.close!
46
+ render json: {}
47
+ end
48
+
49
+
50
+ def add_ticket
51
+ authorize! :update_tickets, milestone
52
+ ticket = Ticket.find params[:ticket_id]
53
+ ticket.update_attribute :milestone_id, milestone.id
54
+ head :ok
55
+ end
56
+
57
+
58
+ def create_ticket
59
+ authorize! :update_tickets, milestone
60
+ authorize! :create, milestone.tickets.build
61
+
62
+ ticket = project.create_ticket!(
63
+ milestone_id: milestone.id,
64
+ type: "Feature",
65
+ summary: params[:summary],
66
+ reporter: current_user)
67
+
68
+ if ticket.persisted?
69
+ render json: TicketPresenter.new(ticket), status: :created, location: ticket.ticket_tracker_ticket_url
70
+ else
71
+ render json: ticket.errors, status: :unprocessable_entity
72
+ end
73
+ end
74
+
75
+
76
+ def remove_ticket
77
+ authorize! :update_tickets, milestone
78
+ ticket = Ticket.find params[:ticket_id]
79
+ ticket.update_attribute :milestone_id, nil
80
+ head :ok
81
+ end
82
+
83
+
84
+ def update_order
85
+ authorize! :update_tickets, milestone
86
+
87
+ ids = Array.wrap(params[:order]).map(&:to_i).reject(&:zero?)
88
+
89
+ ::Ticket.transaction do
90
+ milestone.tickets.where(::Ticket.arel_table[:id].not_in(ids))
91
+ .update_all("extended_attributes = extended_attributes || 'milestoneSequence=>NULL'::hstore")
92
+
93
+ ids.each_with_index do |id, i|
94
+ ::Ticket.unscoped.where(id: id).update_all("extended_attributes = extended_attributes || 'milestoneSequence=>#{i+1}'::hstore")
95
+ end
96
+ end
97
+
98
+ head :ok
99
+ end
100
+
101
+
102
+ private
103
+
104
+ def find_milestone
105
+ @milestone = Milestone.unscoped.find(params[:id])
106
+ end
107
+
108
+ def project
109
+ milestone.project
110
+ end
111
+
112
+ def milestone_attributes
113
+ params.fetch(:milestone).pick(:name, :band, :lanes, :start_date, :end_date, :locked, :goal, :feedback_query)
114
+ end
115
+
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,24 @@
1
+ module Houston
2
+ module Roadmaps
3
+ class ProjectGoalsController < ApplicationController
4
+ layout "houston/roadmaps/application"
5
+ before_filter :find_project
6
+
7
+
8
+ def index
9
+ authorize! :read, Milestone.new(project_id: @project.id)
10
+
11
+ @title = "Goals for #{@project.name}"
12
+ @milestones = @project.milestones
13
+ end
14
+
15
+
16
+ private
17
+
18
+ def find_project
19
+ @project = Project.find_by_slug! params[:project_slug]
20
+ end
21
+
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,36 @@
1
+ module Houston
2
+ module Roadmaps
3
+ class RoadmapMilestonesController < RoadmapsController
4
+ layout "houston/roadmaps/application"
5
+ before_filter :find_roadmap
6
+
7
+
8
+ def index
9
+ authorize! :read, Roadmap
10
+ render json: Houston::Roadmaps::RoadmapMilestonePresenter.new(@roadmap.milestones)
11
+ end
12
+
13
+
14
+ def update
15
+ authorize! :update, @roadmap
16
+
17
+ @roadmap.commits.create!(
18
+ user: current_user,
19
+ message: params[:message],
20
+ milestone_changes: params.fetch(:roadmap, {}).values)
21
+
22
+ head :ok
23
+ rescue ActiveRecord::RecordInvalid
24
+ render json: $!.record.errors, status: 422
25
+ end
26
+
27
+
28
+ private
29
+
30
+ def find_roadmap
31
+ @roadmap = Roadmap.find params[:id]
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,77 @@
1
+ module Houston
2
+ module Roadmaps
3
+ class RoadmapsController < ApplicationController
4
+ layout "houston/roadmaps/application"
5
+ before_filter :find_roadmap, only: [:show, :history, :edit, :update, :update_milestones, :destroy]
6
+
7
+
8
+ def index
9
+ authorize! :read, Roadmap
10
+ @title = "Roadmaps"
11
+ @roadmaps = Roadmap.all.preload(:projects, :milestones => {:milestone => :project}).order(:name)
12
+ end
13
+
14
+
15
+ def show
16
+ authorize! :read, @roadmap
17
+ @title = @roadmap.name
18
+ @goals = @roadmap.projects.goals
19
+ @milestones = @roadmap.milestones
20
+
21
+ render template: "houston/roadmaps/roadmaps/show_editable" if can?(:update, @roadmap)
22
+ end
23
+
24
+
25
+ def history
26
+ authorize! :read, @roadmap
27
+ @title = "#{@roadmap.name} History"
28
+
29
+ @commits = @roadmap.commits.order(created_at: :desc)
30
+ @commit_id = params[:commit_id].to_i
31
+
32
+ @milestones = @roadmap.milestones.including_destroyed
33
+ @markers = Houston::Roadmaps.config.markers
34
+ end
35
+
36
+
37
+ def edit
38
+ authorize! :update, @roadmap
39
+ end
40
+
41
+
42
+ def update
43
+ authorize! :update, @roadmap
44
+ if @roadmap.update_attributes params[:roadmap]
45
+ redirect_to roadmaps_url, notice: "Roadmap created"
46
+ else
47
+ render action: :edit
48
+ end
49
+ end
50
+
51
+
52
+ def new
53
+ authorize! :create, Roadmap
54
+ @roadmap = Roadmap.new
55
+ end
56
+
57
+
58
+ def create
59
+ authorize! :create, Roadmap
60
+ @roadmap = Roadmap.create params[:roadmap]
61
+ if @roadmap.persisted?
62
+ redirect_to roadmaps_url, notice: "Roadmap created"
63
+ else
64
+ render action: :new
65
+ end
66
+ end
67
+
68
+
69
+ protected
70
+
71
+ def find_roadmap
72
+ @roadmap = Roadmap.find params[:id]
73
+ end
74
+
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,4 @@
1
+ module Houston::Roadmaps
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,9 @@
1
+ class Roadmap < ActiveRecord::Base
2
+
3
+ has_many :milestones, class_name: "RoadmapMilestone"
4
+ has_many :commits, class_name: "RoadmapCommit"
5
+ has_and_belongs_to_many :projects
6
+
7
+ validates :name, presence: true
8
+
9
+ end
@@ -0,0 +1,35 @@
1
+ class RoadmapCommit < ActiveRecord::Base
2
+ attr_accessor :milestone_changes
3
+
4
+ belongs_to :roadmap
5
+ belongs_to :user
6
+ has_many :milestone_versions, class_name: "RoadmapMilestoneVersion"
7
+
8
+ validates :user, :message, :roadmap, :milestone_changes, presence: true
9
+
10
+ after_save :commit_milestone_changes
11
+
12
+ private
13
+
14
+ def commit_milestone_changes
15
+ milestone_changes.each do |change|
16
+ id = change.delete(:id)
17
+ remove = change.delete(:removed)
18
+ milestone = id && roadmap.milestones.find_by_id(id)
19
+ if remove == true || remove == "true"
20
+ milestone.update_attributes!(destroyed_at: Time.now) if milestone
21
+ else
22
+ milestone_attributes = change.pick(:band, :lanes, :start_date, :end_date)
23
+ if milestone
24
+ milestone.update_attributes!(milestone_attributes)
25
+ else
26
+ milestone = roadmap.milestones.create!(
27
+ milestone_attributes.merge(milestone_id: change[:milestoneId]))
28
+ end
29
+ end
30
+ version = milestone.versions.at(milestone.version)
31
+ version.update_column :roadmap_commit_id, self.id if version
32
+ end
33
+ end
34
+
35
+ end
@@ -0,0 +1,29 @@
1
+ class RoadmapMilestone < ActiveRecord::Base
2
+
3
+ default_scope { where(destroyed_at: nil).preload(:milestone) }
4
+
5
+ belongs_to :roadmap
6
+ belongs_to :milestone, -> { unscope(where: :destroyed_at) }
7
+
8
+ validates :roadmap_id, :milestone_id, :band, :start_date, :end_date, :lanes, presence: true
9
+
10
+ versioned only: [:start_date, :end_date, :band, :lanes, :destroyed_at], class_name: "RoadmapMilestoneVersion", initial_version: true
11
+
12
+ delegate :project,
13
+ :project_id,
14
+ :name,
15
+ :completed?,
16
+ to: :milestone
17
+
18
+
19
+ class << self
20
+ def including_destroyed
21
+ unscope(where: :destroyed_at)
22
+ end
23
+
24
+ def during(range)
25
+ where(arel_table[:start_date].lteq(range.end)).where(arel_table[:end_date].gteq(range.begin))
26
+ end
27
+ end
28
+
29
+ end
@@ -0,0 +1,8 @@
1
+ class RoadmapMilestoneVersion < VestalVersions::Version
2
+ self.table_name = "roadmap_milestone_versions"
3
+
4
+ attr_accessor :user_name
5
+
6
+ belongs_to :roadmap_commit
7
+
8
+ end
@@ -0,0 +1,27 @@
1
+ class Houston::Roadmaps::MilestoneApiPresenter
2
+
3
+ def initialize(milestones)
4
+ @milestones = OneOrMany.new(milestones)
5
+ end
6
+
7
+ def as_json(*args)
8
+ @milestones.map(&method(:to_hash))
9
+ end
10
+
11
+ def to_hash(milestone)
12
+ project = milestone.project
13
+ { id: milestone.id,
14
+ name: milestone.name,
15
+ projectId: project.id,
16
+ projectName: project.name,
17
+ projectSlug: project.slug,
18
+ tickets: milestone.tickets.map { |ticket| {
19
+ id: ticket.id,
20
+ number: ticket.number,
21
+ summary: ticket.summary
22
+ } },
23
+ startDate: milestone.start_date,
24
+ endDate: milestone.end_date }
25
+ end
26
+
27
+ end
@@ -0,0 +1,74 @@
1
+ class Houston::Roadmaps::MilestonePresenter
2
+
3
+ def initialize(milestones)
4
+ @milestones = OneOrMany.new(milestones)
5
+ end
6
+
7
+ def as_json(*args)
8
+ @milestones.map(&method(:to_hash))
9
+ end
10
+
11
+ def to_hash(milestone)
12
+ project = milestone.project
13
+ { id: milestone.id,
14
+ name: milestone.name,
15
+ roadmaps: milestone.roadmap_ids,
16
+ projectId: project.id,
17
+ projectColor: project.color,
18
+ projectName: project.name,
19
+ tickets: milestone.tickets_count,
20
+ ticketsCompleted: milestone.closed_tickets_count,
21
+ percentComplete: percent_complete(milestone),
22
+ completed: milestone.completed?,
23
+ band: milestone.band,
24
+ lanes: milestone.lanes,
25
+ locked: milestone.locked? || milestone.completed?,
26
+ startDate: milestone.start_date,
27
+ endDate: milestone.end_date,
28
+ removed: milestone.destroyed_at.present? }
29
+ end
30
+
31
+ private
32
+
33
+ def percent_complete(milestone)
34
+ percent_complete_by_ticket.fetch(milestone.id, 0)
35
+ end
36
+
37
+ def percent_complete_by_ticket
38
+ @percent_complete_by_ticket ||= Hash[Milestone.connection.select_rows(<<-SQL)
39
+ SELECT
40
+ tickets.milestone_id,
41
+ tickets.id,
42
+ tickets.closed_at IS NOT NULL "closed",
43
+ all_tasks.count,
44
+ completed_tasks.count
45
+ FROM tickets
46
+ LEFT OUTER JOIN (
47
+ SELECT ticket_id, COUNT(id) "count"
48
+ FROM tasks
49
+ GROUP BY ticket_id)
50
+ AS all_tasks
51
+ ON all_tasks.ticket_id=tickets.id
52
+ LEFT OUTER JOIN (
53
+ SELECT ticket_id, COUNT(id) "count"
54
+ FROM tasks
55
+ WHERE tasks.completed_at IS NOT NULL
56
+ GROUP BY ticket_id)
57
+ AS completed_tasks
58
+ ON completed_tasks.ticket_id=tickets.id
59
+ WHERE tickets.destroyed_at IS NULL
60
+ AND tickets.milestone_id IN (#{milestone_ids.join(", ")})
61
+ SQL
62
+ .map { |milestone_id, _, closed, tasks, completed_tasks|
63
+ [ milestone_id.to_i,
64
+ closed == "t" ? 1.0 : (completed_tasks.to_f / (tasks.to_i + 1)) ] }
65
+ .group_by { |(milestone_id, _)| milestone_id }
66
+ .map { |(milestone_id, tickets)|
67
+ [milestone_id, (tickets.sum { |(_, percent)| percent } / tickets.length)] }]
68
+ end
69
+
70
+ def milestone_ids
71
+ @milestone_ids ||= Array(@milestones.map(&:id))
72
+ end
73
+
74
+ end