time_log_robot 0.1.5 → 0.2.0

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: ea745bfe20d10a98d2a22ada30fea34c6ea9d138
4
- data.tar.gz: ef7dc87e71be0e41c932f2d692fa80507b8a02c0
3
+ metadata.gz: f61caa0dc4d6525a101c6ce36384098b0b84d707
4
+ data.tar.gz: 946edcfc71a494b8687572411440988dabfb6fc0
5
5
  SHA512:
6
- metadata.gz: 25361f569f95c89e8d06fef9841d31926bfd6292b54108c0e814444a4de98c0c4780fd893d7d5124b9dca7ca924c157a9306288e8cc07190551724379f6f3e66
7
- data.tar.gz: 0d4a18ca78cc8cb920f151bd7a15f65001f90d989cf9306277967fa9019380d968c093260ec7fb8c802d6ae1ac98323b4c1a7151ec153e80598497f23f324fac
6
+ metadata.gz: 7d9a8d4838ec1f7178e9ee66111d9e13dabd1324ec24de3d634d662b7d389c08739ed797cb4980470dd3144f38d82074c7ccd3ebb74a6821ab834a369f4ce973
7
+ data.tar.gz: ce614922c96cdaf8fa142bf4c7fb005d43d1f4cdac73eb421f7dd72f7a920ace010d8ebc606479ccdb89051d62970db1e5047429d98f56f611dbcb842544a6e8
data/.codeclimate.yml CHANGED
@@ -12,8 +12,13 @@ engines:
12
12
  - php
13
13
  fixme:
14
14
  enabled: true
15
+ exclude_fingerprints:
16
+ - 9b1a9060d3684be85a832e44ca1ea7dd
15
17
  rubocop:
16
18
  enabled: true
19
+ checks:
20
+ Rubocop/Lint/AssignmentInCondition:
21
+ enabled: false
17
22
  ratings:
18
23
  paths:
19
24
  - Gemfile.lock
@@ -26,3 +31,4 @@ ratings:
26
31
  - "**.rb"
27
32
  exclude_paths:
28
33
  - test/
34
+ - README.md
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- time_log_robot (0.1.3)
4
+ time_log_robot (0.2.0)
5
5
  activesupport (~> 4.2, >= 4.2.6)
6
6
  commander (~> 4.1, >= 4.1.6)
7
7
  httparty (~> 0.13, >= 0.13.0)
@@ -18,14 +18,19 @@ GEM
18
18
  tzinfo (~> 1.1)
19
19
  ansi (1.5.0)
20
20
  builder (3.2.2)
21
- commander (4.1.6)
22
- highline (~> 1.6.11)
23
- highline (1.6.21)
24
- httparty (0.13.3)
21
+ codeclimate-test-reporter (0.5.2)
22
+ simplecov (>= 0.7.1, < 1.0.0)
23
+ coderay (1.1.1)
24
+ commander (4.4.0)
25
+ highline (~> 1.7.2)
26
+ docile (1.1.5)
27
+ highline (1.7.8)
28
+ httparty (0.13.7)
25
29
  json (~> 1.8)
26
30
  multi_xml (>= 0.5.2)
27
31
  i18n (0.7.0)
28
32
  json (1.8.3)
33
+ method_source (0.8.2)
29
34
  minitest (5.8.4)
30
35
  minitest-reporters (1.1.9)
31
36
  ansi
@@ -33,8 +38,18 @@ GEM
33
38
  minitest (>= 5.0)
34
39
  ruby-progressbar
35
40
  multi_xml (0.5.5)
41
+ pry (0.10.3)
42
+ coderay (~> 1.1.0)
43
+ method_source (~> 0.8.1)
44
+ slop (~> 3.4)
36
45
  rake (10.5.0)
37
46
  ruby-progressbar (1.8.1)
47
+ simplecov (0.11.2)
48
+ docile (~> 1.1.0)
49
+ json (~> 1.8)
50
+ simplecov-html (~> 0.10.0)
51
+ simplecov-html (0.10.0)
52
+ slop (3.6.0)
38
53
  thread_safe (0.3.5)
39
54
  tzinfo (1.2.2)
40
55
  thread_safe (~> 0.1)
@@ -44,10 +59,12 @@ PLATFORMS
44
59
 
45
60
  DEPENDENCIES
46
61
  bundler (~> 1.3)
62
+ codeclimate-test-reporter (~> 0.5)
47
63
  minitest (~> 5.8)
48
64
  minitest-reporters (~> 1.1)
