timetrap-harvest 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []