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.
- data/LICENSE +13 -0
- data/README.md +58 -0
- data/bin/geostats +33 -0
- data/lib/geostats.rb +36 -0
- data/lib/geostats/commands/base.rb +73 -0
- data/lib/geostats/commands/console.rb +17 -0
- data/lib/geostats/commands/generate.rb +21 -0
- data/lib/geostats/commands/help.rb +20 -0
- data/lib/geostats/commands/init.rb +31 -0
- data/lib/geostats/commands/migrate.rb +21 -0
- data/lib/geostats/commands/push.rb +41 -0
- data/lib/geostats/commands/set.rb +35 -0
- data/lib/geostats/commands/update.rb +175 -0
- data/lib/geostats/console.rb +2 -0
- data/lib/geostats/core_ext.rb +5 -0
- data/lib/geostats/database.rb +15 -0
- data/lib/geostats/generator.rb +43 -0
- data/lib/geostats/grabber/cache.rb +95 -0
- data/lib/geostats/grabber/log.rb +27 -0
- data/lib/geostats/grabber/logs.rb +37 -0
- data/lib/geostats/grabber/user.rb +15 -0
- data/lib/geostats/http.rb +97 -0
- data/lib/geostats/migrations/001_create_caches.rb +27 -0
- data/lib/geostats/migrations/002_create_cache_types.rb +16 -0
- data/lib/geostats/migrations/003_create_logs.rb +13 -0
- data/lib/geostats/migrations/004_create_log_types.rb +15 -0
- data/lib/geostats/migrations/005_create_settings.rb +9 -0
- data/lib/geostats/migrations/006_add_cito_cache_type.rb +5 -0
- data/lib/geostats/models/cache.rb +47 -0
- data/lib/geostats/models/cache_type.rb +9 -0
- data/lib/geostats/models/log.rb +33 -0
- data/lib/geostats/models/log_type.rb +9 -0
- data/lib/geostats/models/setting.rb +16 -0
- data/lib/geostats/stats.rb +220 -0
- data/lib/geostats/templates/default.mustache +10 -0
- data/lib/geostats/templates/difficulty_terrain_matrix.mustache +40 -0
- data/lib/geostats/templates/founds_by_cache_type.mustache +17 -0
- data/lib/geostats/templates/milestones.mustache +19 -0
- data/lib/geostats/templates/monthly_founds.mustache +18 -0
- data/lib/geostats/templates/stylesheet.css +160 -0
- data/lib/geostats/utils.rb +42 -0
- data/lib/geostats/version.rb +3 -0
- data/lib/geostats/views/base.rb +19 -0
- data/lib/geostats/views/default.rb +13 -0
- data/lib/geostats/views/difficulty_terrain_matrix.rb +34 -0
- data/lib/geostats/views/founds_by_cache_type.rb +24 -0
- data/lib/geostats/views/milestones.rb +23 -0
- data/lib/geostats/views/monthly_founds.rb +24 -0
- metadata +165 -0
@@ -0,0 +1,15 @@
|
|
1
|
+
module Geostats
|
2
|
+
class Database
|
3
|
+
def self.connect!(directory=nil)
|
4
|
+
directory ||= File.expand_path("~/.geostats")
|
5
|
+
|
6
|
+
file = File.join(directory, "database.sqlite3")
|
7
|
+
raise "file does not exist: #{file}" unless File.exists?(file)
|
8
|
+
|
9
|
+
ActiveRecord::Base.establish_connection(
|
10
|
+
:adapter => "sqlite3",
|
11
|
+
:database => file
|
12
|
+
)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require "hpricot"
|
2
|
+
require "css_parser"
|
3
|
+
|
4
|
+
require "geostats/views/base"
|
5
|
+
require "geostats/views/default"
|
6
|
+
|
7
|
+
module Geostats
|
8
|
+
class Generator
|
9
|
+
PARTS = %w(difficulty_terrain_matrix milestones monthly_founds
|
10
|
+
founds_by_cache_type)
|
11
|
+
|
12
|
+
def self.generate
|
13
|
+
new.generate
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
@view = Views::Default.new
|
18
|
+
@css = CssParser::Parser.new
|
19
|
+
|
20
|
+
file = File.join(File.dirname(__FILE__), "templates", "stylesheet.css")
|
21
|
+
@css.add_block!(File.read(file))
|
22
|
+
end
|
23
|
+
|
24
|
+
def generate
|
25
|
+
PARTS.each do |part|
|
26
|
+
require "geostats/views/#{part}"
|
27
|
+
@view[part.to_sym] = "Geostats::Views::#{part.camelcase}".constantize.render
|
28
|
+
end
|
29
|
+
|
30
|
+
@doc = Hpricot(@view.render)
|
31
|
+
|
32
|
+
@css.each_selector do |selector, style|
|
33
|
+
@doc.search(selector) do |el|
|
34
|
+
if el.elem?
|
35
|
+
el["style"] = "#{el.attributes['style']} #{style}".strip
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
@doc.to_html
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module Geostats
|
2
|
+
module Grabber
|
3
|
+
class Cache
|
4
|
+
def initialize(uuid)
|
5
|
+
@resp, @data = HTTP.get("/seek/cache_details.aspx?guid=#{uuid}&log=y")
|
6
|
+
end
|
7
|
+
|
8
|
+
def code
|
9
|
+
if @data =~ /<div id="ctl00_cacheCodeWidget".*?>\s*<p>\s*([A-Z0-9]+)<\/p>\s*<\/div>/
|
10
|
+
$1
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def name
|
15
|
+
if @data =~ /<span id="ctl00_ContentBody_CacheName">(.*?)<\/span>/
|
16
|
+
name = Utils.unescape($1)
|
17
|
+
name =~ /^<strike>(.*)<\/strike>$/ ? $1 : name
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def type
|
22
|
+
if @data =~ /<h2.*?WptTypes\/(\d+)\.gif".*?h2>/
|
23
|
+
$1.to_i
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def hidden_at
|
28
|
+
if @data =~ /<span id="ctl00_ContentBody_DateHidden">(\d{1,2})\/(\d{1,2})\/(\d{4})<\/span>/
|
29
|
+
Time.parse([$2, $1, $3].join("."))
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def size
|
34
|
+
if @data =~ /<img src="\/images\/icons\/container\/(.*?)\.gif" alt="Size: .*?" \/>/
|
35
|
+
$1 if %w(micro small regular large other not_chosen).include?($1)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def owner
|
40
|
+
if @data =~ /<span id="ctl00_ContentBody_CacheOwner">by.*?\/profile\/\?guid=(.*?)&wid=.*<\/a><\/span>/
|
41
|
+
user = Grabber::User.new($1)
|
42
|
+
if name = user.username
|
43
|
+
name
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def difficulty
|
49
|
+
if @data =~ /<span id="ctl00_ContentBody_Difficulty"><img src=".*?" alt="([0-9\.]+) out of 5" \/><\/span>/
|
50
|
+
$1.to_f
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def terrain
|
55
|
+
if @data =~ /<span id="ctl00_ContentBody_Terrain"><img src=".*?" alt="([0-9\.]+) out of 5" \/><\/span>/
|
56
|
+
$1.to_f
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def latitude
|
61
|
+
if @data =~ /\/wpt\/\?lat=(.*)&lon=(.*)&detail=1/
|
62
|
+
$1.to_f
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def longitude
|
67
|
+
if @data =~ /\/wpt\/\?lat=(.*)&lon=(.*)&detail=1/
|
68
|
+
$2.to_f
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def location
|
73
|
+
if @data =~ /<span id="ctl00_ContentBody_Location">In (.*?)<\/span>/
|
74
|
+
$1
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def founds_count
|
79
|
+
@data.scan(/<strong><img src=".*?icon_smile\.gif" alt="" \/> /).length
|
80
|
+
end
|
81
|
+
|
82
|
+
def dnf_count
|
83
|
+
@data.scan(/<strong><img src=".*?icon_sad\.gif" alt="" \/> /).length
|
84
|
+
end
|
85
|
+
|
86
|
+
def archived?
|
87
|
+
!!(@data =~ /<li>This cache has been archived/)
|
88
|
+
end
|
89
|
+
|
90
|
+
def pmonly?
|
91
|
+
!!(@data =~ /<p class="Warning">Sorry, the owner of this listing has made it viewable to Premium Members only./)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Geostats
|
2
|
+
module Grabber
|
3
|
+
class Log
|
4
|
+
def initialize(uuid)
|
5
|
+
@resp, @data = HTTP.get("/seek/log.aspx?LUID=#{uuid}")
|
6
|
+
end
|
7
|
+
|
8
|
+
def icon
|
9
|
+
if @data =~ /<img id="ctl00_ContentBody_LogBookPanel1_LogImage".*?src="\/images\/icons\/(.*?).gif" .*? \/>/
|
10
|
+
$1
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def message
|
15
|
+
if @data =~ /<p><span id="ctl00_ContentBody_LogBookPanel1_LogText">(.*)<\/span><\/p>/
|
16
|
+
Utils.replace_smilie_img_tags(Utils.unescape($1))
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def logged_at
|
21
|
+
if @data =~ /<span id="ctl00_ContentBody_LogBookPanel1_LogDate">(.*?)<\/span>/
|
22
|
+
Time.parse($1) rescue nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Geostats
|
2
|
+
module Grabber
|
3
|
+
class Logs
|
4
|
+
class Log < Struct.new(:icon, :logged_at, :cache_uuid,
|
5
|
+
:cache_name, :location, :log_uuid)
|
6
|
+
end
|
7
|
+
|
8
|
+
REGEX = %r{<tr.*?>\s*<td><img src="/images/icons/([a-z_]+)\.gif" width="16" height="16" alt=".*?" /></td>\s*<td>([0-9\/]+)</td>\s*<td><a href="http://www\.geocaching\.com/seek/cache_details\.aspx\?guid=([a-f0-9\-]+)">(.*?)</a> </td>\s*<td>(.*?) </td>\s*<td><a href="http://www\.geocaching\.com/seek/log\.aspx\?LUID=([a-f0-9\-]+)" target="_blank" title="Visit Log">Visit Log</a></td>\s*</tr>}
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@resp, @data = HTTP.get("/my/logs.aspx?s=1")
|
12
|
+
end
|
13
|
+
|
14
|
+
def logs
|
15
|
+
@data.scan(REGEX).map do |match|
|
16
|
+
log = Log.new
|
17
|
+
|
18
|
+
log.icon = match[0]
|
19
|
+
log.cache_uuid = match[2]
|
20
|
+
log.cache_name = Utils.unescape(match[3])
|
21
|
+
log.location = Utils.unescape(match[4])
|
22
|
+
log.log_uuid = match[5]
|
23
|
+
log.logged_at = match[1]
|
24
|
+
|
25
|
+
log.cache_name = Utils.unescape($1) if log.cache_name =~ /^<span class="Strike Warning">(.*)<\/span>$/
|
26
|
+
log.cache_name = Utils.unescape($1) if log.cache_name =~ /^<strike>(.*)<\/strike>$/
|
27
|
+
|
28
|
+
if log.logged_at =~ /^(\d+)\/(\d+)\/(\d+)$/
|
29
|
+
log.logged_at = Time.parse([$2, $1, $3].join("."))
|
30
|
+
end
|
31
|
+
|
32
|
+
log
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Geostats
|
2
|
+
module Grabber
|
3
|
+
class User
|
4
|
+
def initialize(uuid)
|
5
|
+
@resp, @data = HTTP.get("/profile/?guid=#{uuid}")
|
6
|
+
end
|
7
|
+
|
8
|
+
def username
|
9
|
+
if @data =~ /<h2><span id="ctl00_ContentBody_lblUserProfile".*?>Profile for User: (.*?)<\/span><\/h2>/
|
10
|
+
Utils.unescape($1)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require "net/http"
|
2
|
+
|
3
|
+
module Geostats
|
4
|
+
class HTTP
|
5
|
+
attr_writer :username, :password
|
6
|
+
|
7
|
+
def self.instance
|
8
|
+
@instance ||= new
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.login(username, password)
|
12
|
+
self.instance.username = username
|
13
|
+
self.instance.password = password
|
14
|
+
self.instance.login
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.logout
|
18
|
+
self.instance.logout
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.get(path)
|
22
|
+
self.instance.get(path)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.post(path, data={})
|
26
|
+
self.instance.post(path, data)
|
27
|
+
end
|
28
|
+
|
29
|
+
def login
|
30
|
+
raise ArgumentError, "Missing username" unless @username
|
31
|
+
raise ArgumentError, "Missing password" unless @password
|
32
|
+
|
33
|
+
resp, data = post("/login/default.aspx", {
|
34
|
+
"ctl00$ContentBody$myUsername" => @username,
|
35
|
+
"ctl00$ContentBody$myPassword" => @password,
|
36
|
+
"ctl00$ContentBody$Button1" => "Login",
|
37
|
+
"ctl00$ContentBody$cookie" => "on"
|
38
|
+
})
|
39
|
+
|
40
|
+
@cookie = resp.response["set-cookie"]
|
41
|
+
end
|
42
|
+
|
43
|
+
def logout
|
44
|
+
get("/login/default.aspx?RESET=Y")
|
45
|
+
@cookie = nil
|
46
|
+
end
|
47
|
+
|
48
|
+
def get(path)
|
49
|
+
header = default_header
|
50
|
+
header["Cookie"] = @cookie if @cookie
|
51
|
+
|
52
|
+
http.get(path, header)
|
53
|
+
end
|
54
|
+
|
55
|
+
def post(path, data={})
|
56
|
+
metadata(path).each do |key, value|
|
57
|
+
data[key] = value
|
58
|
+
end
|
59
|
+
|
60
|
+
header = default_header
|
61
|
+
header["Cookie"] = @cookie if @cookie
|
62
|
+
|
63
|
+
http.post(path, data.map { |k,v| "#{k}=#{v}" }.join("&"), header)
|
64
|
+
end
|
65
|
+
|
66
|
+
protected
|
67
|
+
|
68
|
+
def metadata(path)
|
69
|
+
resp, data = get(path)
|
70
|
+
meta = {}
|
71
|
+
|
72
|
+
data.scan(/<input type="hidden" name="__([A-Z]+)" id="__[A-Z]+" value="(.*?)" \/>/).each do |match|
|
73
|
+
meta["__#{match[0]}"] = CGI.escape(match[1])
|
74
|
+
end
|
75
|
+
|
76
|
+
meta
|
77
|
+
end
|
78
|
+
|
79
|
+
def user_agent
|
80
|
+
"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.9.0.13) Gecko/2009073022 Firefox/3.0.13 (.NET CLR 3.5.30729)"
|
81
|
+
end
|
82
|
+
|
83
|
+
def default_header
|
84
|
+
{
|
85
|
+
"User-Agent" => user_agent
|
86
|
+
}
|
87
|
+
end
|
88
|
+
|
89
|
+
def http
|
90
|
+
@http ||= begin
|
91
|
+
@http = Net::HTTP.new("www.geocaching.com")
|
92
|
+
@http.open_timeout = 5
|
93
|
+
@http
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class CreateCaches < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :caches do |t|
|
4
|
+
t.integer :cache_type_id
|
5
|
+
t.string :uuid
|
6
|
+
t.string :code
|
7
|
+
t.string :name
|
8
|
+
t.float :latitude
|
9
|
+
t.float :longitude
|
10
|
+
t.float :difficulty
|
11
|
+
t.float :terrain
|
12
|
+
t.string :owner
|
13
|
+
t.string :size
|
14
|
+
t.string :location
|
15
|
+
t.datetime :hidden_at
|
16
|
+
t.boolean :is_pmonly
|
17
|
+
t.boolean :is_archived
|
18
|
+
t.integer :gcvote_votes
|
19
|
+
t.float :gcvote_average
|
20
|
+
t.float :gcvote_median
|
21
|
+
t.integer :founds_count
|
22
|
+
t.integer :dnf_count
|
23
|
+
t.datetime :synced_at
|
24
|
+
t.timestamps
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class CreateCacheTypes < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :cache_types do |t|
|
4
|
+
t.string :name
|
5
|
+
end
|
6
|
+
|
7
|
+
cache_types = ["Traditional Cache", "Multi-Cache", "Mystery Cache",
|
8
|
+
"Letterbox Hybrid", "Wherigo Cache", "EarthCache", "Virtual Cache",
|
9
|
+
"Webcam Cache", "Reverse Cache", "Event Cache", "Mega-Event Cache",
|
10
|
+
"10 Years! Event Cache"]
|
11
|
+
|
12
|
+
cache_types.each do |cache_type|
|
13
|
+
Geostats::CacheType.create! :name => cache_type
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class CreateLogTypes < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :log_types do |t|
|
4
|
+
t.string :name
|
5
|
+
end
|
6
|
+
|
7
|
+
log_types = ["Found it", "Didn't find it", "Note", "Needs archived",
|
8
|
+
"Needs maintenance", "Archive", "Enable Listing","Temporarily Disable Listing",
|
9
|
+
"Update Coordinates", "Owner Maintenance", "Will attend", "Attended"]
|
10
|
+
|
11
|
+
log_types.each do |log_type|
|
12
|
+
Geostats::LogType.create! :name => log_type
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Geostats
|
2
|
+
class Cache < ActiveRecord::Base
|
3
|
+
belongs_to :cache_type, :class_name => "Geostats::CacheType"
|
4
|
+
has_many :logs, :class_name => "Geostats::Log"
|
5
|
+
|
6
|
+
scope :found_or_attended, lambda { where("logs.log_type_id IN (?)", [1, 12]) }
|
7
|
+
|
8
|
+
validates :uuid, :presence => true, :uniqueness => true
|
9
|
+
validates :code, :presence => true, :uniqueness => true
|
10
|
+
validates :name, :presence => true
|
11
|
+
validates :longitude, :presence => true, :numericality => true
|
12
|
+
validates :latitude, :presence => true, :numericality => true
|
13
|
+
validates :owner, :presence => true
|
14
|
+
validates :hidden_at, :presence => true
|
15
|
+
validates :size, :presence => true
|
16
|
+
validates :location, :presence => true
|
17
|
+
validates :founds_count, :presence => true, :numericality => true
|
18
|
+
validates :dnf_count, :presence => true, :numericality => true
|
19
|
+
|
20
|
+
def self.create_from_website(uuid)
|
21
|
+
cache = Cache.new(:uuid => uuid)
|
22
|
+
cache.update_from_website
|
23
|
+
cache
|
24
|
+
end
|
25
|
+
|
26
|
+
def update_from_website
|
27
|
+
info = Grabber::Cache.new(self.uuid)
|
28
|
+
|
29
|
+
[:name, :code, :size, :latitude, :longitude, :difficulty, :terrain,
|
30
|
+
:location, :owner, :hidden_at, :founds_count, :dnf_count
|
31
|
+
].each do |attribute|
|
32
|
+
if value = info.send(attribute)
|
33
|
+
self.attributes = { attribute => value }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
self.is_pmonly = info.pmonly?
|
38
|
+
self.is_archived = info.archived?
|
39
|
+
|
40
|
+
if type = info.type and index = CacheType::MAPPING.index(type)
|
41
|
+
self.cache_type = CacheType.where(:id => index + 1).first
|
42
|
+
end
|
43
|
+
|
44
|
+
self.synced_at = Time.now
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|