alchemy_cms 4.5.0 → 4.6.0

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.

Potentially problematic release.


This version of alchemy_cms might be problematic. Click here for more details.

Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +23 -17
  3. data/.rubocop.yml +7 -15
  4. data/CHANGELOG.md +17 -0
  5. data/app/assets/javascripts/alchemy/alchemy.link_dialog.js.coffee +5 -5
  6. data/app/assets/javascripts/alchemy/templates/page.hbs +1 -1
  7. data/app/assets/stylesheets/alchemy/_mixins.scss +2 -3
  8. data/app/assets/stylesheets/alchemy/_variables.scss +2 -2
  9. data/app/assets/stylesheets/alchemy/lists.scss +0 -8
  10. data/app/assets/stylesheets/alchemy/nodes.scss +1 -1
  11. data/app/assets/stylesheets/alchemy/sitemap.scss +59 -21
  12. data/app/controllers/alchemy/admin/pages_controller.rb +0 -1
  13. data/app/controllers/alchemy/api/pages_controller.rb +2 -0
  14. data/app/decorators/alchemy/content_editor.rb +55 -0
  15. data/app/helpers/alchemy/admin/pages_helper.rb +16 -16
  16. data/app/models/alchemy/content.rb +8 -22
  17. data/app/models/alchemy/node.rb +8 -7
  18. data/app/models/alchemy/page.rb +11 -0
  19. data/app/models/alchemy/page/url_path.rb +66 -0
  20. data/app/serializers/alchemy/page_serializer.rb +2 -1
  21. data/app/serializers/alchemy/page_tree_serializer.rb +4 -3
  22. data/app/views/alchemy/admin/layoutpages/index.html.erb +5 -1
  23. data/app/views/alchemy/admin/nodes/_form.html.erb +2 -2
  24. data/app/views/alchemy/admin/pages/_form.html.erb +1 -1
  25. data/app/views/alchemy/admin/pages/_menu_fields.html.erb +33 -29
  26. data/app/views/alchemy/admin/pages/_page.html.erb +3 -6
  27. data/app/views/alchemy/admin/pages/_sitemap.html.erb +6 -0
  28. data/app/views/alchemy/admin/pages/info.html.erb +1 -1
  29. data/config/alchemy/config.yml +0 -6
  30. data/config/locales/alchemy.en.yml +6 -6
  31. data/lib/alchemy/config.rb +30 -2
  32. data/lib/alchemy/ssl_protection.rb +3 -1
  33. data/lib/alchemy/upgrader/four_point_six.rb +50 -0
  34. data/lib/alchemy/version.rb +1 -1
  35. data/lib/tasks/alchemy/convert.rake +2 -0
  36. data/lib/tasks/alchemy/upgrade.rake +67 -46
  37. metadata +6 -2
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ class ContentEditor < SimpleDelegator
5
+ alias_method :content, :__getobj__
6
+
7
+ def to_partial_path
8
+ "alchemy/essences/#{essence_partial_name}_editor"
9
+ end
10
+
11
+ def css_classes
12
+ [
13
+ "content_editor",
14
+ essence_partial_name,
15
+ ].compact
16
+ end
17
+
18
+ def data_attributes
19
+ {
20
+ content_id: id,
21
+ content_name: name,
22
+ }
23
+ end
24
+
25
+ # Returns a string to be passed to Rails form field tags to ensure we have same params layout everywhere.
26
+ #
27
+ # === Example:
28
+ #
29
+ # <%= text_field_tag content_editor.form_field_name, content_editor.ingredient %>
30
+ #
31
+ # === Options:
32
+ #
33
+ # You can pass an Essence column_name. Default is 'ingredient'
34
+ #
35
+ # ==== Example:
36
+ #
37
+ # <%= text_field_tag content_editor.form_field_name(:link), content_editor.ingredient %>
38
+ #
39
+ def form_field_name(essence_column = "ingredient")
40
+ "contents[#{id}][#{essence_column}]"
41
+ end
42
+
43
+ def form_field_id(essence_column = "ingredient")
44
+ "contents_#{id}_#{essence_column}"
45
+ end
46
+
47
+ # Fixes Rails partial renderer calling to_model on the object
48
+ # which reveals the delegated content instead of this decorator.
49
+ def respond_to?(method_name)
50
+ return false if method_name == :to_model
51
+
52
+ super
53
+ end
54
+ end
55
+ end
@@ -9,13 +9,13 @@ module Alchemy
9
9
  #
