diff --git a/Gemfile b/Gemfile index 678845e36..a1a0ecf7a 100644 --- a/Gemfile +++ b/Gemfile @@ -33,6 +33,7 @@ gem 'omniauth-facebook', '~> 4.0.0' gem 'omniauth-google-oauth2', '~> 0.4.0' gem 'omniauth-twitter', '~> 1.4.0' gem 'paperclip', '~> 5.1.0' +gem 'jquery-fileupload-rails' gem 'paranoia', '~> 2.3.1' gem 'pg', '~> 0.20.0' gem 'pg_search', '~> 2.0.1' diff --git a/Gemfile.lock b/Gemfile.lock index 3ad07dcfd..a6437663e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -201,6 +201,10 @@ GEM railties (>= 3.1, < 6.0) invisible_captcha (0.9.2) rails (>= 3.2.0) + jquery-fileupload-rails (0.4.7) + actionpack (>= 3.1) + railties (>= 3.1) + sass (>= 3.2) jquery-rails (4.3.1) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) @@ -517,6 +521,7 @@ DEPENDENCIES i18n-tasks (~> 0.9.15) initialjs-rails (~> 0.2.0.5) invisible_captcha (~> 0.9.2) + jquery-fileupload-rails jquery-rails (~> 4.3.1) jquery-ui-rails (~> 6.0.1) kaminari (~> 1.0.1) @@ -562,4 +567,4 @@ DEPENDENCIES BUNDLED WITH - 1.15.1 + 1.15.3 diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index cf2758b78..2ff87100a 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -14,6 +14,7 @@ //= require jquery_ujs //= require jquery-ui/widgets/datepicker //= require jquery-ui/i18n/datepicker-es +//= require jquery-fileupload/basic //= require foundation //= require turbolinks //= require ckeditor/loader @@ -59,6 +60,7 @@ //= require legislation_annotatable //= require watch_form_changes //= require followable +//= require documentable //= require tree_navigator //= require custom @@ -94,6 +96,7 @@ var initialize_modules = function() { App.LegislationAnnotatable.initialize(); App.WatchFormChanges.initialize(); App.TreeNavigator.initialize(); + App.Documentable.initialize(); }; $(function(){ diff --git a/app/assets/javascripts/documentable.js.coffee b/app/assets/javascripts/documentable.js.coffee new file mode 100644 index 000000000..8683ce5e2 --- /dev/null +++ b/app/assets/javascripts/documentable.js.coffee @@ -0,0 +1,101 @@ +App.Documentable = + + initialize: -> + @initializeDirectUploads() + @initializeInterface() + + initializeDirectUploads: -> + + $('input.document_ajax_attachment[type=file]').fileupload + + paramName: "document[attachment]" + + formData: null + + add: (e, data) -> + wrapper = $(e.target).closest('.document') + index = $(e.target).data('index') + is_nested_document = $(e.target).data('nested-document') + $(wrapper).find('.progress-bar-placeholder').empty() + data.progressBar = $(wrapper).find('.progress-bar-placeholder').html('
') + $(wrapper).find('.progress-bar-placeholder').css('display','block') + data.formData = { + "document[title]": $(wrapper).find('input.document-title').val() || data.files[0].name + "index": index, + "nested_document": is_nested_document + } + 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.document_ajax_attachment[type=file]') + + $.each input_files, (index, file) -> + wrapper = $(file).parent() + App.Documentable.watchRemoveDocumentbutton(wrapper) + + watchRemoveDocumentbutton: (wrapper) -> + remove_document_button = $(wrapper).find('.remove-document') + $(remove_document_button).on 'click', (e) -> + e.preventDefault() + $(wrapper).remove() + $('#new_document_link').show() + $('.max-documents-notice').hide() + + uploadNestedDocument: (id, nested_document, result) -> + $('#' + id).replaceWith(nested_document) + @updateLoadingBar(id, result) + @initialize() + + uploadPlainDocument: (id, nested_document, result) -> + $('#' + id).replaceWith(nested_document) + @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) -> + $(".documents-list").append(nested_fields) + @initialize() + + destroyNestedDocument: (id, notice) -> + $('#' + id).remove() + @updateNotice(notice) + + replacePlainDocument: (id, notice, plain_document) -> + $('#' + id).replaceWith(plain_document) + @updateNotice(notice) + @initialize() + + updateNotice: (notice) -> + if $('[data-alert]').length > 0 + $('[data-alert]').replaceWith(notice) + else + $("body").append(notice) + + updateNewDocumentButton: (link) -> + if $('.document').length >= $('.documents').data('max-documents') + $('#new_document_link').hide() + $('.max-documents-notice').removeClass('hide') + $('.max-documents-notice').show() + else if $('#new_document_link').length > 0 + $('#new_document_link').replaceWith(link) + $('.max-documents-notice').hide() + else + $('.max-documents-notice').hide() + $(link).insertBefore('.documents hr:last') diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index a14cf3383..6db475365 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -15,3 +15,4 @@ @import 'annotator_overrides'; @import 'jquery-ui/datepicker'; @import 'datepicker_overrides'; +@import 'documentable'; diff --git a/app/assets/stylesheets/documentable.scss b/app/assets/stylesheets/documentable.scss new file mode 100644 index 000000000..2aa015a14 --- /dev/null +++ b/app/assets/stylesheets/documentable.scss @@ -0,0 +1,59 @@ +.progress-bar-placeholder { + display: none; +} + +.document-form { + .document .file-name { + margin-top: 0; + } + .progress-bar-placeholder { + margin-bottom: 15px; + } + .document .loading-bar.errors { + margin-top: $line-height * 2; + } +} + +.document { + .button { + font-weight: normal; + } + + .progress-bar { + width: 100%; + background-color: $light-gray; + } + + input.document_ajax_attachment[type=file]{ + display: none; + } + + .file-name { + margin-top: $line-height / 2; + } + + .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/icons.scss b/app/assets/stylesheets/icons.scss index 782b5cdb2..5e92ee460 100644 --- a/app/assets/stylesheets/icons.scss +++ b/app/assets/stylesheets/icons.scss @@ -97,6 +97,10 @@ content: '\72'; } +.icon-documents::before { + content: '\68'; +} + .icon-proposals::before { content: '\68'; } diff --git a/app/assets/stylesheets/layout.scss b/app/assets/stylesheets/layout.scss index 7f6f44001..cf07f35ca 100644 --- a/app/assets/stylesheets/layout.scss +++ b/app/assets/stylesheets/layout.scss @@ -18,6 +18,7 @@ // 16. Flags // 17. Activity // 18. Banners +// 19. Documents // // 01. Global styles @@ -91,6 +92,11 @@ a { color: $link; } +.button.hollow.error { + border-color: $alert-border; + color: $color-alert; +} + .postfix.button { padding: 0; } @@ -2133,3 +2139,125 @@ table { text-decoration: none; } } + +// 19. Documents +.document-form form { + + .radio-buttons { + label { + margin-right: $line-height; + } + } + + .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; + } +} + +.documents-list { + + table { + border: 0; + } + + td { + position: relative; + + @include breakpoint(small) { + float: left; + width: 100%; + } + + @include breakpoint(medium) { + float: none; + } + + a { + width: 100%; + } + + &:first-child { + padding-left: $line-height * 1.5; + + @include breakpoint(small) { + width: 100%; + } + + @include breakpoint(medium) { + width: 70%; + } + + @include breakpoint(large) { + width: 80%; + } + } + + &:first-child::before { + color: #007bb7; + content: 'G'; + font-family: "icons" !important; + font-size: rem-calc(24); + left: rem-calc(6); + position: absolute; + top: 0; + + @include breakpoint(small) { + padding-top: rem-calc(12); + } + + @include breakpoint(medium) { + padding-top: rem-calc(22); + } + + } + + } +} diff --git a/app/assets/stylesheets/participation.scss b/app/assets/stylesheets/participation.scss index 38a9fb2f8..8d79eca55 100644 --- a/app/assets/stylesheets/participation.scss +++ b/app/assets/stylesheets/participation.scss @@ -248,11 +248,13 @@ .debate-form, .proposal-form, .budget-investment-form, -.spending-proposal-form { +.spending-proposal-form, +.document-form { .icon-debates, .icon-proposals, - .icon-budget { + .icon-budget, + .icon-documents { font-size: rem-calc(50); line-height: $line-height; opacity: 0.5; @@ -262,7 +264,8 @@ color: $debates; } - .icon-proposals { + .icon-proposals, + .icon-documents { color: $proposals; } @@ -294,7 +297,8 @@ } } -.proposal-form { +.proposal-form, +.document-form { .recommendations li::before { color: $proposals; @@ -746,6 +750,12 @@ display: none; } +.document-form{ + max-width: 75rem; + margin-left: auto; + margin-right: auto; +} + .more-info { clear: both; color: $text-medium; diff --git a/app/controllers/budgets/investments_controller.rb b/app/controllers/budgets/investments_controller.rb index 16144cb27..799d1f76d 100644 --- a/app/controllers/budgets/investments_controller.rb +++ b/app/controllers/budgets/investments_controller.rb @@ -44,10 +44,12 @@ module Budgets set_comment_flags(@comment_tree.comments) load_investment_votes(@investment) @investment_ids = [@investment.id] + @document = Document.new(documentable: @investment) end def create @investment.author = current_user + recover_documents_from_cache(@investment) if @investment.save Mailer.budget_investment_created(@investment).deliver_later @@ -104,7 +106,8 @@ module Budgets def investment_params params.require(:budget_investment).permit(:title, :description, :external_url, :heading_id, :tag_list, - :organization_name, :location, :terms_of_service) + :organization_name, :location, :terms_of_service, + documents_attributes: [:id, :title, :attachment, :cached_attachment, :user_id]) end def load_ballot diff --git a/app/controllers/concerns/commentable_actions.rb b/app/controllers/concerns/commentable_actions.rb index 62bcdaaac..1c47b0c06 100644 --- a/app/controllers/concerns/commentable_actions.rb +++ b/app/controllers/concerns/commentable_actions.rb @@ -58,6 +58,8 @@ module CommentableActions def update resource.assign_attributes(strong_params) + recover_documents_from_cache(resource) + if resource.save redirect_to resource, notice: t("flash.actions.update.#{resource_name.underscore}") else @@ -110,4 +112,11 @@ module CommentableActions nil end + def recover_documents_from_cache(resource) + return false unless resource.try(:documents) + resource.documents = resource.documents.each do |document| + document.set_attachment_from_cached_attachment if document.cached_attachment.present? + end + end + end diff --git a/app/controllers/documents_controller.rb b/app/controllers/documents_controller.rb new file mode 100644 index 000000000..ceb7d191f --- /dev/null +++ b/app/controllers/documents_controller.rb @@ -0,0 +1,100 @@ +class DocumentsController < ApplicationController + before_action :authenticate_user! + before_filter :find_documentable, except: :destroy + before_filter :prepare_new_document, only: [:new, :new_nested] + before_filter :prepare_document_for_creation, only: :create + + 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 @document.save + flash[:notice] = t "documents.actions.create.notice" + redirect_to params[:from] + else + flash[:alert] = t "documents.actions.create.alert" + render :new + end + end + + def destroy + respond_to do |format| + format.html do + if @document.destroy + flash[:notice] = t "documents.actions.destroy.notice" + else + flash[:alert] = t "documents.actions.destroy.alert" + end + redirect_to params[:from] + end + format.js do + if @document.destroy + flash.now[:notice] = t "documents.actions.destroy.notice" + else + flash.now[:alert] = t "documents.actions.destroy.alert" + end + end + end + end + + def destroy_upload + @document = Document.new(cached_attachment: params[:path]) + @document.set_attachment_from_cached_attachment + @document.documentable = @documentable + + if @document.attachment.destroy + flash.now[:notice] = t "documents.actions.destroy.notice" + else + flash.now[:alert] = t "documents.actions.destroy.alert" + end + render :destroy + end + + def upload + @document = Document.new(document_params.merge(user: current_user)) + @document.documentable = @documentable + + if @document.valid? + @document.attachment.save + @document.set_cached_attachment_from_attachment(URI(request.url)) + else + @document.attachment.destroy + end + end + + private + + def document_params + params.require(:document).permit(:title, :documentable_type, :documentable_id, + :attachment, :cached_attachment, :user_id) + end + + def find_documentable + @documentable = params[:documentable_type].constantize.find_or_initialize_by(id: params[:documentable_id]) + end + + def prepare_new_document + @document = Document.new(documentable: @documentable, user_id: current_user.id) + end + + def prepare_document_for_creation + @document = Document.new(document_params) + @document.documentable = @documentable + @document.user = current_user + end + + def recover_attachments_from_cache + if @document.attachment.blank? && @document.cached_attachment.present? + @document.set_attachment_from_cached_attachment + end + end + +end diff --git a/app/controllers/proposals_controller.rb b/app/controllers/proposals_controller.rb index 8b75a10cf..cedc4e8ef 100644 --- a/app/controllers/proposals_controller.rb +++ b/app/controllers/proposals_controller.rb @@ -19,11 +19,13 @@ class ProposalsController < ApplicationController def show super @notifications = @proposal.notifications + @document = Document.new(documentable: @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) if @proposal.save redirect_to share_proposal_path(@proposal), notice: I18n.t('flash.actions.create.proposal') @@ -75,7 +77,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) + :responsible_name, :tag_list, :terms_of_service, :geozone_id, + documents_attributes: [:id, :title, :attachment, :cached_attachment, :user_id] ) end def retired_params @@ -121,4 +124,5 @@ class ProposalsController < ApplicationController def load_successful_proposals @proposal_successful_exists = Proposal.successful.exists? end + end diff --git a/app/helpers/documentables_helper.rb b/app/helpers/documentables_helper.rb new file mode 100644 index 000000000..4fd737908 --- /dev/null +++ b/app/helpers/documentables_helper.rb @@ -0,0 +1,41 @@ +module DocumentablesHelper + + def documentable_class(documentable) + documentable.class.name.parameterize('_') + end + + def max_documents_allowed(documentable) + documentable.class.max_documents_allowed + end + + def max_file_size(documentable) + bytesToMeg(documentable.class.max_file_size) + end + + def accepted_content_types(documentable) + documentable.class.accepted_content_types + end + + def accepted_content_types_extensions(documentable_class) + documentable_class.accepted_content_types + .collect{ |content_type| ".#{content_type.split("/").last}" } + .join(",") + end + + def humanized_accepted_content_types(documentable) + documentable.class.accepted_content_types + .collect{ |content_type| content_type.split("/").last } + .join(", ") + end + + def documentables_note(documentable) + t "documents.form.note", max_documents_allowed: max_documents_allowed(documentable), + accepted_content_types: humanized_accepted_content_types(documentable), + max_file_size: max_file_size(documentable) + end + + def max_documents_allowed?(documentable) + documentable.documents.count >= documentable.class.max_documents_allowed + end + +end \ No newline at end of file diff --git a/app/helpers/documents_helper.rb b/app/helpers/documents_helper.rb new file mode 100644 index 000000000..17d70068b --- /dev/null +++ b/app/helpers/documents_helper.rb @@ -0,0 +1,89 @@ +module DocumentsHelper + + def document_attachment_file_name(document) + document.attachment_file_name + end + + def errors_on_attachment(document) + document.errors[:attachment].join(', ') if document.errors.key?(:attachment) + end + + def bytesToMeg(bytes) + bytes / Numeric::MEGABYTE + end + + def document_nested_field_name(document, index, field) + parent = document.documentable_type.parameterize.underscore + "#{parent.parameterize}[documents_attributes][#{index}][#{field}]" + end + + def document_nested_field_id(document, index, field) + parent = document.documentable_type.parameterize.underscore + "#{parent.parameterize}_documents_attributes_#{index}_#{field}" + end + + def document_nested_field_wrapper_id(index) + "document_#{index}" + end + + def render_destroy_document_link(document, index) + if document.persisted? + link_to t('documents.form.delete_button'), + document_path(document, index: index, nested_document: true), + method: :delete, + remote: true, + data: { confirm: t('documents.actions.destroy.confirm') }, + class: "delete float-right" + elsif !document.persisted? && document.cached_attachment.present? + link_to t('documents.form.delete_button'), + destroy_upload_documents_path(path: document.cached_attachment, + nested_document: true, + index: index, + documentable_type: document.documentable_type, + documentable_id: document.documentable_id), + method: :delete, + remote: true, + class: "delete float-right" + else + link_to t('documents.form.delete_button'), + "#", + class: "delete float-right remove-document" + end + end + + def render_attachment(document, index) + html = file_field_tag :attachment, + accept: accepted_content_types_extensions(document.documentable_type.constantize), + class: 'document_ajax_attachment', + data: { + url: document_direct_upload_url(document), + cached_attachment_input_field: document_nested_field_id(document, index, :cached_attachment), + multiple: false, + index: index, + nested_document: true + }, + name: document_nested_field_name(document, index, :attachment), + id: document_nested_field_id(document, index, :attachment) + if document.attachment.blank? && document.cached_attachment.blank? + klass = document.errors[:attachment].any? ? "error" : "" + html += label_tag document_nested_field_id(document, index, :attachment), + t("documents.form.attachment_label"), + class: "button hollow #{klass}" + if document.errors[:attachment].any? + html += content_tag :small, class: "error" do + errors_on_attachment(document) + end + end + end + html + end + + def document_direct_upload_url(document) + upload_documents_url( + documentable_type: document.documentable_type, + documentable_id: document.documentable_id, + format: :js + ) + end + +end diff --git a/app/models/abilities/administrator.rb b/app/models/abilities/administrator.rb index bc2fea5d4..b8f80c0cd 100644 --- a/app/models/abilities/administrator.rb +++ b/app/models/abilities/administrator.rb @@ -73,6 +73,7 @@ module Abilities can [:manage], ::Legislation::Question cannot :comment_as_moderator, [::Legislation::Question, Legislation::Annotation] + can [:create, :destroy], Document end end end diff --git a/app/models/abilities/common.rb b/app/models/abilities/common.rb index 33a9d50f6..311b0dade 100644 --- a/app/models/abilities/common.rb +++ b/app/models/abilities/common.rb @@ -36,6 +36,9 @@ module Abilities can [:create, :destroy], Follow + can [:create, :destroy, :new], Document, documentable: { author_id: user.id } + can [:new_nested, :upload, :destroy_upload], Document + unless user.organization? can :vote, Debate can :vote, Comment diff --git a/app/models/budget/investment.rb b/app/models/budget/investment.rb index a5bea4508..0dfd7836c 100644 --- a/app/models/budget/investment.rb +++ b/app/models/budget/investment.rb @@ -1,12 +1,16 @@ class Budget class Investment < ActiveRecord::Base - include Measurable include Sanitizable include Taggable include Searchable include Reclassification include Followable + include Documentable + documentable max_documents_allowed: 3, + max_file_size: 3.megabytes, + accepted_content_types: [ "application/pdf" ] + accepts_nested_attributes_for :documents, allow_destroy: true acts_as_votable acts_as_paranoid column: :hidden_at diff --git a/app/models/concerns/documentable.rb b/app/models/concerns/documentable.rb new file mode 100644 index 000000000..4aeaf6eab --- /dev/null +++ b/app/models/concerns/documentable.rb @@ -0,0 +1,20 @@ +module Documentable + extend ActiveSupport::Concern + + included do + has_many :documents, as: :documentable, dependent: :destroy + end + + module ClassMethods + attr_reader :max_documents_allowed, :max_file_size, :accepted_content_types + + private + + def documentable(options= {}) + @max_documents_allowed = options[:max_documents_allowed] + @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 new file mode 100644 index 000000000..7fd82ea33 --- /dev/null +++ b/app/models/document.rb @@ -0,0 +1,81 @@ +class Document < ActiveRecord::Base + include DocumentsHelper + include DocumentablesHelper + has_attached_file :attachment, path: ":rails_root/public/system/:class/:prefix/:style/:filename" + attr_accessor :cached_attachment + + belongs_to :user + belongs_to :documentable, polymorphic: true + + # Disable paperclip security validation due to polymorphic configuration + # Paperclip do not allow to user Procs on valiations definition + do_not_validate_attachment_file_type :attachment + validate :attachment_presence + validate :validate_attachment_content_type, if: -> { attachment.present? } + validate :validate_attachment_size, if: -> { attachment.present? } + validates :title, presence: true + validates :user_id, presence: true + validates :documentable_id, presence: true, if: -> { persisted? } + validates :documentable_type, presence: true, if: -> { persisted? } + + after_save :remove_cached_document, if: -> { valid? && persisted? && cached_attachment.present? } + + 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 + + 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 + + def validate_attachment_size + if documentable.present? && + attachment_file_size > documentable.class.max_file_size + errors[:attachment] = I18n.t("documents.errors.messages.in_between", + min: "0 Bytes", + max: "#{max_file_size(documentable)} MB") + end + end + + def validate_attachment_content_type + if documentable.present? && + !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)) + end + end + + def attachment_presence + if attachment.blank? && cached_attachment.blank? + errors[:attachment] = I18n.t("errors.messages.blank") + end + end + + def remove_cached_document + File.delete(cached_attachment) if File.exists?(cached_attachment) + end + +end diff --git a/app/models/proposal.rb b/app/models/proposal.rb index 21335c5a5..000c2d42d 100644 --- a/app/models/proposal.rb +++ b/app/models/proposal.rb @@ -9,6 +9,11 @@ class Proposal < ActiveRecord::Base include HasPublicAuthor include Graphqlable include Followable + include Documentable + documentable max_documents_allowed: 3, + max_file_size: 3.megabytes, + accepted_content_types: [ "application/pdf" ] + accepts_nested_attributes_for :documents, allow_destroy: true acts_as_votable acts_as_paranoid column: :hidden_at diff --git a/app/views/budgets/investments/_filter_subnav.html.erb b/app/views/budgets/investments/_filter_subnav.html.erb index a46c33b2d..01fef4657 100644 --- a/app/views/budgets/investments/_filter_subnav.html.erb +++ b/app/views/budgets/investments/_filter_subnav.html.erb @@ -17,6 +17,14 @@ <% end %> +
  • + <%= link_to "#tab-documents" do %> +

    + <%= t("documents.tab") %> + (<%= @investment.documents.count %>) +

    + <% end %> +
  • diff --git a/app/views/budgets/investments/_form.html.erb b/app/views/budgets/investments/_form.html.erb index 94f6283e9..7cf2bb1e2 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 'documents/nested_documents', documentable: @investment %> +
    +
    <%= f.text_field :location %>
    diff --git a/app/views/budgets/investments/_investment_show.html.erb b/app/views/budgets/investments/_investment_show.html.erb index 434934c7e..3f1890ae8 100644 --- a/app/views/budgets/investments/_investment_show.html.erb +++ b/app/views/budgets/investments/_investment_show.html.erb @@ -4,6 +4,12 @@
    <%= back_link_to budget_investments_path(investment.budget, heading_id: investment.heading) %> + <% if can?(:create, @document) && investment.documents.size < Budget::Investment.max_documents_allowed %> + <%= link_to t("documents.upload_document"), + new_document_path(documentable_id:investment, documentable_type: investment.class.name, from: request.url), + class: 'button hollow float-right' %> + <% end %> +

    <%= investment.title %>

    @@ -14,7 +20,6 @@  •  <%= investment.heading.name %>
    -

    <%= t("budgets.investments.show.code_html", code: investment.id) %> @@ -51,6 +56,7 @@

    <%= t('budgets.investments.show.price_explanation') %>

    <%= investment.price_explanation %>

    <% end %> +