locomotive_cms 2.0.0.rc2 → 2.0.0.rc4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. data/README.textile +4 -4
  2. data/app/assets/images/locomotive/icons/start.png +0 -0
  3. data/app/assets/javascripts/locomotive/models/page.js.coffee +1 -1
  4. data/app/assets/javascripts/locomotive/models/site.js.coffee +1 -1
  5. data/app/assets/javascripts/locomotive/views/content_entries/_form_view.js.coffee +11 -4
  6. data/app/assets/javascripts/locomotive/views/editable_elements/control_view.js.coffee +24 -0
  7. data/app/assets/javascripts/locomotive/views/editable_elements/edit_all_view.js.coffee +4 -0
  8. data/app/assets/javascripts/locomotive/views/editable_elements/short_text_view.js.coffee +5 -0
  9. data/app/assets/javascripts/locomotive/views/pages/_form_view.js.coffee +19 -0
  10. data/app/assets/javascripts/locomotive/views/shared/fields/has_many_view.js.coffee +3 -0
  11. data/app/assets/javascripts/locomotive/views/shared/fields/many_to_many_view.js.coffee +3 -0
  12. data/app/assets/javascripts/locomotive/views/shared/form_view.js.coffee +6 -0
  13. data/app/assets/stylesheets/locomotive/backoffice/application.css.scss +8 -0
  14. data/app/assets/stylesheets/locomotive/inline_editor/layout.css.scss +1 -0
  15. data/app/controllers/locomotive/pages_controller.rb +4 -3
  16. data/app/helpers/locomotive/pages_helper.rb +13 -0
  17. data/app/models/locomotive/editable_control.rb +56 -0
  18. data/app/models/locomotive/editable_element.rb +90 -18
  19. data/app/models/locomotive/editable_file.rb +63 -1
  20. data/app/models/locomotive/editable_long_text.rb +2 -0
  21. data/app/models/locomotive/editable_short_text.rb +32 -5
  22. data/app/models/locomotive/extensions/page/editable_elements.rb +14 -45
  23. data/app/models/locomotive/extensions/page/listed.rb +1 -0
  24. data/app/models/locomotive/extensions/page/parse.rb +10 -3
  25. data/app/models/locomotive/extensions/page/render.rb +80 -0
  26. data/app/models/locomotive/extensions/page/templatized.rb +43 -2
  27. data/app/models/locomotive/extensions/site/subdomain_domains.rb +1 -0
  28. data/app/models/locomotive/page.rb +7 -1
  29. data/app/models/locomotive/site.rb +4 -0
  30. data/app/presenters/locomotive/content_entry_presenter.rb +1 -1
  31. data/app/presenters/locomotive/editable_control_presenter.rb +18 -0
  32. data/app/presenters/locomotive/page_presenter.rb +2 -2
  33. data/app/views/locomotive/pages/_editable_elements.html.haml +12 -0
  34. data/app/views/locomotive/pages/_form.html.haml +6 -4
  35. data/app/views/locomotive/pages/_page.html.haml +3 -0
  36. data/app/views/locomotive/shared/menu/_contents.html.haml +1 -1
  37. data/config/locales/admin_ui.fr.yml +29 -3
  38. data/config/locales/default.en.yml +1 -1
  39. data/config/locales/default.fr.yml +23 -16
  40. data/config/locales/formtastic.en.yml +1 -7
  41. data/config/locales/formtastic.fr.yml +7 -14
  42. data/lib/locomotive.rb +1 -1
  43. data/lib/locomotive/engine.rb +8 -8
  44. data/lib/locomotive/liquid.rb +1 -0
  45. data/lib/locomotive/liquid/drops/content_entry.rb +2 -2
  46. data/lib/locomotive/liquid/drops/page.rb +1 -1
  47. data/lib/locomotive/liquid/errors.rb +2 -0
  48. data/lib/locomotive/liquid/filters/base.rb +47 -0
  49. data/lib/locomotive/liquid/filters/html.rb +16 -76
  50. data/lib/locomotive/liquid/filters/misc.rb +43 -18
  51. data/lib/locomotive/liquid/filters/text.rb +18 -0
  52. data/lib/locomotive/liquid/tags/editable.rb +1 -1
  53. data/lib/locomotive/liquid/tags/editable/base.rb +15 -24
  54. data/lib/locomotive/liquid/tags/editable/control.rb +31 -0
  55. data/lib/locomotive/liquid/tags/editable/file.rb +17 -1
  56. data/lib/locomotive/liquid/tags/editable/long_text.rb +5 -3
  57. data/lib/locomotive/liquid/tags/editable/short_text.rb +9 -3
  58. data/lib/locomotive/liquid/tags/extends.rb +3 -1
  59. data/lib/locomotive/liquid/tags/locale_switcher.rb +2 -2
  60. data/lib/locomotive/liquid/tags/nav.rb +26 -7
  61. data/lib/locomotive/liquid/tags/paginate.rb +9 -2
  62. data/lib/locomotive/middlewares.rb +2 -1
  63. data/lib/locomotive/middlewares/inline_editor.rb +31 -0
  64. data/lib/locomotive/render.rb +5 -29
  65. data/lib/locomotive/version.rb +1 -1
  66. data/vendor/assets/javascripts/locomotive/cmd.js +31 -0
  67. data/vendor/assets/javascripts/locomotive/menu_toggler.js +1 -1
  68. metadata +89 -74
  69. data/app/middlewares/locomotive/inline_editor_middleware.rb +0 -24
  70. data/lib/locomotive/liquid/tags/blueprint.rb +0 -21
  71. data/lib/locomotive/liquid/tags/editable/content.rb +0 -49
  72. data/lib/locomotive/liquid/tags/jquery.rb +0 -17
