Commit 45a5d5c6 by Tấn Trần Thanh

merge plugin workflow report

parents b041836a aa117c95
Pipeline #1583 canceled with stages
in 0 seconds
......@@ -14,7 +14,7 @@ gem "protected_attributes"
gem "actionpack-xml_parser"
gem "roadie-rails", "~> 1.1.1"
gem "roadie", "~> 3.2.1"
gem "mimemagic"
gem 'mimemagic', '~> 0.4.3'
gem "nokogiri", (RUBY_VERSION >= "2.1" ? "~> 1.7.2" : "~> 1.6.8")
gem "i18n", "~> 0.7.0"
......
......@@ -40,6 +40,7 @@ GEM
addressable (2.5.2)
public_suffix (>= 2.0.2, < 4.0)
arel (6.0.4)
ast (2.4.2)
builder (3.2.3)
byebug (9.1.0)
capybara (1.1.4)
......@@ -56,17 +57,30 @@ GEM
crass (1.0.3)
css_parser (1.6.0)
addressable
descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1)
docile (1.1.5)
erubis (2.7.0)
faraday (0.17.6)
multipart-post (>= 1.2, < 3)
ffi (1.9.18)
github_api (0.19.0)
addressable (~> 2.4)
descendants_tracker (~> 0.0.4)
faraday (>= 0.8, < 2)
hashie (~> 3.5, >= 3.5.2)
oauth2 (~> 1.0)
globalid (0.4.1)
activesupport (>= 4.2.0)
hashie (3.6.0)
htmlentities (4.3.4)
httpclient (2.8.3)
i18n (0.7.0)
jaro_winkler (1.5.4)
jquery-rails (3.1.4)
railties (>= 3.0, < 5.0)
thor (>= 0.14, < 2.0)
jwt (2.3.0)
liquid (2.6.3)
loofah (2.1.1)
crass (~> 1.0.2)
......@@ -77,17 +91,30 @@ GEM
mime-types (3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521)
mimemagic (0.3.2)
mimemagic (0.4.3)
nokogiri (~> 1)
rake
mini_mime (1.0.0)
mini_portile2 (2.1.0)
minitest (5.11.1)
mocha (1.3.0)
metaclass (~> 0.0.1)
multi_json (1.13.0)
multi_xml (0.6.0)
multipart-post (2.3.0)
mysql2 (0.4.10)
net-ldap (0.12.1)
nokogiri (1.7.2)
mini_portile2 (~> 2.1.0)
oauth2 (1.4.11)
faraday (>= 0.17.3, < 3.0)
jwt (>= 1.0, < 3.0)
multi_json (~> 1.3)
multi_xml (~> 0.5)
rack (>= 1.2, < 4)
parallel (1.19.2)
parser (3.2.2.0)
ast (~> 2.4.1)
protected_attributes (1.1.4)
activemodel (>= 4.0.1, < 5.0)
public_suffix (3.0.1)
......@@ -121,6 +148,7 @@ GEM
activesupport (= 4.2.8)
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
rainbow (3.1.1)
rake (12.3.0)
rbpdf (1.19.3)
htmlentities
......@@ -135,6 +163,7 @@ GEM
actionpack (>= 4.2, < 6)
rails (>= 4.2, < 6)
request_store (1.0.5)
rexml (3.2.5)
rmagick (2.16.0)
roadie (3.2.2)
css_parser (~> 1.4)
......@@ -142,7 +171,18 @@ GEM
roadie-rails (1.1.1)
railties (>= 3.0, < 5.1)
roadie (~> 3.1)
rubocop (0.81.0)
jaro_winkler (~> 1.5.1)
parallel (~> 1.10)
parser (>= 2.7.0.1)
rainbow (>= 2.2.2, < 4.0)
rexml
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 2.0)
rubocop-performance (1.5.2)
rubocop (>= 0.71.0)
ruby-openid (2.3.0)
ruby-progressbar (1.13.0)
rubyzip (1.2.1)
selenium-webdriver (2.53.4)
childprocess (~> 0.5)
......@@ -153,6 +193,13 @@ GEM
multi_json (~> 1.0)
simplecov-html (~> 0.9.0)
simplecov-html (0.9.0)
slim (4.1.0)
temple (>= 0.7.6, < 0.9)
tilt (>= 2.0.6, < 2.1)
slim-rails (3.6.2)
actionpack (>= 3.1)
railties (>= 3.1)
slim (>= 3.0, < 6.0, != 5.0.0)
sprockets (3.7.1)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
......@@ -160,12 +207,18 @@ GEM
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
temple (0.8.2)
test_after_commit (0.4.2)
activerecord (>= 3.2)
thor (0.20.0)
thread_safe (0.3.6)
tilt (2.0.11)
tzinfo (1.2.4)
thread_safe (~> 0.1)
unicode-display_width (1.8.0)
validate_url (1.0.15)
activemodel (>= 3.0.0)
public_suffix
websocket (1.2.5)
xpath (0.1.4)
nokogiri (~> 1.3)
......@@ -176,19 +229,25 @@ PLATFORMS
DEPENDENCIES
actionpack-xml_parser
addressable (~> 2.5, >= 2.5.2)
byebug (~> 9.0, >= 9.0.6)
capybara
coderay (~> 1.1.1)
descendants_tracker (~> 0.0.4)
faraday (~> 0.17.6)
github_api (~> 0.19.0)
hashie (~> 3.5, >= 3.5.7)
httpclient
i18n (~> 0.7.0)
jquery-rails (~> 3.1.4)
mime-types (~> 3.0)
mimemagic
mimemagic (~> 0.4.3)
minitest
mocha
mysql2 (~> 0.4.6)
net-ldap (~> 0.12.0)
nokogiri (~> 1.7.2)
oauth2 (~> 1.2)
protected_attributes
rack-openid
rails (= 4.2.8)
......@@ -203,11 +262,15 @@ DEPENDENCIES
rmagick (>= 2.14.0)
roadie (~> 3.2.1)
roadie-rails (~> 1.1.1)
rubocop
rubocop-performance
ruby-openid (~> 2.3.0)
selenium-webdriver (~> 2.53.4)
simplecov (~> 0.9.1)
slim-rails
test_after_commit (~> 0.4.2)
tzinfo-data
validate_url
yard
BUNDLED WITH
......
# Sample plugin controller
class ExampleController < ApplicationController
unloadable
# unloadable
layout 'base'
before_action :find_project, :authorize
......
class <%= @controller_class %>Controller < ApplicationController
unloadable
# unloadable
<% actions.each do |action| -%>
......
class <%= @model_class %> < ActiveRecord::Base
unloadable
# unloadable
end
......@@ -18,7 +18,7 @@
# along with redmine_agile. If not, see <http://www.gnu.org/licenses/>.
class AgileBoardsController < ApplicationController
unloadable
# unloadable
menu_item :agile
......
......@@ -18,7 +18,7 @@
# along with redmine_agile. If not, see <http://www.gnu.org/licenses/>.
class AgileChartsController < ApplicationController
unloadable
# unloadable
menu_item :agile
......
......@@ -18,7 +18,7 @@
# along with redmine_agile. If not, see <http://www.gnu.org/licenses/>.
class AgileJournalDetailsController < ApplicationController
unloadable
# unloadable
before_filter :find_issue
......
......@@ -18,7 +18,7 @@
# along with redmine_agile. If not, see <http://www.gnu.org/licenses/>.
class AgileVersionsController < ApplicationController
unloadable
# unloadable
menu_item :agile
......
......@@ -18,7 +18,7 @@
# along with redmine_agile. If not, see <http://www.gnu.org/licenses/>.
class AgileChartsQuery < AgileQuery
unloadable
# unloadable
validate :validate_query_dates
......
......@@ -18,7 +18,7 @@
# along with redmine_agile. If not, see <http://www.gnu.org/licenses/>.
class AgileData < ActiveRecord::Base
unloadable
# unloadable
belongs_to :issue
validates :story_points, :numericality => {:only_integer => true, :greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid}
end
......@@ -18,7 +18,7 @@
# along with redmine_agile. If not, see <http://www.gnu.org/licenses/>.
class AgileQuery < Query
unloadable
# unloadable
attr_reader :truncated
......
......@@ -18,7 +18,7 @@
# along with redmine_agile. If not, see <http://www.gnu.org/licenses/>.
class AgileQuery < Query
unloadable
# unloadable
VISIBILITY_PRIVATE = 0
VISIBILITY_ROLES = 1
......
......@@ -27,7 +27,7 @@ module RedmineAgile
def self.included(base)
base.send(:include, InstanceMethods)
base.class_eval do
unloadable
# unloadable
has_one :agile_data, :dependent => :destroy
delegate :position, :to => :agile_data, :allow_nil => true
scope :sorted_by_rank, lambda {eager_load(:agile_data).
......
......@@ -23,7 +23,7 @@ module RedmineAgile
module ProjectPatch
def self.included(base)
base.class_eval do
unloadable
# unloadable
acts_as_colored
safe_attributes 'agile_color_attributes',
:if => lambda {|project, user| user.allowed_to?(:edit_project, project) && user.allowed_to?(:view_agile_queries, project) && RedmineAgile.use_colors?}
......
......@@ -23,7 +23,7 @@ module RedmineAgile
module UserPatch
def self.included(base)
base.class_eval do
unloadable
# unloadable
acts_as_colored
safe_attributes 'agile_color_attributes',
:if => lambda {|user, current_user| (current_user.admin? || (user.new_record? && current_user.anonymous? && Setting.self_registration?)) && RedmineAgile.use_colors? }
......
= workflow_report
Description goes here
This plugin is designed to produce monthly workflow reports with precise customization options. Users can select a specific month, year, and team of interest. Upon making these selections, the plugin generates a detailed report that is exclusively based on the chosen month and year.
require 'csv'
class WorkflowReportController < ApplicationController
include WorkflowReport
before_action :authorize_global
before_action :require_xhr_request, only: %i[export show_daily_report]
......@@ -36,237 +35,17 @@ class WorkflowReportController < ApplicationController
end
def export
thead = ['root_id', 'project', 'subject', 'target_version', ' created_on ', 'closed_on ', 'due_date ', ' status ', 'Estimated_hours (*final version) ', 'Actual time', 'Diff', 'Estimated_hours (*Initial version)',
'Number of estimation changes', 'Note of estimation changes', '1. Requirement', '2. Design', '3. Coding', '4. Testing', '5. Bug fixing', '6. Release', 'Others', '1. Requirement', '2. Design', '3. Coding',
'4. Testing', '5. Bug fixing', '6. Release', 'Others', 'testcases', 'vn STG bug', 'Jp STG bug', 'Production', 'Issue', 'Issue comment', 'PR comment', 'Review comment', 'Commits', 'File changed', 'Addtion', 'Deletetion']
team = params[:team]
project_ids = $workflow_report_config['teams'].select { |hash| hash.key?(team) }[0][team]
result = fetch_data(project_ids, params[:year].to_i, params[:month].to_i, column_number: thead.size)
result = WorkflowReport.build_report(params[:year].to_i, params[:month].to_i, project_ids)
respond_to do |format|
format.js { render 'build_table', locals: { result: result, thead: thead } }
format.js { render 'build_table', locals: { result: result, thead: TABLE_HEADER } }
end
end
private
def fetch_data(project_ids, year, month, column_number: 0)
github = Github.new oauth_token: $workflow_report_config['github_token']
result = Array.new(column_number)
root_ids = Issue.select('issues.root_id')
.joins('INNER JOIN projects ON projects.id = issues.project_id')
.joins('LEFT OUTER JOIN time_entries ON issues.id = time_entries.issue_id')
.joins('INNER JOIN enabled_modules ON enabled_modules.project_id = projects.id')
.where('projects.status <> 9')
.where("projects.id IN (#{project_ids.join(',')})")
.where(['enabled_modules.name = ? ', 'time_tracking'])
.where([
'((time_entries.spent_on IS NOT NULL AND time_entries.tyear = ? AND time_entries.tmonth = ?) OR (issues.closed_on BETWEEN ? AND ?))',
year,
month,
DateTime.new(year, month).beginning_of_day,
DateTime.new(year, month, -1).end_of_day])
.distinct.pluck(:root_id)
return unless root_ids.length.positive?
sum_hours_records = Issue.select('issues.root_id, sum(time_entries.hours) as hours, max(IFNULL(c1.value,-1)) as testcases, max(IFNULL(c2.value,-1)) as bugs,max(IFNULL(stg.value,-1)) as stg_bugs, max(IFNULL(prod.value,-1)) as prod_bugs')
.joins('INNER JOIN time_entries ON issues.id = time_entries.issue_id')
.joins('LEFT OUTER JOIN custom_values c1 ON c1.customized_id = issues.id and c1.custom_field_id=22')
.joins('LEFT OUTER JOIN custom_values c2 ON c2.customized_id = issues.id and c2.custom_field_id=23')
.joins('LEFT OUTER JOIN custom_values stg ON stg.customized_id = issues.id and stg.custom_field_id=27')
.joins('LEFT OUTER JOIN custom_values prod ON prod.customized_id = issues.id and prod.custom_field_id=28')
.where("issues.root_id IN (#{root_ids.join(',')})")
.where('issues.tracker_id <> 1')
.group('issues.root_id')
raw_tasks_records = Issue.select('issues.root_id, issues.id, tracker_id, issues.subject, issues.due_date, issues.created_on, issues.closed_on, estimated_hours, issue_statuses.name as status, versions.name as target_version, projects.name as project, pr.value as pr, jr.value as jp_request')
.joins('INNER JOIN projects ON projects.id = issues.project_id')
.joins('INNER JOIN issue_statuses ON issue_statuses.id = issues.status_id')
.joins('LEFT JOIN versions ON issues.fixed_version_id = versions.id')
.joins('LEFT OUTER JOIN custom_values jr ON jr.customized_id = issues.id and jr.custom_field_id=16')
.joins('LEFT OUTER JOIN custom_values pr ON pr.customized_id = issues.id and pr.custom_field_id=18')
.where("issues.root_id IN (#{root_ids.join(',')})")
.where('issues.tracker_id <> 1')
.order('issues.root_id')
result.map! { |item| item.to_a }
raw_tasks_records.group_by(&:root_id).each do |root_id, record|
sum_hours_record = sum_hours_records.find { |hr| hr.root_id == root_id }
issue_ids = record.map(&:id)
journals = ActiveRecord::Base.connection.execute("select journalized_id, old_value, value, created_on, notes FROM journals
INNER JOIN journal_details ON journals.id = journal_id WHERE journalized_id IN (#{issue_ids.join(',')})
AND prop_key = 'estimated_hours' ORDER BY journalized_id, journals.id").to_a
result[0] << root_id
result[1] << record.first[:project].gsub(/[^[:print:]]/, '')
result[2] << record.first[:subject].gsub(/[^[:print:]]/, '')
result[3] << record.first[:target_version]
issue_created_on = record.min_by { |i| i[:created_on] if i[:created_on].present? }
result[4].push(issue_created_on.present? ? issue_created_on[:created_on]&.strftime('%Y-%m-%d %H:%M:%S') : '')
closed_on_issues = record.map(&:closed_on).compact
issue_closed_on = closed_on_issues.max_by { |close_on| close_on }
result[5].push(issue_closed_on.present? ? issue_closed_on.strftime('%Y-%m-%d %H:%M:%S') : '')
issue_due_dates = record.map(&:due_date).compact
issue_due_date = issue_due_dates.max_by { |due_date| due_date }
result[6].push(issue_due_date.present? ? issue_due_date&.strftime('%Y-%m-%d %H:%M:%S') : '')
result[7].push(record.first[:status].present? ? record.first[:status] : '')
sum_estimated_hours = record.map { |i| i[:estimated_hours] }.compact.sum
result[8] << sum_estimated_hours
if sum_hours_record.present?
result[9] << sum_hours_record[:hours]
result[10] << (sum_hours_record[:hours] - sum_estimated_hours)
else
result[9] << ''
result[10] << ''
end
if journals.length.positive?
number_of_est_changes = journals.count { |j| !j[1].nil? } || ''
est_changes = journals.reject { |j| j[1].nil? }.select { |e| e[4].present? }
notes = est_changes.inject('') { |all, i| "#{all}#{i[4]}\n" } || ''
original_est_hours = journals.group_by(&:shift).inject(0) { |sum, i| sum + i[1][0][1].to_f unless i.nil? } || ''
result[11] << original_est_hours
result[12] << number_of_est_changes
result[13] << notes
else
result[11] << ''
result[12] << ''
result[13] << ''
end
pull_request = ''
jp_request = ''
record.each do |issue|
if issue.tracker_id == 12
pull_request = issue.pr
jp_request = issue.jp_request
end
process = get_process(issue.subject.gsub(/[^[:print:]]/, ''))
index_1 = 14 # first index for estimation detail
index_2 = 21 # first index for actual detail
case process
when '1. Requirement'
result[index_1].push(issue.estimated_hours.present? ? issue.estimated_hours : '')
result[index_2].push(issue.spent_hours.positive? ? issue.spent_hours : '')
when '2. Design'
result[index_1 + 1].push(issue.estimated_hours.present? ? issue.estimated_hours : '')
result[index_2 + 1].push(issue.spent_hours.positive? ? issue.spent_hours : '')
when '3. Coding'
result[index_1 + 2].push(issue.estimated_hours.present? ? issue.estimated_hours : '')
result[index_2 + 2].push(issue.spent_hours.positive? ? issue.spent_hours : '')
when '4. Testing'
result[index_1 + 3].push(issue.estimated_hours.present? ? issue.estimated_hours : '')
result[index_2 + 3].push(issue.spent_hours.positive? ? issue.spent_hours : '')
when '5. Bug fixing'
result[index_1 + 4].push(issue.estimated_hours.present? ? issue.estimated_hours : '')
result[index_2 + 4].push(issue.spent_hours.positive? ? issue.spent_hours : '')
when '6. Release'
result[index_1 + 5].push(issue.estimated_hours.present? ? issue.estimated_hours : '')
result[index_2 + 5].push(issue.spent_hours.positive? ? issue.spent_hours : '')
else
result[index_1 + 6].push(issue.estimated_hours.present? ? issue.estimated_hours : '')
result[index_2 + 6].push(issue.spent_hours.positive? ? issue.spent_hours : '')
end
end
if sum_hours_record.nil?
# testcases
result[28] << ''
# internal bug
result[29] << ''
# stg bug
result[30] << ''
# prod bug
result[31] << ''
else
# testcases
result[28] << sum_hours_record[:testcases] if sum_hours_record[:testcases].to_i >= 0
# internal bug
result[29] << sum_hours_record[:bugs] if sum_hours_record[:bugs].to_i >= 0
# stg bug
result[30] << sum_hours_record[:stg_bugs] if sum_hours_record[:stg_bugs].to_i >= 0
# prod bug
result[31] << sum_hours_record[:prod_bugs] if sum_hours_record[:prod_bugs].to_i >= 0
end
if jp_request.present?
jp_request = jp_request.strip
result[32] << jp_request
jp_request_arr = URI(jp_request).path.split('/').reject(&:blank?)
if jp_request_arr.length == 4 && jp_request_arr[3].to_i.positive?
issue_detail = github.issues.find user: jp_request_arr[0], repo: jp_request_arr[1], number: jp_request_arr[3]
result[33].push(issue_detail.success? ? issue_detail.comments : '')
end
else
result[32] << ''
result[33] << ''
end
# pr detail
next unless pull_request.present?
pr_comments = 0
pr_review_comments = 0
pr_commits = 0
pr_additions = 0
pr_deletions = 0
pr_changed_files = 0
prs = pull_request.split("\r\n").compact
prs.each do |link|
pr_link_arr = URI(link.strip).path.split('/').compact_blank
next unless pr_link_arr.length == 4
pr_detail = github.pull_requests.find user: pr_link_arr[0], repo: pr_link_arr[1], number: pr_link_arr[3]
if pr_detail.success?
pr_comments += pr_detail.comments
pr_review_comments += pr_detail.review_comments
pr_commits += pr_detail.commits
pr_additions += pr_detail.additions
pr_deletions += pr_detail.deletions
pr_changed_files += pr_detail.changed_files
end
result[34] << pr_comments
result[35] << pr_review_comments
result[36] << pr_commits
result[37] << pr_changed_files
result[38] << pr_additions
result[39] << pr_deletions
end
end
result
end
def get_process(subject)
name = ''
if !subject.nil? && index = subject.strip.index(' -')
subject = subject.strip
id = subject[0, 1]
name = subject[0, index]
if id.to_i != 0
if ['3. Code review'].include? name
name = '3. Coding'
elsif ['4. Create Test case'].include? name
name = '4. Testing'
else
name
end
elsif name.include? 'Requirement'
name = "1. #{name}"
elsif name.include? 'Design'
name = "2. #{name}"
elsif ['Coding', 'Code review'].include? name
name = '3. Coding'
elsif ['Testing', 'Create Test case'].include? name
name = '4. Testing'
elsif name.include? 'Bug fixing'
name = "5. #{name}"
elsif name.include? 'Release'
name = "6. #{name}"
end
end
name.strip
end
def require_xhr_request
head :unprocessable_entity unless request.xhr?
end
......
class WorkflowReportIssue < Issue
TESTCASE_FIELD_ID = 22
BUGS_FIELD_ID = 23
STG_BUGS_FIELD_ID = 27
PROD_BUGS_FIELD_ID = 28
PR_FIELD_ID = 18
JP_REQUEST_FIELD_ID = 16
scope :find_root_ids, ->(year, month, project_ids) {
joins(:project, project: :enabled_modules)
.joins('LEFT OUTER JOIN time_entries ON issues.id = time_entries.issue_id')
.where.not(projects: { status: 9 })
.where(projects: { id: project_ids }, enabled_modules: { name: 'time_tracking' })
.where(['((time_entries.spent_on IS NOT NULL AND time_entries.tyear = ? AND time_entries.tmonth = ?) OR (issues.closed_on BETWEEN ? AND ?))',
year, month, DateTime.new(year, month).beginning_of_day, DateTime.new(year, month, -1).end_of_day])
.distinct.pluck(:root_id)
}
scope :find_sum_hours_records, ->(root_ids) {
select(:root_id, 'sum(time_entries.hours) as hours', 'max(IFNULL(c1.value,-1)) as testcases', 'max(IFNULL(c2.value,-1)) as bugs', 'max(IFNULL(stg.value,-1)) as stg_bugs', 'max(IFNULL(prod.value,-1)) as prod_bugs')
.joins(:time_entries)
.joins("LEFT OUTER JOIN custom_values c1 ON c1.customized_id = issues.id and c1.custom_field_id=#{TESTCASE_FIELD_ID}")
.joins("LEFT OUTER JOIN custom_values c2 ON c2.customized_id = issues.id and c2.custom_field_id=#{BUGS_FIELD_ID}")
.joins("LEFT OUTER JOIN custom_values stg ON stg.customized_id = issues.id and stg.custom_field_id=#{STG_BUGS_FIELD_ID}")
.joins("LEFT OUTER JOIN custom_values prod ON prod.customized_id = issues.id and prod.custom_field_id=#{PROD_BUGS_FIELD_ID}")
.where(issues: { root_id: root_ids })
.where.not(issues: { tracker_id: 1 })
.group(:root_id)
}
scope :raw_tasks_records, ->(root_ids) {
select(:root_id, :id, :tracker_id, :subject, :due_date, :created_on, :closed_on, :estimated_hours, 'issue_statuses.name as status', 'versions.name as target_version', 'projects.name as project', 'pr.value as pr', 'jr.value as jp_request')
.joins(:project, :status)
.joins('LEFT JOIN versions ON issues.fixed_version_id = versions.id')
.joins("LEFT OUTER JOIN custom_values jr ON jr.customized_id = issues.id and jr.custom_field_id=#{JP_REQUEST_FIELD_ID}")
.joins("LEFT OUTER JOIN custom_values pr ON pr.customized_id = issues.id and pr.custom_field_id=#{PR_FIELD_ID}")
.where(issues: { root_id: root_ids })
.where.not(issues: { tracker_id: 1 })
.order(:root_id)
}
end
class WorkflowReportJournal < Journal
scope :find_journal_by_issue_ids, ->(issue_ids) {
joins(:details)
.where(journals: { journalized_id: issue_ids }, journal_details: { prop_key: 'estimated_hours' })
.order(:journalized_id, :id)
.pluck(:journalized_id, :old_value, :value, :created_on, :notes)
}
end
module WorkflowReport
TABLE_HEADER = ['root_id', 'project', 'subject', 'target_version', 'created_on ', 'closed_on ', 'due_date ', 'status ', 'Estimated_hours (*final version)', 'Actual time', 'Diff', 'Estimated_hours (*Initial version)',
'Number of estimation changes', 'Note of estimation changes', '1. Requirement', '2. Design', '3. Coding', '4. Testing', '5. Bug fixing', '6. Release', 'Others', '1. Requirement', '2. Design', '3. Coding',
'4. Testing', '5. Bug fixing', '6. Release', 'Others', 'testcases', 'vn STG bug', 'Jp STG bug', 'Production', 'Issue', 'Issue comment', 'PR comment', 'Review comment', 'Commits', 'File changed', 'Addtion', 'Deletetion']
EST_DETAIL_FIRST_COL = 14
ACTUAL_TIME_DETAIL_FIRST_COL = 21
class << self
def build_report(year, month, project_ids)
github = Github.new oauth_token: $workflow_report_config['github_token']
result = TABLE_HEADER.length.times.map { [] }
root_ids = WorkflowReportIssue.find_root_ids(year, month, project_ids)
return unless root_ids.length.positive?
sum_hours_records = WorkflowReportIssue.find_sum_hours_records(root_ids)
raw_tasks = WorkflowReportIssue.raw_tasks_records(root_ids)
raw_tasks.group_by(&:root_id).each do |root_id, record|
sum_hours_record = sum_hours_records.find { |hr| hr.root_id == root_id }
issue_ids = record.map(&:id)
journals = WorkflowReportJournal.find_journal_by_issue_ids(issue_ids).to_a
result[0] << root_id
result[1] << record.first[:project].gsub(/[^[:print:]]/, '')
result[2] << record.first[:subject].gsub(/[^[:print:]]/, '')
result[3] << record.first[:target_version]
issue_created_on = record.min_by { |i| i[:created_on] if i[:created_on].present? }
result[4].push(issue_created_on.present? ? issue_created_on[:created_on]&.strftime('%Y-%m-%d %H:%M:%S') : '')
closed_on_issues = record.map(&:closed_on).compact
issue_closed_on = closed_on_issues.max_by { |close_on| close_on }
result[5].push(issue_closed_on.present? ? issue_closed_on.strftime('%Y-%m-%d %H:%M:%S') : '')
issue_due_dates = record.map(&:due_date).compact
issue_due_date = issue_due_dates.max_by { |due_date| due_date }
result[6].push(issue_due_date.present? ? issue_due_date&.strftime('%Y-%m-%d %H:%M:%S') : '')
result[7].push(record.first[:status].present? ? record.first[:status] : '')
sum_estimated_hours = record.map { |i| i[:estimated_hours] }.compact.sum
result[8] << sum_estimated_hours
if sum_hours_record.present?
result[9] << sum_hours_record[:hours]
result[10] << (sum_hours_record[:hours] - sum_estimated_hours)
else
result[9] << ''
result[10] << ''
end
if journals.length.positive?
number_of_est_changes = journals.count { |j| !j[1].nil? } || ''
est_changes = journals.reject { |j| j[1].nil? }.select { |e| e[4].present? }
notes = est_changes.inject('') { |all, i| "#{all}#{i[4]}\n" } || ''
original_est_hours = journals.group_by(&:shift).inject(0) { |sum, i| sum + i[1][0][1].to_f unless i.nil? } || ''
result[11] << original_est_hours
result[12] << number_of_est_changes
result[13] << notes
else
result[11] << ''
result[12] << ''
result[13] << ''
end
pull_request = ''
jp_request = ''
record.each do |issue|
if issue.tracker_id == 12
pull_request = issue.pr
jp_request = issue.jp_request
end
process = get_process(issue.subject.gsub(/[^[:print:]]/, ''))
index_1 = EST_DETAIL_FIRST_COL # first index for estimation detail
index_2 = ACTUAL_TIME_DETAIL_FIRST_COL # first index for actual detail
case process
when '1. Requirement'
result[index_1].push(issue.estimated_hours.present? ? issue.estimated_hours : '')
result[index_2].push(issue.spent_hours.positive? ? issue.spent_hours : '')
when '2. Design'
result[index_1 + 1].push(issue.estimated_hours.present? ? issue.estimated_hours : '')
result[index_2 + 1].push(issue.spent_hours.positive? ? issue.spent_hours : '')
when '3. Coding'
result[index_1 + 2].push(issue.estimated_hours.present? ? issue.estimated_hours : '')
result[index_2 + 2].push(issue.spent_hours.positive? ? issue.spent_hours : '')
when '4. Testing'
result[index_1 + 3].push(issue.estimated_hours.present? ? issue.estimated_hours : '')
result[index_2 + 3].push(issue.spent_hours.positive? ? issue.spent_hours : '')
when '5. Bug fixing'
result[index_1 + 4].push(issue.estimated_hours.present? ? issue.estimated_hours : '')
result[index_2 + 4].push(issue.spent_hours.positive? ? issue.spent_hours : '')
when '6. Release'
result[index_1 + 5].push(issue.estimated_hours.present? ? issue.estimated_hours : '')
result[index_2 + 5].push(issue.spent_hours.positive? ? issue.spent_hours : '')
else
result[index_1 + 6].push(issue.estimated_hours.present? ? issue.estimated_hours : '')
result[index_2 + 6].push(issue.spent_hours.positive? ? issue.spent_hours : '')
end
end
if sum_hours_record.nil?
# testcases
result[28] << ''
# internal bug
result[29] << ''
# stg bug
result[30] << ''
# prod bug
result[31] << ''
else
# testcases
result[28] << sum_hours_record[:testcases] if sum_hours_record[:testcases].to_i >= 0
# internal bug
result[29] << sum_hours_record[:bugs] if sum_hours_record[:bugs].to_i >= 0
# stg bug
result[30] << sum_hours_record[:stg_bugs] if sum_hours_record[:stg_bugs].to_i >= 0
# prod bug
result[31] << sum_hours_record[:prod_bugs] if sum_hours_record[:prod_bugs].to_i >= 0
end
if jp_request.present?
jp_request = jp_request.strip
result[32] << jp_request
jp_request_arr = URI(jp_request).path.split('/').reject(&:blank?)
if jp_request_arr.length == 4 && jp_request_arr[3].to_i.positive?
issue_detail = github.issues.find user: jp_request_arr[0], repo: jp_request_arr[1], number: jp_request_arr[3]
result[33].push(issue_detail.success? ? issue_detail.comments : '')
end
else
result[32] << ''
result[33] << ''
end
# pr detail
next unless pull_request.present?
pr_comments = 0
pr_review_comments = 0
pr_commits = 0
pr_additions = 0
pr_deletions = 0
pr_changed_files = 0
prs = pull_request.split("\r\n").compact
prs.each do |link|
pr_link_arr = URI(link.strip).path.split('/').compact_blank
next unless pr_link_arr.length == 4
pr_detail = github.pull_requests.find user: pr_link_arr[0], repo: pr_link_arr[1], number: pr_link_arr[3]
if pr_detail.success?
pr_comments += pr_detail.comments
pr_review_comments += pr_detail.review_comments
pr_commits += pr_detail.commits
pr_additions += pr_detail.additions
pr_deletions += pr_detail.deletions
pr_changed_files += pr_detail.changed_files
end
result[34] << pr_comments
result[35] << pr_review_comments
result[36] << pr_commits
result[37] << pr_changed_files
result[38] << pr_additions
result[39] << pr_deletions
end
end
result
end
private
def get_process(subject)
name = ''
if !subject.nil? && index = subject.strip.index(' -')
subject = subject.strip
id = subject[0, 1]
name = subject[0, index]
if id.to_i != 0
if ["3. Code review"].include? name
name = "3. Coding"
elsif ["4. Create Test case"].include? name
name = "4. Testing"
else
name
end
else
if name.include? "Requirement"
name = "1. " + name
elsif name.include? "Design"
name = "2. " + name
elsif ["Coding", "Code review"].include? name
name = "3. Coding"
elsif ["Testing", "Create Test case"].include? name
name = "4. Testing"
elsif name.include? "Bug fixing"
name = "5. " + name
elsif name.include? "Release"
name = "6. " + name
end
end
end
name.strip
end
end
end
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment