beeminder 0.2.3 → 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
data/TODO CHANGED
@@ -1,4 +1,5 @@
1
- * TODO [0/3]
1
+ * TODO [0/4]
2
2
  - [ ] add a few more sanity checks
3
3
  - [ ] cache goals / datapoints
4
4
  - [ ] add update function to Datapoint
5
+ - [ ] constructors could be more elegant, but don't wanna break compatibility again, so it can wait until the next major rewrite
data/beeminder.gemspec CHANGED
@@ -16,8 +16,10 @@ Gem::Specification.new do |gem|
16
16
  gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
17
  gem.require_paths = ["lib"]
18
18
 
19
+ gem.add_dependency 'activesupport', '~> 3.2'
19
20
  gem.add_dependency 'chronic', '~> 0.7'
20
21
  gem.add_dependency 'json'
21
22
  gem.add_dependency 'highline', '~> 1.6'
22
23
  gem.add_dependency 'trollop', '~> 2'
24
+ gem.add_dependency 'tzinfo'
23
25
  end
data/lib/beeminder.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # coding: utf-8
2
2
 
3
+ require 'active_support/core_ext'
3
4
  require 'date'
4
5
  require 'json'
5
6
  require 'net/https'
@@ -1,4 +1,4 @@
1
- # coding: utf-8
1
+ # -*- encoding: utf-8 -*-
2
2
 
3
3
  module Beeminder
4
4
  class Goal
@@ -81,7 +81,13 @@ module Beeminder
81
81
  # @return [Array<Beeminder::Datapoint>] returns list of datapoints
82
82
  def datapoints
83
83
  info = @user.get "users/me/goals/#{slug}/datapoints.json"
84
- datapoints = info.map{|d| Datapoint.new d}
84
+ datapoints = info.map do |d_info|
85
+ d_info = {
86
+ :goal => self,
87
+ }.merge(d_info)
88
+
89
+ Datapoint.new d_info
90
+ end
85
91
 
86
92
  datapoints
87
93
  end
@@ -106,8 +112,12 @@ module Beeminder
106
112
  def dial_road dials={}
107
113
  raise "Set exactly two dials." if dials.keys.size != 2
108
114
 
109
- dials["goaldate"] = dials["goaldate"].strftime('%s') unless dials["goaldate"].nil?
110
-
115
+ # convert to proper timestamp
116
+ unless dials["goaldate"].nil?
117
+ dials["goaldate"] = @user.convert_to_timezone dials["goaldate"] if @user.enforce_timezone?
118
+ dials["goaldate"] = dials["goaldate"].strftime('%s')
119
+ end
120
+
111
121
  @user.post "users/me/goals/#{@slug}/dial_road.json", dials
112
122
  end
113
123
 
@@ -117,12 +127,15 @@ module Beeminder
117
127
  def add datapoints, opts={}
118
128
  datapoints = [*datapoints]
119
129
 
120
- # FIXME create_all doesn't work because Ruby's POST encoding of arrays is broken.
121
130
  datapoints.each do |dp|
131
+ # we grab these datapoints for ourselves
132
+ dp.goal = self
133
+
122
134
  data = {
123
135
  "sendmail" => opts[:sendmail] || false
124
136
  }.merge(dp.to_hash)
125
137
 
138
+ # TODO create_all doesn't work because Ruby's POST encoding of arrays is broken.
126
139
  @user.post "users/me/goals/#{@slug}/datapoints.json", data
127
140
  end
128
141
  end
@@ -160,10 +173,10 @@ module Beeminder
160
173
  end
161
174
 
162
175
  # some conversions
163
- @goaldate = DateTime.strptime(@goaldate.to_s, '%s') unless @goaldate.nil?
176
+ @goaldate = DateTime.strptime(@goaldate.to_s, '%s').in_time_zone(@user.timezone) unless @goaldate.nil?
164
177
  @goal_type = @goal_type.to_sym unless @goal_type.nil?
165
- @losedate = DateTime.strptime(@losedate.to_s, '%s') unless @losedate.nil?
166
- @updated_at = DateTime.strptime(@updated_at.to_s, '%s')
178
+ @losedate = DateTime.strptime(@losedate.to_s, '%s').in_time_zone(@user.timezone) unless @losedate.nil?
179
+ @updated_at = DateTime.strptime(@updated_at.to_s, '%s').in_time_zone(@user.timezone)
167
180
  end
168
181
  end
169
182
 
@@ -183,6 +196,10 @@ module Beeminder
183
196
  # @return [DateTime] The time that this datapoint was entered or last updated.
184
197
  attr_reader :updated_at
185
198
 
199
+ # @return [Beeminder::Goal] Goal this datapoint belongs to.
200
+ # Optional for new datapoints. Use `Goal#add` to add new datapoints to a goal.
201
+ attr_accessor :goal
202
+
186
203
  def initialize info={}
187
204
  # set variables
188
205
  info.each do |k,v|
@@ -196,11 +213,21 @@ module Beeminder
196
213
  # some conversions
197
214
  @timestamp = DateTime.strptime(@timestamp.to_s, '%s') unless @timestamp.is_a?(Date) || @timestamp.is_a?(Time)
198
215
  @updated_at = DateTime.strptime(@updated_at.to_s, '%s') unless @updated_at.nil?
216
+
217
+ # set timezone if possible
218
+ unless @goal.nil?
219
+ @timestamp = @timestamp.in_time_zone @goal.user.timezone
220
+ @updated_at = @updated_at.in_time_zone @goal.user.timezone
221
+ end
199
222
  end
200
223
 
201
224
  # Convert datapoint to hash for POSTing.
202
225
  # @return [Hash]
203
226
  def to_hash
227
+ if not @goal.nil? and @goal.user.enforce_timezone?
228
+ @timestamp = @goal.user.convert_to_timezone @timestamp
229
+ end
230
+
204
231
  {
205
232
  "timestamp" => @timestamp.strftime('%s'),
206
233
  "value" => @value || 0,
@@ -1,4 +1,4 @@
1
- # coding: utf-8
1
+ # -*- encoding: utf-8 -*-
2
2
 
3
3
  module Beeminder
4
4
  class User
@@ -16,14 +16,19 @@ module Beeminder
16
16
 
17
17
  # @return [Symbol] Type of user, can be `:personal` (default) or `:oauth`.
18
18
  attr_reader :auth_type
19
+
20
+ # @return [true|false] Enforce user timezone for all passed times? Should be true unless you know what you're doing. (Default: `true`.)
21
+ attr_accessor :enforce_timezone
19
22
 
20
23
  def initialize token, opts={}
21
24
  opts = {
22
25
  :auth_type => :personal,
26
+ :enforce_timezone => true,
23
27
  }.merge(opts)
24
28
 
25
- @token = token
26
- @auth_type = opts[:auth_type]
29
+ @token = token
30
+ @auth_type = opts[:auth_type]
31
+ @enforce_timezone = opts[:enforce_timezone]
27
32
 
28
33
  @token_type =
29
34
  case @auth_type
@@ -38,14 +43,21 @@ module Beeminder
38
43
  info = get "users/me.json"
39
44
 
40
45
  @name = info["username"]
41
- @timezone = info["timezone"]
42
- @updated_at = DateTime.strptime(info["updated_at"].to_s, '%s')
46
+ @timezone = info["timezone"] || "UTC"
47
+ @updated_at = DateTime.strptime(info["updated_at"].to_s, '%s').in_time_zone(@timezone)
43
48
  end
44
49
 
50
+ # Enforce timezone for all passed times?
51
+ #
52
+ # @return [true|false]
53
+ def enforce_timezone?
54
+ !!@enforce_timezone
55
+ end
56
+
45
57
  # List of goals.
46
58
  #
47
59
  # @param filter [Symbol] filter goals, can be `:all` (default), `:frontburner` or `:backburner`
48
- # @ return [Array<Beeminder::Goal>] returns list of goals
60
+ # @return [Array<Beeminder::Goal>] returns list of goals
49
61
  def goals filter=:all
50
62
  raise "invalid goal filter: #{filter}" unless [:all, :frontburner, :backburner].include? filter
51
63
 
@@ -115,8 +127,25 @@ module Beeminder
115
127
  _connection :put, cmd, data
116
128
  end
117
129
 
118
- private
130
+ # Converts time object to one with user's timezone.
131
+ #
132
+ # @param time [Date|DateTime|Time] Time to convert.
133
+ # @return [Time] Converted time.
134
+ def convert_to_timezone time
135
+ # TODO seems way too hack-ish
136
+ old_tz = Time.zone
137
+ Time.zone = @timezone
138
+
139
+ time = time.to_time
140
+ t = Time.new(time.year, time.month, time.day, time.hour, time.min, time.sec)
141
+
142
+ Time.zone = old_tz
143
+
144
+ t
145
+ end
119
146
 
147
+ private
148
+
120
149
  # Establish HTTPS connection to API.
121
150
  def _connection type, cmd, data
122
151
  api = "https://www.beeminder.com/api/v1/#{cmd}"
@@ -128,7 +157,7 @@ module Beeminder
128
157
  http.use_ssl = true
129
158
  http.verify_mode = OpenSSL::SSL::VERIFY_NONE # FIXME: actually verify
130
159
 
131
- # FIXME Sanity check for wrong timestamp. Source of bug unknown, so we prevent screwing up someone's graph.
160
+ # FIXME Sanity check for wrong timestamp. Source of bug unknown, but at least we can prevent screwing up someone's graph.
132
161
  unless data["timestamp"].nil?
133
162
  if not data["timestamp"].match(/^\d+$/) or data["timestamp"] < "1280000000"
134
163
  raise ArgumentError, "invalid timestamp: #{data["timestamp"]}"
@@ -1,3 +1,3 @@
1
1
  module Beeminder
2
- VERSION = "0.2.3"
2
+ VERSION = "0.2.4"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: beeminder
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.2.4
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,8 +9,24 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-09-16 00:00:00.000000000 Z
12
+ date: 2012-09-19 00:00:00.000000000 Z
13
13
  dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activesupport
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '3.2'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '3.2'
14
30
  - !ruby/object:Gem::Dependency
15
31
  name: chronic
16
32
  requirement: !ruby/object:Gem::Requirement
@@ -75,6 +91,22 @@ dependencies:
75
91
  - - ~>
76
92
  - !ruby/object:Gem::Version
77
93
  version: '2'
94
+ - !ruby/object:Gem::Dependency
95
+ name: tzinfo
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :runtime
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
78
110
  description: Convenient access to Beeminder's API.
79
111
  email:
80
112
  - mail@muflax.com
@@ -110,7 +142,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
110
142
  version: '0'
111
143
  segments:
112
144
  - 0
113
- hash: -4420533533001191387
145
+ hash: 4040090933613779056
114
146
  required_rubygems_version: !ruby/object:Gem::Requirement
115
147
  none: false
116
148
  requirements:
@@ -119,7 +151,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
119
151
  version: '0'
120
152
  segments:
121
153
  - 0
122
- hash: -4420533533001191387
154
+ hash: 4040090933613779056
123
155
  requirements: []
124
156
  rubyforge_project:
125
157
  rubygems_version: 1.8.23