alchemy_cms 4.0.0.rc2 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +1 -1
  3. data/.travis.yml +2 -2
  4. data/CHANGELOG.md +18 -0
  5. data/Gemfile +2 -1
  6. data/README.md +4 -11
  7. data/alchemy_cms.gemspec +3 -3
  8. data/app/assets/stylesheets/alchemy/_extends.scss +1 -2
  9. data/app/assets/stylesheets/alchemy/_mixins.scss +0 -8
  10. data/app/assets/stylesheets/alchemy/_variables.scss +2 -0
  11. data/app/assets/stylesheets/alchemy/archive.scss +23 -52
  12. data/app/assets/stylesheets/alchemy/base.scss +10 -8
  13. data/app/assets/stylesheets/alchemy/buttons.scss +3 -4
  14. data/app/assets/stylesheets/alchemy/dashboard.scss +0 -2
  15. data/app/assets/stylesheets/alchemy/dialogs.scss +13 -14
  16. data/app/assets/stylesheets/alchemy/elements.scss +41 -51
  17. data/app/assets/stylesheets/alchemy/forms.scss +0 -3
  18. data/app/assets/stylesheets/alchemy/frame.scss +0 -3
  19. data/app/assets/stylesheets/alchemy/image_library.scss +0 -3
  20. data/app/assets/stylesheets/alchemy/jquery.datetimepicker.scss +0 -2
  21. data/app/assets/stylesheets/alchemy/lists.scss +1 -0
  22. data/app/assets/stylesheets/alchemy/navigation.scss +1 -4
  23. data/app/assets/stylesheets/alchemy/pagination.scss +1 -3
  24. data/app/assets/stylesheets/alchemy/selects.scss +2 -2
  25. data/app/assets/stylesheets/alchemy/sitemap.scss +1 -1
  26. data/app/assets/stylesheets/alchemy/upload.scss +0 -1
  27. data/app/assets/stylesheets/tinymce/skins/alchemy/skin.min.css.scss +0 -2
  28. data/app/helpers/alchemy/elements_helper.rb +12 -2
  29. data/app/models/alchemy/essence_picture.rb +28 -10
  30. data/app/models/alchemy/essence_picture_view.rb +1 -1
  31. data/app/models/alchemy/picture/transformations.rb +2 -3
  32. data/app/models/alchemy/picture/url.rb +10 -2
  33. data/app/views/alchemy/admin/contents/create.js.erb +1 -3
  34. data/app/views/alchemy/admin/essence_pictures/crop.html.erb +4 -3
  35. data/app/views/alchemy/essences/_essence_file_editor.html.erb +1 -1
  36. data/app/views/alchemy/essences/shared/_essence_picture_tools.html.erb +1 -1
  37. data/config/brakeman.ignore +5 -5
  38. data/config/locales/alchemy.de.yml +2 -2
  39. data/config/locales/alchemy.en.yml +2 -2
  40. data/config/locales/alchemy.es.yml +1 -1
  41. data/config/locales/alchemy.fr.yml +1 -1
  42. data/config/locales/alchemy.it.yml +1 -1
  43. data/config/locales/alchemy.nl.yml +1 -1
  44. data/config/locales/alchemy.ru.yml +1 -1
  45. data/db/migrate/20160422195310_add_image_file_format_to_alchemy_pictures.rb +0 -12
  46. data/lib/alchemy/errors.rb +6 -1
  47. data/lib/alchemy/logger.rb +1 -1
  48. data/lib/alchemy/permissions.rb +0 -7
  49. data/lib/alchemy/tasks/tidy.rb +130 -0
  50. data/lib/alchemy/test_support/factories/attachment_factory.rb +2 -2
  51. data/lib/alchemy/test_support/factories/cell_factory.rb +3 -3
  52. data/lib/alchemy/test_support/factories/content_factory.rb +2 -2
  53. data/lib/alchemy/test_support/factories/dummy_user_factory.rb +2 -2
  54. data/lib/alchemy/test_support/factories/element_factory.rb +2 -2
  55. data/lib/alchemy/test_support/factories/essence_file_factory.rb +2 -2
  56. data/lib/alchemy/test_support/factories/essence_picture_factory.rb +2 -2
  57. data/lib/alchemy/test_support/factories/essence_text_factory.rb +2 -2
  58. data/lib/alchemy/test_support/factories/language_factory.rb +2 -2
  59. data/lib/alchemy/test_support/factories/page_factory.rb +4 -4
  60. data/lib/alchemy/test_support/factories/picture_factory.rb +2 -2
  61. data/lib/alchemy/test_support/factories/site_factory.rb +2 -2
  62. data/lib/alchemy/upgrader/three_point_four.rb +25 -0
  63. data/lib/alchemy/version.rb +1 -1
  64. data/lib/tasks/alchemy/tidy.rake +1 -129
  65. data/lib/tasks/alchemy/upgrade.rake +9 -1
  66. metadata +22 -9
@@ -56,23 +56,35 @@ module Alchemy
56
56
  # @option options crop [Boolean]
57
57
  # If set to true the picture will be cropped to fit the size value.
58
58
  #
59
+ # @return [String]
59
60
  def picture_url(options = {})
60
61
  return if picture.nil?
61
62
 
62
- options = {
63
- format: picture.default_render_format,
64
- crop_from: crop_from,
65
- crop_size: crop_size
66
- }.merge(options)
63
+ picture.url picture_url_options.merge(options)
64
+ end
67
65
 
68
- picture.url(options)
66
+ # Picture rendering options
67
+ #
68
+ # Returns the +default_render_format+ of the associated +Alchemy::Picture+
69
+ # together with the +crop_from+ and +crop_size+ values
70
+ #
71
+ # @return [HashWithIndifferentAccess]
72
+ def picture_url_options
73
+ return {} if picture.nil?
74
+
75
+ {
76
+ format: picture.default_render_format,
77
+ crop_from: crop_from.presence,
78
+ crop_size: crop_size.presence
79
+ }.with_indifferent_access
69
80
  end
70
81
 
71
- # Renders a thumbnail representation of the assigned image
82
+ # Returns an url for the thumbnail representation of the assigned picture
72
83
  #
73
84
  # It takes cropping values into account, so it always represents the current
74
85
  # image displayed in the frontend.
75
86
  #
87
+ # @return [String]
76
88
  def thumbnail_url(options = {})
77
89
  return if picture.nil?
78
90
 
@@ -96,6 +108,7 @@ module Alchemy
96
108
  # @param max [Integer]
97
109
  # The maximum length of the text returned.
98
110
  #
111
+ # @return [String]
99
112
  def preview_text(max = 30)
100
113
  return "" if picture.nil?
101
114
  picture.name.to_s[0..max - 1]
@@ -103,6 +116,7 @@ module Alchemy
103
116
 
104
117
  # A Hash of coordinates suitable for the graphical image cropper.
105
118
  #
119
+ # @return [Hash]
106
120
  def cropping_mask
107
121
  return if crop_from.blank? || crop_size.blank?
108
122
  crop_from = point_from_string(read_attribute(:crop_from))
@@ -112,6 +126,8 @@ module Alchemy
112
126
  end
113
127
 
114
128
  # Returns a serialized ingredient value for json api
129
+ #
130
+ # @return [String]
115
131
  def serialized_ingredient
116
132
  picture_url(content.settings)
117
133
  end
@@ -132,13 +148,15 @@ module Alchemy
132
148
  private
133
149
 
134
150
  def fix_crop_values
135
- %w(crop_from crop_size).each do |crop_value|
136
- write_attribute crop_value, normalize_crop_value(crop_value)
151
+ %i(crop_from crop_size).each do |crop_value|
152
+ if self[crop_value].is_a?(String)
153
+ write_attribute crop_value, normalize_crop_value(crop_value)
154
+ end
137
155
  end
138
156
  end
139
157
 
140
158
  def normalize_crop_value(crop_value)
141
- send(crop_value).to_s.split('x').map { |n| normalize_number(n) }.join('x')
159
+ self[crop_value].split('x').map { |n| normalize_number(n) }.join('x')
142
160
  end
143
161
 
144
162
  def normalize_number(number)
@@ -14,7 +14,7 @@ module Alchemy
14
14
  disable_link: false,
15
15
  srcset: [],
16
16
  sizes: []
17
- }
17
+ }.with_indifferent_access
18
18
 
19
19
  def initialize(content, options = {}, html_options = {})
20
20
  @content = content
@@ -111,12 +111,11 @@ module Alchemy
111
111
  end
112
112
 
113
113
  # An Image smaller than dimensions
114
- # can not be cropped to string - unless upsample is true.
114
+ # can not be cropped to given size - unless upsample is true.
115
115
  #
116
116
  def can_be_cropped_to(string, upsample = false)
117
- dimensions = sizes_from_string(string)
118
117
  return true if upsample
119
- is_bigger_than(dimensions)
118
+ is_bigger_than sizes_from_string(string)
120
119
  end
121
120
 
122
121
  # Returns true if the class we're included in has a meaningful render_size attribute
@@ -2,6 +2,8 @@
2
2
 
3
3
  module Alchemy
4
4
  module Picture::Url
5
+ include Alchemy::Logger
6
+
5
7
  TRANSFORMATION_OPTIONS = [
6
8
  :crop,
7
9
  :crop_from,
@@ -30,6 +32,9 @@ module Alchemy
30
32
  image = encoded_image(image, options)
31
33
 
32
34
  image.url(options.except(*TRANSFORMATION_OPTIONS).merge(name: name))
35
+ rescue MissingImageFileError, WrongImageFormatError => e
36
+ log_warning e.message
37
+ nil
33
38
  end
34
39
 
35
40
  private
@@ -41,7 +46,7 @@ module Alchemy
41
46
 
42
47
  return image unless size.present? && has_convertible_format?
43
48
 
44
- if options[:crop_size].present? && options[:crop_from].present? || options[:crop].present?
49
+ if options[:crop]
45
50
  crop(size, options[:crop_from], options[:crop_size], upsample)
46
51
  else
47
52
  resize(size, upsample)
@@ -55,7 +60,10 @@ module Alchemy
55
60
  #
56
61
  def encoded_image(image, options = {})
57
62
  target_format = options[:format] || default_render_format
58
- raise WrongImageFormatError if !target_format.in?(Alchemy::Picture.allowed_filetypes)
63
+
64
+ unless target_format.in?(Alchemy::Picture.allowed_filetypes)
65
+ raise WrongImageFormatError.new(self, target_format)
66
+ end
59
67
 
60
68
  options = {
61
69
  flatten: target_format != 'gif' && image_file_format == 'gif'
@@ -1,7 +1,5 @@
1
1
  var editor_html = '<%= j(render "alchemy/essences/#{@content.essence_partial_name}_editor", {
2
- content: @content,
3
- options: options_from_params.symbolize_keys,
4
- html_options: @html_options.symbolize_keys
2
+ content: @content, options: options_from_params, html_options: @html_options
5
3
  }) %>';
6
4
 
7
5
  <% if params[:was_missing] %>
@@ -19,11 +19,12 @@
19
19
  <%= f.hidden_field :crop_from %>
20
20
  <%= f.hidden_field :crop_size %>
21
21
  <%= hidden_field_tag :content_id, @content.id %>
22
- <%= f.button "#{render_icon(:true)} #{Alchemy.t(:apply)}".html_safe, class: 'with_icon' %>
23
- <%= link_to Alchemy.t('Reset Imagemask'), '#', {
22
+ <%= button_tag Alchemy.t('Reset Imagemask'), {
24
23
  onclick: 'Alchemy.ImageCropper.reset(); return false',
25
- class: 'reset_mask'
24
+ class: 'reset_mask',
25
+ type: 'reset'
26
26
  } %>
27
+ <%= f.button Alchemy.t(:apply) %>
27
28
  <% end %>
28
29
  </div>
29
30
  <% end %>
@@ -11,7 +11,7 @@
11
11
  padding: false
12
12
  },
13
13
  {
14
- class: 'assign_file',
14
+ class: 'icon assign_file',
15
15
  title: Alchemy.t(:assign_file)
16
16
  }
17
17
  ) %>
@@ -6,7 +6,7 @@
6
6
  content.essence,
7
7
  options: content.settings.update(options)
8
8
  ), {
9
- size: "1000x615",
9
+ size: "1080x615",
10
10
  title: Alchemy.t('Edit Picturemask'),
11
11
  image_loader: false,
12
12
  padding: false
@@ -7,7 +7,7 @@
7
7
  "check_name": "MassAssignment",
8
8
  "message": "Parameters should be whitelisted for mass assignment",
9
9
  "file": "app/controllers/alchemy/admin/resources_controller.rb",
10
- "line": 128,
10
+ "line": 130,
11
11
  "link": "http://brakemanscanner.org/docs/warning_types/mass_assignment/",
12
12
  "code": "params.require(resource_handler.namespaced_resource_name).permit!",
13
13
  "render_path": null,
@@ -23,13 +23,13 @@
23
23
  {
24
24
  "warning_type": "Dynamic Render Path",
25
25
  "warning_code": 15,
26
- "fingerprint": "26461414e9f6be7b68dd8c7dda1c69b09c92a8e9997c0ac204e1756cae7f3d68",
26
+ "fingerprint": "79e194e21561d40888d86ebc7fd2ab474fdb0ce32d605dbe9ac6e8984ecc5e92",
27
27
  "check_name": "Render",
28
28
  "message": "Render path contains parameter value",
29
29
  "file": "app/views/alchemy/admin/contents/create.js.erb",
30
30
  "line": 1,
31
31
  "link": "http://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
32
- "code": "render(action => \"alchemy/essences/#{Content.create_from_scratch(Element.find(params[:content][:element_id]), content_params).essence_partial_name}_editor\", { :content => Content.create_from_scratch(Element.find(params[:content][:element_id]), content_params), :options => options_from_params.symbolize_keys, :html_options => (params[:html_options] or {}).symbolize_keys })",
32
+ "code": "render(action => \"alchemy/essences/#{Content.create_from_scratch(Element.find(params[:content][:element_id]), content_params).essence_partial_name}_editor\", { :content => Content.create_from_scratch(Element.find(params[:content][:element_id]), content_params), :options => options_from_params, :html_options => ((params[:html_options] or {})) })",
33
33
  "render_path": [{"type":"controller","class":"Alchemy::Admin::ContentsController","method":"create","line":21,"file":"app/controllers/alchemy/admin/contents_controller.rb"}],
34
34
  "location": {
35
35
  "type": "template",
@@ -60,6 +60,6 @@
60
60
  "note": "`Alchemy::Content` is a polymorphic association of any kind of model extending `Alchemy::Essence`. Since we can't know the attributes of all potential essences we need to permit all attributes. As this all happens inside the password protected /admin namespace this can be considered a false positive."
61
61
  }
62
62
  ],
63
- "updated": "2017-08-16 15:07:26 +0200",
64
- "brakeman_version": "3.7.0"
63
+ "updated": "2017-10-23 11:49:41 +0200",
64
+ "brakeman_version": "4.0.1"
65
65
  }
@@ -252,7 +252,7 @@ de:
252
252
  "Remove item from clipboard": "Inhalt aus der Zwischenablage entfernen"
253
253
  "Remove tag filter": "Filter entfernen"
254
254
  "Remove this content": "Diesen Inhalt entfernen"
255
- "Reset Imagemask": "Bildmaske zurücksetzen"
255
+ "Reset Imagemask": "zurücksetzen"
256
256
  "Reset password instructions": "Anweisungen um Ihr Passwort zurückzusetzen"
257
257
  "Select all": "Alle auswählen"
258
258
  "Select an content": "Wählen Sie einen Inhalt aus"
@@ -365,7 +365,7 @@ de:
365
365
  element_of_type: "Element"
366
366
  element_saved: "Element wurde gespeichert."
367
367
  enter_external_link: "Geben Sie hier die Adresse der Seite ein zu der Sie einen Link setzen wollen."
368
- explain_cropping: "<p>Sie können den Rahmen verschieben und in der Größe verändern um den Bildausschnitt festzulegen.</p><p>Wenn Sie zufrieden sind, dann klicken Sie bitte auf speichern.</p>"
368
+ explain_cropping: "<p>Sie können den Rahmen mit der Maus und den Pfeiltasten verschieben und in der Größe verändern, um einen eigenen Bildausschnitt festzulegen. Wenn Sie zufrieden sind, dann klicken Sie bitte auf \"übernehmen\".</p><p>Um zu dem zentrierten Bildausschnitt zurückzukehren, wie er im Layout vorgesehen wurde, klicken Sie bitte auf \"zurücksezen\" und \"übernehmen\" anschließend die Änderungen.</p>"
369
369
  explain_publishing: "Die gecachte Version vom Server löschen und die aktuellen Änderungen veröffentlichen"
370
370
  explain_sitemap_dragndrop_sorting: "Tip: Halten Sie zum Sortieren der Seiten das Seitensymbol mit der Maus fest und bewegen Sie sie an ihre neue Position."
371
371
  explain_unlocking: "Die Seite verlassen und für andere Benutzer zum Bearbeiten freigeben."
@@ -251,7 +251,7 @@ en:
251
251
  "Remove item from clipboard": "Remove item from clipboard"
252
252
  "Remove tag filter": "Remove tag filter"
253
253
  "Remove this content": "Remove this content"
254
- "Reset Imagemask": "Reset Imagemask"
254
+ "Reset Imagemask": "Reset mask"
255
255
  "Reset password instructions": "Reset password instructions"
256
256
  "Select all": "Select all"
257
257
  "Select an content": "Select a content"
@@ -363,7 +363,7 @@ en:
363
363
  element_of_type: "Element"
364
364
  element_saved: "Saved element."
365
365
  enter_external_link: "Please enter the URL you want to link with"
366
- explain_cropping: "<p>Move the frame and change its size to adjust the image section.</p><p>Click on apply when you are satisfied.</p>"
366
+ explain_cropping: "<p>Move the frame and change its size with the mouse or arrow keys to adjust the image mask. Click on \"apply\" when you are satisfied with your selection.</p><p>If you want to return to the original centered image mask like it was defined in the layout, click \"reset\" and \"apply\" afterwards.</p>"
367
367
  explain_publishing: "Publish the page and remove the cached version from the server."
368
368
  explain_sitemap_dragndrop_sorting: "Tip: Drag the pages at the icon in order to sort them."
369
369
  explain_unlocking: "Leave page and unlock it for other users."
@@ -252,7 +252,7 @@ es:
252
252
  "Remove item from clipboard": "Quitar elemento del portapapeles"
253
253
  "Remove tag filter": "Quitar filtro de etiquetas"
254
254
  "Remove this content": "Quitar este contenido"
255
- "Reset Imagemask": "Reiniciar máscara de imagen"
255
+ "Reset Imagemask": "Reiniciar"
256
256
  "Reset password instructions": "Instrucciones para restablecer la contraseña"
257
257
  "Select all": "Seleccionar todo"
258
258
  "Select an content": "Seleccionar contenido"
@@ -271,7 +271,7 @@ fr:
271
271
  "Remove item from clipboard": "Supprimer le contenu du presse-papiers"
272
272
  "Remove tag filter": "Enlever filtre"
273
273
  "Remove this content": "Retirer ce contenu"
274
- "Reset Imagemask": "Enlever le masque d'image"
274
+ "Reset Imagemask": "Enlever"
275
275
  "Reset password instructions": "Instructions pour réinitialiser votre mot de passe"
276
276
  "Select all": "Sélectionner tous"
277
277
  "Select an content": "Sélectionnez un contenu de"
@@ -252,7 +252,7 @@ it:
252
252
  "Remove item from clipboard": "Rimuovi elemento dagli appunti"
253
253
  "Remove tag filter": "Rimuovi filtro tag"
254
254
  "Remove this content": "Rimuovi questo contenuto"
255
- "Reset Imagemask": "Ripristina la maschera delle immagini"
255
+ "Reset Imagemask": "Ripristina"
256
256
  "Reset password instructions": "Ripristina le istruzioni per la password"
257
257
  "Select all": "Seleziona tutto"
258
258
  "Select an content": "Seleziona un contenuto"
@@ -250,7 +250,7 @@ nl:
250
250
  "Remove item from clipboard": "Item van klembord verwijderen"
251
251
  "Remove tag filter": "Filter verwijderen"
252
252
  "Remove this content": "Inhoud verwijderen"
253
- "Reset Imagemask": "Afbeeldingsmasker resetten"
253
+ "Reset Imagemask": "resetten"
254
254
  "Reset password instructions": "Instructies om het wachtwoord te resetten."
255
255
  "Select all": "Alles selecteren"
256
256
  "Select an content": "Selecteer de inhoud"
@@ -251,7 +251,7 @@ ru:
251
251
  "Remove item from clipboard": "Удалить элемент из буфера обмена"
252
252
  "Remove tag filter": "Удалить фильтр меток"
253
253
  "Remove this content": "Удалить этот контент"
254
- "Reset Imagemask": "Сбросить маску изображений"
254
+ "Reset Imagemask": "Сбросить"
255
255
  "Reset password instructions": "Инструкция по сбросу пароля"
256
256
  "Select all": "Выбрать все"
257
257
  "Select an content": "Выбрать контент"
@@ -1,18 +1,6 @@
1
1
  class AddImageFileFormatToAlchemyPictures < ActiveRecord::Migration[4.2]
2
2
  def up
3
3
  add_column :alchemy_pictures, :image_file_format, :string
4
-
5
- say_with_time "Storing file format of existing pictures" do
6
- Alchemy::Picture.all.each do |pic|
7
- begin
8
- format = pic.image_file.identify('-ping -format "%m"')
9
- pic.update_column('image_file_format', format.to_s.chomp.downcase)
10
- rescue Dragonfly::Job::Fetch::NotFound => e
11
- say(e.message, true)
12
- end
13
- end
14
- Alchemy::Picture.count
15
- end
16
4
  end
17
5
 
18
6
  def down
@@ -57,9 +57,14 @@ module Alchemy
57
57
 
58
58
  # Raised if calling +image_file+ on a Picture object returns nil.
59
59
  class WrongImageFormatError < StandardError
60
+ def initialize(image, requested_format)
61
+ @image = image
62
+ @requested_format = requested_format
63
+ end
64
+
60
65
  def message
61
66
  allowed_filetypes = Alchemy::Picture.allowed_filetypes.map(&:upcase).to_sentence
62
- "Requested image format is not one of allowed filetypes (#{allowed_filetypes})."
67
+ "Requested image format (#{@requested_format.inspect}) for #{@image.inspect} is not one of allowed filetypes (#{allowed_filetypes})."
63
68
  end
64
69
  end
65
70
 
@@ -9,7 +9,7 @@ module Alchemy
9
9
  end
10
10
 
11
11
  def log_warning(message)
12
- Alchemy::Logger.warn(message, caller(0..0))
12
+ Alchemy::Logger.warn(message, caller(1..1))
13
13
  end
14
14
  end
15
15
  end
@@ -37,7 +37,6 @@ module Alchemy
37
37
  def alchemy_guest_user_rules
38
38
  can([:show, :download], Alchemy::Attachment) { |a| !a.restricted? }
39
39
  can :see, Alchemy::Page, restricted: false, visible: true
40
- can(:display, Alchemy::Picture) { |p| !p.restricted? }
41
40
 
42
41
  can :read, Alchemy::Content, Alchemy::Content.available.not_restricted do |c|
43
42
  c.public? && !c.restricted? && !c.trashed?
@@ -67,7 +66,6 @@ module Alchemy
67
66
  can [:show, :download], Alchemy::Attachment
68
67
  can :read, Alchemy::Page, Alchemy::Page.published, &:public?
69
68
  can :see, Alchemy::Page, restricted: true, visible: true
70
- can :display, Alchemy::Picture
71
69
 
72
70
  can :read, Alchemy::Content, Alchemy::Content.available do |c|
73
71
  c.public? && !c.trashed?
@@ -217,11 +215,6 @@ module Alchemy
217
215
  :unlock,
218
216
  :visit,
219
217
  to: :edit_content
220
-
221
- alias_action :show,
222
- :thumbnail,
223
- :zoom,
224
- to: :display
225
218
  end
226
219
 
227
220
  # Include the role specific permissions.
@@ -0,0 +1,130 @@
1
+ require 'alchemy/shell'
2
+
3
+ module Alchemy
4
+ class Tidy
5
+ extend Shell
6
+
7
+ class << self
8
+ def create_missing_cells(page_layouts, cells)
9
+ page_layouts.each do |layout|
10
+ next if layout['cells'].blank?
11
+ cells_for_layout = cells.select { |cell| layout['cells'].include? cell['name'] }
12
+ Alchemy::Page.where(page_layout: layout['name']).each do |page|
13
+ cells_for_layout.each do |cell_for_layout|
14
+ cell = Alchemy::Cell.find_or_initialize_by(name: cell_for_layout['name'], page_id: page.id)
15
+ if cell.new_record?
16
+ log "Creating cell #{cell.name} for page #{page.name}"
17
+ else
18
+ log "Cell #{cell.name} for page #{page.name} already present", :skip
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ def update_element_positions
26
+ Alchemy::Page.all.each do |page|
27
+ if page.elements.any?
28
+ puts "\n## Updating element positions of page `#{page.name}`"
29
+ end
30
+ page.elements.group_by(&:cell_id).each do |_cell_id, elements|
31
+ elements.each_with_index do |element, idx|
32
+ position = idx + 1
33
+ if element.position != position
34
+ log "Updating position for element ##{element.id} to #{position}"
35
+ element.update_column(:position, position)
36
+ else
37
+ log "Position for element ##{element.id} is already correct (#{position})", :skip
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ def update_content_positions
45
+ Alchemy::Element.all.each do |element|
46
+ if element.contents.any?
47
+ puts "\n## Updating content positions of element `#{element.name}`"
48
+ end
49
+ element.contents.group_by(&:essence_type).each do |essence_type, contents|
50
+ puts "-> Contents of type `#{essence_type}`"
51
+ contents.each_with_index do |content, idx|
52
+ position = idx + 1
53
+ if content.position != position
54
+ log "Updating position for content ##{content.id} to #{position}"
55
+ content.update_column(:position, position)
56
+ else
57
+ log "Position for content ##{content.id} is already correct (#{position})", :skip
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ def remove_orphaned_cells
65
+ puts "\n## Removing orphaned cells"
66
+ cells = Alchemy::Cell.unscoped.all
67
+ if cells.any?
68
+ orphaned_cells = cells.select do |cell|
69
+ cell.page.nil? && cell.page_id.present?
70
+ end
71
+ if orphaned_cells.any?
72
+ log "Found #{orphaned_cells.size} orphaned cells"
73
+ destroy_orphaned_records(orphaned_cells, 'cell')
74
+ else
75
+ log "No orphaned cells found", :skip
76
+ end
77
+ else
78
+ log "No cells found", :skip
79
+ end
80
+ end
81
+
82
+ def remove_orphaned_elements
83
+ puts "\n## Removing orphaned elements"
84
+ elements = Alchemy::Element.unscoped.all
85
+ if elements.any?
86
+ orphaned_elements = elements.select do |element|
87
+ element.page.nil? && element.page_id.present? ||
88
+ element.cell.nil? && element.cell_id.present?
89
+ end
90
+ if orphaned_elements.any?
91
+ log "Found #{orphaned_elements.size} orphaned elements"
92
+ destroy_orphaned_records(orphaned_elements, 'element')
93
+ else
94
+ log "No orphaned elements found", :skip
95
+ end
96
+ else
97
+ log "No elements found", :skip
98
+ end
99
+ end
100
+
101
+ def remove_orphaned_contents
102
+ puts "\n## Removing orphaned contents"
103
+ contents = Alchemy::Content.unscoped.all
104
+ if contents.any?
105
+ orphaned_contents = contents.select do |content|
106
+ content.essence.nil? && content.essence_id.present? ||
107
+ content.element.nil? && content.element_id.present?
108
+ end
109
+ if orphaned_contents.any?
110
+ log "Found #{orphaned_contents.size} orphaned contents"
111
+ destroy_orphaned_records(orphaned_contents, 'content')
112
+ else
113
+ log "No orphaned contents found", :skip
114
+ end
115
+ else
116
+ log "No contents found", :skip
117
+ end
118
+ end
119
+
120
+ private
121
+
122
+ def destroy_orphaned_records(records, class_name)
123
+ records.each do |record|
124
+ log "Destroy orphaned #{class_name}: #{record.inspect}"
125
+ record.destroy
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end