costagent 0.1.6 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/README.rdoc +12 -0
  2. data/lib/costagent.rb +150 -84
  3. metadata +4 -4
data/README.rdoc CHANGED
@@ -12,6 +12,8 @@ This isn't meant to be an entire Ruby library for the FA API - this is simply su
12
12
 
13
13
  This assumes that when calculating money earnt, you want it in GBP, and it only copes with calculating timeslips either in GBP, or in USD (which is converted using xe.com where possible).
14
14
 
15
+ Version 0.2.0 introduces some breaking changes to the existing API - namely moving from structs to dedicated classes backed by hashes for the FreeAgent resources. It also introduces a couple of new methods, and the ability for a third party caching provider to be plugged in - but this now means that there is NO in memory caching of anything (such as projects) at all. In short, just using this library and not supplying even a basic cache provider will mean that every single request will result in an HTTP call. Some default caching providers coming soon.
16
+
15
17
  == SYNOPSIS:
16
18
 
17
19
  To initialize:
@@ -20,6 +22,12 @@ costagent = CostAgent.new subdomain, username, password
20
22
 
21
23
  (remember you need to enable API access from within your FreeAgent account)
22
24
 
25
+ To set a caching provider to limit repeat lookups to FreeAgent:
26
+
27
+ CostAgent.cache_provider = MyCacheProvider.new
28
+
29
+ (see the tests for a reference in memory cache provider)
30
+
23
31
  To see all active projects:
24
32
 
25
33
  projects = costagent.projects
@@ -52,6 +60,10 @@ To return your FA user ID:
52
60
 
53
61
  costagent.user_id
54
62
 
63
+ And to return all details about the logged in user:
64
+
65
+ costagent.user
66
+
55
67
  To return a total of the amount of hours worked for a specific timeframe:
56
68
 
57
69
  hours = costagent.worked(start_date, end_date)
data/lib/costagent.rb CHANGED
@@ -6,14 +6,53 @@ require "open-uri"
6
6
 
7
7
  # This exposes additional billable tracking functionality around the Freeagent API
8
8
  class CostAgent
9
- Project = Struct.new(:id, :name, :currency, :hourly_billing_rate, :daily_billing_rate, :hours_per_day)
10
- Timeslip = Struct.new(:id, :project, :task, :hours, :date, :cost, :comment, :status)
11
- Task = Struct.new(:id, :name, :project, :hourly_billing_rate, :daily_billing_rate, :billable)
12
- Invoice = Struct.new(:id, :project_id, :description, :reference, :amount, :status, :date, :due, :items)
13
- InvoiceItem = Struct.new(:id, :invoice_id, :project_id, :item_type, :description, :price, :quantity, :cost)
9
+ # This provides shared functionality for all of the wrapped FA data types
10
+ class Base
11
+ attr_accessor :data
14
12
 
13
+ def initialize(data = {})
14
+ @data = data
15
+ end
16
+
17
+ def method_missing(name, *args)
18
+ key = name.to_s
19
+ if key[key.length - 1] == "="
20
+ @data[key[0...key.length - 1]] = args.first
21
+ end
22
+ @data[key] || @data[key.to_sym]
23
+ end
24
+ end
25
+
26
+ # Our FA data types
27
+ class Project < Base; end
28
+ class Timeslip < Base; end
29
+ class Task < Base; end
30
+ class Invoice < Base; end
31
+ class InvoiceItem < Base; end
32
+ class User < Base; end
33
+
34
+ # Our configuration for FA access
15
35
  attr_accessor :subdomain, :username, :password
16
36
 
37
+ # Our cache provider, to help to limit the amount of requests and lookups to FA
38
+ class << self
39
+ attr_accessor :cache_provider
40
+ end
41
+
42
+ # This calls out to the external third party provider for caching
43
+ def cache(resource, identifier, reload = false, &block)
44
+ if CostAgent.cache_provider.nil?
45
+ block.call
46
+ else
47
+ if (!reload && CostAgent.cache_provider.exists?(self.subdomain, resource, identifier))
48
+ CostAgent.cache_provider.get(self.subdomain, resource, identifier)
49
+ else
50
+ CostAgent.cache_provider.set(self.subdomain, resource, identifier, block.call)
51
+ end
52
+ end
53
+ end
54
+
55
+ # Initialize and validate input data
17
56
  def initialize(subdomain, username, password)
18
57
  self.subdomain = subdomain
19
58
  self.username = username
@@ -25,101 +64,117 @@ class CostAgent
25
64
  end
26
65
 
27
66
  # Returns all projects
28
- def projects(filter = "active")
29
- @projects ||= {}
30
- @projects[filter] ||= (self.api("projects", {:view => filter})/"project").collect do |project|
31
- billing_rate = (project/"normal-billing-rate").text.to_f
32
- hours_per_day = (project/"hours-per-day").text.to_f
33
- billing_period = (project/"billing-period").text
34
- hourly_rate = (billing_period == "hour" ? billing_rate : billing_rate / hours_per_day)
35
- daily_rate = (billing_period == "hour" ? billing_rate * hours_per_day : billing_rate)
36
- Project.new((project/"id").text.to_i,
37
- (project/"name").text,
38
- (project/"currency").text,
39
- hourly_rate,
40
- daily_rate,
41
- hours_per_day)
67
+ def projects(filter = "active", reload = false)
68
+ self.cache(CostAgent::Project, filter, reload) do
69
+ (self.api("projects", {:view => filter})/"project").collect do |project|
70
+ billing_rate = (project/"normal-billing-rate").text.to_f
71
+ hours_per_day = (project/"hours-per-day").text.to_f
72
+ billing_period = (project/"billing-period").text
73
+ hourly_rate = (billing_period == "hour" ? billing_rate : billing_rate / hours_per_day)
74
+ daily_rate = (billing_period == "hour" ? billing_rate * hours_per_day : billing_rate)
75
+ Project.new(
76
+ :id => (project/"id").text.to_i,
77
+ :name => (project/"name").text,
78
+ :currency => (project/"currency").text,
79
+ :hourly_billing_rate => hourly_rate,
80
+ :daily_billing_rate => daily_rate,
81
+ :hours_per_day => hours_per_day)
82
+ end
42
83
  end
43
84
  end
44
-
85
+
45
86
  # This returns the specified project
46
87
  def project(id)
47
88
  self.projects("all").detect { |p| p.id == id }
48
89
  end
49
90
 
50
91
  # This returns all timeslips for the specified date range, with additional cost information
51
- def timeslips(start_date = DateTime.now, end_date = start_date)
52
- (self.api("timeslips", :view => "#{start_date.strftime("%Y-%m-%d")}_#{end_date.strftime("%Y-%m-%d")}")/"timeslip").collect do |timeslip|
53
- # Find the project and hours for this timeslip
54
- project = self.project((timeslip/"project-id").text.to_i)
55
- if project
56
- task = self.tasks(project.id).detect { |t| t.id == (timeslip/"task-id").text.to_i }
57
- hours = (timeslip/"hours").text.to_f
58
- cost = (task.nil? ? project : task).hourly_billing_rate * hours
59
- # Build the timeslip out using the timeslip data and the project it's tied to
60
- Timeslip.new((timeslip/"id").text.to_i,
61
- project,
62
- task,
63
- hours,
64
- DateTime.parse((timeslip/"dated-on").text),
65
- cost,
66
- (timeslip/"comment").text,
67
- (timeslip/"status").text)
68
- else
69
- nil
70
- end
71
- end - [nil]
92
+ def timeslips(start_date = DateTime.now, end_date = start_date, reload = false)
93
+ self.cache(CostAgent::Timeslip, "#{start_date.strftime("%Y-%m-%d")}_#{end_date.strftime("%Y-%m-%d")}", reload) do
94
+ timeslips = (self.api("timeslips", :view => "#{start_date.strftime("%Y-%m-%d")}_#{end_date.strftime("%Y-%m-%d")}")/"timeslip").collect do |timeslip|
95
+ # Find the project and hours for this timeslip
96
+ project = self.project((timeslip/"project-id").text.to_i)
97
+ if project
98
+ task = self.tasks(project.id).detect { |t| t.id == (timeslip/"task-id").text.to_i }
99
+ hours = (timeslip/"hours").text.to_f
100
+ cost = (task.nil? ? project : task).hourly_billing_rate * hours
101
+ # Build the timeslip out using the timeslip data and the project it's tied to
102
+ Timeslip.new(
103
+ :id => (timeslip/"id").text.to_i,
104
+ :project_id => project.id,
105
+ :project => project,
106
+ :task_id => task.id,
107
+ :task => task,
108
+ :hours => hours,
109
+ :date => DateTime.parse((timeslip/"dated-on").text),
110
+ :cost => cost,
111
+ :comment => (timeslip/"comment").text,
112
+ :status => (timeslip/"status").text)
113
+ else
114
+ nil
115
+ end
116
+ end - [nil]
117
+ end
72
118
  end
73
119
 
74
120
  # This returns all tasks for the specified project_id
75
- def tasks(project_id)
76
- (self.api("projects/#{project_id}/tasks")/"task").collect do |task|
77
- # Find the project for this task
78
- project = self.project((task/"project-id").text.to_i)
79
- # Calculate rates
80
- billing_rate = (task/"billing-rate").text.to_f
81
- billing_period = (task/"billing-period").text
82
- hourly_rate = (billing_period == "hour" ? billing_rate : billing_rate / project.hours_per_day)
83
- daily_rate = (billing_period == "hour" ? billing_rate * project.hours_per_day : billing_rate)
84
- # Build the task out using the task data and the project it's tied to
85
- Task.new((task/"id").text.to_i,
86
- (task/"name").text,
87
- project,
88
- hourly_rate,
89
- daily_rate,
90
- (task/"is-billable").text == "true")
121
+ def tasks(project_id, reload = false)
122
+ self.cache(CostAgent::Task, project_id, reload) do
123
+ (self.api("projects/#{project_id}/tasks")/"task").collect do |task|
124
+ # Find the project for this task
125
+ project = self.project((task/"project-id").text.to_i)
126
+ # Calculate rates
127
+ billing_rate = (task/"billing-rate").text.to_f
128
+ billing_period = (task/"billing-period").text
129
+ hourly_rate = (billing_period == "hour" ? billing_rate : billing_rate / project.hours_per_day)
130
+ daily_rate = (billing_period == "hour" ? billing_rate * project.hours_per_day : billing_rate)
131
+ # Build the task out using the task data and the project it's tied to
132
+ Task.new(
133
+ :id => (task/"id").text.to_i,
134
+ :name => (task/"name").text,
135
+ :project_id => project.id,
136
+ :project => project,
137
+ :hourly_billing_rate => hourly_rate,
138
+ :daily_billing_rate => daily_rate,
139
+ :billable => (task/"is-billable").text == "true")
140
+ end
91
141
  end
92
142
  end
93
143
 
94
144
  # This returns all invoices
95
- def invoices
96
- @invoices ||= (self.api("invoices")/"invoice").collect do |invoice|
97
- items = (invoice/"invoice-item").collect do |item|
98
- price = (item/"price").first.inner_text.to_f
99
- quantity = (item/"quantity").first.inner_text.to_f
100
- cost = price * quantity
101
- InvoiceItem.new(
102
- (item/"id").first.inner_text.to_i,
103
- (item/"invoice-id").first.inner_text.to_i,
104
- (item/"project-id").first.inner_text.to_i,
105
- (item/"item-type").first.inner_text,
106
- (item/"description").first.inner_text,
107
- price,
108
- quantity,
109
- cost)
145
+ def invoices(reload = false)
146
+ self.cache(CostAgent::Invoice, :all, reload) do
147
+ (self.api("invoices")/"invoice").collect do |invoice|
148
+ items = (invoice/"invoice-item").collect do |item|
149
+ price = (item/"price").first.inner_text.to_f
150
+ quantity = (item/"quantity").first.inner_text.to_f
151
+ cost = price * quantity
152
+ project = self.project((item/"project-id").first.inner_text.to_i)
153
+ InvoiceItem.new(
154
+ :id => (item/"id").first.inner_text.to_i,
155
+ :invoice_id => (item/"invoice-id").first.inner_text.to_i,
156
+ :project_id => project.id,
157
+ :project => project,
158
+ :item_type => (item/"item-type").first.inner_text,
159
+ :description => (item/"description").first.inner_text,
160
+ :price => price,
161
+ :quantity => quantity,
162
+ :cost => cost)
163
+ end
164
+ project = self.project((invoice/"project-id").first.inner_text.to_i)
165
+ Invoice.new(
166
+ :id => (invoice/"id").first.inner_text.to_i,
167
+ :project_id => project.id,
168
+ :project => project,
169
+ :description => (invoice/"description").first.inner_text,
170
+ :reference => (invoice/"reference").text,
171
+ :amount => (invoice/"net-value").text.to_f,
172
+ :status => (invoice/"status").text,
173
+ :date => DateTime.parse((invoice/"dated-on").text),
174
+ :due => DateTime.parse((invoice/"due-on").text),
175
+ :items => items)
110
176
  end
111
- Invoice.new(
112
- (invoice/"id").first.inner_text.to_i,
113
- (invoice/"project-id").first.inner_text.to_i,
114
- (invoice/"description").first.inner_text,
115
- (invoice/"reference").text,
116
- (invoice/"net-value").text.to_f,
117
- (invoice/"status").text,
118
- DateTime.parse((invoice/"dated-on").text),
119
- DateTime.parse((invoice/"due-on").text),
120
- items)
121
177
  end
122
- @invoices
123
178
  end
124
179
 
125
180
  # This returns the specific invoice by ID
@@ -127,9 +182,20 @@ class CostAgent
127
182
  self.invoices.detect { |i| i.id == id }
128
183
  end
129
184
 
185
+ # This contains the logged in user information for the configured credentials
186
+ def user(reload = false)
187
+ self.cache(CostAgent::User, self.username, reload) do
188
+ data = self.client("verify").get.headers
189
+ [User.new(
190
+ :id => data[:user_id],
191
+ :permissions => data[:user_permission_level],
192
+ :company_type => data[:company_type])]
193
+ end.first
194
+ end
195
+
130
196
  # This looks up the user ID using the CostAgent credentials
131
197
  def user_id
132
- self.client("verify").get.headers[:user_id]
198
+ self.user.id
133
199
  end
134
200
 
135
201
  # This returns the amount of hours worked
metadata CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 0
7
- - 1
8
- - 6
9
- version: 0.1.6
7
+ - 2
8
+ - 0
9
+ version: 0.2.0
10
10
  platform: ruby
11
11
  authors:
12
12
  - Elliott Draper
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-10-11 00:00:00 +01:00
17
+ date: 2010-10-13 00:00:00 +01:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency