opsask 2.0.11 → 2.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 86cc1427b2aa693c495ece2fec5fb897343d29c2
4
- data.tar.gz: e6dfba89cafdafd3739f5065a1694f525ba6d6d0
3
+ metadata.gz: 64a1996b5ca927b9165bf0b852c8914558154b1c
4
+ data.tar.gz: 9137f959e9afcd4137cb5db3434f78df3fab3b9a
5
5
  SHA512:
6
- metadata.gz: e66966205436bc1883304d327b65b21fc47832361c1dccd0691179b0b158a6c164508a1aef7a9f17b473b18eb02dbf5b5890542dd881af250c2bb230f1a98e32
7
- data.tar.gz: d0c217ad5384e9686bcbca7b965e5a9661f7afaad0247ad5613771c9afd195b840f71075995dc26b6586363d3d3b804cc5ce68cc4fd6f12c56cb8283f9a8ffc2
6
+ metadata.gz: a9bed853571ccac80880043300a3261d60d404474ce677e0698fb93a50dd886c19466bb098c07c939d4f4637500e9958bfa0f67670fa11ce73eec869a9c9e26d
7
+ data.tar.gz: c00de6d25a2ff7cf79915601471733b94a5f44cb60bd2fa35de15b902951bf529a4bfaa88fc4ce8c2b7bec1a74a14802b8c1c38a87e502d17e59c4c678433f6c
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- opsask (2.0.10)
4
+ opsask (2.0.11)
5
5
  curb (~> 0.8)
6
6
  rack (~> 1)
7
7
  rack-flash3 (~> 1)
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.0.11
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
@@ -123,6 +123,10 @@ textarea { height: 100px; }
123
123
 
124
124
  .label a, .label a:visited { color: #fff; }
125
125
 
126
+ .need-to-login-to-jira a {
127
+ text-decoration: underline !important;
128
+ }
129
+
126
130
  h3.need-to-login-to-jira,
127
131
  h3.ops-queues-are-full {
128
132
  color: #c60f13;
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.11
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: 2014-12-26 00:00:00.000000000 Z
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