lazylead 0.8.2 → 0.9.3

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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.docker/docker-compose.yml +1 -1
  3. data/.rubocop.yml +1 -0
  4. data/Rakefile +1 -1
  5. data/bin/lazylead +3 -0
  6. data/lazylead.gemspec +11 -11
  7. data/lib/lazylead/log.rb +11 -0
  8. data/lib/lazylead/os.rb +55 -0
  9. data/lib/lazylead/system/jira.rb +10 -7
  10. data/lib/lazylead/task/accuracy/accuracy.rb +14 -2
  11. data/lib/lazylead/task/accuracy/screenshots.rb +20 -7
  12. data/lib/lazylead/task/accuracy/testcase.rb +4 -4
  13. data/lib/lazylead/task/assignment.rb +9 -13
  14. data/lib/lazylead/task/fix_version.rb +5 -8
  15. data/lib/lazylead/task/loading.rb +6 -1
  16. data/lib/lazylead/task/micromanager.rb +111 -0
  17. data/lib/lazylead/task/svn/diff.rb +11 -11
  18. data/lib/lazylead/task/svn/grep.rb +8 -71
  19. data/lib/lazylead/task/svn/svn.rb +107 -0
  20. data/lib/lazylead/task/svn/touch.rb +5 -7
  21. data/lib/lazylead/version.rb +1 -1
  22. data/lib/messages/illegal_assignee_change.erb +1 -2
  23. data/lib/messages/illegal_duedate_change.erb +122 -0
  24. data/lib/messages/loading.erb +9 -8
  25. data/lib/messages/svn_diff.erb +29 -29
  26. data/lib/messages/svn_diff_attachment.erb +5 -7
  27. data/readme.md +9 -5
  28. data/test/lazylead/system/jira_test.rb +8 -0
  29. data/test/lazylead/task/accuracy/accuracy_test.rb +1 -1
  30. data/test/lazylead/task/accuracy/onlyll_test.rb +1 -1
  31. data/test/lazylead/task/accuracy/score_test.rb +11 -0
  32. data/test/lazylead/task/accuracy/screenshots_test.rb +61 -7
  33. data/test/lazylead/task/accuracy/testcase_test.rb +18 -0
  34. data/test/lazylead/task/alert/alertif_test.rb +2 -1
  35. data/test/lazylead/task/assignment_test.rb +2 -1
  36. data/test/lazylead/task/created_recently_test.rb +2 -1
  37. data/test/lazylead/task/duedate_test.rb +2 -2
  38. data/test/lazylead/task/fix_version_test.rb +2 -1
  39. data/test/lazylead/task/loading_test.rb +9 -4
  40. data/test/lazylead/task/micromanager_test.rb +61 -0
  41. data/test/lazylead/task/missing_comment_test.rb +1 -1
  42. data/test/lazylead/task/svn/diff_test.rb +1 -1
  43. data/test/lazylead/task/svn/grep_test.rb +2 -1
  44. data/test/test.rb +4 -3
  45. metadata +27 -21
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fde968b00a6b6868ea644d731c54d23d08f365b133926fa2cf13699e982a0802
4
- data.tar.gz: 3340adabd89a6699cf0ba2fdc723ccc9e372d505b8b14b81cd7eef6c816c7fdc
3
+ metadata.gz: d79bc577cc5d288972a0e76fd800bf2b34ff7ab24ae9c26440c733222fb2fd08
4
+ data.tar.gz: b627e3f6e109bd0d9e33d8a3c7e388027ec299d8482cb771883f223c874ea093
5
5
  SHA512:
