comfortable_media_surfer 3.1.7 → 3.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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/claude-code-review.yml +2 -0
  3. data/.github/workflows/claude.yml +99 -1
  4. data/.github/workflows/coveralls.yml +4 -3
  5. data/.github/workflows/rubyonrails.yml +34 -5
  6. data/CHANGELOG.md +12 -1
  7. data/CONTRIBUTING.md +5 -1
  8. data/Gemfile +6 -5
  9. data/README.md +4 -4
  10. data/Rakefile +201 -0
  11. data/app/assets/javascripts/comfy/admin/cms/application.js +3 -0
  12. data/app/assets/javascripts/comfy/admin/cms/base.js +1 -0
  13. data/app/assets/javascripts/comfy/admin/cms/file_link.js +34 -11
  14. data/app/assets/javascripts/comfy/admin/cms/file_upload.js +37 -28
  15. data/app/assets/javascripts/comfy/admin/cms/files_modal.js +76 -26
  16. data/app/controllers/comfy/admin/cms/categories_controller.rb +1 -1
  17. data/app/controllers/comfy/admin/cms/files_controller.rb +1 -1
  18. data/app/controllers/comfy/admin/cms/layouts_controller.rb +1 -1
  19. data/app/controllers/comfy/admin/cms/pages_controller.rb +33 -4
  20. data/app/controllers/comfy/admin/cms/revisions/base_controller.rb +2 -2
  21. data/app/controllers/comfy/admin/cms/revisions/page_controller.rb +5 -5
  22. data/app/controllers/comfy/admin/cms/revisions/translation_controller.rb +5 -5
  23. data/app/controllers/comfy/admin/cms/sites_controller.rb +3 -3
  24. data/app/controllers/comfy/admin/cms/snippets_controller.rb +1 -1
  25. data/app/controllers/comfy/admin/cms/translations_controller.rb +15 -3
  26. data/app/controllers/comfy/cms/base_controller.rb +16 -7
  27. data/app/controllers/comfy/cms/content_controller.rb +4 -4
  28. data/app/controllers/concerns/comfy/reorder_action.rb +3 -4
  29. data/app/models/comfy/cms/categorization.rb +9 -0
  30. data/app/models/comfy/cms/category.rb +1 -1
  31. data/app/models/comfy/cms/fragment.rb +7 -0
  32. data/app/models/comfy/cms/layout.rb +7 -2
  33. data/app/models/comfy/cms/page.rb +13 -4
  34. data/app/models/comfy/cms/site.rb +3 -3
  35. data/app/models/comfy/cms/translation.rb +5 -0
  36. data/app/models/concerns/comfy/cms/with_categories.rb +1 -1
  37. data/app/models/concerns/comfy/cms/with_fragments.rb +1 -1
  38. data/app/views/comfy/admin/cms/pages/edit.html.haml +5 -1
  39. data/config/database.yml +1 -1
  40. data/config/initializers/json.rb +2 -0
  41. data/config/locales/ar.yml +6 -0
  42. data/config/locales/ca.yml +6 -0
  43. data/config/locales/cs.yml +6 -0
  44. data/config/locales/da.yml +6 -0
  45. data/config/locales/de.yml +6 -0
  46. data/config/locales/en.yml +6 -0
  47. data/config/locales/es.yml +6 -0
  48. data/config/locales/fi.yml +6 -0
  49. data/config/locales/fr.yml +6 -0
  50. data/config/locales/gr.yml +6 -0
  51. data/config/locales/hr.yml +6 -0
  52. data/config/locales/it.yml +6 -0
  53. data/config/locales/ja.yml +6 -0
  54. data/config/locales/nb.yml +6 -0
  55. data/config/locales/nl.yml +6 -0
  56. data/config/locales/pl.yml +6 -0
  57. data/config/locales/pt-BR.yml +6 -0
  58. data/config/locales/ru.yml +6 -0
  59. data/config/locales/sk.yml +6 -0
  60. data/config/locales/sv.yml +6 -0
  61. data/config/locales/tr.yml +6 -0
  62. data/config/locales/uk.yml +6 -0
  63. data/config/locales/zh-CN.yml +6 -0
  64. data/config/locales/zh-TW.yml +6 -0
  65. data/config/storage.yml +1 -1
  66. data/gemfiles/7.2.gemfile +1 -0
  67. data/gemfiles/8.0.gemfile +1 -0
  68. data/gemfiles/8.1.gemfile +1 -0
  69. data/lib/comfortable_media_surfer/access_control/admin_authentication.rb +2 -2
  70. data/lib/comfortable_media_surfer/extensions/has_revisions.rb +2 -2
  71. data/lib/comfortable_media_surfer/routing.rb +2 -0
  72. data/lib/comfortable_media_surfer/seeds/page/importer.rb +40 -12
  73. data/lib/comfortable_media_surfer/seeds.rb +1 -3
  74. data/lib/comfortable_media_surfer/version.rb +1 -1
  75. metadata +2 -3
  76. data/gemfiles/7.1.gemfile +0 -35
