alchemy_cms 4.4.3 → 4.6.2

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 (59) 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 +35 -1
  5. data/alchemy_cms.gemspec +1 -0
  6. data/app/assets/javascripts/alchemy/admin.js +3 -0
  7. data/app/assets/javascripts/alchemy/alchemy.link_dialog.js.coffee +5 -5
  8. data/app/assets/javascripts/alchemy/alchemy.node_tree.js +66 -0
  9. data/app/assets/javascripts/alchemy/alchemy.utils.js +45 -0
  10. data/app/assets/javascripts/alchemy/templates/index.js +1 -0
  11. data/app/assets/javascripts/alchemy/templates/node_folder.hbs +3 -0
  12. data/app/assets/javascripts/alchemy/templates/page.hbs +1 -1
  13. data/app/assets/stylesheets/alchemy/_mixins.scss +2 -3
  14. data/app/assets/stylesheets/alchemy/_variables.scss +2 -2
  15. data/app/assets/stylesheets/alchemy/lists.scss +0 -8
  16. data/app/assets/stylesheets/alchemy/nodes.scss +6 -1
  17. data/app/assets/stylesheets/alchemy/sitemap.scss +59 -21
  18. data/app/controllers/alchemy/admin/dashboard_controller.rb +1 -1
  19. data/app/controllers/alchemy/admin/nodes_controller.rb +0 -10
  20. data/app/controllers/alchemy/admin/pages_controller.rb +0 -1
  21. data/app/controllers/alchemy/api/nodes_controller.rb +29 -0
  22. data/app/controllers/alchemy/api/pages_controller.rb +2 -0
  23. data/app/decorators/alchemy/content_editor.rb +55 -0
  24. data/app/helpers/alchemy/admin/pages_helper.rb +16 -16
  25. data/app/helpers/alchemy/pages_helper.rb +1 -1
  26. data/app/models/alchemy/content.rb +8 -22
  27. data/app/models/alchemy/node.rb +29 -6
  28. data/app/models/alchemy/page.rb +11 -0
  29. data/app/models/alchemy/page/page_naming.rb +1 -1
  30. data/app/models/alchemy/page/url_path.rb +66 -0
  31. data/app/serializers/alchemy/node_serializer.rb +12 -0
  32. data/app/serializers/alchemy/page_serializer.rb +2 -1
  33. data/app/serializers/alchemy/page_tree_serializer.rb +4 -3
  34. data/app/views/alchemy/admin/layoutpages/index.html.erb +5 -1
  35. data/app/views/alchemy/admin/nodes/_form.html.erb +13 -8
  36. data/app/views/alchemy/admin/nodes/_node.html.erb +10 -20
  37. data/app/views/alchemy/admin/nodes/index.html.erb +7 -17
  38. data/app/views/alchemy/admin/pages/_form.html.erb +1 -1
  39. data/app/views/alchemy/admin/pages/_menu_fields.html.erb +33 -29
  40. data/app/views/alchemy/admin/pages/_page.html.erb +3 -6
  41. data/app/views/alchemy/admin/pages/_sitemap.html.erb +6 -0
  42. data/app/views/alchemy/admin/pages/info.html.erb +1 -1
  43. data/app/views/alchemy/admin/partials/_routes.html.erb +8 -0
  44. data/config/alchemy/config.yml +0 -6
  45. data/config/locales/alchemy.en.yml +14 -6
  46. data/config/routes.rb +8 -5
  47. data/lib/alchemy/config.rb +30 -2
  48. data/lib/alchemy/ssl_protection.rb +3 -1
  49. data/lib/alchemy/upgrader/four_point_six.rb +50 -0
  50. data/lib/alchemy/version.rb +1 -1
  51. data/lib/rails/generators/alchemy/install/install_generator.rb +1 -1
  52. data/lib/rails/generators/alchemy/install/templates/menus.yml.tt +8 -0
  53. data/lib/rails/generators/alchemy/menus/menus_generator.rb +3 -3
  54. data/lib/rails/generators/alchemy/menus/templates/wrapper.html.haml +1 -1
  55. data/lib/rails/generators/alchemy/menus/templates/wrapper.html.slim +1 -1
  56. data/lib/tasks/alchemy/convert.rake +2 -0
  57. data/lib/tasks/alchemy/upgrade.rake +67 -46
  58. data/vendor/assets/javascripts/sortable/Sortable.min.js +2 -0
  59. metadata +29 -4
@@ -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
@@ -68,7 +68,7 @@ module Alchemy
68
68
  base.push(parent) if parent.visible?
69
69
  end
70
70
  else
71
- ancestors.visible.contentpages.where(language_root: nil).to_a
71
+ ancestors.visible.contentpages.where(language_root: [nil, false]).to_a
72
72
  end
73
73
  end
74
74
 
@@ -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
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ class NodeSerializer < ActiveModel::Serializer
5
+ attributes :id,
6
+ :name,
7
+ :lft,
8
+ :rgt,
9
+ :url,
10
+ :parent_id
11
+ end
12
+ 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>
@@ -1,10 +1,15 @@
1
1
  <%= alchemy_form_for([:admin, node]) do |f| %>
