cadmus 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +8 -1
  3. data/Gemfile +2 -0
  4. data/README.md +6 -35
  5. data/Rakefile +33 -2
  6. data/app/helpers/cadmus/rendering_helper.rb +24 -0
  7. data/app/views/cadmus/layouts/_form.html.erb +11 -0
  8. data/app/views/cadmus/layouts/edit.html.erb +5 -0
  9. data/app/views/cadmus/layouts/index.html.erb +25 -0
  10. data/app/views/cadmus/layouts/new.html.erb +5 -0
  11. data/app/views/cadmus/pages/index.html.erb +2 -2
  12. data/app/views/cadmus/pages/show.html.erb +1 -1
  13. data/app/views/cadmus/partials/_form.html.erb +35 -0
  14. data/app/views/cadmus/partials/edit.html.erb +5 -0
  15. data/app/views/cadmus/partials/index.html.erb +25 -0
  16. data/app/views/cadmus/partials/new.html.erb +5 -0
  17. data/app/views/cadmus/partials/show.html.erb +4 -0
  18. data/bin/rake +16 -0
  19. data/cadmus.gemspec +2 -2
  20. data/lib/cadmus.rb +23 -6
  21. data/lib/cadmus/concerns/controller_with_parent.rb +70 -0
  22. data/lib/cadmus/concerns/liquid_template_field.rb +33 -0
  23. data/lib/cadmus/concerns/model_with_parent.rb +14 -0
  24. data/lib/cadmus/concerns/other_class_accessor.rb +50 -0
  25. data/lib/cadmus/engine.rb +9 -0
  26. data/lib/cadmus/layout.rb +32 -0
  27. data/lib/cadmus/layouts_controller.rb +70 -0
  28. data/lib/cadmus/page.rb +22 -7
  29. data/lib/cadmus/{controller_extensions.rb → pages_controller.rb} +8 -60
  30. data/lib/cadmus/partial.rb +32 -0
  31. data/lib/cadmus/partial_file_system.rb +28 -0
  32. data/lib/cadmus/partials_controller.rb +70 -0
  33. data/lib/cadmus/renderers.rb +1 -1
  34. data/lib/cadmus/version.rb +1 -1
  35. data/test/cadmus_test.rb +7 -0
  36. data/test/dummy/Rakefile +6 -0
  37. data/test/dummy/app/assets/config/manifest.js +4 -0
  38. data/test/dummy/app/assets/images/.keep +0 -0
  39. data/test/dummy/app/assets/javascripts/application.js +13 -0
  40. data/test/dummy/app/assets/javascripts/cable.js +13 -0
  41. data/test/dummy/app/assets/javascripts/channels/.keep +0 -0
  42. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  43. data/test/dummy/app/channels/application_cable/channel.rb +4 -0
  44. data/test/dummy/app/channels/application_cable/connection.rb +4 -0
  45. data/test/dummy/app/controllers/application_controller.rb +3 -0
  46. data/test/dummy/app/controllers/concerns/.keep +0 -0
  47. data/test/dummy/app/helpers/application_helper.rb +2 -0
  48. data/test/dummy/app/jobs/application_job.rb +2 -0
  49. data/test/dummy/app/mailers/application_mailer.rb +4 -0
  50. data/test/dummy/app/models/application_record.rb +3 -0
  51. data/test/dummy/app/models/concerns/.keep +0 -0
  52. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  53. data/test/dummy/app/views/layouts/mailer.html.erb +13 -0
  54. data/test/dummy/app/views/layouts/mailer.text.erb +1 -0
  55. data/test/dummy/bin/bundle +3 -0
  56. data/test/dummy/bin/rails +4 -0
  57. data/test/dummy/bin/rake +4 -0
  58. data/test/dummy/bin/setup +38 -0
  59. data/test/dummy/bin/update +29 -0
  60. data/test/dummy/bin/yarn +10 -0
  61. data/test/dummy/config.ru +5 -0
  62. data/test/dummy/config/application.rb +18 -0
  63. data/test/dummy/config/boot.rb +5 -0
  64. data/test/dummy/config/cable.yml +10 -0
  65. data/test/dummy/config/database.yml +25 -0
  66. data/test/dummy/config/environment.rb +5 -0
  67. data/test/dummy/config/environments/development.rb +54 -0
  68. data/test/dummy/config/environments/production.rb +91 -0
  69. data/test/dummy/config/environments/test.rb +42 -0
  70. data/test/dummy/config/initializers/application_controller_renderer.rb +6 -0
  71. data/test/dummy/config/initializers/assets.rb +14 -0
  72. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  73. data/test/dummy/config/initializers/cookies_serializer.rb +5 -0
  74. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  75. data/test/dummy/config/initializers/inflections.rb +16 -0
  76. data/test/dummy/config/initializers/mime_types.rb +4 -0
  77. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  78. data/test/dummy/config/locales/en.yml +33 -0
  79. data/test/dummy/config/puma.rb +56 -0
  80. data/test/dummy/config/routes.rb +3 -0
  81. data/test/dummy/config/secrets.yml +32 -0
  82. data/test/dummy/config/spring.rb +6 -0
  83. data/test/dummy/db/development.sqlite3 +0 -0
  84. data/test/dummy/db/test.sqlite3 +0 -0
  85. data/test/dummy/lib/assets/.keep +0 -0
  86. data/test/dummy/log/.keep +0 -0
  87. data/test/dummy/log/development.log +5 -0
  88. data/test/dummy/package.json +5 -0
  89. data/test/dummy/public/404.html +67 -0
  90. data/test/dummy/public/422.html +67 -0
  91. data/test/dummy/public/500.html +66 -0
  92. data/test/dummy/public/apple-touch-icon-precomposed.png +0 -0
  93. data/test/dummy/public/apple-touch-icon.png +0 -0
  94. data/test/dummy/public/favicon.ico +0 -0
  95. data/test/test_helper.rb +17 -0
  96. metadata +150 -9
  97. data/lib/cadmus/liquid_template_field.rb +0 -19
@@ -0,0 +1,33 @@
1
+ module Cadmus
2
+ module Concerns
3
+ module LiquidTemplateField
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+ def liquid_template_field(method_name, field_name)
8
+ define_method method_name do
9
+ content = send(field_name)
10
+
11
+ begin
12
+ Liquid::Template.parse(content)
13
+ rescue Exception => exception
14
+ Liquid::Template.parse("#{exception.class.name}: #{exception.message}")
15
+ end
16
+ end
17
+ end
18
+
19
+ def validates_template_validity(field_name)
20
+ validate do |model|
21
+ content = model.send(field_name)
22
+
23
+ begin
24
+ Liquid::Template.parse(content)
25
+ rescue Exception => exception
26
+ model.errors.add(field_name, "failed to parse: #{exception.class.name}: #{exception.message}")
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,14 @@
1
+ module Cadmus
2
+ module Concerns
3
+ module ModelWithParent
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+ def model_with_parent
8
+ belongs_to :parent, polymorphic: true, optional: true
9
+ scope :global, -> { where(:parent_id => nil, :parent_type => nil) }
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,50 @@
1
+ module Cadmus
2
+ module Concerns
3
+ module OtherClassAccessor
4
+ # Defines an accessor method for a field that stores a class. Simply storing the class can mess up Rails
5
+ # reloading, so behind the scenes, this stores the name of the class and then, when something tries to get the
6
+ # value of the field, safe_constantizes that name and memoizes the result.
7
+ #
8
+ # When using this in a gem, it's important to call the clear_<field name>_cache! method in the engine's
9
+ # to_prepare block, e.g.:
10
+ #
11
+ # config.to_prepare do
12
+ # Cadmus::PartialFileSystem.clear_partial_model_cache!
13
+ # end
14
+ def other_class_accessor(field_name)
15
+ name_ivar = "@_#{field_name}_name"
16
+ class_memo_ivar = "@_#{field_name}"
17
+
18
+ # getter method
19
+ define_singleton_method field_name do
20
+ memoized_class = instance_variable_get(class_memo_ivar)
21
+
22
+ unless memoized_class
23
+ name = instance_variable_get(name_ivar)
24
+ return unless name
25
+
26
+ memoized_class = name.safe_constantize
27
+ instance_variable_set(class_memo_ivar, memoized_class)
28
+ end
29
+
30
+ memoized_class
31
+ end
32
+
33
+ # setter method
34
+ define_singleton_method "#{field_name}=" do |klass|
35
+ class_name = case klass
36
+ when Class then klass.name
37
+ else klass
38
+ end
39
+
40
+ instance_variable_set(name_ivar, class_name)
41
+ end
42
+
43
+ # clear memoized class method
44
+ define_singleton_method "clear_#{field_name}_cache!" do
45
+ instance_variable_set(class_memo_ivar, nil)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,9 @@
1
+ module Cadmus
2
+ class Engine < Rails::Engine
3
+ config.to_prepare do
4
+ Cadmus.clear_layout_model_cache!
5
+ Cadmus.clear_page_model_cache!
6
+ Cadmus.clear_partial_model_cache!
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,32 @@
1
+ module Cadmus
2
+ module Layout
3
+ extend ActiveSupport::Concern
4
+ include Cadmus::Concerns::LiquidTemplateField
5
+ include Cadmus::Concerns::ModelWithParent
6
+
7
+ module ClassMethods
8
+ # Sets up a model to behave as a Cadmus layout. This will add the following behaviors:
9
+ #
10
+ # * A name field that determines the name of the layout for administrative UI
11
+ # * An optional, polymorphic +parent+ field
12
+ # * A scope called +global+ that returns instances of this class that have no parent
13
+ # * A +liquid_template+ method that parses the value of this model's +content+ field as a Liquid
14
+ # template
15
+ # * A validator that ensure that this layout has a name
16
+ #
17
+ # @param options [Hash] options to modify the default behavior
18
+ # @option options :name_field the name of the field to be used as the layout name. Defaults to +:name+.
19
+ # @option options :skip_template_validation if present, skips the validation of the content template.
20
+ def cadmus_layout(options = {})
21
+ model_with_parent
22
+
23
+ cattr_accessor :name_field
24
+ self.name_field = (options.delete(:name_field) || :name).to_s
25
+ validates_uniqueness_of name_field, scope: [:parent_id, :parent_type]
26
+
27
+ liquid_template_field :liquid_template, :content
28
+ validates_template_validity :content unless options[:skip_template_validation]
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,70 @@
1
+ module Cadmus
2
+ module LayoutsController
3
+ extend ActiveSupport::Concern
4
+ include Cadmus::Concerns::ControllerWithParent
5
+
6
+ included do
7
+ before_action :load_parent_and_cms_layout
8
+ end
9
+
10
+ def index
11
+ @cms_layouts = cms_layout_scope.order(cms_layout_scope.klass.name_field).all
12
+ render 'cadmus/layouts/index'
13
+ end
14
+
15
+ def new
16
+ @cms_layout = cms_layout_scope.new
17
+ render 'cadmus/layouts/new'
18
+ end
19
+
20
+ def edit
21
+ render 'cadmus/layouts/edit'
22
+ end
23
+
24
+ def create
25
+ @cms_layout = cms_layout_scope.new(cms_layout_params)
26
+
27
+ if @cms_layout.save
28
+ redirect_to(url_for(action: 'index'))
29
+ else
30
+ render 'cadmus/layouts/new'
31
+ end
32
+ end
33
+
34
+ def update
35
+ if @cms_layout.update_attributes(cms_layout_params)
36
+ redirect_to(url_for(action: 'index'))
37
+ else
38
+ render 'cadmus/layouts/edit'
39
+ end
40
+ end
41
+
42
+ def destroy
43
+ @cms_layout.destroy
44
+ redirect_to(url_for(action: 'index'))
45
+ end
46
+
47
+ protected
48
+
49
+ # Returns the ActiveRecord::Relation that will be used for finding layouts. If there is a parent
50
+ # for this request, this will be the "cms_layouts" scope defined by the parent object. If there isn't,
51
+ # this will be the "global" scope of the layout class (i.e. layouts with no parent object).
52
+ def cms_layout_scope
53
+ @cms_layout_scope ||= parent_model ? parent_model.cms_layouts : cms_layout_model.global
54
+ end
55
+
56
+ def cms_layout_model
57
+ Cadmus.layout_model
58
+ end
59
+
60
+ def cms_layout_params
61
+ params.require(:cms_layout).permit(:name, :content)
62
+ end
63
+
64
+ def load_parent_and_cms_layout
65
+ if params[:id]
66
+ @cms_layout = cms_layout_scope.find(params[:id])
67
+ end
68
+ end
69
+ end
70
+ end
data/lib/cadmus/page.rb CHANGED
@@ -1,12 +1,11 @@
1
- require 'liquid'
2
-
3
1
  module Cadmus