@@ -41,7 +41,7 @@ import plupload from "plupload";
41
41
 
42
42
  destroy() {
43
43
  this.uploader.destroy();
44
- for (const cleanupFn of this.cleanupFns) {
44
+ for (const cleanupFn of this.cleanupFns.splice(0)) {
45
45
  cleanupFn();
46
46
  }
47
47
  }
@@ -75,9 +75,13 @@ import plupload from "plupload";
75
75
  this.addCleanup(() => {
76
76
  document.removeEventListener("dragenter", onDragEnter);
77
77
  });
78
+ const onDragEnd = () => {
79
+ this.ui.dropElement.classList.remove(DROP_TARGET_ACTIVE_CLASS);
80
+ };
78
81
  for (const eventName of ["drop", "dragleave"]) {
79
- this.ui.dropElement.addEventListener(eventName, () => {
80
- this.ui.dropElement.classList.remove(DROP_TARGET_ACTIVE_CLASS);
82
+ this.ui.dropElement.addEventListener(eventName, onDragEnd);
83
+ this.addCleanup(() => {
84
+ this.ui.dropElement.removeEventListener(eventName, onDragEnd);
81
85
  });
82
86
  }
83
87
  }
@@ -131,11 +135,9 @@ import plupload from "plupload";
131
135
  }
132
136
 
133
137
  addFile(file) {
134
- this.ui.list.insertAdjacentHTML(
135
- "afterbegin",
136
- FileUpload.buildListItemHTML(file)
137
- );
138
- this.fileListItem(file)
138
+ const listItem = FileUpload.buildListItem(file);
139
+ this.ui.list.prepend(listItem);
140
+ listItem
139
141
  .querySelector(".cms-uploader-file-delete")
140
142
  .addEventListener("click", (evt) => {
141
143
  evt.preventDefault();
@@ -158,7 +160,7 @@ import plupload from "plupload";
158
160
  case plupload.FAILED:
159
161
  progressBar.style.width = "100%";
160
162
  progressBar.classList.add("progress-bar-danger");
161
- progressBar.querySelector("span").innerHTML = file.error_message;
163
+ progressBar.querySelector("span").textContent = file.error_message;
162
164
  break;
163
165
  }
164
166
  }
@@ -167,19 +169,22 @@ import plupload from "plupload";
167
169
  return this.ui.container.querySelector(`#${id}`);
168
170
  }
169
171
 
170
- static buildListItemHTML({ id, name }) {
171
- return `<li id='${id}' class='row temp'>
172
- <div class='col-md-9 d-flex align-items-center'>
173
- <div class='progress'>
174
- <div class='progress-bar progress-bar-striped progress-bar-animated'>
175
- <span>${name}</span>
176
- </div>
172
+ static buildListItem({ id, name }) {
173
+ const listItem = document.createElement("li");
174
+ listItem.id = id;
175
+ listItem.className = "row temp";
176
+ listItem.innerHTML = `<div class='col-md-9 d-flex align-items-center'>
177
+ <div class='progress'>
178
+ <div class='progress-bar progress-bar-striped progress-bar-animated'>
179
+ <span></span>
177
180
  </div>
178
181
  </div>
179
- <div class='col-md-3 d-flex align-items-center justify-content-md-end'>
180
- <a class='btn btn-sm btn-danger float-right cms-uploader-file-delete' href='#'>Delete</a>
181
- </div>
182
- </li>`;
182
+ </div>
183
+ <div class='col-md-3 d-flex align-items-center justify-content-md-end'>
184
+ <a class='btn btn-sm btn-danger float-right cms-uploader-file-delete' href='#'>Delete</a>
185
+ </div>`;
186
+ listItem.querySelector("span").textContent = name;
187
+ return listItem;
183
188
  }
184
189
 
185
190
  static defaultUploaderSettings(id) {
@@ -194,12 +199,15 @@ import plupload from "plupload";
194
199
  }
195
200
  }
196
201
 
197
- const uploaders = [];
202
+ const uploaders = new Map();
198
203
  window.CMS.fileUpload = {
199
204
  init(root = document) {
200
- const el = root.querySelector("#cms-uploader");
201
- if (el === null) return;
202
- uploaders.push(
205
+ const el = root.matches?.("#cms-uploader")
206
+ ? root
207
+ : root.querySelector("#cms-uploader");
208
+ if (el === null || uploaders.has(el)) return;
209
+ uploaders.set(
210
+ el,
203
211
  new FileUpload(el, {
204
212
  url: el.dataset.cmsUploaderUrl,
205
213
  multipart_params: {
@@ -210,11 +218,12 @@ import plupload from "plupload";
210
218
  })
211
219
  );
212
220
  },
213
- dispose() {
214
- for (const uploader of uploader) {
215
- uploader.dispose();
221
+ dispose(root = null) {
222
+ for (const [el, uploader] of uploaders) {
223
+ if (root !== null && el !== root && !root.contains(el)) continue;
224
+ uploader.destroy();
225
+ uploaders.delete(el);
216
226
  }
217
- uploaders.length = 0;
218
227
  },
219
228
  };
220
229
  })();
@@ -1,47 +1,97 @@
1
- import Modal from "bootstrap/js/src/popover";
1
+ import Modal from "bootstrap/js/src/modal";
2
2
 
3
3
  // Site files modal.
4
4
  (() => {
5
5
  let modal = null;
6
+ let modalToggle = null;
7
+ let modalContainer = null;
8
+ let modalContent = null;
9
+ let onModalToggle = null;
10
+ let onModalContentDragStart = null;
11
+ let requestId = 0;
6
12
 
7
- const initModalContent = (modalContent) => {
13
+ const initModalContent = () => {
8
14
  window.CMS.fileUpload.init(modalContent);
9
15
  window.CMS.fileLinks(modalContent);
10
- modalContent.addEventListener("dragstart", (evt) => {
11
- if (
12
- evt.target.nodeType === Node.ELEMENT_NODE &&
13
- evt.target.matches(".cms-uploader-filelist .item-title a") &&
14
- modal != null
15
- ) {
16
- modal.hide();
17
- }
18
- });
16
+ };
17
+
18
+ const dispose = () => {
19
+ requestId += 1;
20
+ if (modalToggle !== null && onModalToggle !== null) {
21
+ modalToggle.removeEventListener("click", onModalToggle);
22
+ }
23
+ if (modalContent !== null && onModalContentDragStart !== null) {
24
+ modalContent.removeEventListener("dragstart", onModalContentDragStart);
25
+ window.CMS.fileUpload.dispose(modalContent);
26
+ window.CMS.fileLinks.dispose(modalContent);
27
+ }
28
+ if (modal !== null) {
29
+ const wasAnimated = modalContainer.classList.contains("fade");
30
+ modalContainer.classList.remove("fade");
31
+ modal.hide();
32
+ if (wasAnimated) modalContainer.classList.add("fade");
33
+ modal.dispose();
34
+ }
35
+ modal = null;
36
+ modalToggle = null;
37
+ modalContainer = null;
38
+ modalContent = null;
39
+ onModalToggle = null;
40
+ onModalContentDragStart = null;
19
41
  };
20
42
 
21
43
  window.CMS.files = {
22
44
  init() {
23
- const modalToggle = document.querySelector(".cms-files-open-modal");
24
- const modalContainer = document.querySelector(".cms-files-modal");
25
- if (modalToggle === null || modalContainer === null) return;
26
- const modalContent = modalContainer.querySelector(".modal-content");
27
- modalToggle.addEventListener("click", (evt) => {
45
+ const nextModalToggle = document.querySelector(".cms-files-open-modal");
46
+ const nextModalContainer = document.querySelector(".cms-files-modal");
47
+ if (nextModalToggle === null || nextModalContainer === null) return;
48
+ if (
49
+ modalToggle === nextModalToggle &&
50
+ modalContainer === nextModalContainer
51
+ ) {
52
+ return;
53
+ }
54
+
55
+ dispose();
56
+ modalToggle = nextModalToggle;
57
+ modalContainer = nextModalContainer;
58
+ modalContent = modalContainer.querySelector(".modal-content");
59
+ onModalContentDragStart = (evt) => {
60
+ if (
61
+ evt.target.nodeType === Node.ELEMENT_NODE &&
62
+ evt.target.matches(".cms-uploader-filelist .item-title a") &&
63
+ modal !== null
64
+ ) {
65
+ modal.hide();
66
+ }
67
+ };
68
+ modalContent.addEventListener("dragstart", onModalContentDragStart);
69
+ onModalToggle = (evt) => {
28
70
  evt.preventDefault();
29
- fetch(modalContainer.dataset.url, { credentials: "same-origin" })
71
+ const currentRequestId = ++requestId;
72
+ const currentModalContent = modalContent;
73
+ const url = new URL(modalContainer.dataset.url, document.location.href);
74
+ url.username = "";
75
+ url.password = "";
76
+ fetch(url, { credentials: "same-origin" })
30
77
  .then((resp) => resp.text())
31
78
  .then((html) => {
79
+ if (
80
+ currentRequestId !== requestId ||
81
+ currentModalContent !== modalContent
82
+ ) {
83
+ return;
84
+ }
85
+ window.CMS.fileUpload.dispose(modalContent);
86
+ window.CMS.fileLinks.dispose(modalContent);
32
87
  modalContent.innerHTML = `<div class="modal-body">${html}</div>`;
33
- initModalContent(modalContent);
88
+ initModalContent();
34
89
  });
35
90
  modal = modal || new Modal(modalContainer);
36
91
  modal.show();
37
- });
38
- },
39
- dispose() {
40
- if (modal !== null) {
41
- modal.hide();
42
- modal.dispose();
43
- modal = null;
44
- }
92
+ };
93
+ modalToggle.addEventListener("click", onModalToggle);
45
94
  },
95
+ dispose,
46
96
  };
47
97
  })();
@@ -33,6 +33,6 @@ protected
33
33
  end
34
34
 
35
35
  def category_params
36
- params.fetch(:category, {}).permit!
36
+ params.fetch(:category, {}).permit(:label, :categorized_type)
37
37
  end
38
38
  end
@@ -135,6 +135,6 @@ protected
135
135
  params[:file] = {}
136
136
  params[:file][:file] = file
137
137
  end
138
- params.fetch(:file, {}).permit!
138
+ params.fetch(:file, {}).permit(:label, :description, :file, category_ids: [])
139
139
  end
140
140
  end
@@ -64,6 +64,6 @@ protected
64
64
  end
65
65
 
66
66
  def layout_params
67
- params.fetch(:layout, {}).permit!
67
+ params.fetch(:layout, {}).permit(:label, :identifier, :parent_id, :app_layout, :content, :css, :js)
68
68
  end
69
69
  end
@@ -1,16 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Comfy::Admin::Cms::PagesController < Comfy::Admin::Cms::BaseController
4
+ FRAGMENT_PARAMS = [
5
+ :identifier, :tag, :content, :datetime, :boolean, :files,
6
+ { files: [], file_ids_destroy: [] }
7
+ ].freeze
8
+
4
9
  include ::Comfy::ReorderAction
5
10
 
6
11
  self.reorder_action_resource = ::Comfy::Cms::Page
7
12
 
8
13
  before_action :check_for_layouts, only: %i[new edit]
9
14
  before_action :build_page, only: %i[new create]
10
- before_action :load_page, only: %i[edit update destroy]
15
+ before_action :load_page, only: %i[edit update destroy publish_children unpublish_children]
11
16
 
12
17
  before_action :authorize
13
-
18
+ before_action :assign_page_params, only: :update
14
19
  before_action :preview_page, only: %i[create update]
15
20
 
16
21
  def index
@@ -89,8 +94,26 @@ class Comfy::Admin::Cms::PagesController < Comfy::Admin::Cms::BaseController
89
94
  render nothing: true
90
95
  end
91
96
 
97
+ def publish_children
98
+ update_children_publication(true, :children_published)
99
+ end
100
+
101
+ def unpublish_children
102
+ update_children_publication(false, :children_unpublished)
103
+ end
104
+
92
105
  protected
93
106
 
107
+ def update_children_publication(is_published, message)
108
+ descendant_ids = @page.descendants.map(&:id)
109
+ @site.pages
110
+ .where(id: descendant_ids)
111
+ .update_all(is_published: is_published, updated_at: Time.current)
112
+ default = is_published ? 'Child pages published' : 'Child pages unpublished'
113
+ flash[:success] = I18n.t("comfy.admin.cms.pages.#{message}", default: default)
114
+ redirect_to action: :edit, id: @page
115
+ end
116
+
94
117
  def index_for_redactor
95
118
  tree_walker = ->(page, list, offset) do
96
119
  return unless page.present?
@@ -134,12 +157,15 @@ protected
134
157
 
135
158
  def load_page
136
159
  @page = @site.pages.find(params[:id])
137
- @page.attributes = page_params
138
160
  rescue ActiveRecord::RecordNotFound
139
161
  flash[:danger] = I18n.t('comfy.admin.cms.pages.not_found')
140
162
  redirect_to action: :index
141
163
  end
142
164
 
165
+ def assign_page_params
166
+ @page.assign_attributes(page_params)
167
+ end
168
+
143
169
  def preview_page
144
170
  return unless params[:preview]
145
171
 
@@ -159,6 +185,9 @@ protected
159
185
  end
160
186
 
161
187
  def page_params
162
- params.fetch(:page, {}).permit!
188
+ params.fetch(:page, {}).permit(
189
+ :label, :slug, :parent_id, :layout_id, :target_page_id, :is_published,
190
+ category_ids: [], fragments_attributes: FRAGMENT_PARAMS
191
+ )
163
192
  end
164
193
  end
@@ -17,8 +17,8 @@ class Comfy::Admin::Cms::Revisions::BaseController < Comfy::Admin::Cms::BaseCont
17
17
  end
18
18
 
19
19
  def show
20
- @current_content = @record.revision_fields.each_with_object({}) { |f, c| c[f] = @record.send(f) }
21
- @versioned_content = @record.revision_fields.each_with_object({}) { |f, c| c[f] = @revision.data[f] }
20
+ @current_content = @record.revision_fields.to_h { |field| [field, @record.send(field)] }
21
+ @versioned_content = @record.revision_fields.to_h { |field| [field, @revision.data[field]] }
22
22
 
23
23
  render 'comfy/admin/cms/revisions/show'
24
24
  end
@@ -2,12 +2,12 @@
2
2
 
3
3
  class Comfy::Admin::Cms::Revisions::PageController < Comfy::Admin::Cms::Revisions::BaseController
4
4
  def show
5
- @current_content = @record.fragments.each_with_object({}) do |b, c|
6
- c[b.identifier] = b.content
5
+ @current_content = @record.fragments.to_h do |fragment|
6
+ [fragment.identifier, fragment.content]
7
7
  end
8
- @versioned_content = @record.fragments.each_with_object({}) do |b, c|
9
- d = @revision.data['fragments_attributes'].detect { |r| r[:identifier] == b.identifier }
10
- c[b.identifier] = d.try(:[], :content)
8
+ @versioned_content = @record.fragments.to_h do |fragment|
9
+ data = @revision.data['fragments_attributes'].detect { |item| item[:identifier] == fragment.identifier }
10
+ [fragment.identifier, data.try(:[], :content)]
11
11
  end
12
12
 
13
13
  render 'comfy/admin/cms/revisions/show'
@@ -2,12 +2,12 @@
2
2
 
3
3
  class Comfy::Admin::Cms::Revisions::TranslationController < Comfy::Admin::Cms::Revisions::BaseController
4
4
  def show
5
- @current_content = @record.fragments.each_with_object({}) do |b, c|
6
- c[b.identifier] = b.content
5
+ @current_content = @record.fragments.to_h do |fragment|
6
+ [fragment.identifier, fragment.content]
7
7
  end
8
- @versioned_content = @record.fragments.each_with_object({}) do |b, c|
9
- d = @revision.data['fragments_attributes'].detect { |r| r[:identifier] == b.identifier }
10
- c[b.identifier] = d.try(:[], :content)
8
+ @versioned_content = @record.fragments.to_h do |fragment|
9
+ data = @revision.data['fragments_attributes'].detect { |item| item[:identifier] == fragment.identifier }
10
+ [fragment.identifier, data.try(:[], :content)]
11
11
  end
12
12
 
13
13
  render 'comfy/admin/cms/revisions/show'
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Comfy::Admin::Cms::SitesController < Comfy::Admin::Cms::BaseController
4
- skip_before_action :load_admin_site,
5
- :load_seeds
4
+ skip_before_action :load_admin_site,
5
+ :load_seeds
6
6
 
7
7
  before_action :build_site, only: %i[new create]
8
8
  before_action :load_site, only: %i[edit update destroy]
@@ -63,6 +63,6 @@ protected
63
63
  end
64
64
 
65
65
  def site_params
66
- params.fetch(:site, {}).permit!
66
+ params.fetch(:site, {}).permit(:label, :identifier, :hostname, :path, :locale)
67
67
  end
68
68
  end
@@ -61,6 +61,6 @@ protected
61
61
  end
62
62
 
63
63
  def snippet_params
64
- params.fetch(:snippet, {}).permit!
64
+ params.fetch(:snippet, {}).permit(:label, :identifier, :markdown, :content, category_ids: [])
65
65
  end
66
66
  end
@@ -1,12 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Comfy::Admin::Cms::TranslationsController < Comfy::Admin::Cms::BaseController
4
+ FRAGMENT_PARAMS = [
5
+ :identifier, :tag, :content, :datetime, :boolean, :files,
6
+ { files: [], file_ids_destroy: [] }
7
+ ].freeze
8
+
4
9
  helper_method :translation_select_options
5
10
 
6
11
  before_action :load_page
7
12
  before_action :build_translation, only: %i[new create]
8
13
  before_action :load_translation, only: %i[edit update destroy]
9
14
  before_action :authorize
15
+ before_action :assign_translation_params, only: :update
10
16
  before_action :preview_translation, only: %i[create update]
11
17
 
12
18
  def new
@@ -27,7 +33,7 @@ class Comfy::Admin::Cms::TranslationsController < Comfy::Admin::Cms::BaseControl
27
33
  end
28
34
 
29
35
  def update
30
- @translation.update!(translation_params)
36
+ @translation.save!
31
37
  flash[:success] = I18n.t('comfy.admin.cms.translations.updated')
32
38
  redirect_to action: :edit, id: @translation
33
39
  rescue ActiveRecord::RecordInvalid
@@ -74,14 +80,20 @@ private
74
80
 
75
81
  def load_translation
76
82
  @translation = @page.translations.find(params[:id])
77
- @translation.attributes = translation_params
78
83
  rescue ActiveRecord::RecordNotFound
79
84
  flash[:danger] = I18n.t('comfy.admin.cms.translations.not_found')
80
85
  redirect_to edit_comfy_admin_cms_site_page_path(@site, @page)
81
86
  end
82
87
 
88
+ def assign_translation_params
89
+ @translation.assign_attributes(translation_params)
90
+ end
91
+
83
92
  def translation_params
84
- params.fetch(:translation, {}).permit!
93
+ params.fetch(:translation, {}).permit(
94
+ :locale, :label, :layout_id, :is_published,
95
+ fragments_attributes: FRAGMENT_PARAMS
96
+ )
85
97
  end
86
98
 
87
99
  def preview_translation
@@ -8,19 +8,28 @@ class Comfy::Cms::BaseController < ComfortableMediaSurfer.config.public_base_con
8
8
  protected
9
9
 
10
10
  def load_cms_site
11
- @cms_site ||=
12
- if params[:site_id]
13
- ::Comfy::Cms::Site.find_by_id(params[:site_id])
14
- else
15
- ::Comfy::Cms::Site.find_site(request.host_with_port.downcase, request.fullpath)
16
- end
11
+ route_site_id = request.path_parameters[:site_id]
12
+ @cms_site ||= if route_site_id
13
+ find_cms_site_for_asset(route_site_id)
14
+ else
15
+ ::Comfy::Cms::Site.find_site(request.host_with_port.downcase, request.fullpath)
16
+ end
17
17
 
18
18
  raise ActionController::RoutingError, 'Site Not Found' unless @cms_site
19
19
 
20
- return unless @cms_site.path.present? && !params[:site_id]
20
+ return unless @cms_site.path.present? && !route_site_id
21
21
  raise ActionController::RoutingError, 'Site Not Found' unless params[:cms_path]&.match(%r{\A#{@cms_site.path}})
22
22
 
23
23
  params[:cms_path].gsub!(%r{\A#{@cms_site.path}}, '')
24
24
  params[:cms_path]&.gsub!(%r{\A/}, '')
25
25
  end
26
+
27
+ def find_cms_site_for_asset(site_id)
28
+ site = ::Comfy::Cms::Site.find_by_id(site_id)
29
+ return site if site && ::Comfy::Cms::Site.one?
30
+ return unless site
31
+
32
+ request_host = ::Comfy::Cms::Site.real_host_from_aliases(request.host_with_port.downcase)
33
+ site if site.hostname.downcase == request_host
34
+ end
26
35
  end
@@ -34,10 +34,10 @@ class Comfy::Cms::ContentController < Comfy::Cms::BaseController
34
34
  protected
35
35
 
36
36
  def render_page(status = :ok)
37
- render inline: @cms_page.content_cache,
38
- layout: app_layout,
39
- status: status,
40
- content_type: mime_type
37
+ render inline: @cms_page.content_cache,
38
+ layout: app_layout,
39
+ status: status,
40
+ content_type: mime_type
41
41
  end
42
42
 
43
43
  # it's possible to control mimetype of a page by creating a `mime_type` field
@@ -9,12 +9,11 @@ module Comfy::ReorderAction
9
9
 
10
10
  def reorder
11
11
  resource_class = self.class.reorder_action_resource
12
+ site_resources = resource_class.where(site_id: @site.id)
12
13
  (params.permit(order: [])[:order] || []).each_with_index do |id, index|
13
- resource_class.where(id: id).update_all(position: index)
14
- end
15
- if resource_class == ::Comfy::Cms::Page
16
- Comfy::Cms::Page.all.each(&:save!)
14
+ site_resources.where(id: id).update_all(position: index)
17
15
  end
16
+ @site.pages.each(&:save!) if resource_class == ::Comfy::Cms::Page
18
17
  head :ok
19
18
  end
20
19
  end
@@ -11,4 +11,13 @@ class Comfy::Cms::Categorization < ActiveRecord::Base
11
11
  # -- Validations -------------------------------------------------------------
12
12
  validates :category_id,
13
13
  uniqueness: { scope: %i[categorized_type categorized_id] }
14
+ validate :category_belongs_to_site
15
+
16
+ private
17
+
18
+ def category_belongs_to_site
19
+ return unless category && categorized.respond_to?(:site_id)
20
+
21
+ errors.add(:category_id, :invalid) if category.site_id != categorized.site_id
22
+ end
14
23
  end
@@ -13,7 +13,7 @@ class Comfy::Cms::Category < ActiveRecord::Base
13
13
  presence: true,
14
14
  uniqueness: { scope: %i[categorized_type site_id] }
15
15
  validates :categorized_type,
16
- presence: true
16
+ presence: true
17
17
 
18
18
  # -- Scopes ---------------------------------------------------------------
19
19
  scope :of_type, ->(type) {
@@ -20,6 +20,7 @@ class Comfy::Cms::Fragment < ActiveRecord::Base
20
20
  validates :identifier,
21
21
  presence: true,
22
22
  uniqueness: { scope: :record }
23
+ validate :record_is_immutable, on: :update
23
24
 
24
25
  # -- Instance Methods --------------------------------------------------------
25
26
 
@@ -38,6 +39,12 @@ class Comfy::Cms::Fragment < ActiveRecord::Base
38
39
 
39
40
  protected
40
41
 
42
+ def record_is_immutable
43
+ return unless will_save_change_to_record_id? || will_save_change_to_record_type?
44
+
45
+ errors.add(:record, :invalid)
46
+ end
47
+
41
48
  def remove_attachments
42
49
  return unless @file_ids_destroy.present?
43
50
 
@@ -20,13 +20,14 @@ class Comfy::Cms::Layout < ActiveRecord::Base
20
20
 
21
21
  # -- Validations -------------------------------------------------------------
22
22
  validates :site_id,
23
- presence: true
23
+ presence: true
24
24
  validates :label,
25
- presence: true
25
+ presence: true
26
26
  validates :identifier,
27
27
  presence: true,
28
28
  uniqueness: { scope: :site_id },
29
29
  format: { with: %r{\A\w[a-z0-9_-]*\z}i }
30
+ validate :parent_belongs_to_site
30
31
 
31
32
  # -- Class Methods -----------------------------------------------------------
32
33
  # Tree-like structure for layouts
@@ -101,6 +102,10 @@ class Comfy::Cms::Layout < ActiveRecord::Base
101
102
 
102
103
  protected
103
104
 
105
+ def parent_belongs_to_site
106
+ errors.add(:parent_id, :invalid) if site && parent && site_id != parent.site_id
107
+ end
108
+
104
109
  def assign_label
105
110
  self.label = label.blank? ? identifier.try(:titleize) : label
106
111
  end
@@ -40,6 +40,7 @@ class Comfy::Cms::Page < ActiveRecord::Base
40
40
  }
41
41
  validate :validate_target_page
42
42
  validate :validate_format_of_unescaped_slug
43
+ validate :associations_belong_to_site
43
44
 
44
45
  # -- Scopes ------------------------------------------------------------------
45
46
  scope :published, -> { where(is_published: true) }
@@ -106,6 +107,14 @@ class Comfy::Cms::Page < ActiveRecord::Base
106
107
 
107
108
  protected
108
109
 
110
+ def associations_belong_to_site
111
+ return unless site
112
+
113
+ errors.add(:parent_id, :invalid) if parent && site_id != parent.site_id
114
+ errors.add(:layout_id, :invalid) if layout && site_id != layout.site_id
115
+ errors.add(:target_page_id, :invalid) if target_page && site_id != target_page.site_id
116
+ end
117
+
109
118
  def assigns_label
110
119
  self.label = label.blank? ? slug.try(:titleize) : label
111
120
  end
@@ -118,18 +127,18 @@ protected
118
127
 
119
128
  def assign_full_path
120
129
  self.full_path =
121
- if self.parent
122
- [CGI.escape(self.parent.full_path).gsub('%2F', '/'), slug].join('/').squeeze('/')
130
+ if parent
131
+ [CGI.escape(parent.full_path).gsub('%2F', '/'), slug].join('/').squeeze('/')
123
132
  else
124
133
  '/'
125
134
  end
126
135
  end
127
136
 
128
137
  def assign_position
129
- return unless self.parent
138
+ return unless parent
130
139
  return if position.to_i.positive?
131
140
 
132
- max = self.parent.children.maximum(:position)
141
+ max = parent.children.maximum(:position)
133
142
  self.position = max ? max + 1 : 0
134
143
  end
135
144