nesta 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. data/.gitignore +13 -0
  2. data/Gemfile +6 -0
  3. data/Gemfile.lock +58 -0
  4. data/LICENSE +19 -0
  5. data/README.md +45 -0
  6. data/Rakefile +12 -0
  7. data/bin/nesta +67 -0
  8. data/config.ru +9 -0
  9. data/config/config.yml.sample +73 -0
  10. data/config/deploy.rb.sample +62 -0
  11. data/lib/nesta/app.rb +199 -0
  12. data/lib/nesta/cache.rb +139 -0
  13. data/lib/nesta/commands.rb +135 -0
  14. data/lib/nesta/config.rb +87 -0
  15. data/lib/nesta/models.rb +313 -0
  16. data/lib/nesta/nesta.rb +0 -0
  17. data/lib/nesta/overrides.rb +59 -0
  18. data/lib/nesta/path.rb +11 -0
  19. data/lib/nesta/plugins.rb +15 -0
  20. data/lib/nesta/version.rb +3 -0
  21. data/nesta.gemspec +49 -0
  22. data/scripts/import-from-mephisto +207 -0
  23. data/spec/atom_spec.rb +138 -0
  24. data/spec/commands_spec.rb +220 -0
  25. data/spec/config_spec.rb +69 -0
  26. data/spec/model_factory.rb +94 -0
  27. data/spec/models_spec.rb +445 -0
  28. data/spec/overrides_spec.rb +113 -0
  29. data/spec/page_spec.rb +428 -0
  30. data/spec/path_spec.rb +28 -0
  31. data/spec/sitemap_spec.rb +102 -0
  32. data/spec/spec.opts +1 -0
  33. data/spec/spec_helper.rb +72 -0
  34. data/templates/Gemfile +8 -0
  35. data/templates/Rakefile +35 -0
  36. data/templates/config.ru +9 -0
  37. data/templates/config/config.yml +73 -0
  38. data/templates/config/deploy.rb +47 -0
  39. data/views/analytics.haml +12 -0
  40. data/views/atom.builder +28 -0
  41. data/views/categories.haml +3 -0
  42. data/views/comments.haml +8 -0
  43. data/views/error.haml +13 -0
  44. data/views/feed.haml +3 -0
  45. data/views/index.haml +5 -0
  46. data/views/layout.haml +27 -0
  47. data/views/master.sass +246 -0
  48. data/views/not_found.haml +13 -0
  49. data/views/page.haml +29 -0
  50. data/views/sidebar.haml +3 -0
  51. data/views/sitemap.builder +15 -0
  52. data/views/summaries.haml +14 -0
  53. metadata +302 -0
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,11 @@
1
+ module Nesta
2
+ class Path
3
+ def self.local(*args)
4
+ File.expand_path(File.join(args), Nesta::App.root)
5
+ end
6
+
7
+ def self.themes(*args)
8
+ File.expand_path(File.join('themes', *args), Nesta::App.root)
9
+ end
10
+ end
11
+ end
@@ -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
@@ -0,0 +1,3 @@
1
+ module Nesta
2
+ VERSION = "0.9.0"
3
+ 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