6
- metadata.gz: ecbf953aa53ae51b6ed4cc056794f3b8eb9e693d07cc3262070a95592546f9d45797f4ebdf4bbbacc650ad369033e9fd433e6d1975fd3f37688f5c7c3c321ee9
7
- data.tar.gz: f2dae8c55b5b98ddca9fe769a007eb4ea40d6c2b52675fa47911bc924d9ad7b56453f1a4c3705275f2ba2840d9d662ffde58bd92c179ea2af5a2da5baf1b3156
6
+ metadata.gz: da3d31eb92822381047ea2cd8c3ab363765e4d7e644bfe8921644a8998eac5e4ee58f4b3000e2a46ddc0098f9c9176f563aa4c0f084f1a456fa95a905f26b3f4
7
+ data.tar.gz: 206b8e56cbbb7807b4c92c291a7d2b03a8ec3bb05f7f75d60b31601ab925ec3768d0f24b84d4a124c6c0c3e99f7e39f8ea6851110144d9202237460b0d0fedfa
@@ -20,4 +20,4 @@ services:
20
20
  volumes:
21
21
  - .local/dumps:/lazylead/dumps
22
22
  - .local/logs:/lazylead/logs
23
- entrypoint: bin/lazylead --trace --verbose
23
+ entrypoint: bin/lazylead --trace --verbose --log-file /lazylead/logs/ll{{.%Y-%m-%dT%H:%M:%S}}.log
data/.rubocop.yml CHANGED
@@ -15,6 +15,7 @@ Layout/LineLength:
15
15
  Exclude:
16
16
  - "*.gemspec"
17
17
  - "test/**/*"
18
+ - "bin/lazylead"
18
19
 
19
20
  Metrics/AbcSize:
20
21
  Max: 21
data/Rakefile CHANGED
@@ -45,7 +45,7 @@ def version
45
45
  Gem::Specification.load(Dir["*.gemspec"].first).version
46
46
  end
47
47
 
48
- task default: %i[clean test rubocop sqlint xcop copyright docker]
48
+ task default: %i[clean rubocop test sqlint xcop copyright docker]
49
49
 
50
50
  require "rake/testtask"
51
51
  desc "Run all unit tests"
data/bin/lazylead CHANGED
@@ -70,6 +70,9 @@ Available options:"
70
70
  o.bool "--testdata",
71
71
  "Apply the database VCS migration with test data",
72
72
  default: false
73
+ o.string "--log-file", "The path to the log file"
74
+ o.string "--rolling-age", "The maximum age (in seconds) of a log file before it is rolled. The age can also be given as 'daily', 'weekly', or 'monthly'"
75
+ o.string "--rolling-files", "The number of rolled log files to keep."
73
76
  o.on "--verbose", "Enable extra logging information" do
74
77
  log.verbose
75
78
  end
data/lazylead.gemspec CHANGED
@@ -32,7 +32,7 @@ Gem::Specification.new do |s|
32
32
  s.rubygems_version = "2.2"
33
33
  s.required_ruby_version = ">=2.6.5"
34
34
  s.name = "lazylead"
35
- s.version = "0.8.2"
35
+ s.version = "0.9.3"
36
36
  s.license = "MIT"
37
37
  s.summary = "Eliminate the annoying work within bug-trackers."
38
38
  s.description = "Ticketing systems (Github, Jira, etc.) are strongly
@@ -45,7 +45,7 @@ tasks instead of solving technical problems."
45
45
  s.authors = ["Yurii Dubinka"]
46
46
  s.email = "yurii.dubinka@gmail.com"
47
47
  s.homepage = "http://github.com/dgroup/lazylead"
48
- s.post_install_message = "Thanks for installing Lazylead v0.8.2!
48
+ s.post_install_message = "Thanks for installing Lazylead v0.9.3!
49
49
  Read our blog posts: https://lazylead.org
50
50
  Stay in touch with the community in Telegram: https://t.me/lazylead
51
51
  Follow us on Twitter: https://twitter.com/lazylead
@@ -58,7 +58,7 @@ tasks instead of solving technical problems."
58
58
  s.add_runtime_dependency "activerecord", "6.1.3"
59
59
  s.add_runtime_dependency "backtrace", "0.3"
60
60
  s.add_runtime_dependency "colorize", "0.8.1"
61
- s.add_runtime_dependency "faraday", "1.3.0"
61
+ s.add_runtime_dependency "faraday", "1.4.2"
62
62
  s.add_runtime_dependency "get_process_mem", "0.2.7"
