ninetails 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +17 -0
- data/Rakefile +19 -0
- data/app/components/ninetails/element.rb +55 -0
- data/app/components/ninetails/element_definition.rb +67 -0
- data/app/components/ninetails/property.rb +11 -0
- data/app/components/ninetails/property_store.rb +22 -0
- data/app/components/ninetails/property_type.rb +27 -0
- data/app/components/ninetails/section_template.rb +66 -0
- data/app/controllers/ninetails/application_controller.rb +4 -0
- data/app/controllers/ninetails/page_revisions_controller.rb +31 -0
- data/app/controllers/ninetails/pages_controller.rb +14 -0
- data/app/controllers/ninetails/section_templates_controller.rb +17 -0
- data/app/models/ninetails/page.rb +37 -0
- data/app/models/ninetails/page_revision.rb +28 -0
- data/app/models/ninetails/page_revision_section.rb +6 -0
- data/app/models/ninetails/section.rb +30 -0
- data/config/initializers/error_classes.rb +4 -0
- data/config/initializers/json_stuff.rb +9 -0
- data/config/routes.rb +9 -0
- data/db/migrate/20151006093040_create_pages.rb +10 -0
- data/db/migrate/20151006093559_create_page_revisions.rb +8 -0
- data/db/migrate/20151006093754_create_sections.rb +11 -0
- data/db/migrate/20151006094337_create_page_revision_sections.rb +8 -0
- data/db/migrate/20151111085201_add_message_to_page_revisions.rb +5 -0
- data/db/schema.rb +53 -0
- data/lib/ninetails.rb +4 -0
- data/lib/ninetails/engine.rb +37 -0
- data/lib/ninetails/version.rb +3 -0
- data/lib/tasks/ninetails_tasks.rake +8 -0
- data/spec/components/element_definition_spec.rb +104 -0
- data/spec/components/element_spec.rb +92 -0
- data/spec/components/property_spec.rb +34 -0
- data/spec/components/property_store_spec.rb +64 -0
- data/spec/components/property_type_spec.rb +69 -0
- data/spec/components/section_spec.rb +72 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/components/element/button.rb +7 -0
- data/spec/dummy/app/components/element/button_icon.rb +8 -0
- data/spec/dummy/app/components/element/figure.rb +7 -0
- data/spec/dummy/app/components/element/meta.rb +7 -0
- data/spec/dummy/app/components/element/text.rb +7 -0
- data/spec/dummy/app/components/property/icon.rb +8 -0
- data/spec/dummy/app/components/property/image.rb +10 -0
- data/spec/dummy/app/components/property/link.rb +11 -0
- data/spec/dummy/app/components/property/text.rb +8 -0
- data/spec/dummy/app/components/section_template/billboard.rb +14 -0
- data/spec/dummy/app/components/section_template/document_head.rb +10 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/bin/setup +29 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +35 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/database.yml +12 -0
- data/spec/dummy/config/database.yml.travis +4 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +41 -0
- data/spec/dummy/config/environments/production.rb +79 -0
- data/spec/dummy/config/environments/test.rb +42 -0
- data/spec/dummy/config/initializers/assets.rb +11 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +4 -0
- data/spec/dummy/config/initializers/session_store.rb +3 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +23 -0
- data/spec/dummy/config/routes.rb +3 -0
- data/spec/dummy/config/secrets.yml +22 -0
- data/spec/dummy/db/schema.rb +53 -0
- data/spec/dummy/public/404.html +67 -0
- data/spec/dummy/public/422.html +67 -0
- data/spec/dummy/public/500.html +66 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/factories/page_revisions.rb +8 -0
- data/spec/factories/pages.rb +25 -0
- data/spec/factories/sections.rb +15 -0
- data/spec/models/page_revision_spec.rb +5 -0
- data/spec/models/page_spec.rb +26 -0
- data/spec/models/section_spec.rb +113 -0
- data/spec/rails_helper.rb +60 -0
- data/spec/requests/page_revisions_spec.rb +117 -0
- data/spec/requests/pages_spec.rb +68 -0
- data/spec/requests/section_templates_spec.rb +17 -0
- data/spec/spec_helper.rb +98 -0
- data/spec/support/factory_girl.rb +5 -0
- data/spec/support/request_helpers.rb +9 -0
- data/spec/support/shared_examples.rb +34 -0
- metadata +378 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 5c7c8740a522a490770f25cf5bac37825ec24a1e
|
4
|
+
data.tar.gz: e4cd25c9dbcd7ae2799745e180a7d3d95b47cc98
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 52af399db0d77ca5b95646da1de1011ad319ab8f23a9957395a24e873009069312c644ad439ba0c99664443cb91c357d67fb563ca73fd0fe4ae3880924df2cfc
|
7
|
+
data.tar.gz: 661dc75c75578353f715d370668c169a46803000328c3f492c8c5aa672166301215e8aa3fa5174169699b75933a06811b79abf6cbc9efb22f47456d5ec82731e
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2015 iZettle AB
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
![Ninetails](http://i.imgur.com/jv28Kg3.png)
|
2
|
+
|
3
|
+
[![Build Status](https://travis-ci.org/iZettle/ninetails.svg)](https://travis-ci.org/iZettle/ninetails)
|
4
|
+
|
5
|
+
Ninetails is a simple CMS API engine. It is designed on the idea of a CMS which is limited in scope in that not all aspects of a page should be editable. Instead, you can define Sections, Elements, and Properties which are editable and can then be loaded in and styled in a reliable manner. It gives you the control to decide exactly what should and what should not be editable in your project.
|
6
|
+
|
7
|
+
## Getting started
|
8
|
+
|
9
|
+
Ninetails is a mountable Rails engine, so it needs to be loaded into your app. You app is also where you will define your components which Ninetails should expose. Start by adding it to your Gemfile:
|
10
|
+
|
11
|
+
```
|
12
|
+
gem "ninetails"
|
13
|
+
```
|
14
|
+
|
15
|
+
## Thinking in sections
|
16
|
+
|
17
|
+
## Setting up Sections, Elements, and Components
|
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
|
8
|
+
load 'rails/tasks/engine.rake'
|
9
|
+
|
10
|
+
Bundler::GemHelper.install_tasks
|
11
|
+
|
12
|
+
Dir[File.join(File.dirname(__FILE__), 'tasks/**/*.rake')].each {|f| load f }
|
13
|
+
|
14
|
+
require 'rspec/core'
|
15
|
+
require 'rspec/core/rake_task'
|
16
|
+
|
17
|
+
desc "Run all specs in spec directory (excluding plugin specs)"
|
18
|
+
RSpec::Core::RakeTask.new(:spec => 'app:db:test:prepare')
|
19
|
+
task :default => :spec
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Ninetails
|
2
|
+
class Element
|
3
|
+
include Ninetails::PropertyStore
|
4
|
+
|
5
|
+
def reference
|
6
|
+
@reference ||= SecureRandom.uuid
|
7
|
+
end
|
8
|
+
|
9
|
+
def deserialize(input)
|
10
|
+
properties_instances.collect do |property|
|
11
|
+
property.serialized_values = input[property.name.to_s]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def properties_structure
|
16
|
+
generate_structure.convert_keys(:to_api).with_indifferent_access
|
17
|
+
end
|
18
|
+
|
19
|
+
# Validate each property_type's property.
|
20
|
+
#
|
21
|
+
# If the property_type doesn't respond to valid?, then it means it must be a
|
22
|
+
# native type, or at least not something which inherits Ninetails::Property, so we will class it
|
23
|
+
# as valid blindly.
|
24
|
+
def valid?
|
25
|
+
validations = properties_instances.collect do |property_type|
|
26
|
+
if property_type.property.respond_to?(:valid?)
|
27
|
+
property_type.property.valid?
|
28
|
+
else
|
29
|
+
true
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
validations.all?
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def properties_instances
|
39
|
+
@properties_instances ||= self.class.properties.collect(&:clone)
|
40
|
+
end
|
41
|
+
|
42
|
+
def generate_structure
|
43
|
+
properties_instances.each_with_object({}) do |property_type, hash|
|
44
|
+
hash[:type] = self.class.name.demodulize
|
45
|
+
hash[:reference] = reference
|
46
|
+
hash[property_type.name] = property_type.serialize
|
47
|
+
|
48
|
+
if property_type.property.try(:errors).try(:present?)
|
49
|
+
hash[property_type.name].merge! errors: property_type.property.errors.messages
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Ninetails
|
2
|
+
class ElementDefinition
|
3
|
+
attr_accessor :name, :type, :count, :elements
|
4
|
+
|
5
|
+
delegate :properties, to: :type
|
6
|
+
|
7
|
+
def initialize(name, type, count)
|
8
|
+
@name = name
|
9
|
+
@type = type
|
10
|
+
@count = count
|
11
|
+
@elements = []
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_to_hash(hash)
|
15
|
+
if count == :single
|
16
|
+
hash[name] = single_element_structure
|
17
|
+
else
|
18
|
+
hash[name] = multiple_element_structure
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def deserialize(input)
|
23
|
+
self.elements = []
|
24
|
+
|
25
|
+
if input.is_a? Array
|
26
|
+
input.each do |hash|
|
27
|
+
add_element hash
|
28
|
+
end
|
29
|
+
else
|
30
|
+
add_element input
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def properties_structure
|
35
|
+
@properties_structure ||= type.new.properties_structure
|
36
|
+
end
|
37
|
+
|
38
|
+
def all_elements_valid?
|
39
|
+
elements.all? &:valid?
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def single_element_structure
|
45
|
+
if elements.present?
|
46
|
+
elements.first.properties_structure
|
47
|
+
else
|
48
|
+
properties_structure
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def multiple_element_structure
|
53
|
+
if elements.present?
|
54
|
+
elements.collect &:properties_structure
|
55
|
+
else
|
56
|
+
[properties_structure]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def add_element(input)
|
61
|
+
element = type.new
|
62
|
+
element.deserialize input
|
63
|
+
elements << element
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Ninetails
|
2
|
+
module PropertyStore
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
include Virtus.model
|
7
|
+
include ActiveModel::Validations
|
8
|
+
end
|
9
|
+
|
10
|
+
class_methods do
|
11
|
+
def property(name, type)
|
12
|
+
attribute name, type
|
13
|
+
properties << Ninetails::PropertyType.new(name, type)
|
14
|
+
end
|
15
|
+
|
16
|
+
def properties
|
17
|
+
@properties ||= []
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Ninetails
|
2
|
+
class PropertyType
|
3
|
+
attr_accessor :name, :type, :serialized_values, :property
|
4
|
+
|
5
|
+
def initialize(name, type)
|
6
|
+
@name = name
|
7
|
+
@type = type
|
8
|
+
@property = type.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def serialized_values=(hash)
|
12
|
+
@serialized_values = hash
|
13
|
+
@property = type.new serialized_values
|
14
|
+
end
|
15
|
+
|
16
|
+
def serialize
|
17
|
+
if serialized_values.present?
|
18
|
+
serialized_values
|
19
|
+
elsif type.respond_to? :serialize
|
20
|
+
type.serialize
|
21
|
+
else
|
22
|
+
type.new
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Ninetails
|
2
|
+
class SectionTemplate
|
3
|
+
ALLOWED_POSITIONS = [:head, :body]
|
4
|
+
|
5
|
+
attr_accessor :elements_instances
|
6
|
+
|
7
|
+
def self.located_in(position)
|
8
|
+
unless ALLOWED_POSITIONS.include?(position)
|
9
|
+
fail SectionConfigurationError, "position must be in #{ALLOWED_POSITIONS}"
|
10
|
+
end
|
11
|
+
|
12
|
+
@position = position
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.position
|
16
|
+
@position
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.has_element(name, type)
|
20
|
+
define_element name, type, :single
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.has_many_elements(name, type)
|
24
|
+
define_element name, type, :multiple
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.elements
|
28
|
+
@elements
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.find_element(name)
|
32
|
+
elements.find { |element| element.name == name.to_sym }
|
33
|
+
end
|
34
|
+
|
35
|
+
def serialize
|
36
|
+
{
|
37
|
+
name: "",
|
38
|
+
type: self.class.name.demodulize,
|
39
|
+
tags: tags,
|
40
|
+
elements: serialize_elements
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
def tags
|
45
|
+
{
|
46
|
+
position: self.class.position
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
def serialize_elements
|
51
|
+
elements = elements_instances || self.class.elements
|
52
|
+
|
53
|
+
elements.each_with_object({}) do |element, hash|
|
54
|
+
element.add_to_hash hash
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def self.define_element(name, type, count)
|
61
|
+
@elements ||= []
|
62
|
+
@elements << Ninetails::ElementDefinition.new(name, type, count)
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Ninetails
|
2
|
+
class PageRevisionsController < ApplicationController
|
3
|
+
|
4
|
+
before_action :find_page
|
5
|
+
|
6
|
+
def index
|
7
|
+
render json: { revisions: @page.revisions }
|
8
|
+
end
|
9
|
+
|
10
|
+
def create
|
11
|
+
@page.build_revision_from_params revision_params
|
12
|
+
|
13
|
+
if @page.revision.save
|
14
|
+
render json: @page.to_builder.target!, status: :created
|
15
|
+
else
|
16
|
+
render json: @page.to_builder.target!, status: :bad_request
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def find_page
|
23
|
+
@page = Page.find params[:page_id]
|
24
|
+
end
|
25
|
+
|
26
|
+
def revision_params
|
27
|
+
params.require(:page_revision).permit!
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Ninetails
|
2
|
+
class PagesController < ApplicationController
|
3
|
+
|
4
|
+
def show
|
5
|
+
@page = Page.find_by! url: params[:id]
|
6
|
+
@page.revision = @page.revisions.find params[:revision_id] if params[:revision_id]
|
7
|
+
|
8
|
+
render json: @page.to_builder.target!
|
9
|
+
rescue ActiveRecord::RecordNotFound
|
10
|
+
render json: {}, status: :not_found
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Ninetails
|
2
|
+
class SectionTemplatesController < ApplicationController
|
3
|
+
|
4
|
+
def index
|
5
|
+
templates = Rails.root.join("app", "components", "section_template").children.collect do |entry|
|
6
|
+
entry.basename.to_s.sub(/(\..*)$/, '').classify
|
7
|
+
end
|
8
|
+
|
9
|
+
render json: { templates: templates }
|
10
|
+
end
|
11
|
+
|
12
|
+
def show
|
13
|
+
render json: "SectionTemplate::#{params[:id]}".safe_constantize.new.serialize
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Ninetails
|
2
|
+
class Page < ActiveRecord::Base
|
3
|
+
has_many :revisions, class_name: "PageRevision"
|
4
|
+
has_one :current_revision, class_name: "PageRevision"
|
5
|
+
|
6
|
+
attr_writer :revision
|
7
|
+
|
8
|
+
def revision
|
9
|
+
@revision || current_revision
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_builder
|
13
|
+
Jbuilder.new do |json|
|
14
|
+
json.page do
|
15
|
+
json.call(self, :id, :name, :url)
|
16
|
+
json.revision_id revision.id
|
17
|
+
json.sections sections_to_builder
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def sections_to_builder
|
23
|
+
revision.sections.collect do |section|
|
24
|
+
section.to_builder.attributes!
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def build_revision_from_params(page_revision)
|
29
|
+
self.revision = revisions.build message: page_revision[:message]
|
30
|
+
|
31
|
+
page_revision[:sections].each do |section_json|
|
32
|
+
revision.sections.build section_json.except(:id)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Ninetails
|
2
|
+
class PageRevision < ActiveRecord::Base
|
3
|
+
belongs_to :page
|
4
|
+
has_many :page_revision_sections
|
5
|
+
has_many :sections, through: :page_revision_sections
|
6
|
+
|
7
|
+
validate :sections_are_all_valid
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
# Validate all sections and rebuild the sections array with the instances which now
|
12
|
+
# contain error messages
|
13
|
+
def sections_are_all_valid
|
14
|
+
sections.each do |section|
|
15
|
+
deserialized_section = section.deserialize
|
16
|
+
|
17
|
+
deserialized_section.elements_instances.each do |element_definition|
|
18
|
+
unless element_definition.all_elements_valid?
|
19
|
+
errors.add :base, "Element #{element_definition.name} is not valid"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
section.elements = deserialized_section.serialize[:elements]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|