locomotive_cms 2.0.0.rc1 → 2.0.0.rc2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. data/Gemfile +1 -1
  2. data/LICENSE +1 -1
  3. data/app/assets/javascripts/locomotive/models/editable_element.js.coffee +5 -0
  4. data/app/assets/stylesheets/locomotive/backoffice/application.css.scss +12 -0
  5. data/app/assets/stylesheets/locomotive/backoffice/formtastic_changes.css.scss +5 -3
  6. data/app/assets/stylesheets/locomotive/shared/_helpers.css.scss +15 -0
  7. data/app/controllers/locomotive/api/content_entries_controller.rb +6 -1
  8. data/app/helpers/locomotive/content_types_helper.rb +2 -2
  9. data/app/helpers/locomotive/custom_fields_helper.rb +1 -1
  10. data/app/models/locomotive/content_entry.rb +12 -2
  11. data/app/models/locomotive/content_type.rb +32 -83
  12. data/app/models/locomotive/editable_file.rb +2 -2
  13. data/app/models/locomotive/extensions/content_type/default_values.rb +59 -0
  14. data/app/models/locomotive/extensions/content_type/sync.rb +62 -0
  15. data/app/models/locomotive/extensions/page/tree.rb +4 -4
  16. data/app/models/locomotive/extensions/site/locales.rb +0 -1
  17. data/app/models/locomotive/page.rb +6 -2
  18. data/app/models/locomotive/snippet.rb +3 -1
  19. data/app/presenters/locomotive/content_entry_presenter.rb +14 -9
  20. data/app/views/locomotive/content_entries/_list.html.haml +4 -0
  21. data/app/views/locomotive/pages/_page.html.haml +3 -0
  22. data/config/locales/admin_ui.de.yml +98 -100
  23. data/config/locales/admin_ui.en.yml +2 -0
  24. data/config/locales/admin_ui.fr.yml +2 -0
  25. data/config/locales/default.es.yml +2 -2
  26. data/lib/generators/locomotive/install/templates/carrierwave.rb +8 -5
  27. data/lib/locomotive.rb +0 -2
  28. data/lib/locomotive/carrierwave/base.rb +5 -5
  29. data/lib/locomotive/configuration.rb +1 -1
  30. data/lib/locomotive/custom_fields.rb +1 -1
  31. data/lib/locomotive/dependencies.rb +0 -2
  32. data/lib/locomotive/engine.rb +0 -2
  33. data/lib/locomotive/liquid/drops/content_entry.rb +19 -2
  34. data/lib/locomotive/liquid/drops/page.rb +4 -0
  35. data/lib/locomotive/liquid/drops/uploader.rb +15 -0
  36. data/lib/locomotive/liquid/tags/nav.rb +18 -5
  37. data/lib/locomotive/liquid/tags/with_scope.rb +17 -9
  38. data/lib/locomotive/routing/site_dispatcher.rb +1 -1
  39. data/lib/locomotive/version.rb +1 -1
  40. data/lib/tasks/locomotive.rake +7 -185
  41. metadata +71 -68
data/Gemfile CHANGED
@@ -33,7 +33,7 @@ group :test do
33
33
 
34
34
  # gem 'growl-glue'
35
35
 
36
- gem 'cucumber-rails'
36
+ gem 'cucumber-rails', :require => false
37
37
  gem 'rspec-rails', '~> 2.8.0'
38
38
  gem 'shoulda-matchers'
39
39
 
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  == MIT License
2
2
 
3
- Copyright (c) 2010, Didier Lafforgue.
3
+ Copyright (c) 2010-2012, Didier Lafforgue.
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -5,6 +5,11 @@ class Locomotive.Models.EditableElement extends Backbone.Model
5
5
  for key, value of @.toJSON()
6
6
  hash[key] = value if _.include(['id', 'source', 'content', 'remove_source'], key)
7
7
 
8
+ if @get('type') == 'EditableFile'
9
+ delete hash['content']
10
+ else
11
+ delete hash['source']
12
+
8
13
  class Locomotive.Models.EditableElementsCollection extends Backbone.Collection
