From 6f71da07eecabf065a057d50c2375c3c31eab3e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sen=C3=A9n=20Rodero=20Rodr=C3=ADguez?= Date: Tue, 12 Sep 2017 18:12:49 +0200 Subject: [PATCH] Duplicate documentable code and rename for imageable --- app/assets/javascripts/application.js | 2 + app/assets/javascripts/imageable.js.coffee | 101 ++++++ app/assets/stylesheets/application.scss | 1 + app/assets/stylesheets/icons.scss | 2 +- app/assets/stylesheets/imageable.scss | 64 ++++ app/assets/stylesheets/layout.scss | 56 +++ app/assets/stylesheets/participation.scss | 10 +- .../budgets/investments_controller.rb | 26 +- .../concerns/commentable_actions.rb | 7 + app/controllers/images_controller.rb | 106 ++++++ app/controllers/proposals_controller.rb | 5 +- app/helpers/documentables_helper.rb | 4 +- app/helpers/documents_helper.rb | 4 +- app/helpers/imageables_helper.rb | 36 ++ app/helpers/images_helper.rb | 98 ++++++ app/helpers/investments_helper.rb | 4 +- app/models/abilities/administrator.rb | 1 + app/models/abilities/common.rb | 3 + app/models/budget/investment.rb | 2 +- app/models/concerns/documentable.rb | 1 + app/models/document.rb | 4 +- app/models/image.rb | 101 ++++-- app/models/proposal.rb | 1 + app/views/budgets/investments/_form.html.erb | 27 +- .../budgets/investments/_image_form.html.erb | 53 --- .../budgets/investments/_investment.html.erb | 7 +- .../investments/_investment_show.html.erb | 30 +- .../budgets/investments/edit_image.html.erb | 19 - .../documents/_nested_documents.html.erb | 3 +- app/views/documents/_plain_fields.html.erb | 2 +- app/views/documents/new.html.erb | 2 +- app/views/images/_form.html.erb | 20 ++ app/views/images/_image.html.erb | 19 + app/views/images/_nested_fields.html.erb | 32 ++ app/views/images/_nested_images.html.erb | 16 + app/views/images/_plain_fields.html.erb | 50 +++ app/views/images/destroy.js.erb | 17 + app/views/images/new.html.erb | 26 ++ app/views/images/new_nested.js.erb | 9 + app/views/images/upload.js.erb | 12 + app/views/proposals/_form.html.erb | 4 + app/views/proposals/show.html.erb | 21 +- config/i18n-tasks.yml | 1 + config/locales/en/activerecord.yml | 6 + config/locales/en/general.yml | 1 + config/locales/en/images.yml | 33 ++ config/locales/es/activerecord.yml | 9 +- config/locales/es/images.yml | 33 ++ config/routes.rb | 12 +- .../20170911110109_add_user_id_to_images.rb | 5 + db/schema.rb | 3 + spec/factories.rb | 14 +- spec/features/budgets/investments_spec.rb | 257 +------------- spec/features/proposals_spec.rb | 2 + spec/models/abilities/administrator_spec.rb | 12 + spec/models/budget/investment_spec.rb | 77 +--- spec/shared/features/documentable.rb | 2 +- spec/shared/features/imageable.rb | 331 ++++++++++++++++++ spec/shared/models/acts_as_imageable.rb | 76 ++++ 59 files changed, 1362 insertions(+), 520 deletions(-) create mode 100644 app/assets/javascripts/imageable.js.coffee create mode 100644 app/assets/stylesheets/imageable.scss create mode 100644 app/controllers/images_controller.rb create mode 100644 app/helpers/imageables_helper.rb create mode 100644 app/helpers/images_helper.rb delete mode 100644 app/views/budgets/investments/_image_form.html.erb delete mode 100644 app/views/budgets/investments/edit_image.html.erb create mode 100644 app/views/images/_form.html.erb create mode 100644 app/views/images/_image.html.erb create mode 100644 app/views/images/_nested_fields.html.erb create mode 100644 app/views/images/_nested_images.html.erb create mode 100644 app/views/images/_plain_fields.html.erb create mode 100644 app/views/images/destroy.js.erb create mode 100644 app/views/images/new.html.erb create mode 100644 app/views/images/new_nested.js.erb create mode 100644 app/views/images/upload.js.erb create mode 100644 config/locales/en/images.yml create mode 100644 config/locales/es/images.yml create mode 100644 db/migrate/20170911110109_add_user_id_to_images.rb create mode 100644 spec/shared/features/imageable.rb create mode 100644 spec/shared/models/acts_as_imageable.rb diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 1f825ffed..67d006483 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -63,6 +63,7 @@ //= require followable //= require flaggable //= require documentable +//= require imageable //= require tree_navigator //= require custom //= require tag_autocomplete @@ -100,6 +101,7 @@ var initialize_modules = function() { App.WatchFormChanges.initialize(); App.TreeNavigator.initialize(); App.Documentable.initialize(); + App.Imageable.initialize(); App.TagAutocomplete.initialize(); }; diff --git a/app/assets/javascripts/imageable.js.coffee b/app/assets/javascripts/imageable.js.coffee new file mode 100644 index 000000000..42f6b315b --- /dev/null +++ b/app/assets/javascripts/imageable.js.coffee @@ -0,0 +1,101 @@ +App.Imageable = + + initialize: -> + @initializeDirectUploads() + @initializeInterface() + + initializeDirectUploads: -> + + $('input.image_ajax_attachment[type=file]').fileupload + + paramName: "image[attachment]" + + formData: null + + add: (e, data) -> + wrapper = $(e.target).closest('.image') + index = $(e.target).data('index') + is_nested_image = $(e.target).data('nested-image') + $(wrapper).find('.progress-bar-placeholder').empty() + data.progressBar = $(wrapper).find('.progress-bar-placeholder').html('
') + $(wrapper).find('.progress-bar-placeholder').css('display','block') + data.formData = { + "image[title]": $(wrapper).find('input.image-title').val() || data.files[0].name + "index": index, + "nested_image": is_nested_image + } + data.submit() + + change: (e, data) -> + wrapper = $(e.target).parent() + $.each(data.files, (index, file)-> + $(wrapper).find('.file-name').text(file.name) + ) + + progress: (e, data) -> + progress = parseInt(data.loaded / data.total * 100, 10) + $(data.progressBar).find('.loading-bar').css 'width', progress + '%' + return + + initializeInterface: -> + input_files = $('input.image_ajax_attachment[type=file]') + + $.each input_files, (index, file) -> + wrapper = $(file).parent() + App.Imageable.watchRemoveImagebutton(wrapper) + + watchRemoveImagebutton: (wrapper) -> + remove_image_button = $(wrapper).find('.remove-image') + $(remove_image_button).on 'click', (e) -> + e.preventDefault() + $(wrapper).remove() + $('#new_image_link').show() + $('.max-images-notice').hide() + + uploadNestedImage: (id, nested_image, result) -> + $('#' + id).replaceWith(nested_image) + @updateLoadingBar(id, result) + @initialize() + + uploadPlainImage: (id, nested_image, result) -> + $('#' + id).replaceWith(nested_image) + @updateLoadingBar(id, result) + @initialize() + + updateLoadingBar: (id, result) -> + if result + $('#' + id).find('.loading-bar').addClass 'complete' + else + $('#' + id).find('.loading-bar').addClass 'errors' + $('#' + id).find('.progress-bar-placeholder').css('display','block') + + new: (nested_fields) -> + $(".images-list").append(nested_fields) + @initialize() + + destroyNestedImage: (id, notice) -> + $('#' + id).remove() + @updateNotice(notice) + + replacePlainImage: (id, notice, plain_image) -> + $('#' + id).replaceWith(plain_image) + @updateNotice(notice) + @initialize() + + updateNotice: (notice) -> + if $('[data-alert]').length > 0 + $('[data-alert]').replaceWith(notice) + else + $("body").append(notice) + + updateNewImageButton: (link) -> + if $('.image').length >= $('.images').data('max-images') + $('#new_image_link').hide() + $('.max-images-notice').removeClass('hide') + $('.max-images-notice').show() + else if $('#new_image_link').length > 0 + $('#new_image_link').replaceWith(link) + $('.max-images-notice').hide() + else + $('.max-images-notice').hide() + $(link).insertBefore('.images hr:last') diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index e4dd4ea1c..956846757 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -19,3 +19,4 @@ @import 'jquery-ui/autocomplete'; @import 'autocomplete_overrides'; @import 'documentable'; +@import 'imageable'; diff --git a/app/assets/stylesheets/icons.scss b/app/assets/stylesheets/icons.scss index 39eadf745..e55832667 100644 --- a/app/assets/stylesheets/icons.scss +++ b/app/assets/stylesheets/icons.scss @@ -197,7 +197,7 @@ content: '\53'; } -.icon-budget-investment-image::before { +.icon-image::before { content: '\68'; } diff --git a/app/assets/stylesheets/imageable.scss b/app/assets/stylesheets/imageable.scss new file mode 100644 index 000000000..c65bf425b --- /dev/null +++ b/app/assets/stylesheets/imageable.scss @@ -0,0 +1,64 @@ +.progress-bar-placeholder { + display: none; +} + +.image-form { + .image .file-name { + margin-top: 0; + } + .progress-bar-placeholder { + margin-bottom: 15px; + } + .image .loading-bar.errors { + margin-top: $line-height * 2; + } +} + +.cached-image{ + max-width: 150px; + max-height: 150px; +} +.image { + + .button { + font-weight: normal; + } + + .progress-bar { + width: 100%; + background-color: $light-gray; + } + + input.image_ajax_attachment[type=file]{ + display: none; + } + + .file-name { + margin-top: 0px; + } + + .loading-bar { + height: 5px; + width: 0; + transition: width 500ms ease-out; + + &.uploading { + background-color: $dark-gray; + } + + &.complete { + background-color: $success-color; + width: 100%; + } + + &.errors { + background-color: $alert-color; + width: 100%; + margin-top: $line-height / 2; + } + } + + .loading-bar.no-transition { + transition: none; + } +} diff --git a/app/assets/stylesheets/layout.scss b/app/assets/stylesheets/layout.scss index 6b4ea4f29..ca36cd511 100644 --- a/app/assets/stylesheets/layout.scss +++ b/app/assets/stylesheets/layout.scss @@ -2282,3 +2282,59 @@ table { } } + +// 19. Images +.image-form form { + + .source-option-link { + input { + padding-bottom: 0; + } + + .error { + margin-bottom: $line-height; + } + + label { + &.error { + margin-bottom: 0; + } + } + } + + .source-option-file { + .file-name { + label { + + @include breakpoint(small medium) { + float: none; + } + + @include breakpoint(large) { + float: left; + } + } + + p { + + @include breakpoint(small medium) { + float: none; + margin-top: 0; + margin-left: 0; + margin-bottom: 0; + } + + @include breakpoint(large) { + float: left; + margin-bottom: 0; + margin-top: $line-height / 2; + margin-left: $line-height; + } + } + } + } + + .attachment-errors { + margin-bottom: $line-height; + } +} \ No newline at end of file diff --git a/app/assets/stylesheets/participation.scss b/app/assets/stylesheets/participation.scss index 7ea5956f9..5551017c3 100644 --- a/app/assets/stylesheets/participation.scss +++ b/app/assets/stylesheets/participation.scss @@ -252,13 +252,13 @@ .document-form, .topic-new, .topic-form, -.budget-investment-image-form { +.image-form { .icon-debates, .icon-proposals, .icon-budget, .icon-documents, - .icon-budget-investment-image { + .icon-image { font-size: rem-calc(50); line-height: $line-height; opacity: 0.5; @@ -277,7 +277,7 @@ color: $budget; } - .icon-budget-investment-image { + .icon-image { color: $budget-investment-image; } } @@ -315,7 +315,7 @@ } } -.budget-investment-image-form { +.image-form { .recommendations li::before { color: $budget-investment-image; @@ -955,7 +955,7 @@ } } -.budget-investment-image-form { +.image-form { @include image-upload; diff --git a/app/controllers/budgets/investments_controller.rb b/app/controllers/budgets/investments_controller.rb index 739c6da71..f6952fbe1 100644 --- a/app/controllers/budgets/investments_controller.rb +++ b/app/controllers/budgets/investments_controller.rb @@ -45,11 +45,13 @@ module Budgets load_investment_votes(@investment) @investment_ids = [@investment.id] @document = Document.new(documentable: @investment) + @image = Image.new(imageable: @investment) end def create @investment.author = current_user recover_documents_from_cache(@investment) + recover_image_from_cache(@investment) if @investment.save Mailer.budget_investment_created(@investment).deliver_later @@ -80,24 +82,6 @@ module Budgets super end - def edit_image - end - - def update_image - if @investment.update(investment_params) - redirect_to budget_investment_path(@investment.budget, @investment), - notice: t("flash.actions.update_image.budget_investment") - else - render :edit_image - end - end - - def remove_image - @investment.image.destroy! - redirect_to budget_investment_path(@investment.budget, @investment), - notice: t("flash.actions.remove_image.budget_investment") - end - private def resource_model @@ -124,9 +108,9 @@ module Budgets def investment_params params.require(:budget_investment) - .permit(:title, :description, :external_url, :heading_id, - :tag_list, :organization_name, :location, :terms_of_service, - image_attributes: [:title, :attachment], + .permit(:title, :description, :external_url, :heading_id, :tag_list, + :organization_name, :location, :terms_of_service, + image_attributes: [:id, :title, :attachment, :cached_attachment, :user_id], documents_attributes: [:id, :title, :attachment, :cached_attachment, :user_id]) end diff --git a/app/controllers/concerns/commentable_actions.rb b/app/controllers/concerns/commentable_actions.rb index 1c47b0c06..e7c115a7a 100644 --- a/app/controllers/concerns/commentable_actions.rb +++ b/app/controllers/concerns/commentable_actions.rb @@ -59,6 +59,7 @@ module CommentableActions def update resource.assign_attributes(strong_params) recover_documents_from_cache(resource) + recover_image_from_cache(resource) if resource.save redirect_to resource, notice: t("flash.actions.update.#{resource_name.underscore}") @@ -119,4 +120,10 @@ module CommentableActions end end + def recover_image_from_cache(resource) + return false unless resource.try(:image) + + resource.image.attachment = resource.image.set_attachment_from_cached_attachment if resource.image.cached_attachment.present? + end + end diff --git a/app/controllers/images_controller.rb b/app/controllers/images_controller.rb new file mode 100644 index 000000000..353ef9550 --- /dev/null +++ b/app/controllers/images_controller.rb @@ -0,0 +1,106 @@ +class ImagesController < ApplicationController + before_action :authenticate_user! + before_filter :find_imageable, except: :destroy + before_filter :prepare_new_image, only: [:new, :new_nested] + before_filter :prepare_image_for_creation, only: :create + before_filter :find_image, only: :destroy + + load_and_authorize_resource except: :upload + skip_authorization_check only: :upload + + def new + end + + def new_nested + end + + def create + recover_attachments_from_cache + + if @image.save + flash[:notice] = t "images.actions.create.notice" + redirect_to params[:from] + else + flash[:alert] = t "images.actions.create.alert" + render :new + end + end + + def destroy + respond_to do |format| + format.html do + if @image.destroy + flash[:notice] = t "images.actions.destroy.notice" + else + flash[:alert] = t "images.actions.destroy.alert" + end + redirect_to params[:from] + end + format.js do + if @image.destroy + flash.now[:notice] = t "images.actions.destroy.notice" + else + flash.now[:alert] = t "images.actions.destroy.alert" + end + end + end + end + + def destroy_upload + @image = Image.new(cached_attachment: params[:path]) + @image.set_attachment_from_cached_attachment + @image.cached_attachment = nil + @image.imageable = @imageable + + if @image.attachment.destroy + flash.now[:notice] = t "images.actions.destroy.notice" + else + flash.now[:alert] = t "images.actions.destroy.alert" + end + render :destroy + end + + def upload + @image = Image.new(image_params.merge(user: current_user)) + @image.imageable = @imageable + + if @image.valid? + @image.attachment.save + @image.set_cached_attachment_from_attachment(URI(request.url)) + else + @image.attachment.destroy + end + end + + private + + def image_params + params.require(:image).permit(:title, :imageable_type, :imageable_id, + :attachment, :cached_attachment, :user_id) + end + + def find_imageable + @imageable = params[:imageable_type].constantize.find_or_initialize_by(id: params[:imageable_id]) + end + + def find_image + @image = Image.find(params[:id]) + end + + def prepare_new_image + @image = Image.new(imageable: @imageable) + end + + def prepare_image_for_creation + @image = Image.new(image_params) + @image.imageable = @imageable + @image.user = current_user + end + + def recover_attachments_from_cache + if @image.attachment.blank? && @image.cached_attachment.present? + @image.set_attachment_from_cached_attachment + end + end + +end diff --git a/app/controllers/proposals_controller.rb b/app/controllers/proposals_controller.rb index 06e170ee2..4bdc17b29 100644 --- a/app/controllers/proposals_controller.rb +++ b/app/controllers/proposals_controller.rb @@ -20,12 +20,14 @@ class ProposalsController < ApplicationController super @notifications = @proposal.notifications @document = Document.new(documentable: @proposal) + @image = Image.new(imageable: @proposal) redirect_to proposal_path(@proposal), status: :moved_permanently if request.path != proposal_path(@proposal) end def create @proposal = Proposal.new(proposal_params.merge(author: current_user)) recover_documents_from_cache(@proposal) + recover_image_from_cache(@proposal) if @proposal.save redirect_to share_proposal_path(@proposal), notice: I18n.t('flash.actions.create.proposal') @@ -78,7 +80,8 @@ class ProposalsController < ApplicationController def proposal_params params.require(:proposal).permit(:title, :question, :summary, :description, :external_url, :video_url, :responsible_name, :tag_list, :terms_of_service, :geozone_id, - documents_attributes: [:id, :title, :attachment, :cached_attachment, :user_id]) + image_attributes: [:id, :title, :attachment, :cached_attachment, :user_id], + documents_attributes: [:id, :title, :attachment, :cached_attachment, :user_id] ) end def retired_params diff --git a/app/helpers/documentables_helper.rb b/app/helpers/documentables_helper.rb index edbf88131..4db3cc450 100644 --- a/app/helpers/documentables_helper.rb +++ b/app/helpers/documentables_helper.rb @@ -22,7 +22,7 @@ module DocumentablesHelper .join(",") end - def humanized_accepted_content_types(documentable) + def documentable_humanized_accepted_content_types(documentable) documentable.class.accepted_content_types .collect{ |content_type| content_type.split("/").last } .join(", ") @@ -30,7 +30,7 @@ module DocumentablesHelper def documentables_note(documentable) t "documents.form.note", max_documents_allowed: max_documents_allowed(documentable), - accepted_content_types: humanized_accepted_content_types(documentable), + accepted_content_types: documentable_humanized_accepted_content_types(documentable), max_file_size: max_file_size(documentable) end diff --git a/app/helpers/documents_helper.rb b/app/helpers/documents_helper.rb index cc39f7857..638ef902f 100644 --- a/app/helpers/documents_helper.rb +++ b/app/helpers/documents_helper.rb @@ -4,7 +4,7 @@ module DocumentsHelper document.attachment_file_name end - def errors_on_attachment(document) + def document_errors_on_attachment(document) document.errors[:attachment].join(', ') if document.errors.key?(:attachment) end @@ -71,7 +71,7 @@ module DocumentsHelper class: "button hollow #{klass}" if document.errors[:attachment].any? html += content_tag :small, class: "error" do - errors_on_attachment(document) + document_errors_on_attachment(document) end end end diff --git a/app/helpers/imageables_helper.rb b/app/helpers/imageables_helper.rb new file mode 100644 index 000000000..108c1177d --- /dev/null +++ b/app/helpers/imageables_helper.rb @@ -0,0 +1,36 @@ +module ImageablesHelper + + def imageable_class(imageable) + imageable.class.name.parameterize('_') + end + + def imageable_max_file_size + bytesToMeg(Image::MAX_IMAGE_SIZE) + end + + def bytesToMeg(bytes) + bytes / Numeric::MEGABYTE + end + + def imageable_accepted_content_types + Image::ACCEPTED_CONTENT_TYPE + end + + def imageable_accepted_content_types_extensions + Image::ACCEPTED_CONTENT_TYPE + .collect{ |content_type| ".#{content_type.split("/").last}" } + .join(",") + end + + def imageable_humanized_accepted_content_types + Image::ACCEPTED_CONTENT_TYPE + .collect{ |content_type| content_type.split("/").last } + .join(", ") + end + + def imageables_note(imageable) + t "images.form.note", accepted_content_types: imageable_humanized_accepted_content_types, + max_file_size: max_file_size(imageable) + end + +end \ No newline at end of file diff --git a/app/helpers/images_helper.rb b/app/helpers/images_helper.rb new file mode 100644 index 000000000..47c7c0c47 --- /dev/null +++ b/app/helpers/images_helper.rb @@ -0,0 +1,98 @@ +module ImagesHelper + + def image_attachment_file_name(image) + image.attachment_file_name + end + + def image_errors_on_attachment(image) + image.errors[:attachment].join(', ') if image.errors.key?(:attachment) + end + + def image_bytesToMeg(bytes) + bytes / Numeric::MEGABYTE + end + + def image_nested_field_name(image, field) + parent = image.imageable_type.parameterize.underscore + "#{parent.parameterize}[image_attributes]#{field}" + end + + def image_nested_field_id(image, field) + parent = image.imageable_type.parameterize.underscore + "#{parent.parameterize}_image_attributes_#{field}" + end + + def image_nested_field_wrapper_id + "nested_image" + end + + def image_class(image) + image.persisted? ? "image" : "cached-image" + end + + def render_destroy_image_link(image) + if image.persisted? + link_to t('images.form.delete_button'), + image_path(image, nested_image: true), + method: :delete, + remote: true, + data: { confirm: t('images.actions.destroy.confirm') }, + class: "delete float-right" + elsif !image.persisted? && image.cached_attachment.present? + link_to t('images.form.delete_button'), + destroy_upload_images_path(path: image.cached_attachment, + nested_image: true, + imageable_type: image.imageable_type, + imageable_id: image.imageable_id), + method: :delete, + remote: true, + class: "delete float-right" + else + link_to t('images.form.delete_button'), + "#", + class: "delete float-right remove-image" + end + end + + def render_image_attachment(image) + html = file_field_tag :attachment, + accept: imageable_accepted_content_types_extensions, + class: 'image_ajax_attachment', + data: { + url: image_direct_upload_url(image), + cached_attachment_input_field: image_nested_field_id(image, :cached_attachment), + multiple: false, + nested_image: true + }, + name: image_nested_field_name(image, :attachment), + id: image_nested_field_id(image, :attachment) + if image.attachment.blank? && image.cached_attachment.blank? + klass = image.errors[:attachment].any? ? "error" : "" + html += label_tag image_nested_field_id(image, :attachment), + t("images.form.attachment_label"), + class: "button hollow #{klass}" + if image.errors[:attachment].any? + html += content_tag :small, class: "error" do + image_errors_on_attachment(image) + end + end + end + html + end + + def render_image(image, version, show_caption = true) + version = image.persisted? ? version : :original + render partial: "images/image", locals: { image: image, + version: version, + show_caption: show_caption } + end + + def image_direct_upload_url(image) + upload_images_url( + imageable_type: image.imageable_type, + imageable_id: image.imageable_id, + format: :js + ) + end + +end diff --git a/app/helpers/investments_helper.rb b/app/helpers/investments_helper.rb index 2adccaa5c..e7a7112ce 100644 --- a/app/helpers/investments_helper.rb +++ b/app/helpers/investments_helper.rb @@ -5,7 +5,7 @@ module InvestmentsHelper end def investment_image_advice_note(investment) - if investment.image.exists? + if investment.image.present? t("budgets.investments.edit_image.edit_note", title: investment.title) else t("budgets.investments.edit_image.add_note", title: investment.title) @@ -13,7 +13,7 @@ module InvestmentsHelper end def investment_image_button_text(investment) - investment.image.exists? ? t("budgets.investments.show.edit_image") : t("budgets.investments.show.add_image") + investment.image.present? ? t("budgets.investments.show.edit_image") : t("budgets.investments.show.add_image") end def errors_on_image(investment) diff --git a/app/models/abilities/administrator.rb b/app/models/abilities/administrator.rb index 40eae0d49..f08a1a2cc 100644 --- a/app/models/abilities/administrator.rb +++ b/app/models/abilities/administrator.rb @@ -74,6 +74,7 @@ module Abilities cannot :comment_as_moderator, [::Legislation::Question, Legislation::Annotation] can [:create, :destroy], Document + can [:create, :destroy], Image end end end diff --git a/app/models/abilities/common.rb b/app/models/abilities/common.rb index 5dd68f045..169772dc2 100644 --- a/app/models/abilities/common.rb +++ b/app/models/abilities/common.rb @@ -40,6 +40,9 @@ module Abilities can [:create, :destroy, :new], Document, documentable: { author_id: user.id } can [:new_nested, :upload, :destroy_upload], Document + can [:create, :destroy, :new], Image, imageable: { author_id: user.id } + can [:new_nested, :upload, :destroy_upload], Image + unless user.organization? can :vote, Debate can :vote, Comment diff --git a/app/models/budget/investment.rb b/app/models/budget/investment.rb index e99bbca9a..fbca66117 100644 --- a/app/models/budget/investment.rb +++ b/app/models/budget/investment.rb @@ -7,8 +7,8 @@ class Budget include Reclassification include Followable include Communitable - include Documentable include Imageable + include Documentable documentable max_documents_allowed: 3, max_file_size: 3.megabytes, accepted_content_types: [ "application/pdf" ] diff --git a/app/models/concerns/documentable.rb b/app/models/concerns/documentable.rb index 729a0b0f8..3b1fc5fb3 100644 --- a/app/models/concerns/documentable.rb +++ b/app/models/concerns/documentable.rb @@ -15,6 +15,7 @@ module Documentable @max_file_size = options[:max_file_size] @accepted_content_types = options[:accepted_content_types] end + end end diff --git a/app/models/document.rb b/app/models/document.rb index 3556d4c0a..a08c6fe80 100644 --- a/app/models/document.rb +++ b/app/models/document.rb @@ -8,7 +8,7 @@ class Document < ActiveRecord::Base belongs_to :documentable, polymorphic: true # Disable paperclip security validation due to polymorphic configuration - # Paperclip do not allow to user Procs on valiations definition + # 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? } @@ -64,7 +64,7 @@ class Document < ActiveRecord::Base !accepted_content_types(documentable).include?(attachment_content_type) errors[:attachment] = I18n.t("documents.errors.messages.wrong_content_type", content_type: attachment_content_type, - accepted_content_types: humanized_accepted_content_types(documentable)) + accepted_content_types: documentable_humanized_accepted_content_types(documentable)) end end diff --git a/app/models/image.rb b/app/models/image.rb index 089ec9856..08c7c1dbd 100644 --- a/app/models/image.rb +++ b/app/models/image.rb @@ -1,35 +1,62 @@ class Image < ActiveRecord::Base + include ImagesHelper + include ImageablesHelper + TITLE_LEGHT_RANGE = 4..80 MIN_SIZE = 475 + MAX_IMAGE_SIZE = 1.megabyte + ACCEPTED_CONTENT_TYPE = %w(image/jpeg image/jpg) - attr_accessor :content_type, :original_filename, :attachment_data, :attachment_urls + has_attached_file :attachment, styles: { large: "x#{MIN_SIZE}", medium: "300x300#", thumb: "140x245#" }, + path: ":rails_root/public/system/:class/:prefix/:style/:filename", + url: "/system/:class/:prefix/:style/:filename" + attr_accessor :cached_attachment + + belongs_to :user belongs_to :imageable, polymorphic: true - before_validation :set_styles - has_attached_file :attachment, styles: { large: "x475", medium: "300x300#", thumb: "140x245#" }, - url: "/system/:class/:attachment/:imageable_name_path/:style/:hash.:extension", - hash_secret: Rails.application.secrets.secret_key_base - validates_attachment :attachment, presence: true, content_type: { content_type: %w(image/jpeg image/jpg) }, - size: { less_than: 1.megabytes } + + # 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, length: { in: TITLE_LEGHT_RANGE } - validate :check_image_dimensions + validates :user_id, presence: true + validates :imageable_id, presence: true, if: -> { persisted? } + validates :imageable_type, presence: true, if: -> { persisted? } + + validate :validate_image_dimensions, if: -> { attachment.present? && attachment.dirty? } after_create :redimension_using_origin_styles - accepts_nested_attributes_for :imageable + after_save :remove_cached_image, if: -> { valid? && persisted? && cached_attachment.present? } - # # overwrite default styles for Image class - # def set_image_styles - # { large: "x#{MIN_SIZE}", medium: "300x300#", thumb: "140x245#" } - # end - def set_styles - if imageable - imageable.set_styles if imageable.respond_to? :set_styles - else - { large: "x#{MIN_SIZE}", medium: "300x300#", thumb: "140x245#" } - end + def set_cached_attachment_from_attachment(prefix) + self.cached_attachment = if Paperclip::Attachment.default_options[:storage] == :filesystem + attachment.path + else + prefix + attachment.url + end end - Paperclip.interpolates :imageable_name_path do |attachment, _style| - attachment.instance.imageable.class.to_s.downcase.split('::').map(&:pluralize).join('/') + 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 + + def prefix(attachment, style) + if !attachment.instance.persisted? + "cached_attachments/user/#{attachment.instance.user_id}" + else + ":attachment/:id_partition" + end end private @@ -38,11 +65,37 @@ class Image < ActiveRecord::Base attachment.reprocess! end - def check_image_dimensions - return unless attachment? - + def validate_image_dimensions dimensions = Paperclip::Geometry.from_file(attachment.queued_for_write[:original].path) errors.add(:attachment, :min_image_width, required_min_width: MIN_SIZE) if dimensions.width < MIN_SIZE errors.add(:attachment, :min_image_height, required_min_height: MIN_SIZE) if dimensions.height < MIN_SIZE end + + def validate_attachment_size + if imageable.present? && + attachment_file_size > 1.megabytes + errors[:attachment] = I18n.t("images.errors.messages.in_between", + min: "0 Bytes", + max: "#{imageable_max_file_size} MB") + end + end + + def validate_attachment_content_type + if imageable.present? && + !imageable_accepted_content_types.include?(attachment_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) + end + end + + def attachment_presence + if attachment.blank? && cached_attachment.blank? + errors[:attachment] = I18n.t("errors.messages.blank") + end + end + + def remove_cached_image + File.delete(cached_attachment) if File.exists?(cached_attachment) + end end diff --git a/app/models/proposal.rb b/app/models/proposal.rb index bce8b9af2..5e9ee68b8 100644 --- a/app/models/proposal.rb +++ b/app/models/proposal.rb @@ -10,6 +10,7 @@ class Proposal < ActiveRecord::Base include Graphqlable include Followable include Communitable + include Imageable include Documentable documentable max_documents_allowed: 3, max_file_size: 3.megabytes, diff --git a/app/views/budgets/investments/_form.html.erb b/app/views/budgets/investments/_form.html.erb index 7d612a79e..bc7bd07f8 100644 --- a/app/views/budgets/investments/_form.html.erb +++ b/app/views/budgets/investments/_form.html.erb @@ -21,6 +21,10 @@ <%= f.text_field :external_url %> +
+ <%= render 'images/nested_images', imageable: @investment %> +
+
<%= render 'documents/nested_documents', documentable: @investment %>
@@ -53,29 +57,6 @@ data: {js_url: suggest_tags_path} %> - <%= f.fields_for :image do |builder| %> - -
-
- <%= f.file_field :attachment, accept: 'image/jpg,image/jpeg', label: false, class:'show-for-sr' %> -
- <%= f.label :attachment, t("budgets.investments.form.image_label"), class:'button' %> -
-
- - <% if @investment.errors.has_key?(:attachment) %> -
-
- <%= errors_on_image(@investment)%> -
-
- <% end %> - -
- <%= f.text_field :title %> -
- <% end %> - <% unless current_user.manager? %>
diff --git a/app/views/budgets/investments/_image_form.html.erb b/app/views/budgets/investments/_image_form.html.erb deleted file mode 100644 index d2cefe79a..000000000 --- a/app/views/budgets/investments/_image_form.html.erb +++ /dev/null @@ -1,53 +0,0 @@ -<%= form_for([@investment.budget, @investment], url: update_image_budget_investment_path(@investment.budget, @investment), multipart: true, method: :put) do |f| %> - <%= render 'shared/errors', resource: @investment %> - -
-
- -

