locomotivecms_mounter_pull_19 1.5.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +4 -0
  5. data/Gemfile +7 -0
  6. data/Gemfile.lock +131 -0
  7. data/MIT-LICENSE +20 -0
  8. data/NOTES +4 -0
  9. data/README.md +26 -0
  10. data/Rakefile +37 -0
  11. data/TODO +82 -0
  12. data/init.rb +2 -0
  13. data/lib/locomotive/mounter.rb +106 -0
  14. data/lib/locomotive/mounter/config.rb +21 -0
  15. data/lib/locomotive/mounter/engine_api.rb +205 -0
  16. data/lib/locomotive/mounter/exceptions.rb +44 -0
  17. data/lib/locomotive/mounter/extensions/compass.rb +38 -0
  18. data/lib/locomotive/mounter/extensions/httmultiparty.rb +22 -0
  19. data/lib/locomotive/mounter/extensions/sprockets.rb +46 -0
  20. data/lib/locomotive/mounter/extensions/tilt/css.rb +31 -0
  21. data/lib/locomotive/mounter/fields.rb +254 -0
  22. data/lib/locomotive/mounter/models/base.rb +42 -0
  23. data/lib/locomotive/mounter/models/content_asset.rb +84 -0
  24. data/lib/locomotive/mounter/models/content_entry.rb +372 -0
  25. data/lib/locomotive/mounter/models/content_field.rb +190 -0
  26. data/lib/locomotive/mounter/models/content_select_option.rb +29 -0
  27. data/lib/locomotive/mounter/models/content_type.rb +274 -0
  28. data/lib/locomotive/mounter/models/editable_element.rb +27 -0
  29. data/lib/locomotive/mounter/models/page.rb +442 -0
  30. data/lib/locomotive/mounter/models/site.rb +28 -0
  31. data/lib/locomotive/mounter/models/snippet.rb +55 -0
  32. data/lib/locomotive/mounter/models/theme_asset.rb +148 -0
  33. data/lib/locomotive/mounter/models/translation.rb +28 -0
  34. data/lib/locomotive/mounter/mounting_point.rb +65 -0
  35. data/lib/locomotive/mounter/reader/api.rb +64 -0
  36. data/lib/locomotive/mounter/reader/api/base.rb +67 -0
  37. data/lib/locomotive/mounter/reader/api/content_assets_reader.rb +39 -0
  38. data/lib/locomotive/mounter/reader/api/content_entries_reader.rb +142 -0
  39. data/lib/locomotive/mounter/reader/api/content_types_reader.rb +76 -0
  40. data/lib/locomotive/mounter/reader/api/pages_reader.rb +192 -0
  41. data/lib/locomotive/mounter/reader/api/site_reader.rb +42 -0
  42. data/lib/locomotive/mounter/reader/api/snippets_reader.rb +61 -0
  43. data/lib/locomotive/mounter/reader/api/theme_assets_reader.rb +42 -0
  44. data/lib/locomotive/mounter/reader/api/translations_reader.rb +30 -0
  45. data/lib/locomotive/mounter/reader/file_system.rb +43 -0
  46. data/lib/locomotive/mounter/reader/file_system/base.rb +65 -0
  47. data/lib/locomotive/mounter/reader/file_system/content_assets_reader.rb +90 -0
  48. data/lib/locomotive/mounter/reader/file_system/content_entries_reader.rb +97 -0
  49. data/lib/locomotive/mounter/reader/file_system/content_types_reader.rb +88 -0
  50. data/lib/locomotive/mounter/reader/file_system/pages_reader.rb +211 -0
  51. data/lib/locomotive/mounter/reader/file_system/site_reader.rb +27 -0
  52. data/lib/locomotive/mounter/reader/file_system/snippets_reader.rb +115 -0
  53. data/lib/locomotive/mounter/reader/file_system/theme_assets_reader.rb +83 -0
  54. data/lib/locomotive/mounter/reader/file_system/translations_reader.rb +36 -0
  55. data/lib/locomotive/mounter/reader/runner.rb +89 -0
  56. data/lib/locomotive/mounter/utils/hash.rb +31 -0
  57. data/lib/locomotive/mounter/utils/output.rb +124 -0
  58. data/lib/locomotive/mounter/utils/string.rb +40 -0
  59. data/lib/locomotive/mounter/utils/yaml.rb +125 -0
  60. data/lib/locomotive/mounter/utils/yaml_front_matters_template.rb +45 -0
  61. data/lib/locomotive/mounter/version.rb +8 -0
  62. data/lib/locomotive/mounter/writer/api.rb +74 -0
  63. data/lib/locomotive/mounter/writer/api/base.rb +172 -0
  64. data/lib/locomotive/mounter/writer/api/content_assets_writer.rb +74 -0
  65. data/lib/locomotive/mounter/writer/api/content_entries_writer.rb +227 -0
  66. data/lib/locomotive/mounter/writer/api/content_types_writer.rb +151 -0
  67. data/lib/locomotive/mounter/writer/api/pages_writer.rb +250 -0
  68. data/lib/locomotive/mounter/writer/api/site_writer.rb +125 -0
  69. data/lib/locomotive/mounter/writer/api/snippets_writer.rb +111 -0
  70. data/lib/locomotive/mounter/writer/api/theme_assets_writer.rb +201 -0
  71. data/lib/locomotive/mounter/writer/api/translations_writer.rb +85 -0
  72. data/lib/locomotive/mounter/writer/file_system.rb +44 -0
  73. data/lib/locomotive/mounter/writer/file_system/base.rb +70 -0
  74. data/lib/locomotive/mounter/writer/file_system/content_assets_writer.rb +38 -0
  75. data/lib/locomotive/mounter/writer/file_system/content_entries_writer.rb +33 -0
  76. data/lib/locomotive/mounter/writer/file_system/content_types_writer.rb +32 -0
  77. data/lib/locomotive/mounter/writer/file_system/pages_writer.rb +93 -0
  78. data/lib/locomotive/mounter/writer/file_system/site_writer.rb +30 -0
  79. data/lib/locomotive/mounter/writer/file_system/snippets_writer.rb +59 -0
  80. data/lib/locomotive/mounter/writer/file_system/theme_assets_writer.rb +72 -0
  81. data/lib/locomotive/mounter/writer/file_system/translations_writer.rb +29 -0
  82. data/lib/locomotive/mounter/writer/runner.rb +73 -0
  83. data/locomotivecms_mounter.gemspec +64 -0
  84. metadata +539 -0
@@ -0,0 +1,42 @@
1
+ module Locomotive
2
+ module Mounter
3
+ module Models
4
+
5
+ class Base
6
+
7
+ include Locomotive::Mounter::Fields
8
+
9
+ attr_accessor :_id, :mounting_point, :created_at, :updated_at
10
+
11
+ ## methods ##
12
+
13
+ def initialize(attributes = {})
14
+ self.created_at = self.updated_at = Time.now
15
+ self.mounting_point = attributes.delete(:mounting_point)
16
+ super
17
+ end
18
+
19
+ def persisted?
20
+ !self._id.blank?
21
+ end
22
+
23
+ protected
24
+
25
+ # Take a list of field names and return a hash with
26
+ # their values only if they are not nil.
27
+ #
28
+ # @param [ Array ] fields List of field names (string)
29
+ #
30
+ # @return [ Hash ] A hash with symbolize keys
31
+ #
32
+ def filter_attributes(fields)
33
+ self.attributes.clone.delete_if do |k, v|
34
+ !fields.include?(k.to_s) || (!v.is_a?(FalseClass) && v.blank?)
35
+ end.deep_symbolize_keys
36
+ end
37
+
38
+ end
39
+
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,84 @@
1
+ module Locomotive
2
+ module Mounter
3
+ module Models
4
+
5
+ class ContentAsset < Base
6
+
7
+ ## fields ##
8
+ field :source
9
+
10
+ ## other accessors ##
11
+ attr_accessor :folder, :filepath, :uri
12
+
13
+ ## methods ##
14
+
15
+ # Name of the file
16
+ #
17
+ # @return [ String ] Name of the file
18
+ #
19
+ def filename
20
+ return @filename if @filename
21
+
22
+ if self.uri
23
+ @filename = File.basename(self.uri.path)
24
+ else
25
+ @filename = File.basename(self.filepath)
26
+ end
27
+ end
28
+
29
+ # Content of the asset.
30
+ #
31
+ # @return [ String ] The content of the asset
32
+ #
33
+ def content
34
+ return @raw if @raw
35
+
36
+ if self.uri
37
+ @raw = HTTParty.get(self.uri.to_s).body
38
+ else
39
+ @raw = File.read(self.filepath)
40
+ end
41
+ end
42
+
43
+ def size
44
+ if self.uri
45
+ self.content.size
46
+ else
47
+ File.size(self.filepath)
48
+ end
49
+ end
50
+
51
+ # Return true if the uri or the file exists.
52
+ #
53
+ # @return [ Boolean ] True if it exists
54
+ #
55
+ def exists?
56
+ self.size
57
+ true
58
+ rescue
59
+ false
60
+ end
61
+
62
+ def local_filepath
63
+ File.join('/', self.folder, self.filename)
64
+ end
65
+
66
+ # Return the params used for the API.
67
+ #
68
+ # @return [ Hash ] The params
69
+ #
70
+ def to_params
71
+ return {} if self.uri
72
+
73
+ { source: File.new(self.filepath) }
74
+ end
75
+
76
+ def to_s
77
+ self.uri ? self.uri.path : self.filename
78
+ end
79
+
80
+ end
81
+
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,372 @@
1
+ module Locomotive
2
+ module Mounter
3
+ module Models
4
+
5
+ class ContentEntry < Base
6
+
7
+ ## fields ##
8
+ field :_slug, localized: true
9
+ field :_position, default: 0
10
+ field :_visible, default: true
11
+ field :seo_title, localized: true
12
+ field :meta_keywords, localized: true
13
+ field :meta_description, localized: true
14
+
15
+ field :content_type, association: true
16
+
17
+ attr_accessor :dynamic_attributes, :main_locale, :errors
18
+
19
+ alias :_permalink :_slug
20
+ alias :_permalink= :_slug=
21
+
22
+ ## callbacks ##
23
+ set_callback :initialize, :after, :set_default_main_locale
24
+ set_callback :initialize, :after, :set_default_dynamic_attributes
25
+
26
+ ## methods ##
27
+
28
+ # By definition, if the label field defined in the content type is
29
+ # localized, then the content entry will be considered as localized.
30
+ #
31
+ # @return [ Boolean ] True if the label field is localized.
32
+ #
33
+ def localized?
34
+ field = self.content_type.label_field
35
+ !!field.try(:localized)
36
+ end
37
+
38
+ # Return the internal label used to identify a content entry
39
+ # in a YAML file for instance. It is based on the first field
40
+ # of the related content type.
41
+ #
42
+ # @return [ String ] The internal label
43
+ #
44
+ def _label
45
+ field = self.content_type.label_field
46
+ value = self.dynamic_getter(field.name)
47
+
48
+ if field.type == :belongs_to
49
+ value.try(:_label)
50
+ elsif field.type == :file
51
+ value['url']
52
+ else
53
+ value
54
+ end
55
+ end
56
+
57
+ # Process a minimal validation by checking if the required fields
58
+ # are filled in or not.
59
+ #
60
+ # @return [ Boolean ] False if one of the required fields is missing.
61
+ #
62
+ def valid?
63
+ self.errors = []
64
+ self.content_type.fields.each do |field|
65
+ if field.required
66
+ if self.dynamic_getter(field.name).blank?
67
+ self.errors << field.name
68
+ end
69
+ end
70
+ end
71
+ self.errors.blank?
72
+ end
73
+
74
+ # Return the list of the fields defined in the content type
75
+ # for which there is a value assigned.
76
+ #
77
+ # @return [ Array ] The list of fields
78
+ #
79
+ def dynamic_fields
80
+ self.dynamic_attributes.keys.map do |name|
81
+ self.content_type.find_field(name)
82
+ end
83
+ end
84
+
85
+ # Loop over the list of dynamic fields defined in
86
+ # the content type for which there is a value assigned.
87
+ #
88
+ # @example: each_dynamic_field { |field, value| .... }
89
+ #
90
+ def each_dynamic_field(&block)
91
+ return unless block_given?
92
+
93
+ self.dynamic_fields.each do |field|
94
+ value = self.localized_dynamic_attribute_value(field)
95
+ block.call(field, value)
96
+ end
97
+ end
98
+
99
+ # Determine if field passed in parameter is one of the dynamic fields.
100
+ #
101
+ # @param [ String/Symbol ] name Name of the dynamic field
102
+ #
103
+ # @return [ Boolean ] True if it is a dynamic field
104
+ #
105
+ def is_dynamic_field?(name)
106
+ name = name.to_s.gsub(/\=$/, '').to_sym
107
+ !self.content_type.try(:find_field, name).nil?
108
+ end
109
+
110
+ # Return the value of a dynamic field and cast it depending
111
+ # on the type of the field (string, date, belongs_to, ...etc).
112
+ #
113
+ # @param [ String/Symbol ] name Name of the dynamic field
114
+ #
115
+ # @return [ Object ] The casted value (String, Date, ContentEntry, ...etc)
116
+ #
117
+ def dynamic_getter(name)
118
+ field = self.content_type.find_field(name)
119
+ value = self.localized_dynamic_attribute_value(field)
120
+
121
+ case field.type
122
+ when :date, :date_time
123
+ value.is_a?(String) ? Chronic.parse(value) : value
124
+ when :file
125
+ value.present? ? { 'url' => value } : nil
126
+ when :belongs_to
127
+ field.klass.find_entry(value)
128
+ when :has_many
129
+ field.klass.find_entries_by(field.inverse_of, [self._label, self._permalink])
130
+ when :many_to_many
131
+ field.klass.find_entries_among(value)
132
+ else
133
+ # :string, :text, :select, :boolean, :email, :integer, :float, :tags
134
+ value
135
+ end
136
+ end
137
+
138
+ # Set the value of a dynamic field. If the value is a hash,
139
+ # it assumes that it represents the translations.
140
+ #
141
+ # @param [ String/Symbol ] name Name of the dynamic field
142
+ # @param [ Object ] value Value to set
143
+ #
144
+ def dynamic_setter(name, value)
145
+ self.dynamic_attributes ||= {}
146
+ self.dynamic_attributes[name.to_sym] ||= {}
147
+
148
+ field = self.content_type.find_field(name)
149
+
150
+ if value.is_a?(Hash) # already localized
151
+ value.keys.each { |locale| self.add_locale(locale) }
152
+ self.dynamic_attributes[name.to_sym].merge!(value.symbolize_keys)
153
+ else
154
+ if field.is_relationship? || !field.localized
155
+ self.dynamic_attributes[name.to_sym] = value
156
+ else
157
+ self.add_locale(Locomotive::Mounter.locale)
158
+ self.dynamic_attributes[name.to_sym][Locomotive::Mounter.locale] = value
159
+ end
160
+ end
161
+ end
162
+
163
+ # We also have to deal with dynamic attributes so that
164
+ # it does not raise an exception when calling the attributes=
165
+ # method.
166
+ #
167
+ # @param [ Hash ] attributes The new attributes
168
+ #
169
+ def write_attributes(attributes)
170
+ _attributes = attributes.select do |name, value|
171
+ if self.is_dynamic_field?(name)
172
+ self.dynamic_setter(name, value)
173
+ false
174
+ else
175
+ true
176
+ end
177
+ end
178
+
179
+ super(_attributes)
180
+ end
181
+
182
+ alias :attributes= :write_attributes
183
+
184
+ def [](name)
185
+ if is_dynamic_field?(name)
186
+ self.dynamic_getter(name.to_sym)
187
+ else
188
+ super
189
+ end
190
+ end
191
+
192
+ # The magic of dynamic fields happens within this method.
193
+ # It calls the getter/setter of a dynamic field if it is one of them.
194
+ def method_missing(name, *args, &block)
195
+ if self.is_dynamic_field?(name)
196
+ if name.to_s.ends_with?('=')
197
+ name = name.to_s.gsub(/\=$/, '').to_sym
198
+ self.dynamic_setter(name, args.first)
199
+ else
200
+ self.dynamic_getter(name)
201
+ end
202
+ else
203
+ super
204
+ end
205
+ end
206
+
207
+ # Return a hash with the label_field value as the key and the other fields as the value
208
+ #
209
+ # @param [ Boolean ] nested True to have a hash of hash (whose key is the label)
210
+ #
211
+ # @return [ Hash ] A simple hash (nested to false) or a hash of hash
212
+ #
213
+ def to_hash(nested = true)
214
+ # no need of _position and _visible (unless it's false)
215
+ hash = super.delete_if { |k, v| k == '_position' || (k == '_visible' && v == true) }
216
+
217
+ # also no need of the content type
218
+ hash.delete('content_type')
219
+
220
+ # dynamic attributes
221
+ hash.merge!(self.dynamic_attributes.deep_stringify_keys)
222
+
223
+ # no need of the translation of the field name in the current locale
224
+ label_field = self.content_type.label_field
225
+
226
+ if label_field.localized
227
+ if !hash[label_field.name].empty?
228
+ hash[label_field.name].delete(Locomotive::Mounter.locale.to_s)
229
+
230
+ hash.delete(label_field.name) if hash[label_field.name].empty?
231
+ end
232
+ else
233
+ hash.delete(label_field.name)
234
+ end
235
+
236
+ nested ? { self._label => hash } : hash
237
+ end
238
+
239
+ # Return the main default params used for the API, meaning all except
240
+ # the dynamic fields which have to be defined outside the model.
241
+ #
242
+ # @return [ Hash ] The params
243
+ #
244
+ def to_params
245
+ self.filter_attributes %w(_slug _position _visible seo_title meta_keywords meta_description)
246
+ end
247
+
248
+ def to_s
249
+ "#{self.content_type.slug} / #{self._slug}"
250
+ end
251
+
252
+ protected
253
+
254
+ # Sets the slug of the instance by using the value of the highlighted field
255
+ # (if available). If a sibling content instance has the same permalink then a
256
+ # unique one will be generated.
257
+ # It applies that to every translated version of the content entry.
258
+ def set_slug
259
+ self.translated_in.each do |locale|
260
+ Locomotive::Mounter.with_locale(locale) do
261
+ # first attempt from the label
262
+ if self._slug.blank?
263
+ self._slug = self._label.try(:dup)
264
+ end
265
+
266
+ # from the content type itself
267
+ if self._slug.blank?
268
+ self._slug = self.content_type.send(:label_to_slug)
269
+ end
270
+
271
+ self._slug.permalink!
272
+
273
+ self._slug = self.next_unique_slug if self.slug_already_taken?
274
+ end
275
+ end
276
+
277
+ self.fill_with_default_slug
278
+ end
279
+
280
+ # In case the content entry is not localized, we need to make sure
281
+ # it has an non empty slug for each locale of the site.
282
+ #
283
+ def fill_with_default_slug
284
+ return if self.localized?
285
+
286
+ # we do not want to add a new translation because the content entry
287
+ # is not truly "localized".
288
+ __locales = self._locales.dup
289
+
290
+ default_slug = self._slug_translations[self.mounting_point.default_locale]
291
+
292
+ self.mounting_point.locales.each do |locale|
293
+ Locomotive::Mounter.with_locale(locale) do
294
+ self._slug = default_slug if self._slug.blank?
295
+ end
296
+ end
297
+
298
+ self._locales = __locales
299
+ end
300
+
301
+ # Once the entry has been initialized, we keep track of the current locale
302
+ #
303
+ def set_default_main_locale
304
+ self.main_locale = self.content_type.mounting_point.default_locale
305
+ end
306
+
307
+ def set_default_dynamic_attributes
308
+ self.dynamic_attributes ||= {}
309
+ end
310
+
311
+ # Return the next available unique slug as a string
312
+ #
313
+ # @return [ String] An unique permalink (or slug)
314
+ #
315
+ def next_unique_slug
316
+ slug = self._slug.gsub(/-\d*$/, '')
317
+ next_number = 0
318
+
319
+ self.content_type.entries.each do |entry|
320
+ if entry._permalink =~ /^#{slug}-?(\d*)$/i
321
+ next_number = $1.to_i if $1.to_i > next_number
322
+ end
323
+ end
324
+
325
+ [slug, next_number + 1].join('-')
326
+ end
327
+
328
+ def slug_already_taken?
329
+ entry = self.content_type.find_entry(self._slug)
330
+ entry.try(:_slug) == self._slug
331
+ end
332
+
333
+ # Return the value of a dynamic attribute specified by its
334
+ # corresponding content field.
335
+ # If that attribute is localized and in the current locale
336
+ # its value is nil, it returns the value in the main locale.
337
+ #
338
+ # @param [ String / Object ] The content field or the name of the field
339
+ #
340
+ # @return [ Object ] The value
341
+ #
342
+ def localized_dynamic_attribute_value(field)
343
+ if field.is_a?(String)
344
+ field = self.content_type.find_field(field)
345
+ end
346
+
347
+ return nil if field.nil?
348
+
349
+ value = (self.dynamic_attributes || {})[field.name.to_sym]
350
+
351
+ # DEBUG puts "[#{field.name.inspect}] #{value.inspect} / #{field.localized.inspect} / #{value.is_a?(Hash).inspect}"
352
+
353
+ if !field.is_relationship? && field.localized && value.is_a?(Hash)
354
+ # get the localized value for the current locale
355
+ _value = value[Locomotive::Mounter.locale]
356
+
357
+ # no value for the current locale, give a try to the main one
358
+ if _value.nil? && Locomotive::Mounter.locale != self.main_locale
359
+ _value = value[self.main_locale]
360
+ end
361
+
362
+ value = _value
363
+ end
364
+
365
+ value # DEBUG .tap { |v| puts "[#{field.name}] returning #{v.inspect}" }
366
+ end
367
+
368
+ end
369
+
370
+ end
371
+ end
372
+ end