9
14
 
10
15
  model: Locomotive.Models.EditableElement
@@ -78,6 +78,12 @@ ul.list {
78
78
  outline: none;
79
79
  }
80
80
 
81
+ span.untranslated {
82
+ @include label;
83
+ top: -1px;
84
+ left: 5px;
85
+ }
86
+
81
87
  div.more {
82
88
  position: absolute;
83
89
  top: 0px;
@@ -212,6 +218,12 @@ ul.list {
212
218
 
213
219
  &.hidden a { font-style: italic; font-weight: normal; }
214
220
 
221
+ span.untranslated {
222
+ @include label;
223
+ top: 3px;
224
+ left: 5px;
225
+ }
226
+
215
227
  .more {
216
228
  position: absolute;
217
229
  top: 0px;
@@ -568,9 +568,11 @@ form.formtastic {
568
568
  }
569
569
 
570
570
  &.label {
571
- margin-left: 8px;
572
- font-weight: bold;
573
- color: #000;
571
+ margin-left: 8px;
572
+ font-weight: bold;
573
+ color: #000;
574
+ height: 31px;
575
+ width: auto;
574
576
  }
575
577
 
576
578
  } // ul .col
@@ -55,3 +55,18 @@
55
55
  border-bottom: 1px dotted #efe4a5;
56
56
  }
57
57
  }
58
+
59
+ @mixin label {
60
+ display: inline-block;
61
+ position: relative;
62
+
63
+ background-color: #858585;
64
+ @include border-radius(3px);
65
+ padding: 1px 3px 2px;
66
+
67
+ line-height: 16px;
68
+ font-size: 11px;
69
+ font-weight: bold;
70
+ color: #fff;
71
+ @include single-text-shadow(rgba(0, 0, 0, 0.6), 0px, -1px, 0px);
72
+ }
@@ -5,10 +5,15 @@ module Locomotive
5
5
  before_filter :set_content_type
6
6
 
7
7
  def index
8
- @content_entries = @content_type.list_or_group_entries
8
+ @content_entries = @content_type.ordered_entries
9
9
  respond_with @content_entries
10
10
  end
11
11
 
12
+ def show
13
+ @content_entry = @content_type.entries.any_of({ :_id => params[:id] }, { :_slug => params[:id] }).first
14
+ respond_with @content_entry, :status => @content_entry ? :ok : :not_found
15
+ end
16
+
12
17
  def create
13
18
  @content_entry = @content_type.entries.create(params[:content_entry])
14
19
  respond_with @content_entry, :location => main_app.locomotive_api_content_entries_url(@content_type.slug)
@@ -16,9 +16,9 @@ module Locomotive
16
16
  current_site.content_types.ordered.only(:site_id, :name, :slug, :label_field_name).each_with_index do |content_type, index|
17
17
  next if !content_type.persisted?
18
18
 
19
- if index >= Locomotive.config.ui.max_content_types
19
+ if index >= Locomotive.config.ui[:max_content_types]
20
20
  if self.is_content_type_selected(content_type)
21
- others << visible.delete_at(Locomotive.config.ui.max_content_types - 1) # swap content types
21
+ others << visible.delete_at(Locomotive.config.ui[:max_content_types] - 1) # swap content types
22
22
  visible.insert(0, content_type)
23
23
  else
24
24
  others << content_type # fills the "..." menu
@@ -17,7 +17,7 @@ module Locomotive
17
17
 
18
18
  def options_for_group_by_field(content_type)
19
19
  content_type.ordered_entries_custom_fields.find_all do |field|
20
- %w(select).include?(field.type)
20
+ %w(select belongs_to).include?(field.type)
21
21
  end.map do |field|
22
22
  [field.label, field._id]
23
23
  end
@@ -30,7 +30,7 @@ module Locomotive
30
30
 
31
31
  ## named scopes ##
32
32
  scope :visible, :where => { :_visible => true }
33
- scope :latest_updated, :order_by => :updated_at.desc, :limit => Locomotive.config.ui.latest_entries_nb
33
+ scope :latest_updated, :order_by => :updated_at.desc, :limit => Locomotive.config.ui[:latest_entries_nb]
34
34
 
35
35
  ## methods ##
36
36
 
@@ -39,11 +39,21 @@ module Locomotive
39
39
  alias :_permalink= :_slug=
40
40
 
41
41
  def _label(type = nil)
42
- if self._label_field_name
42
+ value = if self._label_field_name
43
43
  self.send(self._label_field_name.to_sym)
44
44
  else
45
45
  self.send((type || self.content_type).label_field_name.to_sym)
46
46
  end
47
+
48
+ value.respond_to?(:to_label) ? value.to_label : value
49
+ end
50
+
51
+ def translated?
52
+ if self.respond_to?(:"#{self._label_field_name}_translations")
53
+ self.send(:"#{self._label_field_name}_translations").key?(::Mongoid::Fields::I18n.locale.to_s) #rescue false
54
+ else
55
+ true
56
+ end
47
57
  end
48
58
 
49
59
  def next
@@ -5,7 +5,9 @@ module Locomotive
5
5
 
6
6
  ## extensions ##
7
7
  include CustomFields::Source
8
+ include Extensions::ContentType::DefaultValues
8
9
  include Extensions::ContentType::ItemTemplate
10
+ include Extensions::ContentType::Sync
9
11
 
10
12
  ## fields ##
11
13
  field :name
@@ -32,7 +34,6 @@ module Locomotive
32
34
  ## callbacks ##
33
35
  before_validation :normalize_slug
34
36
  after_validation :bubble_fields_errors_up
35
- before_save :set_default_values
36
37
  before_update :update_label_field_name_in_entries
37
38
 
38
39
  ## validations ##
@@ -55,11 +56,12 @@ module Locomotive
55
56
  end
56
57
 
57
58
  def ordered_entries(conditions = {})
58
- self.entries.order_by([order_by_definition]).where(conditions)
59
+ _order_by_definition = (conditions || {}).delete(:order_by).try(:split) || self.order_by_definition
60
+ self.entries.order_by([_order_by_definition]).where(conditions)
59
61
  end
60
62
 
61
63
  def groupable?
62
- !!self.group_by_field && group_by_field.type == 'select'
64
+ !!self.group_by_field && %w(select belongs_to).include?(group_by_field.type)
63
65
  end
64
66
 
65
67
  def group_by_field
@@ -68,7 +70,11 @@ module Locomotive
68
70
 
69
71
  def list_or_group_entries
70
72
  if self.groupable?
71
- self.entries.group_by_select_option(self.group_by_field.name, self.order_by_definition)
73
+ if group_by_field.type == 'select'
74
+ self.entries.group_by_select_option(self.group_by_field.name, self.order_by_definition)
75
+ else
76
+ group_by_belongs_to_field(self.group_by_field)
77
+ end
72
78
  else
73
79
  self.ordered_entries
74
80
  end
@@ -110,30 +116,33 @@ module Locomotive
110
116
 
111
117
  protected
112
118
 
119
+ def group_by_belongs_to_field(field)
120
+ grouped_entries = self.ordered_entries.group_by(&:"#{field.name}_id")
121
+ columns = grouped_entries.keys
122
+ target_content_type = self.class_name_to_content_type(field.class_name)
123
+ all_columns = target_content_type.ordered_entries
124
+
125
+ all_columns.map do |column|
126
+ if columns.include?(column._id)
127
+ {
128
+ :name => column._label(target_content_type),
129
+ :entries => grouped_entries.delete(column._id)
130
+ }
131
+ else
132
+ nil
133
+ end
134
+ end.compact.tap do |groups|
135
+ unless grouped_entries.empty? # "orphans" ?
136
+ groups << { :name => nil, :entries => grouped_entries.values.flatten }
137
+ end
138
+ end
139
+ end
140
+
113
141
  def order_by_attribute
114
142
  return self.order_by if %w(created_at updated_at _position).include?(self.order_by)
115
143
  self.entries_custom_fields.find(self.order_by).name rescue 'created_at'
116
144
  end
117
145
 
118
- def set_default_values
119
- self.order_by ||= 'created_at'
120
-
121
- if @new_label_field_name.present?
122
- self.label_field_id = self.entries_custom_fields.detect { |f| f.name == @new_label_field_name.underscore }._id
123
- end
124
-
125
- if self.label_field_id.blank?
126
- self.label_field_id = self.entries_custom_fields.first._id
127
- end
128
-
129
- field = self.entries_custom_fields.find(self.label_field_id)
130
-
131
- # the label field should always be required
132
- field.required = true
133
-
134
- self.label_field_name = field.name
135
- end
136
-
137
146
  def normalize_slug
138
147
  self.slug = self.name.clone if self.slug.blank? && self.name.present?
139
148
  self.slug.permalink! if self.slug.present?
@@ -179,63 +188,3 @@ module Locomotive
179
188
  end
180
189
  end
181
190
 
182
- # def list_or_group_contents
183
- # if self.groupable?
184
- # groups = self.contents.klass.send(:"group_by_#{self.group_by_field._alias}", :ordered_contents)
185
- #
186
- # # look for items with no category or unknown ones
187
- # items_without_category = self.contents.find_all { |c| !self.group_by_field.category_ids.include?(c.send(self.group_by_field_name)) }
188
- # if not items_without_category.empty?
189
- # groups << { :name => nil, :items => items_without_category }
190
- # else
191
- # groups
192
- # end
193
- # else
194
- # self.ordered_contents
195
- # end
196
- # end
197
- #
198
- # def latest_updated_contents
199
- # self.contents.latest_updated.reject { |c| !c.persisted? }
200
- # end
201
- #
202
- # def ordered_contents(conditions = {})
203
- # column = self.order_by.to_sym
204
- #
205
- # list = (if conditions.nil? || conditions.empty?
206
- # self.contents
207
- # else
208
- # conditions_with_names = {}
209
- #
210
- # conditions.each do |key, value|
211
- # # convert alias (key) to name
212
- # field = self.entries_custom_fields.detect { |f| f._alias == key }
213
- #
214
- # case field.kind.to_sym
215
- # when :category
216
- # if (category_item = field.category_items.where(:name => value).first).present?
217
- # conditions_with_names[field._name.to_sym] = category_item._id
218
- # end
219
- # else
220
- # conditions_with_names[field._name.to_sym] = value
221
- # end
222
- # end
223
- #
224
- # self.contents.where(conditions_with_names)
225
- # end).sort { |a, b| (a.send(column) && b.send(column)) ? (a.send(column) || 0) <=> (b.send(column) || 0) : 0 }
226
- #
227
- # return list if self.order_manually?
228
- #
229
- # self.asc_order? ? list : list.reverse
230
- # end
231
- #
232
- # def sort_contents!(ids)
233
- # ids.each_with_index do |id, position|
234
- # self.contents.find(BSON::ObjectId(id))._position_in_list = position
235
- # end
236
- # self.save
237
- # end
238
- #
239
- # def group_by_field
240
- # @group_by_field ||= self.entries_custom_fields.detect { |f| f._name == self.group_by_field_name }
241
- # end
@@ -1,9 +1,9 @@
1
1
  module Locomotive
2
2
  class EditableFile < EditableElement
3
3
 
4
- mount_uploader :source, EditableFileUploader
4
+ mount_uploader 'source', EditableFileUploader
5
5
 
6
- replace_field :source, ::String, true
6
+ replace_field 'source', ::String, true
7
7
 
8
8
  def content
9
9
  self.source? ? self.source.url : self.default_content
@@ -0,0 +1,59 @@
1
+ module Locomotive
2
+ module Extensions
3
+ module ContentType
4
+ module DefaultValues
5
+
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ before_save :set_order_by
10
+ before_save :set_label_field
11
+ before_save :set_default_order_by_for_has_many_fields
12
+ end
13
+
14
+ protected
15
+
16
+ def set_order_by
17
+ unless self.order_by.nil? || %w(created_at updated_at _position).include?(self.order_by)
18
+ field = self.entries_custom_fields.where(:name => self.order_by).first || self.entries_custom_fields.find(self.order_by)
19
+
20
+ if field
21
+ self.order_by = field._id
22
+ end
23
+ end
24
+
25
+ self.order_by ||= 'created_at'
26
+ end
27
+
28
+ def set_label_field
29
+ if @new_label_field_name.present?
30
+ self.label_field_id = self.entries_custom_fields.detect { |f| f.name == @new_label_field_name.underscore }._id
31
+ end
32
+
33
+ # unknown label_field_name, get the first one instead
34
+ if self.label_field_id.blank?
35
+ self.label_field_id = self.entries_custom_fields.first._id
36
+ end
37
+
38
+ field = self.entries_custom_fields.find(self.label_field_id)
39
+
40
+ # the label field should always be required
41
+ field.required = true
42
+
43
+ self.label_field_name = field.name
44
+ end
45
+
46
+ def set_default_order_by_for_has_many_fields
47
+ self.entries_custom_fields.where(:type.in => %w(has_many many_to_many)).each do |field|
48
+ if field.ui_enabled?
49
+ field.order_by = nil
50
+ else
51
+ field.order_by = field.class_name_to_content_type.order_by_definition
52
+ end
53
+ end
54
+ end
55
+
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,62 @@
1
+ module Locomotive
2
+ module Extensions
3
+ module ContentType
4
+ module Sync
5
+
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ after_save :sync_relationships_order_by
10
+ end
11
+
12
+ protected
13
+
14
+ # If the user changes the order of the content type, we have to make
15
+ # sure that other related content types tied to the current one through
16
+ # a belongs_to / has_many relationship also gets updated.
17
+ #
18
+ def sync_relationships_order_by
19
+ current_class_name = self.klass_with_custom_fields(:entries).name
20
+
21
+ self.entries_custom_fields.where(:type => 'belongs_to').each do |field|
22
+ target_content_type = self.class_name_to_content_type(field.class_name)
23
+
24
+ operations = { '$set' => {} }
25
+
26
+ target_content_type.entries_custom_fields.where(:type.in => %w(has_many many_to_many), :ui_enabled => false, :class_name => current_class_name).each do |target_field|
27
+ if target_field.order_by != self.order_by_definition
28
+ target_field.order_by = self.order_by_definition # needed by the custom_fields_recipe_for method in order to be up to date
29
+
30
+ operations['$set']["entries_custom_fields.#{target_field._index}.order_by"] = self.order_by_definition
31
+ end
32
+ end
33
+
34
+ unless operations['$set'].empty?
35
+ persist_content_type_changes target_content_type, operations
36
+ end
37
+ end
38
+ end
39
+
40
+ # Save the changes for the content type passed in parameter without forgetting
41
+ # to bump the version.. It also updates the recipe for related entries.
42
+ # That method does not call the Mongoid API but directly MongoDB.
43
+ #
44
+ # @param [ ContentType ] content_type The content type to update
45
+ # @param [ Hash ] operations The MongoDB atomic operations
46
+ #
47
+ def persist_content_type_changes(content_type, operations)
48
+ content_type.entries_custom_fields_version += 1
49
+
50
+ operations['$set']['entries_custom_fields_version'] = content_type.entries_custom_fields_version
51
+
52
+ self.collection.update({ '_id' => content_type._id }, operations)
53
+
54
+ collection, selector = content_type.entries.collection, content_type.entries.criteria.selector
55
+
56
+ collection.update selector, { '$set' => { 'custom_fields_recipe' => content_type.custom_fields_recipe_for(:entries) } }, :multi => true
57
+ end
58
+
59
+ end
60
+ end
61
+ end
62
+ end