ninetails 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +17 -0
  4. data/Rakefile +19 -0
  5. data/app/components/ninetails/element.rb +55 -0
  6. data/app/components/ninetails/element_definition.rb +67 -0
  7. data/app/components/ninetails/property.rb +11 -0
  8. data/app/components/ninetails/property_store.rb +22 -0
  9. data/app/components/ninetails/property_type.rb +27 -0
  10. data/app/components/ninetails/section_template.rb +66 -0
  11. data/app/controllers/ninetails/application_controller.rb +4 -0
  12. data/app/controllers/ninetails/page_revisions_controller.rb +31 -0
  13. data/app/controllers/ninetails/pages_controller.rb +14 -0
  14. data/app/controllers/ninetails/section_templates_controller.rb +17 -0
  15. data/app/models/ninetails/page.rb +37 -0
  16. data/app/models/ninetails/page_revision.rb +28 -0
  17. data/app/models/ninetails/page_revision_section.rb +6 -0
  18. data/app/models/ninetails/section.rb +30 -0
  19. data/config/initializers/error_classes.rb +4 -0
  20. data/config/initializers/json_stuff.rb +9 -0
  21. data/config/routes.rb +9 -0
  22. data/db/migrate/20151006093040_create_pages.rb +10 -0
  23. data/db/migrate/20151006093559_create_page_revisions.rb +8 -0
  24. data/db/migrate/20151006093754_create_sections.rb +11 -0
  25. data/db/migrate/20151006094337_create_page_revision_sections.rb +8 -0
  26. data/db/migrate/20151111085201_add_message_to_page_revisions.rb +5 -0
  27. data/db/schema.rb +53 -0
  28. data/lib/ninetails.rb +4 -0
  29. data/lib/ninetails/engine.rb +37 -0
  30. data/lib/ninetails/version.rb +3 -0
  31. data/lib/tasks/ninetails_tasks.rake +8 -0
  32. data/spec/components/element_definition_spec.rb +104 -0
  33. data/spec/components/element_spec.rb +92 -0
  34. data/spec/components/property_spec.rb +34 -0
  35. data/spec/components/property_store_spec.rb +64 -0
  36. data/spec/components/property_type_spec.rb +69 -0
  37. data/spec/components/section_spec.rb +72 -0
  38. data/spec/dummy/Rakefile +6 -0
  39. data/spec/dummy/app/components/element/button.rb +7 -0
  40. data/spec/dummy/app/components/element/button_icon.rb +8 -0
  41. data/spec/dummy/app/components/element/figure.rb +7 -0
  42. data/spec/dummy/app/components/element/meta.rb +7 -0
  43. data/spec/dummy/app/components/element/text.rb +7 -0
  44. data/spec/dummy/app/components/property/icon.rb +8 -0
  45. data/spec/dummy/app/components/property/image.rb +10 -0
  46. data/spec/dummy/app/components/property/link.rb +11 -0
  47. data/spec/dummy/app/components/property/text.rb +8 -0
  48. data/spec/dummy/app/components/section_template/billboard.rb +14 -0
  49. data/spec/dummy/app/components/section_template/document_head.rb +10 -0
  50. data/spec/dummy/bin/bundle +3 -0
  51. data/spec/dummy/bin/rails +4 -0
  52. data/spec/dummy/bin/rake +4 -0
  53. data/spec/dummy/bin/setup +29 -0
  54. data/spec/dummy/config.ru +4 -0
  55. data/spec/dummy/config/application.rb +35 -0
  56. data/spec/dummy/config/boot.rb +5 -0
  57. data/spec/dummy/config/database.yml +12 -0
  58. data/spec/dummy/config/database.yml.travis +4 -0
  59. data/spec/dummy/config/environment.rb +5 -0
  60. data/spec/dummy/config/environments/development.rb +41 -0
  61. data/spec/dummy/config/environments/production.rb +79 -0
  62. data/spec/dummy/config/environments/test.rb +42 -0
  63. data/spec/dummy/config/initializers/assets.rb +11 -0
  64. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  65. data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
  66. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  67. data/spec/dummy/config/initializers/inflections.rb +16 -0
  68. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  69. data/spec/dummy/config/initializers/session_store.rb +3 -0
  70. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  71. data/spec/dummy/config/locales/en.yml +23 -0
  72. data/spec/dummy/config/routes.rb +3 -0
  73. data/spec/dummy/config/secrets.yml +22 -0
  74. data/spec/dummy/db/schema.rb +53 -0
  75. data/spec/dummy/public/404.html +67 -0
  76. data/spec/dummy/public/422.html +67 -0
  77. data/spec/dummy/public/500.html +66 -0
  78. data/spec/dummy/public/favicon.ico +0 -0
  79. data/spec/factories/page_revisions.rb +8 -0
  80. data/spec/factories/pages.rb +25 -0
  81. data/spec/factories/sections.rb +15 -0
  82. data/spec/models/page_revision_spec.rb +5 -0
  83. data/spec/models/page_spec.rb +26 -0
  84. data/spec/models/section_spec.rb +113 -0
  85. data/spec/rails_helper.rb +60 -0
  86. data/spec/requests/page_revisions_spec.rb +117 -0
  87. data/spec/requests/pages_spec.rb +68 -0
  88. data/spec/requests/section_templates_spec.rb +17 -0
  89. data/spec/spec_helper.rb +98 -0
  90. data/spec/support/factory_girl.rb +5 -0
  91. data/spec/support/request_helpers.rb +9 -0
  92. data/spec/support/shared_examples.rb +34 -0
  93. metadata +378 -0
@@ -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
@@ -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.
@@ -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
@@ -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,11 @@
1
+ module Ninetails
2
+ class Property
3
+ include Ninetails::PropertyStore
4
+
5
+ def self.serialize
6
+ properties.each_with_object({}) do |property, hash|
7
+ hash[property.name] = property.serialize
8
+ end
9
+ end
10
+ end
11
+ 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,4 @@
1
+ module Ninetails
2
+ class ApplicationController < ActionController::API
3
+ end
4
+ 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