nesta 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +13 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +58 -0
- data/LICENSE +19 -0
- data/README.md +45 -0
- data/Rakefile +12 -0
- data/bin/nesta +67 -0
- data/config.ru +9 -0
- data/config/config.yml.sample +73 -0
- data/config/deploy.rb.sample +62 -0
- data/lib/nesta/app.rb +199 -0
- data/lib/nesta/cache.rb +139 -0
- data/lib/nesta/commands.rb +135 -0
- data/lib/nesta/config.rb +87 -0
- data/lib/nesta/models.rb +313 -0
- data/lib/nesta/nesta.rb +0 -0
- data/lib/nesta/overrides.rb +59 -0
- data/lib/nesta/path.rb +11 -0
- data/lib/nesta/plugins.rb +15 -0
- data/lib/nesta/version.rb +3 -0
- data/nesta.gemspec +49 -0
- data/scripts/import-from-mephisto +207 -0
- data/spec/atom_spec.rb +138 -0
- data/spec/commands_spec.rb +220 -0
- data/spec/config_spec.rb +69 -0
- data/spec/model_factory.rb +94 -0
- data/spec/models_spec.rb +445 -0
- data/spec/overrides_spec.rb +113 -0
- data/spec/page_spec.rb +428 -0
- data/spec/path_spec.rb +28 -0
- data/spec/sitemap_spec.rb +102 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +72 -0
- data/templates/Gemfile +8 -0
- data/templates/Rakefile +35 -0
- data/templates/config.ru +9 -0
- data/templates/config/config.yml +73 -0
- data/templates/config/deploy.rb +47 -0
- data/views/analytics.haml +12 -0
- data/views/atom.builder +28 -0
- data/views/categories.haml +3 -0
- data/views/comments.haml +8 -0
- data/views/error.haml +13 -0
- data/views/feed.haml +3 -0
- data/views/index.haml +5 -0
- data/views/layout.haml +27 -0
- data/views/master.sass +246 -0
- data/views/not_found.haml +13 -0
- data/views/page.haml +29 -0
- data/views/sidebar.haml +3 -0
- data/views/sitemap.builder +15 -0
- data/views/summaries.haml +14 -0
- metadata +302 -0
data/lib/nesta/nesta.rb
ADDED
File without changes
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Nesta
|
2
|
+
module Overrides
|
3
|
+
module Renderers
|
4
|
+
def haml(template, options = {}, locals = {})
|
5
|
+
defaults = Overrides.render_options(template, :haml)
|
6
|
+
super(template, defaults.merge(options), locals)
|
7
|
+
end
|
8
|
+
|
9
|
+
def scss(template, options = {}, locals = {})
|
10
|
+
defaults = Overrides.render_options(template, :scss)
|
11
|
+
super(template, defaults.merge(options), locals)
|
12
|
+
end
|
13
|
+
|
14
|
+
def sass(template, options = {}, locals = {})
|
15
|
+
defaults = Overrides.render_options(template, :sass)
|
16
|
+
super(template, defaults.merge(options), locals)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.load_local_app
|
21
|
+
require Nesta::Path.local("app")
|
22
|
+
rescue LoadError
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.load_theme_app
|
26
|
+
if Nesta::Config.theme
|
27
|
+
require Nesta::Path.themes(Nesta::Config.theme, "app")
|
28
|
+
end
|
29
|
+
rescue LoadError
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
def self.template_exists?(engine, views, template)
|
34
|
+
views && File.exist?(File.join(views, "#{template}.#{engine}"))
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.render_options(template, engine)
|
38
|
+
if template_exists?(engine, local_view_path, template)
|
39
|
+
{ :views => local_view_path }
|
40
|
+
elsif template_exists?(engine, theme_view_path, template)
|
41
|
+
{ :views => theme_view_path }
|
42
|
+
else
|
43
|
+
{}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.local_view_path
|
48
|
+
Nesta::Path.local("views")
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.theme_view_path
|
52
|
+
if Nesta::Config.theme.nil?
|
53
|
+
nil
|
54
|
+
else
|
55
|
+
Nesta::Path.themes(Nesta::Config.theme, "views")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/nesta/path.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
module Nesta
|
2
|
+
module Plugins
|
3
|
+
def self.load_local_plugins
|
4
|
+
plugins = Dir.glob(File.expand_path('../plugins/*', File.dirname(__FILE__)))
|
5
|
+
plugins.each { |plugin| require_plugin(plugin) }
|
6
|
+
end
|
7
|
+
|
8
|
+
private
|
9
|
+
def self.require_plugin(plugin)
|
10
|
+
require File.join(plugin, 'lib', File.basename(plugin))
|
11
|
+
rescue LoadError => e
|
12
|
+
$stderr.write("Couldn't load plugins/#{File.basename(plugin)}: #{e}\n")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/nesta.gemspec
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "nesta/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "nesta"
|
7
|
+
s.version = Nesta::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Graham Ashton"]
|
10
|
+
s.email = ["graham@effectif.com"]
|
11
|
+
s.homepage = "http://effectif.com/nesta"
|
12
|
+
s.summary = %q{Ruby CMS, written in Sinatra}
|
13
|
+
s.description = <<-EOF
|
14
|
+
Nesta is a lightweight Content Management System, written in Ruby using
|
15
|
+
the Sinatra web framework. Nesta has the simplicity of a static site
|
16
|
+
generator, but (being a fully fledged Rack application) allows you to
|
17
|
+
serve dynamic content on demand.
|
18
|
+
|
19
|
+
Content is stored on disk in plain text files (there is no database).
|
20
|
+
Edit your content in a text editor and keep it under version control
|
21
|
+
(most people use git, but any version control system will do fine).
|
22
|
+
|
23
|
+
Implementing your site's design is easy, but Nesta also has a small
|
24
|
+
selection of themes to choose from.
|
25
|
+
EOF
|
26
|
+
|
27
|
+
s.rubyforge_project = "nesta"
|
28
|
+
|
29
|
+
s.files = `git ls-files`.split("\n")
|
30
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
31
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
32
|
+
s.require_paths = ["lib"]
|
33
|
+
|
34
|
+
s.add_dependency("builder", "2.1.2")
|
35
|
+
s.add_dependency("haml", "3.0.12")
|
36
|
+
s.add_dependency("maruku", "0.6.0")
|
37
|
+
s.add_dependency("RedCloth", "4.2.2")
|
38
|
+
s.add_dependency("sinatra", "1.1.0")
|
39
|
+
|
40
|
+
# Useful in development
|
41
|
+
s.add_dependency("shotgun", ">= 0.8")
|
42
|
+
|
43
|
+
# Test libraries
|
44
|
+
s.add_development_dependency("hpricot", "0.8.2")
|
45
|
+
s.add_development_dependency("rack-test", "0.5.3")
|
46
|
+
s.add_development_dependency("rspec", "1.3.0")
|
47
|
+
s.add_development_dependency("rspec_hpricot_matchers", "1.0")
|
48
|
+
s.add_development_dependency("test-unit", "1.2.3")
|
49
|
+
end
|
@@ -0,0 +1,207 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# Assumptions:
|
4
|
+
#
|
5
|
+
# - It's okay to ignore mephisto's sections and sites.
|
6
|
+
# - Each mephisto tag should be converted to a Nesta category.
|
7
|
+
|
8
|
+
require "getoptlong"
|
9
|
+
require "time"
|
10
|
+
|
11
|
+
require "rubygems"
|
12
|
+
require "active_record"
|
13
|
+
|
14
|
+
require File.join(File.dirname(__FILE__), *%w[.. lib config])
|
15
|
+
require File.join(File.dirname(__FILE__), *%w[.. lib models])
|
16
|
+
|
17
|
+
class Content < ActiveRecord::Base
|
18
|
+
has_many :taggings, :as => :taggable
|
19
|
+
has_many :tags, :through => :taggings
|
20
|
+
|
21
|
+
def self.inheritance_column
|
22
|
+
@inheritance_column = "none"
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.articles
|
26
|
+
conditions = "type = 'Article' and published_at is not NULL"
|
27
|
+
find(:all, :conditions => conditions).map { |c| ArticleLoader.new(c) }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class Tagging < ActiveRecord::Base
|
32
|
+
belongs_to :content
|
33
|
+
belongs_to :tag
|
34
|
+
end
|
35
|
+
|
36
|
+
class Tag < ActiveRecord::Base
|
37
|
+
has_many :taggings
|
38
|
+
has_many :contents, :through => :taggings
|
39
|
+
|
40
|
+
def filename
|
41
|
+
File.join(Nesta::Config.page_path, "#{name}.mdown")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
module ContentWrapper
|
46
|
+
def initialize(content)
|
47
|
+
@content = content
|
48
|
+
end
|
49
|
+
|
50
|
+
def permalink
|
51
|
+
@content.permalink
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
def metadata_string(metadata)
|
56
|
+
metadata.map { |key, value| "#{key}: #{value}" }.sort.join("\n")
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class ArticleLoader
|
61
|
+
include ContentWrapper
|
62
|
+
|
63
|
+
def filename
|
64
|
+
File.join(Nesta::Config.page_path, "#{permalink}.mdown")
|
65
|
+
end
|
66
|
+
|
67
|
+
def date
|
68
|
+
@content.published_at
|
69
|
+
end
|
70
|
+
|
71
|
+
def categories
|
72
|
+
@content.tags.map { |t| t.name }.sort.join(", ")
|
73
|
+
end
|
74
|
+
|
75
|
+
def summary
|
76
|
+
@content.excerpt.strip.gsub(/\r?\n/, '\n')
|
77
|
+
end
|
78
|
+
|
79
|
+
def atom_id(domain)
|
80
|
+
published = "#{@content.created_at.strftime('%Y-%m-%d')}"
|
81
|
+
"tag:#{domain},#{published}:#{@content.id}"
|
82
|
+
end
|
83
|
+
|
84
|
+
def metadata(domain)
|
85
|
+
metadata = {
|
86
|
+
"Date" => date,
|
87
|
+
"Categories" => categories,
|
88
|
+
"Summary" => summary
|
89
|
+
}
|
90
|
+
metadata.merge!("Atom ID" => atom_id(domain)) if domain
|
91
|
+
metadata_string(metadata)
|
92
|
+
end
|
93
|
+
|
94
|
+
def content
|
95
|
+
["# #{@content.title}", @content.body.gsub("\r\n", "\n")].join("\n\n")
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
class App
|
100
|
+
def usage
|
101
|
+
$stderr.write <<-EOF
|
102
|
+
Usage: #{File.basename $0} [OPTIONS] -u <username> -p <password>
|
103
|
+
|
104
|
+
OPTIONS (defaults shown in brackets)
|
105
|
+
|
106
|
+
-a, --adapter Database adapter (mysql)
|
107
|
+
--domain Site's domain name (for preserving <id/> tags in feed)
|
108
|
+
--clobber Overwrite existing files
|
109
|
+
-d, --database Database name (mephisto_production)
|
110
|
+
-h, --host Database hostname (localhost)
|
111
|
+
-p, --password Database password
|
112
|
+
-u, --username Database username
|
113
|
+
|
114
|
+
EOF
|
115
|
+
exit 1
|
116
|
+
end
|
117
|
+
|
118
|
+
def parse_command_line
|
119
|
+
parser = GetoptLong.new
|
120
|
+
parser.set_options(
|
121
|
+
["-a", "--adapter", GetoptLong::REQUIRED_ARGUMENT],
|
122
|
+
["--domain", GetoptLong::REQUIRED_ARGUMENT],
|
123
|
+
["--clobber", GetoptLong::NO_ARGUMENT],
|
124
|
+
["-d", "--database", GetoptLong::REQUIRED_ARGUMENT],
|
125
|
+
["-h", "--host", GetoptLong::REQUIRED_ARGUMENT],
|
126
|
+
["-u", "--username", GetoptLong::REQUIRED_ARGUMENT],
|
127
|
+
["-p", "--password", GetoptLong::REQUIRED_ARGUMENT])
|
128
|
+
loop do
|
129
|
+
opt, arg = parser.get
|
130
|
+
break if not opt
|
131
|
+
case opt
|
132
|
+
when "-a"
|
133
|
+
@adapter = arg
|
134
|
+
when "--domain"
|
135
|
+
@domain = arg
|
136
|
+
when "--clobber"
|
137
|
+
@clobber = true
|
138
|
+
when "-d"
|
139
|
+
@database = arg
|
140
|
+
when "-h"
|
141
|
+
@host = arg
|
142
|
+
when "-p"
|
143
|
+
@password = arg
|
144
|
+
when "-u"
|
145
|
+
@username = arg
|
146
|
+
end
|
147
|
+
end
|
148
|
+
@adapter ||= "mysql"
|
149
|
+
@clobber.nil? && @clobber = false
|
150
|
+
@host ||= "localhost"
|
151
|
+
@database ||= "mephisto_production"
|
152
|
+
usage if @username.nil? || @password.nil?
|
153
|
+
end
|
154
|
+
|
155
|
+
def connect_to_database
|
156
|
+
ActiveRecord::Base.establish_connection(
|
157
|
+
:adapter => @adapter,
|
158
|
+
:host => @host,
|
159
|
+
:username => @username,
|
160
|
+
:password => @password,
|
161
|
+
:database => @database
|
162
|
+
)
|
163
|
+
end
|
164
|
+
|
165
|
+
def should_import?(item)
|
166
|
+
if File.exist?(item.filename) && (! @clobber)
|
167
|
+
puts "skipping (specify --clobber to overwrite)"
|
168
|
+
false
|
169
|
+
else
|
170
|
+
true
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def import_articles
|
175
|
+
Content.articles.each do |article|
|
176
|
+
puts "Importing article: #{article.permalink}"
|
177
|
+
if should_import?(article)
|
178
|
+
File.open(article.filename, "w") do |file|
|
179
|
+
file.write [article.metadata(@domain), article.content].join("\n\n")
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def import_tags
|
186
|
+
Tag.find(:all).each do |tag|
|
187
|
+
puts "Importing tag: #{tag.name}"
|
188
|
+
if should_import?(tag)
|
189
|
+
File.open(tag.filename, "w") do |file|
|
190
|
+
file.write("# #{tag.name.capitalize}\n")
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def main
|
197
|
+
parse_command_line
|
198
|
+
connect_to_database
|
199
|
+
import_articles
|
200
|
+
import_tags
|
201
|
+
rescue GetoptLong::Error
|
202
|
+
exit 1
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
app = App.new
|
207
|
+
app.main
|
data/spec/atom_spec.rb
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
require File.expand_path('spec_helper', File.dirname(__FILE__))
|
2
|
+
require File.expand_path('model_factory', File.dirname(__FILE__))
|
3
|
+
|
4
|
+
describe "atom feed" do
|
5
|
+
include ConfigSpecHelper
|
6
|
+
include ModelFactory
|
7
|
+
include RequestSpecHelper
|
8
|
+
|
9
|
+
before(:each) do
|
10
|
+
stub_configuration
|
11
|
+
stub_config_key("author", {
|
12
|
+
"name" => "Fred Bloggs",
|
13
|
+
"uri" => "http://fredbloggs.com",
|
14
|
+
"email" => "fred@fredbloggs.com"
|
15
|
+
})
|
16
|
+
get "/articles.xml"
|
17
|
+
end
|
18
|
+
|
19
|
+
after(:each) do
|
20
|
+
remove_fixtures
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should render successfully" do
|
24
|
+
last_response.should be_ok
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should use Atom's XML namespace" do
|
28
|
+
body.should have_tag("/feed[@xmlns=http://www.w3.org/2005/Atom]")
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should have an ID element" do
|
32
|
+
body.should have_tag("/feed/id", "tag:example.org,2009:/")
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should have an alternate link element" do
|
36
|
+
body.should have_tag("/feed/link[@rel=alternate][@href='http://example.org']")
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should have a self link element" do
|
40
|
+
body.should have_tag(
|
41
|
+
"/feed/link[@rel=self][@href='http://example.org/articles.xml']")
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should have title and subtitle" do
|
45
|
+
body.should have_tag("/feed/title[@type=text]", "My blog")
|
46
|
+
body.should have_tag("/feed/subtitle[@type=text]", "about stuff")
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should include the author details" do
|
50
|
+
body.should have_tag("/feed/author/name", "Fred Bloggs")
|
51
|
+
body.should have_tag("/feed/author/uri", "http://fredbloggs.com")
|
52
|
+
body.should have_tag("/feed/author/email", "fred@fredbloggs.com")
|
53
|
+
end
|
54
|
+
|
55
|
+
describe "for article" do
|
56
|
+
before(:each) do
|
57
|
+
@heading = "Heading"
|
58
|
+
@articles = []
|
59
|
+
@category = create_category
|
60
|
+
11.times do |i|
|
61
|
+
@articles << create_article(
|
62
|
+
:heading => "Article #{i + 1}",
|
63
|
+
:path => "article-#{i + 1}",
|
64
|
+
:metadata => {
|
65
|
+
"categories" => @category.path,
|
66
|
+
"date" => "#{i + 1} January 2009"
|
67
|
+
},
|
68
|
+
:content => "Blah blah\n\n## #{@heading}\n\n[link](/foo)"
|
69
|
+
)
|
70
|
+
end
|
71
|
+
@article = @articles.last
|
72
|
+
get "/articles.xml"
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should set title" do
|
76
|
+
body.should have_tag("entry/title", "Article 11")
|
77
|
+
end
|
78
|
+
|
79
|
+
it "should link to the HTML version" do
|
80
|
+
url = "http://example.org/#{@article.path}"
|
81
|
+
body.should have_tag(
|
82
|
+
"entry/link[@href='#{url}'][@rel=alternate][@type='text/html']")
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should define unique ID" do
|
86
|
+
body.should have_tag(
|
87
|
+
"entry/id", "tag:example.org,2009-01-11:#{@article.abspath}")
|
88
|
+
end
|
89
|
+
|
90
|
+
it "should specify date published" do
|
91
|
+
body.should have_tag("entry/published", "2009-01-11T00:00:00+00:00")
|
92
|
+
end
|
93
|
+
|
94
|
+
it "should specify article categories" do
|
95
|
+
body.should have_tag("category[@term=#{@category.permalink}]")
|
96
|
+
end
|
97
|
+
|
98
|
+
it "should have article content" do
|
99
|
+
body.should have_tag(
|
100
|
+
"entry/content[@type=html]", /<h2[^>]*>#{@heading}<\/h2>/)
|
101
|
+
end
|
102
|
+
|
103
|
+
it "should include hostname in URLs" do
|
104
|
+
body.should have_tag("entry/content", /href='http:\/\/example.org\/foo/)
|
105
|
+
end
|
106
|
+
|
107
|
+
it "should not include article heading in content" do
|
108
|
+
body.should_not have_tag("entry/summary", /#{@article.heading}/)
|
109
|
+
end
|
110
|
+
|
111
|
+
it "should list the latest 10 articles" do
|
112
|
+
body.should have_tag("entry", :count => 10)
|
113
|
+
body.should_not have_tag("entry/title", @articles.first.heading)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
describe "page with no date" do
|
118
|
+
before(:each) do
|
119
|
+
create_category(:path => "no-date")
|
120
|
+
get "/articles.xml"
|
121
|
+
end
|
122
|
+
|
123
|
+
it "should not appear in feed" do
|
124
|
+
body.should_not have_tag("entry/id", /tag.*no-date/)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
describe "article with atom ID" do
|
129
|
+
it "should use pre-defined ID" do
|
130
|
+
create_article(:metadata => {
|
131
|
+
"date" => "1 January 2009",
|
132
|
+
"atom id" => "use-this-id"
|
133
|
+
})
|
134
|
+
get "/articles.xml"
|
135
|
+
body.should have_tag("entry/id", "use-this-id")
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|