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