Merge pull request #4595 from consul/attachable

Reduce duplication in attachments code
This commit is contained in:
Javi Martín
2021-09-13 11:32:43 +02:00
committed by GitHub
16 changed files with 232 additions and 293 deletions

View File

@@ -0,0 +1,31 @@
<div class="<%= singular_name %> direct-upload nested-fields">
<%= f.hidden_field :id %>
<%= f.hidden_field :user_id, value: current_user.id %>
<%= f.hidden_field :cached_attachment %>
<div class="small-12 column title">
<%= f.text_field :title, placeholder: t("#{plural_name}.form.title_placeholder") %>
</div>
<% if attachable.attachment.exists? && attachable.attachment.styles.keys.include?(:thumb) %>
<%= render_image(attachable, :thumb, false) %>
<% end %>
<div class="small-12 column attachment-actions">
<p class="file-name small-9 column"><%= file_name %></p>
<div class="small-9 column action-add attachment-errors <%= singular_name %>-attachment">
<%= file_field %>
</div>
<div class="small-3 column action-remove text-right">
<%= destroy_link %>
</div>
</div>
<div class="small-12 column">
<div class="progress-bar-placeholder"><div class="loading-bar"></div></div>
</div>
<hr>
</div>

View File

@@ -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

View File

@@ -1,28 +1,6 @@
<div class="document direct-upload document-fields nested-fields">
<%= f.hidden_field :id %>
<%= f.hidden_field :user_id, value: current_user.id %>
<%= f.hidden_field :cached_attachment %>
<div class="small-12 column title">
<%= f.text_field :title, placeholder: t("documents.form.title_placeholder") %>
</div>
<div class="small-12 column attachment-actions">
<p class="file-name small-9 column"><%= file_name %></p>
<div class="small-9 column action-add attachment-errors document-attachment">
<%= file_field %>
</div>
<div class="small-3 column action-remove text-right">
<%= destroy_link %>
</div>
</div>
<div class="small-12 column">
<div class="progress-bar-placeholder"><div class="loading-bar"></div></div>
</div>
<hr>
</div>
<%= render Attachable::FieldsComponent.new(
f,
resource_type: document.documentable_type,
resource_id: document.documentable_id,
relation_name: "documents"
) %>

View File

@@ -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

View File

@@ -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?

View File

@@ -1,29 +1,6 @@
<div class="image direct-upload nested-fields">
<%= f.hidden_field :id %>
<%= f.hidden_field :user_id, value: current_user.id %>
<%= f.hidden_field :cached_attachment %>
<div class="small-12 margin-top title">
<%= f.text_field :title, placeholder: t("images.form.title_placeholder") %>
</div>
<%= render_image(image, :thumb, false) if image.attachment.exists? %>
<div class="small-12 column attachment-actions">
<p class="file-name small-9 column"><%= file_name %></p>
<div class="small-9 column action-add attachment-errors image-attachment">
<%= file_field %>
</div>
<div class="small-3 column action-remove text-right">
<%= destroy_link %>
</div>
</div>
<div class="small-12 column">
<div class="progress-bar-placeholder"><div class="loading-bar"></div></div>
</div>
<hr>
</div>
<%= render Attachable::FieldsComponent.new(
f,
resource_type: imageable.class.name,
resource_id: imageable.id,
relation_name: "image"
) %>

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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