<%= investment_image_advice_note(@investment) %>

-
-
- - <% if @investment.image.exists? %> -
- <%= image_tag @investment.image_url(:large) %> -
- <% end %> - -
-
- <%= f.file_field :attachment, accept: 'image/jpg,image/jpeg', label: false, class:'show-for-sr' %> -
- <%= f.label :attachment, t("budgets.investments.edit_image.form.image_label"), class:'button' %> -
-
- - <% if @investment.errors.has_key?(:image) %> -
-
- <%= errors_on_image(@investment)%> -
-
- <% end %> - -
- <%= f.label :title, t("budgets.investments.edit_image.form.image_title") %> - <%= f.text_field :title, placeholder: t("budgets.investments.edit_image.form.image_title"), label: false %> -
- -
- <%= f.submit(class: "button", value: t("budgets.investments.edit_image.form.submit_button")) %> - - <% if @investment.image.exists? %> -
- <%= link_to t("budgets.investments.edit_image.form.remove_button"), - remove_image_budget_investment_path(@investment.budget, @investment), - class: "button hollow alert", - method: :delete, - data: { confirm: t("budgets.investments.edit_image.form.remove_alert") } %> -
- <% end %> -
- -
-<% end %> diff --git a/app/views/budgets/investments/_investment.html.erb b/app/views/budgets/investments/_investment.html.erb index ffe71f955..e663229ad 100644 --- a/app/views/budgets/investments/_investment.html.erb +++ b/app/views/budgets/investments/_investment.html.erb @@ -3,11 +3,8 @@
- <% if investment.image.exists? %> -
- <%= image_tag investment.image_url(:thumb), alt: investment.image.title %> -
<%= investment.image.title %>
-
+ <% if investment.image.present? %> + <%= image_tag investment.image_url(:thumb), alt: investment.image.title %> <% else %>
<% end %> diff --git a/app/views/budgets/investments/_investment_show.html.erb b/app/views/budgets/investments/_investment_show.html.erb index f200b1263..cf228658b 100644 --- a/app/views/budgets/investments/_investment_show.html.erb +++ b/app/views/budgets/investments/_investment_show.html.erb @@ -10,9 +10,16 @@ class: 'button hollow float-right' %> <% end %> - <% if can?(:edit_image, @investment) %> - <%= link_to investment_image_button_text(@investment), - edit_image_budget_investment_path(investment.budget, investment), + <% if can?(:create, @image) %> + <%= link_to t("images.upload_image"), + new_image_path(imageable_id:investment, imageable_type: investment.class.name, from: request.url), + class: 'button hollow float-right' %> + <% end %> + + <% if @investment.image.present? && can?(:destroy, @investment.image) %> + <%= link_to t("images.remove_image"), + image_path(@investment.image, from: request.url), + method: :delete, class: 'button hollow float-right' %> <% end %> @@ -28,20 +35,7 @@
- <% if investment.image.exists? %> -
-
-
- <%= image_tag investment.image_url(:large), - alt: investment.image.title %> -
- <%= investment.image.title %> -
-
-
-
-
- <% end %> + <%= render_image(investment.image, :large, true) if investment.image.present? %>

<%= t("budgets.investments.show.code_html", code: investment.id) %> @@ -138,7 +132,7 @@ <%= render partial: 'shared/social_share', locals: { share_title: t("budgets.investments.show.share"), title: investment.title, - image_url: investment.image.exists? ? investment_image_full_url(investment, :thumb) : '', + image_url: investment.image.present? ? investment_image_full_url(investment, :thumb) : '', url: budget_investment_url(budget_id: investment.budget_id, id: investment.id) } %> diff --git a/app/views/budgets/investments/edit_image.html.erb b/app/views/budgets/investments/edit_image.html.erb deleted file mode 100644 index a13023e1c..000000000 --- a/app/views/budgets/investments/edit_image.html.erb +++ /dev/null @@ -1,19 +0,0 @@ -

- -
- <%= render "shared/back_link" %> - -

<%= t("budgets.investments.edit_image.title") %>

- - <%= render "image_form" %> -
- -
- -

<%= t("budgets.investments.edit_image.recommendation_title") %>

-
    -
  • <%= t("budgets.investments.edit_image.recommendation_one") %>
  • -
  • <%= t("budgets.investments.edit_image.recommendation_two") %>
  • -
-
-
diff --git a/app/views/documents/_nested_documents.html.erb b/app/views/documents/_nested_documents.html.erb index 5aded04af..77a79e6c0 100644 --- a/app/views/documents/_nested_documents.html.erb +++ b/app/views/documents/_nested_documents.html.erb @@ -3,8 +3,9 @@

