timetrap-harvest 1.0.0

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 03fab5bae6ce25a24e1981f32c5922b288873441
4
+ data.tar.gz: d8a7d3b7ab8afd04e425b2563ed6b5e34155e126
5
+ SHA512:
6
+ metadata.gz: d99ff0d276e539a1b7912ed936caad6e0f404161fe9ec945e5475ea195b28fba0fd860954b74588e2f26f32d53920f6d3576fc0b9378515c5131a80e13c0a5fd
7
+ data.tar.gz: 951ccb521d6f69789ce7a94ffb72b86ac4d23a30b8c516a267e8b590ebd020df8a9a34eae4499da311c22d7a46b582ef568b1c5d8bfcb662b1b7b9aa635c2194
@@ -0,0 +1,42 @@
1
+ require_relative './timetrap_harvest/config'
2
+ require_relative './timetrap_harvest/network_client'
3
+ require_relative './timetrap_harvest/formatter'
4
+ require_relative './timetrap_harvest/harvester'
5
+ require_relative './timetrap_harvest/output'
6
+
7
+ begin
8
+ Module.const_get('Timetrap')
9
+ rescue NameError
10
+ module Timetrap;
11
+ module Formatters; end
12
+ Config = { 'harvest' => { 'aliases' => {} } }
13
+ end
14
+ end
15
+
16
+ class Timetrap::Formatters::Harvest
17
+ attr_reader :entries
18
+ attr_writer :client, :config
19
+
20
+ def initialize(entries)
21
+ @entries = entries
22
+ end
23
+
24
+ def output
25
+ results = entries.map { |entry| TimetrapHarvest::Formatter.new(entry, config).format }
26
+
27
+ harvester = TimetrapHarvest::Harvester.new(results, client)
28
+ results = harvester.harvest
29
+
30
+ TimetrapHarvest::Output.new(results).generate
31
+ end
32
+
33
+ private
34
+
35
+ def config
36
+ @config ||= TimetrapHarvest::Config.new
37
+ end
38
+
39
+ def client
40
+ @client ||= TimetrapHarvest::NetworkClient.new(config)
41
+ end
42
+ end
@@ -0,0 +1,62 @@
1
+ class TimetrapHarvest::Config
2
+ MissingHarvestConfig = Class.new(StandardError)
3
+ MissingHarvestAliases = Class.new(StandardError)
4
+ MissingHarvestSubdomain = Class.new(StandardError)
5
+ DEFAULT_ROUND_IN_MINUTES = 15
6
+
7
+ attr_reader :timetrap_config
8
+
9
+ def initialize(timetrap_config = Timetrap::Config)
10
+ @timetrap_config = timetrap_config
11
+ end
12
+
13
+ def email
14
+ config['email']
15
+ end
16
+
17
+ def password
18
+ config['password']
19
+ end
20
+
21
+ def subdomain
22
+ ensure_subdomain!
23
+
24
+ config['subdomain']
25
+ end
26
+
27
+ def round_in_minutes
28
+ config['round_in_minutes'] || DEFAULT_ROUND_IN_MINUTES
29
+ end
30
+
31
+ def alias_config(code)
32
+ if config = aliases[code]
33
+ config = config.split(' ')
34
+
35
+ { project_id: config.first, task_id: config.last }
36
+ end
37
+ end
38
+
39
+ def aliases
40
+ ensure_aliases!
41
+
42
+ config['aliases']
43
+ end
44
+
45
+ def config
46
+ ensure_config!
47
+
48
+ timetrap_config['harvest']
49
+ end
50
+
51
+ def ensure_config!
52
+ fail(MissingHarvestConfig, 'Missing harvest key in .timetrap.yml config file') if timetrap_config.nil? || timetrap_config['harvest'].nil?
53
+ end
54
+
55
+ def ensure_aliases!
56
+ fail(MissingHarvestAliases, 'Missing aliases key in .timetrap.yml config file') if config['aliases'].nil?
57
+ end
58
+
59
+ def ensure_subdomain!
60
+ fail(MissingHarvestSubdomain, 'Missing subdomain key in .timetrap.yml config file') if config['subdomain'].nil?
61
+ end
62
+ end
@@ -0,0 +1,57 @@
1
+ class TimetrapHarvest::Formatter
2
+ HARVESTABLE_REGEX = /@(.*)/
3
+
4
+ attr_reader :entry, :config
5
+
6
+ def initialize(entry, config)
7
+ @entry = entry
8
+ @config = config
9
+ end
10
+
11
+ def format
12
+ if alias_config
13
+ { notes: entry[:note],
14
+ hours: hours_for_time(entry[:start], entry[:end]),
15
+ project_id: project_id.to_i,
16
+ task_id: task_id.to_i,
17
+ spent_at: entry[:start].strftime('%Y%m%d')
18
+ }
19
+ elsif code
20
+ { error: 'Missing task alias config', note: entry[:note] }
21
+ else
22
+ { error: 'No task alias provided', note: entry[:note] }
23
+ end
24
+ end
25
+
26
+ def project_id
27
+ alias_config[:project_id]
28
+ end
29
+
30
+ def task_id
31
+ alias_config[:task_id]
32
+ end
33
+
34
+ def alias_config
35
+ config.alias_config(code)
36
+ end
37
+
38
+ def round_in_minutes
39
+ config.round_in_minutes
40
+ end
41
+
42
+ def code
43
+ if match = HARVESTABLE_REGEX.match(entry[:note])
44
+ code = match[1]
45
+ end
46
+ end
47
+
48
+ def hours_for_time(start_time, end_time)
49
+ minutes = (end_time - start_time) / 60
50
+ rounded = round(minutes)
51
+ hours = (rounded / 60)
52
+ end
53
+
54
+ def round(value, nearest = round_in_minutes)
55
+ (value % nearest).zero? ? value : (value + nearest) - (value % nearest)
56
+ end
57
+ end
@@ -0,0 +1,30 @@
1
+ class TimetrapHarvest::Harvester
2
+ attr_reader :results, :client
3
+
4
+ def initialize(results, client)
5
+ @results = results
6
+ @client = client
7
+ end
8
+
9
+ def harvest
10
+ results.each do |result|
11
+ if result.key? :error
12
+ failed << result
13
+ else
14
+ client.post(result)
15
+
16
+ submitted << result
17
+ end
18
+ end
19
+
20
+ { submitted: submitted, failed: failed }
21
+ end
22
+
23
+ def submitted
24
+ @submitted ||= []
25
+ end
26
+
27
+ def failed
28
+ @failed ||= []
29
+ end
30
+ end
@@ -0,0 +1,29 @@
1
+ require 'net/http'
2
+ require 'json'
3
+ require 'uri'
4
+
5
+ class TimetrapHarvest::NetworkClient
6
+ attr_reader :email, :password, :subdomain
7
+
8
+ def initialize(config)
9
+ @email = config.email
10
+ @password = config.password
11
+ @subdomain = config.subdomain
12
+ end
13
+
14
+ def post(payload)
15
+ req = Net::HTTP::Post.new(harvest_add_uri.request_uri)
16
+ req.basic_auth(email, password)
17
+ req.body = payload.to_json
18
+ req['Content-Type'] = 'application/json'
19
+ req['Accept'] = 'application/json'
20
+
21
+ res = Net::HTTP.start(harvest_add_uri.hostname, harvest_add_uri.port, use_ssl: true) do |http|
22
+ http.request(req)
23
+ end
24
+ end
25
+
26
+ def harvest_add_uri
27
+ URI("https://#{subdomain}.harvestapp.com/daily/add")
28
+ end
29
+ end
@@ -0,0 +1,51 @@
1
+ class TimetrapHarvest::Output
2
+ LINE_DIVIDER = '-' * 80
3
+ SUBMITTED_HEADER = "Submitted entries\n#{LINE_DIVIDER}"
4
+ FAILED_HEADER = "Failed entries\n#{LINE_DIVIDER}"
5
+
6
+ attr_reader :results
7
+
8
+ def initialize(results = {})
9
+ @results = results
10
+ end
11
+
12
+ def generate
13
+ messages = [stats]
14
+
15
+ unless submitted.empty?
16
+ messages << SUBMITTED_HEADER
17
+ messages += submitted.map { |submitted| success_message(submitted[:notes]) }
18
+ messages << "\n"
19
+ end
20
+
21
+ unless failed.empty?
22
+ messages << FAILED_HEADER
23
+ messages += failed.map { |failed| failed_message(failed[:note], failed[:error]) }
24
+ messages << "\n"
25
+ end
26
+
27
+ messages.join("\n")
28
+ end
29
+
30
+ private
31
+
32
+ def stats
33
+ "Submitted: #{submitted.count}\nFailed: #{failed.count}\n"
34
+ end
35
+
36
+ def submitted
37
+ results.fetch(:submitted, [])
38
+ end
39
+
40
+ def failed
41
+ results.fetch(:failed, [])
42
+ end
43
+
44
+ def success_message(note)
45
+ "Submitted: #{note}"
46
+ end
47
+
48
+ def failed_message(note, error)
49
+ "Failed (#{error}): #{note}"
50
+ end
51
+ end
@@ -0,0 +1,3 @@
1
+ module TimetrapHarvest
2
+ VERSION = '1.0.0'
3
+ end
metadata ADDED
@@ -0,0 +1,113 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: timetrap-harvest
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Devon Blandin
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-06-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: timetrap
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.7'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.7.0
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '1.7'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 1.7.0
33
+ - !ruby/object:Gem::Dependency
34
+ name: rspec
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '3.0'
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: 3.0.0
43
+ type: :development
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: '3.0'
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 3.0.0
53
+ - !ruby/object:Gem::Dependency
54
+ name: pry
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '0.10'
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: 0.10.0
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '0.10'
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: 0.10.0
73
+ description: |2
74
+ timetrap-harvest bridges the gap between your entries in Timetrap and your
75
+ project tasks in Harvest allowing for incredible easy timesheet
76
+ submissions.
77
+ email: dblandin@gmail.com
78
+ executables: []
79
+ extensions: []
80
+ extra_rdoc_files: []
81
+ files:
82
+ - lib/timetrap-harvest.rb
83
+ - lib/timetrap_harvest/config.rb
84
+ - lib/timetrap_harvest/formatter.rb
85
+ - lib/timetrap_harvest/harvester.rb
86
+ - lib/timetrap_harvest/network_client.rb
87
+ - lib/timetrap_harvest/output.rb
88
+ - lib/timetrap_harvest/version.rb
89
+ homepage: https://github.com/dblandin/timetrap-harvest
90
+ licenses:
91
+ - MIT
92
+ metadata: {}
93
+ post_install_message:
94
+ rdoc_options: []
95
+ require_paths:
96
+ - lib
97
+ required_ruby_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ required_rubygems_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ requirements: []
108
+ rubyforge_project:
109
+ rubygems_version: 2.3.0
110
+ signing_key:
111
+ specification_version: 4
112
+ summary: A Harvest formatter for Timetrap
113
+ test_files: []