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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +23 -17
- data/.rubocop.yml +7 -15
- data/CHANGELOG.md +17 -0
- data/app/assets/javascripts/alchemy/alchemy.link_dialog.js.coffee +5 -5
- data/app/assets/javascripts/alchemy/templates/page.hbs +1 -1
- data/app/assets/stylesheets/alchemy/_mixins.scss +2 -3
- data/app/assets/stylesheets/alchemy/_variables.scss +2 -2
- data/app/assets/stylesheets/alchemy/lists.scss +0 -8
- data/app/assets/stylesheets/alchemy/nodes.scss +1 -1
- data/app/assets/stylesheets/alchemy/sitemap.scss +59 -21
- data/app/controllers/alchemy/admin/pages_controller.rb +0 -1
- data/app/controllers/alchemy/api/pages_controller.rb +2 -0
- data/app/decorators/alchemy/content_editor.rb +55 -0
- data/app/helpers/alchemy/admin/pages_helper.rb +16 -16
- data/app/models/alchemy/content.rb +8 -22
- data/app/models/alchemy/node.rb +8 -7
- data/app/models/alchemy/page.rb +11 -0
- data/app/models/alchemy/page/url_path.rb +66 -0
- data/app/serializers/alchemy/page_serializer.rb +2 -1
- data/app/serializers/alchemy/page_tree_serializer.rb +4 -3
- data/app/views/alchemy/admin/layoutpages/index.html.erb +5 -1
- data/app/views/alchemy/admin/nodes/_form.html.erb +2 -2
- data/app/views/alchemy/admin/pages/_form.html.erb +1 -1
- data/app/views/alchemy/admin/pages/_menu_fields.html.erb +33 -29
- data/app/views/alchemy/admin/pages/_page.html.erb +3 -6
- data/app/views/alchemy/admin/pages/_sitemap.html.erb +6 -0
- data/app/views/alchemy/admin/pages/info.html.erb +1 -1
- data/config/alchemy/config.yml +0 -6
- data/config/locales/alchemy.en.yml +6 -6
- data/lib/alchemy/config.rb +30 -2
- data/lib/alchemy/ssl_protection.rb +3 -1
- data/lib/alchemy/upgrader/four_point_six.rb +50 -0
- data/lib/alchemy/version.rb +1 -1
- data/lib/tasks/alchemy/convert.rake +2 -0
- data/lib/tasks/alchemy/upgrade.rake +67 -46
- 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
|
-
|
13
|
-
[Alchemy.t(
|
14
|
-
[Alchemy.t(
|
15
|
-
[Alchemy.t(
|
16
|
-
[Alchemy.t(
|
17
|
-
[Alchemy.t(
|
18
|
-
[Alchemy.t(
|
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(
|
30
|
+
Alchemy.t(:page_type),
|
31
|
+
].join(" ").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
|
-
|
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:
|
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:
|
46
|
-
"#{checkbox}\n#{
|
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#{
|
50
|
+
content = "#{checkbox}\n#{label_text}".html_safe
|
51
51
|
end
|
52
52
|
|
53
|
-
content_tag(:label, class:
|
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
|
data/app/models/alchemy/node.rb
CHANGED
@@ -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:
|
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:
|
11
|
-
belongs_to :language, class_name:
|
12
|
-
belongs_to :page, class_name:
|
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
|
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
|
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
|
61
|
+
page&.url_path || read_attribute(:url).presence
|
61
62
|
end
|
62
63
|
|
63
64
|
def to_partial_path
|
data/app/models/alchemy/page.rb
CHANGED
@@ -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
|
@@ -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:
|
65
|
-
root_or_leaf:
|
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
|
-
<
|
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
|
-
|
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(
|
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.
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
-
{{
|
163
|
-
|
164
|
-
|
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>
|