locomotive_cms 2.3.1 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/locomotive/models/content_entry.js.coffee +4 -0
  3. data/app/assets/javascripts/locomotive/views/content_assets/picker_view.js.coffee +0 -2
  4. data/app/assets/javascripts/locomotive/views/content_entries/_form_view.js.coffee +1 -1
  5. data/app/assets/javascripts/locomotive/views/content_entries/_popup_form_view.js.coffee +7 -0
  6. data/app/assets/javascripts/locomotive/views/inline_editor/application_view.js.coffee +1 -1
  7. data/app/assets/javascripts/locomotive/views/pages/_form_view.js.coffee +2 -1
  8. data/app/assets/javascripts/locomotive/views/shared/fields/_relationship_view.js.coffee +45 -0
  9. data/app/assets/javascripts/locomotive/views/shared/fields/belongs_to_view.js.coffee +4 -20
  10. data/app/assets/javascripts/locomotive/views/shared/fields/many_to_many_view.js.coffee +53 -42
  11. data/app/assets/javascripts/locomotive/views/snippets/_form_view.js.coffee +0 -1
  12. data/app/assets/stylesheets/locomotive/backoffice/application.css.scss +31 -0
  13. data/app/assets/stylesheets/locomotive/backoffice/formtastic_changes.css.scss +19 -0
  14. data/app/controllers/locomotive/api/accounts_controller.rb +6 -0
  15. data/app/controllers/locomotive/content_entries_controller.rb +1 -1
  16. data/app/helpers/locomotive/content_entries_helper.rb +22 -0
  17. data/app/models/locomotive/content_entry.rb +16 -47
  18. data/app/models/locomotive/content_type.rb +19 -3
  19. data/app/models/locomotive/editable_text.rb +7 -1
  20. data/app/models/locomotive/extensions/content_entry/localized.rb +62 -0
  21. data/app/models/locomotive/extensions/page/tree.rb +5 -0
  22. data/app/models/locomotive/extensions/shared/slug.rb +33 -0
  23. data/app/models/locomotive/page.rb +4 -11
  24. data/app/models/locomotive/snippet.rb +6 -7
  25. data/app/models/locomotive/translation.rb +2 -2
  26. data/app/presenters/locomotive/account_presenter.rb +1 -1
  27. data/app/views/locomotive/content_entries/_list.html.haml +3 -1
  28. data/app/views/locomotive/custom_fields/types/_boolean.html.haml +1 -1
  29. data/app/views/locomotive/custom_fields/types/_date.html.haml +1 -1
  30. data/app/views/locomotive/custom_fields/types/_date_time.html.haml +1 -1
  31. data/app/views/locomotive/custom_fields/types/_email.html.haml +1 -1
  32. data/app/views/locomotive/custom_fields/types/_file.html.haml +1 -1
  33. data/app/views/locomotive/custom_fields/types/_float.html.haml +1 -1
  34. data/app/views/locomotive/custom_fields/types/_integer.html.haml +1 -1
  35. data/app/views/locomotive/custom_fields/types/_many_to_many.html.haml +1 -5
  36. data/app/views/locomotive/custom_fields/types/_select.html.haml +1 -1
  37. data/app/views/locomotive/custom_fields/types/_string.html.haml +1 -1
  38. data/app/views/locomotive/custom_fields/types/_tags.html.haml +1 -1
  39. data/app/views/locomotive/custom_fields/types/_text.html.haml +1 -1
  40. data/config/locales/admin_ui.de.yml +4 -0
  41. data/config/locales/default.zh-CN.yml +162 -52
  42. data/config/locales/devise.bg.yml +1 -1
  43. data/config/locales/devise.cs.yml +1 -1
  44. data/config/locales/devise.de.yml +1 -1
  45. data/config/locales/devise.en.yml +1 -1
  46. data/config/locales/devise.es.yml +2 -2
  47. data/config/locales/devise.et.yml +1 -1
  48. data/config/locales/devise.fr.yml +1 -1
  49. data/config/locales/devise.it.yml +1 -1
  50. data/config/locales/devise.ja.yml +1 -1
  51. data/config/locales/devise.nb.yml +1 -1
  52. data/config/locales/devise.nl.yml +2 -2
  53. data/config/locales/devise.pl.yml +1 -1
  54. data/config/locales/devise.pt-BR.yml +2 -2
  55. data/config/locales/devise.ru.yml +1 -1
  56. data/config/locales/devise.zh-CN.yml +1 -1
  57. data/config/routes.rb +2 -2
  58. data/features/api/accounts.feature +19 -1
  59. data/features/api/authorization/accounts.feature +52 -12
  60. data/features/backoffice/content_types/has_many.feature +3 -3
  61. data/features/backoffice/content_types/integer.feature +4 -4
  62. data/features/backoffice/content_types/many_to_many.feature +1 -1
  63. data/features/backoffice/my_account.feature +1 -0
  64. data/features/step_definitions/web_steps.rb +7 -0
  65. data/lib/locomotive/action_controller/public_responder.rb +24 -1
  66. data/lib/locomotive/core_ext.rb +8 -2
  67. data/lib/locomotive/devise.rb +21 -0
  68. data/lib/locomotive/httparty/webservice.rb +17 -9
  69. data/lib/locomotive/liquid.rb +1 -0
  70. data/lib/locomotive/liquid/tags/consume.rb +12 -11
  71. data/lib/locomotive/liquid/tags/link_to.rb +6 -60
  72. data/lib/locomotive/liquid/tags/path_helper.rb +82 -0
  73. data/lib/locomotive/liquid/tags/path_to.rb +21 -0
  74. data/lib/locomotive/liquid/tags/snippet.rb +1 -1
  75. data/lib/locomotive/render.rb +2 -1
  76. data/lib/locomotive/version.rb +1 -1
  77. data/spec/lib/locomotive/liquid/tags/consume_spec.rb +12 -0
  78. data/spec/lib/locomotive/liquid/tags/path_to_spec.rb +111 -0
  79. data/spec/models/locomotive/content_entry_spec.rb +2 -2
  80. data/spec/models/locomotive/snippet_spec.rb +21 -0
  81. metadata +13 -6
@@ -5,8 +5,9 @@ module Locomotive
5
5
 
6
6
  ## extensions ##
7
7
  include ::CustomFields::Target
8
- include Extensions::Shared::Seo
9
8
  include Extensions::ContentEntry::Csv
9
+ include Extensions::ContentEntry::Localized
10
+ include Extensions::Shared::Seo
10
11
 
11
12
  ## fields ##
12
13
  field :_slug, localize: true
@@ -33,6 +34,7 @@ module Locomotive
33
34
  ## named scopes ##
34
35
  scope :visible, where(_visible: true)
35
36
  scope :latest_updated, order_by(updated_at: :desc).limit(Locomotive.config.ui[:latest_entries_nb])
37
+ scope :next_or_previous, ->(condition, order_by) { where({ _visible: true }.merge(condition)).limit(1).order_by(order_by) }
36
38
 
37
39
  ## methods ##
38
40
 
@@ -59,41 +61,6 @@ module Locomotive
59
61
 
60
62
  alias :to_label :_label
61
63
 
62
- # Tell if the content entry has been translated or not.
63
- # It just checks if the field used for the label has been translated.
64
- # It assumes the entry is localized.
65
- #
66
- # @return [ Boolean ] True if translated, false otherwise
67
- #
68
- def translated?
69
- if self.respond_to?(:"#{self._label_field_name}_translations")
70
- self.send(:"#{self._label_field_name}_translations").key?(::Mongoid::Fields::I18n.locale.to_s) #rescue false
71
- else
72
- true
73
- end
74
- end
75
-
76
- # Return the locales the content entry has been translated to.
77
- #
78
- # @return [ Array ] The list of locales. Nil if not localized
79
- #
80
- def translated_in
81
- if self.localized?
82
- self.send(:"#{self._label_field_name}_translations").keys
83
- else
84
- nil
85
- end
86
- end
87
-
88
- # Tell if the entry is localized or not, meaning if the label field
89
- # is localized or not.
90
- #
91
- # @return [ Boolean ] True if localized, false otherwise
92
- #
93
- def localized?
94
- self.respond_to?(:"#{self._label_field_name}_translations")
95
- end
96
-
97
64
  # Return the next content entry based on the order defined in the parent content type.
98
65
  #
99
66
  # @param [ Object ] The next content entry or nil if not found
@@ -123,13 +90,14 @@ module Locomotive
123
90
  # Sort the content entries from an ordered array of content entry ids.
124
91
  # Their new positions are persisted.
125
92
  #
93
+ # @param [ Array ] content_type The content type describing the entries
126
94
  # @param [ Array ] The ordered array of ids
127
95
  #
128
- def self.sort_entries!(ids)
96
+ def self.sort_entries!(ids, column = :_position)
129
97
  list = self.any_in(_id: ids.map { |id| Moped::BSON::ObjectId.from_string(id.to_s) }).to_a
130
98
  ids.each_with_index do |id, position|
131
99
  if entry = list.detect { |e| e._id.to_s == id.to_s }
132
- entry.update_attributes _position: position
100
+ entry.update_attributes column => position
133
101
  end
134
102
  end
135
103
  end
@@ -162,11 +130,13 @@ module Locomotive
162
130
  # @return [ Object ] The next or previous content entry or nil if none
163
131
  #
164
132
  def next_or_previous(matcher = :gt)
165
- order_by = self.content_type.order_by_definition(matcher == :lt)
166
- criterion = self.content_type.order_by_attribute.to_sym.send(matcher)
167
- value = self.send(self.content_type.order_by_attribute.to_sym)
133
+ attribute, direction = self.content_type.order_by_definition(matcher == :lt)
134
+
135
+ criterion = attribute.to_sym.send(matcher)
136
+ value = self.send(attribute.to_sym)
137
+ order_by = [attribute, direction]
168
138
 
169
- self.class.where(criterion => value).order_by(:order_by.asc).limit(1).first
139
+ self.class.next_or_previous({ criterion => value }, order_by).first
170
140
  end
171
141
 
172
142
  # Set the slug of the instance by using the value of the highlighted field
@@ -176,10 +146,7 @@ module Locomotive
176
146
  self._slug = self._label.dup if self._slug.blank? && self._label.present?
177
147
 
178
148
  if self._slug.present?
179
- # if the slug includes one "_" at least, we consider that the "_" is used instead of "-".
180
- underscore = !self._slug.index('_').nil?
181
-
182
- self._slug.permalink!(underscore)
149
+ self._slug.permalink!
183
150
 
184
151
  self._slug = self.next_unique_slug if self.slug_already_taken?
185
152
  end
@@ -239,8 +206,10 @@ module Locomotive
239
206
  def send_notifications
240
207
  return if !self.content_type.public_submission_enabled? || self.content_type.public_submission_accounts.blank?
241
208
 
209
+ account_ids = self.content_type.public_submission_accounts.map(&:to_s)
210
+
242
211
  self.site.accounts.each do |account|
243
- next unless self.content_type.public_submission_accounts.map(&:to_s).include?(account._id.to_s)
212
+ next unless account_ids.include?(account._id.to_s)
244
213
 
245
214
  Locomotive::Notifications.new_content_entry(account, self).deliver
246
215
  end
@@ -109,9 +109,19 @@ module Locomotive
109
109
  self.find_entries_custom_field(self.group_by_field_id)
110
110
  end
111
111
 
112
+ def sortable_column
113
+ # only the belongs_to field has a special column for relative positionning
114
+ # that's why we don't call groupable?
115
+ if self.group_by_field.try(:type) == 'belongs_to' && self.order_manually?
116
+ "position_in_#{self.group_by_field.name}"
117
+ else
118
+ '_position'
119
+ end
120
+ end
121
+
112
122
  def list_or_group_entries(options = {})
113
123
  if self.groupable?
114
- if group_by_field.type == 'select'
124
+ if self.group_by_field.type == 'select'
115
125
  self.entries.group_by_select_option(self.group_by_field.name, self.order_by_definition)
116
126
  else
117
127
  group_by_belongs_to_field(self.group_by_field)
@@ -201,8 +211,14 @@ module Locomotive
201
211
  end
202
212
 
203
213
  def order_by_attribute
204
- return self.order_by if %w(created_at updated_at _position).include?(self.order_by)
205
- self.entries_custom_fields.find(self.order_by).name rescue 'created_at'
214
+ case self.order_by
215
+ when '_position'
216
+ self.sortable_column
217
+ when 'created_at', 'updated_at'
218
+ self.order_by
219
+ else
220
+ self.entries_custom_fields.find(self.order_by).name rescue 'created_at'
221
+ end
206
222
  end
207
223
 
208
224
  def normalize_slug
@@ -35,6 +35,8 @@ module Locomotive
35
35
  def copy_attributes_from(el)
36
36
  super(el)
37
37
 
38
+ self.copy_formatting_attributes_from(el)
39
+
38
40
  self.attributes['content'] = el.content_translations || {}
39
41
  self.attributes['default_content'] = el.default_content_translations
40
42
  end
@@ -42,8 +44,12 @@ module Locomotive
42
44
  def copy_default_attributes_from(el)
43
45
  super(el)
44
46
 
47
+ self.copy_formatting_attributes_from(el)
48
+ end
49
+
50
+ def copy_formatting_attributes_from(el)
45
51
  %w(format rows line_break).each do |attr|
46
- self.send(:"#{attr}=", el.send(attr.to_sym))
52
+ self.attributes[attr] = el.attributes[attr]
47
53
  end
48
54
  end
49
55
 
@@ -0,0 +1,62 @@
1
+ module Locomotive
2
+ module Extensions
3
+ module ContentEntry
4
+ module Localized
5
+
6
+ # Tell if the content entry has been translated or not.
7
+ # It just checks if the field used for the label has been translated.
8
+ # It assumes the entry is localized.
9
+ #
10
+ # @return [ Boolean ] True if translated, false otherwise
11
+ #
12
+ def translated?
13
+ if self.respond_to?(:"#{self._label_field_name}_translations")
14
+ self.send(:"#{self._label_field_name}_translations").key?(::Mongoid::Fields::I18n.locale.to_s) #rescue false
15
+ else
16
+ true
17
+ end
18
+ end
19
+
20
+ # Return the locales the content entry has been translated to.
21
+ #
22
+ # @return [ Array ] The list of locales. Nil if not localized
23
+ #
24
+ def translated_in
25
+ if self.localized?
26
+ self.send(:"#{self._label_field_name}_translations").keys
27
+ else
28
+ nil
29
+ end
30
+ end
31
+
32
+ # Tell if the field of the content entry has been translated
33
+ # in the current locale or not.
34
+ #
35
+ # @param [ String/Object ] field The field or the name of the field
36
+ #
37
+ # @return [ Boolean ] True if translated, false if not, nil if the field is not localized
38
+ #
39
+ def translated_field?(field_or_name)
40
+ field = field_or_name.respond_to?(:name) ? field_or_name : self.fields[field_or_name.to_s]
41
+
42
+ if field.try(:localized?)
43
+ locale = ::Mongoid::Fields::I18n.locale.to_s
44
+ (self.attributes[field.name] || {}).key?(locale)
45
+ else
46
+ nil
47
+ end
48
+ end
49
+
50
+ # Tell if the entry is localized or not, meaning if the label field
51
+ # is localized or not.
52
+ #
53
+ # @return [ Boolean ] True if localized, false otherwise
54
+ #
55
+ def localized?
56
+ self.respond_to?(:"#{self._label_field_name}_translations")
57
+ end
58
+
59
+ end
60
+ end
61
+ end
62
+ end
@@ -15,6 +15,7 @@ module Locomotive
15
15
 
16
16
  ## callbacks ##
17
17
  before_save :persist_depth
18
+ before_save :ensure_index_position
18
19
  before_destroy :delete_descendants
19
20
 
20
21
  ## indexes ##
@@ -140,6 +141,10 @@ module Locomotive
140
141
  self.depth = self.parent_ids.count
141
142
  end
142
143
 
144
+ def ensure_index_position
145
+ self.position = 0 if self.index?
146
+ end
147
+
143
148
  end
144
149
  end
145
150
  end
@@ -0,0 +1,33 @@
1
+ module Locomotive
2
+ module Extensions
3
+ module Shared
4
+ module Slug
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+
9
+ def slugify_from(field)
10
+ class_eval <<-EOV
11
+ before_validation { |object| object.send(:normalize_slug, :#{field.to_s}) }
12
+ EOV
13
+ end
14
+ end
15
+
16
+ protected
17
+
18
+ def normalize_slug(field)
19
+ value = self.send(field)
20
+
21
+ if self.slug.blank? && value.present?
22
+ self.slug = value.clone
23
+ end
24
+
25
+ if self.slug.present?
26
+ self.slug.permalink!
27
+ end
28
+ end
29
+
30
+ end
31
+ end
32
+ end
33
+ end
@@ -13,6 +13,7 @@ module Locomotive
13
13
  include Extensions::Page::Templatized
14
14
  include Extensions::Page::Redirect
15
15
  include Extensions::Page::Listed
16
+ include Extensions::Shared::Slug
16
17
  include Extensions::Shared::Seo
17
18
 
18
19
  ## fields ##
@@ -34,9 +35,11 @@ module Locomotive
34
35
  index parent_id: 1
35
36
  index fullpath: 1, site_id: 1
36
37
 
38
+ ## behaviours ##
39
+ slugify_from :title
40
+
37
41
  ## callbacks ##
38
42
  after_initialize :set_default_raw_template
39
- before_validation :normalize_slug
40
43
  before_save :build_fullpath
41
44
  before_save :record_current_locale
42
45
  before_destroy :do_not_remove_index_and_404_pages
@@ -104,16 +107,6 @@ module Locomotive
104
107
  self.errors.empty?
105
108
  end
106
109
 
107
- def normalize_slug
108
- self.slug = self.title.clone if self.slug.blank? && self.title.present?
109
- if self.slug.present?
110
- # if the slug includes one "_" at least, we consider that the "_" is used instead of "-".
111
- underscore = !self.slug.index('_').nil?
112
-
113
- self.slug.permalink!(underscore)
114
- end
115
- end
116
-
117
110
  def set_default_raw_template
118
111
  self.raw_template ||= ::I18n.t('attributes.defaults.pages.other.body')
119
112
  end
@@ -2,6 +2,7 @@ module Locomotive
2
2
  class Snippet
3
3
 
4
4
  include Locomotive::Mongoid::Document
5
+ include Extensions::Shared::Slug
5
6
 
6
7
  ## fields ##
7
8
  field :name
@@ -12,7 +13,6 @@ module Locomotive
12
13
  belongs_to :site, class_name: 'Locomotive::Site'
13
14
 
14
15
  ## callbacks ##
15
- before_validation :normalize_slug
16
16
  after_save :update_templates
17
17
  after_destroy :update_templates
18
18
 
@@ -21,6 +21,7 @@ module Locomotive
21
21
  validates_uniqueness_of :slug, scope: :site_id
22
22
 
23
23
  ## behaviours ##
24
+ slugify_from :name
24
25
  attr_protected :id
25
26
  attr_accessible :name, :slug, :template
26
27
 
@@ -28,11 +29,6 @@ module Locomotive
28
29
 
29
30
  protected
30
31
 
31
- def normalize_slug
32
- self.slug = self.name.clone if self.slug.blank? && self.name.present?
33
- self.slug.permalink!(true) if self.slug.present?
34
- end
35
-
36
32
  def update_templates
37
33
  return unless (self.site rescue false) # not run if the site is being destroyed
38
34
 
@@ -54,7 +50,7 @@ module Locomotive
54
50
  def _change_snippet_inside_template(node)
55
51
  case node
56
52
  when Locomotive::Liquid::Tags::Snippet
57
- node.refresh(self) if node.slug == self.slug
53
+ node.refresh(self, _default_context) if node.slug == self.slug
58
54
  when Locomotive::Liquid::Tags::InheritedBlock
59
55
  _change_snippet_inside_template(node.parent) if node.parent
60
56
  end
@@ -66,5 +62,8 @@ module Locomotive
66
62
  end
67
63
  end
68
64
 
65
+ def _default_context
66
+ { site: site }
67
+ end
69
68
  end
70
69
  end
@@ -29,10 +29,10 @@ class Locomotive::Translation
29
29
  #
30
30
  def underscore_key
31
31
  if self.key
32
- self.key = self.key.permalink.underscore
32
+ self.key = self.key.permalink(true)
33
33
  end
34
34
  end
35
-
35
+
36
36
  def remove_blanks
37
37
  self.values.delete_if { |k,v| v.blank? }
38
38
  end
@@ -2,7 +2,7 @@ module Locomotive
2
2
  class AccountPresenter < BasePresenter
3
3
 
4
4
  ## properties ##
5
- properties :name, :email, :locale
5
+ properties :name, :email, :locale, :encrypted_password, :password_salt
6
6
  property :admin, only_getter: true
7
7
 
8
8
  with_options only_setter: true do |presenter|
@@ -1,10 +1,12 @@
1
1
  - if entries.empty?
2
2
  %p.no-items!= t('.no_items', url: new_content_entry_path(content_type.slug))
3
3
  - else
4
- %ul{ id: 'entries-list', class: "#{'list' unless content_type.groupable?} #{'sortable' if content_type.order_manually?}", :'data-url' => sort_content_entries_path(content_type.slug, :json) }
4
+ %ul{ id: 'entries-list', class: "#{content_type.groupable? ? 'grouped' : 'list'} #{'sortable' if content_type.order_manually?}", :'data-url' => sort_content_entries_path(content_type.slug, :json) }
5
5
  - entries.each do |entry|
6
6
  %li.item{ id: "entry-#{entry._id}" }
7
7
  %span.handle
8
+ - if content_type.groupable?
9
+ %i.icon-reorder
8
10
 
9
11
  %strong= link_to entry_label(content_type, entry), edit_content_entry_path(content_type.slug, entry)
10
12
 
@@ -1,4 +1,4 @@
1
1
  = f.input name,
2
- label: field.label,
2
+ label: label_for_custom_field(f.object, field),
3
3
  hint: field.hint,
4
4
  as: :'Locomotive::Toggle'
@@ -1,5 +1,5 @@
1
1
  = f.input :"formatted_#{name}",
2
- label: field.label,
2
+ label: label_for_custom_field(f.object, field),
3
3
  hint: field.hint,
4
4
  wrapper_html: { class: 'date' },
5
5
  input_html: { maxlength: 10 }
@@ -1,5 +1,5 @@
1
1
  = f.input :"formatted_#{name}",
2
- label: field.label,
2
+ label: label_for_custom_field(f.object, field),
3
3
  hint: field.hint,
4
4
  wrapper_html: { class: 'date-time' },
5
5
  input_html: { maxlength: 10 }
@@ -1,4 +1,4 @@
1
1
  = f.input name,
2
- label: field.label,
2
+ label: label_for_custom_field(f.object, field),
3
3
  hint: field.hint,
4
4
  wrapper_html: { class: "#{'highlighted' if highlighted}" }