Module: TogglCache

Defined in:
lib/toggl_cache.rb,
lib/toggl_cache/data.rb,
lib/toggl_cache/version.rb,
lib/toggl_cache/data/report_repository.rb

Overview

Facility to store/cache Toggl reports data in a PostgreSQL database.

Defined Under Namespace

Modules: Data

Constant Summary collapse

HOUR =
3_600
DAY =
24 * HOUR
WEEK =
7 * DAY
DEFAULT_DATE_SINCE =
Time.now - 1 * WEEK
DEFAULT_WORKSPACE_ID =
ENV["TOGGL_WORKSPACE_ID"]
VERSION =
File.read(File.expand_path('../../../VERSION', __FILE__)).strip

Class Method Summary collapse

Class Method Details

.cache_total(year:, month: nil) ⇒ Object

Returns the total duration in TogglCache reports for the specified year or month.



141
142
143
144
145
146
147
148
149
150
151
# File 'lib/toggl_cache.rb', line 141

def self.cache_total(year:, month: nil)
  reports = TogglCache::Data::ReportRepository.new
  date_since = month ? Date.civil(year, month, 1).to_s : Date.civil(year, 1, 1)
  date_until = month ? Date.civil(year, month, -1).to_s : Date.civil(year, 12, -1)
  time_since = Time.parse("#{date_since} 00:00:00Z")
  time_until = Time.parse("#{date_until} 23:59:59Z")
  reports.starting(
    time_since: time_since,
    time_until: time_until
  ).inject(0) { |sum, r| sum + r[:duration] } / 3600
end

.clear_cache(time_since:, time_until:, logger: default_logger) ⇒ Object

Remove TogglCache’s reports between the specified dates.



105
106
107
108
109
110
111
112
# File 'lib/toggl_cache.rb', line 105

def self.clear_cache(time_since:, time_until:, logger: default_logger)
  logger.info "Clearing cache from #{time_since} to #{time_until}."
  reports = Data::ReportRepository.new
  reports.delete_starting(
    time_since: time_since,
    time_until: time_until
  )
end

.clear_cache_for_month(year:, month:, logger: default_logger) ⇒ Object

Remove TogglCache’s reports for the specified month.



115
116
117
118
119
120
121
122
123
# File 'lib/toggl_cache.rb', line 115

def self.clear_cache_for_month(year:, month:, logger: default_logger)
  date_since = Date.civil(year, month, 1)
  date_until = Date.civil(year, month, -1)
  clear_cache(
    time_since: Time.parse("#{date_since} 00:00:00Z"),
    time_until: Time.parse("#{date_until} 23:59:59Z"),
    logger: logger
  )
end

.default_client(logger: default_logger) ⇒ Object



201
202
203
204
# File 'lib/toggl_cache.rb', line 201

def self.default_client(logger: default_logger)
  return TogglAPI::ReportsClient.new(logger: logger) if logger
  TogglAPI::ReportsClient.new
end

.default_date_sinceObject



210
211
212
# File 'lib/toggl_cache.rb', line 210

def self.default_date_since
  DEFAULT_DATE_SINCE
end

.default_log_levelObject



220
221
222
# File 'lib/toggl_cache.rb', line 220

def self.default_log_level
  Logger.const_get(ENV["TOGGL_CACHE_LOG_LEVEL"]&.upcase || "ERROR")
end

.default_loggerObject



214
215
216
217
218
# File 'lib/toggl_cache.rb', line 214

def self.default_logger
  logger = ::Logger.new(STDOUT)
  logger.level = default_log_level
  logger
end

.default_workspace_idObject



206
207
208
# File 'lib/toggl_cache.rb', line 206

def self.default_workspace_id
  DEFAULT_WORKSPACE_ID
end

.fetch_reports(client: default_client, workspace_id: default_workspace_id, date_since:, date_until: Time.now, &block) ⇒ Object

Fetch from Toggl

Handles a fetch over multiple years, which requires splitting the requests over periods extending on a single year (Toggl API requirement). # @param client [TogglCache::Client] configured client

Parameters:

  • workspace_id (String) (defaults to: default_workspace_id)

    Toggl workspace ID

  • date_since (Date)

    Date since when to fetch the reports

  • date_until (Date) (defaults to: Time.now)

    Date until when to fetch the reports, defaults to Time.now



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/toggl_cache.rb', line 163

def self.fetch_reports(
  client: default_client,
  workspace_id: default_workspace_id,
  date_since:,
  date_until: Time.now,
  &block
)
  raise "You must give a block to process fetched records" unless block_given?
  if date_since && date_until.year > date_since.year
    [
      [date_since, Date.new(date_since.year, 12, 31)],
      [Date.new(date_since.year + 1, 1, 1), date_until]
    ].each do |dates|
      fetch_reports(
        client: client,
        workspace_id: workspace_id,
        date_since: dates.first,
        date_until: dates.last,
        &block
      )
    end
  else
    options = {
      workspace_id: workspace_id, until: date_until.strftime("%Y-%m-%d")
    }
    options[:since] = date_since.strftime("%Y-%m-%d") unless date_since.nil?
    client.fetch_reports(options, &block)
  end
