geostats 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. data/LICENSE +13 -0
  2. data/README.md +58 -0
  3. data/bin/geostats +33 -0
  4. data/lib/geostats.rb +36 -0
  5. data/lib/geostats/commands/base.rb +73 -0
  6. data/lib/geostats/commands/console.rb +17 -0
  7. data/lib/geostats/commands/generate.rb +21 -0
  8. data/lib/geostats/commands/help.rb +20 -0
  9. data/lib/geostats/commands/init.rb +31 -0
  10. data/lib/geostats/commands/migrate.rb +21 -0
  11. data/lib/geostats/commands/push.rb +41 -0
  12. data/lib/geostats/commands/set.rb +35 -0
  13. data/lib/geostats/commands/update.rb +175 -0
  14. data/lib/geostats/console.rb +2 -0
  15. data/lib/geostats/core_ext.rb +5 -0
  16. data/lib/geostats/database.rb +15 -0
  17. data/lib/geostats/generator.rb +43 -0
  18. data/lib/geostats/grabber/cache.rb +95 -0
  19. data/lib/geostats/grabber/log.rb +27 -0
  20. data/lib/geostats/grabber/logs.rb +37 -0
  21. data/lib/geostats/grabber/user.rb +15 -0
  22. data/lib/geostats/http.rb +97 -0
  23. data/lib/geostats/migrations/001_create_caches.rb +27 -0
  24. data/lib/geostats/migrations/002_create_cache_types.rb +16 -0
  25. data/lib/geostats/migrations/003_create_logs.rb +13 -0
  26. data/lib/geostats/migrations/004_create_log_types.rb +15 -0
  27. data/lib/geostats/migrations/005_create_settings.rb +9 -0
  28. data/lib/geostats/migrations/006_add_cito_cache_type.rb +5 -0
  29. data/lib/geostats/models/cache.rb +47 -0
  30. data/lib/geostats/models/cache_type.rb +9 -0
  31. data/lib/geostats/models/log.rb +33 -0
  32. data/lib/geostats/models/log_type.rb +9 -0
  33. data/lib/geostats/models/setting.rb +16 -0
  34. data/lib/geostats/stats.rb +220 -0
  35. data/lib/geostats/templates/default.mustache +10 -0
  36. data/lib/geostats/templates/difficulty_terrain_matrix.mustache +40 -0
  37. data/lib/geostats/templates/founds_by_cache_type.mustache +17 -0
  38. data/lib/geostats/templates/milestones.mustache +19 -0
  39. data/lib/geostats/templates/monthly_founds.mustache +18 -0
  40. data/lib/geostats/templates/stylesheet.css +160 -0
  41. data/lib/geostats/utils.rb +42 -0
  42. data/lib/geostats/version.rb +3 -0
  43. data/lib/geostats/views/base.rb +19 -0
  44. data/lib/geostats/views/default.rb +13 -0
  45. data/lib/geostats/views/difficulty_terrain_matrix.rb +34 -0
  46. data/lib/geostats/views/founds_by_cache_type.rb +24 -0
  47. data/lib/geostats/views/milestones.rb +23 -0
  48. data/lib/geostats/views/monthly_founds.rb +24 -0
  49. metadata +165 -0
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright (c) 2010 Thomas Cyron <thomas@thcyron.de>
2
+
3
+ Permission to use, copy, modify, and/or distribute this software for any
4
+ purpose with or without fee is hereby granted, provided that the above
5
+ copyright notice and this permission notice appear in all copies.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
@@ -0,0 +1,58 @@
1
+ Geostats — Geocaching.com Statistics Generator
2
+ ===============================================
3
+
4
+ Geostats is a tool for generating statistics about your geocaching.com
5
+ account. It generates HTML files and allows you to push them to your
6
+ public profile at geocaching.com. It doesn’t rely on Pocket Queries.
7
+
8
+
9
+ Getting Started
10
+ ---------------
11
+
12
+ bin/geostats init <username> <password>
13
+ bin/geostats update
14
+ bin/geostats generate > stats.html
15
+ open stats.html
16
+
17
+ bin/geostats console
18
+
19
+ Inside the console you have access to the following
20
+ ActiveRecord models:
21
+
22
+ * Cache
23
+ * CacheType
24
+ * Log
25
+ * LogType
26
+
27
+
28
+ Todo
29
+ ----
30
+
31
+ * Better error handling
32
+ * Logging / More output
33
+ * HTML generation
34
+ * Pushing
35
+
36
+
37
+ Usage
38
+ -----
39
+
40
+ Environment Variables:
41
+ GEOSTATS_DIR="~/geostats"
42
+
43
+ Commands:
44
+ Initialize new database: $ geostats init username password
45
+ $ geostats init username password directory
46
+
47
+ Update database: $ geostats update
48
+ $ geostats update all
49
+ $ geostats update all caches
50
+ $ geostats update all logs
51
+ $ geostats update GC12345 GC67890
52
+
53
+ -d <secs> (delay)
54
+
55
+ Generate statistics: $ geostats generate
56
+ $ geostats generate default
57
+
58
+ Push statistics to geocaching.com: $ geostats push
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin
4
+ require "geostats"
5
+ rescue LoadError
6
+ $LOAD_PATH.unshift(File.expand_path(File.join(File.dirname(__FILE__), "..", "lib")))
7
+ require "geostats"
8
+ end
9
+
10
+ command_name = ARGV.shift || "help"
11
+ command_instance = nil
12
+
13
+ possibilities = Geostats::COMMANDS.select do |cmd|
14
+ cmd =~ /^#{Regexp.escape(command_name)}/
15
+ end
16
+
17
+ if possibilities.length == 1
18
+ command_name = possibilities.first
19
+ else
20
+ command_name = "help"
21
+ end
22
+
23
+ require "geostats/commands/#{command_name}"
24
+
25
+ command_const = "Geostats::Commands::#{command_name.camelcase}".constantize
26
+ command_instance = command_const.new(ARGV)
27
+
28
+ begin
29
+ command_instance.invoke
30
+ rescue Geostats::Commands::UsageError
31
+ command_instance.usage
32
+ exit 1
33
+ end
@@ -0,0 +1,36 @@
1
+ require "cgi"
2
+ require "time"
3
+ require "active_record"
4
+
5
+ require "geostats/core_ext"
6
+ require "geostats/version"
7
+
8
+ require "geostats/models/cache"
9
+ require "geostats/models/cache_type"
10
+ require "geostats/models/log"
11
+ require "geostats/models/log_type"
12
+ require "geostats/models/setting"
13
+
14
+ module Geostats
15
+ COMMANDS = %w(console generate help init migrate push set update)
16
+ ROOT = File.join(File.expand_path(File.dirname(__FILE__)), "geostats")
17
+
18
+ module Commands
19
+ autoload :Base, "geostats/commands/base"
20
+ autoload :Help, "geostats/commands/help"
21
+ end
22
+
23
+ autoload :Database, "geostats/database"
24
+
25
+ module Grabber
26
+ autoload :Cache, "geostats/grabber/cache"
27
+ autoload :Log, "geostats/grabber/log"
28
+ autoload :Logs, "geostats/grabber/logs"
29
+ autoload :User, "geostats/grabber/user"
30
+ end
31
+
32
+ autoload :Generator, "geostats/generator"
33
+ autoload :HTTP, "geostats/http"
34
+ autoload :Stats, "geostats/stats"
35
+ autoload :Utils, "geostats/utils"
36
+ end
@@ -0,0 +1,73 @@
1
+ require "term/ansicolor"
2
+
3
+ module Term::ANSIColor
4
+ def erase(n=nil)
5
+ "\e[#{n}K"
6
+ end
7
+ end
8
+
9
+ module Geostats
10
+ module Commands
11
+ class UsageError < Exception
12
+ end
13
+
14
+ class Base
15
+ include Term::ANSIColor
16
+
17
+ def initialize(args=[])
18
+ @args = args
19
+ end
20
+
21
+ def invoke
22
+ parse_args
23
+ before; run; after
24
+ end
25
+
26
+ protected
27
+
28
+ def parse_args; end
29
+ def before; end
30
+ def after; end
31
+
32
+ def log(*args)
33
+ print *args.join
34
+ end
35
+
36
+ def warn(message)
37
+ log "Warning: #{message}"
38
+ end
39
+
40
+ def error(message)
41
+ STDERR.puts "Error: #{message}"
42
+ exit 1
43
+ end
44
+
45
+ def verbose(message)
46
+ log message
47
+ end
48
+
49
+ def connect_database(directory = ENV["GEOSTATS_DIR"])
50
+ Database.connect!(directory)
51
+ rescue => e
52
+ error "could not connect to database: #{e.message}"
53
+ end
54
+
55
+ def login
56
+ HTTP.login(Setting.get(:username), Setting.get(:password))
57
+ rescue ArgumentError, Net::HTTPExceptions => e
58
+ error "Failed to log in: #{e.message}"
59
+ end
60
+
61
+ def logout
62
+ HTTP.logout
63
+ rescue Net::HTTPExceptions => e
64
+ error "Failed to log out: #{e.message}"
65
+ end
66
+
67
+ def usage
68
+ STDERR.puts "Usage: geostats command [arguments ...]"
69
+ exit 1
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,17 @@
1
+ module Geostats
2
+ module Commands
3
+ class Console < Base
4
+ def usage
5
+ STDERR.puts "Usage: geostats console"
6
+ end
7
+
8
+ def parse_args
9
+ raise UsageError unless @args.length.zero?
10
+ end
11
+
12
+ def run
13
+ exec "irb -I lib -r geostats -r geostats/console"
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,21 @@
1
+ module Geostats
2
+ module Commands
3
+ class Generate < Base
4
+ def usage
5
+ STDERR.puts "Usage: geostats generate"
6
+ end
7
+
8
+ def parse_args
9
+ raise UsageError unless @args.length.zero?
10
+ end
11
+
12
+ def before
13
+ connect_database
14
+ end
15
+
16
+ def run
17
+ puts Generator.generate
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,20 @@
1
+ module Geostats
2
+ module Commands
3
+ class Help < Base
4
+ def usage
5
+ run
6
+ end
7
+
8
+ def run
9
+ STDERR.puts "Usage: geostats <command> [...]"
10
+ STDERR.puts
11
+ STDERR.puts "Commands: init <username> <password> [<directory>]"
12
+ STDERR.puts " update [all | <code>]"
13
+ STDERR.puts " generate [<template>]"
14
+ STDERR.puts " push"
15
+ STDERR.puts " console"
16
+ STDERR.puts " set [<key>] [<value>]"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,31 @@
1
+ require "fileutils"
2
+ require "geostats/commands/migrate"
3
+
4
+ module Geostats
5
+ module Commands
6
+ class Init < Base
7
+ def usage
8
+ STDERR.puts "Usage: geostats init <username> <password> [<directory>]"
9
+ end
10
+
11
+ def parse_args
12
+ raise UsageError unless @args.length == 2 or @args.length == 3
13
+
14
+ @username, @password, @directory = @args
15
+ @directory ||= File.expand_path("~/.geostats")
16
+
17
+ FileUtils.mkdir_p(@directory) unless File.exists?(@directory)
18
+ end
19
+
20
+ def before
21
+ connect_database(@directory)
22
+ end
23
+
24
+ def run
25
+ Migrate.new.invoke
26
+ Setting.set(:username, @username)
27
+ Setting.set(:password, @password)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,21 @@
1
+ module Geostats
2
+ module Commands
3
+ class Migrate < Base
4
+ def usage
5
+ STDERR.puts "Usage: geostats migrate"
6
+ end
7
+
8
+ def parse_args
9
+ raise UsageError unless @args.length.zero?
10
+ end
11
+
12
+ def before
13
+ connect_database
14
+ end
15
+
16
+ def run
17
+ ActiveRecord::Migrator.migrate(File.join(Geostats::ROOT, "migrations"))
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,41 @@
1
+ module Geostats
2
+ module Commands
3
+ class Push < Base
4
+ def usage
5
+ STDERR.puts "Usage: geostats push"
6
+ end
7
+
8
+ def parse_args
9
+ raise UsageError unless @args.length.zero?
10
+ end
11
+
12
+ def before
13
+ connect_database
14
+ login
15
+ end
16
+
17
+ def after
18
+ logout
19
+ end
20
+
21
+ def run
22
+ log yellow("Generating statistics")
23
+ @html = Generator.generate
24
+ log "\r", green("Successfully generated statistics"), erase, "\n"
25
+
26
+ log yellow("Pushing statistics to geocaching.com")
27
+
28
+ resp, data = HTTP.post("/account/editprofiledetails.aspx", {
29
+ "ctl00$ContentBody$uxProfileDetails" => @html,
30
+ "ctl00$ContentBody$uxSave" => "Save Changes"
31
+ })
32
+
33
+ log "\r",
34
+ green("Successfully pushed statistics to geocaching.com"),
35
+ erase, "\n"
36
+ rescue Net::HTTPExceptions => e
37
+ error e.message
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,35 @@
1
+ module Geostats
2
+ module Commands
3
+ class Set < Base
4
+ def usage
5
+ STDERR.puts "Usage: geostats set"
6
+ STDERR.puts " set <key>"
7
+ STDERR.puts " set <key> <value>"
8
+ end
9
+
10
+ def parse_args
11
+ raise UsageError if @args.length > 2
12
+ @key, @value = @args
13
+ end
14
+
15
+ def before
16
+ connect_database
17
+ end
18
+
19
+ def run
20
+ if @key
21
+ if @value
22
+ Setting.set(@key, @value)
23
+ puts "#{@key} = #{@value}"
24
+ else
25
+ puts "#{@key} = #{Setting.get(@key) || "<null>"}"
26
+ end
27
+ else
28
+ Setting.all.each do |setting|
29
+ puts "#{setting.key} = #{setting.value || "<null>"}"
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,175 @@
1
+ module Geostats
2
+ module Commands
3
+ class Update < Base
4
+ def usage
5
+ STDERR.puts "Usage: geostats update"
6
+ STDERR.puts " update all"
7
+ end
8
+
9
+ def parse_args
10
+ @update_all = (@args.first == "all")
11
+ end
12
+
13
+ def before
14
+ connect_database
15
+ login
16
+
17
+ @delay = Setting.get(:delay) || 5
18
+ @sleep = false
19
+ end
20
+
21
+ def after
22
+ logout
23
+ end
24
+
25
+ def run
26
+ begin
27
+ @logs = Grabber::Logs.new.logs
28
+ rescue Net::HTTPExceptions => e
29
+ error "Failed to fetch your logs: #{e.message}"
30
+ end
31
+
32
+ @logs.reverse.each do |loginfo|
33
+ handle_log(loginfo)
34
+
35
+ if @sleep
36
+ sleep(@delay + rand(@delay))
37
+ @sleep = false
38
+ end
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def handle_log(loginfo)
45
+ missing = []
46
+
47
+ [:log_uuid, :logged_at, :icon, :cache_uuid, :cache_name].each do |attribute|
48
+ missing << attribute unless loginfo.send(attribute)
49
+ end
50
+
51
+ if missing.any?
52
+ log red("Skipping log because there are missing attributes: #{attributes.join(", ")}")
53
+ return
54
+ end
55
+
56
+ cache = create_or_update_cache(loginfo)
57
+ return unless cache
58
+
59
+ log = create_or_update_log(loginfo)
60
+ return unless log
61
+
62
+ cache.logs << log
63
+ end
64
+
65
+ def create_or_update_cache(loginfo)
66
+ cache = Cache.where(:uuid => loginfo.cache_uuid).first
67
+
68
+ if cache.nil?
69
+ log \
70
+ yellow("Adding new cache "),
71
+ yellow(bold(loginfo.cache_name))
72
+
73
+ @sleep = true
74
+
75
+ begin
76
+ cache = Cache.create_from_website(loginfo.cache_uuid)
77
+ rescue Net::HTTPError => e
78
+ log "\r",
79
+ red("Failed to add new cache "),
80
+ red(bold(loginfo.cache_name)),
81
+ red(": "),
82
+ red("Could not fetch cache information from website: #{e.message}"),
83
+ erase, "\n"
84
+ return
85
+ end
86
+
87
+ if cache.save
88
+ log "\r",
89
+ green("Successfully added new cache "),
90
+ green(bold(cache.name)),
91
+ erase, "\n"
92
+ else
93
+ log "\r",
94
+ red("Failed to add new cache "),
95
+ red(bold(loginfo.cache_name)),
96
+ red(": "),
97
+ red("Could not fetch all needed information from website"),
98
+ erase, "\n"
99
+
100
+ cache.errors.full_messages.each do |msg|
101
+ verbose " o #{msg}"
102
+ end
103
+
104
+ cache = nil
105
+ end
106
+ else
107
+ cache.update_attribute(:name, loginfo.cache_name)
108
+ end
109
+
110
+ cache
111
+ end
112
+
113
+ def create_or_update_log(loginfo)
114
+ # Don't handle reviewer notes
115
+ return nil if loginfo.icon == "big_smile"
116
+
117
+ log = Log.where(:uuid => loginfo.log_uuid).first
118
+
119
+ if log.nil?
120
+ log \
121
+ yellow("Adding new log for "),
122
+ yellow(bold(loginfo.cache_name)),
123
+ yellow(" from "),
124
+ yellow(bold(loginfo.logged_at.strftime("%F")))
125
+
126
+ @sleep = true
127
+
128
+ begin
129
+ log = Log.create_from_website(loginfo.log_uuid)
130
+ rescue Net::HTTPEeror => e
131
+ log "\r",
132
+ red("Failed to add new log for "),
133
+ red(bold(loginfo.cache_name)),
134
+ red(" from "),
135
+ red(bold(loginfo.logged_at.strftime("%F"))),
136
+ red(": "),
137
+ red("Could not fetch log information from website: #{e.message}"),
138
+ erase, "\n"
139
+ return
140
+ end
141
+
142
+ if log.save
143
+ log "\r",
144
+ green("Successfully added log for "),
145
+ green(bold(loginfo.cache_name)),
146
+ green(" from "),
147
+ green(bold(log.logged_at.strftime("%F"))),
148
+ erase, "\n"
149
+ else
150
+ log "\r",
151
+ red("Failed to add new log for "),
152
+ red(bold(loginfo.cache_name)),
153
+ red(" from "),
154
+ red(bold(loginfo.logged_at.strftime("%F"))),
155
+ red(": "),
156
+ red("Could not fetch all needed information from website"),
157
+ erase, "\n"
158
+
159
+ log.errors.full_messages.each do |msg|
160
+ verbose " o #{msg}"
161
+ end
162
+
163
+ log = nil
164
+ end
165
+ else
166
+ log.logged_at = loginfo.logged_at
167
+ log.set_type_from_icon(loginfo.icon)
168
+ log.save
169
+ end
170
+
171
+ log
172
+ end
173
+ end
174
+ end
175
+ end