diff --git a/app/components/attachable/fields_component.html.erb b/app/components/attachable/fields_component.html.erb new file mode 100644 index 000000000..ef329a31e --- /dev/null +++ b/app/components/attachable/fields_component.html.erb @@ -0,0 +1,31 @@ +
+ <%= f.hidden_field :id %> + <%= f.hidden_field :user_id, value: current_user.id %> + <%= f.hidden_field :cached_attachment %> + +
+ <%= f.text_field :title, placeholder: t("#{plural_name}.form.title_placeholder") %> +
+ + <% if attachable.attachment.exists? && attachable.attachment.styles.keys.include?(:thumb) %> + <%= render_image(attachable, :thumb, false) %> + <% end %> + +
+

<%= file_name %>

+ +
+ <%= file_field %> +
+ +
+ <%= destroy_link %> +
+
+ +
+
+
+ +
+
diff --git a/app/components/attachable/fields_component.rb b/app/components/attachable/fields_component.rb new file mode 100644 index 000000000..ae217456a --- /dev/null +++ b/app/components/attachable/fields_component.rb @@ -0,0 +1,70 @@ +class Attachable::FieldsComponent < ApplicationComponent + attr_reader :f, :resource_type, :resource_id, :relation_name + delegate :current_user, :render_image, to: :helpers + + def initialize(f, resource_type:, resource_id:, relation_name:) + @f = f + @resource_type = resource_type + @resource_id = resource_id + @relation_name = relation_name + end + + private + + def attachable + f.object + end + + def singular_name + attachable.model_name.singular + end + + def plural_name + attachable.model_name.plural + end + + def file_name + attachable.attachment_file_name + end + + def destroy_link + if !attachable.persisted? && attachable.cached_attachment.present? + link_to t("#{plural_name}.form.delete_button"), "#", class: "delete remove-cached-attachment" + else + link_to_remove_association remove_association_text, f, class: "delete remove-#{singular_name}" + end + end + + def remove_association_text + if attachable.new_record? + t("documents.form.cancel_button") + else + t("#{plural_name}.form.delete_button") + end + end + + def file_field + klass = attachable.persisted? || attachable.cached_attachment.present? ? " hide" : "" + f.file_field :attachment, + label_options: { class: "button hollow #{klass}" }, + accept: accepted_content_types_extensions, + class: "js-#{singular_name}-attachment", + data: { url: direct_upload_path } + end + + def direct_upload_path + direct_uploads_path("direct_upload[resource_type]": resource_type, + "direct_upload[resource_id]": resource_id, + "direct_upload[resource_relation]": relation_name) + end + + def accepted_content_types_extensions + Setting.accepted_content_types_for(plural_name).map do |content_type| + if content_type == "jpg" + ".jpg,.jpeg" + else + ".#{content_type}" + end + end.join(",") + end +end diff --git a/app/components/documents/fields_component.html.erb b/app/components/documents/fields_component.html.erb index 5822f89ba..4520d7527 100644 --- a/app/components/documents/fields_component.html.erb +++ b/app/components/documents/fields_component.html.erb @@ -1,28 +1,6 @@ -
- <%= f.hidden_field :id %> - <%= f.hidden_field :user_id, value: current_user.id %> - <%= f.hidden_field :cached_attachment %> - -
- <%= f.text_field :title, placeholder: t("documents.form.title_placeholder") %> -
- -
-

<%= file_name %>

- -
- <%= file_field %> -
- -
- <%= destroy_link %> -
-
- -
-
-
- -
- -
+<%= render Attachable::FieldsComponent.new( + f, + resource_type: document.documentable_type, + resource_id: document.documentable_id, + relation_name: "documents" +) %> diff --git a/app/components/documents/fields_component.rb b/app/components/documents/fields_component.rb index 1ef9febea..01da058c0 100644 --- a/app/components/documents/fields_component.rb +++ b/app/components/documents/fields_component.rb @@ -1,6 +1,5 @@ class Documents::FieldsComponent < ApplicationComponent attr_reader :f - delegate :current_user, to: :helpers def initialize(f) @f = f @@ -11,35 +10,4 @@ class Documents::FieldsComponent < ApplicationComponent def document f.object end - - def file_name - document.attachment_file_name - end - - def destroy_link - if !document.persisted? && document.cached_attachment.present? - link_to t("documents.form.delete_button"), "#", class: "delete remove-cached-attachment" - else - link_to_remove_association document.new_record? ? t("documents.form.cancel_button") : t("documents.form.delete_button"), f, class: "delete remove-document" - end - end - - def file_field - klass = document.persisted? || document.cached_attachment.present? ? " hide" : "" - f.file_field :attachment, - label_options: { class: "button hollow #{klass}" }, - accept: accepted_content_types_extensions, - class: "js-document-attachment", - data: { url: direct_upload_path } - end - - def direct_upload_path - direct_uploads_path("direct_upload[resource_type]": document.documentable_type, - "direct_upload[resource_id]": document.documentable_id, - "direct_upload[resource_relation]": "documents") - end - - def accepted_content_types_extensions - Setting.accepted_content_types_for("documents").map { |content_type| ".#{content_type}" }.join(",") - end end diff --git a/app/components/documents/nested_component.rb b/app/components/documents/nested_component.rb index a37587e81..2b4f6f909 100644 --- a/app/components/documents/nested_component.rb +++ b/app/components/documents/nested_component.rb @@ -1,6 +1,5 @@ class Documents::NestedComponent < ApplicationComponent attr_reader :f - delegate :documentable_humanized_accepted_content_types, :max_file_size, to: :helpers def initialize(f) @f = f @@ -18,8 +17,8 @@ class Documents::NestedComponent < ApplicationComponent def note t "documents.form.note", max_documents_allowed: max_documents_allowed, - accepted_content_types: documentable_humanized_accepted_content_types(documentable.class), - max_file_size: max_file_size(documentable.class) + accepted_content_types: Document.humanized_accepted_content_types, + max_file_size: documentable.class.max_file_size end def max_documents_allowed? diff --git a/app/components/images/fields_component.html.erb b/app/components/images/fields_component.html.erb index 49d8f1fd8..4a2df33af 100644 --- a/app/components/images/fields_component.html.erb +++ b/app/components/images/fields_component.html.erb @@ -1,29 +1,6 @@ -
- <%= f.hidden_field :id %> - <%= f.hidden_field :user_id, value: current_user.id %> - <%= f.hidden_field :cached_attachment %> - -
- <%= f.text_field :title, placeholder: t("images.form.title_placeholder") %> -
- - <%= render_image(image, :thumb, false) if image.attachment.exists? %> - -
-

<%= file_name %>

- -
- <%= file_field %> -
- -
- <%= destroy_link %> -
-
- -
-
-
- -
-
+<%= render Attachable::FieldsComponent.new( + f, + resource_type: imageable.class.name, + resource_id: imageable.id, + relation_name: "image" +) %> diff --git a/app/components/images/fields_component.rb b/app/components/images/fields_component.rb index ddf60fa45..44c3ad0a1 100644 --- a/app/components/images/fields_component.rb +++ b/app/components/images/fields_component.rb @@ -1,52 +1,8 @@ class Images::FieldsComponent < ApplicationComponent attr_reader :f, :imageable - delegate :current_user, :render_image, to: :helpers def initialize(f, imageable:) @f = f @imageable = imageable end - - private - - def image - f.object - end - - def file_name - image.attachment_file_name - end - - def destroy_link - if !image.persisted? && image.cached_attachment.present? - link_to t("images.form.delete_button"), "#", class: "delete remove-cached-attachment" - else - link_to_remove_association t("images.form.delete_button"), f, class: "delete remove-image" - end - end - - def file_field - klass = image.persisted? || image.cached_attachment.present? ? " hide" : "" - f.file_field :attachment, - label_options: { class: "button hollow #{klass}" }, - accept: accepted_content_types_extensions, - class: "js-image-attachment", - data: { url: direct_upload_path } - end - - def direct_upload_path - direct_uploads_path("direct_upload[resource_type]": imageable.class.name, - "direct_upload[resource_id]": imageable.id, - "direct_upload[resource_relation]": "image") - end - - def accepted_content_types_extensions - Setting.accepted_content_types_for("images").map do |content_type| - if content_type == "jpg" - ".jpg,.jpeg" - else - ".#{content_type}" - end - end.join(",") - end end diff --git a/app/components/images/nested_component.rb b/app/components/images/nested_component.rb index 61087bcff..768f4d880 100644 --- a/app/components/images/nested_component.rb +++ b/app/components/images/nested_component.rb @@ -1,6 +1,5 @@ class Images::NestedComponent < ApplicationComponent attr_reader :f, :image_fields - delegate :imageable_humanized_accepted_content_types, :imageable_max_file_size, to: :helpers def initialize(f, image_fields: :image) @f = f @@ -14,7 +13,7 @@ class Images::NestedComponent < ApplicationComponent end def note - t "images.form.note", accepted_content_types: imageable_humanized_accepted_content_types, - max_file_size: imageable_max_file_size + t "images.form.note", accepted_content_types: Image.humanized_accepted_content_types, + max_file_size: Image.max_file_size end end diff --git a/app/helpers/documentables_helper.rb b/app/helpers/documentables_helper.rb deleted file mode 100644 index 5dcffacb3..000000000 --- a/app/helpers/documentables_helper.rb +++ /dev/null @@ -1,13 +0,0 @@ -module DocumentablesHelper - def max_file_size(documentable_class) - documentable_class.max_file_size / Numeric::MEGABYTE - end - - def accepted_content_types(documentable_class) - documentable_class.accepted_content_types - end - - def documentable_humanized_accepted_content_types(documentable_class) - Setting.accepted_content_types_for("documents").join(", ") - end -end diff --git a/app/helpers/imageables_helper.rb b/app/helpers/imageables_helper.rb index c0e4c4893..930342025 100644 --- a/app/helpers/imageables_helper.rb +++ b/app/helpers/imageables_helper.rb @@ -2,16 +2,4 @@ module ImageablesHelper def can_destroy_image?(imageable) imageable.image.present? && can?(:destroy, imageable.image) end - - def imageable_max_file_size - Setting["uploads.images.max_size"].to_i - end - - def imageable_accepted_content_types - Setting["uploads.images.content_types"]&.split(" ") || ["image/jpeg"] - end - - def imageable_humanized_accepted_content_types - Setting.accepted_content_types_for("images").join(", ") - end end diff --git a/app/models/concerns/attachable.rb b/app/models/concerns/attachable.rb new file mode 100644 index 000000000..fbc29a055 --- /dev/null +++ b/app/models/concerns/attachable.rb @@ -0,0 +1,75 @@ +module Attachable + extend ActiveSupport::Concern + + included do + attr_accessor :cached_attachment + + # Disable paperclip security validation due to polymorphic configuration + # Paperclip do not allow to use Procs on valiations definition + do_not_validate_attachment_file_type :attachment + validate :attachment_presence + validate :validate_attachment_content_type, if: -> { attachment.present? } + validate :validate_attachment_size, if: -> { attachment.present? } + + before_save :set_attachment_from_cached_attachment, if: -> { cached_attachment.present? } + + Paperclip.interpolates :prefix do |attachment, style| + attachment.instance.prefix(attachment, style) + end + end + + def association_class + type = send("#{association_name}_type") + + type.constantize if type.present? + end + + def set_cached_attachment_from_attachment + self.cached_attachment = if Paperclip::Attachment.default_options[:storage] == :filesystem + attachment.path + else + attachment.url + end + end + + def set_attachment_from_cached_attachment + self.attachment = if Paperclip::Attachment.default_options[:storage] == :filesystem + File.open(cached_attachment) + else + URI.parse(cached_attachment) + end + end + + def prefix(attachment, _style) + if attachment.instance.persisted? + ":attachment/:id_partition" + else + "cached_attachments/user/#{attachment.instance.user_id}" + end + end + + private + + def validate_attachment_size + if association_class && attachment_file_size > max_file_size.megabytes + errors.add(:attachment, I18n.t("#{model_name.plural}.errors.messages.in_between", + min: "0 Bytes", + max: "#{max_file_size} MB")) + end + end + + def validate_attachment_content_type + if association_class && !accepted_content_types.include?(attachment_content_type) + message = I18n.t("#{model_name.plural}.errors.messages.wrong_content_type", + content_type: attachment_content_type, + accepted_content_types: self.class.humanized_accepted_content_types) + errors.add(:attachment, message) + end + end + + def attachment_presence + if attachment.blank? && cached_attachment.blank? + errors.add(:attachment, I18n.t("errors.messages.blank")) + end + end +end diff --git a/app/models/concerns/documentable.rb b/app/models/concerns/documentable.rb index dcff3461d..dd4aa531a 100644 --- a/app/models/concerns/documentable.rb +++ b/app/models/concerns/documentable.rb @@ -12,7 +12,7 @@ module Documentable end def max_file_size - Setting["uploads.documents.max_size"].to_i.megabytes + Setting["uploads.documents.max_size"].to_i end def accepted_content_types diff --git a/app/models/document.rb b/app/models/document.rb index da9bbfd08..cc24ef871 100644 --- a/app/models/document.rb +++ b/app/models/document.rb @@ -1,60 +1,27 @@ class Document < ApplicationRecord - include DocumentsHelper - include DocumentablesHelper + include Attachable + has_attached_file :attachment, url: "/system/:class/:prefix/:style/:hash.:extension", hash_data: ":class/:style/:custom_hash_data", use_timestamp: false, hash_secret: Rails.application.secrets.secret_key_base - attr_accessor :cached_attachment belongs_to :user belongs_to :documentable, polymorphic: true - # Disable paperclip security validation due to polymorphic configuration - # Paperclip do not allow to use Procs on valiations definition - do_not_validate_attachment_file_type :attachment - validate :attachment_presence - validate :validate_attachment_content_type, if: -> { attachment.present? } - validate :validate_attachment_size, if: -> { attachment.present? } validates :title, presence: true validates :user_id, presence: true validates :documentable_id, presence: true, if: -> { persisted? } validates :documentable_type, presence: true, if: -> { persisted? } - before_save :set_attachment_from_cached_attachment, if: -> { cached_attachment.present? } - scope :admin, -> { where(admin: true) } - def set_cached_attachment_from_attachment - self.cached_attachment = if Paperclip::Attachment.default_options[:storage] == :filesystem - attachment.path - else - attachment.url - end - end - - def set_attachment_from_cached_attachment - self.attachment = if Paperclip::Attachment.default_options[:storage] == :filesystem - File.open(cached_attachment) - else - URI.parse(cached_attachment) - end - end - - Paperclip.interpolates :prefix do |attachment, style| - attachment.instance.prefix(attachment, style) - end - Paperclip.interpolates :custom_hash_data do |attachment, _style| attachment.instance.custom_hash_data(attachment) end - def prefix(attachment, _style) - if attachment.instance.persisted? - ":attachment/:id_partition" - else - "cached_attachments/user/#{attachment.instance.user_id}" - end + def self.humanized_accepted_content_types + Setting.accepted_content_types_for("documents").join(", ") end def custom_hash_data(attachment) @@ -70,35 +37,21 @@ class Document < ApplicationRecord attachment_content_type.split("/").last.upcase end + def max_file_size + documentable_class.max_file_size + end + + def accepted_content_types + documentable_class.accepted_content_types + end + private + def association_name + :documentable + end + def documentable_class - documentable_type.constantize if documentable_type.present? - end - - def validate_attachment_size - if documentable_class.present? && - attachment_file_size > documentable_class.max_file_size - errors.add(:attachment, I18n.t("documents.errors.messages.in_between", - min: "0 Bytes", - max: "#{max_file_size(documentable_class)} MB")) - end - end - - def validate_attachment_content_type - if documentable_class && - !accepted_content_types(documentable_class).include?(attachment_content_type) - accepted_content_types = documentable_humanized_accepted_content_types(documentable_class) - message = I18n.t("documents.errors.messages.wrong_content_type", - content_type: attachment_content_type, - accepted_content_types: accepted_content_types) - errors.add(:attachment, message) - end - end - - def attachment_presence - if attachment.blank? && cached_attachment.blank? - errors.add(:attachment, I18n.t("errors.messages.blank")) - end + association_class end end diff --git a/app/models/image.rb b/app/models/image.rb index 961307b23..19825d629 100644 --- a/app/models/image.rb +++ b/app/models/image.rb @@ -1,6 +1,5 @@ class Image < ApplicationRecord - include ImagesHelper - include ImageablesHelper + include Attachable has_attached_file :attachment, styles: { large: "x#{Setting["uploads.images.min_height"]}", @@ -11,17 +10,10 @@ class Image < ApplicationRecord hash_data: ":class/:style", use_timestamp: false, hash_secret: Rails.application.secrets.secret_key_base - attr_accessor :cached_attachment belongs_to :user belongs_to :imageable, polymorphic: true - # Disable paperclip security validation due to polymorphic configuration - # Paperclip do not allow to use Procs on valiations definition - do_not_validate_attachment_file_type :attachment - validate :attachment_presence - validate :validate_attachment_content_type, if: -> { attachment.present? } - validate :validate_attachment_size, if: -> { attachment.present? } validates :title, presence: true validate :validate_title_length validates :user_id, presence: true @@ -29,40 +21,34 @@ class Image < ApplicationRecord validates :imageable_type, presence: true, if: -> { persisted? } validate :validate_image_dimensions, if: -> { attachment.present? && attachment.dirty? } - before_save :set_attachment_from_cached_attachment, if: -> { cached_attachment.present? } - - def set_cached_attachment_from_attachment - self.cached_attachment = if Paperclip::Attachment.default_options[:storage] == :filesystem - attachment.path - else - attachment.url - end + def self.max_file_size + Setting["uploads.images.max_size"].to_i end - def set_attachment_from_cached_attachment - self.attachment = if Paperclip::Attachment.default_options[:storage] == :filesystem - File.open(cached_attachment) - else - URI.parse(cached_attachment) - end + def self.accepted_content_types + Setting["uploads.images.content_types"]&.split(" ") || ["image/jpeg"] end - Paperclip.interpolates :prefix do |attachment, style| - attachment.instance.prefix(attachment, style) + def self.humanized_accepted_content_types + Setting.accepted_content_types_for("images").join(", ") end - def prefix(attachment, _style) - if attachment.instance.persisted? - ":attachment/:id_partition" - else - "cached_attachments/user/#{attachment.instance.user_id}" - end + def max_file_size + self.class.max_file_size + end + + def accepted_content_types + self.class.accepted_content_types end private + def association_name + :imageable + end + def imageable_class - imageable_type.constantize if imageable_type.present? + association_class end def validate_image_dimensions @@ -77,15 +63,6 @@ class Image < ApplicationRecord end end - def validate_attachment_size - if imageable_class && - attachment_file_size > Setting["uploads.images.max_size"].to_i.megabytes - errors.add(:attachment, I18n.t("images.errors.messages.in_between", - min: "0 Bytes", - max: "#{imageable_max_file_size} MB")) - end - end - def validate_title_length if title.present? title_min_length = Setting["uploads.images.title.min_length"].to_i @@ -101,22 +78,7 @@ class Image < ApplicationRecord end end - def validate_attachment_content_type - if imageable_class && !attachment_of_valid_content_type? - message = I18n.t("images.errors.messages.wrong_content_type", - content_type: attachment_content_type, - accepted_content_types: imageable_humanized_accepted_content_types) - errors.add(:attachment, message) - end - end - - def attachment_presence - if attachment.blank? && cached_attachment.blank? - errors.add(:attachment, I18n.t("errors.messages.blank")) - end - end - def attachment_of_valid_content_type? - attachment.present? && imageable_accepted_content_types.include?(attachment_content_type) + attachment.present? && accepted_content_types.include?(attachment_content_type) end end diff --git a/spec/shared/models/document_validations.rb b/spec/shared/models/document_validations.rb index 57daf7cd1..194c8fbba 100644 --- a/spec/shared/models/document_validations.rb +++ b/spec/shared/models/document_validations.rb @@ -1,9 +1,7 @@ shared_examples "document validations" do |documentable_factory| - include DocumentablesHelper - let!(:document) { build(:document, documentable_factory.to_sym) } - let!(:maxfilesize) { max_file_size(document.documentable.class) } - let!(:acceptedcontenttypes) { accepted_content_types(document.documentable.class) } + let!(:maxfilesize) { document.max_file_size } + let!(:acceptedcontenttypes) { document.accepted_content_types } it "is valid" do expect(document).to be_valid diff --git a/spec/shared/models/image_validations.rb b/spec/shared/models/image_validations.rb index 27cc26da8..092372550 100644 --- a/spec/shared/models/image_validations.rb +++ b/spec/shared/models/image_validations.rb @@ -1,8 +1,6 @@ shared_examples "image validations" do |imageable_factory| - include ImageablesHelper - let!(:image) { build(:image, imageable_factory.to_sym) } - let!(:acceptedcontenttypes) { imageable_accepted_content_types } + let!(:acceptedcontenttypes) { Image.accepted_content_types } it "is valid" do expect(image).to be_valid