aerial 0.0.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/.gitignore +1 -0
- data/MIT-LICENSE +22 -0
- data/README.md +74 -0
- data/Rakefile +96 -0
- data/VERSION +1 -0
- data/config/config.yml +19 -0
- data/config/deploy.rb +50 -0
- data/lib/aerial.rb +100 -0
- data/lib/aerial/article.rb +241 -0
- data/lib/aerial/base.rb +172 -0
- data/lib/aerial/comment.rb +160 -0
- data/lib/aerial/config.rb +41 -0
- data/lib/aerial/content.rb +74 -0
- data/lib/aerial/vendor/akismetor.rb +52 -0
- data/lib/aerial/vendor/cache.rb +139 -0
- data/lib/features/article.feature +10 -0
- data/lib/features/home.feature +16 -0
- data/lib/features/step_definitions/article_steps.rb +4 -0
- data/lib/features/step_definitions/home_steps.rb +8 -0
- data/lib/features/support/env.rb +38 -0
- data/lib/features/support/pages/article.rb +9 -0
- data/lib/features/support/pages/homepage.rb +9 -0
- data/lib/spec/aerial_spec.rb +203 -0
- data/lib/spec/article_spec.rb +338 -0
- data/lib/spec/base_spec.rb +65 -0
- data/lib/spec/comment_spec.rb +216 -0
- data/lib/spec/config_spec.rb +25 -0
- data/lib/spec/fixtures/articles/sample-article/sample-article.article +6 -0
- data/lib/spec/fixtures/articles/test-article-one/test-article.article +7 -0
- data/lib/spec/fixtures/articles/test-article-three/test-article.article +7 -0
- data/lib/spec/fixtures/articles/test-article-two/comment-missing-fields.comment +8 -0
- data/lib/spec/fixtures/articles/test-article-two/test-article.article +7 -0
- data/lib/spec/fixtures/articles/test-article-two/test-comment.comment +10 -0
- data/lib/spec/fixtures/config.yml +35 -0
- data/lib/spec/fixtures/public/javascripts/application.js +109 -0
- data/lib/spec/fixtures/public/javascripts/jquery-1.3.1.min.js +19 -0
- data/lib/spec/fixtures/public/javascripts/jquery.template.js +255 -0
- data/lib/spec/fixtures/views/article.haml +19 -0
- data/lib/spec/fixtures/views/articles.haml +2 -0
- data/lib/spec/fixtures/views/comment.haml +8 -0
- data/lib/spec/fixtures/views/home.haml +2 -0
- data/lib/spec/fixtures/views/layout.haml +22 -0
- data/lib/spec/fixtures/views/post.haml +27 -0
- data/lib/spec/fixtures/views/rss.haml +15 -0
- data/lib/spec/fixtures/views/sidebar.haml +21 -0
- data/lib/spec/fixtures/views/style.sass +163 -0
- data/lib/spec/spec_helper.rb +117 -0
- metadata +101 -0
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
|
3
|
+
# http://railscasts.com/episodes/65-stopping-spam-with-akismet
|
4
|
+
# http://github.com/hornbeck/blerb-core/tree/master/lib/akismetor.rb
|
5
|
+
|
6
|
+
class Akismetor
|
7
|
+
attr_accessor :attributes
|
8
|
+
|
9
|
+
# Does a comment-check on Akismet with the submitted hash.
|
10
|
+
# Returns true or false depending on response.
|
11
|
+
def self.spam?(attributes)
|
12
|
+
self.new(attributes).execute('comment-check') == "true"
|
13
|
+
end
|
14
|
+
|
15
|
+
# Does a submit-spam on Akismet with the submitted hash.
|
16
|
+
# Use this when Akismet incorrectly approves a spam comment.
|
17
|
+
def self.submit_spam(attributes)
|
18
|
+
self.new(attributes).execute('submit-spam')
|
19
|
+
end
|
20
|
+
|
21
|
+
# Does a submit-ham on Akismet with the submitted hash.
|
22
|
+
# Use this for a false positive, when Akismet incorrectly rejects a normal comment
|
23
|
+
def self.submit_ham(attributes)
|
24
|
+
self.new(attributes).execute('submit-ham')
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
def initialize(attributes)
|
29
|
+
@attributes = attributes
|
30
|
+
end
|
31
|
+
|
32
|
+
def execute(command)
|
33
|
+
http = Net::HTTP.new("#{attributes[:key]}.rest.akismet.com", 80)
|
34
|
+
response, content = http.post("/1.1/#{command}", attributes_for_post, http_headers)
|
35
|
+
content
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def http_headers
|
41
|
+
{
|
42
|
+
'User-Agent' => 'Akismetor Ruby Library/1.0',
|
43
|
+
'Content-Type' => 'application/x-www-form-urlencoded'
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
def attributes_for_post
|
48
|
+
result = attributes.map { |k, v| "#{k}=#{v}" }.join('&')
|
49
|
+
URI.escape(result)
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
# require 'sinatra/base'
|
3
|
+
|
4
|
+
module Sinatra
|
5
|
+
|
6
|
+
# Sinatra Caching module
|
7
|
+
#
|
8
|
+
# TODO:: Need to write documentation here
|
9
|
+
#
|
10
|
+
module Cache
|
11
|
+
|
12
|
+
VERSION = 'Sinatra::Cache v0.2.2'
|
13
|
+
def self.version; VERSION; end
|
14
|
+
|
15
|
+
|
16
|
+
module Helpers
|
17
|
+
|
18
|
+
# Caches the given URI to a html file in /public
|
19
|
+
#
|
20
|
+
# <b>Usage:</b>
|
21
|
+
# >> cache( erb(:contact, :layout => :layout))
|
22
|
+
# => returns the HTML output written to /public/<CACHE_DIR_PATH>/contact.html
|
23
|
+
#
|
24
|
+
# Also accepts an Options Hash, with the following options:
|
25
|
+
# * :extension => in case you need to change the file extension
|
26
|
+
#
|
27
|
+
# TODO:: implement the opts={} hash functionality. What other options are needed?
|
28
|
+
#
|
29
|
+
def cache(content, opts={})
|
30
|
+
return content unless options.respond_to?("cache_enabled") && options.cache_enabled
|
31
|
+
|
32
|
+
unless content.nil?
|
33
|
+
content = "#{content}\n#{page_cached_timestamp}"
|
34
|
+
path = cache_page_path(request.path_info,opts)
|
35
|
+
FileUtils.makedirs(File.dirname(path))
|
36
|
+
open(path, 'wb+') { |f| f << content }
|
37
|
+
log("Cached Page: [#{path}]",:info)
|
38
|
+
content
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Expires the cached URI (as .html file) in /public
|
43
|
+
#
|
44
|
+
# <b>Usage:</b>
|
45
|
+
# >> cache_expire('/contact')
|
46
|
+
# => deletes the /public/<CACHE_DIR_PATH>contact.html page
|
47
|
+
#
|
48
|
+
# get '/contact' do
|
49
|
+
# cache_expire # deletes the /public/<CACHE_DIR_PATH>contact.html page as well
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# TODO:: implement the options={} hash functionality. What options are really needed ?
|
53
|
+
def cache_expire(path = nil, opts={})
|
54
|
+
return unless options.respond_to?("cache_enabled") && options.cache_enabled
|
55
|
+
|
56
|
+
path = (path.nil?) ? cache_page_path(request.path_info) : cache_page_path(path)
|
57
|
+
if File.exist?(path)
|
58
|
+
File.delete(path)
|
59
|
+
log("Expired Page deleted at: [#{path}]",:info)
|
60
|
+
else
|
61
|
+
log("No Expired Page was found at the path: [#{path}]",:info)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Prints a basic HTML comment with a timestamp in it, so that you can see when a file was cached last.
|
66
|
+
#
|
67
|
+
# *NB!* IE6 does NOT like this to be the first line of a HTML document, so output
|
68
|
+
# inside the <head> tag. Many hours wasted on that lesson ;-)
|
69
|
+
#
|
70
|
+
# <b>Usage:</b>
|
71
|
+
# >> <%= page_cached_timestamp %>
|
72
|
+
# => <!-- page cached: 2009-02-24 12:00:00 -->
|
73
|
+
#
|
74
|
+
def page_cached_timestamp
|
75
|
+
"<!-- page cached: #{Time.now.strftime("%Y-%d-%m %H:%M:%S")} -->\n" if options.cache_enabled
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
# Establishes the file name of the cached file from the path given
|
82
|
+
#
|
83
|
+
# TODO:: implement the opts={} functionality, and support for custom extensions on a per request basis.
|
84
|
+
#
|
85
|
+
def cache_file_name(path,opts={})
|
86
|
+
name = (path.empty? || path == "/") ? "index" : Rack::Utils.unescape(path.sub(/^(\/)/,'').chomp('/'))
|
87
|
+
name << options.cache_page_extension unless (name.split('/').last || name).include? '.'
|
88
|
+
return name
|
89
|
+
end
|
90
|
+
|
91
|
+
# Sets the full path to the cached page/file
|
92
|
+
# Dependent upon Sinatra.options .public and .cache_dir variables being present and set.
|
93
|
+
#
|
94
|
+
def cache_page_path(path,opts={})
|
95
|
+
# test if given a full path rather than relative path, otherwise join the public path to cache_dir
|
96
|
+
# and ensure it is a full path
|
97
|
+
cache_dir = (options.cache_output_dir == File.expand_path(options.cache_output_dir)) ?
|
98
|
+
options.cache_output_dir : File.expand_path("#{options.public}/#{options.cache_output_dir}")
|
99
|
+
cache_dir = cache_output_dir[0..-2] if cache_dir[-1,1] == '/'
|
100
|
+
"#{cache_dir}/#{cache_file_name(path,opts)}"
|
101
|
+
end
|
102
|
+
|
103
|
+
# TODO:: this implementation really stinks, how do I incorporate Sinatra's logger??
|
104
|
+
def log(msg,scope=:debug)
|
105
|
+
if options.cache_logging
|
106
|
+
"Log: msg=[#{msg}]" if scope == options.cache_logging_level
|
107
|
+
else
|
108
|
+
# just ignore the stuff...
|
109
|
+
# puts "just ignoring msg=[#{msg}] since cache_logging => [#{options.cache_logging.to_s}]"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
end #/module Helpers
|
114
|
+
|
115
|
+
|
116
|
+
# Sets the default options:
|
117
|
+
#
|
118
|
+
# * +:cache_enabled+ => toggle for the cache functionality. Default is: +true+
|
119
|
+
# * +:cache_page_extension+ => sets the default extension for cached files. Default is: +.html+
|
120
|
+
# * +:cache_dir+ => sets cache directory where cached files are stored. Default is: ''(empty) == root of /public.<br>
|
121
|
+
# set to empty, since the ideal 'system/cache/' does not work with Passenger & mod_rewrite :(
|
122
|
+
# * +cache_logging+ => toggle for logging the cache calls. Default is: +true+
|
123
|
+
# * +cache_logging_level+ => sets the level of the cache logger. Default is: <tt>:info</tt>.<br>
|
124
|
+
# Options:(unused atm) [:info, :warn, :debug]
|
125
|
+
#
|
126
|
+
def self.registered(app)
|
127
|
+
app.helpers(Cache::Helpers)
|
128
|
+
app.set :cache_enabled, true
|
129
|
+
app.set :cache_page_extension, '.html'
|
130
|
+
app.set :cache_output_dir, ''
|
131
|
+
app.set :cache_logging, true
|
132
|
+
app.set :cache_logging_level, :info
|
133
|
+
end
|
134
|
+
|
135
|
+
end #/module Cache
|
136
|
+
|
137
|
+
#register(Sinatra::Cache)
|
138
|
+
|
139
|
+
end #/module Sinatra
|
@@ -0,0 +1,10 @@
|
|
1
|
+
Feature: Article
|
2
|
+
In order to an Article
|
3
|
+
As anonymous
|
4
|
+
wants to view the the first article
|
5
|
+
|
6
|
+
Scenario: View a single article
|
7
|
+
Given there is one article
|
8
|
+
When I view the Article
|
9
|
+
Then the title should be "Aerial | Congratulations! Aerial is configured correctly"
|
10
|
+
Then I should see "Comments"
|
@@ -0,0 +1,16 @@
|
|
1
|
+
Feature: Homepage
|
2
|
+
In order to view recent Articles
|
3
|
+
As anonymous
|
4
|
+
wants to view the most recent articles
|
5
|
+
|
6
|
+
Scenario: Opening the home page
|
7
|
+
Given there are articles
|
8
|
+
When I view the Homepage
|
9
|
+
Then the title should be "Aerial | A Microblog by Matt Sears"
|
10
|
+
Then I should see "Congratulations! Aerial is configured correctly"
|
11
|
+
Then I should see a link to "Home":http://localhost:4567/home
|
12
|
+
Then I should see a link to "About":http://localhost:4567/about
|
13
|
+
Then I should see a link to "ruby":http://localhost:4567/tags/ruby
|
14
|
+
Then I should see a link to "sinatra":http://localhost:4567/tags/sinatra
|
15
|
+
Then I should see a link to "git":http://localhost:4567/tags/git
|
16
|
+
Then I should see a link to "aerial":http://localhost:4567/tags/aerial
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'spec'
|
2
|
+
require 'webrat'
|
3
|
+
require 'spec/expectations'
|
4
|
+
require 'webrat/sinatra'
|
5
|
+
require 'safariwatir'
|
6
|
+
|
7
|
+
$0 = File.expand_path(File.dirname(__FILE__) + "/../../aerial.rb")
|
8
|
+
require File.expand_path(File.dirname(__FILE__) + "/../../aerial")
|
9
|
+
|
10
|
+
browser = Watir::Safari.new
|
11
|
+
pages = {
|
12
|
+
"Homepage" => "http://localhost:4567",
|
13
|
+
"Article" => "http://localhost:4567/2009/3/31/congratulations-aerial-is-configured-correctly"
|
14
|
+
}
|
15
|
+
|
16
|
+
Before do
|
17
|
+
@browser = browser
|
18
|
+
@pages = pages
|
19
|
+
end
|
20
|
+
|
21
|
+
# Common steps - should these go somewhere else?
|
22
|
+
|
23
|
+
When /^I view the (.*)$/ do |page|
|
24
|
+
@page = eval("#{page}.new(@browser)")
|
25
|
+
@page.goto
|
26
|
+
end
|
27
|
+
|
28
|
+
Then /^the title should be "(.*)"$/ do |text|
|
29
|
+
@browser.title.should == text
|
30
|
+
end
|
31
|
+
|
32
|
+
Then /^I should see "(.*)"$/ do |text|
|
33
|
+
@browser.text.should include(text)
|
34
|
+
end
|
35
|
+
|
36
|
+
Then /I should see a link to "(.*)":(.*)/ do |text, url|
|
37
|
+
@browser.link(:url, url).text.should == text
|
38
|
+
end
|
@@ -0,0 +1,203 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/spec_helper"
|
2
|
+
|
3
|
+
describe 'main aerial application' do
|
4
|
+
|
5
|
+
before do
|
6
|
+
@articles = Article.all
|
7
|
+
setup_repo
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should define the root directory" do
|
11
|
+
AERIAL_ROOT.should_not be_nil
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should define the configuration file" do
|
15
|
+
CONFIG.should_not be_nil
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'should show the default index page' do
|
19
|
+
get '/'
|
20
|
+
@response.should be_ok
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'should send 404 properly' do
|
24
|
+
get '/not-found.html'
|
25
|
+
@response.status.should == 404
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should render a single article page" do
|
29
|
+
get '/2009/1/31/test-article'
|
30
|
+
@response.status.should == 200
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should render the home page" do
|
34
|
+
get '/home'
|
35
|
+
@response.status.should == 200
|
36
|
+
end
|
37
|
+
|
38
|
+
describe "calling /style.css" do
|
39
|
+
|
40
|
+
before do
|
41
|
+
get '/style.css'
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should return an okay status code" do
|
45
|
+
@response.status.should == 200
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should return a css stylesheet" do
|
49
|
+
@response.headers["Content-Type"].should == "text/css;charset=utf-8"
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
describe "calling /tags" do
|
55
|
+
|
56
|
+
before do
|
57
|
+
@article = Article.new(:title => "Test Article",
|
58
|
+
:tags => "git, sinatra",
|
59
|
+
:published_at => DateTime.new,
|
60
|
+
:comments => [],
|
61
|
+
:file_name => "test-article")
|
62
|
+
Aerial::Article.stub!(:with_tag).and_return([@article])
|
63
|
+
get '/tags/git'
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should return a valid response" do
|
67
|
+
@response.status.should == 200
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should return a response body" do
|
71
|
+
@response.body.should_not be_empty
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
describe "calling /feed" do
|
77
|
+
|
78
|
+
before do
|
79
|
+
@articles = Article.find_all
|
80
|
+
Aerial::Article.stub!(:all).and_return(@articles)
|
81
|
+
get '/feed'
|
82
|
+
end
|
83
|
+
|
84
|
+
it "should produce an rss tag" do
|
85
|
+
@response.body.should have_tag('//rss')
|
86
|
+
end
|
87
|
+
|
88
|
+
it "should contain a title tag" do
|
89
|
+
@response.body.should have_tag('//title').with_text(Aerial.config.title)
|
90
|
+
end
|
91
|
+
|
92
|
+
it "should contain a language tag" do
|
93
|
+
@response.body.should have_tag('//language').with_text("en-us")
|
94
|
+
end
|
95
|
+
|
96
|
+
it "should contain a description tag that containts the subtitle" do
|
97
|
+
@response.body.should have_tag('//description').with_text(Aerial.config.subtitle)
|
98
|
+
end
|
99
|
+
|
100
|
+
it "should contain an item tag" do
|
101
|
+
@response.body.should have_tag('//item')
|
102
|
+
end
|
103
|
+
|
104
|
+
it "should have the title tags for the articles" do
|
105
|
+
@response.body.should have_tag('//item[1]/title').with_text(@articles[0].title)
|
106
|
+
@response.body.should have_tag('//item[2]/title').with_text(@articles[1].title)
|
107
|
+
end
|
108
|
+
|
109
|
+
it "should have the link tag with the articles permalink" do
|
110
|
+
#@response.body.should have_tag('//item[1]/link').with_text("http://#{@articles[0].permalink}")
|
111
|
+
end
|
112
|
+
|
113
|
+
it "should have a pubDate tag with the article's publication date" do
|
114
|
+
@response.body.should have_tag('//item[1]/pubDate').with_text(@articles[0].published_at.to_s)
|
115
|
+
@response.body.should have_tag('//item[2]/pubDate').with_text(@articles[1].published_at.to_s)
|
116
|
+
end
|
117
|
+
|
118
|
+
it "should have a guid date that matches the articles id" do
|
119
|
+
@response.body.should have_tag('//item[1]/guid').with_text(@articles[0].id)
|
120
|
+
@response.body.should have_tag('//item[2]/guid').with_text(@articles[1].id)
|
121
|
+
end
|
122
|
+
|
123
|
+
after do
|
124
|
+
@articles = nil
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
|
129
|
+
describe "calling /feed" do
|
130
|
+
|
131
|
+
before do
|
132
|
+
@articles = Article.find_all
|
133
|
+
Aerial::Article.stub!(:all).and_return(@articles)
|
134
|
+
get '/articles'
|
135
|
+
end
|
136
|
+
|
137
|
+
it "should return a valid response" do
|
138
|
+
@response.status.should == 200
|
139
|
+
end
|
140
|
+
|
141
|
+
it "should return a response body" do
|
142
|
+
@response.body.should_not be_empty
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
|
147
|
+
|
148
|
+
describe "calling /arhives" do
|
149
|
+
before do
|
150
|
+
@article = Article.new(:title => "Test Article",
|
151
|
+
:body => "Test Content",
|
152
|
+
:id => 333,
|
153
|
+
:published_at => DateTime.new,
|
154
|
+
:comments => [],
|
155
|
+
:file_name => "test-article.article")
|
156
|
+
Aerial::Article.stub!(:with_date).and_return([@article])
|
157
|
+
get '/archives/year/month'
|
158
|
+
end
|
159
|
+
|
160
|
+
it "should return a valid response" do
|
161
|
+
@response.status.should == 200
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
165
|
+
|
166
|
+
describe "posting a new comment" do
|
167
|
+
|
168
|
+
before do
|
169
|
+
@article = Article.new(:title => "Test Article",
|
170
|
+
:body => "Test Content",
|
171
|
+
:file_name => "test-article",
|
172
|
+
:archive_name => "test-article",
|
173
|
+
:id => 333)
|
174
|
+
Aerial::Article.stub!(:find).and_return(@article)
|
175
|
+
@article.stub!(:comments).and_return(Array.new)
|
176
|
+
@article.stub!(:permalink).and_return('/permalink')
|
177
|
+
post "/article/#{@article.id}/comments"
|
178
|
+
end
|
179
|
+
|
180
|
+
it "should return a valid response" do
|
181
|
+
@response.status.should == 204
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
describe "calling Git operations" do
|
186
|
+
|
187
|
+
before do
|
188
|
+
@dir = "#{Aerial.repo.working_dir}/articles/new_dir"
|
189
|
+
FileUtils.mkdir(@dir)
|
190
|
+
FileUtils.cd(@dir) do
|
191
|
+
FileUtils.touch 'new.file'
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
it "should commit all new untracked and tracked content" do
|
196
|
+
Aerial.repo.status.untracked.should_not be_empty
|
197
|
+
get '/'
|
198
|
+
# Aerial.repo.status.untracked.should be_empty
|
199
|
+
end
|
200
|
+
|
201
|
+
end
|
202
|
+
|
203
|
+
end
|