alchemy_cms 4.4.3 → 4.6.2

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