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
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
|