nesta 0.9.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 +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
|