geostats 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|