alchemy_cms 8.1.12 → 8.2.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.
Files changed (118) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +12 -1
  3. data/app/assets/builds/alchemy/admin.css +1 -1
  4. data/app/assets/builds/alchemy/alchemy_admin.min.js +1 -1
  5. data/app/assets/builds/alchemy/alchemy_admin.min.js.map +1 -1
  6. data/app/assets/builds/alchemy/dark-theme.css +1 -1
  7. data/app/assets/builds/alchemy/light-theme.css +1 -1
  8. data/app/assets/builds/alchemy/theme.css +1 -1
  9. data/app/assets/builds/alchemy/welcome.css +1 -1
  10. data/app/assets/builds/tinymce/skins/content/alchemy/content.min.css +1 -1
  11. data/app/assets/builds/tinymce/skins/content/alchemy-dark/content.min.css +1 -1
  12. data/app/assets/images/alchemy/icons-sprite.svg +1 -1
  13. data/app/components/alchemy/admin/current_user_name.rb +34 -0
  14. data/app/components/alchemy/admin/locale_select.rb +12 -8
  15. data/app/components/alchemy/admin/page_node.html.erb +3 -2
  16. data/app/components/alchemy/admin/picture_thumbnail.rb +1 -1
  17. data/app/components/alchemy/admin/preview_time_select.rb +55 -0
  18. data/app/components/alchemy/admin/publish_element_button.html.erb +41 -0
  19. data/app/components/alchemy/admin/publish_element_button.rb +13 -0
  20. data/app/components/alchemy/admin/timezone_select.rb +47 -0
  21. data/app/components/alchemy/ingredients/select_editor.rb +6 -1
  22. data/app/controllers/alchemy/admin/attachments_controller.rb +0 -9
  23. data/app/controllers/alchemy/admin/base_controller.rb +1 -0
  24. data/app/controllers/alchemy/admin/elements_controller.rb +54 -34
  25. data/app/controllers/alchemy/admin/pages_controller.rb +1 -0
  26. data/app/controllers/alchemy/admin/pictures_controller.rb +2 -23
  27. data/app/controllers/alchemy/admin/resources_controller.rb +11 -6
  28. data/app/controllers/alchemy/pages_controller.rb +1 -2
  29. data/app/helpers/alchemy/admin/base_helper.rb +4 -7
  30. data/app/helpers/alchemy/url_helper.rb +2 -10
  31. data/app/javascript/alchemy_admin/components/element_editor/publish_element_button.js +28 -27
  32. data/app/javascript/alchemy_admin/components/element_editor.js +11 -2
  33. data/app/javascript/alchemy_admin/components/message.js +5 -1
  34. data/app/javascript/alchemy_admin/components/picture_thumbnail.js +1 -0
  35. data/app/javascript/alchemy_admin/components/uploader/file_upload.js +5 -5
  36. data/app/javascript/alchemy_admin/image_cropper.js +10 -6
  37. data/app/javascript/alchemy_admin/initializer.js +6 -33
  38. data/app/javascript/alchemy_admin/shoelace_theme.js +6 -2
  39. data/app/javascript/alchemy_admin/templates/compiled.js +1 -1
  40. data/app/javascript/alchemy_admin.js +12 -2
  41. data/app/models/alchemy/attachment.rb +1 -33
  42. data/app/models/alchemy/current.rb +5 -1
  43. data/app/models/alchemy/element/element_ingredients.rb +11 -3
  44. data/app/models/alchemy/element.rb +10 -0
  45. data/app/models/alchemy/ingredient.rb +2 -0
  46. data/app/models/alchemy/ingredients/select.rb +1 -2
  47. data/app/models/alchemy/page/etag_generator.rb +21 -0
  48. data/app/models/alchemy/page/url_path.rb +11 -2
  49. data/app/models/alchemy/page.rb +12 -2
  50. data/app/models/alchemy/page_version.rb +5 -5
  51. data/app/models/alchemy/picture.rb +19 -2
  52. data/app/models/alchemy/storage_adapter/active_storage.rb +9 -0
  53. data/app/models/alchemy/storage_adapter/dragonfly.rb +9 -0
  54. data/app/models/alchemy/storage_adapter.rb +1 -0
  55. data/app/models/concerns/alchemy/publishable.rb +20 -12
  56. data/app/models/concerns/alchemy/relatable_resource.rb +19 -15
  57. data/app/models/concerns/alchemy/touch_elements.rb +3 -3
  58. data/app/services/alchemy/element_preloader.rb +107 -0
  59. data/app/stylesheets/alchemy/_custom-properties.scss +1 -0
  60. data/app/stylesheets/alchemy/_mixins.scss +1 -1
  61. data/app/stylesheets/alchemy/_themes.scss +2 -0
  62. data/app/stylesheets/alchemy/admin/archive.scss +2 -2
  63. data/app/stylesheets/alchemy/admin/base.scss +2 -1
  64. data/app/stylesheets/alchemy/admin/elements.scss +22 -19
  65. data/app/stylesheets/alchemy/admin/form_fields.scss +3 -0
  66. data/app/stylesheets/alchemy/admin/forms.scss +14 -1
  67. data/app/stylesheets/alchemy/admin/frame.scss +9 -8
  68. data/app/stylesheets/alchemy/admin/images.scss +2 -2
  69. data/app/stylesheets/alchemy/admin/notices.scss +1 -10
  70. data/app/stylesheets/alchemy/admin/popover.scss +37 -0
  71. data/app/stylesheets/alchemy/admin/selects.scss +4 -0
  72. data/app/stylesheets/alchemy/admin/shoelace.scss +16 -4
  73. data/app/stylesheets/alchemy/admin/toolbar.scss +8 -0
  74. data/app/stylesheets/alchemy/admin.scss +1 -0
  75. data/app/views/alchemy/admin/_header.html.erb +4 -0
  76. data/app/views/alchemy/admin/_left_menu.html.erb +24 -0
  77. data/app/views/alchemy/admin/_main_navi.html.erb +6 -0
  78. data/app/views/alchemy/admin/_top_menu.html.erb +6 -0
  79. data/app/views/alchemy/admin/_user_info.html.erb +5 -0
  80. data/app/views/alchemy/admin/attachments/_files_list.html.erb +1 -1
  81. data/app/views/alchemy/admin/crop.html.erb +6 -11
  82. data/app/views/alchemy/admin/elements/_header.html.erb +16 -6
  83. data/app/views/alchemy/admin/elements/_schedule.html.erb +62 -0
  84. data/app/views/alchemy/admin/elements/_toolbar.html.erb +1 -15
  85. data/app/views/alchemy/admin/elements/publish.turbo_stream.erb +28 -0
  86. data/app/views/alchemy/admin/nodes/index.html.erb +1 -1
  87. data/app/views/alchemy/admin/pages/_locked_pages.html.erb +5 -0
  88. data/app/views/alchemy/admin/pages/_publication_fields.html.erb +4 -4
  89. data/app/views/alchemy/admin/pages/_table.html.erb +2 -2
  90. data/app/views/alchemy/admin/pages/edit.html.erb +6 -2
  91. data/app/views/alchemy/admin/partials/_language_tree_select.html.erb +10 -10
  92. data/app/views/alchemy/admin/partials/_site_select.html.erb +6 -3
  93. data/app/views/alchemy/admin/pictures/_filter_and_size_bar.html.erb +3 -3
  94. data/app/views/alchemy/admin/pictures/_picture.html.erb +1 -1
  95. data/app/views/alchemy/admin/pictures/_picture_to_assign.html.erb +1 -1
  96. data/app/views/alchemy/admin/pictures/index.html.erb +2 -2
  97. data/app/views/alchemy/admin/tinymce/_setup.html.erb +9 -16
  98. data/app/views/alchemy/admin/uploader/_setup.html.erb +1 -6
  99. data/app/views/alchemy/language_links/_language.html.erb +1 -2
  100. data/app/views/layouts/alchemy/admin.html.erb +2 -45
  101. data/config/importmap.rb +7 -2
  102. data/config/locales/alchemy.en.yml +35 -5
  103. data/lib/alchemy/admin/preview_time.rb +23 -0
  104. data/lib/alchemy/admin/preview_url.rb +13 -2
  105. data/lib/alchemy/admin/timezone.rb +56 -0
  106. data/lib/alchemy/configurations/main.rb +13 -1
  107. data/lib/alchemy/test_support/factories/element_factory.rb +2 -2
  108. data/lib/alchemy/test_support/relatable_resource_examples.rb +2 -2
  109. data/lib/alchemy/test_support/shared_publishable_examples.rb +44 -2
  110. data/lib/alchemy/upgrader.rb +3 -1
  111. data/lib/alchemy/version.rb +1 -1
  112. data/lib/alchemy_cms.rb +2 -0
  113. data/lib/generators/alchemy/install/install_generator.rb +2 -1
  114. data/vendor/javascript/handlebars.min.js +4 -4
  115. data/vendor/javascript/shoelace.min.js +1419 -1323
  116. data/vendor/javascript/sortable.min.js +2 -2
  117. data/vendor/javascript/tinymce.min.js +1 -1
  118. metadata +33 -1
