From b9e1a5cbb5d3e80c98f8d3eec2c689f9d505de69 Mon Sep 17 00:00:00 2001 From: ricardoo27 <1345603+ricardoo27@users.noreply.github.com> Date: Tue, 26 Nov 2024 16:31:09 +0100 Subject: [PATCH 1/7] Added concern Clonable --- app/cells/folio/console/index/actions_cell.rb | 5 + .../folio/console/pages_controller.rb | 9 ++ app/models/concerns/folio/console/clonable.rb | 152 ++++++++++++++++++ app/models/folio/page.rb | 5 + app/views/folio/console/pages/index.slim | 2 +- config/locales/activerecord.cs.yml | 3 + config/locales/activerecord.en.yml | 3 + config/locales/console.cs.yml | 1 + config/locales/console.en.yml | 1 + config/locales/cs.yml | 12 ++ config/locales/en.yml | 12 ++ config/routes.rb | 1 + 12 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 app/models/concerns/folio/console/clonable.rb diff --git a/app/cells/folio/console/index/actions_cell.rb b/app/cells/folio/console/index/actions_cell.rb index c5633568b..a223c3f6b 100644 --- a/app/cells/folio/console/index/actions_cell.rb +++ b/app/cells/folio/console/index/actions_cell.rb @@ -35,6 +35,11 @@ def default_actions icon: :edit_box, url: -> (record) { through_aware_console_url_for(record, action: :edit, safe: true) }, }, + clone: { + name: :clone, + icon: :square_outline, + url: -> (record) { through_aware_console_url_for(record, action: :clone, safe: true) }, + }, show: { name: :show, icon: :eye, diff --git a/app/controllers/folio/console/pages_controller.rb b/app/controllers/folio/console/pages_controller.rb index d5c434b15..584986799 100644 --- a/app/controllers/folio/console/pages_controller.rb +++ b/app/controllers/folio/console/pages_controller.rb @@ -15,6 +15,15 @@ def index end end + def clone + page = Folio::Page.find(params[:id]) + @page = page.create_clone + @page.title = t('.cloned_title', + original_title: page.title, + date: Date.today.strftime('%d. %m. %Y')) + render :new + end + private def index_filters { diff --git a/app/models/concerns/folio/console/clonable.rb b/app/models/concerns/folio/console/clonable.rb new file mode 100644 index 000000000..efec5a183 --- /dev/null +++ b/app/models/concerns/folio/console/clonable.rb @@ -0,0 +1,152 @@ +# frozen_string_literal: true + +module Folio::Console::Clonable + extend ActiveSupport::Concern + + DEFAULT_RESET_ATTRIBUTES = [:published_at, :published] + + included do + class_attribute :reference_associations, :duplicated_associations, :reset_attributes, + default: [], instance_writer: false + + self.reset_attributes = DEFAULT_RESET_ATTRIBUTES + end + + class_methods do + def references_original(*associations) + validate_associations!(associations) + self.reference_associations = associations + end + + def duplicates_with_relations(*associations) + validate_associations!(associations) + self.duplicated_associations = associations + end + + def reset_attributes_on_clone(*attributes) + validate_attributes!(attributes) + self.reset_attributes = DEFAULT_RESET_ATTRIBUTES + attributes + end + + private + def validate_associations!(associations) + associations.each do |assoc| + unless reflect_on_association(assoc) + raise ArgumentError, I18n.t("activerecord.errors.clonable.association_not_found", + association: assoc, + model: self.name) + end + end + end + + def validate_attributes!(attributes) + attributes.each do |attr| + unless column_names.include?(attr.to_s) + raise ArgumentError, I18n.t("activerecord.errors.clonable.attribute_not_found", + attribute: attr, + model: self.name) + end + end + end + end + + def create_clone + log("CLONING", :info) + log(I18n.t("cloning.start", model: self.class.name, id: id)) + + clone = deep_dup + log(I18n.t("cloning.deep_dup_finished")) + + copy_references(clone) + log(I18n.t("cloning.references_copied", references: self.class.reference_associations)) + + duplicate_nested_records(clone) + log(I18n.t("cloning.associations_duplicated", associations: self.class.duplicated_associations)) + + reset_clone_attributes(clone) + log(I18n.t("cloning.finished")) + clone +rescue => e + log(I18n.t("cloning.error", message: e.message), :error) + log(e.backtrace.first(5).join("\n"), :error) + raise + end + + private + def reset_clone_attributes(clone) + self.class.reset_attributes.each do |attr| + clone[attr] = nil if clone.has_attribute?(attr) + end + end + + def copy_references(cloned) + return unless self.class.reference_associations.present? + + self.class.reference_associations.each do |assoc| + cloned.public_send("#{assoc}=", public_send(assoc)) + end + end + + def duplicate_nested_records(cloned) + return unless self.class.duplicated_associations.present? + + self.class.duplicated_associations.each do |assoc| + cloned.public_send("#{assoc}=", clone_associated_records(assoc)) + end + end + + def clone_associated_records(association) + originals = public_send(association) + originals = [originals] unless originals.is_a?(ActiveRecord::Relation) + + clones = originals.map do |orig| + clone = orig.deep_dup + + if orig.class.reflect_on_all_associations.present? + orig.class.reflect_on_all_associations.each do |association| + clone_association(orig, clone, association) + end + end + + clone + end + + originals.is_a?(ActiveRecord::Relation) ? clones : clones.first + end + + def clone_association(original, clone, association) + if association.macro == :has_many + clone_has_many_association(original, clone, association) + else + clone_single_association(original, clone, association) + end + end + + def clone_has_many_association(original, clone, association) + return if [:files, :placements].include?(association.name) + associated_records = original.public_send(association.name) + associated_records = associated_records.map { |r| r.deep_dup } unless self.class.reference_associations.include?(association.name) + clone.association(association.name).build(associated_records.map(&:attributes)) + end + + def clone_single_association(original, clone, association) + associated_record = original.public_send(association.name) + return unless associated_record + + associated_record_dup = self.class.reference_associations.include?(association.name) ? + associated_record : + associated_record.deep_dup + + clone.public_send("#{association.name}=", associated_record_dup) + end + + def log(message, level = :info) + if Rails.env.development? && Rails.logger + Rails.logger.tagged("CLONING") do + Rails.logger.public_send(level, message) + end + else + puts "[CLONING] #{message}" + end + end +end diff --git a/app/models/folio/page.rb b/app/models/folio/page.rb index 634e05d2a..e823fce88 100644 --- a/app/models/folio/page.rb +++ b/app/models/folio/page.rb @@ -30,6 +30,8 @@ class Folio::Page < Folio::ApplicationRecord include Folio::Taggable include Folio::Transportable::Model include PgSearch::Model + include Folio::Console::Clonable + if Rails.application.config.folio_pages_audited include Folio::Audited @@ -64,6 +66,9 @@ class Folio::Page < Folio::ApplicationRecord presence: true end + references_original :cover + duplicates_with_relations :atoms + # Scopes scope :ordered, -> { order(position: :asc, created_at: :asc) } scope :featured, -> { where(featured: true) } diff --git a/app/views/folio/console/pages/index.slim b/app/views/folio/console/pages/index.slim index 8b573ae5a..914e18456 100644 --- a/app/views/folio/console/pages/index.slim +++ b/app/views/folio/console/pages/index.slim @@ -26,7 +26,7 @@ Folio::Publishable::PREVIEW_PARAM_NAME => record.preview_token) end - actions({ preview: }, :edit, :destroy) + actions({ preview: }, :edit, :destroy, :clone) elsif record.is_a?(Folio::Page) && record.class.try(:public_rails_path) actions({ preview: controller.main_app.send(record.class.public_rails_path, locale: locale) }, :edit, diff --git a/config/locales/activerecord.cs.yml b/config/locales/activerecord.cs.yml index f435a91e5..34fcab423 100644 --- a/config/locales/activerecord.cs.yml +++ b/config/locales/activerecord.cs.yml @@ -183,6 +183,9 @@ cs: attributes: roles: not_available_for_site: "Role %{roles} nejsou všechny dostupné pro web '%{site}'." + clonable: + association_not_found: "Asociace '%{association}' neexistuje pro %{model}" + attribute_not_found: "Atribut '%{attribute}' neexistuje pro %{model}" models: folio/atom: few: Kapitoly diff --git a/config/locales/activerecord.en.yml b/config/locales/activerecord.en.yml index e64bcf63d..fc7452dde 100644 --- a/config/locales/activerecord.en.yml +++ b/config/locales/activerecord.en.yml @@ -156,6 +156,9 @@ en: attributes: roles: not_available_for_site: "Roles %{roles} are not completelly available for site '%{site}'." + clonable: + association_not_found: "Association '%{association}' does not exist for %{model}" + attribute_not_found: "Attribute '%{attribute}' does not exist for %{model}" models: folio/atom: diff --git a/config/locales/console.cs.yml b/config/locales/console.cs.yml index 7b8cf5914..4865007b3 100644 --- a/config/locales/console.cs.yml +++ b/config/locales/console.cs.yml @@ -9,6 +9,7 @@ cs: cancel: Zrušit confirm: Potvrdit continue: Pokračovat + clone: Duplikovat close: Zavřít destroy: Smazat discard: Archivovat diff --git a/config/locales/console.en.yml b/config/locales/console.en.yml index 694b9a6b1..c231c9856 100644 --- a/config/locales/console.en.yml +++ b/config/locales/console.en.yml @@ -9,6 +9,7 @@ en: cancel: Cancel confirm: Confirm continue: Continue + clone: Clone close: Close destroy: Destroy discard: Archive diff --git a/config/locales/cs.yml b/config/locales/cs.yml index 529444630..4f7fc1836 100644 --- a/config/locales/cs.yml +++ b/config/locales/cs.yml @@ -54,6 +54,10 @@ cs: accept: Ano cancel: Ne confirmation: Určitě? + console: + pages: + clone: + cloned_title: "%{original_title} - duplikát %{date}" head: description: '' @@ -69,3 +73,11 @@ cs: time: formats: folio_short: "%d. %m. %Y, %H:%M" + + cloning: + start: "Začátek klonování %{model} #%{id}" + deep_dup_finished: "Deep dup dokončen" + references_copied: "Reference zkopirovány: %{references}" + associations_duplicated: "Asociace duplikovány: %{associations}" + finished: "Klonování dokončeno" + error: "Chyba při klonování: %{message}" diff --git a/config/locales/en.yml b/config/locales/en.yml index 992ad590a..ec5bcdbcd 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -52,6 +52,10 @@ en: content: This image contains sensitive content. To see it, verify your age. Are you over 18? accept: Yes cancel: No + console: + pages: + clone: + cloned_title: "%{original_title} - copy %{date}" head: description: '' @@ -68,3 +72,11 @@ en: time: formats: folio_short: "%d. %m. %Y, %H:%M" + + cloning: + start: "Started cloning %{model} #%{id}" + deep_dup_finished: "Deep dup finished" + references_copied: "References copied: %{references}" + associations_duplicated: "Associations duplicated: %{associations}" + finished: "Cloning finished" + error: "Cloning error: %{message}" diff --git a/config/routes.rb b/config/routes.rb index 6f1cfbedb..58096de8a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -54,6 +54,7 @@ get :revision, path: "revision/:version" post :restore, path: "restore/:version" end + get :clone end end From 4f1b7d2229e5f7ad300f6ee61c472aabef934216 Mon Sep 17 00:00:00 2001 From: ricardoo27 <1345603+ricardoo27@users.noreply.github.com> Date: Tue, 26 Nov 2024 18:23:24 +0100 Subject: [PATCH 2/7] Added test for clonable concern --- config/locales/cs.yml | 1 - .../folio/console/pages_controller_test.rb | 6 +++ test/models/concerns/folio/clonable_test.rb | 50 +++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 test/models/concerns/folio/clonable_test.rb diff --git a/config/locales/cs.yml b/config/locales/cs.yml index 4f7fc1836..5036b87e4 100644 --- a/config/locales/cs.yml +++ b/config/locales/cs.yml @@ -55,7 +55,6 @@ cs: cancel: Ne confirmation: Určitě? console: - pages: clone: cloned_title: "%{original_title} - duplikát %{date}" diff --git a/test/controllers/folio/console/pages_controller_test.rb b/test/controllers/folio/console/pages_controller_test.rb index 8f1415f02..77a76bf37 100644 --- a/test/controllers/folio/console/pages_controller_test.rb +++ b/test/controllers/folio/console/pages_controller_test.rb @@ -49,4 +49,10 @@ class Folio::Console::PagesControllerTest < Folio::Console::BaseControllerTest assert_redirected_to url_for([:edit, :console, page]) end + + test "clone" do + page = create(:folio_page) + get url_for([:clone, :console, page]) + assert_response :success + end end diff --git a/test/models/concerns/folio/clonable_test.rb b/test/models/concerns/folio/clonable_test.rb new file mode 100644 index 000000000..a08d888b1 --- /dev/null +++ b/test/models/concerns/folio/clonable_test.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require "test_helper" + +class Folio::ClonableTest < ActiveSupport::TestCase + test "create clone of page" do + page = create(:folio_page) + + create_atom(Dummy::Atom::Contents::Text, + placement: page, + content: "Původní text") + + image = create(:folio_file_image) + + create_atom(Dummy::Atom::Cards::Image, + placement: page, + title: "Původní titulek", + description: "Původní popis", + url: "https://example.com", + cover: image) + page.cover = image + + original_attributes = page.attributes + clone = page.create_clone + + clone.title = "clone" + assert clone.valid? + + assert_not_equal page.atoms, clone.atoms + assert_equal page.cover, clone.cover + assert_not_equal page.cover_placement, clone.cover_placement + + clone.atoms.first.update!(content: "Změněný text") + clone.atoms.last.update!(title: "Změněný titulek", description: "Změněný popis", url: "https://example2.com") + + clone.update!( + title: "Nový titulek", + perex: "Nový perex", + published_at: Time.current, + published: true, + ) + + page.reload + assert_equal original_attributes, page.attributes + assert_equal "Původní text", page.atoms.first.content + assert_not_equal page.atoms.first.content, clone.atoms.first.content + assert_equal image, page.atoms.second.cover + assert_equal image, clone.atoms.second.cover + end +end From a3d4c11a0a06757924ad88f552ef839a0482384a Mon Sep 17 00:00:00 2001 From: ricardoo27 <1345603+ricardoo27@users.noreply.github.com> Date: Wed, 27 Nov 2024 09:38:31 +0100 Subject: [PATCH 3/7] Deverted default actions --- app/views/folio/console/pages/index.slim | 2 +- .../app/views/folio/console/pages/index.slim | 38 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 test/dummy/app/views/folio/console/pages/index.slim diff --git a/app/views/folio/console/pages/index.slim b/app/views/folio/console/pages/index.slim index 914e18456..8b573ae5a 100644 --- a/app/views/folio/console/pages/index.slim +++ b/app/views/folio/console/pages/index.slim @@ -26,7 +26,7 @@ Folio::Publishable::PREVIEW_PARAM_NAME => record.preview_token) end - actions({ preview: }, :edit, :destroy, :clone) + actions({ preview: }, :edit, :destroy) elsif record.is_a?(Folio::Page) && record.class.try(:public_rails_path) actions({ preview: controller.main_app.send(record.class.public_rails_path, locale: locale) }, :edit, diff --git a/test/dummy/app/views/folio/console/pages/index.slim b/test/dummy/app/views/folio/console/pages/index.slim new file mode 100644 index 000000000..914e18456 --- /dev/null +++ b/test/dummy/app/views/folio/console/pages/index.slim @@ -0,0 +1,38 @@ += index_header + += catalogue(@catalogue_model || @pages, @catalogue_options || {}) + ruby: + edit_link :title + + type + + locale_flag if Rails.application.config.folio_pages_locales + + published_toggle + + date(:published_at) + + position_controls if model && model[:ancestry] + + locale = (Folio::Current.site.locales.size > 1 || Rails.application.config.folio_console_add_locale_to_preview_links) ? I18n.locale : nil + + if record.is_a?(Folio::Page) && record.class.try(:public?) + preview = if record.published? && !model[:ancestry] + controller.main_app.page_path(record.to_preview_param, + locale: locale) + else + controller.main_app.page_path(record.to_preview_param, + locale: locale, + Folio::Publishable::PREVIEW_PARAM_NAME => record.preview_token) + end + + actions({ preview: }, :edit, :destroy, :clone) + elsif record.is_a?(Folio::Page) && record.class.try(:public_rails_path) + actions({ preview: controller.main_app.send(record.class.public_rails_path, locale: locale) }, + :edit, + :destroy) + else + actions(:edit, :destroy) + end + + transportable_dropdown From dcc77c27c16038383c03d471e185538d92131e72 Mon Sep 17 00:00:00 2001 From: ricardoo27 <1345603+ricardoo27@users.noreply.github.com> Date: Wed, 27 Nov 2024 09:55:22 +0100 Subject: [PATCH 4/7] Changed icon for clone. --- app/cells/folio/console/index/actions_cell.rb | 2 +- app/controllers/folio/console/pages_controller.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/cells/folio/console/index/actions_cell.rb b/app/cells/folio/console/index/actions_cell.rb index a223c3f6b..b450481b3 100644 --- a/app/cells/folio/console/index/actions_cell.rb +++ b/app/cells/folio/console/index/actions_cell.rb @@ -37,7 +37,7 @@ def default_actions }, clone: { name: :clone, - icon: :square_outline, + icon: :plus_circle_multiple_outline, url: -> (record) { through_aware_console_url_for(record, action: :clone, safe: true) }, }, show: { diff --git a/app/controllers/folio/console/pages_controller.rb b/app/controllers/folio/console/pages_controller.rb index 584986799..e49b85029 100644 --- a/app/controllers/folio/console/pages_controller.rb +++ b/app/controllers/folio/console/pages_controller.rb @@ -18,9 +18,9 @@ def index def clone page = Folio::Page.find(params[:id]) @page = page.create_clone - @page.title = t('.cloned_title', + @page.title = t("folio.console.clone.cloned_title", original_title: page.title, - date: Date.today.strftime('%d. %m. %Y')) + date: Date.today.strftime("%d. %m. %Y")) render :new end From 6189a82348eb23be9f71e7fcb4ebdf0f75f8fd7c Mon Sep 17 00:00:00 2001 From: ricardoo27 <1345603+ricardoo27@users.noreply.github.com> Date: Mon, 9 Dec 2024 12:17:50 +0100 Subject: [PATCH 5/7] Refactored concern Clonable. --- app/cells/folio/console/index/actions_cell.rb | 5 +- .../concerns/folio/console/default_actions.rb | 8 ++ .../folio/console/pages_controller.rb | 9 -- app/lib/folio/clonable/cloner.rb | 94 +++++++++++++ app/models/concerns/folio/console/clonable.rb | 131 +++--------------- app/models/folio/ability.rb | 1 + app/models/folio/page.rb | 9 +- app/views/folio/console/pages/index.slim | 2 +- config/routes.rb | 2 +- lib/folio/engine.rb | 1 + .../folio/console/pages_controller_test.rb | 2 +- .../app/views/folio/console/pages/index.slim | 38 ----- test/dummy/config/application.rb | 1 + test/models/concerns/folio/clonable_test.rb | 2 +- 14 files changed, 136 insertions(+), 169 deletions(-) create mode 100644 app/lib/folio/clonable/cloner.rb delete mode 100644 test/dummy/app/views/folio/console/pages/index.slim diff --git a/app/cells/folio/console/index/actions_cell.rb b/app/cells/folio/console/index/actions_cell.rb index b450481b3..9df239d96 100644 --- a/app/cells/folio/console/index/actions_cell.rb +++ b/app/cells/folio/console/index/actions_cell.rb @@ -35,10 +35,10 @@ def default_actions icon: :edit_box, url: -> (record) { through_aware_console_url_for(record, action: :edit, safe: true) }, }, - clone: { + duplicate: { name: :clone, icon: :plus_circle_multiple_outline, - url: -> (record) { through_aware_console_url_for(record, action: :clone, safe: true) }, + url: -> (record) { through_aware_console_url_for(record, action: :duplicate, safe: true) }, }, show: { name: :show, @@ -63,7 +63,6 @@ def default_actions def actions acts = [] with_default = (options[:actions].presence || %i[edit destroy]) - with_default.each do |sym_or_hash| if sym_or_hash.is_a?(Symbol) next if sym_or_hash == :destroy && model.class.try(:indestructible?) diff --git a/app/controllers/concerns/folio/console/default_actions.rb b/app/controllers/concerns/folio/console/default_actions.rb index d1f6f6cd9..0d981d0b9 100644 --- a/app/controllers/concerns/folio/console/default_actions.rb +++ b/app/controllers/concerns/folio/console/default_actions.rb @@ -43,6 +43,14 @@ def edit folio_console_record.valid? if params[:prevalidate] end + def duplicate + cloned_record = Folio::Clonable::Cloner.new(folio_console_record).create_clone + cloned_record.after_clone + + instance_variable_set(folio_console_record_variable_name, cloned_record) + render :new + end + def merge @folio_console_merge = @klass index diff --git a/app/controllers/folio/console/pages_controller.rb b/app/controllers/folio/console/pages_controller.rb index e49b85029..d5c434b15 100644 --- a/app/controllers/folio/console/pages_controller.rb +++ b/app/controllers/folio/console/pages_controller.rb @@ -15,15 +15,6 @@ def index end end - def clone - page = Folio::Page.find(params[:id]) - @page = page.create_clone - @page.title = t("folio.console.clone.cloned_title", - original_title: page.title, - date: Date.today.strftime("%d. %m. %Y")) - render :new - end - private def index_filters { diff --git a/app/lib/folio/clonable/cloner.rb b/app/lib/folio/clonable/cloner.rb new file mode 100644 index 000000000..d23b80a0e --- /dev/null +++ b/app/lib/folio/clonable/cloner.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +class Folio::Clonable::Cloner + def initialize(record) + @record = record + validate_associations!(@record.class.ignored_associations) + validate_associations!(@record.class.referenced_associations) + validate_attributes!(@record.class.reset_attributes) + end + + def create_clone + log("CLONING", :info) + log(I18n.t("cloning.start", model: self.class.name, id: @record.id)) + clone, duplicated = clone_nested_records_recursively(@record) + log(I18n.t("cloning.associations_duplicated", associations: duplicated)) + reset_clone_attributes(clone) + log(I18n.t("cloning.finished")) + + if clone.respond_to?(:title=) + clone.title = @record.class.generate_cloned_title(@record.title) + end + clone + rescue => e + log(I18n.t("cloning.error", message: e.message), :error) + log(e.backtrace.first(5).join("\n"), :error) + raise + end + + def clone_nested_records_recursively(original) + duplicated = [] + cloned = original.deep_dup + copy_references(original, cloned) + original.class.reflect_on_all_associations.each do |association| + next if @record.class.ignored_associations.include?(association.name) + next if @record.class.referenced_associations.include?(association.name) + if original.public_send(association.name).present? + duplicated << association.name + if association.macro == :has_many + associated_record = original.public_send(association.name).map { |a| clone_nested_records_recursively(a).first } + else + associated_record = @record.class.referenced_associations.include?(association.name) ? original.public_send(association.name) : original.public_send(association.name).deep_dup + end + cloned.public_send("#{association.name}=", associated_record) + end + end + [cloned, duplicated] + end + + private + def validate_associations!(associations) + associations.each do |assoc| + unless @record.class.reflect_on_association(assoc) + raise ArgumentError, I18n.t("activerecord.errors.clonable.association_not_found", + association: assoc, + model: @record.class.name) + end + end + end + + def validate_attributes!(attributes) + attributes.each do |attr| + unless @record.class.column_names.include?(attr.to_s) + raise ArgumentError, I18n.t("activerecord.errors.clonable.attribute_not_found", + attribute: attr, + model: self.name) + end + end + end + + def reset_clone_attributes(clone) + @record.class.reset_attributes.each do |attr| + clone[attr] = nil if clone.has_attribute?(attr) + end + end + + def copy_references(original, cloned) + return unless @record.class.referenced_associations.present? + @record.class.referenced_associations.each do |assoc| + next unless original.class.reflect_on_association(assoc) + cloned.public_send("#{assoc}=", original.public_send(assoc)) + end + end + + + def log(message, level = :info) + if Rails.env.development? && Rails.logger + Rails.logger.tagged("CLONING") do + Rails.logger.public_send(level, message) + end + else + puts "[CLONING] #{message}" + end + end +end diff --git a/app/models/concerns/folio/console/clonable.rb b/app/models/concerns/folio/console/clonable.rb index efec5a183..860d4b1e1 100644 --- a/app/models/concerns/folio/console/clonable.rb +++ b/app/models/concerns/folio/console/clonable.rb @@ -6,26 +6,31 @@ module Folio::Console::Clonable DEFAULT_RESET_ATTRIBUTES = [:published_at, :published] included do - class_attribute :reference_associations, :duplicated_associations, :reset_attributes, - default: [], instance_writer: false - - self.reset_attributes = DEFAULT_RESET_ATTRIBUTES + def after_clone + end end class_methods do - def references_original(*associations) - validate_associations!(associations) - self.reference_associations = associations + def is_clonable? + true + end + + def ignored_associations + [] + end + + def referenced_associations + [] end - def duplicates_with_relations(*associations) - validate_associations!(associations) - self.duplicated_associations = associations + def reset_attributes + [] end - def reset_attributes_on_clone(*attributes) - validate_attributes!(attributes) - self.reset_attributes = DEFAULT_RESET_ATTRIBUTES + attributes + def generate_cloned_title(original_title) + I18n.t("folio.console.clone.cloned_title", + original_title:, + date: Date.today.strftime("%d. %m. %Y")) end private @@ -49,104 +54,4 @@ def validate_attributes!(attributes) end end end - - def create_clone - log("CLONING", :info) - log(I18n.t("cloning.start", model: self.class.name, id: id)) - - clone = deep_dup - log(I18n.t("cloning.deep_dup_finished")) - - copy_references(clone) - log(I18n.t("cloning.references_copied", references: self.class.reference_associations)) - - duplicate_nested_records(clone) - log(I18n.t("cloning.associations_duplicated", associations: self.class.duplicated_associations)) - - reset_clone_attributes(clone) - log(I18n.t("cloning.finished")) - clone -rescue => e - log(I18n.t("cloning.error", message: e.message), :error) - log(e.backtrace.first(5).join("\n"), :error) - raise - end - - private - def reset_clone_attributes(clone) - self.class.reset_attributes.each do |attr| - clone[attr] = nil if clone.has_attribute?(attr) - end - end - - def copy_references(cloned) - return unless self.class.reference_associations.present? - - self.class.reference_associations.each do |assoc| - cloned.public_send("#{assoc}=", public_send(assoc)) - end - end - - def duplicate_nested_records(cloned) - return unless self.class.duplicated_associations.present? - - self.class.duplicated_associations.each do |assoc| - cloned.public_send("#{assoc}=", clone_associated_records(assoc)) - end - end - - def clone_associated_records(association) - originals = public_send(association) - originals = [originals] unless originals.is_a?(ActiveRecord::Relation) - - clones = originals.map do |orig| - clone = orig.deep_dup - - if orig.class.reflect_on_all_associations.present? - orig.class.reflect_on_all_associations.each do |association| - clone_association(orig, clone, association) - end - end - - clone - end - - originals.is_a?(ActiveRecord::Relation) ? clones : clones.first - end - - def clone_association(original, clone, association) - if association.macro == :has_many - clone_has_many_association(original, clone, association) - else - clone_single_association(original, clone, association) - end - end - - def clone_has_many_association(original, clone, association) - return if [:files, :placements].include?(association.name) - associated_records = original.public_send(association.name) - associated_records = associated_records.map { |r| r.deep_dup } unless self.class.reference_associations.include?(association.name) - clone.association(association.name).build(associated_records.map(&:attributes)) - end - - def clone_single_association(original, clone, association) - associated_record = original.public_send(association.name) - return unless associated_record - - associated_record_dup = self.class.reference_associations.include?(association.name) ? - associated_record : - associated_record.deep_dup - - clone.public_send("#{association.name}=", associated_record_dup) - end - - def log(message, level = :info) - if Rails.env.development? && Rails.logger - Rails.logger.tagged("CLONING") do - Rails.logger.public_send(level, message) - end - else - puts "[CLONING] #{message}" - end - end end diff --git a/app/models/folio/ability.rb b/app/models/folio/ability.rb index dba1d4e30..3e7ee162e 100644 --- a/app/models/folio/ability.rb +++ b/app/models/folio/ability.rb @@ -72,6 +72,7 @@ def folio_rules cannot :impersonate, Folio::User # `can :do_anything` enabled it, so we must deny it here cannot :set_superadmin, Folio::User cannot :change_auth_site, Folio::User + cannot :duplicate, :all unless Rails.application.config.folio_console_clonable_enabled end end alias_method :folio_console_rules, :folio_rules diff --git a/app/models/folio/page.rb b/app/models/folio/page.rb index e823fce88..5452792d8 100644 --- a/app/models/folio/page.rb +++ b/app/models/folio/page.rb @@ -66,8 +66,13 @@ class Folio::Page < Folio::ApplicationRecord presence: true end - references_original :cover - duplicates_with_relations :atoms + def self.referenced_associations + [:cover] + end + + def self.ignored_associations + [:files, :slugs, :file_placements, :pg_search_document, :site] + end # Scopes scope :ordered, -> { order(position: :asc, created_at: :asc) } diff --git a/app/views/folio/console/pages/index.slim b/app/views/folio/console/pages/index.slim index 8b573ae5a..8d95f46c3 100644 --- a/app/views/folio/console/pages/index.slim +++ b/app/views/folio/console/pages/index.slim @@ -26,7 +26,7 @@ Folio::Publishable::PREVIEW_PARAM_NAME => record.preview_token) end - actions({ preview: }, :edit, :destroy) + actions({ preview: }, :edit, :destroy, :duplicate) elsif record.is_a?(Folio::Page) && record.class.try(:public_rails_path) actions({ preview: controller.main_app.send(record.class.public_rails_path, locale: locale) }, :edit, diff --git a/config/routes.rb b/config/routes.rb index 58096de8a..0e270bd87 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -54,7 +54,7 @@ get :revision, path: "revision/:version" post :restore, path: "restore/:version" end - get :clone + get :duplicate end end diff --git a/lib/folio/engine.rb b/lib/folio/engine.rb index 814d190a6..a576177d9 100644 --- a/lib/folio/engine.rb +++ b/lib/folio/engine.rb @@ -37,6 +37,7 @@ class Engine < ::Rails::Engine config.folio_console_default_routes_contstraints = {} config.folio_console_add_locale_to_preview_links = false config.folio_console_files_additional_html_api_url_lambda = -> (file) { nil } + config.folio_console_clonable_enabled = false config.folio_newsletter_subscription_service = :mailchimp config.folio_server_names = [] diff --git a/test/controllers/folio/console/pages_controller_test.rb b/test/controllers/folio/console/pages_controller_test.rb index 77a76bf37..2aec2f32f 100644 --- a/test/controllers/folio/console/pages_controller_test.rb +++ b/test/controllers/folio/console/pages_controller_test.rb @@ -52,7 +52,7 @@ class Folio::Console::PagesControllerTest < Folio::Console::BaseControllerTest test "clone" do page = create(:folio_page) - get url_for([:clone, :console, page]) + get url_for([:duplicate, :console, page]) assert_response :success end end diff --git a/test/dummy/app/views/folio/console/pages/index.slim b/test/dummy/app/views/folio/console/pages/index.slim deleted file mode 100644 index 914e18456..000000000 --- a/test/dummy/app/views/folio/console/pages/index.slim +++ /dev/null @@ -1,38 +0,0 @@ -= index_header - -= catalogue(@catalogue_model || @pages, @catalogue_options || {}) - ruby: - edit_link :title - - type - - locale_flag if Rails.application.config.folio_pages_locales - - published_toggle - - date(:published_at) - - position_controls if model && model[:ancestry] - - locale = (Folio::Current.site.locales.size > 1 || Rails.application.config.folio_console_add_locale_to_preview_links) ? I18n.locale : nil - - if record.is_a?(Folio::Page) && record.class.try(:public?) - preview = if record.published? && !model[:ancestry] - controller.main_app.page_path(record.to_preview_param, - locale: locale) - else - controller.main_app.page_path(record.to_preview_param, - locale: locale, - Folio::Publishable::PREVIEW_PARAM_NAME => record.preview_token) - end - - actions({ preview: }, :edit, :destroy, :clone) - elsif record.is_a?(Folio::Page) && record.class.try(:public_rails_path) - actions({ preview: controller.main_app.send(record.class.public_rails_path, locale: locale) }, - :edit, - :destroy) - else - actions(:edit, :destroy) - end - - transportable_dropdown diff --git a/test/dummy/config/application.rb b/test/dummy/config/application.rb index 87f7f95eb..28945fcd7 100644 --- a/test/dummy/config/application.rb +++ b/test/dummy/config/application.rb @@ -19,6 +19,7 @@ class Application < Rails::Application config.folio_leads_from_component_class_name = "Folio::Leads::FormComponent" config.folio_newsletter_subscriptions = true config.folio_site_default_test_factory = :dummy_site + config.folio_console_clonable_enabled = true I18n.available_locales = [:cs, :en] I18n.default_locale = :cs diff --git a/test/models/concerns/folio/clonable_test.rb b/test/models/concerns/folio/clonable_test.rb index a08d888b1..3ec1309df 100644 --- a/test/models/concerns/folio/clonable_test.rb +++ b/test/models/concerns/folio/clonable_test.rb @@ -21,7 +21,7 @@ class Folio::ClonableTest < ActiveSupport::TestCase page.cover = image original_attributes = page.attributes - clone = page.create_clone + clone = Folio::Clonable::Cloner.new(page).create_clone clone.title = "clone" assert clone.valid? From caa3b50e0fb57564b5a2c142af1932e7f87ecd6d Mon Sep 17 00:00:00 2001 From: ricardoo27 <1345603+ricardoo27@users.noreply.github.com> Date: Mon, 9 Dec 2024 12:59:15 +0100 Subject: [PATCH 6/7] Renamed Clonable class methods. --- app/lib/folio/clonable/cloner.rb | 18 +++++++++--------- app/models/concerns/folio/console/clonable.rb | 6 +++--- app/models/folio/page.rb | 8 ++++++-- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/app/lib/folio/clonable/cloner.rb b/app/lib/folio/clonable/cloner.rb index d23b80a0e..974ca6959 100644 --- a/app/lib/folio/clonable/cloner.rb +++ b/app/lib/folio/clonable/cloner.rb @@ -3,9 +3,9 @@ class Folio::Clonable::Cloner def initialize(record) @record = record - validate_associations!(@record.class.ignored_associations) - validate_associations!(@record.class.referenced_associations) - validate_attributes!(@record.class.reset_attributes) + validate_associations!(@record.class.clonable_ignored_associations) + validate_associations!(@record.class.clonable_referenced_associations) + validate_attributes!(@record.class.clonable_reset_attributes) end def create_clone @@ -31,14 +31,14 @@ def clone_nested_records_recursively(original) cloned = original.deep_dup copy_references(original, cloned) original.class.reflect_on_all_associations.each do |association| - next if @record.class.ignored_associations.include?(association.name) - next if @record.class.referenced_associations.include?(association.name) + next if @record.class.clonable_ignored_associations.include?(association.name) + next if @record.class.clonable_referenced_associations.include?(association.name) if original.public_send(association.name).present? duplicated << association.name if association.macro == :has_many associated_record = original.public_send(association.name).map { |a| clone_nested_records_recursively(a).first } else - associated_record = @record.class.referenced_associations.include?(association.name) ? original.public_send(association.name) : original.public_send(association.name).deep_dup + associated_record = @record.class.clonable_referenced_associations.include?(association.name) ? original.public_send(association.name) : original.public_send(association.name).deep_dup end cloned.public_send("#{association.name}=", associated_record) end @@ -68,14 +68,14 @@ def validate_attributes!(attributes) end def reset_clone_attributes(clone) - @record.class.reset_attributes.each do |attr| + @record.class.clonable_reset_attributes.each do |attr| clone[attr] = nil if clone.has_attribute?(attr) end end def copy_references(original, cloned) - return unless @record.class.referenced_associations.present? - @record.class.referenced_associations.each do |assoc| + return unless @record.class.clonable_referenced_associations.present? + @record.class.clonable_referenced_associations.each do |assoc| next unless original.class.reflect_on_association(assoc) cloned.public_send("#{assoc}=", original.public_send(assoc)) end diff --git a/app/models/concerns/folio/console/clonable.rb b/app/models/concerns/folio/console/clonable.rb index 860d4b1e1..832ae054a 100644 --- a/app/models/concerns/folio/console/clonable.rb +++ b/app/models/concerns/folio/console/clonable.rb @@ -15,15 +15,15 @@ def is_clonable? true end - def ignored_associations + def clonable_ignored_associations [] end - def referenced_associations + def clonable_referenced_associations [] end - def reset_attributes + def clonable_reset_attributes [] end diff --git a/app/models/folio/page.rb b/app/models/folio/page.rb index 5452792d8..e46c1440d 100644 --- a/app/models/folio/page.rb +++ b/app/models/folio/page.rb @@ -66,14 +66,18 @@ class Folio::Page < Folio::ApplicationRecord presence: true end - def self.referenced_associations + def self.clonable_referenced_associations [:cover] end - def self.ignored_associations + def self.clonable_ignored_associations [:files, :slugs, :file_placements, :pg_search_document, :site] end + def self.clonable_reset_attributes + [:published, :published_at] + end + # Scopes scope :ordered, -> { order(position: :asc, created_at: :asc) } scope :featured, -> { where(featured: true) } From ecaf31d9c74c1843b9c1586d255c4290b73cbe26 Mon Sep 17 00:00:00 2001 From: ricardoo27 <1345603+ricardoo27@users.noreply.github.com> Date: Mon, 9 Dec 2024 13:21:11 +0100 Subject: [PATCH 7/7] guard. --- app/lib/folio/clonable/cloner.rb | 98 ++++++++++++++++---------------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/app/lib/folio/clonable/cloner.rb b/app/lib/folio/clonable/cloner.rb index 974ca6959..cace1f087 100644 --- a/app/lib/folio/clonable/cloner.rb +++ b/app/lib/folio/clonable/cloner.rb @@ -26,69 +26,69 @@ def create_clone raise end - def clone_nested_records_recursively(original) - duplicated = [] - cloned = original.deep_dup - copy_references(original, cloned) - original.class.reflect_on_all_associations.each do |association| - next if @record.class.clonable_ignored_associations.include?(association.name) - next if @record.class.clonable_referenced_associations.include?(association.name) - if original.public_send(association.name).present? - duplicated << association.name - if association.macro == :has_many - associated_record = original.public_send(association.name).map { |a| clone_nested_records_recursively(a).first } - else - associated_record = @record.class.clonable_referenced_associations.include?(association.name) ? original.public_send(association.name) : original.public_send(association.name).deep_dup + private + def clone_nested_records_recursively(original) + duplicated = [] + cloned = original.deep_dup + copy_references(original, cloned) + original.class.reflect_on_all_associations.each do |association| + next if @record.class.clonable_ignored_associations.include?(association.name) + next if @record.class.clonable_referenced_associations.include?(association.name) + if original.public_send(association.name).present? + duplicated << association.name + if association.macro == :has_many + associated_record = original.public_send(association.name).map { |a| clone_nested_records_recursively(a).first } + else + associated_record = @record.class.clonable_referenced_associations.include?(association.name) ? original.public_send(association.name) : original.public_send(association.name).deep_dup + end + cloned.public_send("#{association.name}=", associated_record) end - cloned.public_send("#{association.name}=", associated_record) end + [cloned, duplicated] end - [cloned, duplicated] - end - private - def validate_associations!(associations) - associations.each do |assoc| - unless @record.class.reflect_on_association(assoc) - raise ArgumentError, I18n.t("activerecord.errors.clonable.association_not_found", - association: assoc, - model: @record.class.name) - end + def validate_associations!(associations) + associations.each do |assoc| + unless @record.class.reflect_on_association(assoc) + raise ArgumentError, I18n.t("activerecord.errors.clonable.association_not_found", + association: assoc, + model: @record.class.name) end - end + end + end - def validate_attributes!(attributes) - attributes.each do |attr| - unless @record.class.column_names.include?(attr.to_s) - raise ArgumentError, I18n.t("activerecord.errors.clonable.attribute_not_found", - attribute: attr, - model: self.name) - end + def validate_attributes!(attributes) + attributes.each do |attr| + unless @record.class.column_names.include?(attr.to_s) + raise ArgumentError, I18n.t("activerecord.errors.clonable.attribute_not_found", + attribute: attr, + model: self.name) end end + end - def reset_clone_attributes(clone) - @record.class.clonable_reset_attributes.each do |attr| - clone[attr] = nil if clone.has_attribute?(attr) - end + def reset_clone_attributes(clone) + @record.class.clonable_reset_attributes.each do |attr| + clone[attr] = nil if clone.has_attribute?(attr) end + end - def copy_references(original, cloned) - return unless @record.class.clonable_referenced_associations.present? - @record.class.clonable_referenced_associations.each do |assoc| - next unless original.class.reflect_on_association(assoc) - cloned.public_send("#{assoc}=", original.public_send(assoc)) - end + def copy_references(original, cloned) + return unless @record.class.clonable_referenced_associations.present? + @record.class.clonable_referenced_associations.each do |assoc| + next unless original.class.reflect_on_association(assoc) + cloned.public_send("#{assoc}=", original.public_send(assoc)) end + end - def log(message, level = :info) - if Rails.env.development? && Rails.logger - Rails.logger.tagged("CLONING") do - Rails.logger.public_send(level, message) - end - else - puts "[CLONING] #{message}" + def log(message, level = :info) + if Rails.env.development? && Rails.logger + Rails.logger.tagged("CLONING") do + Rails.logger.public_send(level, message) end + else + puts "[CLONING] #{message}" end + end end