63
63
  s.add_runtime_dependency "inifile", "3.0.0"
64
64
  s.add_runtime_dependency "jira-ruby", "2.1.5"
@@ -71,7 +71,7 @@ tasks instead of solving technical problems."
71
71
  s.add_runtime_dependency "require_all", "3.0.0"
72
72
  s.add_runtime_dependency "rubyzip", "2.3.0"
73
73
  s.add_runtime_dependency "rufus-scheduler", "3.7.0"
74
- s.add_runtime_dependency "slop", "4.8.2"
74
+ s.add_runtime_dependency "slop", "4.9.0"
75
75
  s.add_runtime_dependency "sqlite3", "1.4.2"
76
76
  s.add_runtime_dependency "tempfile", "0.1.1"
77
77
  s.add_runtime_dependency "tilt", "2.0.10"
@@ -79,8 +79,8 @@ tasks instead of solving technical problems."
79
79
  s.add_runtime_dependency "tzinfo-data", "1.2021.1"
80
80
  s.add_runtime_dependency "vcs4sql", "0.1.1"
81
81
  s.add_runtime_dependency "viewpoint", "1.1.1"
82
- s.add_development_dependency "codecov", "0.5.1"
83
- s.add_development_dependency "guard", "2.16.2"
82
+ s.add_development_dependency "codecov", "0.5.2"
83
+ s.add_development_dependency "guard", "2.17.0"
84
84
  s.add_development_dependency "guard-minitest", "2.4.6"
85
85
  s.add_development_dependency "minitest", "5.14.4"
86
86
  s.add_development_dependency "minitest-fail-fast", "0.1.0"
@@ -89,12 +89,12 @@ tasks instead of solving technical problems."
89
89
  s.add_development_dependency "net-ping", "2.0.8"
90
90
  s.add_development_dependency "rake", "13.0.3"
91
91
  s.add_development_dependency "random-port", "0.5.1"
92
- s.add_development_dependency "rdoc", "6.3.0"
93
- s.add_development_dependency "rubocop", "1.12.0"
94
- s.add_development_dependency "rubocop-minitest", "0.11.0"
95
- s.add_development_dependency "rubocop-performance", "1.10.2"
92
+ s.add_development_dependency "rdoc", "6.3.1"
93
+ s.add_development_dependency "rubocop", "1.15.0"
94
+ s.add_development_dependency "rubocop-minitest", "0.12.1"
95
+ s.add_development_dependency "rubocop-performance", "1.11.3"
96
96
  s.add_development_dependency "rubocop-rake", "0.5.1"
97
- s.add_development_dependency "rubocop-rspec", "2.2.0"
97
+ s.add_development_dependency "rubocop-rspec", "2.3.0"
98
98
  s.add_development_dependency "sqlint", "0.2.0"
99
99
  s.add_development_dependency "tempfile", "0.1.1"
100
100
  s.add_development_dependency "xcop", "0.6.2"
data/lib/lazylead/log.rb CHANGED
@@ -74,6 +74,15 @@ module Lazylead
74
74
  )
75
75
  )
76
76
 
77
+ if ARGV.include? "--log-file"
78
+ name = ARGV[ARGV.find_index("--log-file") + 1]
79
+ age = "daily"
80
+ age = ARGV[ARGV.find_index("--rolling-age") + 1] if ARGV.include? "--rolling-age"
81
+ files = 7
82
+ files = ARGV[ARGV.find_index("--rolling-files") + 1] if ARGV.include? "--rolling-files"
83
+ FILE_APPENDER = Logging.appenders.rolling_file("file", filename: name, age: age, keep: files)
84
+ end
85
+
77
86
  # Nothing to log
78
87
  NOTHING = Logging.logger["nothing"]
79
88
  NOTHING.level = :off
@@ -83,12 +92,14 @@ module Lazylead
83
92
  DEBUG = Logging.logger["debug"]
84
93
  DEBUG.level = :debug
85
94
  DEBUG.add_appenders "stdout"
95
+ DEBUG.add_appenders(FILE_APPENDER) if ARGV.include? "--log-file"
86
96
  DEBUG.freeze
