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.
Files changed (48) hide show
  1. data/.gitignore +1 -0
  2. data/MIT-LICENSE +22 -0
  3. data/README.md +74 -0
  4. data/Rakefile +96 -0
  5. data/VERSION +1 -0
  6. data/config/config.yml +19 -0
  7. data/config/deploy.rb +50 -0
  8. data/lib/aerial.rb +100 -0
  9. data/lib/aerial/article.rb +241 -0
  10. data/lib/aerial/base.rb +172 -0
  11. data/lib/aerial/comment.rb +160 -0
  12. data/lib/aerial/config.rb +41 -0
  13. data/lib/aerial/content.rb +74 -0
  14. data/lib/aerial/vendor/akismetor.rb +52 -0
  15. data/lib/aerial/vendor/cache.rb +139 -0
  16. data/lib/features/article.feature +10 -0
  17. data/lib/features/home.feature +16 -0
  18. data/lib/features/step_definitions/article_steps.rb +4 -0
  19. data/lib/features/step_definitions/home_steps.rb +8 -0
  20. data/lib/features/support/env.rb +38 -0
  21. data/lib/features/support/pages/article.rb +9 -0
  22. data/lib/features/support/pages/homepage.rb +9 -0
  23. data/lib/spec/aerial_spec.rb +203 -0
  24. data/lib/spec/article_spec.rb +338 -0
  25. data/lib/spec/base_spec.rb +65 -0
  26. data/lib/spec/comment_spec.rb +216 -0
  27. data/lib/spec/config_spec.rb +25 -0
  28. data/lib/spec/fixtures/articles/sample-article/sample-article.article +6 -0
  29. data/lib/spec/fixtures/articles/test-article-one/test-article.article +7 -0
  30. data/lib/spec/fixtures/articles/test-article-three/test-article.article +7 -0
  31. data/lib/spec/fixtures/articles/test-article-two/comment-missing-fields.comment +8 -0
  32. data/lib/spec/fixtures/articles/test-article-two/test-article.article +7 -0
  33. data/lib/spec/fixtures/articles/test-article-two/test-comment.comment +10 -0
  34. data/lib/spec/fixtures/config.yml +35 -0
  35. data/lib/spec/fixtures/public/javascripts/application.js +109 -0
  36. data/lib/spec/fixtures/public/javascripts/jquery-1.3.1.min.js +19 -0
  37. data/lib/spec/fixtures/public/javascripts/jquery.template.js +255 -0
  38. data/lib/spec/fixtures/views/article.haml +19 -0
  39. data/lib/spec/fixtures/views/articles.haml +2 -0
  40. data/lib/spec/fixtures/views/comment.haml +8 -0
  41. data/lib/spec/fixtures/views/home.haml +2 -0
  42. data/lib/spec/fixtures/views/layout.haml +22 -0
  43. data/lib/spec/fixtures/views/post.haml +27 -0
  44. data/lib/spec/fixtures/views/rss.haml +15 -0
  45. data/lib/spec/fixtures/views/sidebar.haml +21 -0
  46. data/lib/spec/fixtures/views/style.sass +163 -0
  47. data/lib/spec/spec_helper.rb +117 -0
  48. 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,4 @@
1
+ Given /^there is one article$/ do
2
+ @articles = Aerial::Article.all.first
3
+ end
4
+
@@ -0,0 +1,8 @@
1
+
2
+ Given /^there are articles$/ do
3
+ @articles = Aerial::Article.all
4
+ end
5
+
6
+ Then /^I should have 2 articles$/ do
7
+ @articles.size.should == 2
8
+ end
@@ -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,9 @@
1
+ class Article
2
+ def initialize(browser)
3
+ @browser = browser
4
+ end
5
+
6
+ def goto
7
+ @browser.goto 'http://localhost:4567/2009/3/31/congratulations-aerial-is-configured-correctly'
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ class Homepage
2
+ def initialize(browser)
3
+ @browser = browser
4
+ end
5
+
6
+ def goto
7
+ @browser.goto 'http://localhost:4567/'
8
+ end
9
+ 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