10
10
  def preview_sizes_for_select
11
11
  options_for_select([
12
- 'auto',
13
- [Alchemy.t('240', scope: 'preview_sizes'), 240],
14
- [Alchemy.t('320', scope: 'preview_sizes'), 320],
15
- [Alchemy.t('480', scope: 'preview_sizes'), 480],
16
- [Alchemy.t('768', scope: 'preview_sizes'), 768],
17
- [Alchemy.t('1024', scope: 'preview_sizes'), 1024],
18
- [Alchemy.t('1280', scope: 'preview_sizes'), 1280]
12
+ "auto",
13
+ [Alchemy.t("240", scope: "preview_sizes"), 240],
14
+ [Alchemy.t("320", scope: "preview_sizes"), 320],
15
+ [Alchemy.t("480", scope: "preview_sizes"), 480],
16
+ [Alchemy.t("768", scope: "preview_sizes"), 768],
17
+ [Alchemy.t("1024", scope: "preview_sizes"), 1024],
18
+ [Alchemy.t("1280", scope: "preview_sizes"), 1280],
19
19
  ])
20
20
  end
21
21
 
@@ -27,30 +27,30 @@ module Alchemy
27
27
  if page.persisted? && page.definition.blank?
28
28
  [
29
29
  page_layout_missing_warning,
30
- Alchemy.t(:page_type)
31
- ].join('&nbsp;').html_safe
30
+ Alchemy.t(:page_type),
31
+ ].join("&nbsp;").html_safe
32
32
  else
33
33
  Alchemy.t(:page_type)
34
34
  end
35
35
  end
36
36
 
37
- def page_status_checkbox(page, attribute)
38
- label = page.class.human_attribute_name(attribute)
37
+ def page_status_checkbox(page, attribute, label: nil)
38
+ label_text = label || page.class.human_attribute_name(attribute)
39
39
 
40
40
  if page.attribute_fixed?(attribute)
41
41
  checkbox = check_box(:page, attribute, disabled: true)
42
- hint = content_tag(:span, class: 'hint-bubble') do
42
+ hint = content_tag(:span, class: "hint-bubble") do
43
43
  Alchemy.t(:attribute_fixed, attribute: attribute)
44
44
  end
45
- content = content_tag(:span, class: 'with-hint') do
46
- "#{checkbox}\n#{label}\n#{hint}".html_safe
45
+ content = content_tag(:span, class: "with-hint") do
46
+ "#{checkbox}\n#{label_text}\n#{hint}".html_safe
47
47
  end
48
48
  else
49
49
  checkbox = check_box(:page, attribute)
50
- content = "#{checkbox}\n#{label}".html_safe
50
+ content = "#{checkbox}\n#{label_text}".html_safe
51
51
  end
52
52
 
53
- content_tag(:label, class: 'checkbox') { content }
53
+ content_tag(:label, class: "checkbox") { content }
54
54
  end
55
55
  end
56
56
  end
@@ -172,28 +172,6 @@ module Alchemy
172
172
  definition['validate'].present?
173
173
  end
174
174
 
175
- # Returns a string to be passed to Rails form field tags to ensure we have same params layout everywhere.
176
- #
177
- # === Example:
178
- #
179
- # <%= text_field_tag content.form_field_name, content.ingredient %>
180
- #
181
- # === Options:
182
- #
183
- # You can pass an Essence column_name. Default is 'ingredient'
184
- #
185
- # ==== Example:
186
- #
187
- # <%= text_field_tag content.form_field_name(:link), content.ingredient %>
188
- #
189
- def form_field_name(essence_column = 'ingredient')
190
- "contents[#{id}][#{essence_column}]"
191
- end
192
-
193
- def form_field_id(essence_column = 'ingredient')
194
- "contents_#{id}_#{essence_column}"
195
- end
196
-
197
175
  # Returns a string used as dom id on html elements.
198
176
  def dom_id
199
177
  return '' if essence.nil?
@@ -245,6 +223,14 @@ module Alchemy
245
223
  "has_tinymce" + (has_custom_tinymce_config? ? " #{element.name}_#{name}" : "")
246
224
  end
247
225
 
226
+ def editor
227
+ @_editor ||= ContentEditor.new(self)
228
+ end
229
+ delegate :form_field_name, to: :editor
230
+ deprecate form_field_name: "use Alchemy::ContentEditor#form_field_name instead", deprecator: Alchemy::Deprecation
231
+ delegate :form_field_id, to: :editor
232
+ deprecate form_field_id: "use Alchemy::ContentEditor#form_field_id instead", deprecator: Alchemy::Deprecation
233
+
248
234
  # Returns the default value from content definition
249
235
  #
250
236
  # If the value is a symbol it gets passed through i18n
@@ -4,12 +4,12 @@ module Alchemy
4
4
  class Node < BaseRecord
5
5
  VALID_URL_REGEX = /\A(\/|\D[a-z\+\d\.\-]+:)/
6
6
 
7
- acts_as_nested_set scope: 'language_id', touch: true
7
+ acts_as_nested_set scope: "language_id", touch: true
8
8
  stampable stamper_class_name: Alchemy.user_class_name
9
9
 
10
- belongs_to :site, class_name: 'Alchemy::Site'
11
- belongs_to :language, class_name: 'Alchemy::Language'
12
- belongs_to :page, class_name: 'Alchemy::Page', optional: true, inverse_of: :nodes
10
+ belongs_to :site, class_name: "Alchemy::Site"
11
+ belongs_to :language, class_name: "Alchemy::Language"
12
+ belongs_to :page, class_name: "Alchemy::Page", optional: true, inverse_of: :nodes
13
13
 
14
14
  validates :name, presence: true, if: -> { page.nil? }
15
15
  validates :url, format: { with: VALID_URL_REGEX }, unless: -> { url.nil? }
@@ -25,7 +25,8 @@ module Alchemy
25
25
  class << self
26
26
  # Returns all root nodes for current language
27
27
  def language_root_nodes
28
- raise 'No language found' if Language.current.nil?
28
+ raise "No language found" if Language.current.nil?
29
+
29
30
  roots.where(language_id: Language.current.id)
30
31
  end
31
32
 
@@ -48,7 +49,7 @@ module Alchemy
48
49
  # Returns the +menus.yml+ file path
49
50
  #
50
51
  def definitions_file_path
51
- Rails.root.join 'config/alchemy/menus.yml'
52
+ Rails.root.join "config/alchemy/menus.yml"
52
53
  end
53
54
  end
54
55
 
@@ -57,7 +58,7 @@ module Alchemy
57
58
  # Either the value is stored in the database, aka. an external url.
58
59
  # Or, if attached, the values comes from a page.
59
60
  def url
60
- page && "/#{page.urlname}" || read_attribute(:url).presence
61
+ page&.url_path || read_attribute(:url).presence
61
62
  end
62
63
 
63
64
  def to_partial_path
@@ -161,6 +161,10 @@ module Alchemy
161
161
 
162
162
  attr_accessor :menu_id
163
163
 
164
+ deprecate visible: "Page slugs will be visible in URLs of child pages all the time. " \
165
+ "Please use Menus and Tags instead to re-organize your pages if your page tree does not reflect the URL hierarchy.",
166
+ deprecator: Alchemy::Deprecation
167
+
164
168
  # Class methods
165
169
  #
166
170
  class << self
@@ -347,6 +351,13 @@ module Alchemy
347
351
  finder.elements(page: self)
348
352
  end
349
353
 
354
+ # = The url_path for this page
355
+ #
356
+ # @see Alchemy::Page::UrlPath#call
357
+ def url_path
358
+ Alchemy::Page::UrlPath.new(self).call
359
+ end
360
+
350
361
  # The page's view partial is dependent from its page layout
351
362
  #
352
363
  # == Define page layouts
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ class Page
5
+ # = The url_path for this page
6
+ #
7
+ # Use this to build relative links to this page
8
+ #
9
+ # It takes several circumstances into account:
10
+ #
11
+ # 1. It returns just a slash for language root pages of the default langauge
12
+ # 2. It returns a url path with a leading slash for regular pages
13
+ # 3. It returns a url path with a leading slash and language code prefix for pages not having the default language
14
+ # 4. It returns a url path with a leading slash and the language code for language root pages of a non-default language
15
+ #
16
+ # == Examples
17
+ #
18
+ # Using Rails' link_to helper
19
+ #
20
+ # link_to page.url
21
+ #
22
+ class UrlPath
23
+ ROOT_PATH = "/"
24
+
25
+ def initialize(page)
26
+ @page = page
27
+ @language = @page.language
28
+ @site = @language.site
29
+ end
30
+
31
+ def call
32
+ return @page.urlname if @page.definition["redirects_to_external"]
33
+
34
+ if @page.language_root?
35
+ language_root_path
36
+ elsif @site.languages.select(&:public?).length > 1
37
+ page_path_with_language_prefix
38
+ else
39
+ page_path_with_leading_slash
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def language_root_path
46
+ @language.default? ? ROOT_PATH : language_path
47
+ end
48
+
49
+ def page_path_with_language_prefix
50
+ @language.default? ? page_path : language_path + page_path
51
+ end
52
+
53
+ def page_path_with_leading_slash
54
+ @page.language_root? ? ROOT_PATH : page_path
55
+ end
56
+
57
+ def language_path
58
+ "/#{@page.language_code}"
59
+ end
60
+
61
+ def page_path
62
+ "/#{@page.urlname}"
63
+ end
64
+ end
65
+ end
66
+ end
@@ -13,7 +13,8 @@ module Alchemy
13
13
  :tag_list,
14
14
  :created_at,
15
15
  :updated_at,
16
- :status
16
+ :status,
17
+ :url_path
17
18
 
18
19
  has_many :elements
19
20
  end
@@ -9,7 +9,7 @@ module Alchemy
9
9
  def pages
10
10
  tree = []
11
11
  path = [{id: object.parent_id, children: tree}]
12
- page_list = object.self_and_descendants
12
+ page_list = object.self_and_descendants.includes({ language: :site }, :locker)
13
13
  base_level = object.level - 1
14
14
  # Load folded pages in advance
15
15
  folded_user_pages = FoldedPage.folded_for_user(opts[:user]).pluck(:page_id)
@@ -60,9 +60,10 @@ module Alchemy
60
60
  redirects_to_external: page.definition['redirects_to_external'],
61
61
  urlname: page.urlname,
62
62
  external_urlname: page.definition['redirects_to_external'] ? page.external_urlname : nil,
63
+ url_path: page.url_path,
63
64
  level: level,
64
- root: level == 1,
65
- root_or_leaf: level == 1 || !has_children,
65
+ root: page.depth == 1,
66
+ root_or_leaf: page.depth == 1 || !has_children,
66
67
  children: []
67
68
  }
68
69
 
@@ -30,6 +30,10 @@
30
30
  </div>
31
31
  <% end %>
32
32
 
33
- <ul class="list" id="layoutpages">
33
+ <h4 id="sitemap_heading">
34
+ <span class="page_name"><%= Alchemy::Page.human_attribute_name(:name) %></span>
35
+ </h4>
36
+
37
+ <ul class="list" id="sitemap">
34
38
  <%= render partial: "layoutpage", collection: @layout_root.children %>
35
39
  </ul>
@@ -30,7 +30,7 @@
30
30
  initialSelection: {
31
31
  id: <%= node.page_id %>,
32
32
  text: "<%= node.page.name %>",
33
- url: "/<%= node.page.urlname %>"
33
+ url_path: "<%= node.page.url_path %>"
34
34
  }
35
35
  <% end %>
36
36
  }).on('change', function(e) {
@@ -39,7 +39,7 @@
39
39
  $('#node_url').val('').prop('disabled', false)
40
40
  } else {
41
41
  $('#node_name').attr('placeholder', e.added.name)
42
- $('#node_url').val('/' + e.added.urlname).prop('disabled', true)
42
+ $('#node_url').val(e.added.url_path).prop('disabled', true)
43
43
  }
44
44
  })
45
45
  </script>
@@ -18,7 +18,7 @@
18
18
  </div>
