aerial 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
lib/spec/repo
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2009, Matt Sears, Littlelines, LLC.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person
|
4
|
+
obtaining a copy of this software and associated documentation
|
5
|
+
files (the "Software"), to deal in the Software without
|
6
|
+
restriction, including without limitation the rights to use,
|
7
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the
|
9
|
+
Software is furnished to do so, subject to the following
|
10
|
+
conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be
|
13
|
+
included in all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
17
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
19
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
20
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
21
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
Aerial
|
2
|
+
====
|
3
|
+
|
4
|
+
Aerial is a simple, blogish, semi-static web application written in Sinatra.
|
5
|
+
Designed for developers, there is no admin interface and no SQL database.
|
6
|
+
Articles are written in your favorite text editor and versioned with Git.
|
7
|
+
Comments are also stored as plain-text files and pushed to the remote
|
8
|
+
repository when created. It uses Grit (http://github.com/mojombo/grit) to
|
9
|
+
interface with local and remote Git repositories.
|
10
|
+
|
11
|
+
Aerial was designed for small personal blogs and simple semi-static websites
|
12
|
+
such as marketing sites. The main goals are to provide a no-fuss alternative
|
13
|
+
with a basic set features.
|
14
|
+
|
15
|
+
Aerial is still in active development.
|
16
|
+
|
17
|
+
## Features #################################################################
|
18
|
+
|
19
|
+
* Akismet spam filtering (see vendor/akismetor.rb)
|
20
|
+
* Page caching (see vendor/cache.rb)
|
21
|
+
* Support for Markdown
|
22
|
+
* Vlad deployment tasks
|
23
|
+
* YAML configuration
|
24
|
+
* 100% code coverage
|
25
|
+
|
26
|
+
## Requirements #############################################################
|
27
|
+
|
28
|
+
* sinatra (for awesomeness)
|
29
|
+
* git (http://git-scm.com)
|
30
|
+
* grit (interface to git)
|
31
|
+
* yaml (for configuration)
|
32
|
+
* rdiscount (markdown-to-html)
|
33
|
+
* Haml (can easily be switch to erb, or whatever)
|
34
|
+
* jQuery (http://jquery.com)
|
35
|
+
|
36
|
+
## Source ###################################################################
|
37
|
+
|
38
|
+
Grit's Git repo is available on GitHub, which can be browsed at:
|
39
|
+
|
40
|
+
http://github.com/mattsears/aerial
|
41
|
+
|
42
|
+
and cloned with:
|
43
|
+
|
44
|
+
git clone git://github.com/mattsears/aerial.git
|
45
|
+
|
46
|
+
## Getting Started ###########################################################
|
47
|
+
|
48
|
+
Install the following Rubygems:
|
49
|
+
|
50
|
+
sudo gem install sinatra rack thin rdiscount grit haml
|
51
|
+
|
52
|
+
Add your custom settings to the configuration file:
|
53
|
+
|
54
|
+
config/config.yml
|
55
|
+
|
56
|
+
Run the bootstrap Rake task to get started with a sample article
|
57
|
+
|
58
|
+
rake bootstrap
|
59
|
+
|
60
|
+
Now open your browser to:
|
61
|
+
|
62
|
+
http://localhost:4567
|
63
|
+
|
64
|
+
## Todo #####################################################################
|
65
|
+
|
66
|
+
* Enable/disable comments for an article.
|
67
|
+
* Limit the number of comments for an article.
|
68
|
+
* Improve bootstrap tasks
|
69
|
+
* Add more details to this README
|
70
|
+
|
71
|
+
## License ###################################################################
|
72
|
+
|
73
|
+
Aerial is Copyright © 2009 Matt Sears, Littlelines. It is free software,
|
74
|
+
and may be redistributed under the terms specified in the MIT-LICENSE file.
|
data/Rakefile
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
# $:.unshift(File.dirname(__FILE__) + '/../../../lib')
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'spec/version'
|
5
|
+
require 'spec/rake/spectask'
|
6
|
+
require 'cucumber/rake/task'
|
7
|
+
|
8
|
+
AERIAL_ROOT = "."
|
9
|
+
require File.join(AERIAL_ROOT, 'lib', 'aerial')
|
10
|
+
|
11
|
+
# Rspec setup
|
12
|
+
desc "Run all specs"
|
13
|
+
Spec::Rake::SpecTask.new do |t|
|
14
|
+
t.spec_files = FileList['lib/spec/**/*_spec.rb']
|
15
|
+
end
|
16
|
+
|
17
|
+
namespace :spec do
|
18
|
+
desc "Run all specs with rcov"
|
19
|
+
Spec::Rake::SpecTask.new('rcov') do |t|
|
20
|
+
t.spec_files = FileList['lib/spec/**/*_spec.rb']
|
21
|
+
t.rcov = true
|
22
|
+
t.rcov_dir = 'coverage'
|
23
|
+
t.rcov_opts = ['--exclude',
|
24
|
+
"lib/spec.rb,spec\/spec,bin\/spec,examples,\.autotest,#{ENV['GEM_HOME']}"]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
desc "Setup the directory structure"
|
29
|
+
task :bootstrap do
|
30
|
+
Rake::Task['setup:articles_directory'].invoke
|
31
|
+
Rake::Task['setup:views_directory'].invoke
|
32
|
+
Rake::Task['setup:public_directory'].invoke
|
33
|
+
Rake::Task['run'].invoke
|
34
|
+
end
|
35
|
+
|
36
|
+
desc 'Run Aerial in development mode'
|
37
|
+
task :run do
|
38
|
+
exec "ruby lib/aerial.rb"
|
39
|
+
end
|
40
|
+
|
41
|
+
namespace :setup do
|
42
|
+
|
43
|
+
desc "Copy over a sample article"
|
44
|
+
task :articles_directory do
|
45
|
+
puts "* Creating article directory in " + Aerial.config.views.dir
|
46
|
+
article_dir = File.join(AERIAL_ROOT, 'lib','spec','fixtures',
|
47
|
+
'articles', 'sample-article')
|
48
|
+
FileUtils.mkdir_p( Aerial.config.articles.dir )
|
49
|
+
FileUtils.cp_r(article_dir, Aerial.config.articles.dir )
|
50
|
+
Aerial::Git.commit(Aerial.config.articles.dir, "Initial import of first article")
|
51
|
+
end
|
52
|
+
|
53
|
+
task :public_directory do
|
54
|
+
puts "* Creating public directory in " + Aerial.config.public.dir
|
55
|
+
FileUtils.cp_r(File.join(AERIAL_ROOT, 'lib', 'spec',
|
56
|
+
'fixtures', 'public'),
|
57
|
+
Aerial.config.public.dir )
|
58
|
+
end
|
59
|
+
|
60
|
+
task :views_directory do
|
61
|
+
puts "* Creating views directory in " + Aerial.config.views.dir
|
62
|
+
FileUtils.cp_r(File.join(AERIAL_ROOT, 'lib', 'spec',
|
63
|
+
'fixtures', 'views'),
|
64
|
+
Aerial.config.views.dir )
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Cucumber setup
|
69
|
+
Cucumber::Rake::Task.new(:features) do |t|
|
70
|
+
t.cucumber_opts = "--format pretty"
|
71
|
+
end
|
72
|
+
|
73
|
+
# Vlad setup
|
74
|
+
begin
|
75
|
+
require "vlad"
|
76
|
+
Vlad.load(:app => nil, :scm => "git")
|
77
|
+
rescue LoadError
|
78
|
+
# do nothing
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
begin
|
83
|
+
require 'jeweler'
|
84
|
+
Jeweler::Tasks.new do |gemspec|
|
85
|
+
gemspec.name = "aerial"
|
86
|
+
gemspec.summary = "A simple, blogish software build with Sinatra, jQuery, and uses Git for data storage "
|
87
|
+
gemspec.description = "A simple, blogish software build with Sinatra, jQuery, and uses Git for data storage "
|
88
|
+
gemspec.email = "matt@mattsears.com"
|
89
|
+
gemspec.homepage = "http://github.com/mattsears/aerial"
|
90
|
+
gemspec.description = "A simple, blogish software build with Sinatra, jQuery, and uses Git for data storage"
|
91
|
+
gemspec.authors = ["Matt Sears"]
|
92
|
+
end
|
93
|
+
Jeweler::GemcutterTasks.new
|
94
|
+
rescue LoadError
|
95
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
96
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.0
|
data/config/config.yml
ADDED
data/config/deploy.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
# =============================================================================
|
2
|
+
# VLAD VARIABLES
|
3
|
+
# =============================================================================
|
4
|
+
|
5
|
+
set :application, ""
|
6
|
+
set :repository, ""
|
7
|
+
set :deploy_to, ""
|
8
|
+
set :user, ""
|
9
|
+
set :domain, ""
|
10
|
+
set :app_command, "/etc/init.d/apache2"
|
11
|
+
|
12
|
+
desc 'Deploy the app!'
|
13
|
+
task :deploy do
|
14
|
+
Rake::Task["vlad:update"].invoke
|
15
|
+
Rake::Task["vlad:setup_repo"].invoke
|
16
|
+
Rake::Task["vlad:update_config"].invoke
|
17
|
+
end
|
18
|
+
|
19
|
+
desc 'Sync local and production code'
|
20
|
+
remote_task :remote_pull do
|
21
|
+
run "cd #{current_release}; #{git_cmd} pull origin master"
|
22
|
+
end
|
23
|
+
|
24
|
+
namespace :vlad do
|
25
|
+
|
26
|
+
desc 'Restart Passenger'
|
27
|
+
remote_task :start_app do
|
28
|
+
run "touch #{current_release}/tmp/restart.txt"
|
29
|
+
end
|
30
|
+
|
31
|
+
desc 'Restarts the apache servers'
|
32
|
+
remote_task :start_web do
|
33
|
+
run "sudo #{app_command} restart"
|
34
|
+
end
|
35
|
+
|
36
|
+
desc 'Copy the git repo over'
|
37
|
+
remote_task :setup_repo do
|
38
|
+
run "cp -fR #{scm_path}/repo/.git #{current_release}/. "
|
39
|
+
run "cd #{current_release}; #{git_cmd} checkout master"
|
40
|
+
end
|
41
|
+
|
42
|
+
desc 'Upload the configuration script'
|
43
|
+
remote_task :update_config do
|
44
|
+
run "mkdir #{current_release}/config" rescue nil
|
45
|
+
rsync "config/config.yml", "#{current_release}/config/config.yml"
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
|
data/lib/aerial.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
libdir = File.dirname(__FILE__)
|
2
|
+
$LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
|
3
|
+
AERIAL_ROOT = File.join(File.dirname(__FILE__), '..') unless defined? AERIAL_ROOT
|
4
|
+
|
5
|
+
# System requirements
|
6
|
+
require 'rubygems'
|
7
|
+
require 'grit'
|
8
|
+
require 'yaml'
|
9
|
+
require 'sinatra'
|
10
|
+
require 'rdiscount'
|
11
|
+
require 'aerial/base'
|
12
|
+
|
13
|
+
before do
|
14
|
+
# kill trailing slashes for all requests except '/'
|
15
|
+
request.env['PATH_INFO'].gsub!(/\/$/, '') if request.env['PATH_INFO'] != '/'
|
16
|
+
end
|
17
|
+
|
18
|
+
# Configuration
|
19
|
+
configure do
|
20
|
+
set :views => Aerial.config.theme_directory
|
21
|
+
set :public => Aerial.config.public.dir
|
22
|
+
end
|
23
|
+
|
24
|
+
# Helpers
|
25
|
+
helpers do
|
26
|
+
include Rack::Utils
|
27
|
+
include Sinatra::Cache::Helpers
|
28
|
+
include Aerial::Helper
|
29
|
+
alias_method :h, :escape_html
|
30
|
+
end
|
31
|
+
|
32
|
+
# Homepage
|
33
|
+
get '/' do
|
34
|
+
@articles = Aerial::Article.recent(:limit => 10)
|
35
|
+
cache haml(Aerial.config.views.default.to_sym)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Articles
|
39
|
+
get '/articles' do
|
40
|
+
@content_for_sidebar = partial(:sidebar)
|
41
|
+
@articles = Aerial::Article.recent(:limit => 10)
|
42
|
+
cache haml(:articles)
|
43
|
+
end
|
44
|
+
|
45
|
+
get '/feed*' do
|
46
|
+
content_type 'text/xml', :charset => 'utf-8'
|
47
|
+
@articles = Aerial::Article.all
|
48
|
+
cache haml(:rss, :layout => false)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Sassy!
|
52
|
+
get '/style.css' do
|
53
|
+
content_type 'text/css', :charset => 'utf-8'
|
54
|
+
cache sass(:style)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Single page
|
58
|
+
get '/:page' do
|
59
|
+
cache haml(params[:page])
|
60
|
+
end
|
61
|
+
|
62
|
+
# Single article page
|
63
|
+
get '/:year/:month/:day/:article' do
|
64
|
+
link = [params[:year], params[:month], params[:day], params[:article]].join("/")
|
65
|
+
@content_for_sidebar = partial(:sidebar)
|
66
|
+
@article = Aerial::Article.with_permalink("/#{link}")
|
67
|
+
throw :halt, [404, not_found ] unless @article
|
68
|
+
@page_title = @article.title
|
69
|
+
cache haml(:post)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Article tags
|
73
|
+
get '/tags/:tag' do
|
74
|
+
@content_for_sidebar = partial(:sidebar)
|
75
|
+
@articles = Aerial::Article.with_tag(params[:tag])
|
76
|
+
cache haml(:articles)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Article archives
|
80
|
+
get '/archives/:year/:month' do
|
81
|
+
@content_for_sidebar = partial(:sidebar)
|
82
|
+
@articles = Aerial::Article.with_date(params[:year], params[:month])
|
83
|
+
cache haml(:articles)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Add comment
|
87
|
+
post '/article/:id/comments' do
|
88
|
+
@article = Aerial::Article.find(params[:id])
|
89
|
+
throw :halt, [404, not_found ] unless @article
|
90
|
+
|
91
|
+
comment = Aerial::Comment.new(params.merge!( {
|
92
|
+
:referrer => request.referrer,
|
93
|
+
:user_agent => request.user_agent,
|
94
|
+
:user_ip => request.ip
|
95
|
+
}))
|
96
|
+
|
97
|
+
@article.comments << comment.save(@article.archive_name)
|
98
|
+
cache_expire( @article.permalink )
|
99
|
+
status 204
|
100
|
+
end
|
@@ -0,0 +1,241 @@
|
|
1
|
+
module Aerial
|
2
|
+
|
3
|
+
class Article < Content
|
4
|
+
|
5
|
+
attr_reader :comments, :id, :tags, :archive_name, :body_html,
|
6
|
+
:meta, :updated_on, :published, :file_name
|
7
|
+
|
8
|
+
# =============================================================================================
|
9
|
+
# PUBLIC CLASS METHODS
|
10
|
+
# =============================================================================================
|
11
|
+
|
12
|
+
# Find all articles, including drafts
|
13
|
+
def self.all(options={})
|
14
|
+
self.find_all
|
15
|
+
end
|
16
|
+
|
17
|
+
# A quick way to load an article
|
18
|
+
# +id+ of the blob
|
19
|
+
def self.open(id, options = {})
|
20
|
+
self.find_by_blob_id(id, options)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Find a single article
|
24
|
+
# +id+ of the blob
|
25
|
+
def self.find(id, options={})
|
26
|
+
self.find_by_id(id, options)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Find a single article
|
30
|
+
# +name+ of the article file
|
31
|
+
def self.with_name(name, options={})
|
32
|
+
self.find_by_name(name, options)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Find articles by tag
|
36
|
+
# +tag+ category
|
37
|
+
def self.with_tag(tag, options={})
|
38
|
+
self.find_by_tag(tag, options)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Find articles by month and year
|
42
|
+
# +year+ of when article was published
|
43
|
+
# + month+ of when the article was published
|
44
|
+
def self.with_date(year, month, options={})
|
45
|
+
self.find_by_date(year, month, options)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Return an article given its permalink value
|
49
|
+
# +link+ full path of the link
|
50
|
+
def self.with_permalink(link, options={})
|
51
|
+
self.find_by_permalink(link, options)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Find the most recent articles
|
55
|
+
def self.recent(options={})
|
56
|
+
limit = options.delete(:limit) || 4
|
57
|
+
self.find_all(options).first(limit)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Return true if the article file exists
|
61
|
+
def self.exists?(id)
|
62
|
+
self.find_by_name(id) ? true : false
|
63
|
+
end
|
64
|
+
|
65
|
+
# Return all the tags assigned to the articles
|
66
|
+
def self.tags
|
67
|
+
self.find_tags
|
68
|
+
end
|
69
|
+
|
70
|
+
# Calculate the ar
|
71
|
+
def self.archives
|
72
|
+
self.find_archives
|
73
|
+
end
|
74
|
+
|
75
|
+
# =============================================================================================
|
76
|
+
# PUBLIC INSTANCE METHODS
|
77
|
+
# =============================================================================================
|
78
|
+
|
79
|
+
# Add a comment to the list of this Article's comments
|
80
|
+
# +comment new comment
|
81
|
+
def add_comment(comment)
|
82
|
+
self.comments << comment.save(self.archive_name) # should we overload the << method?
|
83
|
+
end
|
84
|
+
|
85
|
+
# Permanent link for the article
|
86
|
+
def permalink
|
87
|
+
link = self.file_name.gsub('.article', '')
|
88
|
+
"/#{published_at.year}/#{published_at.month}/#{published_at.day}/#{escape(link)}"
|
89
|
+
end
|
90
|
+
|
91
|
+
# Returns the absolute path of the Article's file
|
92
|
+
def expand_path
|
93
|
+
return "#{self.archive_expand_path}/#{self.file_name}"
|
94
|
+
end
|
95
|
+
|
96
|
+
# Returns the full path of the article's archive
|
97
|
+
def archive_expand_path
|
98
|
+
return unless archive = self.archive_name
|
99
|
+
return "#{Aerial.repo.working_dir}/#{Aerial.config.articles.dir}/#{archive}"
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
# =============================================================================================
|
105
|
+
# PRIVATE CLASS METHODS
|
106
|
+
# =============================================================================================
|
107
|
+
|
108
|
+
# Find a single article given the article name
|
109
|
+
# +name+ file name
|
110
|
+
def self.find_by_name(name, options={})
|
111
|
+
if tree = Aerial.repo.tree/"#{Aerial.config.articles.dir}/#{name}"
|
112
|
+
return self.find_article(tree)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Find the single article given the id
|
117
|
+
# +id+ the blob id
|
118
|
+
# +options+
|
119
|
+
def self.find_by_id(article_id, options = {})
|
120
|
+
if blog = Aerial.repo.tree/"#{Aerial.config.articles.dir}"
|
121
|
+
blog.contents.each do |entry|
|
122
|
+
article = self.find_article(entry, options)
|
123
|
+
return article if article.id == article_id
|
124
|
+
end
|
125
|
+
end
|
126
|
+
raise "Article not found"
|
127
|
+
end
|
128
|
+
|
129
|
+
# Find the article given the blob id.
|
130
|
+
# This is a more efficient way of find and Article
|
131
|
+
# However, we won't know anything else about the article such as the filename, tree, etc
|
132
|
+
# +id+ of the blob
|
133
|
+
def self.find_by_blob_id(id, options = {})
|
134
|
+
blob = Aerial.repo.blob(id)
|
135
|
+
if blob.size > 0
|
136
|
+
attributes = self.extract_article(blob, options)
|
137
|
+
return Article.new(attributes) if attributes
|
138
|
+
end
|
139
|
+
raise "Article doesn't exists"
|
140
|
+
end
|
141
|
+
|
142
|
+
# Returns the articles with the given tag
|
143
|
+
# +tag+ the article category
|
144
|
+
def self.find_by_tag(tag, options = {})
|
145
|
+
articles = []
|
146
|
+
self.find_all.each do |article|
|
147
|
+
if article.tags.include?(tag)
|
148
|
+
articles << article
|
149
|
+
end
|
150
|
+
end
|
151
|
+
return articles
|
152
|
+
end
|
153
|
+
|
154
|
+
# Find a single article given the article's permalink value
|
155
|
+
def self.find_by_permalink(link, options={})
|
156
|
+
if blog = Aerial.repo.tree/"#{Aerial.config.articles.dir}/"
|
157
|
+
blog.contents.each do |entry|
|
158
|
+
article = self.find_article(entry, options)
|
159
|
+
return article if article.permalink == link
|
160
|
+
end
|
161
|
+
end
|
162
|
+
return false
|
163
|
+
end
|
164
|
+
|
165
|
+
# Find all the articles with the given month and date
|
166
|
+
def self.find_by_date(year, month, options ={})
|
167
|
+
articles = []
|
168
|
+
self.find_all.each do |article|
|
169
|
+
if article.published_at.year == year.to_i &&
|
170
|
+
article.published_at.month == month.to_i
|
171
|
+
articles << article
|
172
|
+
end
|
173
|
+
end
|
174
|
+
return articles
|
175
|
+
end
|
176
|
+
|
177
|
+
# Find all the articles in the reposiotory
|
178
|
+
def self.find_all(options={})
|
179
|
+
articles = []
|
180
|
+
if blog = Aerial.repo.tree/"#{Aerial.config.articles.dir}/"
|
181
|
+
blog.contents.first( options[:limit] || 100 ).each do |entry|
|
182
|
+
article = self.find_article(entry, options)
|
183
|
+
articles << self.find_article(entry, options) if article
|
184
|
+
end
|
185
|
+
end
|
186
|
+
return articles.sort_by { |article| article.published_at}.reverse
|
187
|
+
end
|
188
|
+
|
189
|
+
# Look in the given tree, find the article
|
190
|
+
# +tree+ repository tree
|
191
|
+
# +options+ :blob_id
|
192
|
+
def self.find_article(tree, options = {})
|
193
|
+
comments = []
|
194
|
+
attributes = nil
|
195
|
+
tree.contents.each do |archive|
|
196
|
+
if archive.name =~ /article/
|
197
|
+
attributes = self.extract_article(archive, options)
|
198
|
+
attributes[:archive_name] = tree.name
|
199
|
+
attributes[:file_name] = archive.name
|
200
|
+
elsif archive.name =~ /comment/
|
201
|
+
comments << Comment.open(archive.data, :file_name => archive.name)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
return Article.new(attributes.merge(:comments => comments)) if attributes
|
205
|
+
end
|
206
|
+
|
207
|
+
# Find all the tags assign to the articles
|
208
|
+
def self.find_tags
|
209
|
+
tags = []
|
210
|
+
self.all.each do |article|
|
211
|
+
tags.concat(article.tags)
|
212
|
+
end
|
213
|
+
return tags.uniq
|
214
|
+
end
|
215
|
+
|
216
|
+
# Create a histogram of article archives
|
217
|
+
def self.find_archives
|
218
|
+
dates = []
|
219
|
+
self.all.each do |article|
|
220
|
+
date = article.published_at
|
221
|
+
dates << [date.strftime("%Y/%m"), date.strftime("%B %Y")]
|
222
|
+
end
|
223
|
+
return dates.inject(Hash.new(0)) { |h,x| h[x] += 1; h }
|
224
|
+
end
|
225
|
+
|
226
|
+
# Extract the Article attributes from the file
|
227
|
+
def self.extract_article(blob, options={})
|
228
|
+
file = blob.data
|
229
|
+
article = Hash.new
|
230
|
+
article[:id] = blob.id
|
231
|
+
article[:author] = self.extract_header("author", file)
|
232
|
+
article[:title] = self.extract_header("title", file)
|
233
|
+
article[:tags] = self.extract_header("tags", file).split(/, /)
|
234
|
+
article[:published_at] = DateTime.parse(self.extract_header("published", file))
|
235
|
+
article[:body] = self.scan_for_field(file, self.body_field)
|
236
|
+
article[:body_html] = RDiscount::new( article[:body] ).to_html
|
237
|
+
return article
|
238
|
+
end
|
239
|
+
|
240
|
+
end
|
241
|
+
end
|