alchemy_cms 4.2.4 → 4.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +4 -0
  3. data/.travis.yml +8 -6
  4. data/CHANGELOG.md +15 -10
  5. data/Gemfile +2 -10
  6. data/README.md +11 -4
  7. data/Rakefile +3 -2
  8. data/alchemy_cms.gemspec +11 -2
  9. data/app/assets/javascripts/alchemy/alchemy.dialog.js.coffee +3 -3
  10. data/app/assets/javascripts/alchemy/alchemy.elements_window.js.coffee +1 -1
  11. data/app/assets/stylesheets/alchemy/buttons.scss +15 -2
  12. data/app/assets/stylesheets/alchemy/elements.scss +4 -0
  13. data/app/assets/stylesheets/alchemy/form_fields.scss +1 -0
  14. data/app/assets/stylesheets/alchemy/forms.scss +1 -0
  15. data/app/assets/stylesheets/alchemy/frame.scss +9 -29
  16. data/app/assets/stylesheets/alchemy/navigation.scss +30 -6
  17. data/app/assets/stylesheets/alchemy/preview_window.scss +4 -0
  18. data/app/controllers/alchemy/admin/elements_controller.rb +2 -2
  19. data/app/controllers/alchemy/pages_controller.rb +9 -4
  20. data/app/helpers/alchemy/elements_helper.rb +2 -2
  21. data/app/models/alchemy/element.rb +9 -4
  22. data/app/models/alchemy/page/page_elements.rb +17 -25
  23. data/app/models/alchemy/page/page_scopes.rb +1 -1
  24. data/app/models/alchemy/picture.rb +0 -21
  25. data/app/views/alchemy/admin/elements/_element.html.erb +2 -1
  26. data/app/views/alchemy/admin/pages/edit.html.erb +10 -10
  27. data/lib/alchemy/on_page_layout.rb +1 -1
  28. data/lib/alchemy/test_support/factories.rb +3 -1
  29. data/lib/alchemy/test_support/factories/attachment_factory.rb +2 -0
  30. data/lib/alchemy/test_support/factories/content_factory.rb +5 -0
  31. data/lib/alchemy/test_support/factories/dummy_user_factory.rb +2 -0
  32. data/lib/alchemy/test_support/factories/element_factory.rb +3 -0
  33. data/lib/alchemy/test_support/factories/essence_file_factory.rb +2 -0
  34. data/lib/alchemy/test_support/factories/essence_picture_factory.rb +2 -0
  35. data/lib/alchemy/test_support/factories/essence_text_factory.rb +2 -0
  36. data/lib/alchemy/test_support/factories/language_factory.rb +2 -0
  37. data/lib/alchemy/test_support/factories/page_factory.rb +2 -0
  38. data/lib/alchemy/test_support/factories/picture_factory.rb +2 -0
  39. data/lib/alchemy/test_support/factories/site_factory.rb +2 -0
  40. data/lib/alchemy/test_support/shared_uploader_examples.rb +1 -1
  41. data/lib/alchemy/upgrader/tasks/cells_migration.rb +2 -4
  42. data/lib/alchemy/upgrader/tasks/cells_upgrader.rb +2 -3
  43. data/lib/alchemy/upgrader/tasks/picture_gallery_upgrader.rb +4 -4
  44. data/lib/alchemy/version.rb +5 -1
  45. data/lib/alchemy_cms.rb +0 -1
  46. data/lib/rails/generators/alchemy/elements/templates/view.html.erb +1 -1
  47. data/lib/rails/generators/alchemy/elements/templates/view.html.haml +1 -1
  48. data/lib/rails/generators/alchemy/elements/templates/view.html.slim +1 -1
  49. data/vendor/assets/javascripts/fileupload/jquery.fileupload-process.js +5 -2
  50. data/vendor/assets/javascripts/fileupload/jquery.fileupload-validate.js +5 -2
  51. data/vendor/assets/javascripts/fileupload/jquery.fileupload.js +28 -8
  52. data/vendor/assets/javascripts/fileupload/jquery.iframe-transport.js +11 -4
  53. metadata +129 -12
  54. data/.teatro.yml +0 -8
  55. data/lib/alchemy/picture_attributes.rb +0 -28
@@ -16,5 +16,9 @@
16
16
 
17
17
  .collapsed-menu.elements-window-visible & {
18
18
  width: calc(100vw - #{$collapsed-main-menu-width - $default-border-width} - #{$elements-window-width});
19
+
20
+ @media screen and (min-width: $large-screen-break-point) {
21
+ max-width: calc(100vw - #{$collapsed-main-menu-width - $default-border-width} - #{$elements-window-min-width});
22
+ }
19
23
  }
20
24
  }
@@ -8,8 +8,8 @@ module Alchemy
8
8
 
9
9
  def index
10
10
  @page = Page.find(params[:page_id])
11
- @elements = @page.elements
12
- @fixed_elements = @page.fixed_elements
11
+ @elements = @page.all_elements.not_nested.unfixed.not_trashed
12
+ @fixed_elements = @page.all_elements.fixed.not_trashed
13
13
  end
14
14
 
15
15
  def list
@@ -155,10 +155,10 @@ module Alchemy
155
155
  end
156
156
 
157
157
  def set_expiration_headers
158
- if @page.cache_page?
159
- expires_in @page.expiration_time, public: !@page.restricted, must_revalidate: true
160
- else
158
+ if must_not_cache?
161
159
  expires_now
160
+ else
161
+ expires_in @page.expiration_time, public: !@page.restricted, must_revalidate: true
162
162
  end
163
163
  end
164
164
 
@@ -190,12 +190,17 @@ module Alchemy
190
190
  # or the cache is stale, because it's been republished by the user.
191
191
  #
192
192
  def render_fresh_page?
193
- !@page.cache_page? || stale?(etag: page_etag,
193
+ must_not_cache? || stale?(etag: page_etag,
194
194
  last_modified: @page.published_at,
195
195
  public: !@page.restricted,
196
196
  template: 'pages/show')
197
197
  end
198
198
 
199
+ # don't cache pages if we have flash message to display or the page has caching disabled
200
+ def must_not_cache?
201
+ flash.present? || !@page.cache_page?
202
+ end
203
+
199
204
  def page_not_found!
200
205
  not_found_error!("Alchemy::Page not found \"#{request.fullpath}\"")
201
206
  end
@@ -51,7 +51,7 @@ module Alchemy
51
51
  #
52
52
  # class MyCustomNewsArchive
53
53
  # def elements(page:)
54
- # news_page.elements.available.named('news').order(created_at: :desc)
54
+ # news_page.elements.named('news').order(created_at: :desc)
55
55
  # end
56
56
  #
57
57
  # private
@@ -102,7 +102,7 @@ module Alchemy
102
102
 
103
103
  if options[:from_cell]
104
104
  Alchemy::Deprecation.warn "options[:from_cell] has been removed without replacement. " \
105
- "Please `render element.nested_elements.available` instead."
105
+ "Please `render element.nested_elements` instead."
106
106
  end
107
107
 
108
108
  finder = options[:finder] || Alchemy::ElementsFinder.new(options)
@@ -63,14 +63,19 @@ module Alchemy
63
63
  # In order to get contents in creation order we also order them by id.
64
64
  has_many :contents, -> { order(:position, :id) }, dependent: :destroy
65
65
 
66
- # Elements can have other elements nested inside
66
+ has_many :all_nested_elements,
67
+ -> { order(:position) },
68
+ class_name: 'Alchemy::Element',
69
+ foreign_key: :parent_element_id,
70
+ dependent: :destroy
71
+
67
72
  has_many :nested_elements,
68
- -> { order(:position).not_trashed },
73
+ -> { order(:position).available },
69
74
  class_name: 'Alchemy::Element',
70
75
  foreign_key: :parent_element_id,
71
76
  dependent: :destroy
72
77
 
73
- belongs_to :page, touch: true, inverse_of: :descendent_elements
78
+ belongs_to :page, touch: true, inverse_of: :all_elements
74
79
 
75
80
  # A nested element belongs to a parent element.
76
81
  belongs_to :parent_element,
@@ -98,7 +103,7 @@ module Alchemy
98
103
  scope :not_restricted, -> { joins(:page).merge(Page.not_restricted) }
99
104
  scope :available, -> { published.not_trashed }
100
105
  scope :named, ->(names) { where(name: names) }
101
- scope :excluded, ->(names) { where(arel_table[:name].not_in(names)) }
106
+ scope :excluded, ->(names) { where.not(name: names) }
102
107
  scope :fixed, -> { where(fixed: true) }
103
108
  scope :unfixed, -> { where(fixed: false) }
104
109
  scope :from_current_site, -> { where(Language.table_name => {site_id: Site.current || Site.default}).joins(page: 'language') }
@@ -7,26 +7,19 @@ module Alchemy
7
7
  included do
8
8
  attr_accessor :autogenerate_elements
9
9
 
10
- has_many :elements,
11
- -> { order(:position).not_nested.unfixed.not_trashed },
10
+ has_many :all_elements,
11
+ -> { order(:position) },
12
12
  class_name: 'Alchemy::Element'
13
- has_many :elements_including_fixed,
14
- -> { order(:position).not_nested.not_trashed },
13
+ has_many :elements,
14
+ -> { order(:position).not_nested.unfixed.available },
15
15
  class_name: 'Alchemy::Element'
16
16
  has_many :trashed_elements,
17
17
  -> { Element.trashed.order(:position) },
18
18
  class_name: 'Alchemy::Element'
19
19
  has_many :fixed_elements,
20
- -> { order(:position).fixed.not_trashed },
21
- class_name: 'Alchemy::Element'
22
- has_many :descendent_elements,
23
- -> { order(:position).unfixed.not_trashed },
20
+ -> { order(:position).fixed.available },
24
21
  class_name: 'Alchemy::Element'
25
22
  has_many :contents, through: :elements
26
- has_many :descendent_contents,
27
- through: :descendent_elements,
28
- class_name: 'Alchemy::Content',
29
- source: :contents
30
23
  has_and_belongs_to_many :to_be_swept_elements, -> { distinct },
31
24
  class_name: 'Alchemy::Element',
32
25
  join_table: ElementToPage.table_name
@@ -49,15 +42,12 @@ module Alchemy
49
42
  # @return [Array]
50
43
  #
51
44
  def copy_elements(source, target)
52
- new_elements = []
53
- source.elements_including_fixed.each do |source_element|
54
- new_element = Element.copy(source_element, {
45
+ source_elements = source.all_elements.not_nested.not_trashed
46
+ source_elements.order(:position).map do |source_element|
47
+ Element.copy(source_element, {
55
48
  page_id: target.id
56
- })
57
- new_element.move_to_bottom
58
- new_elements << new_element
49
+ }).tap(&:move_to_bottom)
59
50
  end
60
- new_elements
61
51
  end
62
52
  end
63
53
 
@@ -91,7 +81,8 @@ module Alchemy
91
81
 
92
82
  return [] if @_element_definitions.blank?
93
83
 
94
- @_existing_element_names = elements_including_fixed.pluck(:name)
84
+ existing_elements = all_elements.not_nested.not_trashed
85
+ @_existing_element_names = existing_elements.pluck(:name)
95
86
  delete_unique_element_definitions!
96
87
  delete_outnumbered_element_definitions!
97
88
 
@@ -177,14 +168,14 @@ module Alchemy
177
168
  # feed_elements: [element_name, element_2_name]
178
169
  #
179
170
  def feed_elements
180
- elements.available.named(definition['feed_elements'])
171
+ elements.named(definition['feed_elements'])
181
172
  end
182
173
 
183
174
  # Returns an array of all EssenceRichtext contents ids from not folded elements
184
175
  #
185
176
  def richtext_contents_ids
186
- descendent_contents
187
- .where(Element.table_name => {folded: false})
177
+ Alchemy::Content.joins(:element)
178
+ .where(Element.table_name => {page_id: id, folded: false})
188
179
  .select(&:has_tinymce?)
189
180
  .collect(&:id)
190
181
  end
@@ -196,9 +187,10 @@ module Alchemy
196
187
  # And if so, it generates them.
197
188
  #
198
189
  def generate_elements
199
- elements_already_on_page = elements_including_fixed.pluck(:name)
190
+ existing_elements = all_elements.not_nested.not_trashed
191
+ existing_element_names = existing_elements.pluck(:name).uniq
200
192
  definition.fetch('autogenerate', []).each do |element_name|
201
- next if elements_already_on_page.include?(element_name)
193
+ next if existing_element_names.include?(element_name)
202
194
  Element.create(page: self, name: element_name)
203
195
  end
204
196
  end
@@ -66,7 +66,7 @@ module Alchemy
66
66
  # Returns all content pages.
67
67
  #
68
68
  scope :contentpages, -> {
69
- where(layoutpage: [false, nil]).where(Page.arel_table[:parent_id].not_eq(nil))
69
+ where(layoutpage: [false, nil]).where.not(parent_id: nil)
70
70
  }
71
71
 
72
72
  # Returns all public contentpages that are not locked.
@@ -246,26 +246,5 @@ module Alchemy
246
246
  def image_file_dimensions
247
247
  "#{image_file_width}x#{image_file_height}"
248
248
  end
249
-
250
- # Returns a security token for signed picture rendering requests.
251
- #
252
- # Pass a params hash containing:
253
- #
254
- # size [String] (Optional)
255
- # crop [Boolean] (Optional)
256
- # crop_from [String] (Optional)
257
- # crop_size [String] (Optional)
258
- # quality [Integer] (Optional)
259
- #
260
- # to sign them.
261
- #
262
- def security_token(params = {})
263
- params = params.dup.stringify_keys
264
- params.update({
265
- 'crop' => params['crop'] ? 'crop' : nil,
266
- 'id' => id
267
- })
268
- PictureAttributes.secure(params)
269
- end
270
249
  end
271
250
  end
@@ -37,7 +37,8 @@
37
37
  class: "nested-elements", data: {
38
38
  'droppable-elements' => element.nestable_elements.join(' ')
39
39
  } do %>
40
- <%= render partial: 'alchemy/admin/elements/element', collection: element.nested_elements %>
40
+ <%= render partial: 'alchemy/admin/elements/element',
41
+ collection: element.all_nested_elements.not_trashed %>
41
42
  <% end %>
42
43
 
43
44
  <% if element.expanded? || element.fixed? %>
@@ -13,16 +13,6 @@
13
13
  <% end %>
14
14
  </div>
15
15
  <div class="toolbar_spacer"></div>
16
- <% unless @page.layoutpage? %>
17
- <div class="button_with_label">
18
- <%= form_tag alchemy.visit_admin_page_path(@page), id: 'visit_page_form' do %>
19
- <button class="icon_button" title="<%= Alchemy.t('Visit page') %>">
20
- <%= render_icon('external-link-alt') %>
21
- </button>
22
- <label><%= Alchemy.t("Visit page") %></label>
23
- <% end %>
24
- </div>
25
- <% end %>
26
16
  <div class="button_with_label">
27
17
  <%= link_to_dialog(
28
18
  render_icon('info-circle'),
@@ -76,6 +66,16 @@
76
66
  <% end %>
77
67
  </div>
78
68
  <% end %>
69
+ <% unless @page.layoutpage? %>
70
+ <div class="button_with_label">
71
+ <%= form_tag alchemy.visit_admin_page_path(@page), id: 'visit_page_form' do %>
72
+ <%= button_tag class: 'icon_button', disabled: !@page.public? do %>
73
+ <%= render_icon('external-link-alt') %>
74
+ <% end %>
75
+ <label><%= Alchemy.t("Visit page") %></label>
76
+ <% end %>
77
+ </div>
78
+ <% end %>
79
79
  <% if @page.has_hint? %>
80
80
  <div class="toolbar_spacer"></div>
81
81
  <%= render_hint_for(@page) %>
@@ -47,7 +47,7 @@ module Alchemy
47
47
  # Registers a callback for given page layout
48
48
  def self.register_callback(page_layout, callback)
49
49
  @callbacks ||= {}
50
- @callbacks[page_layout] ||= []
50
+ @callbacks[page_layout] ||= Set.new
51
51
  @callbacks[page_layout] << callback
52
52
  end
53
53
 
@@ -1,3 +1,5 @@
1
- Dir[File.dirname(__FILE__) + '/factories/*.rb'].each do |file|
1
+ # frozen_string_literal: true
2
+
3
+ Dir["#{File.dirname(__FILE__)}/factories/*.rb"].each do |file|
2
4
  require file
3
5
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'factory_bot'
2
4
 
3
5
  FactoryBot.define do
@@ -1,4 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'factory_bot'
4
+ require 'alchemy/test_support/factories/element_factory'
5
+ require 'alchemy/test_support/factories/essence_file_factory'
6
+ require 'alchemy/test_support/factories/essence_picture_factory'
2
7
  require 'alchemy/test_support/factories/essence_text_factory'
3
8
 
4
9
  FactoryBot.define do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'factory_bot'
2
4
 
3
5
  FactoryBot.define do
@@ -1,4 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'factory_bot'
4
+ require 'alchemy/test_support/factories/page_factory'
2
5
 
3
6
  FactoryBot.define do
4
7
  factory :alchemy_element, class: 'Alchemy::Element' do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'factory_bot'
2
4
  require 'alchemy/test_support/factories/attachment_factory'
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'factory_bot'
2
4
  require 'alchemy/test_support/factories/picture_factory'
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'factory_bot'
2
4
 
3
5
  FactoryBot.define do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'factory_bot'
2
4
  require 'alchemy/test_support/factories/site_factory'
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'factory_bot'
2
4
  require 'alchemy/test_support/factories/language_factory'
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'factory_bot'
2
4
 
3
5
  FactoryBot.define do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'factory_bot'
2
4
 
3
5
  FactoryBot.define do
@@ -1,7 +1,7 @@
1
1
  RSpec.shared_examples_for "having a json uploader error message" do
2
2
  it "renders json response with error message" do
3
3
  subject
4
- expect(response.content_type).to eq('application/json')
4
+ expect(response.media_type).to eq('application/json')
5
5
  expect(response.status).to eq(422)
6
6
  json = JSON.parse(response.body)
7
7
  expect(json).to have_key('growl_message')
@@ -27,13 +27,11 @@ module Alchemy::Upgrader::Tasks
27
27
  # bust element definitions insta cache
28
28
  Alchemy::Element.instance_variable_set('@definitions', nil)
29
29
  fixed_element = Alchemy::Element.find_or_initialize_by(fixed: true, name: cell.name, page: cell.page)
30
- elements = Alchemy::Element.where(cell_id: cell.id).order(position: :asc)
30
+ elements = Alchemy::Element.where(cell_id: cell.id)
31
31
 
32
32
  if fixed_element.new_record?
33
+ fixed_element.nested_elements = elements
33
34
  fixed_element.save!
34
- Alchemy::Element.acts_as_list_no_update do
35
- elements.update_all(parent_element_id: fixed_element.id)
36
- end
37
35
  puts "Created new fixed element '#{fixed_element.name}' for cell '#{cell.name}'."
38
36
  else
39
37
  puts "Element for cell '#{cell.name}' already present. Skip"
@@ -114,9 +114,9 @@ module Alchemy::Upgrader::Tasks
114
114
  if Dir.exist? cells_view_folder
115
115
  puts "-- Update cell views"
116
116
  Dir.glob("#{cells_view_folder}/*").each do |view|
117
- gsub_file(view, /elements\.published/, 'elements.available')
117
+ gsub_file(view, /elements\.published/, 'elements')
118
118
  gsub_file(view, /cell\.elements(.+)/, 'element.nested_elements\1')
119
- gsub_file(view, /render_elements[\(\s]?:?from_cell:?\s?(=>)?\s?cell\)?/, 'render element.nested_elements.available')
119
+ gsub_file(view, /render_elements[\(\s]?:?from_cell:?\s?(=>)?\s?cell\)?/, 'render element.nested_elements')
120
120
  gsub_file(view, /cell/, 'element')
121
121
  end
122
122
  else
@@ -129,7 +129,6 @@ module Alchemy::Upgrader::Tasks
129
129
  Dir.glob("#{alchemy_views_folder}/**/*").each do |view|
130
130
  next if File.directory?(view)
131
131
  gsub_file(view, /render_cell[\(\s]?([:'"]?[a-z_]+['"]?)\)?/, 'render_elements(only: \1, fixed: true)')
132
- gsub_file(view, /render_elements[\(\s](.*):?from_cell:?\s?(=>)?\s?(['"][a-z_]+['"])\)?/, 'render_elements(\1only: \3, fixed: true)')
133
132
  end
134
133
  end
135
134
 
@@ -91,22 +91,22 @@ module Alchemy::Upgrader::Tasks
91
91
  def find_gallery_pictures_rendering
92
92
  puts '5. Find element views that use gallery pictures:'
93
93
 
94
- erb_snippet = ' <%= render element.nested_elements.available %>'
94
+ erb_snippet = ' <%= render element.nested_elements %>'
95
95
  erb_views = erb_element_partials(:view).select do |view|
96
96
  next if File.read(view).match(GALLERY_PICTURES_ERB_REGEXP).nil?
97
97
 
98
98
  inject_into_file view,
99
- "<%# TODO: Move the content of next block into its nestable element view and `render element.nested_elements.available` instead %>\n",
99
+ "<%# TODO: Move the content of next block into its nestable element view and `render element.nested_elements` instead %>\n",
100
100
  before: GALLERY_PICTURES_ERB_REGEXP
101
101
  true
102
102
  end
103
103
 
104
- haml_slim_snippet = ' = element.nested_elements.available'
104
+ haml_slim_snippet = ' = element.nested_elements'
105
105
  haml_views = haml_slim_element_partials(:view).select do |view|
106
106
  next if File.read(view).match(GALLERY_PICTURES_HAML_REGEXP).nil?
107
107
 
108
108
  inject_into_file view,
109
- "-# TODO: Move the content of next block into its nestable element view and `render element.nested_elements.available` instead\n",
109
+ "-# TODO: Move the content of next block into its nestable element view and `render element.nested_elements` instead\n",
110
110
  before: GALLERY_PICTURES_HAML_REGEXP
111
111
  true
112
112
  end