2
- <%= f.input :name, input_html: {
3
- autofocus: true,
4
- value: node.page && node.read_attribute(:name).blank? ? nil : node.name,
5
- placeholder: node.page ? node.page.name : nil
6
- } %>
7
- <% unless node.root? %>
2
+ <% if node.root? %>
3
+ <%= f.input :name,
4
+ collection: Alchemy::Node.available_menu_names.map { |n| [I18n.t(n, scope: [:alchemy, :menu_names]), n] },
5
+ include_blank: false,
6
+ input_html: { class: 'alchemy_selectbox' } %>
7
+ <% else %>
8
+ <%= f.input :name, input_html: {
9
+ autofocus: true,
10
+ value: node.page && node.read_attribute(:name).blank? ? nil : node.name,
11
+ placeholder: node.page ? node.page.name : nil
12
+ } %>
8
13
  <%= f.input :page_id, label: Alchemy::Page.model_name.human, input_html: { class: 'alchemy_selectbox' } %>
9
14
  <%= f.input :url, input_html: { disabled: node.page }, hint: Alchemy.t(:node_url_hint) %>
10
15
  <%= f.input :title %>
@@ -25,7 +30,7 @@
25
30
  initialSelection: {
26
31
  id: <%= node.page_id %>,
27
32
  text: "<%= node.page.name %>",
28
- url: "/<%= node.page.urlname %>"
33
+ url_path: "<%= node.page.url_path %>"
29
34
  }
30
35
  <% end %>
31
36
  }).on('change', function(e) {
@@ -34,7 +39,7 @@
34
39
  $('#node_url').val('').prop('disabled', false)
35
40
  } else {
36
41
  $('#node_name').attr('placeholder', e.added.name)
37
- $('#node_url').val('/' + e.added.urlname).prop('disabled', true)
42
+ $('#node_url').val(e.added.url_path).prop('disabled', true)
38
43
  }
39
44
  })
40
45
  </script>
@@ -1,21 +1,11 @@
1
- <li>
1
+ <%= content_tag :li, class: 'menu-item', data: { id: node.id, parent_id: node.parent_id, folded: node.folded? } do %>
2
2
  <%= content_tag :div, class: [
3
3
  'sitemap_node',
4
4
  node.external? ? 'external' : 'internal',
5
5
  "sitemap_node-level_#{node.depth}"
6
6
  ] do %>
7
7
  <span class="nodes_tree-left_images">
8
- <% if node.children.any? %>
9
- <a class="node_folder" data-node-id="<%= node.id %>">
10
- <% if node.folded? %>
11
- <i class="far fa-plus-square fa-fw"></i>
12
- <% else %>
13
- <i class="far fa-minus-square fa-fw"></i>
14
- <% end %>
15
- </a>
16
- <% else %>
17
- &nbsp;
18
- <% end %>
8
+ &nbsp;
19
9
  </span>
20
10
  <span class="nodes_tree-right_tools">
21
11
  <% if can?(:edit, node) %>
@@ -57,7 +47,11 @@
57
47
  <% end %>
58
48
  </span>
59
49
  <div class="node_name">
60
- <%= node.name || '&nbsp;'.html_safe %>
50
+ <% if node.root? %>
51
+ <%= I18n.t(node.name, scope: [:alchemy, :menu_names]) %>
52
+ <% else %>
53
+ <%= node.name || '&nbsp;'.html_safe %>
54
+ <% end %>
61
55
  <span class="node_page">
62
56
  <% if node.page %>
63
57
  <i class="icon far fa-file"></i>
@@ -77,11 +71,7 @@
77
71
  <% end %>
78
72
  </div>
79
73
  <% end %>
80
- <% if node.children.any? %>
81
- <ul class="children<%= node.folded? ? ' hidden' : nil %>">
82
- <% unless node.folded? %>
83
- <%= render partial: 'node', collection: node.children.includes(:page, :children) %>
84
- <% end %>
85
- </ul>
74
+ <%= content_tag :ul, class: "children #{' folded' if node.folded?}", data: { node_id: node.id } do %>
75
+ <%= render partial: 'node', collection: node.children.includes(:page, :children) %>
86
76
  <% end %>
87
- </li>
77
+ <% end %>
@@ -33,26 +33,16 @@
33
33
  <%= render_message do %>
34
34
  <%= Alchemy.t(:no_resource_found) % { resource: Alchemy.t(:menu) } %>
35
35
  <% end %>
36
- <%= render 'form', node: Alchemy::Node.new(language: Alchemy::Language.current), button_label: Alchemy.t(:create) %>
36
+ <%= render 'form', node: Alchemy::Node.new(
37
+ site: Alchemy::Site.current,
38
+ language: Alchemy::Language.current
39
+ ),
40
+ button_label: Alchemy.t(:create) %>
37
41
  </div>
38
42
  <% end %>
39
43
  </div>
40
44
 
41
45
  <script>
42
- $('.nodes_tree').on('click', '.node_folder', function() {
43
- var $this = $(this)
44
- var node_id = $this.data('node-id')
45
- var url = '<%= alchemy.toggle_admin_node_path(id: ":id") %>'.replace(':id', node_id)
46
- var $children = $this.closest('li').find('> .children')
47
- $this.find('> i').
48
- toggleClass('fa-plus-square').
49
- toggleClass('fa-minus-square')
50
- $children.toggleClass('hidden')
51
- $.ajax(url, { method: 'PATCH' }).then(function (nodes) {
52
- if ($children.children().length === 0) {
53
- $children.append(nodes)
54
- }
55
- })
56
- return false
57
- })
46
+ Alchemy.NodeTree.init()
47
+
58
48
  </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>
@@ -1,4 +1,10 @@
1
1
  <div id="sitemap-wrapper">
2
+ <h4 id="sitemap_heading">
3
+ <span class="page_name"><%= Alchemy::Page.human_attribute_name(:name) %></span>
4
+ <span class="page_urlname"><%= Alchemy::Page.human_attribute_name(:urlname) %></span>
5
+ <span class="page_status"><%= Alchemy.t(:page_status) %></span>
6
+ </h4>
7
+
2
8
  <p class="loading"></p>
3
9
  </div>
4
10
 
@@ -18,7 +18,7 @@
18
18
  <label><%= Alchemy::Page.human_attribute_name(:urlname) %></label>
19
19
  <p><%= @page.urlname %></p>
20
20
  <% else %>
21
- <label><%= Alchemy::LegacyPageUrl.human_attribute_name(:urlname) %></label>
21
+ <label><%= Alchemy::Page.human_attribute_name(:urlname) %></label>
22
22
  <p><%= "/#{@page.urlname}" %></p>
23
23
  <% end %>
24
24
  </div>
@@ -15,6 +15,14 @@
15
15
  return '<%= alchemy.fold_admin_element_path(id: 1) %>'.replace(/1/, id);
16
16
  },
17
17
 
18
+ toggle_folded_api_node_path: function(id) {
19
+ return '<%= alchemy.toggle_folded_api_node_path(id: 1) %>'.replace(/1/, id);
20
+ },
21
+
22
+ move_api_node_path: function(id) {
23
+ return '<%= alchemy.move_api_node_path(id: 1) %>'.replace(/1/, id);
24
+ },
25
+
18
26
  order_admin_elements_path: '<%= alchemy.order_admin_elements_path %>',
19
27
  order_admin_pages_path: '<%= alchemy.order_admin_pages_path %>',
20
28
  link_admin_pages_path: '<%= alchemy.link_admin_pages_path %>',
@@ -1,12 +1,6 @@
1
1
  # == This is the global Alchemy configuration file
2
2
  #
3
3
 
4
- # === Require SSL for login form and all admin modules
5
- #
6
- # NOTE: You have to create a SSL certificate on your server to make this work
7
- #
8
- require_ssl: false
9
-
10
4
  # === Auto Log Out Time
11
5
  #
12
6
  # The amount of time of inactivity in minutes after which the user is kicked out of his current session.
@@ -49,6 +49,14 @@ en:
49
49
  #
50
50
  content_names:
51
51
 
52
+
53
+ # === Translations for menu names
54
+ # Used for the translations of the names of root menu nodes.
55
+ #
56
+ menu_names:
57
+ main_menu: Main Menu
58
+ footer_menu: Footer Menu
59
+
52
60
  # === Translations for content validations
53
61
  # Used when a user did not enter (correct) values to the content field.
54
62
  #
@@ -565,7 +573,6 @@ en:
565
573
  button_label: Upload image(s)
566
574
  upload_success: "Picture %{name} uploaded successfully"
567
575
  upload_failure: "Error while uploading %{name}: %{error}"
568
- url_name: "URL-Name"
569
576
  visible: "visible"
570
577
  want_to_create_new_language: "Do you want to create a new empty language tree?"
571
578
  want_to_make_copy_of_existing_language: "Do you want to copy an existing language tree?"
@@ -662,9 +669,9 @@ en:
662
669
  page_layout:
663
670
  blank: "^Please choose a page layout."
664
671
  urlname:
665
- too_short: "^The pages urlname is too short (minimum of 3 characters)."
666
- taken: "^URL-Name already taken."
667
- exclusion: "^URL-Name reserved."
672
+ too_short: "^URL-Path is too short (minimum of 3 characters)."
673
+ taken: "^URL-Path already taken."
674
+ exclusion: "^URL-Path reserved."
668
675
  alchemy/picture:
669
676
  attributes:
670
677
  image_file:
@@ -772,7 +779,7 @@ en:
772
779
  locale: Localization
773
780
  code: ISO Code
774
781
  alchemy/legacy_page_url:
775
- urlname: "URL path"
782
+ urlname: "URL-Path"
776
783
  alchemy/node:
777
784
  name: "Name"
778
785
  title: "Title"
@@ -797,7 +804,8 @@ en:
797
804
  tag_list: Tags
798
805
  title: "Title"
799
806
  updated_at: "Updated at"
800
- urlname: "Urlname"
807
+ urlname: "URL-Path"
808
+ slug: "Slug"
801
809
  visible: "visible in navigation"
802
810
  alchemy/picture:
803
811
  image_file_name: "Filename"