module OpsAsk module Helpers def logged_in? !!session[:jira_auth] end def no_ops jiras jiras.reject do |jira| jira.fields['labels'].include? 'OpsOnly' end end def ops? return false unless logged_in? @myself['groups']['items'].each do |i| return true if i['name'] == settings.config[:ops_group] end return false end def one_day 1 * 24 * 60 * 60 # Day * Hour * Minute * Second = Seconds / Day end def now Time.now # + 3 * one_day # DEBUG end def todays_date offset=0 date = now + offset date += one_day if date.saturday? date += one_day if date.sunday? return date end def stats_for issues, resolved_link, unresolved_link, unsized_link return {} unless logged_in? return {} unless issues unsized_issues, resolved_issues, unresolved_issues = [], [], [] issues.map! do |i| key = i['key'] status = i['fields']['status']['name'] resolution = i['fields']['resolution']['name'] rescue nil points = i['fields']['customfield_10002'].to_i issue = { key: key, status: status, resolution: resolution, points: points } if i['fields']['customfield_10002'].nil? unsized_issues << issue end if resolution.nil? unresolved_issues << issue else resolved_issues << issue end end { resolved: { number: resolved_issues.size, points: resolved_issues.map { |i| i[:points] }.reduce(0, :+), link: resolved_link }, unresolved: { number: unresolved_issues.size, points: unresolved_issues.map { |i| i[:points] }.reduce(0, :+), link: unresolved_link }, unsized: { number: unsized_issues.size, link: unsized_link } } end def outlier_query sprint, num starts = current_sprint_starts sprint ends = current_sprint_ends sprint normalized_jql("(sprint != #{sprint['id']} or sprint is empty) and (labels != \"#{sprint_label_prefix}#{num}\" OR labels is empty) and resolved >= \"#{starts}\" and resolved <= \"#{ends}\"") end def outlier_link sprint, num "#{settings.config[:jira_url]}/issues/?jql=#{URI::escape outlier_query(sprint, num)}" end def outlier_issues sprint, num return [] unless logged_in? issues = [] @jira_client.Issue.jql(outlier_query(sprint, num), max_results: 500).each do |i| issues << i.attrs end return issues end def items_in_current_sprint items_in_sprint current_sprint_num end def items_in_sprint num return [] unless logged_in? issues = [] id = get_sprint(num)['id'] query = normalized_jql("sprint = #{id}", project: nil) @jira_client.Issue.jql(query, max_results: 500).each do |i| issues << i.attrs end return issues end def asks_in_current_sprint asks_in_sprint current_sprint_num end def asks_in_sprint num return [] unless logged_in? issues = [] query = normalized_jql("labels in (#{sprint_label_prefix}#{num})", project: nil) @jira_client.Issue.jql(query, max_results: 500).each do |i| issues << i.attrs end return issues end def sprints url = "#{settings.config[:jira_url]}/rest/greenhopper/1.0/sprintquery/#{settings.config[:agile_board]}" curl_request = Curl::Easy.http_get(url) do |curl| curl.headers['Accept'] = 'application/json' curl.headers['Content-Type'] = 'application/json' curl.http_auth_types = :basic curl.username = settings.config[:jira_user] curl.password = settings.config[:jira_pass] curl.verbose = true end raw_response = curl_request.body_str begin data = JSON::parse(raw_response) return data['sprints'] rescue $stderr.puts "Failed to parse response from JIRA: #{raw_response}" end return nil end def sprint_prefix settings.config[:sprint_prefix] ||= 'Sprint' end def sprint_label_prefix sprint_prefix.gsub(/\s+/, '') end def get_sprint num, id=nil unless sprint_id = id sprint = sprints.select { |s| s['name'] == "#{sprint_prefix} #{num}" } sprint_id = sprint.first['id'] end url = "#{settings.config[:jira_url]}/rest/greenhopper/1.0/rapid/charts/sprintreport?rapidViewId=#{settings.config[:agile_board]}&sprintId=#{sprint_id}" curl_request = Curl::Easy.http_get(url) do |curl| curl.headers['Accept'] = 'application/json' curl.headers['Content-Type'] = 'application/json' curl.http_auth_types = :basic curl.username = settings.config[:jira_user] curl.password = settings.config[:jira_pass] curl.verbose = true end raw_response = curl_request.body_str begin data = JSON::parse(raw_response) contents = data.delete('contents') data = data.delete('sprint') return data.merge(contents) rescue $stderr.puts "Failed to parse response from JIRA: #{raw_response}" end return {} end def current_sprint_starts sprint=current_sprint if starts = sprint['startDate'] starts = DateTime.strptime(starts, '%d/%b/%y %l:%M %p') starts.strftime('%Y/%m/%d') end end def current_sprint_ends sprint=current_sprint if ends = sprint['endDate'] ends = DateTime.strptime(ends, '%d/%b/%y %l:%M %p') ends.strftime('%Y/%m/%d') end end def current_sprint_name sprint=current_sprint sprint.nil? ? nil : sprint['name'].gsub(/\s+/, '') end def current_sprint_num sprint=current_sprint sprint.nil? ? nil : sprint['name'].gsub(/\D+/, '') end def current_sprint_id sprint=current_sprint sprint.nil? ? nil : sprint['id'] end def current_sprint keys=[ 'sprintsData', 'sprints', 0 ] url = "#{settings.config[:jira_url]}/rest/greenhopper/1.0/xboard/work/allData.json?rapidViewId=#{settings.config[:agile_board]}" curl_request = Curl::Easy.http_get(url) do |curl| curl.headers['Accept'] = 'application/json' curl.headers['Content-Type'] = 'application/json' curl.http_auth_types = :basic curl.username = settings.config[:jira_user] curl.password = settings.config[:jira_pass] curl.verbose = true end raw_response = curl_request.body_str begin data = JSON::parse(raw_response) keys.each { |k| data = data[k] } return data unless data.nil? rescue $stderr.puts "Failed to parse response from JIRA: #{raw_response}" end get_sprint(nil, sprints.last['id']) end def today offset=0 todays_date(offset).strftime '%Y-%m-%d' end def tomorrow today(one_day) end def name_for_today offset=0 todays_date(offset).strftime '%A %-d %b' end def name_for_tomorrow name_for_today(one_day) end def name_for_coming_week todays_date.strftime 'Week of %-d %b' end def jiras_for date, really=false return [] unless logged_in? if !ops? || really @jira_client.Issue.jql normalized_jql("due = #{date} and labels in (OpsAsk) and labels not in (OpsOnly)"), max_results: 100 else @jira_client.Issue.jql normalized_jql("due = #{date} and labels in (OpsAsk)"), max_results: 100 end end def jira_count_for date, really=false jiras_for(date, really).length end def jira_count_for_today really=false ; jira_count_for(today, really) end def jira_count_for_tomorrow really=false ; jira_count_for(tomorrow, really) end def raw_classes_for jira classes = [ jira.fields['resolution'].nil? ? 'open' : 'closed' ] classes << jira.fields['assignee']['name'].downcase.gsub(/\W+/, '') end def classes_for jira raw_classes_for(jira).join(' ') end def sorting_key_for jira rcs = raw_classes_for(jira) idx = 1 idx = 2 if rcs.include? 'denimcores' idx = 0 if rcs.include? 'closed' return "#{idx}-#{jira.key}" end def today? date Time.new.to_date.strftime('%Y-%m-%d') == date end def issues_for_days n, start=0 now = Time.new.to_date last = start + n start.upto(last-1).inject({}) do |h,i| date = (now + i).strftime('%Y-%m-%d') h[date] = jiras_for(date).sort_by do |jira| sorting_key_for(jira) end.reverse h end end def issues_for date jiras_for(date).sort_by do |jira| sorting_key_for(jira) end.reverse end def its_the_weekend? now.saturday? || now.sunday? end def room_for_new_jiras_for? date, really=false return true if params[:force] return false if params[:full] return true if ops? && !really jira_count_for(date, really) < settings.config[:queue_size] end def date_for_new_jiras really=false if now.hour < settings.config[:cutoff_hour] || its_the_weekend? return today if room_for_new_jiras_for? today, really end # Remove the two-day limit [OPS-1862] n = 1 # day, i.e. tomorrow loop do next_day = today n * one_day return next_day if room_for_new_jiras_for? next_day, really end end def validate_jira_params flash[:error] = [] flash[:error] << 'Summary is required' if params['jira-summary'].empty? redirect '/' unless flash[:error].empty? return [ params['jira-component'], params['jira-summary'], params['jira-description'], !!params['jira-assign_to_me'], nil, !!params['jira-ops_only'], params['jira-datepicker'] ] end def create_jira duedate, component, summary, description, assign_to_me, epic, ops_only assignee = assign_to_me ? @me : settings.config[:assignee] components = [] components = [ { name: component } ] unless component labels = [ 'OpsAsk', current_sprint_name ].compact labels << 'OpsOnly' if ops_only labels << settings.config[:require_label] if settings.config[:require_label] data = { fields: { project: { key: settings.config[:project_key] }, issuetype: { name: settings.config[:issue_type] }, versions: [ { name: settings.config[:version] } ], duedate: duedate, summary: summary, description: description, components: components, assignee: { name: assignee }, reporter: { name: @me }, labels: labels, customfield_10002: 1, # Story Points = 1 customfield_10040: { id: '-1' } # Release Priority = None } } url = "#{settings.config[:jira_url]}/rest/api/latest/issue" curl_request = Curl::Easy.http_post(url, data.to_json) do |curl| curl.headers['Accept'] = 'application/json' curl.headers['Content-Type'] = 'application/json' curl.http_auth_types = :basic curl.username = settings.config[:jira_user] curl.password = settings.config[:jira_pass] curl.verbose = true end raw_response = curl_request.body_str begin response = JSON::parse raw_response rescue $stderr.puts "Failed to parse response from JIRA: #{raw_response}" return nil end return response end def components return @project.components.map(&:name).select { |c| c =~ /^Ops/ } end def untracked_issues return [] unless logged_in? constraints = [ "due < #{today}", "resolution = unresolved", "assignee = denimcores" ].join(' and ') @jira_client.Issue.jql(normalized_jql(constraints), max_results: 100).sort_by do |jira| sorting_key_for(jira) end.reverse end def straggling_issues return [] unless logged_in? constraints = [ "due < #{today}", "labels in (OpsAsk)", "resolution = unresolved", "assignee != denimcores" ].join(' and ') @jira_client.Issue.jql(normalized_jql(constraints), max_results: 100).sort_by do |jira| sorting_key_for(jira) end.reverse end def epics data = { jql: normalized_jql("type = Epic"), startAt: 0, maxResults: 1000 } url = "#{settings.config[:jira_url]}/rest/api/latest/search" curl_request = Curl::Easy.http_post(url, data.to_json) do |curl| curl.headers['Accept'] = 'application/json' curl.headers['Content-Type'] = 'application/json' curl.http_auth_types = :basic curl.username = settings.config[:jira_user] curl.password = settings.config[:jira_pass] curl.verbose = true end raw_response = curl_request.body_str begin response = JSON::parse raw_response rescue $stderr.puts "Failed to parse response from JIRA: #{raw_response}" return nil end return response['issues'].map do |epic| { 'key' => epic['key'], 'name' => epic['fields']['customfield_10351'] || epic['fields']['summary'] } end end def normalized_jql query, \ project: settings.config[:project_name], \ require_label: settings.config[:require_label], ignore_label: settings.config[:ignore_label] # ... query += %Q| and project = #{project}| if project query += %Q| and labels = #{require_label}| if require_label query += %Q| and (labels != #{ignore_label} OR labels is empty)| if ignore_label return query end end end