4
2
 
5
3
  # Adds a +cadmus_page+ extension method to ActiveRecord::Base that sets up a class as a page-like object for
6
4
  # Cadmus.
7
5
  module Page
8
6
  extend ActiveSupport::Concern
9
- include Cadmus::LiquidTemplateField
7
+ include Cadmus::Concerns::LiquidTemplateField
8
+ include Cadmus::Concerns::ModelWithParent
10
9
 
11
10
  module ClassMethods
12
11
 
@@ -26,6 +25,8 @@ module Cadmus
26
25
  # @option options :slug_field the name of the field to be used as the page slug. Defaults to +:slug+.
27
26
  # @option options :slug_generator_field the name of the field to be used as the slug generator.
28
27
  # Defaults to the value of +name_field+ if unspecified.
28
+ # @option options :layout_model_name the name of the model to use as a layout for this page class.
29
+ # @option options :skip_template_validation if present, skips the validation of the content template.
29
30
  def cadmus_page(options={})
30
31
  options[:slug_generator_field] = options[:name_field] unless options.has_key?(:slug_generator_field)
31
32
  has_slug(options)
@@ -33,18 +34,32 @@ module Cadmus
33
34
  cattr_accessor :name_field
34
35
  self.name_field = (options.delete(:name_field) || :name).to_s
