Permalink
Browse files

fix timezone bugs

  • Loading branch information...
1 parent 6a152fc commit 092a500d381b22d1c050961d19292751ee5e00b9 @muflax muflax committed Sep 19, 2012
Showing with 78 additions and 18 deletions.
  1. +2 −1 TODO
  2. +2 −0 beeminder.gemspec
  3. +1 −0 lib/beeminder.rb
  4. +35 −8 lib/beeminder/goals.rb
  5. +37 −8 lib/beeminder/user.rb
  6. +1 −1 lib/beeminder/version.rb
View
3 TODO
@@ -1,4 +1,5 @@
-* TODO [0/3]
+* TODO [0/4]
- [ ] add a few more sanity checks
- [ ] cache goals / datapoints
- [ ] add update function to Datapoint
+- [ ] constructors could be more elegant, but don't wanna break compatibility again, so it can wait until the next major rewrite
View
@@ -16,8 +16,10 @@ Gem::Specification.new do |gem|
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
gem.require_paths = ["lib"]
+ gem.add_dependency 'activesupport', '~> 3.2'
gem.add_dependency 'chronic', '~> 0.7'
gem.add_dependency 'json'
gem.add_dependency 'highline', '~> 1.6'
gem.add_dependency 'trollop', '~> 2'
+ gem.add_dependency 'tzinfo'
end
View
@@ -1,5 +1,6 @@
# coding: utf-8
+require 'active_support/core_ext'
require 'date'
require 'json'
require 'net/https'
View
@@ -1,4 +1,4 @@
-# coding: utf-8
+# -*- encoding: utf-8 -*-
module Beeminder
class Goal
@@ -81,7 +81,13 @@ def reload
# @return [Array<Beeminder::Datapoint>] returns list of datapoints
def datapoints
info = @user.get "users/me/goals/#{slug}/datapoints.json"
- datapoints = info.map{|d| Datapoint.new d}
+ datapoints = info.map do |d_info|
+ d_info = {
+ :goal => self,
+ }.merge(d_info)
+
+ Datapoint.new d_info
+ end
datapoints
end
@@ -106,8 +112,12 @@ def update
def dial_road dials={}
raise "Set exactly two dials." if dials.keys.size != 2
- dials["goaldate"] = dials["goaldate"].strftime('%s') unless dials["goaldate"].nil?
-
+ # convert to proper timestamp
+ unless dials["goaldate"].nil?
+ dials["goaldate"] = @user.convert_to_timezone dials["goaldate"] if @user.enforce_timezone?
+ dials["goaldate"] = dials["goaldate"].strftime('%s')
+ end
+
@user.post "users/me/goals/#{@slug}/dial_road.json", dials
end
@@ -117,12 +127,15 @@ def dial_road dials={}
def add datapoints, opts={}
datapoints = [*datapoints]
- # FIXME create_all doesn't work because Ruby's POST encoding of arrays is broken.
datapoints.each do |dp|
+ # we grab these datapoints for ourselves
+ dp.goal = self
+
data = {
"sendmail" => opts[:sendmail] || false
}.merge(dp.to_hash)
+ # TODO create_all doesn't work because Ruby's POST encoding of arrays is broken.
@user.post "users/me/goals/#{@slug}/datapoints.json", data
end
end
@@ -160,10 +173,10 @@ def _parse_info info
end
# some conversions
- @goaldate = DateTime.strptime(@goaldate.to_s, '%s') unless @goaldate.nil?
+ @goaldate = DateTime.strptime(@goaldate.to_s, '%s').in_time_zone(@user.timezone) unless @goaldate.nil?
@goal_type = @goal_type.to_sym unless @goal_type.nil?
- @losedate = DateTime.strptime(@losedate.to_s, '%s') unless @losedate.nil?
- @updated_at = DateTime.strptime(@updated_at.to_s, '%s')
+ @losedate = DateTime.strptime(@losedate.to_s, '%s').in_time_zone(@user.timezone) unless @losedate.nil?
+ @updated_at = DateTime.strptime(@updated_at.to_s, '%s').in_time_zone(@user.timezone)
end
end
@@ -183,6 +196,10 @@ class Datapoint
# @return [DateTime] The time that this datapoint was entered or last updated.
attr_reader :updated_at
+ # @return [Beeminder::Goal] Goal this datapoint belongs to.
+ # Optional for new datapoints. Use `Goal#add` to add new datapoints to a goal.
+ attr_accessor :goal
+
def initialize info={}
# set variables
info.each do |k,v|
@@ -196,11 +213,21 @@ def initialize info={}
# some conversions
@timestamp = DateTime.strptime(@timestamp.to_s, '%s') unless @timestamp.is_a?(Date) || @timestamp.is_a?(Time)
@updated_at = DateTime.strptime(@updated_at.to_s, '%s') unless @updated_at.nil?
+
+ # set timezone if possible
+ unless @goal.nil?
+ @timestamp = @timestamp.in_time_zone @goal.user.timezone
+ @updated_at = @updated_at.in_time_zone @goal.user.timezone
+ end
end
# Convert datapoint to hash for POSTing.
# @return [Hash]
def to_hash
+ if not @goal.nil? and @goal.user.enforce_timezone?
+ @timestamp = @goal.user.convert_to_timezone @timestamp
+ end
+
{
"timestamp" => @timestamp.strftime('%s'),
"value" => @value || 0,
View
@@ -1,4 +1,4 @@
-# coding: utf-8
+# -*- encoding: utf-8 -*-
module Beeminder
class User
@@ -16,14 +16,19 @@ class User
# @return [Symbol] Type of user, can be `:personal` (default) or `:oauth`.
attr_reader :auth_type
+
+ # @return [true|false] Enforce user timezone for all passed times? Should be true unless you know what you're doing. (Default: `true`.)
+ attr_accessor :enforce_timezone
def initialize token, opts={}
opts = {
:auth_type => :personal,
+ :enforce_timezone => true,
}.merge(opts)
- @token = token
- @auth_type = opts[:auth_type]
+ @token = token
+ @auth_type = opts[:auth_type]
+ @enforce_timezone = opts[:enforce_timezone]
@token_type =
case @auth_type
@@ -38,14 +43,21 @@ def initialize token, opts={}
info = get "users/me.json"
@name = info["username"]
- @timezone = info["timezone"]
- @updated_at = DateTime.strptime(info["updated_at"].to_s, '%s')
+ @timezone = info["timezone"] || "UTC"
+ @updated_at = DateTime.strptime(info["updated_at"].to_s, '%s').in_time_zone(@timezone)
end
+ # Enforce timezone for all passed times?
+ #
+ # @return [true|false]
+ def enforce_timezone?
+ !!@enforce_timezone
+ end
+
# List of goals.
#
# @param filter [Symbol] filter goals, can be `:all` (default), `:frontburner` or `:backburner`
- # @ return [Array<Beeminder::Goal>] returns list of goals
+ # @return [Array<Beeminder::Goal>] returns list of goals
def goals filter=:all
raise "invalid goal filter: #{filter}" unless [:all, :frontburner, :backburner].include? filter
@@ -115,8 +127,25 @@ def put cmd, data={}
_connection :put, cmd, data
end
- private
+ # Converts time object to one with user's timezone.
+ #
+ # @param time [Date|DateTime|Time] Time to convert.
+ # @return [Time] Converted time.
+ def convert_to_timezone time
+ # TODO seems way too hack-ish
+ old_tz = Time.zone
+ Time.zone = @timezone
+
+ time = time.to_time
+ t = Time.new(time.year, time.month, time.day, time.hour, time.min, time.sec)
+
+ Time.zone = old_tz
+
+ t
+ end
+ private
+
# Establish HTTPS connection to API.
def _connection type, cmd, data
api = "https://www.beeminder.com/api/v1/#{cmd}"
@@ -128,7 +157,7 @@ def _connection type, cmd, data
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE # FIXME: actually verify
- # FIXME Sanity check for wrong timestamp. Source of bug unknown, so we prevent screwing up someone's graph.
+ # FIXME Sanity check for wrong timestamp. Source of bug unknown, but at least we can prevent screwing up someone's graph.
unless data["timestamp"].nil?
if not data["timestamp"].match(/^\d+$/) or data["timestamp"] < "1280000000"
raise ArgumentError, "invalid timestamp: #{data["timestamp"]}"
View
@@ -1,3 +1,3 @@
module Beeminder
- VERSION = "0.2.3"
+ VERSION = "0.2.4"
end

0 comments on commit 092a500

Please sign in to comment.