noumenon 0.0.1

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 (34) hide show
  1. data/.gitignore +4 -0
  2. data/.travis.yml +6 -0
  3. data/Gemfile +4 -0
  4. data/README.md +85 -0
  5. data/Rakefile +9 -0
  6. data/lib/noumenon.rb +53 -0
  7. data/lib/noumenon/configuration.rb +26 -0
  8. data/lib/noumenon/core.rb +93 -0
  9. data/lib/noumenon/spec.rb +8 -0
  10. data/lib/noumenon/spec/fixtures.rb +34 -0
  11. data/lib/noumenon/template.rb +88 -0
  12. data/lib/noumenon/version.rb +3 -0
  13. data/noumenon.gemspec +27 -0
  14. data/spec/fixtures/fixture_specs/test +1 -0
  15. data/spec/fixtures/static_example/directory_with_index/index.yml +1 -0
  16. data/spec/fixtures/static_example/found.html +1 -0
  17. data/spec/fixtures/static_example/liquid_example.html +1 -0
  18. data/spec/fixtures/static_example/template_with_substitutions.html +1 -0
  19. data/spec/fixtures/static_example/templates/basic_example.yml +1 -0
  20. data/spec/fixtures/static_example/templates/with_fields.yml +2 -0
  21. data/spec/fixtures/static_example_dependencies/themes/example/assets/example.txt +1 -0
  22. data/spec/fixtures/static_example_dependencies/themes/example/layout.html +3 -0
  23. data/spec/fixtures/static_example_dependencies/themes/example_without_layout/basic_template.html +1 -0
  24. data/spec/fixtures/static_example_dependencies/themes/example_without_layout/fields.html +1 -0
  25. data/spec/fixtures/static_example_dependencies/themes/example_without_layout/template_with_fields.html +23 -0
  26. data/spec/noumenon/config_spec.rb +29 -0
  27. data/spec/noumenon/core_spec.rb +105 -0
  28. data/spec/noumenon/spec/fixtures_spec.rb +41 -0
  29. data/spec/noumenon/spec_spec.rb +7 -0
  30. data/spec/noumenon/template_spec.rb +118 -0
  31. data/spec/noumenon_spec.rb +61 -0
  32. data/spec/spec_helper.rb +19 -0
  33. data/watchr.rb +2 -0
  34. metadata +236 -0
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
@@ -0,0 +1,6 @@
1
+ rvm:
2
+ - 1.8.7
3
+ - 1.9.2
4
+ - ree
5
+ - rbx
6
+ - jruby
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in noumenon.gemspec
4
+ gemspec
@@ -0,0 +1,85 @@
1
+ # Noumenon
2
+
3
+ Noumenon: Noun. The intellectual conception of a thing as it is in itself, not as it is known through perception.
4
+
5
+ ## What is This Thing?
6
+
7
+ Noumenon is a web application based on Sinatra for constructing dynamic websites which use a Git repository
8
+ for storing all content. It's designed to allow technical and non-technical teams to collaborate on development and
9
+ populate of a content management system using the tools they are most comfortable with.
10
+
11
+ In the case of developers (and some designers) that's version control and text editors, while in the case of content
12
+ editors thats more likely to be a web interface.
13
+
14
+ ## How it Works
15
+
16
+ The URL structure and content of a Noumenon site is defined by a Git repository, similar to the structure below:
17
+
18
+ /
19
+ - /config.rb
20
+ - /index.html
21
+ - /about
22
+ - /people.html
23
+ - /company.html
24
+ - /contact
25
+ - /config.rb
26
+ - /blog
27
+ - /config.rb
28
+ - /posts/2011-04-18-an-example-post.md
29
+ - /posts/2011-04-10-another-example.md
30
+
31
+ This git repository is then provided to Noumenon as it's data source. On startup it loads any file called "config.rb"
32
+ and uses it to determine how that directory should behave:
33
+
34
+ # /config.rb
35
+ domain "example.org"
36
+ application "git://github.com/noumenon/apps-static"
37
+ theme "git://github.com/noumenon/themes-example"
38
+
39
+ That example configures Noumenon to use the "Noumenon::Static" application to serve any templates below that point as
40
+ a static page.
41
+
42
+ # /contact/config.rb
43
+ application "git://github.com/noumenon/apps-contact"
44
+ contact.email_address "info@example.org"
45
+
46
+ While the one in /contact specifies that "Noumenon::Contact" should be used to provide the URL tree below /contact, in this
47
+ case a contact form which emails any responses to the specified address.
48
+
49
+ Finally /blogs/config.rb might look something like this:
50
+
51
+ application "git://github.com/noumenon/apps-blog"
52
+ blog.comments true
53
+
54
+ ## Hosting a Noumenon Site
55
+
56
+ *This won't work yet: I havn't implemented automatic check out of a content repository.*
57
+
58
+ To host a site you will need to have Ruby and an application server such as Passenger, Unicorn or Thin. Due it's interactions
59
+ with git as a data store Heroku is not a supported platform for hosting, although I'm sure someone will find a way around that.
60
+
61
+ Install Noumenon: `gem install noumenon`
62
+
63
+ Create a directory, and put the following config.ru in it:
64
+
65
+ require 'noumenon'
66
+ Noumenon::Core.set :content_repository, "git@github.com:Noumenon/example.git"
67
+
68
+ run Noumenon.boot
69
+
70
+ And then set up your application server to provide that application. On startup it will first attempt to check out the content
71
+ repository, then it will parse any configuration files, and install any required dependencies. Finally, it will start hosting
72
+ the site.
73
+
74
+ To update the content repository restart your application server, which will cause it to update the repository, and any dependencies.
75
+
76
+ ### Hosting on Heroku
77
+
78
+ The someone who worked out the way around it was me it seems. This will only work if you're deploying an entirely static site,
79
+ if you need the admin section to work then you'll have to host somewhere else, but otherwise, use a config.ru like this, and make
80
+ sure you have Noumenon in your Gemfile:
81
+
82
+ require 'noumenon'
83
+ Noumenon::Core.set :content_repository_path, File.expand_path("..", __FILE__)
84
+
85
+ run Noumenon.boot
@@ -0,0 +1,9 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ task :default => "specs"
5
+
6
+ desc "Run all specs"
7
+ task "specs" do
8
+ exec "rspec spec"
9
+ end
@@ -0,0 +1,53 @@
1
+ require 'noumenon/configuration'
2
+ require 'noumenon/template'
3
+ require 'noumenon/core'
4
+
5
+ module Noumenon
6
+ class TemplateNotFoundError < StandardError; end
7
+
8
+ class << self
9
+ # Returns a configured Noumenon application. Intended for use in a Rack configuration:
10
+ #
11
+ # Example:
12
+ #
13
+ # require 'noumenon'
14
+ # Noumenon::Core.set :content_repository_path, "/srv/sites/example.org"
15
+ #
16
+ # run Noumenon.boot
17
+ def boot
18
+ Rack::Builder.new do
19
+ Dir.glob(File.join(Noumenon.config.dependencies_path, "themes/*/assets")).each do |path|
20
+ theme = path.split("/")[-2]
21
+ url = "/themes/#{theme}"
22
+
23
+ map(url) { run Rack::File.new(path) }
24
+ end
25
+
26
+ map("/") { run Core.new }
27
+ end
28
+ end
29
+
30
+ # Provides access to the configuration of this application.
31
+ #
32
+ # If passed a block then the current configuration will be yielded in the form
33
+ # of a Noumenon::Configuration instance, allowing it to be changed.
34
+ #
35
+ # Noumenon.config do |c|
36
+ # c.theme = "example_theme"
37
+ # end
38
+ #
39
+ def config
40
+ @config ||= Configuration.new
41
+ yield @config if block_given?
42
+ @config
43
+ end
44
+ alias :configure :config
45
+
46
+ # Set the application configuration
47
+ #
48
+ # Should be provided with something that implements the same interface as Noumenon::Configuration.
49
+ def config=(config)
50
+ @config = config
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,26 @@
1
+ module Noumenon
2
+ # Handles configuration for a Noumenon application.
3
+ class Configuration
4
+ class ThemeNotFoundError < StandardError; end
5
+
6
+ # The path to load content from.
7
+ attr_accessor :content_repository_path
8
+
9
+ # The path to load any depdencies such as themes and applications from.
10
+ attr_accessor :dependencies_path
11
+
12
+ # The theme to use.
13
+ attr_accessor :theme
14
+
15
+ # Sets the current theme.
16
+ #
17
+ # If the theme could not be found under dependencies_path then a ThemeNotFoundError will be raised.
18
+ def theme=(value)
19
+ unless File.exist?(File.join(dependencies_path, "themes", value))
20
+ raise ThemeNotFoundError.new("The theme #{value} could not be found in #{dependencies_path}/themes.")
21
+ end
22
+
23
+ @theme = value
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,93 @@
1
+ require 'sinatra'
2
+ require 'liquid'
3
+ require 'yaml'
4
+
5
+ # :nodoc:
6
+ module Noumenon
7
+ # The base of all Noumenon sites, responsible for building the URL structure, and providing
8
+ # access to the content repository.
9
+ #
10
+ # By default any request will be routed to the content repository to find an item of content
11
+ # using #locate_content, which will then be rendered using the template specified by the content
12
+ # file.
13
+ #
14
+ # A piece of content takes the form of a YAML file, which should provide any fields required by
15
+ # template specified. If any required fields are missing then a MissingContentError will be raised.
16
+ #
17
+ class Core < Sinatra::Base
18
+ def config
19
+ Noumenon.config
20
+ end
21
+
22
+ # Get the path to a file in the content repository.
23
+ def content_path(path)
24
+ File.join(config.content_repository_path, path)
25
+ end
26
+
27
+ # Get the path to a file provided by the current theme.
28
+ def theme_path(path)
29
+ File.join(config.dependencies_path, "themes", config.theme, path)
30
+ end
31
+
32
+ # Render a template, surrounding it with the theme layout if a theme has been set, and
33
+ # it provides a layout.
34
+ #
35
+ # Any locals provided will be passed onto Liquid for use in the template.
36
+ #
37
+ # Valid options:
38
+ #
39
+ # layout: If set to a string, an attempt will be made to use that layout.
40
+ # If set to false no layout will be used.
41
+ # If set to true (the default) the default layout will be used.
42
+ def render_template(path, locals = {}, options = {})
43
+ options[:layout] = "layout" if options[:layout] == true || options[:layout].nil?
44
+
45
+ template = Template.from_file(path)
46
+ body = template.render(locals)
47
+
48
+ if options[:layout]
49
+ begin
50
+ body = render_template(theme_path("#{options[:layout]}.html"), { 'body' => body }, :layout => false)
51
+ rescue Noumenon::Template::NotFoundError => ignore_missing_layouts
52
+ end
53
+ end
54
+
55
+ body
56
+ end
57
+
58
+ # Render a piece of content from the content repository, using the template specified within
59
+ # the content item.
60
+ #
61
+ # If no template was specified then the template "default" will be used.
62
+ def render_content(path)
63
+ content = File.read(content_path(path))
64
+ item = YAML.load( content )
65
+
66
+ [ 200, render_template( theme_path("#{item["template"]}.html"), item ) ]
67
+ end
68
+
69
+ # Locate a piece of content in the content repository based on the URL it is being accessed from.
70
+ #
71
+ # The following files will satisfy a request for "/example", in the order shown:
72
+ #
73
+ # 1. "repository/example.yml
74
+ # 2. "repository/example/index.yml
75
+ #
76
+ def locate_content_for(path)
77
+ if File.exists?(content_path("#{path}.yml"))
78
+ return "#{path}.yml"
79
+ else
80
+ if File.exists?(content_path(path)) && File.exists?(content_path("#{path}/index.yml"))
81
+ return "#{path}/index.yml"
82
+ end
83
+ end
84
+
85
+ nil
86
+ end
87
+
88
+ get '*' do |path|
89
+ path = locate_content_for(path)
90
+ path ? render_content(path) : [ 404, "Not Found" ]
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,8 @@
1
+ require 'noumenon/spec/fixtures'
2
+
3
+ module Noumenon
4
+ # Spec helpers used while testing Noumenon, and Noumenon applications.
5
+ module Spec
6
+ include Noumenon::Spec::Fixtures
7
+ end
8
+ end
@@ -0,0 +1,34 @@
1
+ require 'pathname'
2
+
3
+ module Noumenon
4
+ module Spec
5
+ # Provides a simple way of mocking a content repository for specs.
6
+ module Fixtures
7
+ # The current path that fixtures are looked up from.
8
+ def fixture_path(path = nil)
9
+ @fixture_path ||= 'spec/fixtures'
10
+ path.nil? ? @fixture_path : File.join(@fixture_path, path)
11
+ end
12
+
13
+ # Set the path that fixtures will be looked up from.
14
+ #
15
+ # Unless set to an absolute path the provided path will be assumed to be a sub-directory
16
+ # of "spec/fixtures".
17
+ def fixture_path=(value)
18
+ value = File.join("spec/fixtures", value) unless Pathname.new(value).absolute?
19
+ @fixture_path = value
20
+ end
21
+
22
+ # Loads a fixture from the specified path.
23
+ #
24
+ # Example:
25
+ #
26
+ # File.write(File.join(fixture_path, "index.html"), "<h1>Hello, world!</h1>")
27
+ # fixture("index.html")
28
+ # # => "<h1>Hello, world!</h1>"
29
+ def fixture(path)
30
+ File.read File.join(fixture_path, path)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,88 @@
1
+ require 'liquid'
2
+
3
+ module Noumenon
4
+ # Templates specify the structure and presentation of a particular piece of content in
5
+ # a Noumenon site, and are usually provided by a theme.
6
+ #
7
+ # A template is split into two parts, the metadata, which defines what data is needed for
8
+ # the template, and a Liquid template, which defines how the data should be presented. When
9
+ # loaded from a file, a template will look like this:
10
+ #
11
+ # title:
12
+ # label: Page title
13
+ # required: true
14
+ # type: string
15
+ # help: The title displayed at the top of the page
16
+ # author:
17
+ # label: Author
18
+ # required: false
19
+ # type: string
20
+ # help: The author of the article. If not provided, no byline will be shown.
21
+ # body:
22
+ # label: Body text
23
+ # required: true
24
+ # type: text
25
+ # help: The main article body. Will be processed with Textile for formatting.
26
+ # ---
27
+ # <h1>{{ title }}</h1>
28
+ # {% if author %}
29
+ # <p class="byline">By {{ author }}</p>
30
+ # {% endif %}
31
+ # {{ body | textilize }}
32
+ #
33
+ # And can be rendered like this:
34
+ #
35
+ # Template.from_file("/path/to/template").render("title" => "An Example Page", "author" => "Jon Wood", "body" => "This is an article...")
36
+ #
37
+ # If any required fields are missing from the data provided then a MissingContentError will be raised.
38
+ class Template
39
+ class MissingContentError < StandardError; end
40
+ class NotFoundError < StandardError; end
41
+
42
+ # The location this template was loaded from.
43
+ attr_accessor :source
44
+
45
+ # The template view.
46
+ attr_accessor :content
47
+
48
+ # The fields used by this template.
49
+ attr_accessor :fields
50
+
51
+ # Loads a template from the specified path.
52
+ #
53
+ # If the file does not exist then a NotFoundError will be raised.
54
+ def self.from_file(path)
55
+ raise NotFoundError.new("No template could be found at #{path}.") unless File.exists?(path)
56
+
57
+ content = File.read(path)
58
+
59
+ parts = content.split("\n---\n")
60
+ if parts.size == 1
61
+ fields = {}
62
+ else
63
+ fields = YAML.load(parts.first)
64
+ content = parts.last
65
+ end
66
+
67
+ self.new(path, content, fields)
68
+ end
69
+
70
+ def initialize(source = nil, content = nil, fields = {})
71
+ @source = source
72
+ @content = content
73
+ @fields = fields
74
+ end
75
+
76
+ # Renders the template. If any fields are missing from page_content then a MissingContentError will be raised.
77
+ def render(page_content = {})
78
+ missing_fields = []
79
+ fields.each do |key, field|
80
+ missing_fields << key if field["required"] && !page_content.key?(key)
81
+ end
82
+
83
+ raise MissingContentError.new("The following fields were missing from your content: #{missing_fields.sort.join(", ")}") unless missing_fields.empty?
84
+
85
+ Liquid::Template.parse(content).render(page_content)
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,3 @@
1
+ module Noumenon
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "noumenon/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "noumenon"
7
+ s.version = Noumenon::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Jon Wood"]
10
+ s.email = ["jon@blankpad.net"]
11
+ s.homepage = "https://github.com/Noumenon"
12
+ s.summary = %q{An content management system backed by Git}
13
+ s.description = File.read("README.md")
14
+
15
+ s.rubyforge_project = "noumenon"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+
22
+ s.add_dependency "sinatra", ">= 1.2.3"
23
+ s.add_dependency "liquid", ">= 2.2"
24
+
25
+ s.add_development_dependency "rspec", ">= 2.5.0"
26
+ s.add_development_dependency "rack-test", ">= 0.5"
27
+ end
@@ -0,0 +1 @@
1
+ Hello, fixtures.
@@ -0,0 +1 @@
1
+ template: "basic_template"
@@ -0,0 +1 @@
1
+ <h1>Hello</h1>
@@ -0,0 +1 @@
1
+ template: "basic_template"
@@ -0,0 +1,2 @@
1
+ template: "fields"
2
+ example_field: "value"
@@ -0,0 +1,3 @@
1
+ <div id="layout">
2
+ {{ body }}
3
+ </div>
@@ -0,0 +1,23 @@
1
+ title:
2
+ type: string
3
+ label: Page title
4
+ help: Appears at the top of the page.
5
+ required: true
6
+
7
+ body:
8
+ type: text
9
+ label: Body text
10
+ help: The main body of the page
11
+ required: true
12
+
13
+ author:
14
+ type: string
15
+ label: Author name
16
+ help: If not provided no author will be shown
17
+ required: false
18
+ ---
19
+ <h1>{{ title }}</h1>
20
+ <div id="content">{{ body }}</div>
21
+ {% if author %}
22
+ <p class="byline">Written by {{ author }}.</p>
23
+ {% endif %}
@@ -0,0 +1,29 @@
1
+ require 'spec_helper'
2
+
3
+ describe Noumenon::Configuration do
4
+ it { should_not be_nil }
5
+
6
+ it { should respond_to(:content_repository_path) }
7
+ it { should respond_to(:content_repository_path=) }
8
+
9
+ it { should respond_to(:dependencies_path) }
10
+ it { should respond_to(:dependencies_path=) }
11
+
12
+ it { should respond_to(:theme) }
13
+ it { should respond_to(:theme=) }
14
+
15
+ describe "setting the theme" do
16
+ before(:each) do
17
+ subject.dependencies_path = fixture_path("static_example_dependencies")
18
+ end
19
+
20
+ it "succeeds if the theme exists" do
21
+ subject.theme = "example"
22
+ subject.theme.should == "example"
23
+ end
24
+
25
+ it "raises an exception if the theme could not be found" do
26
+ lambda { subject.theme = "none_existant" }.should raise_error Noumenon::Configuration::ThemeNotFoundError
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,105 @@
1
+ require 'spec_helper'
2
+
3
+ describe Noumenon::Core do
4
+ def app
5
+ Noumenon::Core.new
6
+ end
7
+
8
+ describe "accessing configuration" do
9
+ it { should respond_to(:config) }
10
+ it "returns the global configuration when calling config" do
11
+ app.config.should == Noumenon.config
12
+ end
13
+ end
14
+
15
+ describe "retrieving paths" do
16
+ it { should respond_to(:content_path) }
17
+
18
+ it "locate a path in the content repository" do
19
+ app.content_path("example").should == "#{app.config.content_repository_path}/example"
20
+ end
21
+
22
+ it { should respond_to(:theme_path) }
23
+
24
+ it "can locate a path in the theme directory" do
25
+ app.theme_path("file").should == "#{app.config.dependencies_path}/themes/example/file"
26
+ end
27
+ end
28
+
29
+ describe "rendering a template" do
30
+ it { should respond_to(:render_template) }
31
+
32
+ context "when the theme has no layout" do
33
+ before(:each) { app.config.theme = "example_without_layout" }
34
+
35
+ it "renders the requested template if it does exist" do
36
+ app.render_template( app.content_path("found.html") ).should == fixture("static_example/found.html")
37
+ end
38
+
39
+ it "uses Liquid to replace any tags in the template" do
40
+ app.render_template( app.content_path("template_with_substitutions.html"), 'name' => 'Jon' ).should == "Hello, Jon\n"
41
+ end
42
+ end
43
+
44
+ context "when the theme has a layout" do
45
+ before(:each) { app.config.theme = "example" }
46
+
47
+ it "renders the template within the theme's layout by default" do
48
+ app.render_template( app.content_path("found.html") ).should == %Q{<div id="layout">\n#{fixture("static_example/found.html")}\n</div>\n}
49
+ end
50
+
51
+ it "ignores the layout if the option :layout => false is passed" do
52
+ app.render_template( app.content_path("found.html"), {}, :layout => false ).should == fixture("static_example/found.html")
53
+ end
54
+ end
55
+ end
56
+
57
+ describe "serving content from the content repository" do
58
+ before(:each) { app.config.theme = "example_without_layout" }
59
+
60
+ context "when the requested item does not exist" do
61
+ context "and an index file exists in a directory of the same name" do
62
+ it "renders the index file instead" do
63
+ get "/directory_with_index"
64
+
65
+ last_response.should be_ok
66
+ last_response.body.should == File.read(app.theme_path("basic_template.html"))
67
+ end
68
+ end
69
+
70
+ context "and no index file exists in a directory of the same name" do
71
+ it "responds with a status of 404" do
72
+ get "/empty_directory"
73
+
74
+ last_response.should_not be_ok
75
+ last_response.status.should == 404
76
+ end
77
+ end
78
+
79
+ context "and no directory of the same name exists" do
80
+ it "responds with a status of 404" do
81
+ get "/not_found"
82
+
83
+ last_response.should_not be_ok
84
+ last_response.status.should == 404
85
+ end
86
+ end
87
+ end
88
+
89
+ context "when the requested item exists" do
90
+ it "renders the specified template from the theme" do
91
+ get "/templates/basic_example"
92
+
93
+ last_response.should be_ok
94
+ last_response.body.should == File.read(app.theme_path("basic_template.html"))
95
+ end
96
+
97
+ it "makes substitutions as specified in the template" do
98
+ get "/templates/with_fields"
99
+
100
+ last_response.should be_ok
101
+ last_response.body.should =~ /Field: value/
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,41 @@
1
+ require 'noumenon/spec/fixtures'
2
+
3
+ class FixturedSpec
4
+ include Noumenon::Spec::Fixtures
5
+ end
6
+
7
+ describe "a class with Noumenon::Spec::Fixtures mixed in" do
8
+ subject { FixturedSpec.new }
9
+
10
+ describe "the fixture path" do
11
+ it { should respond_to(:fixture_path) }
12
+ it { should respond_to(:fixture_path=) }
13
+
14
+ it "defaults the fixture path to 'spec/fixtures'" do
15
+ subject.fixture_path.should == 'spec/fixtures'
16
+ end
17
+
18
+ it "appends the provided path to spec/fixtures if relative" do
19
+ subject.fixture_path = "test"
20
+ subject.fixture_path.should == "spec/fixtures/test"
21
+ end
22
+
23
+ it "sets the path provided if absolute" do
24
+ subject.fixture_path = "/tmp"
25
+ subject.fixture_path.should == "/tmp"
26
+ end
27
+
28
+ it "returns the full path to the fixture if fixture_path is passed an argument" do
29
+ subject.fixture_path = "/tmp"
30
+ subject.fixture_path("example").should == "/tmp/example"
31
+ end
32
+ end
33
+
34
+ describe "loading a fixture" do
35
+ it { should respond_to(:fixture) }
36
+
37
+ it "returns the contents of the specified file" do
38
+ subject.fixture("fixture_specs/test").should eq "Hello, fixtures.\n"
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,7 @@
1
+ require 'noumenon/spec'
2
+
3
+ describe Noumenon::Spec do
4
+ it { should_not be_nil }
5
+
6
+ it { should include Noumenon::Spec::Fixtures }
7
+ end
@@ -0,0 +1,118 @@
1
+ require 'spec_helper'
2
+
3
+ describe Noumenon::Template do
4
+ it { should_not be_nil }
5
+
6
+ def template_path(name)
7
+ Noumenon::Core.new.theme_path(name)
8
+ end
9
+
10
+ before(:each) do
11
+ Noumenon.config.theme = "example_without_layout"
12
+ end
13
+
14
+ it { should respond_to(:fields) }
15
+ it { should respond_to(:source) }
16
+ it { should respond_to(:content) }
17
+
18
+ describe "loading a template from disk" do
19
+ it "raises a TemplateNotFoundError if the template does not exist" do
20
+ lambda { Noumenon::Template.from_file "dummy" }.should raise_error Noumenon::Template::NotFoundError
21
+ end
22
+
23
+ context "when the template has no fields specified" do
24
+ subject do
25
+ Noumenon::Template.from_file template_path("basic_template.html")
26
+ end
27
+
28
+ it "leaves #fields empty" do
29
+ subject.fields.should be_empty
30
+ end
31
+
32
+ it "populates #source with the path to the template" do
33
+ subject.source.should == template_path("basic_template.html")
34
+ end
35
+
36
+ it "populates #content with the template body" do
37
+ subject.content.should == File.read(template_path("basic_template.html"))
38
+ end
39
+ end
40
+
41
+ context "when the template has some fields specified" do
42
+ subject do
43
+ Noumenon::Template.from_file template_path("template_with_fields.html")
44
+ end
45
+
46
+ it "populates the field data for the title" do
47
+ subject.fields["title"].should == {
48
+ "type" => "string",
49
+ "label" => "Page title",
50
+ "help" => "Appears at the top of the page.",
51
+ "required" => true
52
+ }
53
+ end
54
+
55
+ it "populates the field data for the body" do
56
+ subject.fields["body"].should == {
57
+ "type" => "text",
58
+ "label" => "Body text",
59
+ "help" => "The main body of the page",
60
+ "required" => true
61
+ }
62
+ end
63
+
64
+ it "populates the field data for the author" do
65
+ subject.fields["author"].should == {
66
+ "type" => "string",
67
+ "label" => "Author name",
68
+ "help" => "If not provided no author will be shown",
69
+ "required" => false
70
+ }
71
+ end
72
+
73
+ it "populates #content with the template body" do
74
+ subject.content.should eq File.read(template_path("template_with_fields.html")).split("\n---\n").last
75
+ end
76
+ end
77
+ end
78
+
79
+ describe "rendering a template" do
80
+ let(:template) { Noumenon::Template.from_file template_path("template_with_fields.html") }
81
+
82
+ it { should respond_to(:render) }
83
+
84
+ context "when missing required fields" do
85
+ it "raises a Noumenon::Template::MissingContentError" do
86
+ lambda { template.render }.should raise_error Noumenon::Template::MissingContentError
87
+ end
88
+
89
+ it "lists the missing fields in the exception" do
90
+ begin
91
+ template.render
92
+ rescue Noumenon::Template::MissingContentError => e
93
+ e.to_s.should eq "The following fields were missing from your content: body, title"
94
+ end
95
+ end
96
+ end
97
+
98
+ context "when all required fields were provided" do
99
+ it "renders the template, replacing any fields" do
100
+ content = template.render("title" => "Example Page", "body" => "This is an example, isn't it lovely.", "author" => "Jon Wood")
101
+
102
+ content.should =~ %r{<h1>Example Page</h1>}
103
+ content.should =~ %r{<div id="content">This is an example, isn't it lovely.</div>}
104
+ content.should =~ %r{<p class="byline">Written by Jon Wood.</p>}
105
+ end
106
+ end
107
+
108
+ context "when provided with fields that are not specified for the template" do
109
+ it "renders the template anyway" do
110
+ content = template.render("title" => "Example Page", "body" => "This is an example, isn't it lovely.", "author" => "Jon Wood", "unknown" => "is here")
111
+
112
+ content.should =~ %r{<h1>Example Page</h1>}
113
+ content.should =~ %r{<div id="content">This is an example, isn't it lovely.</div>}
114
+ content.should =~ %r{<p class="byline">Written by Jon Wood.</p>}
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,61 @@
1
+ require 'spec_helper'
2
+
3
+ describe Noumenon do
4
+ before(:each) do
5
+ Noumenon.config.dependencies_path = fixture_path("static_example_dependencies")
6
+ end
7
+
8
+ specify { Noumenon.should respond_to(:boot) }
9
+
10
+ def app
11
+ Noumenon.boot
12
+ end
13
+
14
+ describe "serving theme assets" do
15
+ it "serves the asset specified at /themes/theme_name/example.txt if the file exists" do
16
+ get '/themes/example/example.txt'
17
+
18
+ last_response.status.should == 200
19
+ last_response.body.should == fixture("static_example_dependencies/themes/example/assets/example.txt")
20
+ end
21
+
22
+ it "returns a status of 404 if the file does not exist" do
23
+ get '/themes/example/not_found.png'
24
+
25
+ last_response.status.should == 404
26
+ end
27
+ end
28
+
29
+ describe "setting configuration" do
30
+ before(:each) do
31
+ Noumenon.config = nil
32
+ end
33
+
34
+ specify { Noumenon.should respond_to(:configure) }
35
+ specify { Noumenon.should respond_to(:config) }
36
+ specify { Noumenon.should respond_to(:config=) }
37
+
38
+ it "sets config to the provided object on config=" do
39
+ config = Noumenon::Configuration.new
40
+ Noumenon.config = config
41
+ Noumenon.config.should == config
42
+ end
43
+
44
+ it "returns a new config instance if no config was previously set" do
45
+ Noumenon.config.should be_instance_of Noumenon::Configuration
46
+ end
47
+
48
+ it "yields the config instance if a block is provided" do
49
+ yielded = false
50
+
51
+ config = Noumenon::Configuration.new
52
+ Noumenon.config = config
53
+ Noumenon.configure do |c|
54
+ c.should == config
55
+ yielded = true
56
+ end
57
+
58
+ yielded.should be_true
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,19 @@
1
+ require 'noumenon'
2
+ require 'noumenon/spec'
3
+ require 'rack/test'
4
+
5
+ RSpec.configure do |spec|
6
+ spec.include Rack::Test::Methods
7
+ spec.include Noumenon::Spec
8
+ include Noumenon::Spec::Fixtures
9
+
10
+ Noumenon::Core.set :environment, :test
11
+
12
+ spec.before(:each) do
13
+ Noumenon.configure do |c|
14
+ c.content_repository_path = fixture_path("static_example")
15
+ c.dependencies_path = fixture_path("static_example_dependencies")
16
+ c.theme = "example"
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,2 @@
1
+ watch( 'spec/.*\.rb' ) { |md| system("rake specs") }
2
+ watch( 'lib/.*\.rb') { |md| system("rake specs") }
metadata ADDED
@@ -0,0 +1,236 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: noumenon
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.0.1
6
+ platform: ruby
7
+ authors:
8
+ - Jon Wood
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-04-19 00:00:00 +01:00
14
+ default_executable:
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: sinatra
18
+ prerelease: false
19
+ requirement: &id001 !ruby/object:Gem::Requirement
20
+ none: false
21
+ requirements:
22
+ - - ">="
23
+ - !ruby/object:Gem::Version
24
+ version: 1.2.3
25
+ type: :runtime
26
+ version_requirements: *id001
27
+ - !ruby/object:Gem::Dependency
28
+ name: liquid
29
+ prerelease: false
30
+ requirement: &id002 !ruby/object:Gem::Requirement
31
+ none: false
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: "2.2"
36
+ type: :runtime
37
+ version_requirements: *id002
38
+ - !ruby/object:Gem::Dependency
39
+ name: rspec
40
+ prerelease: false
41
+ requirement: &id003 !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 2.5.0
47
+ type: :development
48
+ version_requirements: *id003
49
+ - !ruby/object:Gem::Dependency
50
+ name: rack-test
51
+ prerelease: false
52
+ requirement: &id004 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: "0.5"
58
+ type: :development
59
+ version_requirements: *id004
60
+ description: |
61
+ # Noumenon
62
+
63
+ Noumenon: Noun. The intellectual conception of a thing as it is in itself, not as it is known through perception.
64
+
65
+ ## What is This Thing?
66
+
67
+ Noumenon is a web application based on Sinatra for constructing dynamic websites which use a Git repository
68
+ for storing all content. It's designed to allow technical and non-technical teams to collaborate on development and
69
+ populate of a content management system using the tools they are most comfortable with.
70
+
71
+ In the case of developers (and some designers) that's version control and text editors, while in the case of content
72
+ editors thats more likely to be a web interface.
73
+
74
+ ## How it Works
75
+
76
+ The URL structure and content of a Noumenon site is defined by a Git repository, similar to the structure below:
77
+
78
+ /
79
+ - /config.rb
80
+ - /index.html
81
+ - /about
82
+ - /people.html
83
+ - /company.html
84
+ - /contact
85
+ - /config.rb
86
+ - /blog
87
+ - /config.rb
88
+ - /posts/2011-04-18-an-example-post.md
89
+ - /posts/2011-04-10-another-example.md
90
+
91
+ This git repository is then provided to Noumenon as it's data source. On startup it loads any file called "config.rb"
92
+ and uses it to determine how that directory should behave:
93
+
94
+ # /config.rb
95
+ domain "example.org"
96
+ application "git://github.com/noumenon/apps-static"
97
+ theme "git://github.com/noumenon/themes-example"
98
+
99
+ That example configures Noumenon to use the "Noumenon::Static" application to serve any templates below that point as
100
+ a static page.
101
+
102
+ # /contact/config.rb
103
+ application "git://github.com/noumenon/apps-contact"
104
+ contact.email_address "info@example.org"
105
+
106
+ While the one in /contact specifies that "Noumenon::Contact" should be used to provide the URL tree below /contact, in this
107
+ case a contact form which emails any responses to the specified address.
108
+
109
+ Finally /blogs/config.rb might look something like this:
110
+
111
+ application "git://github.com/noumenon/apps-blog"
112
+ blog.comments true
113
+
114
+ ## Hosting a Noumenon Site
115
+
116
+ *This won't work yet: I havn't implemented automatic check out of a content repository.*
117
+
118
+ To host a site you will need to have Ruby and an application server such as Passenger, Unicorn or Thin. Due it's interactions
119
+ with git as a data store Heroku is not a supported platform for hosting, although I'm sure someone will find a way around that.
120
+
121
+ Install Noumenon: `gem install noumenon`
122
+
123
+ Create a directory, and put the following config.ru in it:
124
+
125
+ require 'noumenon'
126
+ Noumenon::Core.set :content_repository, "git@github.com:Noumenon/example.git"
127
+
128
+ run Noumenon.boot
129
+
130
+ And then set up your application server to provide that application. On startup it will first attempt to check out the content
131
+ repository, then it will parse any configuration files, and install any required dependencies. Finally, it will start hosting
132
+ the site.
133
+
134
+ To update the content repository restart your application server, which will cause it to update the repository, and any dependencies.
135
+
136
+ ### Hosting on Heroku
137
+
138
+ The someone who worked out the way around it was me it seems. This will only work if you're deploying an entirely static site,
139
+ if you need the admin section to work then you'll have to host somewhere else, but otherwise, use a config.ru like this, and make
140
+ sure you have Noumenon in your Gemfile:
141
+
142
+ require 'noumenon'
143
+ Noumenon::Core.set :content_repository_path, File.expand_path("..", __FILE__)
144
+
145
+ run Noumenon.boot
146
+
147
+ email:
148
+ - jon@blankpad.net
149
+ executables: []
150
+
151
+ extensions: []
152
+
153
+ extra_rdoc_files: []
154
+
155
+ files:
156
+ - .gitignore
157
+ - .travis.yml
158
+ - Gemfile
159
+ - README.md
160
+ - Rakefile
161
+ - lib/noumenon.rb
162
+ - lib/noumenon/configuration.rb
163
+ - lib/noumenon/core.rb
164
+ - lib/noumenon/spec.rb
165
+ - lib/noumenon/spec/fixtures.rb
166
+ - lib/noumenon/template.rb
167
+ - lib/noumenon/version.rb
168
+ - noumenon.gemspec
169
+ - spec/fixtures/fixture_specs/test
170
+ - spec/fixtures/static_example/directory_with_index/index.yml
171
+ - spec/fixtures/static_example/found.html
172
+ - spec/fixtures/static_example/liquid_example.html
173
+ - spec/fixtures/static_example/template_with_substitutions.html
174
+ - spec/fixtures/static_example/templates/basic_example.yml
175
+ - spec/fixtures/static_example/templates/with_fields.yml
176
+ - spec/fixtures/static_example_dependencies/themes/example/assets/example.txt
177
+ - spec/fixtures/static_example_dependencies/themes/example/layout.html
178
+ - spec/fixtures/static_example_dependencies/themes/example_without_layout/basic_template.html
179
+ - spec/fixtures/static_example_dependencies/themes/example_without_layout/fields.html
180
+ - spec/fixtures/static_example_dependencies/themes/example_without_layout/template_with_fields.html
181
+ - spec/noumenon/config_spec.rb
182
+ - spec/noumenon/core_spec.rb
183
+ - spec/noumenon/spec/fixtures_spec.rb
184
+ - spec/noumenon/spec_spec.rb
185
+ - spec/noumenon/template_spec.rb
186
+ - spec/noumenon_spec.rb
187
+ - spec/spec_helper.rb
188
+ - watchr.rb
189
+ has_rdoc: true
190
+ homepage: https://github.com/Noumenon
191
+ licenses: []
192
+
193
+ post_install_message:
194
+ rdoc_options: []
195
+
196
+ require_paths:
197
+ - lib
198
+ required_ruby_version: !ruby/object:Gem::Requirement
199
+ none: false
200
+ requirements:
201
+ - - ">="
202
+ - !ruby/object:Gem::Version
203
+ version: "0"
204
+ required_rubygems_version: !ruby/object:Gem::Requirement
205
+ none: false
206
+ requirements:
207
+ - - ">="
208
+ - !ruby/object:Gem::Version
209
+ version: "0"
210
+ requirements: []
211
+
212
+ rubyforge_project: noumenon
213
+ rubygems_version: 1.6.2
214
+ signing_key:
215
+ specification_version: 3
216
+ summary: An content management system backed by Git
217
+ test_files:
218
+ - spec/fixtures/fixture_specs/test
219
+ - spec/fixtures/static_example/directory_with_index/index.yml
220
+ - spec/fixtures/static_example/found.html
221
+ - spec/fixtures/static_example/liquid_example.html
222
+ - spec/fixtures/static_example/template_with_substitutions.html
223
+ - spec/fixtures/static_example/templates/basic_example.yml
224
+ - spec/fixtures/static_example/templates/with_fields.yml
225
+ - spec/fixtures/static_example_dependencies/themes/example/assets/example.txt
226
+ - spec/fixtures/static_example_dependencies/themes/example/layout.html
227
+ - spec/fixtures/static_example_dependencies/themes/example_without_layout/basic_template.html
228
+ - spec/fixtures/static_example_dependencies/themes/example_without_layout/fields.html
229
+ - spec/fixtures/static_example_dependencies/themes/example_without_layout/template_with_fields.html
230
+ - spec/noumenon/config_spec.rb
231
+ - spec/noumenon/core_spec.rb
232
+ - spec/noumenon/spec/fixtures_spec.rb
233
+ - spec/noumenon/spec_spec.rb
234
+ - spec/noumenon/template_spec.rb
235
+ - spec/noumenon_spec.rb
236
+ - spec/spec_helper.rb