35
36
 
36
- belongs_to :parent, :polymorphic => true
37
+ model_with_parent
37
38
 
38
39
  validates_presence_of name_field
39
40
  validates_uniqueness_of slug_field, :scope => [:parent_id, :parent_type]
40
41
  validates_exclusion_of slug_field, :in => %w(pages edit)
41
42
 
42
- scope :global, lambda { where(:parent_id => nil, :parent_type => nil) }
43
+ cattr_accessor :layout_model_name
44
+ self.layout_model_name = options.delete(:layout_model_name) || Cadmus.layout_model.try!(:name)
45
+
46
+ if layout_model
47
+ belongs_to :cms_layout, class_name: layout_model.name, optional: true
48
+ end
43
49
 
44
50
  liquid_template_field :liquid_template, :content
51
+ validates_template_validity :content unless options[:skip_template_validation]
52
+ end
53
+
54
+ def layout_model
55
+ return unless layout_model_name
56
+ layout_model_name.safe_constantize
45
57
  end
46
58
  end
59
+
60
+ def effective_cms_layout
61
+ return nil unless respond_to?(:cms_layout)
62
+ cms_layout
63
+ end
47
64
  end
48
65
  end
49
-
50
- ActiveRecord::Base.send :include, Cadmus::Page
@@ -65,12 +65,9 @@ module Cadmus
65
65
  module PagesController
66
66
  extend ActiveSupport::Concern
67
67
  include Cadmus::Renderable
68
+ include Cadmus::Concerns::ControllerWithParent
68
69
 
69
70
  included do
70
- class << self
71
- attr_accessor :page_parent_name, :page_parent_class, :find_parent_by
72
- end
73
-
74
71
  before_action :load_parent_and_page
75
72
  helper_method :cadmus_renderer
76
73
  end
@@ -151,64 +148,15 @@ module Cadmus
151
148
 
152
149
  protected
153
150
 
154
- # This gets kind of meta.
155
- #
156
- # If page_parent_name and page_parent_class are both defined for this class, this method uses it to find
157
- # the parent object in which pages live. For example, if page_parent_class is Blog and page_parent_name
158
- # is "blog", then this is equivalent to calling:
159
- #
160
- # @page_parent = Blog.where(:id => params["blog_id"]).first
161
- #
162
- # If you don't want to use :id to find the parent object, then redefine the find_parent_by method to return
163
- # what you want to use.
164
- def page_parent
165
- return @page_parent if @page_parent
166
-
167
- if page_parent_name && page_parent_class
168
- parent_id_param = "#{page_parent_name}_id"
169
- if params[parent_id_param]
170
- @page_parent = page_parent_class.where(find_parent_by => params[parent_id_param]).first
171
- end
172
- end
173
-
174
- @page_parent
175
- end
176
-
177
- # Returns the name of the page parent object. This will be used for determining the parameter name for
178
- # finding the parent object. For example, if the page parent name is "wiki", the finder will look in
179
- # params["wiki_id"] to determine the object ID.
180
- #
181
- # By default, this will return the value of page_parent_name set at the controller class level, but can
182
- # be overridden for cases where the page parent name must be determined on a per-request basis.
183
- def page_parent_name
184
- self.class.page_parent_name
185
- end
186
-
187
- # Returns the class of the page parent object. For example, if the pages used by this controller are
188
- # children of a Section object, this method should return the Section class.
189
- #
190
- # By default, this will return the value of page_parent_class set at the controller class level, but can
191
- # be overridden for cases where the page parent class must be determined on a per-request basis.
192
- def page_parent_class
193
- self.class.page_parent_class
194
- end
195
-
196
- # Returns the field used to find the page parent object. By default this is :id, but if you need to
197
- # find the page parent object using a different parameter (for example, if you use a "slug" field for
198
- # part of the URL), this can be changed.
199
- #
200
- # By default this method takes its value from the "find_parent_by" accessor set at the controller class
201
- # level, but it can be overridden for cases where the finder field name should be determined on a
202
- # per-request basis.
203
- def find_parent_by
204
- self.class.find_parent_by || :id
205
- end
206
-
207
151
  # Returns the ActiveRecord::Relation that will be used for finding pages. If there is a page parent
208
152
  # for this request, this will be the "pages" scope defined by the parent object. If there isn't,
209
153
  # this will be the "global" scope of the page class (i.e. pages with no parent object).
210
154
  def page_scope
211
- @page_scope ||= page_parent ? page_parent.pages : page_class.global
155
+ @page_scope ||= parent_model ? parent_model.pages : page_class.global
156
+ end
157
+
158
+ def page_class
159
+ Cadmus.page_model
212
160
  end
213
161
 
214
162
  def page_params
@@ -223,7 +171,7 @@ module Cadmus
223
171
  end
224
172
 
225
173
  def liquid_registers
226
- registers = { 'parent' => page_parent }
174
+ registers = { 'parent' => parent_model, :file_system => liquid_file_system }
227
175
 
228
176
  if defined?(super)
229
177
  registers.merge(super)
@@ -232,4 +180,4 @@ module Cadmus
232
180
  end
233
181
  end
234
182
  end
235
- end
183
+ end
@@ -0,0 +1,32 @@
1
+ module Cadmus
2
+ module Partial
3
+ extend ActiveSupport::Concern
4
+ include Cadmus::Concerns::LiquidTemplateField
5
+ include Cadmus::Concerns::ModelWithParent
6
+
7
+ module ClassMethods
8
+ # Sets up a model to behave as a Cadmus partial. This will add the following behaviors:
9
+ #
10
+ # * A name field that determines the name of the partial for administrative UI
11
+ # * An optional, polymorphic +parent+ field
12
+ # * A scope called +global+ that returns instances of this class that have no parent
13
+ # * A +liquid_template+ method that parses the value of this model's +content+ field as a Liquid
14
+ # template
15
+ # * A validator that ensure that this layout has a name
16
+ #
17
+ # @param options [Hash] options to modify the default behavior
18
+ # @option options :name_field the name of the field to be used as the layout name. Defaults to +:name+.
19
+ # @option options :skip_template_validation if present, skips the validation of the content template.
20
+ def cadmus_partial(options = {})
21
+ model_with_parent
22
+
23
+ cattr_accessor :name_field
24
+ self.name_field = (options.delete(:name_field) || :name).to_s
25
+ validates_uniqueness_of name_field, scope: [:parent_id, :parent_type]
26
+
27
+ liquid_template_field :liquid_template, :content
28
+ validates_template_validity :content unless options[:skip_template_validation]
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,28 @@
1
+ module Cadmus
2
+ # An implementation of Liquid's FileSystem interface that lets it read partials from a model that includes Cadmus::Partial
3
+ class PartialFileSystem
4
+ def initialize(parent)
5
+ @parent = parent
6
+ end
7
+
8
+ def read_template_file(template_path)
9
+ partial_scope.find_by!(partial_model.name_field => template_path).content
10
+ end
11
+
12
+ private
13
+
14
+ def partial_model
15
+ Cadmus.partial_model
16
+ end
17
+
18
+ def partial_scope
19
+ if @parent
20
+ partial_model.where(parent: @parent)
21
+ else
22
+ partial_model.global
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ Liquid::Template.file_system = Cadmus::PartialFileSystem.new(nil)
@@ -0,0 +1,70 @@
1
+ module Cadmus
2
+ module PartialsController
3
+ extend ActiveSupport::Concern
4
+ include Cadmus::Concerns::ControllerWithParent
5
+
6
+ included do
7
+ before_action :load_parent_and_cms_partial
8
+ end
9
+
10
+ def index
11
+ @cms_partials = cms_partial_scope.order(cms_partial_class.name_field).all
12
+ render 'cadmus/partials/index'
13
+ end
14
+
15
+ def new
16
+ @cms_partial = cms_partial_scope.new
17
+ render 'cadmus/partials/new'
18
+ end
19
+
20
+ def edit
21
+ render 'cadmus/partials/edit'
22
+ end
23
+
24
+ def create
25
+ @cms_partial = cms_partial_scope.new(cms_partial_params)
26
+
27
+ if @cms_partial.save
28
+ redirect_to(url_for(action: 'index'))
29
+ else
30
+ render 'cadmus/partials/new'
31
+ end
32
+ end
33
+
34
+ def update
35
+ if @cms_partial.update_attributes(cms_partial_params)
36
+ redirect_to(url_for(action: 'index'))
37
+ else
38
+ render 'cadmus/partials/edit'
39
+ end
40
+ end
41
+
42
+ def destroy
43
+ @cms_partial.destroy
44
+ redirect_to(url_for(action: 'index'))
45
+ end
46
+
47
+ protected
48
+
49
+ # Returns the ActiveRecord::Relation that will be used for finding partials. If there is a parent
50
+ # for this request, this will be the "cms_partials" scope defined by the parent object. If there isn't,
51
+ # this will be the "global" scope of the partial class (i.e. partials with no parent object).
52
+ def cms_partial_scope
53
+ @cms_partial_scope ||= parent_model ? parent_model.cms_partials : cms_partial_class.global
54
+ end
55
+
56
+ def cms_partial_class
57
+ Cadmus.partial_model
58
+ end
59
+
60
+ def cms_partial_params
61
+ params.require(:cms_partial).permit(cms_partial_class.name_field, :content)
62
+ end
63
+
64
+ def load_parent_and_cms_partial
65
+ if params[:id]
66
+ @cms_partial = cms_partial_scope.find(params[:id])
67
+ end
68
+ end
69
+ end
70
+ end