opsask 2.0.11 → 2.0.12
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/VERSION +1 -1
- data/lib/opsask/app.rb +3 -424
- data/lib/opsask/helpers.rb +426 -0
- data/public/css/style.css +4 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 64a1996b5ca927b9165bf0b852c8914558154b1c
|
4
|
+
data.tar.gz: 9137f959e9afcd4137cb5db3434f78df3fab3b9a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a9bed853571ccac80880043300a3261d60d404474ce677e0698fb93a50dd886c19466bb098c07c939d4f4637500e9958bfa0f67670fa11ce73eec869a9c9e26d
|
7
|
+
data.tar.gz: c00de6d25a2ff7cf79915601471733b94a5f44cb60bd2fa35de15b902951bf529a4bfaa88fc4ce8c2b7bec1a74a14802b8c1c38a87e502d17e59c4c678433f6c
|
data/Gemfile.lock
CHANGED
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.0.
|
1
|
+
2.0.12
|
data/lib/opsask/app.rb
CHANGED
@@ -5,11 +5,14 @@ require 'rack-flash'
|
|
5
5
|
require 'json'
|
6
6
|
require 'jira'
|
7
7
|
|
8
|
+
require_relative 'helpers'
|
8
9
|
require_relative 'metadata'
|
9
10
|
|
10
11
|
|
11
12
|
module OpsAsk
|
12
13
|
class App < Sinatra::Base
|
14
|
+
include OpsAsk::Helpers
|
15
|
+
|
13
16
|
set :root, OpsAsk::ROOT
|
14
17
|
|
15
18
|
# Add flash support
|
@@ -225,429 +228,5 @@ module OpsAsk
|
|
225
228
|
end
|
226
229
|
end
|
227
230
|
|
228
|
-
|
229
|
-
private
|
230
|
-
def logged_in?
|
231
|
-
!!session[:jira_auth]
|
232
|
-
end
|
233
|
-
|
234
|
-
def ops?
|
235
|
-
return false unless logged_in?
|
236
|
-
@myself['groups']['items'].each do |i|
|
237
|
-
return true if i['name'] == settings.config[:ops_group]
|
238
|
-
end
|
239
|
-
return false
|
240
|
-
end
|
241
|
-
|
242
|
-
def one_day
|
243
|
-
1 * 24 * 60 * 60 # Day * Hour * Minute * Second = Seconds / Day
|
244
|
-
end
|
245
|
-
|
246
|
-
def now
|
247
|
-
Time.now # + 3 * one_day # DEBUG
|
248
|
-
end
|
249
|
-
|
250
|
-
def todays_date offset=0
|
251
|
-
date = now + offset
|
252
|
-
date += one_day if date.saturday?
|
253
|
-
date += one_day if date.sunday?
|
254
|
-
return date
|
255
|
-
end
|
256
|
-
|
257
|
-
def stats_for issues, resolved_link, unresolved_link
|
258
|
-
return {} unless logged_in?
|
259
|
-
return {} unless issues
|
260
|
-
|
261
|
-
resolved_issues, unresolved_issues = [], []
|
262
|
-
|
263
|
-
issues.map! do |i|
|
264
|
-
key = i['key']
|
265
|
-
status = i['fields']['status']['name']
|
266
|
-
resolution = i['fields']['resolution']['name'] rescue nil
|
267
|
-
points = i['fields']['customfield_10002'].to_i
|
268
|
-
|
269
|
-
issue = {
|
270
|
-
key: key,
|
271
|
-
status: status,
|
272
|
-
resolution: resolution,
|
273
|
-
points: points
|
274
|
-
}
|
275
|
-
|
276
|
-
if resolution.nil?
|
277
|
-
unresolved_issues << issue
|
278
|
-
else
|
279
|
-
resolved_issues << issue
|
280
|
-
end
|
281
|
-
end
|
282
|
-
|
283
|
-
{
|
284
|
-
resolved: {
|
285
|
-
number: resolved_issues.size,
|
286
|
-
points: resolved_issues.map { |i| i[:points] }.reduce(0, :+),
|
287
|
-
link: resolved_link
|
288
|
-
},
|
289
|
-
unresolved: {
|
290
|
-
number: unresolved_issues.size,
|
291
|
-
points: unresolved_issues.map { |i| i[:points] }.reduce(0, :+),
|
292
|
-
link: unresolved_link
|
293
|
-
}
|
294
|
-
}
|
295
|
-
end
|
296
|
-
|
297
|
-
def items_in_current_sprint
|
298
|
-
items_in_sprint current_sprint_num
|
299
|
-
end
|
300
|
-
|
301
|
-
def items_in_sprint num
|
302
|
-
return [] unless logged_in?
|
303
|
-
issues = []
|
304
|
-
id = get_sprint(num)['id']
|
305
|
-
query = normalized_jql("sprint = #{id}", nil)
|
306
|
-
@jira_client.Issue.jql(query, max_results: 500).each do |i|
|
307
|
-
issues << i.attrs
|
308
|
-
end
|
309
|
-
return issues
|
310
|
-
end
|
311
|
-
|
312
|
-
def asks_in_current_sprint
|
313
|
-
asks_in_sprint current_sprint_num
|
314
|
-
end
|
315
|
-
|
316
|
-
def asks_in_sprint num
|
317
|
-
return [] unless logged_in?
|
318
|
-
issues = []
|
319
|
-
query = normalized_jql("labels in (Sprint#{num})", nil)
|
320
|
-
@jira_client.Issue.jql(query, max_results: 500).each do |i|
|
321
|
-
issues << i.attrs
|
322
|
-
end
|
323
|
-
return issues
|
324
|
-
end
|
325
|
-
|
326
|
-
def sprints
|
327
|
-
url = "#{settings.config[:jira_url]}/rest/greenhopper/1.0/sprintquery/#{settings.config[:agile_board]}"
|
328
|
-
curl_request = Curl::Easy.http_get(url) do |curl|
|
329
|
-
curl.headers['Accept'] = 'application/json'
|
330
|
-
curl.headers['Content-Type'] = 'application/json'
|
331
|
-
curl.http_auth_types = :basic
|
332
|
-
curl.username = settings.config[:jira_user]
|
333
|
-
curl.password = settings.config[:jira_pass]
|
334
|
-
curl.verbose = true
|
335
|
-
end
|
336
|
-
|
337
|
-
raw_response = curl_request.body_str
|
338
|
-
begin
|
339
|
-
data = JSON::parse(raw_response)
|
340
|
-
return data['sprints']
|
341
|
-
rescue
|
342
|
-
$stderr.puts "Failed to parse response from JIRA: #{raw_response}"
|
343
|
-
end
|
344
|
-
return nil
|
345
|
-
end
|
346
|
-
|
347
|
-
def get_sprint num
|
348
|
-
sprint = sprints.select { |s| s['name'] == "Sprint #{num}" }
|
349
|
-
sprint_id = sprint.first['id']
|
350
|
-
url = "#{settings.config[:jira_url]}/rest/greenhopper/1.0/rapid/charts/sprintreport?rapidViewId=#{settings.config[:agile_board]}&sprintId=#{sprint_id}"
|
351
|
-
curl_request = Curl::Easy.http_get(url) do |curl|
|
352
|
-
curl.headers['Accept'] = 'application/json'
|
353
|
-
curl.headers['Content-Type'] = 'application/json'
|
354
|
-
curl.http_auth_types = :basic
|
355
|
-
curl.username = settings.config[:jira_user]
|
356
|
-
curl.password = settings.config[:jira_pass]
|
357
|
-
curl.verbose = true
|
358
|
-
end
|
359
|
-
|
360
|
-
raw_response = curl_request.body_str
|
361
|
-
begin
|
362
|
-
data = JSON::parse(raw_response)
|
363
|
-
contents = data.delete('contents')
|
364
|
-
data = data.delete('sprint')
|
365
|
-
return data.merge(contents)
|
366
|
-
rescue
|
367
|
-
$stderr.puts "Failed to parse response from JIRA: #{raw_response}"
|
368
|
-
end
|
369
|
-
return {}
|
370
|
-
end
|
371
|
-
|
372
|
-
def current_sprint_name sprint=current_sprint
|
373
|
-
sprint.nil? ? nil : sprint['name'].gsub(/\s+/, '')
|
374
|
-
end
|
375
|
-
|
376
|
-
def current_sprint_num sprint=current_sprint
|
377
|
-
sprint.nil? ? nil : sprint['name'].gsub(/\D+/, '')
|
378
|
-
end
|
379
|
-
|
380
|
-
def current_sprint_id sprint=current_sprint
|
381
|
-
sprint.nil? ? nil : sprint['id']
|
382
|
-
end
|
383
|
-
|
384
|
-
def current_sprint keys=[ 'sprintsData', 'sprints', 0 ]
|
385
|
-
url = "#{settings.config[:jira_url]}/rest/greenhopper/1.0/xboard/work/allData.json?rapidViewId=#{settings.config[:agile_board]}"
|
386
|
-
curl_request = Curl::Easy.http_get(url) do |curl|
|
387
|
-
curl.headers['Accept'] = 'application/json'
|
388
|
-
curl.headers['Content-Type'] = 'application/json'
|
389
|
-
curl.http_auth_types = :basic
|
390
|
-
curl.username = settings.config[:jira_user]
|
391
|
-
curl.password = settings.config[:jira_pass]
|
392
|
-
curl.verbose = true
|
393
|
-
end
|
394
|
-
|
395
|
-
raw_response = curl_request.body_str
|
396
|
-
begin
|
397
|
-
data = JSON::parse(raw_response)
|
398
|
-
keys.each { |k| data = data[k] }
|
399
|
-
return data unless data.nil?
|
400
|
-
rescue
|
401
|
-
$stderr.puts "Failed to parse response from JIRA: #{raw_response}"
|
402
|
-
end
|
403
|
-
|
404
|
-
return sprints.last
|
405
|
-
end
|
406
|
-
|
407
|
-
def today offset=0
|
408
|
-
todays_date(offset).strftime '%Y-%m-%d'
|
409
|
-
end
|
410
|
-
|
411
|
-
def tomorrow
|
412
|
-
today(one_day)
|
413
|
-
end
|
414
|
-
|
415
|
-
def name_for_today offset=0
|
416
|
-
todays_date(offset).strftime '%A %-d %b'
|
417
|
-
end
|
418
|
-
|
419
|
-
def name_for_tomorrow
|
420
|
-
name_for_today(one_day)
|
421
|
-
end
|
422
|
-
|
423
|
-
def name_for_coming_week
|
424
|
-
todays_date.strftime 'Week of %-d %b'
|
425
|
-
end
|
426
|
-
|
427
|
-
def jiras_for date
|
428
|
-
return [] unless logged_in?
|
429
|
-
unless ops?
|
430
|
-
return @jira_client.Issue.jql normalized_jql("due = #{date} and type != Change and labels in (OpsAsk) and labels not in (OpsOnly)"), max_results: 100
|
431
|
-
end
|
432
|
-
return @jira_client.Issue.jql normalized_jql("due = #{date} and labels in (OpsAsk) and type != Change"), max_results: 100
|
433
|
-
end
|
434
|
-
|
435
|
-
def jira_count_for date
|
436
|
-
jiras_for(date).length
|
437
|
-
end
|
438
|
-
|
439
|
-
def jira_count_for_today ; jira_count_for(today) end
|
440
|
-
|
441
|
-
def jira_count_for_tomorrow ; jira_count_for(tomorrow) end
|
442
|
-
|
443
|
-
def raw_classes_for jira
|
444
|
-
classes = [ jira.fields['resolution'].nil? ? 'open' : 'closed' ]
|
445
|
-
classes << jira.fields['assignee']['name'].downcase.gsub(/\W+/, '')
|
446
|
-
end
|
447
|
-
|
448
|
-
def classes_for jira
|
449
|
-
raw_classes_for(jira).join(' ')
|
450
|
-
end
|
451
|
-
|
452
|
-
def sorting_key_for jira
|
453
|
-
rcs = raw_classes_for(jira)
|
454
|
-
idx = 1
|
455
|
-
idx = 2 if rcs.include? 'denimcores'
|
456
|
-
idx = 0 if rcs.include? 'closed'
|
457
|
-
return "#{idx}-#{jira.key}"
|
458
|
-
end
|
459
|
-
|
460
|
-
def issues_for date
|
461
|
-
jiras_for(date).sort_by do |jira|
|
462
|
-
sorting_key_for(jira)
|
463
|
-
end.reverse
|
464
|
-
end
|
465
|
-
|
466
|
-
def its_the_weekend?
|
467
|
-
now.saturday? || now.sunday?
|
468
|
-
end
|
469
|
-
|
470
|
-
def room_for_new_jiras_for? date
|
471
|
-
return true if ops?
|
472
|
-
jira_count_for(date) < settings.config[:queue_size]
|
473
|
-
end
|
474
|
-
|
475
|
-
def date_for_new_jiras
|
476
|
-
if now.hour < settings.config[:cutoff_hour] || its_the_weekend?
|
477
|
-
return today if room_for_new_jiras_for? today
|
478
|
-
return tomorrow if room_for_new_jiras_for? tomorrow
|
479
|
-
else
|
480
|
-
return tomorrow if room_for_new_jiras_for? tomorrow
|
481
|
-
end
|
482
|
-
return nil
|
483
|
-
end
|
484
|
-
|
485
|
-
def room_for_new_jiras?
|
486
|
-
return true if ops?
|
487
|
-
!date_for_new_jiras.nil?
|
488
|
-
end
|
489
|
-
|
490
|
-
def validate_room_for_new_jiras
|
491
|
-
duedate = date_for_new_jiras
|
492
|
-
return duedate unless duedate.nil?
|
493
|
-
flash[:error] = [ "Sorry, there's is no room for new JIRAs" ]
|
494
|
-
redirect '/'
|
495
|
-
end
|
496
|
-
|
497
|
-
def validate_jira_params
|
498
|
-
flash[:error] = []
|
499
|
-
flash[:error] << 'Summary is required' if params['jira-summary'].empty?
|
500
|
-
redirect '/' unless flash[:error].empty?
|
501
|
-
return [
|
502
|
-
params['jira-component'],
|
503
|
-
params['jira-summary'],
|
504
|
-
params['jira-description'],
|
505
|
-
!!params['jira-assign_to_me'],
|
506
|
-
params['jira-epic'],
|
507
|
-
!!params['jira-ops_only']
|
508
|
-
]
|
509
|
-
end
|
510
|
-
|
511
|
-
def create_jira duedate, component, summary, description, assign_to_me, epic, ops_only
|
512
|
-
epic = 'INF-3091' if epic.nil? # OpsAsk default epic
|
513
|
-
assignee = assign_to_me ? @me : settings.config[:assignee]
|
514
|
-
components = []
|
515
|
-
components = [ { name: component } ] unless component
|
516
|
-
labels = [ 'OpsAsk', current_sprint_name ].compact
|
517
|
-
labels << 'OpsOnly' if ops_only
|
518
|
-
labels << settings.config[:require_label] if settings.config[:require_label]
|
519
|
-
data = {
|
520
|
-
fields: {
|
521
|
-
project: { key: settings.config[:project_key] },
|
522
|
-
issuetype: { name: settings.config[:issue_type] },
|
523
|
-
versions: [ { name: settings.config[:version] } ],
|
524
|
-
duedate: duedate,
|
525
|
-
summary: summary,
|
526
|
-
description: description,
|
527
|
-
components: components,
|
528
|
-
assignee: { name: assignee },
|
529
|
-
reporter: { name: @me },
|
530
|
-
labels: labels,
|
531
|
-
customfield_10002: 1, # Story Points = 1
|
532
|
-
# customfield_10350: epic,
|
533
|
-
customfield_10040: { id: '-1' } # Release Priority = None
|
534
|
-
}
|
535
|
-
}
|
536
|
-
|
537
|
-
url = "#{settings.config[:jira_url]}/rest/api/latest/issue"
|
538
|
-
curl_request = Curl::Easy.http_post(url, data.to_json) do |curl|
|
539
|
-
curl.headers['Accept'] = 'application/json'
|
540
|
-
curl.headers['Content-Type'] = 'application/json'
|
541
|
-
curl.http_auth_types = :basic
|
542
|
-
curl.username = settings.config[:jira_user]
|
543
|
-
curl.password = settings.config[:jira_pass]
|
544
|
-
curl.verbose = true
|
545
|
-
end
|
546
|
-
|
547
|
-
raw_response = curl_request.body_str
|
548
|
-
begin
|
549
|
-
response = JSON::parse raw_response
|
550
|
-
rescue
|
551
|
-
$stderr.puts "Failed to parse response from JIRA: #{raw_response}"
|
552
|
-
return nil
|
553
|
-
end
|
554
|
-
return response
|
555
|
-
end
|
556
|
-
|
557
|
-
def components
|
558
|
-
return @project.components.map(&:name).select { |c| c =~ /^Ops/ }
|
559
|
-
end
|
560
|
-
|
561
|
-
def untracked_issues
|
562
|
-
return [] unless logged_in?
|
563
|
-
constraints = [
|
564
|
-
"due < #{today}",
|
565
|
-
"resolution = unresolved",
|
566
|
-
"assignee = denimcores"
|
567
|
-
].join(' and ')
|
568
|
-
@jira_client.Issue.jql(normalized_jql(constraints), max_results: 100).sort_by do |jira|
|
569
|
-
sorting_key_for(jira)
|
570
|
-
end.reverse
|
571
|
-
end
|
572
|
-
|
573
|
-
def straggling_issues
|
574
|
-
return [] unless logged_in?
|
575
|
-
constraints = [
|
576
|
-
"due < #{today}",
|
577
|
-
"labels in (OpsAsk)",
|
578
|
-
"resolution = unresolved",
|
579
|
-
"assignee != denimcores"
|
580
|
-
].join(' and ')
|
581
|
-
@jira_client.Issue.jql(normalized_jql(constraints), max_results: 100).sort_by do |jira|
|
582
|
-
sorting_key_for(jira)
|
583
|
-
end.reverse
|
584
|
-
end
|
585
|
-
|
586
|
-
def epics
|
587
|
-
data = {
|
588
|
-
jql: normalized_jql("type = Epic"),
|
589
|
-
startAt: 0,
|
590
|
-
maxResults: 1000
|
591
|
-
}
|
592
|
-
|
593
|
-
url = "#{settings.config[:jira_url]}/rest/api/latest/search"
|
594
|
-
curl_request = Curl::Easy.http_post(url, data.to_json) do |curl|
|
595
|
-
curl.headers['Accept'] = 'application/json'
|
596
|
-
curl.headers['Content-Type'] = 'application/json'
|
597
|
-
curl.http_auth_types = :basic
|
598
|
-
curl.username = settings.config[:jira_user]
|
599
|
-
curl.password = settings.config[:jira_pass]
|
600
|
-
curl.verbose = true
|
601
|
-
end
|
602
|
-
|
603
|
-
raw_response = curl_request.body_str
|
604
|
-
begin
|
605
|
-
response = JSON::parse raw_response
|
606
|
-
rescue
|
607
|
-
$stderr.puts "Failed to parse response from JIRA: #{raw_response}"
|
608
|
-
return nil
|
609
|
-
end
|
610
|
-
return response['issues'].map do |epic|
|
611
|
-
{
|
612
|
-
'key' => epic['key'],
|
613
|
-
'name' => epic['fields']['customfield_10351'] || epic['fields']['summary']
|
614
|
-
}
|
615
|
-
end
|
616
|
-
end
|
617
|
-
|
618
|
-
def epic key
|
619
|
-
url = "#{settings.config[:jira_url]}/rest/api/latest/issue/#{key}"
|
620
|
-
curl_request = Curl::Easy.http_get(url) do |curl|
|
621
|
-
curl.headers['Accept'] = 'application/json'
|
622
|
-
curl.headers['Content-Type'] = 'application/json'
|
623
|
-
curl.http_auth_types = :basic
|
624
|
-
curl.username = settings.config[:jira_user]
|
625
|
-
curl.password = settings.config[:jira_pass]
|
626
|
-
curl.verbose = true
|
627
|
-
end
|
628
|
-
|
629
|
-
raw_response = curl_request.body_str
|
630
|
-
begin
|
631
|
-
response = JSON::parse raw_response
|
632
|
-
rescue
|
633
|
-
$stderr.puts "Failed to parse response from JIRA: #{raw_response}"
|
634
|
-
return nil
|
635
|
-
end
|
636
|
-
return {
|
637
|
-
'key' => response['key'],
|
638
|
-
'name' => response['fields']['customfield_10351'] || response['fields']['summary']
|
639
|
-
}
|
640
|
-
end
|
641
|
-
|
642
|
-
def normalized_jql query, \
|
643
|
-
project=settings.config[:project_name], \
|
644
|
-
require_label=settings.config[:require_label],
|
645
|
-
ignore_label=settings.config[:ignore_label]
|
646
|
-
# ...
|
647
|
-
query += %Q| and project = #{project}| if project
|
648
|
-
query += %Q| and labels = #{require_label}| if require_label
|
649
|
-
query += %Q| and (labels != #{ignore_label} OR labels is empty)| if ignore_label
|
650
|
-
return query
|
651
|
-
end
|
652
231
|
end
|
653
232
|
end
|
@@ -0,0 +1,426 @@
|
|
1
|
+
module OpsAsk
|
2
|
+
module Helpers
|
3
|
+
def logged_in?
|
4
|
+
!!session[:jira_auth]
|
5
|
+
end
|
6
|
+
|
7
|
+
def ops?
|
8
|
+
return false unless logged_in?
|
9
|
+
@myself['groups']['items'].each do |i|
|
10
|
+
return true if i['name'] == settings.config[:ops_group]
|
11
|
+
end
|
12
|
+
return false
|
13
|
+
end
|
14
|
+
|
15
|
+
def one_day
|
16
|
+
1 * 24 * 60 * 60 # Day * Hour * Minute * Second = Seconds / Day
|
17
|
+
end
|
18
|
+
|
19
|
+
def now
|
20
|
+
Time.now # + 3 * one_day # DEBUG
|
21
|
+
end
|
22
|
+
|
23
|
+
def todays_date offset=0
|
24
|
+
date = now + offset
|
25
|
+
date += one_day if date.saturday?
|
26
|
+
date += one_day if date.sunday?
|
27
|
+
return date
|
28
|
+
end
|
29
|
+
|
30
|
+
def stats_for issues, resolved_link, unresolved_link
|
31
|
+
return {} unless logged_in?
|
32
|
+
return {} unless issues
|
33
|
+
|
34
|
+
resolved_issues, unresolved_issues = [], []
|
35
|
+
|
36
|
+
issues.map! do |i|
|
37
|
+
key = i['key']
|
38
|
+
status = i['fields']['status']['name']
|
39
|
+
resolution = i['fields']['resolution']['name'] rescue nil
|
40
|
+
points = i['fields']['customfield_10002'].to_i
|
41
|
+
|
42
|
+
issue = {
|
43
|
+
key: key,
|
44
|
+
status: status,
|
45
|
+
resolution: resolution,
|
46
|
+
points: points
|
47
|
+
}
|
48
|
+
|
49
|
+
if resolution.nil?
|
50
|
+
unresolved_issues << issue
|
51
|
+
else
|
52
|
+
resolved_issues << issue
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
{
|
57
|
+
resolved: {
|
58
|
+
number: resolved_issues.size,
|
59
|
+
points: resolved_issues.map { |i| i[:points] }.reduce(0, :+),
|
60
|
+
link: resolved_link
|
61
|
+
},
|
62
|
+
unresolved: {
|
63
|
+
number: unresolved_issues.size,
|
64
|
+
points: unresolved_issues.map { |i| i[:points] }.reduce(0, :+),
|
65
|
+
link: unresolved_link
|
66
|
+
}
|
67
|
+
}
|
68
|
+
end
|
69
|
+
|
70
|
+
def items_in_current_sprint
|
71
|
+
items_in_sprint current_sprint_num
|
72
|
+
end
|
73
|
+
|
74
|
+
def items_in_sprint num
|
75
|
+
return [] unless logged_in?
|
76
|
+
issues = []
|
77
|
+
id = get_sprint(num)['id']
|
78
|
+
query = normalized_jql("sprint = #{id}", nil)
|
79
|
+
@jira_client.Issue.jql(query, max_results: 500).each do |i|
|
80
|
+
issues << i.attrs
|
81
|
+
end
|
82
|
+
return issues
|
83
|
+
end
|
84
|
+
|
85
|
+
def asks_in_current_sprint
|
86
|
+
asks_in_sprint current_sprint_num
|
87
|
+
end
|
88
|
+
|
89
|
+
def asks_in_sprint num
|
90
|
+
return [] unless logged_in?
|
91
|
+
issues = []
|
92
|
+
query = normalized_jql("labels in (Sprint#{num})", nil)
|
93
|
+
@jira_client.Issue.jql(query, max_results: 500).each do |i|
|
94
|
+
issues << i.attrs
|
95
|
+
end
|
96
|
+
return issues
|
97
|
+
end
|
98
|
+
|
99
|
+
def sprints
|
100
|
+
url = "#{settings.config[:jira_url]}/rest/greenhopper/1.0/sprintquery/#{settings.config[:agile_board]}"
|
101
|
+
curl_request = Curl::Easy.http_get(url) do |curl|
|
102
|
+
curl.headers['Accept'] = 'application/json'
|
103
|
+
curl.headers['Content-Type'] = 'application/json'
|
104
|
+
curl.http_auth_types = :basic
|
105
|
+
curl.username = settings.config[:jira_user]
|
106
|
+
curl.password = settings.config[:jira_pass]
|
107
|
+
curl.verbose = true
|
108
|
+
end
|
109
|
+
|
110
|
+
raw_response = curl_request.body_str
|
111
|
+
begin
|
112
|
+
data = JSON::parse(raw_response)
|
113
|
+
return data['sprints']
|
114
|
+
rescue
|
115
|
+
$stderr.puts "Failed to parse response from JIRA: #{raw_response}"
|
116
|
+
end
|
117
|
+
return nil
|
118
|
+
end
|
119
|
+
|
120
|
+
def get_sprint num
|
121
|
+
sprint = sprints.select { |s| s['name'] == "Sprint #{num}" }
|
122
|
+
sprint_id = sprint.first['id']
|
123
|
+
url = "#{settings.config[:jira_url]}/rest/greenhopper/1.0/rapid/charts/sprintreport?rapidViewId=#{settings.config[:agile_board]}&sprintId=#{sprint_id}"
|
124
|
+
curl_request = Curl::Easy.http_get(url) do |curl|
|
125
|
+
curl.headers['Accept'] = 'application/json'
|
126
|
+
curl.headers['Content-Type'] = 'application/json'
|
127
|
+
curl.http_auth_types = :basic
|
128
|
+
curl.username = settings.config[:jira_user]
|
129
|
+
curl.password = settings.config[:jira_pass]
|
130
|
+
curl.verbose = true
|
131
|
+
end
|
132
|
+
|
133
|
+
raw_response = curl_request.body_str
|
134
|
+
begin
|
135
|
+
data = JSON::parse(raw_response)
|
136
|
+
contents = data.delete('contents')
|
137
|
+
data = data.delete('sprint')
|
138
|
+
return data.merge(contents)
|
139
|
+
rescue
|
140
|
+
$stderr.puts "Failed to parse response from JIRA: #{raw_response}"
|
141
|
+
end
|
142
|
+
return {}
|
143
|
+
end
|
144
|
+
|
145
|
+
def current_sprint_name sprint=current_sprint
|
146
|
+
sprint.nil? ? nil : sprint['name'].gsub(/\s+/, '')
|
147
|
+
end
|
148
|
+
|
149
|
+
def current_sprint_num sprint=current_sprint
|
150
|
+
sprint.nil? ? nil : sprint['name'].gsub(/\D+/, '')
|
151
|
+
end
|
152
|
+
|
153
|
+
def current_sprint_id sprint=current_sprint
|
154
|
+
sprint.nil? ? nil : sprint['id']
|
155
|
+
end
|
156
|
+
|
157
|
+
def current_sprint keys=[ 'sprintsData', 'sprints', 0 ]
|
158
|
+
url = "#{settings.config[:jira_url]}/rest/greenhopper/1.0/xboard/work/allData.json?rapidViewId=#{settings.config[:agile_board]}"
|
159
|
+
curl_request = Curl::Easy.http_get(url) do |curl|
|
160
|
+
curl.headers['Accept'] = 'application/json'
|
161
|
+
curl.headers['Content-Type'] = 'application/json'
|
162
|
+
curl.http_auth_types = :basic
|
163
|
+
curl.username = settings.config[:jira_user]
|
164
|
+
curl.password = settings.config[:jira_pass]
|
165
|
+
curl.verbose = true
|
166
|
+
end
|
167
|
+
|
168
|
+
raw_response = curl_request.body_str
|
169
|
+
begin
|
170
|
+
data = JSON::parse(raw_response)
|
171
|
+
keys.each { |k| data = data[k] }
|
172
|
+
return data unless data.nil?
|
173
|
+
rescue
|
174
|
+
$stderr.puts "Failed to parse response from JIRA: #{raw_response}"
|
175
|
+
end
|
176
|
+
|
177
|
+
return sprints.last
|
178
|
+
end
|
179
|
+
|
180
|
+
def today offset=0
|
181
|
+
todays_date(offset).strftime '%Y-%m-%d'
|
182
|
+
end
|
183
|
+
|
184
|
+
def tomorrow
|
185
|
+
today(one_day)
|
186
|
+
end
|
187
|
+
|
188
|
+
def name_for_today offset=0
|
189
|
+
todays_date(offset).strftime '%A %-d %b'
|
190
|
+
end
|
191
|
+
|
192
|
+
def name_for_tomorrow
|
193
|
+
name_for_today(one_day)
|
194
|
+
end
|
195
|
+
|
196
|
+
def name_for_coming_week
|
197
|
+
todays_date.strftime 'Week of %-d %b'
|
198
|
+
end
|
199
|
+
|
200
|
+
def jiras_for date
|
201
|
+
return [] unless logged_in?
|
202
|
+
unless ops?
|
203
|
+
return @jira_client.Issue.jql normalized_jql("due = #{date} and type != Change and labels in (OpsAsk) and labels not in (OpsOnly)"), max_results: 100
|
204
|
+
end
|
205
|
+
return @jira_client.Issue.jql normalized_jql("due = #{date} and labels in (OpsAsk) and type != Change"), max_results: 100
|
206
|
+
end
|
207
|
+
|
208
|
+
def jira_count_for date
|
209
|
+
jiras_for(date).length
|
210
|
+
end
|
211
|
+
|
212
|
+
def jira_count_for_today ; jira_count_for(today) end
|
213
|
+
|
214
|
+
def jira_count_for_tomorrow ; jira_count_for(tomorrow) end
|
215
|
+
|
216
|
+
def raw_classes_for jira
|
217
|
+
classes = [ jira.fields['resolution'].nil? ? 'open' : 'closed' ]
|
218
|
+
classes << jira.fields['assignee']['name'].downcase.gsub(/\W+/, '')
|
219
|
+
end
|
220
|
+
|
221
|
+
def classes_for jira
|
222
|
+
raw_classes_for(jira).join(' ')
|
223
|
+
end
|
224
|
+
|
225
|
+
def sorting_key_for jira
|
226
|
+
rcs = raw_classes_for(jira)
|
227
|
+
idx = 1
|
228
|
+
idx = 2 if rcs.include? 'denimcores'
|
229
|
+
idx = 0 if rcs.include? 'closed'
|
230
|
+
return "#{idx}-#{jira.key}"
|
231
|
+
end
|
232
|
+
|
233
|
+
def issues_for date
|
234
|
+
jiras_for(date).sort_by do |jira|
|
235
|
+
sorting_key_for(jira)
|
236
|
+
end.reverse
|
237
|
+
end
|
238
|
+
|
239
|
+
def its_the_weekend?
|
240
|
+
now.saturday? || now.sunday?
|
241
|
+
end
|
242
|
+
|
243
|
+
def room_for_new_jiras_for? date
|
244
|
+
return true if ops?
|
245
|
+
jira_count_for(date) < settings.config[:queue_size]
|
246
|
+
end
|
247
|
+
|
248
|
+
def date_for_new_jiras
|
249
|
+
if now.hour < settings.config[:cutoff_hour] || its_the_weekend?
|
250
|
+
return today if room_for_new_jiras_for? today
|
251
|
+
return tomorrow if room_for_new_jiras_for? tomorrow
|
252
|
+
else
|
253
|
+
return tomorrow if room_for_new_jiras_for? tomorrow
|
254
|
+
end
|
255
|
+
return nil
|
256
|
+
end
|
257
|
+
|
258
|
+
def room_for_new_jiras?
|
259
|
+
return true if ops?
|
260
|
+
!date_for_new_jiras.nil?
|
261
|
+
end
|
262
|
+
|
263
|
+
def validate_room_for_new_jiras
|
264
|
+
duedate = date_for_new_jiras
|
265
|
+
return duedate unless duedate.nil?
|
266
|
+
flash[:error] = [ "Sorry, there's is no room for new JIRAs" ]
|
267
|
+
redirect '/'
|
268
|
+
end
|
269
|
+
|
270
|
+
def validate_jira_params
|
271
|
+
flash[:error] = []
|
272
|
+
flash[:error] << 'Summary is required' if params['jira-summary'].empty?
|
273
|
+
redirect '/' unless flash[:error].empty?
|
274
|
+
return [
|
275
|
+
params['jira-component'],
|
276
|
+
params['jira-summary'],
|
277
|
+
params['jira-description'],
|
278
|
+
!!params['jira-assign_to_me'],
|
279
|
+
params['jira-epic'],
|
280
|
+
!!params['jira-ops_only']
|
281
|
+
]
|
282
|
+
end
|
283
|
+
|
284
|
+
def create_jira duedate, component, summary, description, assign_to_me, epic, ops_only
|
285
|
+
epic = 'INF-3091' if epic.nil? # OpsAsk default epic
|
286
|
+
assignee = assign_to_me ? @me : settings.config[:assignee]
|
287
|
+
components = []
|
288
|
+
components = [ { name: component } ] unless component
|
289
|
+
labels = [ 'OpsAsk', current_sprint_name ].compact
|
290
|
+
labels << 'OpsOnly' if ops_only
|
291
|
+
labels << settings.config[:require_label] if settings.config[:require_label]
|
292
|
+
data = {
|
293
|
+
fields: {
|
294
|
+
project: { key: settings.config[:project_key] },
|
295
|
+
issuetype: { name: settings.config[:issue_type] },
|
296
|
+
versions: [ { name: settings.config[:version] } ],
|
297
|
+
duedate: duedate,
|
298
|
+
summary: summary,
|
299
|
+
description: description,
|
300
|
+
components: components,
|
301
|
+
assignee: { name: assignee },
|
302
|
+
reporter: { name: @me },
|
303
|
+
labels: labels,
|
304
|
+
customfield_10002: 1, # Story Points = 1
|
305
|
+
# customfield_10350: epic,
|
306
|
+
customfield_10040: { id: '-1' } # Release Priority = None
|
307
|
+
}
|
308
|
+
}
|
309
|
+
|
310
|
+
url = "#{settings.config[:jira_url]}/rest/api/latest/issue"
|
311
|
+
curl_request = Curl::Easy.http_post(url, data.to_json) do |curl|
|
312
|
+
curl.headers['Accept'] = 'application/json'
|
313
|
+
curl.headers['Content-Type'] = 'application/json'
|
314
|
+
curl.http_auth_types = :basic
|
315
|
+
curl.username = settings.config[:jira_user]
|
316
|
+
curl.password = settings.config[:jira_pass]
|
317
|
+
curl.verbose = true
|
318
|
+
end
|
319
|
+
|
320
|
+
raw_response = curl_request.body_str
|
321
|
+
begin
|
322
|
+
response = JSON::parse raw_response
|
323
|
+
rescue
|
324
|
+
$stderr.puts "Failed to parse response from JIRA: #{raw_response}"
|
325
|
+
return nil
|
326
|
+
end
|
327
|
+
return response
|
328
|
+
end
|
329
|
+
|
330
|
+
def components
|
331
|
+
return @project.components.map(&:name).select { |c| c =~ /^Ops/ }
|
332
|
+
end
|
333
|
+
|
334
|
+
def untracked_issues
|
335
|
+
return [] unless logged_in?
|
336
|
+
constraints = [
|
337
|
+
"due < #{today}",
|
338
|
+
"resolution = unresolved",
|
339
|
+
"assignee = denimcores"
|
340
|
+
].join(' and ')
|
341
|
+
@jira_client.Issue.jql(normalized_jql(constraints), max_results: 100).sort_by do |jira|
|
342
|
+
sorting_key_for(jira)
|
343
|
+
end.reverse
|
344
|
+
end
|
345
|
+
|
346
|
+
def straggling_issues
|
347
|
+
return [] unless logged_in?
|
348
|
+
constraints = [
|
349
|
+
"due < #{today}",
|
350
|
+
"labels in (OpsAsk)",
|
351
|
+
"resolution = unresolved",
|
352
|
+
"assignee != denimcores"
|
353
|
+
].join(' and ')
|
354
|
+
@jira_client.Issue.jql(normalized_jql(constraints), max_results: 100).sort_by do |jira|
|
355
|
+
sorting_key_for(jira)
|
356
|
+
end.reverse
|
357
|
+
end
|
358
|
+
|
359
|
+
def epics
|
360
|
+
data = {
|
361
|
+
jql: normalized_jql("type = Epic"),
|
362
|
+
startAt: 0,
|
363
|
+
maxResults: 1000
|
364
|
+
}
|
365
|
+
|
366
|
+
url = "#{settings.config[:jira_url]}/rest/api/latest/search"
|
367
|
+
curl_request = Curl::Easy.http_post(url, data.to_json) do |curl|
|
368
|
+
curl.headers['Accept'] = 'application/json'
|
369
|
+
curl.headers['Content-Type'] = 'application/json'
|
370
|
+
curl.http_auth_types = :basic
|
371
|
+
curl.username = settings.config[:jira_user]
|
372
|
+
curl.password = settings.config[:jira_pass]
|
373
|
+
curl.verbose = true
|
374
|
+
end
|
375
|
+
|
376
|
+
raw_response = curl_request.body_str
|
377
|
+
begin
|
378
|
+
response = JSON::parse raw_response
|
379
|
+
rescue
|
380
|
+
$stderr.puts "Failed to parse response from JIRA: #{raw_response}"
|
381
|
+
return nil
|
382
|
+
end
|
383
|
+
return response['issues'].map do |epic|
|
384
|
+
{
|
385
|
+
'key' => epic['key'],
|
386
|
+
'name' => epic['fields']['customfield_10351'] || epic['fields']['summary']
|
387
|
+
}
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
def epic key
|
392
|
+
url = "#{settings.config[:jira_url]}/rest/api/latest/issue/#{key}"
|
393
|
+
curl_request = Curl::Easy.http_get(url) do |curl|
|
394
|
+
curl.headers['Accept'] = 'application/json'
|
395
|
+
curl.headers['Content-Type'] = 'application/json'
|
396
|
+
curl.http_auth_types = :basic
|
397
|
+
curl.username = settings.config[:jira_user]
|
398
|
+
curl.password = settings.config[:jira_pass]
|
399
|
+
curl.verbose = true
|
400
|
+
end
|
401
|
+
|
402
|
+
raw_response = curl_request.body_str
|
403
|
+
begin
|
404
|
+
response = JSON::parse raw_response
|
405
|
+
rescue
|
406
|
+
$stderr.puts "Failed to parse response from JIRA: #{raw_response}"
|
407
|
+
return nil
|
408
|
+
end
|
409
|
+
return {
|
410
|
+
'key' => response['key'],
|
411
|
+
'name' => response['fields']['customfield_10351'] || response['fields']['summary']
|
412
|
+
}
|
413
|
+
end
|
414
|
+
|
415
|
+
def normalized_jql query, \
|
416
|
+
project=settings.config[:project_name], \
|
417
|
+
require_label=settings.config[:require_label],
|
418
|
+
ignore_label=settings.config[:ignore_label]
|
419
|
+
# ...
|
420
|
+
query += %Q| and project = #{project}| if project
|
421
|
+
query += %Q| and labels = #{require_label}| if require_label
|
422
|
+
query += %Q| and (labels != #{ignore_label} OR labels is empty)| if ignore_label
|
423
|
+
return query
|
424
|
+
end
|
425
|
+
end
|
426
|
+
end
|
data/public/css/style.css
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: opsask
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0.
|
4
|
+
version: 2.0.12
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sean Clemmer
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2015-01-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|
@@ -139,6 +139,7 @@ files:
|
|
139
139
|
- bin/opsask
|
140
140
|
- lib/opsask.rb
|
141
141
|
- lib/opsask/app.rb
|
142
|
+
- lib/opsask/helpers.rb
|
142
143
|
- lib/opsask/main.rb
|
143
144
|
- lib/opsask/metadata.rb
|
144
145
|
- opsask.gemspec
|