houston-roadmaps 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/Gemfile +24 -0
- data/Gemfile.lock +358 -0
- data/MIT-LICENSE +20 -0
- data/README.md +31 -0
- data/Rakefile +25 -0
- data/app/assets/images/houston/roadmaps/.gitkeep +0 -0
- data/app/assets/javascripts/houston/roadmaps/app.coffee +2 -0
- data/app/assets/javascripts/houston/roadmaps/application.js +14 -0
- data/app/assets/javascripts/houston/roadmaps/handlebars_helpers.coffee +22 -0
- data/app/assets/javascripts/houston/roadmaps/jquery_extensions.coffee +4 -0
- data/app/assets/javascripts/houston/roadmaps/models/goal.coffee +11 -0
- data/app/assets/javascripts/houston/roadmaps/models/milestone.coffee +124 -0
- data/app/assets/javascripts/houston/roadmaps/models/roadmap.coffee +16 -0
- data/app/assets/javascripts/houston/roadmaps/models/viewport.coffee +3 -0
- data/app/assets/javascripts/houston/roadmaps/resize_listener.js +67 -0
- data/app/assets/javascripts/houston/roadmaps/views/_gantt_chart.coffee +224 -0
- data/app/assets/javascripts/houston/roadmaps/views/_show_milestone_view.coffee +204 -0
- data/app/assets/javascripts/houston/roadmaps/views/edit_gantt_chart.coffee +405 -0
- data/app/assets/javascripts/houston/roadmaps/views/edit_milestone_view.coffee +97 -0
- data/app/assets/javascripts/houston/roadmaps/views/edit_roadmap_view.coffee +80 -0
- data/app/assets/javascripts/houston/roadmaps/views/gantt_thumbnail_view.coffee +173 -0
- data/app/assets/javascripts/houston/roadmaps/views/goal_view.coffee +18 -0
- data/app/assets/javascripts/houston/roadmaps/views/project_goals_view.coffee +30 -0
- data/app/assets/javascripts/houston/roadmaps/views/roadmap_history_view.coffee +67 -0
- data/app/assets/javascripts/houston/roadmaps/views/roadmap_view.coffee +12 -0
- data/app/assets/javascripts/houston/roadmaps/views/roadmaps_view.coffee +29 -0
- data/app/assets/stylesheets/houston/roadmaps/application.css +13 -0
- data/app/assets/stylesheets/houston/roadmaps/colors.scss.erb +41 -0
- data/app/assets/stylesheets/houston/roadmaps/goals.scss +20 -0
- data/app/assets/stylesheets/houston/roadmaps/milestone.scss +58 -0
- data/app/assets/stylesheets/houston/roadmaps/roadmap.scss +276 -0
- data/app/assets/stylesheets/houston/roadmaps/roadmap_history.scss +42 -0
- data/app/assets/stylesheets/houston/roadmaps/roadmaps.scss +89 -0
- data/app/assets/templates/houston/roadmaps/goals/edit.hbs +6 -0
- data/app/assets/templates/houston/roadmaps/goals/index.hbs +9 -0
- data/app/assets/templates/houston/roadmaps/goals/new.hbs +27 -0
- data/app/assets/templates/houston/roadmaps/goals/show.hbs +6 -0
- data/app/assets/templates/houston/roadmaps/milestone/edit.hbs +67 -0
- data/app/assets/templates/houston/roadmaps/milestone/show.hbs +53 -0
- data/app/assets/templates/houston/roadmaps/milestone/ticket.hbs +28 -0
- data/app/assets/templates/houston/roadmaps/roadmap/goal.hbs +6 -0
- data/app/assets/templates/houston/roadmaps/roadmap/history.hbs +22 -0
- data/app/assets/templates/houston/roadmaps/roadmap/show.hbs +12 -0
- data/app/assets/templates/houston/roadmaps/roadmaps/index.hbs +2 -0
- data/app/assets/templates/houston/roadmaps/roadmaps/show.hbs +11 -0
- data/app/controllers/houston/roadmaps/api/v1/roadmap_controller.rb +20 -0
- data/app/controllers/houston/roadmaps/dashboard_controller.rb +34 -0
- data/app/controllers/houston/roadmaps/milestones_controller.rb +118 -0
- data/app/controllers/houston/roadmaps/project_goals_controller.rb +24 -0
- data/app/controllers/houston/roadmaps/roadmap_milestones_controller.rb +36 -0
- data/app/controllers/houston/roadmaps/roadmaps_controller.rb +77 -0
- data/app/helpers/houston/roadmaps/application_helper.rb +4 -0
- data/app/models/roadmap.rb +9 -0
- data/app/models/roadmap_commit.rb +35 -0
- data/app/models/roadmap_milestone.rb +29 -0
- data/app/models/roadmap_milestone_version.rb +8 -0
- data/app/presenters/houston/roadmaps/milestone_api_presenter.rb +27 -0
- data/app/presenters/houston/roadmaps/milestone_presenter.rb +74 -0
- data/app/presenters/houston/roadmaps/roadmap_milestone_presenter.rb +70 -0
- data/app/presenters/houston/roadmaps/roadmap_presenter.rb +37 -0
- data/app/presenters/houston/roadmaps/ticket_presenter.rb +20 -0
- data/app/views/houston/roadmaps/dashboard/show.html.erb +42 -0
- data/app/views/houston/roadmaps/milestones/show.html.erb +40 -0
- data/app/views/houston/roadmaps/project_goals/index.html.erb +42 -0
- data/app/views/houston/roadmaps/roadmaps/_form.html.erb +29 -0
- data/app/views/houston/roadmaps/roadmaps/edit.html.erb +8 -0
- data/app/views/houston/roadmaps/roadmaps/history.html.erb +41 -0
- data/app/views/houston/roadmaps/roadmaps/index.html.erb +24 -0
- data/app/views/houston/roadmaps/roadmaps/new.html.erb +7 -0
- data/app/views/houston/roadmaps/roadmaps/show.html.erb +27 -0
- data/app/views/houston/roadmaps/roadmaps/show_editable.html.erb +35 -0
- data/app/views/layouts/houston/roadmaps/application.html.erb +10 -0
- data/app/views/layouts/houston/roadmaps/dashboard.html.erb +9 -0
- data/bin/rails +8 -0
- data/config/database.yml +13 -0
- data/config/initializers/add_navigation_renderer.rb +13 -0
- data/config/routes.rb +37 -0
- data/db/.keep +0 -0
- data/db/migrate/20140831210254_add_band_to_milestones.rb +5 -0
- data/db/migrate/20140907212311_add_end_date_to_milestones.rb +19 -0
- data/db/migrate/20140916230539_add_locked_to_milestone.rb +5 -0
- data/db/migrate/20140927154728_add_closed_ticket_count_to_milestones.rb +11 -0
- data/db/migrate/20140929024130_create_roadmap_commits.rb +29 -0
- data/db/migrate/20141012023628_add_user_id_to_milestone_versions.rb +11 -0
- data/db/migrate/20150102192805_add_lanes_to_milestones.rb +5 -0
- data/db/migrate/20150119155145_add_goal_and_feedback_query_to_milestones.rb +6 -0
- data/db/migrate/20150524203903_add_project_id_to_roadmap_commits.rb +17 -0
- data/db/migrate/20150603203744_add_timestamps_to_roadmap_commits.rb +20 -0
- data/db/migrate/20160206214746_rename_roadmap_feature_to_milestones.rb +25 -0
- data/db/migrate/20160207154530_create_roadmaps.rb +125 -0
- data/db/structure.sql +2557 -0
- data/houston-roadmaps.gemspec +29 -0
- data/lib/houston-roadmaps.rb +1 -0
- data/lib/houston/roadmaps.rb +15 -0
- data/lib/houston/roadmaps/configuration.rb +14 -0
- data/lib/houston/roadmaps/engine.rb +27 -0
- data/lib/houston/roadmaps/milestone_ext.rb +14 -0
- data/lib/houston/roadmaps/project_ext.rb +20 -0
- data/lib/houston/roadmaps/railtie.rb +17 -0
- data/lib/houston/roadmaps/version.rb +5 -0
- data/lib/tasks/roadmap_tasks.rake +4 -0
- data/test/acceptance/houston_dummy_test.rb +17 -0
- data/test/dummy/houston.rb +23 -0
- data/test/fixtures/projects.yml +3 -0
- data/test/fixtures/users.yml +10 -0
- data/test/test_helper.rb +43 -0
- data/test/unit/fixtures_test.rb +11 -0
- 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,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,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
|