87
97
 
88
98
  # Alerts/errors
89
99
  ERRORS = Logging.logger["errors"]
90
100
  ERRORS.level = :error
91
101
  ERRORS.add_appenders "stdout"
102
+ ERRORS.add_appenders(FILE_APPENDER) if ARGV.include? "--log-file"
92
103
  ERRORS.freeze
93
104
  end
94
105
  end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The MIT License
4
+ #
5
+ # Copyright (c) 2019-2021 Yurii Dubinka
6
+ #
7
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ # of this software and associated documentation files (the "Software"),
9
+ # to deal in the Software without restriction, including without limitation
10
+ # the rights to use, copy, modify, merge, publish, distribute, sublicense,
11
+ # and/or sell copies of the Software, and to permit persons to whom
12
+ # the Software is furnished to do so, subject to the following conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be included
15
+ # in all copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ # FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
20
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
22
+ # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23
+ # OR OTHER DEALINGS IN THE SOFTWARE.
24
+
25
+ module Lazylead
26
+ #
27
+ # Represents a native, operation system.
28
+ #
29
+ # Author:: Yurii Dubinka (yurii.dubinka@gmail.com)
30
+ # Copyright:: Copyright (c) 2019-2020 Yurii Dubinka
31
+ # License:: MIT
32
+ class OS
33
+ #
34
+ # Run OS-oriented command
35
+ # @param cmd
36
+ # The command could be a single string or array of strings.
37
+ # Examples Final command to OS
38
+ # run("ls") "ls" => stdout
39
+ # run("ls", "-lah") "ls -lah" => stdout
40
+ # run() N/A => ""
41
+ # run("ls", nil, "-lah") N/A => ""
42
+ # run("ls", "", "-lah") "ls -lah" => stdout
43
+ #
44
+ # @return stdout
45
+ # Please note, that this is not a raw stdout.
46
+ # The output will be modified by String#scrub! in order to avoid invalid byte sequence
47
+ # in UTF-8 (https://stackoverflow.com/a/24037885/6916890).
48
+ def run(*cmd)
49
+ return "" if cmd.empty? || cmd.any?(&:nil?)
50
+ todo = cmd
51
+ todo = [cmd.first] if cmd.size == 1
52
+ `#{todo.join(" ")}`.scrub!
53
+ end
54
+ end
55
+ end
@@ -129,11 +129,7 @@ module Lazylead
129
129
  end
130
130
 
131
131
  def to_s
132
- inspect
133
- end
134
-
135
- def inspect
136
- "#{@opts['site']} (#{@opts['username']})"
132
+ "#{name} (#{id})"
137
133
  end
138
134
  end
139
135
 
@@ -257,13 +253,20 @@ module Lazylead
257
253
 
258
254
  # Update the labels for a particular issue
259
255
  def labels!(lbl)
260
- return if lbl.nil? || lbl.empty?
261
- save!("fields" => { "labels" => lbl.uniq })
256
+ save!("fields" => { "labels" => lbl.uniq }) unless lbl.empty?
262
257
  end
263
258
 
264
259
  def save!(opts)
265
260
  @issue.save(opts)
266
261
  end
262
+
263
+ def sprint(field = "customfield_10480")
264
+ @sprint ||= if fields[field].nil? || fields[field].empty?
265
+ ""
266
+ else
267
+ fields[field].first.split(",").find { |text| text.starts_with? "name=" }[5..]
268
+ end
269
+ end
267
270
  end
268
271
 
269
272
  # The jira issue comments
@@ -47,7 +47,7 @@ module Lazylead
47
47
  Requires.new(__dir__).load
48
48
  opts[:rules] = opts.construct("rules")
49
49
  opts[:total] = opts[:rules].sum(&:score)
50
- opts[:tickets] = sys.issues(opts["jql"], opts.jira_defaults)
50
+ opts[:tickets] = sys.issues(opts["jql"], opts.jira_defaults.merge(expand: "changelog"))
51
51
  .map { |i| Score.new(i, opts) }