<%= documentables_note(documentable) %>

<% documentable.documents.each_with_index do |document, index| %> - <%= render 'documents/nested_fields', document: document, index: index, documentable: documentable %> + <%= render 'documents/nested_fields', document: document, index: index %> <% end %> +
<% unless max_documents_allowed?(documentable) %> diff --git a/app/views/documents/_plain_fields.html.erb b/app/views/documents/_plain_fields.html.erb index c911a4e7f..929d32398 100644 --- a/app/views/documents/_plain_fields.html.erb +++ b/app/views/documents/_plain_fields.html.erb @@ -39,7 +39,7 @@ <% if document.errors.has_key?(:attachment) %>
- <%= errors_on_attachment(document) %> + <%= document_errors_on_attachment(document) %>
<% end %> diff --git a/app/views/documents/new.html.erb b/app/views/documents/new.html.erb index de60973d2..fa1b9a786 100644 --- a/app/views/documents/new.html.erb +++ b/app/views/documents/new.html.erb @@ -16,7 +16,7 @@
  • <%= t "documents.recommendation_two_html", - accepted_content_types: humanized_accepted_content_types(@document.documentable) %> + accepted_content_types: documentable_humanized_accepted_content_types(@document.documentable) %>
  • <%= t "documents.recommendation_three_html", diff --git a/app/views/images/_form.html.erb b/app/views/images/_form.html.erb new file mode 100644 index 000000000..666eaf5eb --- /dev/null +++ b/app/views/images/_form.html.erb @@ -0,0 +1,20 @@ +<%= form_for @image, + url: images_path( + imageable_type: @image.imageable_type, + imageable_id: @image.imageable_id, + from: params[:from] + ), + html: { multipart: true, class: "imageable"}, + data: { direct_upload_url: upload_images_url(imageable_type: @image.imageable_type, imageable_id: @image.imageable_id) } do |f| %> + + <%= render 'shared/errors', resource: @image %> + +
    + + <%= render 'plain_fields', image: @image %> + +
    + <%= f.submit(t("images.form.submit_button"), class: "button expanded") %> +
    +
    +<% end %> diff --git a/app/views/images/_image.html.erb b/app/views/images/_image.html.erb new file mode 100644 index 000000000..7133e298c --- /dev/null +++ b/app/views/images/_image.html.erb @@ -0,0 +1,19 @@ +
    +
    +
    + <%= image_tag image.attachment.url(version), + class: image_class(image), + alt: image.title %> + <% if show_caption %> +
    + <%= image.title %> +
    + <% end %> +
    + + <% if show_caption %> +
    + <% end %> + +
    +
    \ No newline at end of file diff --git a/app/views/images/_nested_fields.html.erb b/app/views/images/_nested_fields.html.erb new file mode 100644 index 000000000..aaa703bda --- /dev/null +++ b/app/views/images/_nested_fields.html.erb @@ -0,0 +1,32 @@ +
    + <%= hidden_field_tag :id, + image.id, + name: image_nested_field_name(image, :id), + id: image_nested_field_id(image, :id) if image.persisted? %> + <%= hidden_field_tag :user_id, + current_user.id, + name: image_nested_field_name(image, :user_id), + id: image_nested_field_id(image, :user_id) %> + <%= hidden_field_tag :cached_attachment, + image.cached_attachment, + name: image_nested_field_name(image, :cached_attachment), + id: image_nested_field_id(image, :cached_attachment) %> + + <%= label_tag :title, t("activerecord.attributes.image.title") %> + <%= text_field_tag :title, + image.title, + name: image_nested_field_name(image, :title), + id: image_nested_field_id(image, :title), + class: "image-title" %> + <% if image.errors[:title].any? %> + <%= image.errors[:title].join(", ") %> + <% end %> + + <%= render_image(image, :thumb, false) if image.attachment.exists? %> + + <%= render_image_attachment(image) %> + + <%= render_destroy_image_link(image) %> +

    <%= image_attachment_file_name(image) %>

    +
    +
    diff --git a/app/views/images/_nested_images.html.erb b/app/views/images/_nested_images.html.erb new file mode 100644 index 000000000..0736f9c36 --- /dev/null +++ b/app/views/images/_nested_images.html.erb @@ -0,0 +1,16 @@ +
    + <%= label_tag :image, t("images.form.title") %> +

    <%= imageables_note(imageable) %>

    + + <%= render 'images/nested_fields', image: imageable.image if imageable.image.present? %> +
    + +<% if imageable.image.blank? %> + <%= link_to t("images.form.add_new_image"), + new_nested_images_path(imageable_type: imageable.class.name, index: 0), + remote: true, + id: "new_image_link", + class: "button hollow" %> +<% end %> + +
    diff --git a/app/views/images/_plain_fields.html.erb b/app/views/images/_plain_fields.html.erb new file mode 100644 index 000000000..fcfb75566 --- /dev/null +++ b/app/views/images/_plain_fields.html.erb @@ -0,0 +1,50 @@ +
    + +
    + <%= label_tag :image_title, t("activerecord.attributes.image.title") %> + <%= text_field_tag :image_title, image.title, name: "image[title]", class: "image-title" %> + <% if image.errors.has_key?(:title) %> + <%= image.errors[:title].join(", ") %> + <% end %> +
    + +
    + <%= hidden_field_tag :cached_attachment, image.cached_attachment, name: "image[cached_attachment]" %> + <%= file_field_tag :attachment, + accept: imageable_accepted_content_types_extensions, + label: false, + class: 'image_ajax_attachment', + data: { + url: upload_images_url(imageable_type: image.imageable_type, imageable_id: image.imageable_id), + cached_attachment_input_field: "image_cached_attachment", + multiple: false, + nested_image: false + }, + id: "image_attachment", + name: "image[attachment]" %> + + <% if image.cached_attachment.blank? %> + <%= label_tag :image_attachment, t("images.form.attachment_label"), class: 'button hollow' %> + <% else %> + <%= link_to t('images.form.delete_button'), + destroy_upload_images_path(path: image.cached_attachment, + nested_image: false, + imageable_type: image.imageable_type, + imageable_id: image.imageable_id), + method: :delete, + remote: true, + class: "delete float-right" %> + <% end %> + + <% if image.errors.has_key?(:attachment) %> +
    +
    + <%= image_errors_on_attachment(image) %> +
    +
    + <% end %> +

    <%= image_attachment_file_name(image) %>

    +
    +
    + +
    diff --git a/app/views/images/destroy.js.erb b/app/views/images/destroy.js.erb new file mode 100644 index 000000000..03ae571aa --- /dev/null +++ b/app/views/images/destroy.js.erb @@ -0,0 +1,17 @@ +<% if params[:nested_image] == "true" %> + + App.Imageable.destroyNestedImage("<%= image_nested_field_wrapper_id %>", "<%= j render('layouts/flash') %>") + <% new_image_link = link_to t("images.form.add_new_image"), + new_nested_images_path(imageable_type: @image.imageable_type), + remote: true, + id: "new_image_link", + class: "button hollow" %> + App.Imageable.updateNewImageButton("<%= j new_image_link %>") + +<% else %> + + App.Imageable.replacePlainImage("plain_image_fields", + "<%= j render('layouts/flash') %>", + "<%= j render('plain_fields', image: @image) %>") + +<% end %> diff --git a/app/views/images/new.html.erb b/app/views/images/new.html.erb new file mode 100644 index 000000000..003c6f6ca --- /dev/null +++ b/app/views/images/new.html.erb @@ -0,0 +1,26 @@ +
    + +
    + <%= back_link_to params[:from] %> +

    <%= t("images.new.title") %>

    + <%= render "form", form_url: images_url %> +
    + +
    + +

    <%= t("images.recommendations_title") %>

    +
      +
    • + <%= t "images.recommendation_one_html" %> +
    • +
    • + <%= t "images.recommendation_two_html", + accepted_content_types: imageable_humanized_accepted_content_types %> +
    • +
    • + <%= t "images.recommendation_three_html", + max_file_size: imageable_max_file_size %> +
    • +
    +
    +
    diff --git a/app/views/images/new_nested.js.erb b/app/views/images/new_nested.js.erb new file mode 100644 index 000000000..5dd3109e5 --- /dev/null +++ b/app/views/images/new_nested.js.erb @@ -0,0 +1,9 @@ +<% + new_image_link = link_to t("images.form.add_new_image"), + new_nested_images_path(imageable_type: params[:imageable_type]), + remote: true, + id: "new_image_link", + class: "button hollow" +%> +App.Imageable.new("<%= j render('images/nested_fields', image: @image) %>") +App.Imageable.updateNewImageButton("<%= j new_image_link %>") diff --git a/app/views/images/upload.js.erb b/app/views/images/upload.js.erb new file mode 100644 index 000000000..9177a35cd --- /dev/null +++ b/app/views/images/upload.js.erb @@ -0,0 +1,12 @@ +<% if params[:nested_image] == "true" %> + + App.Imageable.uploadNestedImage("<%= image_nested_field_wrapper_id %>", + "<%= j render('images/nested_fields', image: @image) %>", + <%= @image.cached_attachment.present? %>) +<% else %> + + App.Imageable.uploadPlainImage("plain_image_fields", + "<%= j render('images/plain_fields', image: @image) %>", + <%= @image.cached_attachment.present? %>) + +<% end %> \ No newline at end of file diff --git a/app/views/proposals/_form.html.erb b/app/views/proposals/_form.html.erb index b46dbc69e..6c796000f 100644 --- a/app/views/proposals/_form.html.erb +++ b/app/views/proposals/_form.html.erb @@ -46,6 +46,10 @@ <%= f.text_field :external_url, placeholder: t("proposals.form.proposal_external_url"), label: false %>
  • +
    + <%= render 'images/nested_images', imageable: @proposal %> +
    +
    <%= render 'documents/nested_documents', documentable: @proposal %>
    diff --git a/app/views/proposals/show.html.erb b/app/views/proposals/show.html.erb index 4b0cfe6f7..e0143fd08 100644 --- a/app/views/proposals/show.html.erb +++ b/app/views/proposals/show.html.erb @@ -48,6 +48,8 @@
    + <%= render_image(@proposal.image, :large, true) if @proposal.image.present? %> +

    <%= t("proposals.show.code") %> @@ -106,7 +108,8 @@