65
+ pry
49
66
  rake (~> 10.5)
50
67
  time_log_robot!
51
68
 
52
69
  BUNDLED WITH
53
- 1.12.3
70
+ 1.12.5
data/README.md CHANGED
@@ -47,6 +47,35 @@ The simplest usage is just to invoke the robot:
47
47
 
48
48
  $ time_log_robot
49
49
 
50
+ #### Format of time entries
51
+
52
+ Time entries need an issue key (in JIRA, something like `BUG-12`), a start time, and a duration. The robot will try to parse an issue key from the description first, then from the project name, then from the mapping file (see ["Mapping Keys"](#mapping-keys) section).
53
+
54
+ For example, all of these are valid:
55
+
56
+ This is a bug BUG-15
57
+
58
+ (no description) APP-20
59
+
60
+ Meeting
61
+ (In project named: ADMIN-123)
62
+
63
+ ##### Comments
64
+
65
+ Some project management tools also accept comments/descriptions for each time log entry. The robot uses any text within curly braces in the time logging app (Toggl) entry as the description in the project management log entry.
66
+
67
+ For example:
68
+
69
+ This is a bug {This is my comment} BUG-15
70
+
71
+ If there are no curly braces present, the robot will use the entire description as the comment.
72
+
73
+ For example:
74
+
75
+ This whole description is my comment on issue BUG-20
76
+
77
+ #### Specifying how far back to log
78
+
50
79
  By default, the robot will get all time entries since the previous Saturday. To specify a different time, run it with the optional `--since` flag (Note: the date given must be in YYYY-MM-DD format):
51
80
 
52
81
  $ time_log_robot --since 2016-05-01
@@ -126,6 +155,8 @@ To run the app in IRB for debugging run
126
155
 
127
156
  ## Contributing
128
157
 
158
+ Contributions are welcome! See the [Issues](https://github.com/supremebeing7/time_log_robot/issues) for stuff that needs doing, or create a new one if you have an idea and we can discuss. Eventually it would be great to integrate this with other project management and time tracking tools, so if you use something besides JIRA or Toggl and want to build an integration, that would be welcome. (There's some refactoring that needs to be done on the existing code to make this simpler - issue [#18](https://github.com/supremebeing7/time_log_robot/issues/18).)
159
+
129
160
  1. Fork it
130
161
  2. Create your feature branch (`git checkout -b my-new-feature`)
131
162
  3. Write tests for your new code (uses `minitest`)
data/Rakefile CHANGED
@@ -17,4 +17,6 @@ end
17
17
  Rake::TestTask.new do |t|
18
18
  t.libs << 'test'
19
19
  t.test_files = FileList['test/**/*_test.rb']
20
- end
20
+ end
21
+
22
+ task default: :test
data/bin/time_log_robot CHANGED
@@ -2,9 +2,13 @@
2
2
 
3
3
  require 'commander/import'
4
4
  require 'time_log_robot'
5
+ require 'time_log_robot/entry'
6
+ require 'time_log_robot/reporter'
7
+ require 'time_log_robot/tagger'
5
8
  require 'time_log_robot/version'
6
- require 'time_log_robot/toggl/tagger'
9
+ require 'time_log_robot/toggl/entry'
7
10
  require 'time_log_robot/toggl/report'
11
+ require 'time_log_robot/toggl/tagger'
8
12
  require 'time_log_robot/jira/issue_key_parser'
9
13
  require 'time_log_robot/jira/payload_builder'
10
14
  require 'time_log_robot/jira/work_logger'
@@ -0,0 +1,12 @@
1
+ module TimeLogRobot
2
+ class Entry
3
+ class << self
4
+ def new(service:, raw_entry:)
5
+ case service
6
+ when 'Toggl'
7
+ Toggl::Entry.new(raw_entry)
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -1,16 +1,25 @@
1
1
  module TimeLogRobot
2
2
  module JIRA
3
3
  class IssueKeyParser
4
+ ISSUE_KEY_REGEX = /([A-Z]+-\d+)/
4
5
 
5
6
  class << self
6
- def parse(description)
7
- matches = description.match(/(\[(?<issue_key>[^\]]*)\])/)
8
- return matches['issue_key'] unless matches.nil?
9
- get_key_from_key_mapping(description)
7
+ def parse(entry)
8
+ get_key_from_description(entry.description) ||
9
+ get_key_from_project(entry.project_name) ||
10
+ get_key_from_key_mapping(entry.description)
10
11
  end
11
12
 
12
13
  private
13
14
 
15
+ def get_key_from_description(description)
16
+ description.match(ISSUE_KEY_REGEX).to_a[1]
17
+ end
18
+
19
+ def get_key_from_project(project_name)
20
+ project_name.match(ISSUE_KEY_REGEX).to_a[1]
21
+ end
22
+
14
23
  def get_key_from_key_mapping(description)
15
24
  if found_key = mappings.keys.find { |key| description.include?(key) }
16
25
  mappings[found_key]
@@ -7,15 +7,17 @@ module TimeLogRobot
7
7
 
8
8
  @errors = []
9
9
  @logged_count = 0
10
+ @issue_key = nil
10
11
 
11
12
  class << self
12
- attr_accessor :errors, :logged_count
13
+ attr_accessor :errors, :logged_count, :issue_key
13
14
 
14
- def log_all(time_entries:)
15
- time_entries.each do |entry|
15
+ def log_all(service:, time_entries:)
16
+ time_entries.each do |raw_entry|
17
+ entry = Entry.new(service: service, raw_entry: raw_entry)
16
18
  log(entry) unless is_logged?(entry)
17
19
  end
18
- print_report
20
+ report!
19
21
  end
20
22
 
21
23
  private
@@ -33,16 +35,16 @@ module TimeLogRobot
33
35
  end
34
36
 
35
37
  def is_logged?(entry)
36
- (log_tags - entry['tags']).size < log_tags.size
38
+ (log_tags - entry.tags).size < log_tags.size
37
39
  end
38
40
 
39
41
  def log(entry)
40
- issue_key = parse_issue_key(entry)
41
42
  payload = build_payload(entry)
43
+ @issue_key = parse_issue_key(entry)
42
44
  response = post("/issue/#{issue_key}/worklog", basic_auth: auth, headers: headers, body: payload)
43
45
  if response.success?
44
46
  print "\e[32m.\e[0m"
45
- set_entry_as_logged(entry)
47
+ tag!(entry) if should_tag?(entry)
46
48
  @logged_count += 1
47
49
  else
48
50
  print "\e[31mF\e[0m"
@@ -54,36 +56,16 @@ module TimeLogRobot
54
56
  end
55
57
  class UnauthorizedError < Exception; end
56
58
 
57
- def parse_issue_key(entry)
58
- JIRA::IssueKeyParser.parse(entry['description'])
59
+ def report!
60
+ Reporter.report(errors, logged_count)
59
61
  end
60
62
 
61
- def print_report
62
- print_errors if errors.any?
63
- puts "\n\t#{logged_count} entries logged, #{errors.size} failed.\n\n"
63
+ def tag!(entry)
64
+ Tagger.tag(entry)
64
65
  end
65
66
 
66
- def print_errors
67
- puts "\n\t\e[1;31m Failed to log the following entries:\e[0m"
68
- errors.each_with_index do |(entry, response), index|
69
- puts "\e[31m"
70
- puts "\t#{index + 1})\tDescription: #{entry['description']}"
71
- if issue_key = parse_issue_key(entry)
72
- puts "\t\tIssue Key: #{issue_key}"
73
- else
74
- puts "\t\tIssue Key: Missing"
75
- end
76
- unless parse_comment(entry).nil?
77
- puts "\t\tComment: #{parse_comment(entry)}"
78
- end
79
- puts "\t\t#{human_readable_duration(parse_duration(entry))} starting on #{parse_start(entry)}"
80
- puts "\t\tResponse Code: #{response.code}"
81
- puts "\e[0m"
82
- end
83
- end
84
-
85
- def set_entry_as_logged(entry)
86
- Toggl::Tagger.update(entry_id: entry['id'])
67
+ def should_tag?(entry)
68
+ entry.should_tag?
87
69
  end
88
70
 
89
71
  def auth
@@ -99,31 +81,15 @@ module TimeLogRobot
99
81
  end
100
82
 
101
83
  def build_payload(entry)
102
- JIRA::PayloadBuilder.build(
103
- start: parse_start(entry),
104
- duration_in_seconds: parse_duration(entry),
105
- comment: parse_comment(entry)
84
+ PayloadBuilder.build(
85
+ start: entry.start,
86
+ duration_in_seconds: entry.duration_in_seconds,
87
+ comment: entry.comment
106
88
  )
107
89
  end
108
90
 
109
- def parse_start(entry)
110
- DateTime.strptime(entry['start'], "%FT%T%:z").strftime("%FT%T.%L%z")
111
- end
112
-
113
- def parse_duration(entry)
114
- entry['dur']/1000 # Toggl sends times in milliseconds
115
- end
116
-
117
- def human_readable_duration(seconds)
118
- total_minutes = seconds/60
119
- hours = total_minutes/60
120
- remaining_minutes = total_minutes - hours * 60
121
- "#{hours}h #{remaining_minutes}m"
122
- end
123
-
124
- def parse_comment(entry)
125
- matches = entry['description'].match(/(\{(?<comment>[^\}]*)\})/)
126
- matches['comment'] if matches.present?
91
+ def parse_issue_key(entry)
92
+ IssueKeyParser.parse(entry)
127
93
  end
128
94
  end
129
95
  end
@@ -0,0 +1,32 @@
1
+ module TimeLogRobot
2
+ class Reporter
3
+ class << self
4
+ attr_accessor :errors
5
+
6
+ def report(errors, logged_count)
7
+ @errors = errors
8
+ print_errors if errors.any?
9
+ puts "\n\t#{logged_count} entries logged, #{errors.size} failed.\n\n"
10
+ end
11
+
12
+ def print_errors
13
+ puts "\n\t\e[1;31m Failed to log the following entries:\e[0m"
14
+ errors.each_with_index do |(entry, response), index|
15
+ puts "\e[31m"
16
+ puts "\t#{index + 1})\tDescription: #{entry.description}"
17
+ if entry.issue_key
18
+ puts "\t\tIssue Key: #{entry.issue_key}"
19
+ else
20
+ puts "\t\tIssue Key: Missing"
21
+ end
22
+ if entry.comment
23
+ puts "\t\tComment: #{entry.comment}"
24
+ end
25
+ puts "\t\t#{entry.human_readable_duration} starting on #{entry.start}"
26
+ puts "\t\tResponse Code: #{response.code}"
27
+ puts "\e[0m"
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,12 @@
1
+ module TimeLogRobot
2
+ class Tagger
3
+ class << self
4
+ def tag(entry)
5
+ case entry
6
+ when Toggl::Entry
7
+ Toggl::Tagger.update(entry_id: entry.id)
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,54 @@
1
+ module TimeLogRobot
2
+ module Toggl
3
+ class Entry
4
+ attr_accessor :raw_entry, :duration
5
+
6
+ def initialize(raw_entry)
7
+ @raw_entry = raw_entry
8
+ end
9
+
10
+ def description
11
+ raw_entry['description']
12
+ end
13
+
14
+ def comment
15
+ matches = raw_entry['description'].match(/(\{(?<comment>[^\}]*)\})/)
16
+ return matches['comment'] if matches.present?
17
+ description
18
+ end
19
+
20
+ def start
21
+ DateTime.strptime(raw_entry['start'], "%FT%T%:z").strftime("%FT%T.%L%z")
22
+ end
23
+
24
+ def duration_in_seconds
25
+ # Toggl sends times in milliseconds
26
+ @duration_in_seconds ||= raw_entry['dur']/1000
27
+ end
28
+
29
+ # @TODO This probably belongs on the reporter class?
30
+ def human_readable_duration
31
+ total_minutes = duration_in_seconds/60
32
+ hours = total_minutes/60
33
+ remaining_minutes = total_minutes - hours * 60
34
+ "#{hours}h #{remaining_minutes}m"
35
+ end
36
+
37
+ def id
38
+ raw_entry['id']
39
+ end
40
+
41
+ def tags
42
+ raw_entry['tags']
43
+ end
44
+
45
+ def project_name
46
+ raw_entry['project'] || ''
47
+ end
48
+
49
+ def should_tag?
50
+ true
51
+ end
52
+ end
53
+ end
54
+ end
@@ -27,7 +27,7 @@ module TimeLogRobot
27
27
  2.upto(pages) do |page|
28
28
  entries += get_entries_from_next_page(since, page)
29
29
  end
30
- entries
30
+ { service: 'Toggl', entries: entries }
31
31
  end
32
32
 
33
33
  def get_entries_from_next_page(since, page)
@@ -1,3 +1,3 @@
1
1
  module TimeLogRobot
2
- VERSION = '0.1.5'
2
+ VERSION = '0.2.0'
3
3
  end
@@ -7,11 +7,14 @@ module TimeLogRobot
7
7
  end
8
8
 
9
9
  def self.start(since)
10
- time_entries = fetch_time_entries(since)
11
- JIRA::WorkLogger.log_all(time_entries: time_entries)
10
+ report = fetch_time_report(since)
11
+ JIRA::WorkLogger.log_all(
12
+ service: report[:service],
13
+ time_entries: report[:entries]
14
+ )
12
15
  end
13
16
 
14
- def self.fetch_time_entries(since)
17
+ def self.fetch_time_report(since)
15
18
  if since.nil?
16
19
  Toggl::Report.fetch
17
20
  else
@@ -13,26 +13,47 @@ module TimeLogRobot
13
13
  class TestIssueKeyParser < Minitest::Test
14
14
  def setup
15
15
  @described_class = TimeLogRobot::JIRA::IssueKeyParser
16
+ @entry = OpenStruct.new(description: '', project_name: '')
16
17
  end
17
18
 
18
19
  def test_that_it_gets_the_issue_key
19
- assert_equal "PM-12", @described_class.parse('description [PM-12]')
20
+ @entry.description = 'description [PM-12]'
21
+ assert_equal "PM-12", @described_class.parse(@entry)
22
+ end
23
+
24
+ def test_that_it_gets_the_issue_key_without_brackets
25
+ @entry.description = 'description PM-12'
26
+ assert_equal "PM-12", @described_class.parse(@entry)
20
27
  end
21
28
 
22
29
  def test_that_it_gives_nothing_with_no_key_present
23
- assert_equal nil, @described_class.parse('description')
30
+ @entry.description = 'description'
31
+ assert_equal nil, @described_class.parse(@entry)
24
32
  end
25
33
 
26
34
  def test_that_it_fetches_the_key_from_mapping
27
- assert_equal "HI-5", @described_class.parse('mapped description')
35
+ @entry.description = 'mapped description'
36
+ assert_equal "HI-5", @described_class.parse(@entry)
28
37
  end
29
38
 
30
39
  def test_that_parse_is_not_too_strict_with_mappings
31
- assert_equal "HI-5", @described_class.parse('mapped description with more text')
40
+ @entry.description = 'mapped description with more text'
41
+ assert_equal "HI-5", @described_class.parse(@entry)
32
42
  end
33
43
 
34
44
  def test_that_parse_mappings_works_with_comments
35
- assert_equal "HI-5", @described_class.parse('mapped description - {with comment}')
45
+ @entry.description = 'mapped description - {with comment}'
46
+ assert_equal "HI-5", @described_class.parse(@entry)
47
+ end
48
+
49
+ def test_get_issue_key_from_project_name
50
+ @entry.project_name = 'BUG-30'
51
+ assert_equal "BUG-30", @described_class.parse(@entry)
52
+ end
53
+
54
+ def test_get_issue_key_from_project_name_with_extra_text
55
+ @entry.project_name = 'BUG-30 crazy bug'
56
+ assert_equal "BUG-30", @described_class.parse(@entry)
36
57
  end
37
58
  end
38
59
  end
@@ -19,6 +19,7 @@ Gem::Specification.new do |spec|
19
19
  spec.require_paths = ['lib']
20
20
 
21
21
  spec.required_ruby_version = '>= 2.0'
22
+ spec.add_development_dependency 'pry'
22
23
  spec.add_development_dependency 'bundler', '~> 1.3'
23
24
  spec.add_development_dependency 'rake', '~> 10.5'
24
25
  spec.add_development_dependency 'minitest', '~> 5.8'
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: time_log_robot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mark J. Lehman
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-06-16 00:00:00.000000000 Z
11
+ date: 2016-06-19 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: pry
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: bundler
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -179,9 +193,13 @@ files:
179
193
  - Rakefile
180
194
  - bin/time_log_robot
181
195
  - lib/time_log_robot.rb
196
+ - lib/time_log_robot/entry.rb
182
197
  - lib/time_log_robot/jira/issue_key_parser.rb
183
198
  - lib/time_log_robot/jira/payload_builder.rb
184
199
  - lib/time_log_robot/jira/work_logger.rb
200
+ - lib/time_log_robot/reporter.rb
201
+ - lib/time_log_robot/tagger.rb
202
+ - lib/time_log_robot/toggl/entry.rb
185
203
  - lib/time_log_robot/toggl/report.rb
186
204
  - lib/time_log_robot/toggl/tagger.rb
187
205
  - lib/time_log_robot/version.rb
@@ -215,4 +233,3 @@ summary: Automate work time logging
215
233
  test_files:
216
234
  - test/test_helper.rb
217
235
  - test/time_log_robot/jira/issue_key_parser_test.rb
218
- has_rdoc: