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 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