public
Description: Generates summary reports from Basecamp data
Homepage:
Clone URL: git://github.com/wuputah/basecamp-timereport.git
basecamp-timereport / fetch.rb
100644 113 lines (100 sloc) 4.073 kb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#!/usr/bin/env ruby
 
# fetch.rb - Fetches all time data from Basecamp (that you have access to) for a
# given time period and gives you a summarized report in a number of categories.
#
# In order to use this, you must set the following ENV variables:
# BASECAMP_URL BASECAMP_USERNAME BASECAMP_PASSWORD
#
# Instead of launching this directly, put your setting in the 'fetch' shell script
# which is a wrapper to this program. This way, your info is not permanently in
# your shell environment.
#
# Example usage:
# ./fetch.rb 2008-11-01 2008-11-15
#
# You must provide the start date. The end date defaults to today if not provided.
 
# Yes, this code is not all that pretty - this was primarily a mental exercise.
# Primarily, this refers to outputting all the output in a single expression at the end.
 
require 'basecamp'
require 'date'
 
class Object; def with(obj=self) yield(obj); obj; end; end
class Object; def chain(obj=self) yield(obj); end; end
 
def header(txt = nil, chr = '=')
  return chr * 60 if txt.nil?
  extra = 60 - (txt.length + 2)
  chr * (extra / 2 + (extra.odd? ? 1 : 0)) << " " << txt << " " << chr * (extra / 2)
end
 
%w[BASECAMP_URL BASECAMP_USERNAME BASECAMP_PASSWORD].each do |setting|
  unless ENV.has_key?(setting)
    puts "Environment variable #{setting} not set! Aborting..."
    exit
  end
end
 
unless ARGV.size >= 1
  puts "You must specify the start date for the report as an argument. (End date is optional, defaults to today)"
  exit
end
 
Basecamp.establish_connection!(ENV['BASECAMP_URL'],
                               ENV['BASECAMP_USERNAME'],
                               ENV['BASECAMP_PASSWORD'],
                               true)
start_date, end_date = ARGV.collect { |arg| Date.parse(arg) } << Date.today
basecamp = Basecamp.new
 
if end_date < start_date
  start_date, end_date = [end_date, start_date]
end
 
totals = { :people => Hash.new(0.0),
           :projects => Hash.new(0.0),
           :pp => Hash.new { |h, k| h[k] = Hash.new(0.0) } }
cache = { :people => { }, :projects => { } }
 
cur_start_date = start_date
until cur_start_date > end_date
  cur_end_date = [cur_start_date + 180, end_date].min
  Basecamp::TimeEntry.report(:from => cur_start_date.strftime('%Y%m%d'), :to => cur_end_date.strftime('%Y%m%d')).each do |entry|
    person = cache[:people][entry.person_id] ||= basecamp.person(entry.person_id)
    project = cache[:projects][entry.project_id] ||= Basecamp::Project.find(entry.project_id)
    totals[:people][person] += entry.hours
    totals[:projects][project] += entry.hours
    totals[:pp][project][person] += entry.hours
  end
  # basecamp is inclusive on both ends, so you have to add an additional day here
  cur_start_date += 181
end
 
# Yes, this is one expression - its ugly, but its pretty cool, too.
# No, I don't normally do things like this. :)
puts header("TIME REPORT FOR #{start_date.strftime('%Y-%m-%d')} TO #{end_date.strftime('%Y-%m-%d')}", ' '),
     header('By Person'),
     totals[:people].
       collect { |(p, h)| [ [p['last-name'], p['first-name']], h ] }.
       sort.
       collect { |(p, h)| sprintf "%-53s%7.02f" % [p.reverse.join(' '), h] }.
       join("\n"),
     header('By Project'),
     totals[:projects].
       collect { |(p, h)| [ [p.company.name, p.name].join(' - '), h ] }.
       sort.
       collect { |(p, h)| sprintf "%-53s%7.02f" % [p, h] }.
       join("\n"),
     header('By Company'),
     totals[:projects].
       inject(Hash.new(0.0)) { |hsh, (p, h)|
         hsh.with { |hh| hh[p.company.name] += h }
       }.
       sort.
       collect { |(p, h)| sprintf "%-53s%7.02f" % [p, h] }.
       join("\n"),
     header('By Person per Project'),
     totals[:pp].
       collect { |(proj, people)|
         [
           [proj.company.name, proj.name].join(' - '),
           people.collect { |(p, h)| [ [p['last-name'], p['first-name'] ], h ] }.
           sort.
           collect { |(p, h)| sprintf " %-51s%7.02f" % [p.reverse.join(' '), h] }.
           join("\n")
         ]
       }.
       sort.
       flatten.
       join("\n"),
     header