@@ -9,20 +9,13 @@
9
9
  <% end %>
10
10
 
11
11
  <script>
12
- // Setting TinyMCE path.
13
- var tinyMCEPreInit = {
14
- <% if ActionController::Base.config.asset_host %>
15
- base: '<%= asset_url(tinymce_base_path, host: ActionController::Base.config.asset_host) %>',
16
- <% else %>
17
- base: '<%= tinymce_base_path %>',
18
- <% end %>
19
- suffix: '.min'
20
- };
21
- // Holds the default Alchemy TinyMCE configuration
22
- Alchemy.TinymceDefaults = {
23
- plugins: '<%= Alchemy::Tinymce.plugins.join(',') %>',
24
- <% Alchemy::Tinymce.init.each do |k, v| %>
25
- <%= k %>: <%== v.to_json %>,
26
- <% end %>
27
- };
12
+ var tinyMCEPreInit = <%== {
13
+ base: ActionController::Base.config.asset_host ?
14
+ asset_url(tinymce_base_path, host: ActionController::Base.config.asset_host) :
15
+ tinymce_base_path,
16
+ suffix: ".min"
17
+ }.to_json %>;
18
+ Alchemy.TinymceDefaults = <%== Alchemy::Tinymce.init.merge(
19
+ plugins: Alchemy::Tinymce.plugins.join(",")
20
+ ).to_json %>;
28
21
  </script>
@@ -1,8 +1,3 @@
1
1
  <script>
2
- Alchemy.uploader_defaults = {
3
- file_size_limit: <%= Alchemy.config.uploader.file_size_limit -%>,
4
- upload_limit: <%= Alchemy.config.uploader.upload_limit -%>,
5
- allowed_filetype_pictures: "<%= Alchemy.config.uploader.allowed_filetypes.alchemy_pictures.join(", ") -%>",
6
- allowed_filetype_attachments: "<%= Alchemy.config.uploader.allowed_filetypes.alchemy_attachments.join(", ") -%>",
7
- }
2
+ Alchemy.uploader_defaults = <%== Alchemy.config.uploader.to_json %>
8
3
  </script>
@@ -1,8 +1,7 @@
1
1
  <%= link_to(
2
2
  content_tag(:span, language.label(options[:linkname])).html_safe,
3
3
  show_alchemy_page_path(
4
- language.pages.language_roots.first,
5
- locale: prefix_locale?(language.code) ? language.code : nil
4
+ language.pages.language_roots.first
6
5
  ),
7
6
  class: [
8
7
  language.code,
@@ -44,52 +44,9 @@
44
44
  <p><%= Alchemy.t(:javascript_disabled_text) %></p>
45
45
  </noscript>
46
46
  <alchemy-overlay text="<%= Alchemy.t(:please_wait) %>"></alchemy-overlay>
47
- <div id="left_menu">
48
- <div id="main_navi">
49
- <% sorted_alchemy_modules.each do |alchemy_module| %>
50
- <%= alchemy_main_navigation_entry(alchemy_module) %>
51
- <% end %>
52
- <%= yield(:alchemy_main_navigation) %>
53
- </div>
54
-
55
- <div id="logout">
56
- <div class="main_navi_entry">
57
- <% if current_alchemy_user %>
58
- <%= link_to_dialog(
59
- %(
60
- #{render_icon('logout-box-r', class: 'module')}
61
- <label>#{Alchemy.t(:leave)}</label>
62
- ).html_safe,
63
- alchemy.leave_admin_path, {
64
- size: "320x140",
65
- title: Alchemy.t("Leave Alchemy")
66
- }, {'data-alchemy-hotkey' => 'alt+q'}) %>
67
- <% else %>
68
- <%= link_to(alchemy.root_path) do %>
69
- <%= render_icon "logout-box-r", size: "lg" %>
70
- <label><%= Alchemy.t(:leave) %></label>
71
- <% end %>
72
- <% end %>
73
- </div>
74
- </div>
75
- </div>
47
+ <%= render "alchemy/admin/left_menu" %>
76
48
  <% if current_alchemy_user %>
77
- <div id="top_menu">
78
- <div id="header">
79
- <% if @locked_pages.present? %>
80
- <div id="locked_pages">
81
- <%= render partial: 'alchemy/admin/pages/locked_page', collection: @locked_pages %>
82
- </div>
83
- <% end %>
84
- <div id="user_info">
85
- <%= current_alchemy_user_name %>
86
- <%= render Alchemy::Admin::LocaleSelect.new %>
87
- </div>
88
- </div>
89
- <div id="toolbar">
90
- <%= yield(:toolbar) %>
91
- </div>
92
- </div>
49
+ <%= render "alchemy/admin/top_menu", locked_pages: @locked_pages %>
93
50
  <% end %>
94
51
  <%= render 'alchemy/admin/partials/flash_notices' %>
95
52
  <div id="main_content">
data/config/importmap.rb CHANGED
@@ -12,5 +12,10 @@ pin "shoelace", to: "shoelace.min.js", preload: true
12
12
  pin "@rails/ujs", to: "rails-ujs.min.js", preload: true # @7.1.2
13
13
  pin "tinymce", to: "tinymce.min.js", preload: true
14
14
 
15
- pin "alchemy_admin", to: "alchemy_admin.js", preload: true
16
- pin_all_from File.expand_path("../app/javascript/alchemy_admin", __dir__), under: "alchemy_admin", preload: true
15
+ pin "alchemy_admin", to: "alchemy/alchemy_admin.min.js", preload: true
16
+ pin "alchemy_admin/components/remote_select", to: "alchemy/alchemy_admin.min.js"
17
+ pin "alchemy_admin/image_cropper", to: "alchemy/alchemy_admin.min.js"
18
+ pin "alchemy_admin/image_overlay", to: "alchemy/alchemy_admin.min.js"
19
+ pin "alchemy_admin/picture_selector", to: "alchemy/alchemy_admin.min.js"
20
+ pin "alchemy_admin/node_tree", to: "alchemy/alchemy_admin.min.js"
21
+ pin "alchemy_admin/utils/events", to: "alchemy/alchemy_admin.min.js"
@@ -233,6 +233,15 @@ en:
233
233
  elements:
234
234
  toolbar:
235
235
  hide: Hide
236
+ schedule:
237
+ visibility_status:
238
+ public_html: This element is <b>currently visible</b>.
239
+ hidden_html: This element is <b>currently hidden</b>.
240
+ public_no_schedule: Set a "visible until" date to automatically hide it.
241
+ public_scheduled_html: It will be hidden on <b>%{public_until}</b>.
242
+ hidden_no_schedule: Change dates to make it visible again.
243
+ hidden_scheduled_html: It will be visible from <b>%{public_on}</b>.
244
+ hidden_window_html: It will be visible from <b>%{public_on}</b> until <b>%{public_until}</b>.
236
245
  pictures:
237
246
  sorting_select:
238
247
  label: "Sorting"
@@ -390,6 +399,7 @@ en:
390
399
  cannot_visit_unpublic_page: "Publish page before visiting it."
391
400
  choose_file_to_link: "Please choose a file to link"
392
401
  "clear clipboard": "clear clipboard"
402
+ clear_schedule: Clear Schedule
393
403
  click_to_show_all: "Click to show all again."
394
404
  confirm_to_delete_element: "Do you really want to delete this element? It cannot be restored!"
395
405
  confirm_to_delete_file: "Do you really want to delete this file from the server?"
@@ -446,6 +456,9 @@ en:
446
456
  element_hidden: "Hidden"
447
457
  element_of_type: "Element"
448
458
  element_saved: "Saved element."
459
+ element_scheduled:
460
+ public_on: "visible from %{public_on}"
461
+ public_until: "visible until %{public_until}"
449
462
  enter_external_link: "Please enter the URL you want to link with"
450
463
  explain_cropping: '<p>Move the frame and change its size with the mouse or arrow keys to adjust the image mask. Click on "apply" when you are satisfied with your selection.</p><p>If you want to return to the original centered image mask like it was defined in the layout, click "reset" and "apply" afterwards.</p>'
451
464
  explain_publishing: "Publish current page content"
@@ -596,7 +609,9 @@ en:
596
609
  "1024": "iPad Landscape (1024px)"
597
610
  "1280": "Laptop (1280px)"
598
611
  "1440": "Desktop (1440px)"
612
+ preview_time: "Preview Time"
599
613
  preview_url: Preview
614
+ now: "Now"
600
615
  publish_page_language_not_public: Cannot publish page if language is not public
601
616
  publish_page_not_allowed: You have not the permission to publish this page
602
617
  recently_uploaded_only: "Recently uploaded only"
@@ -625,6 +640,7 @@ en:
625
640
  fulltext_search: "Fulltext search"
626
641
  select_element: "Select element"
627
642
  seperate_tags_with_comma: "Seperate tags with comma"
643
+ schedule_element: Schedule visibility
628
644
  show_element_content: "Show content of this element."
629
645
  show_eq: "Show EQ"
630
646
  show_navigation: "Show in navigation"
@@ -635,6 +651,7 @@ en:
635
651
  successfully_added_element: "Successfully added new element."
636
652
  successfully_deleted_tag: "Successfully deleted tag"
637
653
  successfully_saved_element_position: "Element position updated successfully."
654
+ successfully_scheduled_element: "Successfully scheduled element."
638
655
  successfully_updated_tag: "Successfully updated tag"
639
656
  swap_image: "Change image"
640
657
  insert_image: "Insert image"
@@ -672,6 +689,7 @@ en:
672
689
  width: "Width"
673
690
  without_tag: "Without tag"
674
691
  you_can_rename_this_tag: "You can rename this tag"
692
+ timezone: "Timezone"
675
693
  zoom_image: "Zoom this image"
676
694
  "Leave Alchemy": "Leave Alchemy"
677
695
  leave: "Leave"
@@ -712,7 +730,7 @@ en:
712
730
  text: "required"
713
731
  mark: "*"
714
732
  error_notification:
715
- default_message: "Please review the problems below:"
733
+ default_message: "Please review the problems below."
716
734
 
717
735
  # Alchemy date formats
718
736
  date:
@@ -724,14 +742,18 @@ en:
724
742
  time:
725
743
  formats:
726
744
  alchemy:
727
- default: "%Y-%m-%d %H:%M"
745
+ default: "%d-%m-%Y %I:%M%P"
728
746
  ingredient_date: "%Y-%m-%d"
729
- page_status: "%m.%d.%Y %H:%M"
730
- short_datetime: "%d %b %H:%M"
731
- time: "%H:%M"
747
+ page_status: "%d %b %Y, %I:%M%P"
748
+ short_datetime: "%d %b %I:%M%P"
749
+ time: "%I:%M%P"
750
+ element_date: "%d %b %Y, %I:%M%P"
732
751
 
733
752
  # Translations for error messages.
734
753
  errors:
754
+ attributes:
755
+ public_until:
756
+ must_be_after_public_on: "must be after visible from date"
735
757
  models:
736
758
  alchemy/element:
737
759
  attributes:
@@ -812,6 +834,12 @@ en:
812
834
  base:
813
835
  restrict_dependent_destroy:
814
836
  has_many: "There are still %{record} attached to this page. Please remove them first."
837
+ descendants:
838
+ still_attached_to_nodes: "The following descendant pages are still attached to menu nodes: %{page_names}. Please remove them first."
839
+ alchemy/element:
840
+ attributes:
841
+ page_version_id:
842
+ must_match_parent: "must be the same as the parent element's page version"
815
843
  models:
816
844
  gutentag/tag:
817
845
  one: Tag
@@ -867,6 +895,8 @@ en:
867
895
  name: "Name"
868
896
  public: "visible"
869
897
  tag_list: Tags
898
+ public_on: Visible from
899
+ public_until: Visible until
870
900
  alchemy/ingredient:
871
901
  dom_id: Anchor
872
902
  alchemy/ingredients/file:
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ module Admin
5
+ module PreviewTime
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ before_action :set_preview_time, if: :should_set_preview_time?
10
+ end
11
+
12
+ private
13
+
14
+ def set_preview_time
15
+ Current.preview_time = Time.zone.parse(params[:alchemy_preview_time])
16
+ end
17
+
18
+ def should_set_preview_time?
19
+ params[:alchemy_preview_time].present? && can?(:edit_content, Alchemy::Page)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -47,10 +47,13 @@ module Alchemy
47
47
  port: uri.port,
48
48
  path: page.url_path,
49
49
  userinfo: userinfo,
50
- query: {alchemy_preview_mode: true}.to_param
50
+ query: {
51
+ alchemy_preview_mode: true,
52
+ alchemy_preview_time: preview_time
53
+ }.compact.to_param
51
54
  ).to_s
52
55
  else
53
- routes.admin_page_path(page)
56
+ routes.admin_page_path(page, alchemy_preview_time: preview_time)
54
57
  end
55
58
  end
56
59
 
@@ -79,6 +82,14 @@ module Alchemy
79
82
  end
80
83
  end
81
84
 
85
+ # Returns the preview time as ISO 8601 string if explicitly set.
86
+ # We use Current.attributes instead of Current.preview_time because
87
+ # the getter falls back to Time.current, which would freeze a stale
88
+ # timestamp into the preview URL and hide elements created after page load.
89
+ def preview_time
90
+ Current.attributes[:preview_time]&.iso8601
91
+ end
92
+
82
93
  def userinfo
83
94
  auth = @preview_config.auth
84
95
  auth.username ? "#{auth["username"]}:#{auth["password"]}" : nil
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ module Admin
5
+ module Timezone
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ around_action :set_timezone, if: -> { can?(:edit_content, Alchemy::Page) }
10
+ end
11
+
12
+ private
13
+
14
+ # Sets the timezone for the current request.
15
+ #
16
+ # Uses the most preferred timezone or falls back to the server default.
17
+ #
18
+ # It respects the server's configured timezone from +config/application.rb+.
19
+ #
20
+ def set_timezone(&action)
21
+ timezone = if timezone_change_needed?
22
+ resolved_timezone || Time.zone.name
23
+ else
24
+ session[:alchemy_timezone]
25
+ end
26
+ session[:alchemy_timezone] = timezone
27
+ Time.use_zone(timezone, &action)
28
+ end
29
+
30
+ # Checks if we need to change the timezone or not.
31
+ def timezone_change_needed?
32
+ params[:admin_timezone].present? || session[:alchemy_timezone].blank?
33
+ end
34
+
35
+ # Returns the first valid timezone from the priority chain, or nil.
36
+ #
37
+ # The priority order is:
38
+ #
39
+ # * the passed parameter: +params[:admin_timezone]+
40
+ # * the user's timezone preference
41
+ #
42
+ def resolved_timezone
43
+ candidates = [params[:admin_timezone], timezone_from_user].compact
44
+ candidates.detect { |tz| ActiveSupport::TimeZone[tz].present? }
45
+ end
46
+
47
+ # Try to get the timezone from user settings.
48
+ def timezone_from_user
49
+ return if !current_alchemy_user
50
+ return if !current_alchemy_user.respond_to?(:timezone)
51
+
52
+ current_alchemy_user.timezone.presence
53
+ end
54
+ end
55
+ end
56
+ end
@@ -218,7 +218,7 @@ module Alchemy
218
218
 
219
219
  # The storage adapter for Pictures and Attachments
220
220
  #
221
- option :storage_adapter, :string, default: "dragonfly"
221
+ option :storage_adapter, :string, default: "active_storage"
222
222
 
223
223
  # Define page preview sources
224
224
  #
@@ -434,6 +434,18 @@ module Alchemy
434
434
  # The path to the page showing the user they're unauthorized
435
435
  option :unauthorized_path, :string, default: "/"
436
436
 
437
+ # === Edit User Path
438
+ #
439
+ # The path to the edit user form.
440
+ #
441
+ # == Example
442
+ #
443
+ # "/admin/users/:id/edit"
444
+ #
445
+ # NOTE: The :id placeholder will be replaced with the current_alchemy_user's id.
446
+ #
447
+ option :edit_user_path, :string
448
+
437
449
  # === CanCan abilities
438
450
  #
439
451
  # If your app or your engine has own CanCan abilities you must register them.
@@ -4,7 +4,7 @@ FactoryBot.define do
4
4
  factory :alchemy_element, class: "Alchemy::Element" do
5
5
  name { "article" }
6
6
  autogenerate_ingredients { false }
7
- association :page_version, factory: :alchemy_page_version
7
+ page_version { parent_element&.page_version || association(:alchemy_page_version) }
8
8
 
9
9
  trait :fixed do
10
10
  fixed { true }
@@ -21,7 +21,7 @@ FactoryBot.define do
21
21
  end
22
22
 
23
23
  trait :nested do
24
- parent_element { build(:alchemy_element, name: "slider", page_version: page_version) }
24
+ parent_element { association(:alchemy_element, name: "slider") }
25
25
  name { "slide" }
26
26
  end
27
27
 
@@ -1,7 +1,7 @@
1
1
  RSpec.shared_examples_for "a relatable resource" do |args|
2
2
  it { is_expected.to have_many(:related_ingredients) }
3
- it { is_expected.to have_many(:elements).through(:related_ingredients) }
4
- it { is_expected.to have_many(:pages).through(:elements) }
3
+ it { is_expected.to have_many(:related_elements).through(:related_ingredients) }
4
+ it { is_expected.to have_many(:related_pages).through(:related_elements) }
5
5
 
6
6
  describe ".deletable" do
7
7
  subject { described_class.deletable }
@@ -1,6 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  RSpec.shared_examples_for "being publishable" do |factory_name|
4
+ describe "validations" do
5
+ context "when public_until is older than public_on" do
6
+ let(:record) do
7
+ build(
8
+ factory_name,
9
+ public_on: Time.current,
10
+ public_until: Time.current - 1.day
11
+ )
12
+ end
13
+
14
+ it "is not valid" do
15
+ expect(record).not_to be_valid
16
+ expect(record.errors[:public_until]).to include(
17
+ I18n.t("errors.attributes.public_until.must_be_after_public_on")
18
+ )
19
+ end
20
+ end
21
+ end
22
+
4
23
  describe ".draft" do
5
24
  let!(:draft_versions) { create_list(factory_name, 2, public_on: nil) }
6
25
 
@@ -75,7 +94,7 @@ RSpec.shared_examples_for "being publishable" do |factory_name|
75
94
  context "and public_until is nil" do
76
95
  let(:public_until) { nil }
77
96
 
78
- it { expect(subject).to be_nil }
97
+ it { expect(subject).to be(false) }
79
98
  end
80
99
 
81
100
  context "and public_until is in the past" do
@@ -101,7 +120,7 @@ RSpec.shared_examples_for "being publishable" do |factory_name|
101
120
  context "and public_until is nil" do
102
121
  let(:public_until) { nil }
103
122
 
104
- it { expect(subject).to be_nil }
123
+ it { expect(subject).to be(false) }
105
124
  end
106
125
 
107
126
  context "and public_until is in the future" do
@@ -169,5 +188,28 @@ RSpec.shared_examples_for "being publishable" do |factory_name|
169
188
 
170
189
  it { is_expected.to be(false) }
171
190
  end
191
+
192
+ context "when Current.preview_time is set" do
193
+ let(:page_version) do
194
+ build(factory_name,
195
+ public_on: Time.zone.parse("2025-06-01 00:00:00"),
196
+ public_until: Time.zone.parse("2025-06-30 23:59:59"))
197
+ end
198
+
199
+ it "uses preview_time to determine visibility" do
200
+ Alchemy::Current.preview_time = Time.zone.parse("2025-06-15 12:00:00")
201
+ expect(page_version.public?).to be(true)
202
+ end
203
+
204
+ it "returns false when preview_time is outside the public range" do
205
+ Alchemy::Current.preview_time = Time.zone.parse("2025-07-15 12:00:00")
206
+ expect(page_version.public?).to be(false)
207
+ end
208
+
209
+ it "returns false when preview_time is before public_on" do
210
+ Alchemy::Current.preview_time = Time.zone.parse("2025-05-15 12:00:00")
211
+ expect(page_version.public?).to be(false)
212
+ end
213
+ end
172
214
  end
173
215
  end
@@ -32,7 +32,9 @@ module Alchemy
32
32
 
33
33
  def update_config
34
34
  desc "Copy configuration file."
35
- @default_config = Alchemy::Configurations::Main.new
35
+ @default_config = Alchemy::Configurations::Main.new(
36
+ storage_adapter: "dragonfly"
37
+ )
36
38
  template("templates/alchemy.rb.tt", "config/initializers/alchemy.rb")
37
39
  end
38
40
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Alchemy
4
- VERSION = "8.1.12"
4
+ VERSION = "8.2.0"
5
5
 
6
6
  def self.version
7
7
  VERSION
data/lib/alchemy_cms.rb CHANGED
@@ -23,6 +23,8 @@ require "view_component"
23
23
  # Require globally used Alchemy mixins
24
24
  require_relative "alchemy/ability_helper"
25
25
  require_relative "alchemy/admin/locale"
26
+ require_relative "alchemy/admin/timezone"
27
+ require_relative "alchemy/admin/preview_time"
26
28
  require_relative "alchemy/admin/preview_url"
27
29
  require_relative "alchemy/auth_accessors"
28
30
  require_relative "alchemy/cache_digests/template_tracker"
@@ -69,7 +69,8 @@ module Alchemy
69
69
  default_language: {
70
70
  name: @default_language[:name],
71
71
  code: @default_language[:code]
72
- }
72
+ },
73
+ storage_adapter: "active_storage"
73
74
  )
74
75
  template "#{__dir__}/templates/alchemy.rb.tt", app_config_path.join("initializers", "alchemy.rb")
75
76
  end