52
52
  .each(&:evaluate)
53
53
  .each(&:post)
@@ -83,7 +83,7 @@ module Lazylead
83
83
  # The jira comment in markdown format
84
84
  def comment
85
85
  comment = [
86
- "Hi [~#{@issue.reporter.id}],",
86
+ "Hi [~#{reporter}],",
87
87
  "",
88
88
  "The triage accuracy is '{color:#{color}}#{@score}{color}'" \
89
89
  " (~{color:#{color}}#{@accuracy}%{color}), here are the reasons why:",
@@ -133,5 +133,17 @@ module Lazylead
133
133
  def grade(value)
134
134
  (value / 10).floor * 10
135
135
  end
136
+
137
+ # Detect the ticket reporter.
138
+ #
139
+ # If ticket created by some automatic/admin user account then reporter is the first non-system
140
+ # user account who modified the ticket.
141
+ def reporter
142
+ sys = @opts.slice("system-users", ",")
143
+ return @issue.reporter.id if sys.empty? || sys.none? { |susr| susr.eql? @issue.reporter.id }
144
+ first = @issue.history.find { |h| sys.none? { |susr| susr.eql? h["author"]["key"] } }
145
+ return @issue.reporter.id if first.nil?
146
+ first["author"]["key"]
147
+ end
136
148
  end
137
149
  end
@@ -29,11 +29,12 @@ module Lazylead
29
29
  # Check that ticket has screenshot(s).
30
30
  # The screenshots should
31
31
  # 1. present as attachments
32
- # 2. mentioned in description with !<name>.<extension>|thumbnail! (read more https://bit.ly/3rusNgW)
32
+ # 2. has extension .jpg .jpeg .exif .tiff .tff .bmp .png .svg
33
+ # 3. mentioned in description with !<name>.<extension>|thumbnail! (read more https://bit.ly/3rusNgW)
33
34
  #
34
35
  class Screenshots < Lazylead::Requirement
35
36
  # @param minimum The number of expected screenshots
36
- def initialize(minimum: 2, score: 2, ext: %w[.jpg .jpeg .exif .tiff .tff .bmp .png .svg])
37
+ def initialize(minimum: 1, score: 2, ext: %w[.jpg .jpeg .exif .tiff .tff .bmp .png .svg])
37
38
  super "Screenshots", score, "Description,Attachments"
38
39
  @minimum = minimum
39
40
  @ext = ext
@@ -41,11 +42,23 @@ module Lazylead
41
42
 
42
43
  def passed(issue)
43
44
  return false if issue.attachments.nil? || blank?(issue, "description")
44
- regexps = issue.attachments
45
- .select { |a| @ext.include? File.extname(a.filename) }
46
- .map { |a| /!#{a.filename}\|thumbnail!/ }
47
- return false if regexps.size < @minimum
48
- regexps.all? { |r| issue.description.match? r }
45
+ references(issue).count { |r| pictures(issue).any? { |file| r.include? file } } >= @minimum
46
+ end
47
+
48
+ # Detect all references in ticket description to attachments (including web links).
49
+ def references(issue)
50
+ issue.description
51
+ .to_enum(:scan, /!.+!/)
52
+ .map { Regexp.last_match }
53
+ .map(&:to_s)
54
+ .reject { |r| r.match?(%r{(http|https)://.*/images/icons/link_attachment.*.gif}) }
55
+ end
56
+
57
+ # Detect all pictures in ticket attachments and returns an array with file names.
58
+ def pictures(issue)
59
+ @pictures ||= issue.attachments
60
+ .select { |a| @ext.include? File.extname(a.filename).downcase }
61
+ .map(&:filename)
49
62
  end
50
63
  end
51
64
  end
@@ -65,20 +65,20 @@ module Lazylead
65
65
  # Detect index of line with test case
66
66
  def detect_tc(line, index)
67
67
  return unless @tc.negative?
68
- @tc = index if eql? line,
69
- %w[testcase: tc: teststeps: teststeps steps: tcsteps: tc testcases steps]
68
+ @tc = index if eql? line, %w[testcase: tc: teststeps: teststeps steps: tcsteps: tc testcases
69
+ steps usecase]
70
70
  end
71
71
 
72
72
  # Detect index of line with actual result
73
73
  def detect_ar(line, index)
74
74
  return unless @ar.negative? && index > @tc
75
- @ar = index if starts? line, %w[ar: actualresult: ar= [ar]]
75
+ @ar = index if starts? line, %w[ar: actualresult: ar= [ar] actualresult]
76
76
  end
77
77
 
78
78
  # Detect index of line with expected result
79
79
  def detect_er(line, index)
80
80
  return unless @er.negative? && index > @tc
81
- @er = index if starts? line, %w[er: expectedresult: er= [er]]
81
+ @er = index if starts? line, %w[er: expectedresult: er= [er] expectedresult]
82
82
  end
83
83
 
84
84
  def starts?(line, text)
@@ -43,14 +43,12 @@ module Lazylead
43
43
  def run(sys, postman, opts)
44
44
  allowed = opts.slice "allowed", ","
45
45
  silent = opts.key? "silent"
46
- issues = sys.issues opts["jql"],
47
- opts.jira_defaults.merge(expand: "changelog")
48
- return if issues.empty?
49
- postman.send opts.merge(
50
- assignees: issues.map { |i| Assignee.new(i, allowed, silent) }
51
- .select(&:illegal?)
52
- .each(&:add_label)
53
- )
46
+ assignees = sys.issues(opts["jql"], opts.jira_defaults.merge(expand: "changelog"))
47
+ .map { |i| Assignee.new(i, allowed, silent) }
48
+ .select(&:illegal?)
49
+ .each(&:add_label)
50
+ return if assignees.empty?
51
+ postman.send opts.merge(assignees: assignees)
54
52
  end
55
53
  end
56
54
 
@@ -67,10 +65,8 @@ module Lazylead
67
65
  # Gives true when last change of "Assignee" field was done
68
66
  # by not authorized person.
69
67
  def illegal?
70
- @allowed.none? do |a|
71
- return false if last.nil?
72
- a == last["author"]["name"]
73
- end
68
+ return false if last.nil? || @issue.assignee.id.eql?(last["author"]["name"])
69
+ @allowed.none? { |a| a.eql? last["author"]["name"] }
74
70
  end
75
71
 
76
72
  # Detect details about last change of "Assignee" to non-null value
@@ -89,7 +85,7 @@ module Lazylead
89
85
 
90
86
  # The name of current assignee for ticket
91
87
  def to
92
- @issue.fields["assignee"]["name"]
88
+ @issue.fields["assignee"]["displayName"]
93
89
  end
94
90
  end
95
91
  end
@@ -39,15 +39,12 @@ module Lazylead
39
39
  def run(sys, postman, opts)
40
40
  allowed = opts.slice("allowed", ",")
41
41
  silent = opts.key? "silent"
42
- issues = sys.issues(
43
- opts["jql"], opts.jira_defaults.merge(expand: "changelog")
44
- )
42
+ issues = sys.issues(opts["jql"], opts.jira_defaults.merge(expand: "changelog"))
43
+ .map { |i| Version.new(i, allowed, silent) }
44
+ .select(&:changed?)
45
+ .each(&:add_label)
45
46
  return if issues.empty?
46
- postman.send opts.merge(
47
- versions: issues.map { |i| Version.new(i, allowed, silent) }
48
- .select(&:changed?)
49
- .each(&:add_label)
50
- )
47
+ postman.send opts.merge(versions: issues)
51
48
  end
52
49
  end
53
50
 
@@ -36,7 +36,7 @@ module Lazylead
36
36
  end
37
37
 
38
38
  def run(sys, postman, opts)
39
- assignments = sys.issues(opts["jql"])
39
+ assignments = sys.issues(opts["jql"], opts.jira_defaults)
40
40
  .group_by(&:assignee)
41
41
  .map { |user, tasks| [user.id, Teammate.new(user, tasks)] }
42
42
  .to_h
@@ -71,6 +71,11 @@ module Lazylead
71
71
  def to_s
72
72
  "#{id} has #{total} tasks"
73
73
  end
74
+
75
+ def sprints(label)
76
+ return @tasks.group_by(&:sprint).sort if label.nil? || label.blank?
77
+ @tasks.group_by { |t| t.sprint(label) }.sort
78
+ end
74
79
  end
75
80
 
76
81
  # The teammate without tasks.
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The MIT License
4
+ #
5
+ # Copyright (c) 2019-2021 Yurii Dubinka
6
+ #
7
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ # of this software and associated documentation files (the "Software"),
9
+ # to deal in the Software without restriction, including without limitation
10
+ # the rights to use, copy, modify, merge, publish, distribute, sublicense,
11
+ # and/or sell copies of the Software, and to permit persons to whom
12
+ # the Software is furnished to do so, subject to the following conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be included
15
+ # in all copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ # FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
20
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
22
+ # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23
+ # OR OTHER DEALINGS IN THE SOFTWARE.
24
+
25
+ require "date"
26
+ require_relative "../log"
27
+ require_relative "../opts"
28
+ require_relative "../system/jira"
29
+
30
+ module Lazylead
31
+ module Task
32
+ #
33
+ # Email alerts about due date modification by not-authorized persons.
34
+ #
35
+ # Author:: Yurii Dubinka (yurii.dubinka@gmail.com)
36
+ # Copyright:: Copyright (c) 2019-2020 Yurii Dubinka
37
+ # License:: MIT
38
+ class Micromanager
39
+ def initialize(log = Log.new)
40
+ @log = log
41
+ end
42
+
43
+ def run(sys, postman, opts)
44
+ allowed = opts.slice "allowed", ","
45
+ dues = sys.issues(opts["jql"], opts.jira_defaults.merge(expand: "changelog"))
46
+ .map { |i| Due.new(i, allowed, since(opts)) }
47
+ .select(&:illegal?)
48
+ .reject(&:obsolete?)
49
+ return if dues.empty?
50
+ postman.send opts.merge(dues: dues)
51
+ end
52
+
53
+ # Detect history period where search should start.
54
+ #
55
+ # opts["period"] The default period for past is 1 day (86400 seconds).
56
+ # So, if now 2017-04-06 15:50:58.674+0000
57
+ # it returns 2017-04-05 15:50:58 +0000
58
+ #
59
+ # opts["now"] The current time for unit tests.
60
+ # If absent the "Time.now" is used.
61
+ #
62
+ def since(opts)
63
+ @since ||= if opts.key? "now"
64
+ Time.parse(opts["now"])
65
+ else
66
+ Time.now - opts.fetch("period", "86400").to_i
67
+ end
68
+ end
69
+ end
70
+
71
+ # Instance of "Due" history item for the particular ticket.
72
+ class Due
73
+ attr_reader :issue, :when
74
+
75
+ def initialize(issue, allowed, since)
76
+ @issue = issue
77
+ @allowed = allowed
78
+ @since = since
79
+ end
80
+
81
+ # Gives true when last change of "Due Date" field was done
82
+ # by not authorized person.
83
+ def illegal?
84
+ return false if @issue.assignee.id.eql?(last.id)
85
+ @allowed.none? { |a| a.eql? last.id }
86
+ end
87
+
88
+ # Give true when "Due Date" changes happens in past and its alert already sent.
89
+ def obsolete?
90
+ @when < @since
91
+ end
92
+
93
+ # Detect details about last change of "Due Date" to non-null value
94
+ def last
95
+ @last ||= begin
96
+ dd = @issue.history
97
+ .reverse
98
+ .find { |h| h["items"].any? { |i| i["field"] == "duedate" } }
99
+ if dd.nil? && !@issue.duedate.nil?
100
+ @when = @issue["created"]
101
+ dd = @issue.reporter
102
+ else
103
+ @when = dd["created"]
104
+ dd = Lazylead::User.new(dd["author"])
105
+ end
106
+ dd
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end