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.
- 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>
|