@@ -1,17 +1,79 @@
1
1
  module Locomotive
2
2
  class EditableFile < EditableElement
3
3
 
4
+ ## behaviours ##
4
5
  mount_uploader 'source', EditableFileUploader
5
6
 
6
7
  replace_field 'source', ::String, true
7
8
 
9
+ ## fields ##
10
+ field :default_source_url, :localize => true
11
+
12
+ ## callbacks ##
13
+ after_save :propagate_content
14
+
15
+ ## methods ##
16
+
17
+ # Returns the url or the path to the uploaded file
18
+ # if it exists. Otherwise returns the default url.
19
+ #
20
+ # @note This method is not used for the rendering, only for the back-office
21
+ #
22
+ # @return [String] The url or path of the file
23
+ #
8
24
  def content
9
- self.source? ? self.source.url : self.default_content
25
+ self.source? ? self.source.url : self.default_source_url
26
+ end
27
+
28
+ def default_content?
29
+ !self.source? && self.default_source_url.present?
30
+ end
31
+
32
+ def copy_attributes(attributes)
33
+ unless self.default_content?
34
+ attributes.delete(:default_source_url)
35
+ end
36
+
37
+ super(attributes)
38
+ end
39
+
40
+ def copy_attributes_from(el)
41
+ super(el)
42
+
43
+ if el.source_translations.nil?
44
+ self.attributes['default_source_url'] = el.attributes['default_source_url'] || {}
45
+ else
46
+ el.source_translations.keys.each do |locale|
47
+ ::Mongoid::Fields::I18n.with_locale(locale) do
48
+ self.default_source_url = el.source? ? el.source.url : el.default_source_url
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ def remove_source=(value)
55
+ self.source_will_change! # notify the page to run the callbacks for that element
56
+ self.default_source_url = nil
57
+ super
10
58
  end
11
59
 
12
60
  def as_json(options = {})
13
61
  Locomotive::EditableFilePresenter.new(self).as_json
14
62
  end
15
63
 
64
+ protected
65
+
66
+ def propagate_content
67
+ if self.source_changed?
68
+ operations = {
69
+ '$set' => {
70
+ "editable_elements.$.default_source_url.#{::Mongoid::Fields::I18n.locale}" => self.source.url
71
+ }
72
+ }
73
+
74
+ self.page.collection.update self._selector, operations, :multi => true
75
+ end
76
+ end
77
+
16
78
  end
17
79
  end
@@ -1,6 +1,8 @@
1
1
  module Locomotive
2
2
  class EditableLongText < EditableShortText
3
3
 
4
+ ## methods ##
5
+
4
6
  def as_json(options = {})
5
7
  Locomotive::EditableLongTextPresenter.new(self).as_json
6
8
  end
@@ -2,20 +2,47 @@ module Locomotive
2
2
  class EditableShortText < EditableElement
3
3
 
4
4
  ## fields ##
5
- field :content, :localize => true
5
+ field :content, :localize => true
6
+ field :default_content, :type => Boolean, :localize => true, :default => true
6
7
 
7
8
  ## methods ##
8
9
 
9
- def content_with_localization
10
- value = self.content_without_localization
11
- value.blank? ? self.default_content : value
10
+ def content=(value)
11
+ self.add_current_locale
12
+ self.default_content = false unless self.new_record?
13
+ super
12
14
  end
13
15
 
14
- alias_method_chain :content, :localization
16
+ def default_content?
17
+ !!self.default_content
18
+ end
19
+
20
+ def copy_attributes_from(el)
21
+ super(el)
22
+
23
+ self.attributes['content'] = el.content_translations || {}
24
+ self.attributes['default_content'] = el.default_content_translations
25
+ end
15
26
 
16
27
  def as_json(options = {})
17
28
  Locomotive::EditableShortTextPresenter.new(self).as_json
18
29
  end
19
30
 
31
+ protected
32
+
33
+ def propagate_content
34
+ if self.content_changed?
35
+ operations = {
36
+ '$set' => {
37
+ "editable_elements.$.content.#{::Mongoid::Fields::I18n.locale}" => self.content,
38
+ "editable_elements.$.default_content.#{::Mongoid::Fields::I18n.locale}" => false,
39
+ }
40
+ }
41
+
42
+ self.page.collection.update self._selector, operations, :multi => true
43
+ end
44
+ true
45
+ end
46
+
20
47
  end
21
48
  end
@@ -6,15 +6,10 @@ module Locomotive
6
6
  extend ActiveSupport::Concern
7
7
 
8
8
  included do
9
- embeds_many :editable_elements, :class_name => 'Locomotive::EditableElement'
9
+ embeds_many :editable_elements, :class_name => 'Locomotive::EditableElement', :cascade_callbacks => true
10
10
 
11
11
  after_save :remove_disabled_editable_elements
12
12
 
13
- # editable file callbacks
14
- after_save :store_file_sources!
15
- before_save :write_file_source_identifiers
16
- after_destroy :remove_file_sources!
17
-
18
13
  accepts_nested_attributes_for :editable_elements
19
14
  end
20
15
 
@@ -31,7 +26,7 @@ module Locomotive
31
26
  end
32
27
 
33
28
  def enabled_editable_elements
34
- self.editable_elements.by_priority.reject { |el| el.disabled? }
29
+ self.editable_elements.by_priority.find_all(&:editable?)
35
30
  end
36
31
 
37
32
  def editable_elements_grouped_by_blocks
@@ -51,10 +46,14 @@ module Locomotive
51
46
  element = self.find_editable_element(attributes[:block], attributes[:slug])
52
47
 
53
48
  if element
54
- element.attributes = attributes
49
+ element.copy_attributes(attributes)
55
50
  else
56
- self.editable_elements.build(attributes, type)
51
+ element = self.editable_elements.build(attributes, type)
57
52
  end
53
+
54
+ element.add_current_locale
55
+
56
+ element
58
57
  end
59
58
 
60
59
  def enable_editable_elements(block)
@@ -63,26 +62,15 @@ module Locomotive
63
62
 
64
63
  def merge_editable_elements_from_page(source)
65
64
  source.editable_elements.each do |el|
66
- next if el.disabled? or !el.assignable?
65
+ next if el.disabled?
67
66
 
68
67
  existing_el = self.find_editable_element(el.block, el.slug)
69
68
 
70
69
  if existing_el.nil? # new one from parents
71
- new_attributes = el.attributes.merge(:from_parent => true)
72
-
73
- if new_attributes['default_attribute'].present?
74
- new_attributes['default_content'] = self.send(new_attributes['default_attribute']) || el.content
75
- else
76
- if el.respond_to?(:content) # only for text
77
- new_attributes['default_content'] = el.content
78
- end
79
- end
80
-
81
- self.editable_elements.build(new_attributes, el.class)
82
- elsif existing_el.default_attribute.nil?
83
- existing_el.attributes = { :disabled => false, :default_content => el.content }
70
+ new_el = self.editable_elements.build({}, el.class)
71
+ new_el.copy_attributes_from(el)
84
72
  else
85
- existing_el.attributes = { :disabled => false }
73
+ existing_el.disabled = false
86
74
  end
87
75
  end
88
76
  end
@@ -90,27 +78,8 @@ module Locomotive
90
78
  def remove_disabled_editable_elements
91
79
  return unless self.editable_elements.any? { |el| el.disabled? }
92
80
 
93
- # super fast way to remove useless elements all in once (TODO callbacks)
94
- self.collection.update(self.atomic_selector, '$pull' => { 'editable_elements' => { 'disabled' => true } })
95
- end
96
-
97
- protected
98
-
99
- ## callbacks for editable files
100
-
101
- # equivalent to "after_save :store_source!" in EditableFile
102
- def store_file_sources!
103
- self.find_editable_files.collect(&:store_source!)
104
- end
105
-
106
- # equivalent to "before_save :write_source_identifier" in EditableFile
107
- def write_file_source_identifiers
108
- self.find_editable_files.collect(&:write_source_identifier)
109
- end
110
-
111
- # equivalent to "after_destroy :remove_source!" in EditableFile
112
- def remove_file_sources!
113
- self.find_editable_files.collect(&:remove_source!)
81
+ # super fast way to remove useless elements all in once
82
+ self.collection.update(self.atomic_selector, '$pull' => { 'editable_elements' => { "disabled.#{::Mongoid::Fields::I18n.locale}" => true } })
114
83
  end
115
84
 
116
85
  end
@@ -7,6 +7,7 @@ module Locomotive
7
7
 
8
8
  included do
9
9
 
10
+ ## fields ##
10
11
  field :listed, :type => Boolean, :default => true
11
12
 
12
13
  end
@@ -6,17 +6,22 @@ module Locomotive
6
6
  extend ActiveSupport::Concern
7
7
 
8
8
  included do
9
+ ## fields ##
9
10
  field :serialized_template, :type => Binary, :localize => true
10
11
  field :template_dependencies, :type => Array, :default => [], :localize => true
11
12
  field :snippet_dependencies, :type => Array, :default => [], :localize => true
12
13
 
14
+ ## virtual attributes
13
15
  attr_reader :template_changed
14
16
 
17
+ ## callbacks ##
15
18
  before_validation :serialize_template
16
19
  after_save :update_template_descendants
17
20
 
21
+ ## validations ##
18
22
  validate :template_must_be_valid
19
23
 
24
+ ## scopes ##
20
25
  scope :pages, lambda { |domain| { :any_in => { :domains => [*domain] } } }
21
26
  end
22
27
 
@@ -38,6 +43,8 @@ module Locomotive
38
43
  @parsing_errors << I18n.t(:liquid_syntax, :fullpath => self.fullpath, :error => error.to_s, :scope => [:errors, :messages, :page])
39
44
  rescue ::Locomotive::Liquid::PageNotFound => error
40
45
  @parsing_errors << I18n.t(:liquid_extend, :fullpath => self.fullpath, :scope => [:errors, :messages, :page])
46
+ rescue ::Locomotive::Liquid::PageNotTranslated => error
47
+ @parsing_errors << I18n.t(:liquid_translation, :fullpath => self.fullpath, :scope => [:errors, :messages, :page])
41
48
  end
42
49
  end
43
50
  end
@@ -60,8 +67,8 @@ module Locomotive
60
67
 
61
68
  @template = ::Liquid::Template.parse(self.raw_template, context)
62
69
 
63
- self.template_dependencies = context[:templates]
64
- self.snippet_dependencies = context[:snippets]
70
+ self.template_dependencies = context[:templates]
71
+ self.snippet_dependencies = context[:snippets]
65
72
 
66
73
  @template.root.context.clear
67
74
  end
@@ -77,7 +84,7 @@ module Locomotive
77
84
  return unless @template_changed == true
78
85
 
79
86
  # we admit at this point that the current template is up-to-date
80
- template_descendants = self.site.pages.any_in(:template_dependencies => [self.id]).to_a
87
+ template_descendants = self.site.pages.any_in("template_dependencies.#{::Mongoid::Fields::I18n.locale}" => [self.id]).to_a
81
88
 
82
89
  # group them by fullpath for better performance
83
90
  cached = template_descendants.inject({}) { |memo, page| memo[page.fullpath] = page; memo }
@@ -3,10 +3,90 @@ module Locomotive
3
3
  module Page
4
4
  module Render
5
5
 
6
+ extend ActiveSupport::Concern
7
+
6
8
  def render(context)
7
9
  self.template.render(context)
8
10
  end
9
11
 
12
+ module ClassMethods
13
+
14
+ # Given both a site and a path, this method tries
15
+ # to get the matching page.
16
+ # If the page is templatized, the related content entry is
17
+ # associated to the page (page.content_entry stores the entry).
18
+ # If no page is found, then it returns the 404 one instead.
19
+ #
20
+ # @param [ Site ] site The site where to find the page
21
+ # @param [ String ] path The fullpath got from the request
22
+ # @param [ Boolean ] logged_in True if someone is logged in Locomotive
23
+ #
24
+ # @return [ Page ] The page matching the criteria OR the 404 one if none
25
+ #
26
+ def fetch_page_from_path(site, path, logged_in)
27
+ page = nil
28
+ depth = path == 'index' ? 0 : path.split('/').size
29
+
30
+ matching_paths = path == 'index' ? %w(index) : path_combinations(path)
31
+
32
+ site.pages.where(:depth => depth, :fullpath.in => matching_paths).each do |_page|
33
+ if !_page.published? && !logged_in
34
+ next
35
+ else
36
+ if _page.templatized?
37
+ %r(^#{_page.fullpath.gsub('content_type_template', '([^\/]+)')}$) =~ path
38
+
39
+ permalink = $1
40
+
41
+ _page.content_entry = _page.fetch_target_entry(permalink)
42
+
43
+ if _page.content_entry.nil? || (!_page.content_entry.visible? && !logged_in) # content instance not found or not visible
44
+ next
45
+ end
46
+ end
47
+ end
48
+
49
+ page = _page
50
+
51
+ break
52
+ end
53
+
54
+ page || site.pages.not_found.published.first
55
+ end
56
+
57
+ # Calculate all the combinations possible based on the
58
+ # fact that one of the segment of the path could be
59
+ # a content type from a templatized page.
60
+ # We postulate that there is only one templatized page in a path
61
+ # (ie: no nested templatized pages)
62
+ #
63
+ # @param [ String ] path The path to the page
64
+ #
65
+ # @return [ Array ] An array of all the combinations
66
+ #
67
+ def path_combinations(path)
68
+ _path_combinations(path.split('/'))
69
+ end
70
+
71
+ #:nodoc:
72
+ def _path_combinations(segments, can_include_template = true)
73
+ return nil if segments.empty?
74
+
75
+ segment = segments.shift
76
+
77
+ (can_include_template ? [segment, 'content_type_template'] : [segment]).map do |_segment|
78
+ if (_combinations = _path_combinations(segments.clone, can_include_template && _segment != 'content_type_template'))
79
+ [*_combinations].map do |_combination|
80
+ File.join(_segment, _combination)
81
+ end
82
+ else
83
+ [_segment]
84
+ end
85
+ end.flatten
86
+ end
87
+
88
+ end
89
+
10
90
  end
11
91
  end
12
92
  end
@@ -7,7 +7,9 @@ module Locomotive
7
7
 
8
8
  included do
9
9
 
10
- field :templatized, :type => Boolean, :default => false
10
+ ## fields ##
11
+ field :templatized, :type => Boolean, :default => false
12
+ field :templatized_from_parent, :type => Boolean, :default => false
11
13
  field :target_klass_name
12
14
 
13
15
  ## validations ##
@@ -15,11 +17,16 @@ module Locomotive
15
17
  validate :ensure_target_klass_name_security
16
18
 
17
19
  ## callbacks ##
20
+ before_validation :get_templatized_from_parent
18
21
  before_validation :set_slug_if_templatized
19
22
  before_validation :ensure_target_klass_name_security
23
+ after_save :propagate_templatized
20
24
 
21
25
  ## scopes ##
22
26
  scope :templatized, :where => { :templatized => true }
27
+
28
+ ## virtual attributes ##
29
+ attr_accessor :content_entry
23
30
  end
24
31
 
25
32
  # Returns the class specified by the target_klass_name property
@@ -70,10 +77,26 @@ module Locomotive
70
77
 
71
78
  protected
72
79
 
80
+ def get_templatized_from_parent
81
+ return if self.parent.nil?
82
+
83
+ if self.parent.templatized?
84
+ self.templatized = self.templatized_from_parent = true
85
+ self.target_klass_name = self.parent.target_klass_name
86
+ elsif !self.templatized?
87
+ self.templatized = self.templatized_from_parent = false
88
+ self.target_klass_name = nil
89
+ end
90
+ end
91
+
73
92
  def set_slug_if_templatized
74
- self.slug = 'content_type_template' if self.templatized?
93
+ self.slug = 'content_type_template' if self.templatized? && !self.templatized_from_parent?
75
94
  end
76
95
 
96
+ # Makes sure the target_klass is owned by the site OR
97
+ # if it belongs to the models allowed by the application
98
+ # thanks to the models_for_templatization option.
99
+ #
77
100
  def ensure_target_klass_name_security
78
101
  return if !self.templatized? || self.target_klass_name.blank?
79
102
 
@@ -86,7 +109,25 @@ module Locomotive
86
109
  elsif !Locomotive.config.models_for_templatization.include?(self.target_klass_name)
87
110
  self.errors.add :target_klass_name, :security
88
111
  end
112
+ end
89
113
 
114
+ # Sets the templatized, templatized_from_parent properties of
115
+ # the children of the current page ONLY IF the templatized
116
+ # attribute got changed.
117
+ #
118
+ def propagate_templatized
119
+ return unless self.templatized_changed?
120
+
121
+ selector = { 'parent_ids' => { '$in' => [self._id] } }
122
+ operations = {
123
+ '$set' => {
124
+ 'templatized' => self.templatized,
125
+ 'templatized_from_parent' => self.templatized,
126
+ 'target_klass_name' => self.target_klass_name
127
+ }
128
+ }
129
+
130
+ self.collection.update selector, operations, :multi => true
90
131
  end
91
132
 
92
133
  end