19
19
 
20
20
  <%= f.input :name, autofocus: true %>
21
- <%= f.input :urlname, as: 'string', input_html: {value: @page.slug} %>
21
+ <%= f.input :urlname, as: 'string', input_html: {value: @page.slug}, label: Alchemy::Page.human_attribute_name(:slug) %>
22
22
  <%= f.input :title,
23
23
  input_html: {'data-alchemy-char-counter' => 60} %>
24
24
 
@@ -1,33 +1,37 @@
1
- <% if @page.menus.any? %>
2
- <label class="checkbox">
3
- <input type="checkbox" disabled checked>
4
- <%= Alchemy.t(:attached_to) %>
5
- </label>
6
- <% @page.menus.each do |menu| %>
7
- <span class="page-menu-name label">
8
- <%= menu.name %>
9
- </span>
1
+ <% if Alchemy::Node.roots.where(language: @page.language).any? %>
2
+ <% unless @page.language_root %>
3
+ <%= page_status_checkbox(@page, :visible, label: Alchemy.t("show in url of child pages")) %>
4
+ <% end %>
5
+ <% if @page.menus.any? %>
6
+ <label style="vertical-align: middle">
7
+ <%= Alchemy.t(:attached_to) %>
8
+ </label>
9
+ <% @page.menus.each do |menu| %>
10
+ <span class="page-menu-name label">
11
+ <%= I18n.t(menu.name, scope: [:alchemy, :menu_names]) %>
12
+ </span>
13
+ <% end %>
14
+ <% else %>
15
+ <a class="button small" id="attach-page"><%= Alchemy.t("attach to a menu") %></a>
16
+ <%= f.input :menu_id, collection: Alchemy::Node.roots.map { |n|
17
+ [I18n.t(n.name, scope: [:alchemy, :menu_names]), n.id]
18
+ },
19
+ prompt: Alchemy.t("Please choose a menu"),
20
+ input_html: { class: "alchemy_selectbox" },
21
+ wrapper_html: { class: "hidden" },
22
+ label: false %>
23
+ <script>
24
+ (function() {
25
+ var wrapper = document.querySelector(".input.page_menu_id")
26
+ document.querySelector("#attach-page").addEventListener("click", function() {
27
+ var select = wrapper.querySelector("select")
28
+ this.classList.toggle("active")
29
+ wrapper.classList.toggle("hidden")
30
+ $(select).select2("val", "")
31
+ })
32
+ })()
33
+ </script>
10
34
  <% end %>
11
- <% elsif Alchemy::Node.roots.any? %>
12
- <%= page_status_checkbox(@page, :visible) %>
13
- <%= f.input :menu_id, collection: Alchemy::Node.roots.map { |n| [n.name, n.id] },
14
- prompt: Alchemy.t('Please choose a menu'),
15
- input_html: { class: 'alchemy_selectbox' },
16
- wrapper_html: { style: @page.visible? ? 'display: block' : 'display: none' },
17
- label: false %>
18
- <script>
19
- (function() {
20
- var $wrapper = $('.input.page_menu_id')
21
- $('#page_visible').click(function() {
22
- if ($(this).is(':checked')) {
23
- $wrapper.show()
24
- } else {
25
- $wrapper.find('select').val('')
26
- $wrapper.hide()
27
- }
28
- })
29
- })()
30
- </script>
31
35
  <% else %>
32
36
  <%= page_status_checkbox(@page, :visible) %>
33
37
  <% end %>
@@ -159,12 +159,9 @@
159
159
  <span class="hint-bubble">{{status_titles.restricted}}</span>
160
160
  </span>
161
161
  </div>
162
- {{#if redirects_to_external}}
163
- <div class="redirect_url" title="{{urlname}}">
164
- &raquo; <%= Alchemy.t('Redirects to') %>:
165
- {{ external_urlname }}
166
- </div>
167
- {{/if}}
162
+ <div class="sitemap_url" title="{{url_path}}">
163
+ {{ url_path }}
164
+ </div>
168
165
  <div class="sitemap_sitename">
169
166
  {{#if redirects_to_external}}
170
167
  <span class="sitemap_pagename_link inactive">{{ name }}</span>