end

.process_reports(reports, logger: default_logger) ⇒ Object



193
194
195
196
197
198
199
# File 'lib/toggl_cache.rb', line 193

def self.process_reports(reports, logger: default_logger)
  logger.debug "Processing #{reports.count} Toggl reports"
  repository = Data::ReportRepository.new
  reports.each do |report|
    repository.create_or_update(report)
  end
end

.sync_check_and_fix(logger: default_logger) ⇒ Object

Performs a full synchronization check, from the time of the first report in the cache to now. Proceeds by comparing reports total duration from Toggl (using the Reports API) and the total contained in the cache. If a difference is detected, proceeds monthly and clear and reconstructs the cache for the concerned month.

TODO: enable detecting a change in project/task level aggregates.



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/toggl_cache.rb', line 55

def self.sync_check_and_fix(logger: default_logger)
  reports = TogglCache::Data::ReportRepository.new
  first_report = reports.first

  year_start = first_report[:start].year
  year_end = Time.now.year
  month_start = first_report[:start].month
  month_end = Time.now.month

  (year_start..year_end).each do |year|
    year_toggl = TogglCache.toggl_total(year: year)
    year_cache = TogglCache.cache_total(year: year)
    if year_toggl == year_cache
      logger.info "Checked total for #{year}: ✅ (#{year_toggl})"
      next
    end
    logger.info "Checked total for #{year}: ❌ (Toggl: #{year_toggl}, cache: #{year_cache})"
    (1..12).each do |month|
      next if year == year_start && month < month_start
      next if year == year_end && month > month_end
      month_toggl = TogglCache.toggl_total(year: year, month: month)
      month_cache = TogglCache.cache_total(year: year, month: month)
      if month_toggl == month_cache
        logger.info "Checked total for #{year}/#{month}: ✅ (#{month_toggl})"
      else
        logger.info "Checked total for #{year}/#{month}: ❌ (Toggl: #{month_toggl}, cache: #{month_cache})"
        TogglCache.clear_cache_for_month(year: year, month: month, logger: logger)
        TogglCache.sync_reports_for_month(year: year, month: month, logger: logger)
      end
    end
  end
end

.sync_reports(date_since: default_date_since, date_until: Time.now, logger: default_logger, client: default_client) ⇒ Object

Fetches new and updated reports from the specified start date to now. By default, fetches all reports since 1 month ago, allowing updates on old reports to update the cached reports too.

The fetched reports either update the already existing ones, or create new ones.

Parameters:

  • date_since (Date) (defaults to: default_date_since)

    Date since when to fetch the reports.

  • date_until (Date) (defaults to: Time.now)

    Date until when to fetch. Defaults to ‘Time.now`.

  • client (TogglAPI::Client) (defaults to: default_client)

    a configured client



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/toggl_cache.rb', line 27

def self.sync_reports(
  date_since: default_date_since,
  date_until: Time.now,
  logger: default_logger,
  client: default_client
)
  logger.info "Syncing reports from #{date_since} to #{date_until}."
  clear_cache(
    time_since: Time.parse("#{date_since} 00:00:00Z"),
    time_until: Time.parse("#{date_until} 23:59:59Z"),
    logger: logger
  )
  fetch_reports(
    client: client,
    date_since: date_since,
    date_until: date_until
  ) do |reports|
    process_reports(reports)
  end
end

.sync_reports_for_month(year:, month:, logger: default_logger) ⇒ Object

An easy-to-use method to sync reports for a given month. Simply performs a call to ‘sync_reports`.

Parameters:

  • year: (Integer)
  • month: (Integer)

    onth: [Integer

  • logger: (Logger) (defaults to: default_logger)

    (optional)



94
95
96
97
98
99
100
101
102
# File 'lib/toggl_cache.rb', line 94

def self.sync_reports_for_month(year:, month:, logger: default_logger)
  date_since = Date.civil(year, month, 1)
  date_until = Date.civil(year, month, -1)
  sync_reports(
    date_since: date_since,
    date_until: date_until,
    logger: logger
  )
end

.toggl_total(year:, month: nil) ⇒ Object

Returns the total duration from Toggl (using Reports API) for the specified year or month.



127
128
129
130
131
132
133
134
135
136
137
# File 'lib/toggl_cache.rb', line 127

def self.toggl_total(year:, month: nil)
  reports_client = TogglAPI::ReportsClient.new
  date_since = month ? Date.civil(year, month, 1) : Date.civil(year, 1, 1)
  date_until = month ? Date.civil(year, month, -1) : Date.civil(year, 12, -1)
  total_grand = reports_client.fetch_reports_summary_raw(
    since: date_since.to_s,
    until: date_until.to_s,
    workspace_id: ENV["TOGGL_WORKSPACE_ID"]
  )["total_grand"]
  total_grand ? total_grand / 3600 / 1000 : 0
end