noumenon 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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