alchemy_cms 4.5.0 → 4.6.0

Sign up to get free protection for your applications and to get access to all the features.
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>