From c6dabedb4a60b2ecb9fa032dc0084678796f9f4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sen=C3=A9n=20Rodero=20Rodr=C3=ADguez?= Date: Fri, 15 Sep 2017 19:02:49 +0200 Subject: [PATCH] Add missing image model spec. Add shared specs to check image validations at any imageable model --- app/controllers/direct_uploads_controller.rb | 40 +++++++++++ app/helpers/documentables_helper.rb | 10 +-- app/models/direct_upload.rb | 28 ++++++++ app/models/document.rb | 14 ++-- app/models/image.rb | 8 ++- app/views/documents/new.html.erb | 4 +- spec/factories.rb | 19 +++++ spec/fixtures/files/clippy.jpeg | Bin 0 -> 12394 bytes spec/models/direct_upload_spec.rb | 19 +++++ spec/models/image_spec.rb | 8 +++ spec/shared/features/documentable.rb | 2 +- spec/shared/models/document_validations.rb | 2 +- spec/shared/models/image_validations.rb | 70 +++++++++++++++++++ 13 files changed, 208 insertions(+), 16 deletions(-) create mode 100644 app/controllers/direct_uploads_controller.rb create mode 100644 app/models/direct_upload.rb create mode 100644 spec/fixtures/files/clippy.jpeg create mode 100644 spec/models/direct_upload_spec.rb create mode 100644 spec/models/image_spec.rb create mode 100644 spec/shared/models/image_validations.rb diff --git a/app/controllers/direct_uploads_controller.rb b/app/controllers/direct_uploads_controller.rb new file mode 100644 index 000000000..e6a8c139a --- /dev/null +++ b/app/controllers/direct_uploads_controller.rb @@ -0,0 +1,40 @@ +class DirectUploadsController < ApplicationController + + def destroy_upload + @document = Document.new(cached_attachment: params[:path]) + @document.set_attachment_from_cached_attachment + @document.cached_attachment = nil + @document.documentable = @documentable + + if @document.attachment.destroy + flash.now[:notice] = t "documents.actions.destroy.notice" + else + flash.now[:alert] = t "documents.actions.destroy.alert" + end + render :destroy + end + + def upload + @document = Document.new(document_params.merge(user: current_user)) + @document.documentable = @documentable + @document.valid? + + if @document.valid? + @document.attachment.save + @document.set_cached_attachment_from_attachment(URI(request.url)) + else + @document.attachment.destroy + end + end + + private + + def set_attachment_container_resource + @container_resource = params[:resource_type] + end + + def find_attachment_container_resource + @uplo = params[:documentable_type].constantize.find_or_initialize_by(id: params[:documentable_id]) + end + +end \ No newline at end of file diff --git a/app/helpers/documentables_helper.rb b/app/helpers/documentables_helper.rb index 4db3cc450..409399665 100644 --- a/app/helpers/documentables_helper.rb +++ b/app/helpers/documentables_helper.rb @@ -12,8 +12,8 @@ module DocumentablesHelper bytes_to_mega(documentable.class.max_file_size) end - def accepted_content_types(documentable) - documentable.class.accepted_content_types + def accepted_content_types(documentable_class) + documentable_class.accepted_content_types end def accepted_content_types_extensions(documentable_class) @@ -22,15 +22,15 @@ module DocumentablesHelper .join(",") end - def documentable_humanized_accepted_content_types(documentable) - documentable.class.accepted_content_types + def documentable_humanized_accepted_content_types(documentable_class) + documentable_class.accepted_content_types .collect{ |content_type| content_type.split("/").last } .join(", ") end def documentables_note(documentable) t "documents.form.note", max_documents_allowed: max_documents_allowed(documentable), - accepted_content_types: documentable_humanized_accepted_content_types(documentable), + accepted_content_types: documentable_humanized_accepted_content_types(documentable.class), max_file_size: max_file_size(documentable) end diff --git a/app/models/direct_upload.rb b/app/models/direct_upload.rb new file mode 100644 index 000000000..b57618432 --- /dev/null +++ b/app/models/direct_upload.rb @@ -0,0 +1,28 @@ +class DirectUpload + include ActiveModel::Validations + + attr_accessor :resource, :resource_type, :resource_id, :resource_relation, + :attachment, :cached_attachment + + validates_presence_of :attachment, :resource_type, :resource_relation + validate :parent_resource_attachment_validations, + if: -> { attachment.present? && resource_type.present? && resource_relation.present? } + + def parent_resource_attachment_validations + # Proposal or Budget::Investment + resource = resource_type.constantize.find_or_initialize_by(id: resource_id) + + # Document or Image + relation = if resource.class.reflections[resource_relation].macro == :has_one + resource.send("build_#{resource_relation}", attachment: attachment) + else + resource.send(resource_relation).build(attachment: attachment) + end + relation.valid? + + if relation.errors.has_key? :attachment + errors[:attachment] = relation.errors[:attachment] + end + end + +end \ No newline at end of file diff --git a/app/models/document.rb b/app/models/document.rb index a08c6fe80..b66717943 100644 --- a/app/models/document.rb +++ b/app/models/document.rb @@ -50,9 +50,13 @@ class Document < ActiveRecord::Base private + def documentable_class + documentable_type.constantize if documentable_type.present? + end + def validate_attachment_size - if documentable.present? && - attachment_file_size > documentable.class.max_file_size + if documentable_class.present? && + attachment_file_size > documentable_class.max_file_size errors[:attachment] = I18n.t("documents.errors.messages.in_between", min: "0 Bytes", max: "#{max_file_size(documentable)} MB") @@ -60,11 +64,11 @@ class Document < ActiveRecord::Base end def validate_attachment_content_type - if documentable.present? && - !accepted_content_types(documentable).include?(attachment_content_type) + if documentable_class && + !accepted_content_types(documentable_class).include?(attachment_content_type) errors[:attachment] = I18n.t("documents.errors.messages.wrong_content_type", content_type: attachment_content_type, - accepted_content_types: documentable_humanized_accepted_content_types(documentable)) + accepted_content_types: documentable_humanized_accepted_content_types(documentable_class)) end end diff --git a/app/models/image.rb b/app/models/image.rb index 741b61eb3..c331a1886 100644 --- a/app/models/image.rb +++ b/app/models/image.rb @@ -66,6 +66,10 @@ class Image < ActiveRecord::Base attachment.reprocess! end + def imageable_class + imageable_type.constantize if imageable_type.present? + end + def validate_image_dimensions if attachment_of_valid_content_type? dimensions = Paperclip::Geometry.from_file(attachment.queued_for_write[:original].path) @@ -75,7 +79,7 @@ class Image < ActiveRecord::Base end def validate_attachment_size - if imageable.present? && + if imageable_class && attachment_file_size > 1.megabytes errors[:attachment] = I18n.t("images.errors.messages.in_between", min: "0 Bytes", @@ -84,7 +88,7 @@ class Image < ActiveRecord::Base end def validate_attachment_content_type - if imageable.present? && !attachment_of_valid_content_type? + if imageable_class && !attachment_of_valid_content_type? errors[:attachment] = I18n.t("images.errors.messages.wrong_content_type", content_type: attachment_content_type, accepted_content_types: imageable_humanized_accepted_content_types) diff --git a/app/views/documents/new.html.erb b/app/views/documents/new.html.erb index fa1b9a786..be300450e 100644 --- a/app/views/documents/new.html.erb +++ b/app/views/documents/new.html.erb @@ -1,4 +1,4 @@ -
+
<%= back_link_to params[:from] %> @@ -16,7 +16,7 @@
  • <%= t "documents.recommendation_two_html", - accepted_content_types: documentable_humanized_accepted_content_types(@document.documentable) %> + accepted_content_types: documentable_humanized_accepted_content_types(@document.documentable.class) %>
  • <%= t "documents.recommendation_three_html", diff --git a/spec/factories.rb b/spec/factories.rb index 3aa3a0e01..a53389fb9 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -819,4 +819,23 @@ LOREM_IPSUM association :author, factory: :user end + factory :direct_upload do + + trait :proposal do + resource_type "Proposal" + end + trait :budget_investment do + resource_type "Budget::Investment" + end + + trait :documents do + resource_relation "documents" + attachment { File.new("spec/fixtures/files/empty.pdf") } + end + trait :image do + resource_relation "image" + attachment { File.new("spec/fixtures/files/clippy.jpg") } + end + end + end diff --git a/spec/fixtures/files/clippy.jpeg b/spec/fixtures/files/clippy.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..1edb6659b1e4e4010486dd35aa5bdf914acaa654 GIT binary patch literal 12394 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