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 @@
-
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 @@
+
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 @@
- <% if can_create_document?(@document, @proposal) || author_of_proposal?(@proposal) || current_editable?(@proposal) %>
+ <% if can_create_document?(@document, @proposal) || author_of_proposal?(@proposal) || current_editable?(@proposal) ||
+ can?(:create, @image) || (@proposal.image.present? && can?(:destroy, @proposal.image)) %>
<%= t("proposals.show.author") %>
<% end %>
diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml
index e766f66d5..156a38eff 100644
--- a/config/i18n-tasks.yml
+++ b/config/i18n-tasks.yml
@@ -40,6 +40,7 @@ data:
- config/locales/%{locale}/legislation.yml
- config/locales/%{locale}/community.yml
- config/locales/%{locale}/documents.yml
+ - config/locales/%{locale}/images.yml
# Locale files to write new keys to, based on a list of key pattern => file rules. Matched from top to bottom:
# `i18n-tasks normalize -p` will force move the keys according to these rules
diff --git a/config/locales/en/activerecord.yml b/config/locales/en/activerecord.yml
index 27d9b10c5..1e3f62b1a 100644
--- a/config/locales/en/activerecord.yml
+++ b/config/locales/en/activerecord.yml
@@ -79,6 +79,9 @@ en:
documents:
one: "Document"
other: "Documents"
+ images:
+ one: "Image"
+ other: "Images"
attributes:
budget:
name: "Name"
@@ -205,6 +208,9 @@ en:
document:
title: Title
attachment: Attachment
+ image:
+ title: Title
+ attachment: Attachment
errors:
models:
user:
diff --git a/config/locales/en/general.yml b/config/locales/en/general.yml
index 85089ca20..6312a9a5c 100644
--- a/config/locales/en/general.yml
+++ b/config/locales/en/general.yml
@@ -181,6 +181,7 @@ en:
signature_sheet: Signature sheet
document: Document
topic: Topic
+ image: Image
geozones:
none: All city
all: All scopes
diff --git a/config/locales/en/images.yml b/config/locales/en/images.yml
new file mode 100644
index 000000000..1c44df61a
--- /dev/null
+++ b/config/locales/en/images.yml
@@ -0,0 +1,33 @@
+en:
+ images:
+ upload_image: Upload image
+ remove_image: Remove image
+ form:
+ title: Descriptive image
+ attachment_label: Choose image
+ submit_button: Upload image
+ delete_button: Remove image
+ note: "You can upload one image of following content types: %{accepted_content_types}, up to %{max_file_size} MB."
+ add_new_image: Add image
+ new:
+ title: Upload image
+ recommendations_title: File upload tips
+ recommendation_one_html: You can upload only one image. Proposals with image attract more attention of the users.
+ recommendation_two_html: You can upload %{accepted_content_types} image.
+ recommendation_three_html: You can upload one image up to %{max_file_size} MB .
+
+ actions:
+ create:
+ notice: Image was created successfully.
+ alert: Cannot create image. Check form errors and try again.
+ destroy:
+ notice: Image was deleted successfully.
+ alert: Cannot destroy image.
+ confirm: Are you sure you want to delete the image? This action cannot be undone!
+ buttons:
+ download_image: Dowload file
+ destroy_image: Destroy
+ errors:
+ messages:
+ in_between: must be in between %{min} and %{max}
+ wrong_content_type: content type %{content_type} does not match any of accepted content types %{accepted_content_types}
diff --git a/config/locales/es/activerecord.yml b/config/locales/es/activerecord.yml
index a5ab18d9e..108f19fcf 100644
--- a/config/locales/es/activerecord.yml
+++ b/config/locales/es/activerecord.yml
@@ -79,6 +79,9 @@ es:
documents:
one: "Documento"
other: "Documentos"
+ images:
+ one: "Imagen"
+ other: "Imágenes"
attributes:
budget:
name: "Nombre"
@@ -99,8 +102,7 @@ es:
title: "Título"
location: "Ubicación"
organization_name: "Si estás proponiendo en nombre de una organización o colectivo, escribe su nombre"
- image: "Imagen descriptiva de la propuesta"
- image_title: "Título de la imagen"
+ image: "Imagen descriptiva de la propuesta de inversión"
comment:
body: "Comentario"
user: "Usuario"
@@ -200,6 +202,9 @@ es:
document:
title: Título
attachment: Archivo adjunto
+ image:
+ title: Título
+ attachment: Archivo adjunto
errors:
models:
user:
diff --git a/config/locales/es/images.yml b/config/locales/es/images.yml
new file mode 100644
index 000000000..362f16ab8
--- /dev/null
+++ b/config/locales/es/images.yml
@@ -0,0 +1,33 @@
+es:
+ images:
+ upload_image: Subir imagen
+ remove_image: Eliminar imagen
+ form:
+ title: Imagen descriptiva
+ attachment_label: Selecciona una imagen
+ submit_button: Subir imagen
+ delete_button: Eliminar imagen
+ note: "Puedes subir una imagen en los formatos: %{accepted_content_types}, y de hasta %{max_file_size} MB por archivo."
+ add_new_image: Añadir imagen
+ new:
+ title: Subir una imagen
+ recommendations_title: Consejos para subir imágenes
+ recommendation_one_html: Puedes subir una imagen descriptiva, esta imagen se mostrará en los listados y ayudará a que tu propuesta llame más la atención de los usuarios.
+ recommendation_two_html: "Sólo puedes subir imágenes en los formatos: %{accepted_content_types} ."
+ recommendation_three_html: Puedes subir una imagen de hasta %{max_file_size} MB
+
+ actions:
+ create:
+ notice: "La imagen se ha creado correctamente."
+ alert: "La imagen no se ha podido crear. Revise los errores del formulario."
+ destroy:
+ notice: "La imagen se ha eliminado correctamente."
+ alert: "La imagen no se ha podido eliminar."
+ confirm: "¿Está seguro de que desea eliminar la imagen? Esta acción no se puede deshacer!"
+ buttons:
+ download_image: Descargar imagen
+ destroy_image: Eliminar
+ errors:
+ messages:
+ in_between: debe estar entre %{min} y %{max}
+ wrong_content_type: El tipo de contenido %{content_type} de la imagen no coincide con ninguno de los tipos de contenido aceptados %{accepted_content_types}
diff --git a/config/routes.rb b/config/routes.rb
index 400c7bb7e..b8289bae4 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -39,7 +39,6 @@ Rails.application.routes.draw do
concern :imageable do
resources :images
end
-
resources :debates do
member do
post :vote
@@ -84,9 +83,6 @@ Rails.application.routes.draw do
resources :investments, controller: "budgets/investments", only: [:index, :new, :create, :show, :destroy], concerns: :imageable do
member do
post :vote
- get :edit_image
- put :update_image
- delete :remove_image
end
collection { get :suggest }
end
@@ -112,6 +108,14 @@ Rails.application.routes.draw do
end
end
+ resources :images, only: [:new, :create, :destroy] do
+ collection do
+ get :new_nested
+ delete :destroy_upload
+ post :upload
+ end
+ end
+
resources :stats, only: [:index]
resources :legacy_legislations, only: [:show], path: 'legislations'
diff --git a/db/migrate/20170911110109_add_user_id_to_images.rb b/db/migrate/20170911110109_add_user_id_to_images.rb
new file mode 100644
index 000000000..31467acee
--- /dev/null
+++ b/db/migrate/20170911110109_add_user_id_to_images.rb
@@ -0,0 +1,5 @@
+class AddUserIdToImages < ActiveRecord::Migration
+ def change
+ add_reference :images, :user, index: true, foreign_key: true
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 063641d00..41063df03 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -398,9 +398,11 @@ ActiveRecord::Schema.define(version: 20170918231410) do
t.string "attachment_content_type"
t.integer "attachment_file_size"
t.datetime "attachment_updated_at"
+ t.integer "user_id"
end
add_index "images", ["imageable_type", "imageable_id"], name: "index_images_on_imageable_type_and_imageable_id", using: :btree
+ add_index "images", ["user_id"], name: "index_images_on_user_id", using: :btree
create_table "legacy_legislations", force: :cascade do |t|
t.string "title"
@@ -1093,6 +1095,7 @@ ActiveRecord::Schema.define(version: 20170918231410) do
add_foreign_key "geozones_polls", "geozones"
add_foreign_key "geozones_polls", "polls"
add_foreign_key "identities", "users"
+ add_foreign_key "images", "users"
add_foreign_key "legislation_draft_versions", "legislation_processes"
add_foreign_key "locks", "users"
add_foreign_key "managers", "users"
diff --git a/spec/factories.rb b/spec/factories.rb
index fc4e41310..3aa3a0e01 100644
--- a/spec/factories.rb
+++ b/spec/factories.rb
@@ -319,14 +319,20 @@ FactoryGirl.define do
valuation_finished true
end
- trait :with_descriptive_image do
- association :image, factory: :image
- end
end
- factory :image, class: 'Image' do
+ factory :image do
attachment { File.new("spec/fixtures/files/clippy.jpg") }
title "Lorem ipsum dolor sit amet"
+ association :user, factory: :user
+
+ trait :proposal_image do
+ association :imageable, factory: :proposal
+ end
+
+ trait :budget_investment_image do
+ association :imageable, factory: :budget_investment
+ end
end
factory :budget_ballot, class: 'Budget::Ballot' do
diff --git a/spec/features/budgets/investments_spec.rb b/spec/features/budgets/investments_spec.rb
index 3633f4b7f..65f3351c1 100644
--- a/spec/features/budgets/investments_spec.rb
+++ b/spec/features/budgets/investments_spec.rb
@@ -29,17 +29,17 @@ feature 'Budget Investments' do
end
scenario 'Index should show investment descriptive image only when is defined' do
- investment = FactoryGirl.create(:budget_investment, heading: heading)
- investment_with_image = FactoryGirl.create(:budget_investment, :with_descriptive_image, heading: heading)
+ investment = create(:budget_investment, heading: heading)
+ investment_with_image = create(:budget_investment, heading: heading)
+ image = create(:image, imageable: investment_with_image)
visit budget_investments_path(budget, heading_id: heading.id)
within("#budget_investment_#{investment.id}") do
- expect(page).not_to have_css("picture")
+ expect(page).not_to have_css("img")
end
-
within("#budget_investment_#{investment_with_image.id}") do
- expect(page).to have_css("picture img[alt='#{investment_with_image.image.title}']")
+ expect(page).to have_css("img[alt='#{investment_with_image.image.title}']")
end
end
@@ -370,16 +370,6 @@ feature 'Budget Investments' do
expect(page).not_to have_selector ".js-follow"
end
- scenario "Show descriptive image when exists" do
- user = create(:user)
- login_as(user)
- investment_with_image = create(:budget_investment, :with_descriptive_image, heading: heading)
-
- visit budget_investment_path(budget_id: budget.id, id: investment_with_image.id)
-
- expect(page).to have_css("img[alt='#{investment_with_image.image.title}']")
- end
-
scenario "Show back link contains heading id" do
investment = create(:budget_investment, heading: heading)
visit budget_investment_path(budget, investment)
@@ -387,241 +377,6 @@ feature 'Budget Investments' do
expect(page).to have_link "Go back", href: budget_investments_path(budget, heading_id: investment.heading)
end
- context "Show investment image button" do
-
- scenario "Should show add text when investment has not image" do
- investment = create(:budget_investment, heading: heading, author: author)
- login_as(author)
-
- visit budget_investment_path(budget, investment)
-
- expect(page).to have_link "Add image"
- end
-
- scenario "Should show edit text when investment has already an image" do
- investment = create(:budget_investment, :with_descriptive_image, heading: heading, author: author)
- login_as(author)
-
- visit budget_investment_path(budget, investment)
-
- expect(page).to have_link "Edit image"
- end
-
- scenario "Should not be shown for anonymous users" do
- investment = create(:budget_investment, heading: heading)
-
- visit budget_investment_path(budget, investment)
-
- expect(page).not_to have_link "Edit image", href: edit_image_budget_investment_path(budget, investment)
- end
-
- scenario "Should not be shown when current user is not investment author" do
- investment = create(:budget_investment, heading: heading)
-
- visit budget_investment_path(budget, investment)
-
- expect(page).not_to have_link "Edit image", href: edit_image_budget_investment_path(budget, investment)
- end
-
- scenario "Should be shown when current user is investment author" do
- investment = create(:budget_investment, heading: heading, author: author)
- login_as(author)
-
- visit budget_investment_path(budget, investment)
-
- expect(page).to have_link "Add image", href: edit_image_budget_investment_path(budget, investment)
- end
-
- scenario "Should be shown when current user is administrator" do
- administrator = create(:administrator).user
- investment = create(:budget_investment, heading: heading, author: author)
- login_as(administrator)
-
- visit budget_investment_path(budget, investment)
-
- expect(page).to have_link "Add image", href: edit_image_budget_investment_path(budget, investment)
- end
- end
-
- scenario "Create new investment project should show image title validation errors when image selected and title blank" do
- investment = create(:budget_investment, heading: heading, author: author)
- login_as(author)
-
- visit new_budget_investment_path(budget)
- select 'Health: More hospitals', from: 'budget_investment_heading_id'
- fill_in 'budget_investment_title', with: 'Build a skyscraper'
- fill_in 'budget_investment_description', with: 'I want to live in a high tower over the clouds'
- attach_file :budget_investment_image, "spec/fixtures/files/clippy.jpg"
- check 'budget_investment_terms_of_service'
- click_button "Create Investment"
-
- expect(page).to have_content "can't be blank, is too short (minimum is 4 characters)"
- end
-
- scenario "Create new investment project should show image validation error when image selected is too small" do
- investment = create(:budget_investment, heading: heading, author: author)
- login_as(author)
-
- visit new_budget_investment_path(budget)
- select 'Health: More hospitals', from: 'budget_investment_heading_id'
- fill_in 'budget_investment_title', with: 'Build a skyscraper'
- fill_in 'budget_investment_description', with: 'I want to live in a high tower over the clouds'
- attach_file :budget_investment_image, "spec/fixtures/files/logo_header.jpg"
- check 'budget_investment_terms_of_service'
- click_button "Create Investment"
-
- expect(page).to have_content "Image dimensions are too small. For a good quality please upload a larger image. Minimum width: 475px, minimum height: 475px."
- end
-
- scenario "Edit image page should show image title validation errors when image selected and title blank" do
- investment = create(:budget_investment, heading: heading, author: author)
- login_as(author)
-
- visit edit_image_budget_investment_path(budget, investment)
- attach_file :budget_investment_image, "spec/fixtures/files/clippy.jpg"
- click_button "Save image"
-
- expect(page).to have_content "can't be blank, is too short (minimum is 4 characters)"
- end
-
- scenario "Edit image page should show image validation error when image selected is too small" do
- investment = create(:budget_investment, heading: heading, author: author)
- login_as(author)
-
- visit edit_image_budget_investment_path(budget, investment)
- attach_file :budget_investment_image, "spec/fixtures/files/logo_header.jpg"
- click_button "Save image"
-
- expect(page).to have_content "Image dimensions are too small. For a good quality please upload a larger image. Minimum width: 475px, minimum height: 475px."
- end
-
- scenario "Edit image page should not be accesible when there is no logged user" do
- investment = create(:budget_investment, heading: heading, author: author)
-
- visit edit_image_budget_investment_path(budget, investment)
-
- expect(page).to have_content "You must sign in or register to continue"
- end
-
- scenario "Edit image page should redirect to investment show page if logged user is not the author" do
- other_author = create(:user, :level_two, username: 'Manuel')
- investment = create(:budget_investment, heading: heading, author: author)
- login_as(other_author)
-
- visit edit_image_budget_investment_path(budget, investment)
-
- expect(page).to have_content "You do not have permission to carry out the action 'edit_image' on budget/investment."
- end
-
- scenario "Edit image page should be accesible when author is currently logged" do
- investment = create(:budget_investment, heading: heading, author: author)
- login_as(author)
-
- visit edit_image_budget_investment_path(budget, investment)
-
- expect(page).to have_content "Change your project image"
- end
-
- scenario "Edit image page should be accesible when there is logged any administrator" do
- administrator = create(:administrator).user
- investment = create(:budget_investment, heading: heading, author: author)
- login_as(administrator)
-
- visit edit_image_budget_investment_path(budget, investment)
-
- expect(page).to have_content "Change your project image"
- end
-
- scenario "Remove image button should not be present when investment image does not exists" do
- investment = create(:budget_investment, heading: heading, author: author)
- login_as(author)
-
- visit edit_image_budget_investment_path(budget, investment)
-
- expect(page).not_to have_link "Remove image"
- end
-
- scenario "Remove image button should be present when investment has an image defined" do
- investment = create(:budget_investment, :with_descriptive_image, heading: heading, author: author)
- login_as(author)
-
- visit edit_image_budget_investment_path(budget, investment)
-
- expect(page).to have_link "Remove image"
- end
-
- scenario "Remove image button should be possible for administrators" do
- administrator = create(:administrator).user
- investment = create(:budget_investment, :with_descriptive_image, heading: heading, author: author)
- login_as(administrator)
-
- visit edit_image_budget_investment_path(budget, investment)
- click_link "Remove image"
-
- expect(page).to have_content "Investment project image removed succesfully."
- end
-
- scenario "Remove image should be possible for investment author" do
- investment = create(:budget_investment, :with_descriptive_image, heading: heading, author: author)
- login_as(author)
-
- visit edit_image_budget_investment_path(budget, investment)
- click_link "Remove image"
-
- expect(page).to have_content "Investment project image removed succesfully."
- end
-
- scenario "Remove image should not be possible for any other logged users (except administrators and author)" do
- investment = create(:budget_investment, :with_descriptive_image, heading: heading, author: author)
- login_as(create(:user))
-
- visit edit_image_budget_investment_path(budget, investment)
-
- expect(page).to have_content "You do not have permission to carry out the action 'edit_image' on budget/investment."
- end
-
- scenario "Update image should not be possible if logged user is not the author" do
- other_author = create(:user, :level_two, username: 'Manuel')
- investment = create(:budget_investment, heading: heading, author: author)
- login_as(other_author)
-
- visit edit_image_budget_investment_path(investment.budget, investment)
-
- expect(current_path).not_to eq(edit_image_budget_investment_path(investment.budget, investment))
- expect(page).to have_content 'You do not have permission'
- end
-
- scenario "Update image should be possible for authors" do
- investment = create(:budget_investment, heading: heading, author: author)
- login_as(author)
-
- visit edit_image_budget_investment_path(investment.budget, investment)
- fill_in :budget_investment_image_title, with: "New image title"
- attach_file :budget_investment_image, "spec/fixtures/files/clippy.jpg"
- click_on "Save image"
-
- within ".budget-investment-show" do
- expect(page).to have_css("img[src*='clippy.jpg']")
- end
- expect(page).to have_content 'Investment project image updated succesfully. '
- end
-
- scenario "Update image should be possible for administrators" do
- administrator = create(:administrator).user
- investment = create(:budget_investment, heading: heading, author: author)
- login_as(administrator)
-
- visit edit_image_budget_investment_path(investment.budget, investment)
- fill_in :budget_investment_image_title, with: "New image title"
- attach_file :budget_investment_image, "spec/fixtures/files/clippy.jpg"
- click_on "Save image"
-
- within ".budget-investment-show" do
- expect(page).to have_css("img[src*='clippy.jpg']")
- end
- expect(page).to have_content 'Investment project image updated succesfully. '
- end
-
context "Show (feasible budget investment)" do
let(:investment) do
create(:budget_investment,
@@ -707,6 +462,8 @@ feature 'Budget Investments' do
end
end
+ it_behaves_like "imageable", "budget_investment", "budget_investment_path", { "budget_id": "budget_id", "id": "id" }
+
it_behaves_like "followable", "budget_investment", "budget_investment_path", { "budget_id": "budget_id", "id": "id" }
it_behaves_like "documentable", "budget_investment", "budget_investment_path", {"budget_id": "budget_id", "id": "id"}
diff --git a/spec/features/proposals_spec.rb b/spec/features/proposals_spec.rb
index 1fa57890e..49eebb870 100644
--- a/spec/features/proposals_spec.rb
+++ b/spec/features/proposals_spec.rb
@@ -1272,6 +1272,8 @@ feature 'Proposals' do
expect(Flag.flagged?(user, proposal)).to_not be
end
+ it_behaves_like "imageable", "proposal", "proposal_path", { "id": "id" }
+
it_behaves_like "followable", "proposal", "proposal_path", { "id": "id" }
it_behaves_like "documentable", "proposal", "proposal_path", { "id": "id" }
diff --git a/spec/models/abilities/administrator_spec.rb b/spec/models/abilities/administrator_spec.rb
index dcb099ecd..be9eb060d 100644
--- a/spec/models/abilities/administrator_spec.rb
+++ b/spec/models/abilities/administrator_spec.rb
@@ -20,6 +20,9 @@ describe "Abilities::Administrator" do
let(:budget_investment_document) { build(:document, documentable: budget_investment) }
let(:poll_question_document) { build(:document, documentable: poll_question) }
+ let(:proposal_image) { build(:image, imageable: proposal) }
+ let(:budget_investment_image) { build(:image, imageable: budget_investment) }
+
let(:hidden_debate) { create(:debate, :hidden) }
let(:hidden_comment) { create(:comment, :hidden) }
let(:hidden_proposal) { create(:proposal, :hidden) }
@@ -82,6 +85,10 @@ describe "Abilities::Administrator" do
it { should be_able_to(:create, proposal_document) }
it { should be_able_to(:destroy, proposal_document) }
+ it { should be_able_to(:new, proposal_image) }
+ it { should be_able_to(:create, proposal_image) }
+ it { should be_able_to(:destroy, proposal_image) }
+
it { should be_able_to(:new, budget_investment_document) }
it { should be_able_to(:create, budget_investment_document) }
it { should be_able_to(:destroy, budget_investment_document) }
@@ -89,4 +96,9 @@ describe "Abilities::Administrator" do
it { should be_able_to(:new, poll_question_document) }
it { should be_able_to(:create, poll_question_document) }
it { should be_able_to(:destroy, poll_question_document) }
+
+ it { should be_able_to(:new, budget_investment_image) }
+ it { should be_able_to(:create, budget_investment_image) }
+ it { should be_able_to(:destroy, budget_investment_image) }
+
end
diff --git a/spec/models/budget/investment_spec.rb b/spec/models/budget/investment_spec.rb
index 8e78bb88d..f11c4916b 100644
--- a/spec/models/budget/investment_spec.rb
+++ b/spec/models/budget/investment_spec.rb
@@ -29,82 +29,7 @@ describe Budget::Investment do
end
end
- describe "#image and #image_title", :investment_image do
- let(:investment_with_image) { build(:budget_investment, :with_descriptive_image) }
-
- it "should be valid" do
- expect(investment_with_image).to be_valid
- end
-
- describe "file extension" do
- it "should not be valid with '.png' extension" do
- investment_with_image.image = File.new("spec/fixtures/files/clippy.png")
-
- expect(investment_with_image).to_not be_valid
- expect(investment_with_image.errors[:image].size).to eq(1)
- end
-
- it "should not be valid with '.gif' extension" do
- investment_with_image.image = File.new("spec/fixtures/files/clippy.gif")
-
- expect(investment_with_image).to_not be_valid
- expect(investment_with_image.errors[:image].size).to eq(1)
- end
-
- it "should be valid with '.jpg' extension" do
- investment_with_image.image = File.new("spec/fixtures/files/clippy.jpg")
-
- expect(investment_with_image).to be_valid
- end
- end
-
- describe "image dimmessions" do
- it "should be valid when image dimmessions are 475X475 at least" do
- expect(investment_with_image).to be_valid
- end
-
- it "should not be valid when image dimmensions are smaller than 475X475" do
- investment_with_image.image = File.new("spec/fixtures/files/logo_header.jpg")
-
- expect(investment_with_image).not_to be_valid
- end
- end
-
- describe "title" do
-
- it "should be valid when image and image_title are not defined" do
- investment_with_image.image = nil
- investment_with_image.image_title = nil
-
- expect(investment_with_image).to be_valid
- end
-
- it "should not be valid when correct image attached but no image title provided" do
- investment_with_image.image_title = ''
-
- expect(investment_with_image).to_not be_valid
- end
-
- it "should not be valid when image title is too short" do
- investment_with_image.image_title = 'a'*3
-
- expect(investment_with_image).to_not be_valid
- end
-
- it "should not be valid when image title is too long" do
- investment_with_image.image_title = 'a'*81
-
- expect(investment_with_image).to_not be_valid
- end
-
- end
-
- it "image destroy should remove image from file storage" do
- image_url = investment_with_image.image.url
-
- expect{ investment_with_image.image.destroy }.to change{ investment_with_image.image.url }.from(image_url).to("/images/original/missing.png")
- end
- end
+ it_behaves_like "acts as imageable", "budget_investment_image"
it "sanitizes description" do
investment.description = ""
diff --git a/spec/shared/features/documentable.rb b/spec/shared/features/documentable.rb
index 9c31da7b0..78ad25b46 100644
--- a/spec/shared/features/documentable.rb
+++ b/spec/shared/features/documentable.rb
@@ -304,7 +304,7 @@ shared_examples "documentable" do |documentable_factory_name, documentable_path,
from: send(documentable_path, arguments))
expect(page).to have_content "You can upload up to a maximum of #{max_file_size(documentable)} documents."
- expect(page).to have_content "You can upload #{humanized_accepted_content_types(documentable)} files."
+ expect(page).to have_content "You can upload #{documentable_humanized_accepted_content_types(documentable)} files."
expect(page).to have_content "You can upload files up to #{max_file_size(documentable)} MB."
end
diff --git a/spec/shared/features/imageable.rb b/spec/shared/features/imageable.rb
new file mode 100644
index 000000000..1327f8dd6
--- /dev/null
+++ b/spec/shared/features/imageable.rb
@@ -0,0 +1,331 @@
+shared_examples "imageable" do |imageable_factory_name, imageable_path, imageable_path_arguments|
+ include ActionView::Helpers
+ include ImagesHelper
+ include ImageablesHelper
+
+ let!(:administrator) { create(:user) }
+ let!(:user) { create(:user) }
+ let!(:arguments) { {} }
+ let!(:imageable) { create(imageable_factory_name, author: user) }
+ let!(:imageable_dom_name) { imageable_factory_name.parameterize }
+
+ before do
+ create(:administrator, user: administrator)
+
+ imageable_path_arguments.each do |argument_name, path_to_value|
+ arguments.merge!("#{argument_name}": imageable.send(path_to_value))
+ end
+ end
+
+ context "Show" do
+
+ scenario "Show descriptive image when exists", :js do
+ image = create(:image, imageable: imageable)
+
+ visit send(imageable_path, arguments)
+
+ expect(page).to have_css("img[alt='#{image.title}']")
+ end
+
+ scenario "Show image title when image exists" do
+ image = create(:image, imageable: imageable)
+
+ visit send(imageable_path, arguments)
+
+ expect(page).to have_content image.title
+ end
+
+ scenario "Should not display upload image button when there is no logged user" do
+ visit send(imageable_path, arguments)
+
+ within "##{dom_id(imageable)}" do
+ expect(page).not_to have_link("Upload image")
+ end
+ end
+
+ scenario "Should not display upload image button when maximum number of images reached " do
+ create_list(:image, 3, imageable: imageable)
+ visit send(imageable_path, arguments)
+
+ within "##{dom_id(imageable)}" do
+ expect(page).not_to have_link("Upload image")
+ end
+ end
+
+ scenario "Should display upload image button when user is logged in and is imageable owner" do
+ login_as(user)
+
+ visit send(imageable_path, arguments)
+
+ within "##{dom_id(imageable)}" do
+ expect(page).to have_link("Upload image")
+ end
+ end
+
+ scenario "Should display upload image button when admin is logged in" do
+ login_as(administrator)
+
+ visit send(imageable_path, arguments)
+
+ within "##{dom_id(imageable)}" do
+ expect(page).to have_link("Upload image")
+ end
+ end
+
+ scenario "Should navigate to new image page when click un upload button" do
+ login_as(user)
+
+ visit send(imageable_path, arguments)
+ click_link "Upload image"
+
+ expect(page).to have_selector("h1", text: "Upload image")
+ end
+
+ end
+
+ context "New" do
+
+ scenario "Should not be able for unathenticated users" do
+ visit new_image_path(imageable_type: imageable.class.name,
+ imageable_id: imageable.id)
+
+ expect(page).to have_content("You must sign in or register to continue.")
+ end
+
+ scenario "Should not be able for other users" do
+ login_as create(:user)
+
+ visit new_image_path(imageable_type: imageable.class.name,
+ imageable_id: imageable.id)
+
+ expect(page).to have_content("You do not have permission to carry out the action 'new' on image. ")
+ end
+
+ scenario "Should be able to imageable author" do
+ login_as imageable.author
+
+ visit new_image_path(imageable_type: imageable.class.name,
+ imageable_id: imageable.id)
+
+ expect(page).to have_selector("h1", text: "Upload image")
+ end
+
+ scenario "Should display file name after file selection", :js do
+ login_as imageable.author
+ visit new_image_path(imageable_type: imageable.class.name,
+ imageable_id: imageable.id)
+
+ attach_file :image_attachment, "spec/fixtures/files/empty.pdf", make_visible: true
+ sleep 1
+
+ expect(page).to have_content "empty.pdf"
+ end
+
+ scenario "Should not display file name after file selection", :js do
+ login_as imageable.author
+ visit new_image_path(imageable_type: imageable.class.name,
+ imageable_id: imageable.id)
+
+ attach_file :image_attachment, "spec/fixtures/files/logo_header.png", make_visible: true
+ sleep 1
+
+ expect(page).not_to have_content "logo_header.jpg"
+ end
+
+ scenario "Should update loading bar style after valid file upload", :js do
+ login_as imageable.author
+ visit new_image_path(imageable_type: imageable.class.name,
+ imageable_id: imageable.id)
+
+ attach_file :image_attachment, "spec/fixtures/files/clippy.jpg", make_visible: true
+ sleep 1
+
+ expect(page).to have_selector ".loading-bar.complete"
+ end
+
+ scenario "Should update loading bar style after unvalid file upload", :js do
+ login_as imageable.author
+ visit new_image_path(imageable_type: imageable.class.name,
+ imageable_id: imageable.id)
+
+ attach_file :image_attachment, "spec/fixtures/files/logo_header.png", make_visible: true
+ sleep 1
+
+ expect(page).to have_selector ".loading-bar.errors"
+ end
+
+ scenario "Should update image title with attachment original file name after file selection if no title defined by user", :js do
+ login_as imageable.author
+ visit new_image_path(imageable_type: imageable.class.name,
+ imageable_id: imageable.id)
+
+ attach_file :image_attachment, "spec/fixtures/files/clippy.jpg", make_visible: true
+ sleep 1
+
+ expect(find("input[name='image[title]']").value).to eq("clippy.jpg")
+ end
+
+ scenario "Should not update image title with attachment original file name after file selection when title already defined by user", :js do
+ login_as imageable.author
+ visit new_image_path(imageable_type: imageable.class.name,
+ imageable_id: imageable.id)
+
+ fill_in :image_title, with: "My custom title"
+ attach_file :image_attachment, "spec/fixtures/files/clippy.jpg", make_visible: true
+ sleep 1
+
+ expect(find("input[name='image[title]']").value).to eq("My custom title")
+ end
+
+ scenario "Should update image cached_attachment field after valid file upload", :js do
+ login_as imageable.author
+ visit new_image_path(imageable_type: imageable.class.name,
+ imageable_id: imageable.id)
+
+ attach_file :image_attachment, "spec/fixtures/files/clippy.jpg", make_visible: true
+ sleep 1
+
+ expect(find("input[name='image[cached_attachment]']", visible: false).value).to include("clippy.jpg")
+ end
+
+ scenario "Should not update image cached_attachment field after unvalid file upload", :js do
+ login_as imageable.author
+ visit new_image_path(imageable_type: imageable.class.name,
+ imageable_id: imageable.id)
+
+ attach_file :image_attachment, "spec/fixtures/files/logo_header.png", make_visible: true
+ sleep 1
+
+ expect(find("input[name='image[cached_attachment]']", visible: false).value).to eq ""
+ end
+
+ scenario "Should show imageable custom recomentations" do
+ login_as imageable.author
+ visit new_image_path(imageable_type: imageable.class.name,
+ imageable_id: imageable.id,
+ from: send(imageable_path, arguments))
+
+ expect(page).to have_content "You can upload only one image. Proposals with image attract more attention of the users."
+ expect(page).to have_content "You can upload #{imageable_humanized_accepted_content_types} image."
+ expect(page).to have_content "You can upload one image up to #{imageable_max_file_size} MB."
+ end
+
+ end
+
+ context "Create" do
+
+ scenario "Should show validation errors" do
+ login_as imageable.author
+ visit new_image_path(imageable_type: imageable.class.name,
+ imageable_id: imageable.id)
+
+ click_on "Upload image"
+
+ expect(page).to have_content "3 errors prevented this Image from being saved: "
+ expect(page).to have_selector "small.error", text: "can't be blank", count: 2
+ end
+
+ scenario "Should show error notice after unsuccessfull image upload" do
+ login_as imageable.author
+
+ visit new_image_path(imageable_type: imageable.class.name,
+ imageable_id: imageable.id,
+ from: send(imageable_path, arguments))
+ attach_file :image_attachment, "spec/fixtures/files/empty.pdf"
+ sleep 1
+ click_on "Upload image"
+
+ expect(page).to have_content "Cannot create image. Check form errors and try again."
+ end
+
+ scenario "Should show success notice after successfull image upload" do
+ login_as imageable.author
+
+ visit new_image_path(imageable_type: imageable.class.name,
+ imageable_id: imageable.id,
+ from: send(imageable_path, arguments))
+ fill_in :image_title, with: "Image title"
+ attach_file :image_attachment, "spec/fixtures/files/clippy.jpg"
+ sleep 1
+ click_on "Upload image"
+
+ expect(page).to have_content "Image was created successfully."
+ end
+
+ scenario "Should redirect to imageable path after successfull image upload" do
+ login_as imageable.author
+
+ visit new_image_path(imageable_type: imageable.class.name,
+ imageable_id: imageable.id,
+ from: send(imageable_path, arguments))
+ fill_in :image_title, with: "Image title"
+ attach_file :image_attachment, "spec/fixtures/files/clippy.jpg"
+ sleep 1
+ click_on "Upload image"
+
+ within "##{dom_id(imageable)}" do
+ expect(page).to have_selector "h1", text: imageable.title
+ end
+ end
+
+ scenario "Should show new image on imageable images tab after successfull image upload" do
+ login_as imageable.author
+
+ visit new_image_path(imageable_type: imageable.class.name,
+ imageable_id: imageable.id,
+ from: send(imageable_path, arguments))
+ fill_in :image_title, with: "Image title"
+ attach_file :image_attachment, "spec/fixtures/files/clippy.jpg"
+ sleep 1
+ click_on "Upload image"
+
+ expect(page).to have_content "Image title"
+ end
+
+ end
+
+ context "Destroy" do
+
+ let!(:image) { create(:image, imageable: imageable, user: imageable.author) }
+
+ scenario "Should show success notice after successfull deletion by an admin" do
+ login_as administrator
+
+ visit send(imageable_path, arguments)
+ click_on "Remove image"
+
+ expect(page).to have_content "Image was deleted successfully."
+ end
+
+ scenario "Should show success notice after successfull deletion" do
+ login_as imageable.author
+
+ visit send(imageable_path, arguments)
+ click_on "Remove image"
+
+ expect(page).to have_content "Image was deleted successfully."
+ end
+
+ scenario "Should not show image after successful deletion" do
+ login_as imageable.author
+
+ visit send(imageable_path, arguments)
+ click_on "Remove image"
+
+ expect(page).not_to have_selector "figure img"
+ end
+
+ scenario "Should redirect to imageable path after successful deletion" do
+ login_as imageable.author
+
+ visit send(imageable_path, arguments)
+ click_on "Remove image"
+
+ within "##{dom_id(imageable)}" do
+ expect(page).to have_selector "h1", text: imageable.title
+ end
+ end
+
+ end
+
+end
diff --git a/spec/shared/models/acts_as_imageable.rb b/spec/shared/models/acts_as_imageable.rb
new file mode 100644
index 000000000..e4f5f221d
--- /dev/null
+++ b/spec/shared/models/acts_as_imageable.rb
@@ -0,0 +1,76 @@
+shared_examples "acts as imageable" do |imageable_factory|
+
+ let!(:image) { build(:image, imageable_factory.to_sym) }
+ let!(:imageable) { image.imageable }
+
+ it "should be valid" do
+ expect(image).to be_valid
+ end
+
+ describe "file extension" do
+
+ it "should not be valid with '.png' extension" do
+ image.attachment = File.new("spec/fixtures/files/clippy.png")
+
+ expect(image).to_not be_valid
+ expect(image.errors[:attachment].size).to eq(1)
+ end
+
+ it "should not be valid with '.gif' extension" do
+ image.attachment = File.new("spec/fixtures/files/clippy.gif")
+
+ expect(image).to_not be_valid
+ expect(image.errors[:attachment].size).to eq(1)
+ end
+
+ it "should be valid with '.jpg' extension" do
+ image.attachment = File.new("spec/fixtures/files/clippy.jpg")
+
+ expect(image).to be_valid
+ end
+
+ end
+
+ describe "image dimmessions" do
+
+ it "should be valid when image dimmessions are 475X475 at least" do
+ expect(image).to be_valid
+ end
+
+ it "should not be valid when image dimmensions are smaller than 475X475" do
+ image.attachment = File.new("spec/fixtures/files/logo_header.jpg")
+
+ expect(image).not_to be_valid
+ end
+ end
+
+ describe "title" do
+
+ it "should not be valid when correct image attached but no image title provided" do
+ image.title = ''
+
+ expect(image).to_not be_valid
+ end
+
+ it "should not be valid when image title is too short" do
+ image.title = 'a' * 3
+
+ expect(image).to_not be_valid
+ end
+
+ it "should not be valid when image title is too long" do
+ image.title = 'a' * 81
+
+ expect(image).to_not be_valid
+ end
+
+ end
+
+ it "image destroy should remove image from file storage" do
+ image.save
+ image_url = image.attachment.url
+
+ expect{ image.attachment.destroy }.to change{ image.attachment.url }.from(image_url).to("/attachments/original/missing.png")
+ end
+
+end
\ No newline at end of file