Merge branch 'master' into user-recomendations

This commit is contained in:
Alberto
2017-09-05 11:31:05 +02:00
committed by GitHub
58 changed files with 1955 additions and 51 deletions

View File

@@ -33,6 +33,7 @@ gem 'omniauth-facebook', '~> 4.0.0'
gem 'omniauth-google-oauth2', '~> 0.4.0' gem 'omniauth-google-oauth2', '~> 0.4.0'
gem 'omniauth-twitter', '~> 1.4.0' gem 'omniauth-twitter', '~> 1.4.0'
gem 'paperclip', '~> 5.1.0' gem 'paperclip', '~> 5.1.0'
gem 'jquery-fileupload-rails'
gem 'paranoia', '~> 2.3.1' gem 'paranoia', '~> 2.3.1'
gem 'pg', '~> 0.20.0' gem 'pg', '~> 0.20.0'
gem 'pg_search', '~> 2.0.1' gem 'pg_search', '~> 2.0.1'

View File

@@ -52,7 +52,7 @@ GEM
safely_block (>= 0.1.1) safely_block (>= 0.1.1)
user_agent_parser user_agent_parser
uuidtools uuidtools
airbrussh (1.2.0) airbrussh (1.3.0)
sshkit (>= 1.6.1, != 1.7.0) sshkit (>= 1.6.1, != 1.7.0)
akami (1.3.1) akami (1.3.1)
gyoku (>= 0.4.0) gyoku (>= 0.4.0)
@@ -73,7 +73,7 @@ GEM
uniform_notifier (~> 1.10.0) uniform_notifier (~> 1.10.0)
byebug (9.0.6) byebug (9.0.6)
cancancan (1.16.0) cancancan (1.16.0)
capistrano (3.8.1) capistrano (3.8.2)
airbrussh (>= 1.0.0) airbrussh (>= 1.0.0)
i18n i18n
rake (>= 10.0.0) rake (>= 10.0.0)
@@ -87,7 +87,7 @@ GEM
capistrano3-delayed-job (1.7.3) capistrano3-delayed-job (1.7.3)
capistrano (~> 3.0, >= 3.0.0) capistrano (~> 3.0, >= 3.0.0)
daemons (~> 1.2.4) daemons (~> 1.2.4)
capybara (2.14.0) capybara (2.14.4)
addressable addressable
mime-types (>= 1.16) mime-types (>= 1.16)
nokogiri (>= 1.3.3) nokogiri (>= 1.3.3)
@@ -95,17 +95,17 @@ GEM
rack-test (>= 0.5.4) rack-test (>= 0.5.4)
xpath (~> 2.0) xpath (~> 2.0)
chronic (0.10.2) chronic (0.10.2)
ckeditor (4.2.3) ckeditor (4.2.4)
cocaine cocaine
orm_adapter (~> 0.5.0) orm_adapter (~> 0.5.0)
climate_control (0.1.0) climate_control (0.2.0)
cliver (0.3.2) cliver (0.3.2)
cocaine (0.5.8) cocaine (0.5.8)
climate_control (>= 0.0.3, < 1.0) climate_control (>= 0.0.3, < 1.0)
cocoon (1.2.9) cocoon (1.2.10)
coffee-rails (4.2.1) coffee-rails (4.2.2)
coffee-script (>= 2.2.0) coffee-script (>= 2.2.0)
railties (>= 4.0.0, < 5.2.x) railties (>= 4.0.0)
coffee-script (2.4.1) coffee-script (2.4.1)
coffee-script-source coffee-script-source
execjs execjs
@@ -120,11 +120,11 @@ GEM
daemons (1.2.4) daemons (1.2.4)
dalli (2.7.6) dalli (2.7.6)
database_cleaner (1.5.3) database_cleaner (1.5.3)
debug_inspector (0.0.2) debug_inspector (0.0.3)
delayed_job (4.1.2) delayed_job (4.1.3)
activesupport (>= 3.0, < 5.1) activesupport (>= 3.0, < 5.2)
delayed_job_active_record (4.1.1) delayed_job_active_record (4.1.2)
activerecord (>= 3.0, < 5.1) activerecord (>= 3.0, < 5.2)
delayed_job (>= 3.0, < 5) delayed_job (>= 3.0, < 5)
devise (3.5.10) devise (3.5.10)
bcrypt (~> 3.0) bcrypt (~> 3.0)
@@ -144,10 +144,10 @@ GEM
json json
thread thread
thread_safe thread_safe
email_spec (2.1.0) email_spec (2.1.1)
htmlentities (~> 4.3.3) htmlentities (~> 4.3.3)
launchy (~> 2.1) launchy (~> 2.1)
mail (~> 2.6.3) mail (~> 2.6)
errbase (0.0.3) errbase (0.0.3)
erubis (2.7.0) erubis (2.7.0)
execjs (2.7.0) execjs (2.7.0)
@@ -158,7 +158,7 @@ GEM
railties (>= 3.0.0) railties (>= 3.0.0)
faker (1.7.3) faker (1.7.3)
i18n (~> 0.5) i18n (~> 0.5)
faraday (0.11.0) faraday (0.12.1)
multipart-post (>= 1.2, < 3) multipart-post (>= 1.2, < 3)
foundation-rails (6.2.4.0) foundation-rails (6.2.4.0)
railties (>= 3.1.0) railties (>= 3.1.0)
@@ -170,12 +170,12 @@ GEM
activesupport (>= 4.1) activesupport (>= 4.1)
railties (>= 4.1) railties (>= 4.1)
tzinfo (~> 1.2, >= 1.2.2) tzinfo (~> 1.2, >= 1.2.2)
geocoder (1.4.3) geocoder (1.4.4)
globalid (0.4.0) globalid (0.4.0)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
graphiql-rails (1.4.1) graphiql-rails (1.4.2)
rails rails
graphql (1.6.3) graphql (1.6.4)
groupdate (3.2.0) groupdate (3.2.0)
activesupport (>= 3) activesupport (>= 3)
gyoku (1.3.1) gyoku (1.3.1)
@@ -183,9 +183,10 @@ GEM
hashie (3.5.5) hashie (3.5.5)
highline (1.7.8) highline (1.7.8)
htmlentities (4.3.4) htmlentities (4.3.4)
httpi (2.4.1) httpi (2.4.2)
rack rack
i18n (0.8.4) socksify
i18n (0.8.6)
i18n-tasks (0.9.15) i18n-tasks (0.9.15)
activesupport (>= 4.0.2) activesupport (>= 4.0.2)
ast (>= 2.1.0) ast (>= 2.1.0)
@@ -200,6 +201,10 @@ GEM
railties (>= 3.1, < 6.0) railties (>= 3.1, < 6.0)
invisible_captcha (0.9.2) invisible_captcha (0.9.2)
rails (>= 3.2.0) rails (>= 3.2.0)
jquery-fileupload-rails (0.4.7)
actionpack (>= 3.1)
railties (>= 3.1)
sass (>= 3.2)
jquery-rails (4.3.1) jquery-rails (4.3.1)
rails-dom-testing (>= 1, < 3) rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0) railties (>= 4.2.0)
@@ -224,7 +229,7 @@ GEM
knapsack (1.13.3) knapsack (1.13.3)
rake rake
timecop (>= 0.1.0) timecop (>= 0.1.0)
kramdown (1.13.2) kramdown (1.14.0)
launchy (2.4.3) launchy (2.4.3)
addressable (~> 2.3) addressable (~> 2.3)
letter_opener (1.4.1) letter_opener (1.4.1)
@@ -259,9 +264,9 @@ GEM
nokogiri (1.8.0) nokogiri (1.8.0)
mini_portile2 (~> 2.2.0) mini_portile2 (~> 2.2.0)
nori (2.6.0) nori (2.6.0)
oauth (0.5.1) oauth (0.5.3)
oauth2 (1.3.1) oauth2 (1.4.0)
faraday (>= 0.8, < 0.12) faraday (>= 0.8, < 0.13)
jwt (~> 1.0) jwt (~> 1.0)
multi_json (~> 1.3) multi_json (~> 1.3)
multi_xml (~> 0.5) multi_xml (~> 0.5)
@@ -328,7 +333,7 @@ GEM
bundler (>= 1.3.0, < 2.0) bundler (>= 1.3.0, < 2.0)
railties (= 4.2.9) railties (= 4.2.9)
sprockets-rails sprockets-rails
rails-assets-markdown-it (8.2.1) rails-assets-markdown-it (8.2.2)
rails-deprecated_sanitizer (1.0.3) rails-deprecated_sanitizer (1.0.3)
activesupport (>= 4.2.0.alpha) activesupport (>= 4.2.0.alpha)
rails-dom-testing (1.0.8) rails-dom-testing (1.0.8)
@@ -386,7 +391,7 @@ GEM
sshkit (>= 1.2) sshkit (>= 1.2)
safely_block (0.2.0) safely_block (0.2.0)
errbase errbase
sass (3.4.23) sass (3.4.25)
sass-rails (5.0.6) sass-rails (5.0.6)
railties (>= 4.0.0, < 6) railties (>= 4.0.0, < 6)
sass (~> 3.1) sass (~> 3.1)
@@ -408,12 +413,13 @@ GEM
docile (~> 1.1.0) docile (~> 1.1.0)
json (>= 1.8, < 3) json (>= 1.8, < 3)
simplecov-html (~> 0.10.0) simplecov-html (~> 0.10.0)
simplecov-html (0.10.0) simplecov-html (0.10.1)
sitemap_generator (5.3.1) sitemap_generator (5.3.1)
builder (~> 3.0) builder (~> 3.0)
social-share-button (0.10.0) social-share-button (0.10.0)
coffee-rails coffee-rails
spring (2.0.1) socksify (1.7.1)
spring (2.0.2)
activesupport (>= 4.2) activesupport (>= 4.2)
spring-commands-rspec (1.0.4) spring-commands-rspec (1.0.4)
spring (>= 0.9.1) spring (>= 0.9.1)
@@ -428,19 +434,19 @@ GEM
actionpack (>= 4.0) actionpack (>= 4.0)
activesupport (>= 4.0) activesupport (>= 4.0)
sprockets (>= 3.0.0) sprockets (>= 3.0.0)
sshkit (1.13.1) sshkit (1.14.0)
net-scp (>= 1.1.2) net-scp (>= 1.1.2)
net-ssh (>= 2.8.0) net-ssh (>= 2.8.0)
term-ansicolor (1.6.0) term-ansicolor (1.6.0)
tins (~> 1.0) tins (~> 1.0)
terminal-table (1.7.3) terminal-table (1.8.0)
unicode-display_width (~> 1.1.1) unicode-display_width (~> 1.1, >= 1.1.1)
thor (0.19.4) thor (0.19.4)
thread (0.2.2) thread (0.2.2)
thread_safe (0.3.6) thread_safe (0.3.6)
tilt (2.0.7) tilt (2.0.7)
timecop (0.8.1) timecop (0.9.1)
tins (1.13.2) tins (1.15.0)
turbolinks (2.5.3) turbolinks (2.5.3)
coffee-rails coffee-rails
turnout (2.4.0) turnout (2.4.0)
@@ -452,14 +458,14 @@ GEM
thread_safe (~> 0.1) thread_safe (~> 0.1)
uglifier (3.2.0) uglifier (3.2.0)
execjs (>= 0.3.0, < 3) execjs (>= 0.3.0, < 3)
unicode-display_width (1.1.3) unicode-display_width (1.3.0)
unicorn (5.3.0) unicorn (5.3.0)
kgio (~> 2.6) kgio (~> 2.6)
raindrops (~> 0.7) raindrops (~> 0.7)
uniform_notifier (1.10.0) uniform_notifier (1.10.0)
user_agent_parser (2.3.0) user_agent_parser (2.3.1)
uuidtools (2.1.5) uuidtools (2.1.5)
warden (1.2.6) warden (1.2.7)
rack (>= 1.0) rack (>= 1.0)
wasabi (3.5.0) wasabi (3.5.0)
httpi (~> 2.0) httpi (~> 2.0)
@@ -473,7 +479,7 @@ GEM
websocket-extensions (0.1.2) websocket-extensions (0.1.2)
whenever (0.9.7) whenever (0.9.7)
chronic (>= 0.6.3) chronic (>= 0.6.3)
xpath (2.0.0) xpath (2.1.0)
nokogiri (~> 1.3) nokogiri (~> 1.3)
PLATFORMS PLATFORMS
@@ -515,6 +521,7 @@ DEPENDENCIES
i18n-tasks (~> 0.9.15) i18n-tasks (~> 0.9.15)
initialjs-rails (~> 0.2.0.5) initialjs-rails (~> 0.2.0.5)
invisible_captcha (~> 0.9.2) invisible_captcha (~> 0.9.2)
jquery-fileupload-rails
jquery-rails (~> 4.3.1) jquery-rails (~> 4.3.1)
jquery-ui-rails (~> 6.0.1) jquery-ui-rails (~> 6.0.1)
kaminari (~> 1.0.1) kaminari (~> 1.0.1)
@@ -558,5 +565,6 @@ DEPENDENCIES
web-console (~> 3.3.0) web-console (~> 3.3.0)
whenever (~> 0.9.7) whenever (~> 0.9.7)
BUNDLED WITH BUNDLED WITH
1.15.1 1.15.3

View File

@@ -14,6 +14,7 @@
//= require jquery_ujs //= require jquery_ujs
//= require jquery-ui/widgets/datepicker //= require jquery-ui/widgets/datepicker
//= require jquery-ui/i18n/datepicker-es //= require jquery-ui/i18n/datepicker-es
//= require jquery-fileupload/basic
//= require foundation //= require foundation
//= require turbolinks //= require turbolinks
//= require ckeditor/loader //= require ckeditor/loader
@@ -59,6 +60,7 @@
//= require legislation_annotatable //= require legislation_annotatable
//= require watch_form_changes //= require watch_form_changes
//= require followable //= require followable
//= require documentable
//= require tree_navigator //= require tree_navigator
//= require custom //= require custom
@@ -94,6 +96,7 @@ var initialize_modules = function() {
App.LegislationAnnotatable.initialize(); App.LegislationAnnotatable.initialize();
App.WatchFormChanges.initialize(); App.WatchFormChanges.initialize();
App.TreeNavigator.initialize(); App.TreeNavigator.initialize();
App.Documentable.initialize();
}; };
$(function(){ $(function(){

View File

@@ -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('<div class="progress-bar"><div class="loading-bar uploading"></div></div>')
$(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')

View File

@@ -15,3 +15,4 @@
@import 'annotator_overrides'; @import 'annotator_overrides';
@import 'jquery-ui/datepicker'; @import 'jquery-ui/datepicker';
@import 'datepicker_overrides'; @import 'datepicker_overrides';
@import 'documentable';

View File

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

View File

@@ -97,6 +97,10 @@
content: '\72'; content: '\72';
} }
.icon-documents::before {
content: '\68';
}
.icon-proposals::before { .icon-proposals::before {
content: '\68'; content: '\68';
} }

View File

@@ -19,6 +19,7 @@
// 17. Activity // 17. Activity
// 18. Banners // 18. Banners
// 19. Recommended Section Home // 19. Recommended Section Home
// 20. Documents
// //
// 01. Global styles // 01. Global styles
@@ -92,6 +93,11 @@ a {
color: $link; color: $link;
} }
.button.hollow.error {
border-color: $alert-border;
color: $color-alert;
}
.postfix.button { .postfix.button {
padding: 0; padding: 0;
} }
@@ -2154,7 +2160,6 @@ table {
.section-recommended { .section-recommended {
padding: $line-height * 2 0; padding: $line-height * 2 0;
// padding-bottom: $line-height * 2;
h2 { h2 {
margin-bottom: $line-height * 2; margin-bottom: $line-height * 2;
@@ -2278,4 +2283,124 @@ table {
} }
} }
// 20. 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);
}
}
}
} }

View File

@@ -248,11 +248,13 @@
.debate-form, .debate-form,
.proposal-form, .proposal-form,
.budget-investment-form, .budget-investment-form,
.spending-proposal-form { .spending-proposal-form,
.document-form {
.icon-debates, .icon-debates,
.icon-proposals, .icon-proposals,
.icon-budget { .icon-budget,
.icon-documents {
font-size: rem-calc(50); font-size: rem-calc(50);
line-height: $line-height; line-height: $line-height;
opacity: 0.5; opacity: 0.5;
@@ -262,7 +264,8 @@
color: $debates; color: $debates;
} }
.icon-proposals { .icon-proposals,
.icon-documents {
color: $proposals; color: $proposals;
} }
@@ -294,7 +297,8 @@
} }
} }
.proposal-form { .proposal-form,
.document-form {
.recommendations li::before { .recommendations li::before {
color: $proposals; color: $proposals;
@@ -746,6 +750,12 @@
display: none; display: none;
} }
.document-form{
max-width: 75rem;
margin-left: auto;
margin-right: auto;
}
.more-info { .more-info {
clear: both; clear: both;
color: $text-medium; color: $text-medium;

View File

@@ -44,10 +44,12 @@ module Budgets
set_comment_flags(@comment_tree.comments) set_comment_flags(@comment_tree.comments)
load_investment_votes(@investment) load_investment_votes(@investment)
@investment_ids = [@investment.id] @investment_ids = [@investment.id]
@document = Document.new(documentable: @investment)
end end
def create def create
@investment.author = current_user @investment.author = current_user
recover_documents_from_cache(@investment)
if @investment.save if @investment.save
Mailer.budget_investment_created(@investment).deliver_later Mailer.budget_investment_created(@investment).deliver_later
@@ -104,7 +106,8 @@ module Budgets
def investment_params def investment_params
params.require(:budget_investment).permit(:title, :description, :external_url, :heading_id, :tag_list, 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 end
def load_ballot def load_ballot

View File

@@ -63,6 +63,8 @@ module CommentableActions
def update def update
resource.assign_attributes(strong_params) resource.assign_attributes(strong_params)
recover_documents_from_cache(resource)
if resource.save if resource.save
redirect_to resource, notice: t("flash.actions.update.#{resource_name.underscore}") redirect_to resource, notice: t("flash.actions.update.#{resource_name.underscore}")
else else
@@ -115,4 +117,11 @@ module CommentableActions
nil nil
end 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 end

View File

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

View File

@@ -19,11 +19,13 @@ class ProposalsController < ApplicationController
def show def show
super super
@notifications = @proposal.notifications @notifications = @proposal.notifications
@document = Document.new(documentable: @proposal)
redirect_to proposal_path(@proposal), status: :moved_permanently if request.path != proposal_path(@proposal) redirect_to proposal_path(@proposal), status: :moved_permanently if request.path != proposal_path(@proposal)
end end
def create def create
@proposal = Proposal.new(proposal_params.merge(author: current_user)) @proposal = Proposal.new(proposal_params.merge(author: current_user))
recover_documents_from_cache(@proposal)
if @proposal.save if @proposal.save
redirect_to share_proposal_path(@proposal), notice: I18n.t('flash.actions.create.proposal') redirect_to share_proposal_path(@proposal), notice: I18n.t('flash.actions.create.proposal')
@@ -75,7 +77,8 @@ class ProposalsController < ApplicationController
def proposal_params def proposal_params
params.require(:proposal).permit(:title, :question, :summary, :description, :external_url, :video_url, 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 end
def retired_params def retired_params
@@ -121,4 +124,5 @@ class ProposalsController < ApplicationController
def load_successful_proposals def load_successful_proposals
@proposal_successful_exists = Proposal.successful.exists? @proposal_successful_exists = Proposal.successful.exists?
end end
end end

View File

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

View File

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

View File

@@ -73,6 +73,7 @@ module Abilities
can [:manage], ::Legislation::Question can [:manage], ::Legislation::Question
cannot :comment_as_moderator, [::Legislation::Question, Legislation::Annotation] cannot :comment_as_moderator, [::Legislation::Question, Legislation::Annotation]
can [:create, :destroy], Document
end end
end end
end end

View File

@@ -36,6 +36,9 @@ module Abilities
can [:create, :destroy], Follow can [:create, :destroy], Follow
can [:create, :destroy, :new], Document, documentable: { author_id: user.id }
can [:new_nested, :upload, :destroy_upload], Document
unless user.organization? unless user.organization?
can :vote, Debate can :vote, Debate
can :vote, Comment can :vote, Comment

View File

@@ -1,12 +1,16 @@
class Budget class Budget
class Investment < ActiveRecord::Base class Investment < ActiveRecord::Base
include Measurable include Measurable
include Sanitizable include Sanitizable
include Taggable include Taggable
include Searchable include Searchable
include Reclassification include Reclassification
include Followable 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_votable
acts_as_paranoid column: :hidden_at acts_as_paranoid column: :hidden_at

View File

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

81
app/models/document.rb Normal file
View File

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

View File

@@ -9,6 +9,11 @@ class Proposal < ActiveRecord::Base
include HasPublicAuthor include HasPublicAuthor
include Graphqlable include Graphqlable
include Followable 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_votable
acts_as_paranoid column: :hidden_at acts_as_paranoid column: :hidden_at

View File

@@ -17,6 +17,14 @@
</h3> </h3>
<% end %> <% end %>
</li> </li>
<li class="tabs-title">
<%= link_to "#tab-documents" do %>
<h3>
<%= t("documents.tab") %>
(<%= @investment.documents.count %>)
</h3>
<% end %>
</li>
</ul> </ul>
</div> </div>
</div> </div>

View File

@@ -21,6 +21,10 @@
<%= f.text_field :external_url %> <%= f.text_field :external_url %>
</div> </div>
<div class="documents small-12 column" data-max-documents="<%= Budget::Investment.max_documents_allowed %>">
<%= render 'documents/nested_documents', documentable: @investment %>
</div>
<div class="small-12 column"> <div class="small-12 column">
<%= f.text_field :location %> <%= f.text_field :location %>
</div> </div>

View File

@@ -4,6 +4,12 @@
<div class="small-12 medium-9 column"> <div class="small-12 medium-9 column">
<%= back_link_to budget_investments_path(investment.budget, heading_id: investment.heading) %> <%= 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 %>
<h1><%= investment.title %></h1> <h1><%= investment.title %></h1>
<div class="budget-investment-info"> <div class="budget-investment-info">
@@ -14,7 +20,6 @@
<span class="bullet">&nbsp;&bull;&nbsp;</span> <span class="bullet">&nbsp;&bull;&nbsp;</span>
<%= investment.heading.name %> <%= investment.heading.name %>
</div> </div>
<br> <br>
<p id="investment_code"> <p id="investment_code">
<%= t("budgets.investments.show.code_html", code: investment.id) %> <%= t("budgets.investments.show.code_html", code: investment.id) %>
@@ -51,6 +56,7 @@
<h2><%= t('budgets.investments.show.price_explanation') %></h2> <h2><%= t('budgets.investments.show.price_explanation') %></h2>
<p><%= investment.price_explanation %></p> <p><%= investment.price_explanation %></p>
<% end %> <% end %>
</div> </div>
<aside class="small-12 medium-3 column"> <aside class="small-12 medium-3 column">

View File

@@ -19,4 +19,11 @@
comment_flags: @comment_flags, comment_flags: @comment_flags,
display_comments_count: false } %> display_comments_count: false } %>
</div> </div>
<div class="tabs-panel" id="tab-documents">
<%= render 'documents/documents',
documents: @investment.documents,
max_documents_allowed: Budget::Investment.max_documents_allowed %>
</div>
</div> </div>

View File

@@ -0,0 +1,20 @@
<tr id="<%= dom_id(document)%>">
<td class="document-link">
<%= document.title %>
</td>
<td class="text-center">
<%= link_to t('documents.buttons.download_document'),
document.attachment.url,
target: "_blank",
rel: "nofollow",
class: 'button hollow' %>
</td>
<td class="text-center">
<% if can? :destroy, Document %>
<%= link_to t('documents.buttons.destroy_document'),
document_path(document, from: request.url), method: :delete,
data: { confirm: t('documents.actions.destroy.confirm') },
class: 'button hollow alert' %>
<% end %>
</td>
</tr>

View File

@@ -0,0 +1,35 @@
<% if documents.any? %>
<% if documents.size == max_documents_allowed && can?(:create, Document) %>
<div class="row documents-list">
<div class="small-12 column">
<div class="callout warning text-center">
<%= t "documents.max_documents_allowed_reached_html" %>
</div>
</div>
</div>
<% end %>
<div class="row documents-list">
<div class="small-12 column">
<table>
<tbody>
<% documents.each do |document| %>
<%= render "documents/document", document: document %>
<% end %>
</tbody>
</table>
</div>
</div>
<% else %>
<div class="row">
<div class="small-12 column">
<div class="callout primary text-center">
<%= t('documents.no_documents') %>
</div>
</div>
</div>
<% end %>

View File

@@ -0,0 +1,20 @@
<%= form_for @document,
url: documents_path(
documentable_type: @document.documentable_type,
documentable_id: @document.documentable_id,
from: params[:from]
),
html: { multipart: true, class: "documentable"},
data: { direct_upload_url: upload_documents_url(documentable_type: @document.documentable_type, documentable_id: @document.documentable_id) } do |f| %>
<%= render 'shared/errors', resource: @document %>
<div class="row">
<%= render 'plain_fields', document: @document %>
<div class="actions small-12 medium-6 large-4 end column">
<%= f.submit(t("documents.form.submit_button"), class: "button expanded") %>
</div>
</div>
<% end %>

View File

@@ -0,0 +1,22 @@
<div class="documents-list">
<%= label_tag :documents, t("documents.form.title") %>
<p class="help-text"><%= documentables_note(documentable) %></p>
<% documentable.documents.each_with_index do |document, index| %>
<%= render 'documents/nested_fields', document: document, index: index, documentable: documentable %>
<% end %>
</div>
<% unless max_documents_allowed?(documentable) %>
<%= link_to t("documents.form.add_new_document"),
new_nested_documents_path(documentable_type: documentable.class.name, index: documentable.documents.size),
remote: true,
id: "new_document_link",
class: "button hollow" %>
<% end %>
<div class="max-documents-notice callout warning text-center <%= "hide" unless max_documents_allowed?(documentable) %>">
<%= t "documents.max_documents_allowed_reached_html" %>
</div>
<hr>

View File

@@ -0,0 +1,31 @@
<div id="<%= document_nested_field_wrapper_id(index) %>" class="document">
<%= hidden_field_tag :id,
document.id,
name: document_nested_field_name(document, index, :id),
id: document_nested_field_id(document, index, :id) if document.persisted? %>
<%= hidden_field_tag :user_id,
current_user.id,
name: document_nested_field_name(document, index, :user_id),
id: document_nested_field_id(document, index, :user_id) %>
<%= hidden_field_tag :cached_attachment,
document.cached_attachment,
name: document_nested_field_name(document, index, :cached_attachment),
id: document_nested_field_id(document, index, :cached_attachment) %>
<%= label_tag :title, t("activerecord.attributes.document.title") %>
<%= text_field_tag :title,
document.title,
name: document_nested_field_name(document, index, :title),
id: document_nested_field_id(document, index, :title),
class: "document-title" %>
<% if document.errors[:title].any? %>
<small class="error"><%= document.errors[:title].join(", ") %></small>
<% end %>
<%= render_attachment(document, index) %>
<%= render_destroy_document_link(document, index) %>
<p class="file-name"><%= document_attachment_file_name(document) %></p>
<div class="progress-bar-placeholder"><div class="loading-bar"></div></div>
<hr>
</div>

View File

@@ -0,0 +1,50 @@
<div id="plain_document_fields" class="document">
<div class="small-12 column">
<%= label_tag :document_title, t("activerecord.attributes.document.title") %>
<%= text_field_tag :document_title, document.title, name: "document[title]", class: "document-title" %>
<% if document.errors.has_key?(:title) %>
<small class="error"><%= document.errors[:title].join(", ") %></small>
<% end %>
</div>
<div class="small-12 column">
<%= hidden_field_tag :cached_attachment, document.cached_attachment, name: "document[cached_attachment]" %>
<%= file_field_tag :attachment,
accept: accepted_content_types_extensions(document.documentable.class),
label: false,
class: 'document_ajax_attachment',
data: {
url: upload_documents_url(documentable_type: document.documentable_type, documentable_id: document.documentable_id),
cached_attachment_input_field: "document_cached_attachment",
multiple: false,
nested_document: false
},
id: "document_attachment",
name: "document[attachment]" %>
<% if document.cached_attachment.blank? %>
<%= label_tag :document_attachment, t("documents.form.attachment_label"), class: 'button hollow' %>
<% else %>
<%= link_to t('documents.form.delete_button'),
destroy_upload_documents_path(path: document.cached_attachment,
nested_document: false,
documentable_type: document.documentable_type,
documentable_id: document.documentable_id),
method: :delete,
remote: true,
class: "delete float-right" %>
<% end %>
<% if document.errors.has_key?(:attachment) %>
<div class="small-12 column source-option-file">
<div class="attachment-errors">
<small class="error"><%= errors_on_attachment(document) %></small>
</div>
</div>
<% end %>
<p class="file-name"><%= document_attachment_file_name(document) %></p>
<div class="progress-bar-placeholder"><div class="loading-bar"></div></div>
</div>
</div>

View File

@@ -0,0 +1,17 @@
<% if params[:nested_document] == "true" %>
App.Documentable.destroyNestedDocument("<%= document_nested_field_wrapper_id(params[:index]) %>", "<%= j render('layouts/flash') %>")
<% new_document_link = link_to t("documents.form.add_new_document"),
new_nested_documents_path(documentable_type: @document.documentable_type, index: params[:index]),
remote: true,
id: "new_document_link",
class: "button hollow" %>
App.Documentable.updateNewDocumentButton("<%= j new_document_link %>")
<% else %>
App.Documentable.replacePlainDocument("plain_document_fields",
"<%= j render('layouts/flash') %>",
"<%= j render('plain_fields', document: @document) %>")
<% end %>

View File

@@ -0,0 +1,27 @@
<div class="document-form <%= documentable_class(@document.documentable) %> row">
<div class="small-12 medium-9 column">
<%= back_link_to params[:from] %>
<h1><%= t("documents.new.title") %></h1>
<%= render "documents/form", form_url: documents_url %>
</div>
<div class="small-12 medium-3 column">
<span class="icon-documents float-right"></span>
<h2><%= t("documents.recommendations_title") %></h2>
<ul class="recommendations">
<li>
<%= t "documents.recommendation_one_html",
max_documents_allowed: max_documents_allowed(@document.documentable) %>
</li>
<li>
<%= t "documents.recommendation_two_html",
accepted_content_types: humanized_accepted_content_types(@document.documentable) %>
</li>
<li>
<%= t "documents.recommendation_three_html",
max_file_size: max_file_size(@document.documentable) %>
</li>
</ul>
</div>
</div>

View File

@@ -0,0 +1,9 @@
<%
new_document_link = link_to t("documents.form.add_new_document"),
new_nested_documents_path(documentable_type: params[:documentable_type], index: params[:index].to_i + 1),
remote: true,
id: "new_document_link",
class: "button hollow"
%>
App.Documentable.new("<%= j render('documents/nested_fields', document: @document, index: params[:index]) %>")
App.Documentable.updateNewDocumentButton("<%= j new_document_link %>")

View File

@@ -0,0 +1,12 @@
<% if params[:nested_document] == "true" %>
App.Documentable.uploadNestedDocument("<%= document_nested_field_wrapper_id(params[:index]) %>",
"<%= j render('documents/nested_fields', document: @document, index: params[:index]) %>",
<%= @document.cached_attachment.present? %>)
<% else %>
App.Documentable.uploadPlainDocument("plain_document_fields",
"<%= j render('documents/plain_fields', document: @document) %>",
<%= @document.cached_attachment.present? %>)
<% end %>

View File

@@ -17,6 +17,14 @@
</h3> </h3>
<% end %> <% end %>
</li> </li>
<li class="tabs-title">
<%= link_to "#tab-documents" do %>
<h3>
<%= t("documents.tab") %>
(<%= @proposal.documents.count %>)
</h3>
<% end %>
</li>
</ul> </ul>
</div> </div>
</div> </div>

View File

@@ -34,7 +34,6 @@
<%= f.cktext_area :description, maxlength: Proposal.description_max_length, ckeditor: { language: I18n.locale }, label: false %> <%= f.cktext_area :description, maxlength: Proposal.description_max_length, ckeditor: { language: I18n.locale }, label: false %>
</div> </div>
<div class="small-12 column"> <div class="small-12 column">
<%= f.label :video_url, t("proposals.form.proposal_video_url") %> <%= f.label :video_url, t("proposals.form.proposal_video_url") %>
<p class="help-text" id="video-url-help-text"><%= t("proposals.form.proposal_video_url_note") %></p> <p class="help-text" id="video-url-help-text"><%= t("proposals.form.proposal_video_url_note") %></p>
@@ -47,6 +46,10 @@
<%= f.text_field :external_url, placeholder: t("proposals.form.proposal_external_url"), label: false %> <%= f.text_field :external_url, placeholder: t("proposals.form.proposal_external_url"), label: false %>
</div> </div>
<div class="documents small-12 column" data-max-documents="<%= Proposal.max_documents_allowed %>">
<%= render 'documents/nested_documents', documentable: @proposal %>
</div>
<div class="small-12 medium-6 column"> <div class="small-12 medium-6 column">
<%= f.label :geozone_id, t("proposals.form.geozone") %> <%= f.label :geozone_id, t("proposals.form.geozone") %>
<%= f.select :geozone_id, geozone_select_options, {include_blank: t("geozones.none"), label: false} %> <%= f.select :geozone_id, geozone_select_options, {include_blank: t("geozones.none"), label: false} %>

View File

@@ -16,6 +16,12 @@
<div class="small-12 medium-9 column"> <div class="small-12 medium-9 column">
<%= back_link_to %> <%= back_link_to %>
<% if can?(:create, @document) && @proposal.documents.size < Proposal.max_documents_allowed %>
<%= link_to t("documents.upload_document"),
new_document_path(documentable_id: @proposal, documentable_type: @proposal.class.name, from: request.url),
class: 'button hollow float-right' %>
<% end %>
<% if author_of?(@proposal, current_user) %> <% if author_of?(@proposal, current_user) %>
<%= link_to t("proposals.show.send_notification"), new_proposal_notification_path(proposal_id: @proposal.id), <%= link_to t("proposals.show.send_notification"), new_proposal_notification_path(proposal_id: @proposal.id),
class: 'button hollow float-right' %> class: 'button hollow float-right' %>
@@ -165,4 +171,10 @@
<div class="tabs-panel is-active" id="tab-comments"> <div class="tabs-panel is-active" id="tab-comments">
<%= render "proposals/comments" %> <%= render "proposals/comments" %>
</div> </div>
<div class="tabs-panel" id="tab-documents">
<%= render 'documents/documents',
documents: @proposal.documents,
max_documents_allowed: Proposal.max_documents_allowed %>
</div>
</div> </div>

View File

@@ -37,6 +37,7 @@ data:
- config/locales/%{locale}/officing.yml - config/locales/%{locale}/officing.yml
- config/locales/%{locale}/budgets.yml - config/locales/%{locale}/budgets.yml
- config/locales/%{locale}/legislation.yml - config/locales/%{locale}/legislation.yml
- config/locales/%{locale}/documents.yml
# Locale files to write new keys to, based on a list of key pattern => file rules. Matched from top to bottom: # 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 # `i18n-tasks normalize -p` will force move the keys according to these rules

View File

@@ -76,6 +76,9 @@ en:
legislation/answers: legislation/answers:
one: "Answer" one: "Answer"
other: "Answers" other: "Answers"
documents:
one: "Document"
other: "Documents"
attributes: attributes:
budget: budget:
name: "Name" name: "Name"
@@ -197,6 +200,9 @@ en:
value: Value value: Value
legislation/annotation: legislation/annotation:
text: Comment text: Comment
document:
title: Title
attachment: Attachment
errors: errors:
models: models:
user: user:

View File

@@ -0,0 +1,35 @@
en:
documents:
tab: Documents
no_documents: Don't have uploaded documents
upload_document: Upload document
max_documents_allowed_reached_html: You have reached the maximum number of documents allowed! <strong>You have to delete one before you can upload another.</strong>
form:
title: Documents
attachment_label: Choose document
submit_button: Upload document
delete_button: Remove document
note: "You can upload up to a maximum of %{max_documents_allowed} documents of following content types: %{accepted_content_types}, up to %{max_file_size} MB per file."
add_new_document: Add new document
new:
title: Upload document
recommendations_title: File upload tips
recommendation_one_html: You can upload up to a <strong>maximum of %{max_documents_allowed} documents</strong>.
recommendation_two_html: You can upload <strong>%{accepted_content_types}</strong> files.
recommendation_three_html: You can upload files up to <strong>%{max_file_size} MB</strong>.
actions:
create:
notice: Document was created successfully.
alert: Cannot create document. Check form errors and try again.
destroy:
notice: Document was deleted successfully.
alert: Cannot destroy document.
confirm: Are you sure you want to delete the document? This action cannot be undone!
buttons:
download_document: Dowload file
destroy_document: 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}

View File

@@ -181,6 +181,7 @@ en:
user: Account user: Account
verification/sms: phone verification/sms: phone
signature_sheet: Signature sheet signature_sheet: Signature sheet
document: Document
geozones: geozones:
none: All city none: All city
all: All scopes all: All scopes

View File

@@ -76,6 +76,9 @@ es:
legislation/answers: legislation/answers:
one: "Respuesta" one: "Respuesta"
other: "Respuestas" other: "Respuestas"
documents:
one: "Documento"
other: "Documentos"
attributes: attributes:
budget: budget:
name: "Nombre" name: "Nombre"
@@ -192,6 +195,9 @@ es:
value: Valor value: Valor
legislation/annotation: legislation/annotation:
text: Comentario text: Comentario
document:
title: Título
attachment: Archivo adjunto
errors: errors:
models: models:
user: user:

View File

@@ -0,0 +1,34 @@
es:
documents:
tab: Documentos
no_documents: No hay documentos subidos
upload_document: Subir documento
max_documents_allowed_reached_html: ¡Has alcanzado el número máximo de documentos permitidos! <strong>Tienes que eliminar uno antes de poder subir otro.</strong>
form:
title: Documentos
attachment_label: Selecciona un documento
submit_button: Subir documento
delete_button: Eliminar documento
note: "Puedes subir hasta un máximo de %{max_documents_allowed} documentos en los formatos: %{accepted_content_types}, y de hasta %{max_file_size} MB por archivo."
add_new_document: Añadir nuevo documento
new:
title: Subir un documento
recommendations_title: Consejos para subir archivos
recommendation_one_html: Puedes subir hasta un máximo de <strong>%{max_documents_allowed} documentos</strong>
recommendation_two_html: Sólo puedes subir <strong>archivos %{accepted_content_types}</strong>.
recommendation_three_html: Puedes subir archivos de hasta <strong>%{max_file_size} MB</strong>
actions:
create:
notice: "El documento se ha creado correctamente."
alert: "El documento no se ha podido crear. Revise los errores del formulario."
destroy:
notice: "El documento se ha eliminado correctamente."
alert: "El documento no se ha podido eliminar."
confirm: "¿Está seguro de que desea eliminar el documento? Esta acción no se puede deshacer!"
buttons:
download_document: Descargar archivo
destroy_document: Eliminar
errors:
messages:
in_between: debe estar entre %{min} y %{max}
wrong_content_type: El tipo de contenido %{content_type} del archivo no coincide con ninguno de los tipos de contenido aceptados %{accepted_content_types}

View File

@@ -181,6 +181,7 @@ es:
user: la cuenta user: la cuenta
verification/sms: el teléfono verification/sms: el teléfono
signature_sheet: la hoja de firmas signature_sheet: la hoja de firmas
document: el documento
geozones: geozones:
none: Toda la ciudad none: Toda la ciudad
all: Todos los ámbitos de actuación all: Todos los ámbitos de actuación

View File

@@ -95,6 +95,14 @@ Rails.application.routes.draw do
resources :follows, only: [:create, :destroy] resources :follows, only: [:create, :destroy]
resources :documents, only: [:new, :create, :destroy] do
collection do
get :new_nested
delete :destroy_upload
post :upload
end
end
resources :stats, only: [:index] resources :stats, only: [:index]
resources :legacy_legislations, only: [:show], path: 'legislations' resources :legacy_legislations, only: [:show], path: 'legislations'

View File

@@ -0,0 +1,14 @@
class CreateDocuments < ActiveRecord::Migration
def change
create_table :documents do |t|
t.string :title
t.attachment :attachment
t.references :user, index: true, foreign_key: true
t.references :documentable, polymorphic: true, index: true
t.timestamps null: false
end
add_index :documents, [:user_id, :documentable_type, :documentable_id], name: "access_documents"
end
end

View File

@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170719174326) do ActiveRecord::Schema.define(version: 20170720092638) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@@ -298,6 +298,23 @@ ActiveRecord::Schema.define(version: 20170719174326) do
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
end end
create_table "documents", force: :cascade do |t|
t.string "title"
t.string "attachment_file_name"
t.string "attachment_content_type"
t.integer "attachment_file_size"
t.datetime "attachment_updated_at"
t.integer "user_id"
t.integer "documentable_id"
t.string "documentable_type"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "documents", ["documentable_type", "documentable_id"], name: "index_documents_on_documentable_type_and_documentable_id", using: :btree
add_index "documents", ["user_id", "documentable_type", "documentable_id"], name: "access_documents", using: :btree
add_index "documents", ["user_id"], name: "index_documents_on_user_id", using: :btree
create_table "failed_census_calls", force: :cascade do |t| create_table "failed_census_calls", force: :cascade do |t|
t.integer "user_id" t.integer "user_id"
t.string "document_number" t.string "document_number"
@@ -1014,6 +1031,7 @@ ActiveRecord::Schema.define(version: 20170719174326) do
add_foreign_key "administrators", "users" add_foreign_key "administrators", "users"
add_foreign_key "annotations", "legacy_legislations" add_foreign_key "annotations", "legacy_legislations"
add_foreign_key "annotations", "users" add_foreign_key "annotations", "users"
add_foreign_key "documents", "users"
add_foreign_key "failed_census_calls", "poll_officers" add_foreign_key "failed_census_calls", "poll_officers"
add_foreign_key "failed_census_calls", "users" add_foreign_key "failed_census_calls", "users"
add_foreign_key "flags", "users" add_foreign_key "flags", "users"

View File

@@ -368,6 +368,20 @@ FactoryGirl.define do
end end
end end
factory :document do
sequence(:title) { |n| "Document title #{n}" }
association :user, factory: :user
attachment { File.new("spec/fixtures/files/empty.pdf") }
trait :proposal_document do
association :documentable, factory: :proposal
end
trait :budget_investment_document do
association :documentable, factory: :budget_investment
end
end
factory :comment do factory :comment do
association :commentable, factory: :debate association :commentable, factory: :debate
user user

View File

@@ -431,6 +431,16 @@ feature 'Budget Investments' do
it_behaves_like "followable", "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"}
it_behaves_like "nested documentable",
"budget_investment",
"new_budget_investment_path",
{ "budget_id": "budget_id" },
"fill_new_valid_budget_investment",
"Create Investment",
"Budget Investment created successfully."
context "Destroy" do context "Destroy" do
scenario "Admin cannot destroy budget investments" do scenario "Admin cannot destroy budget investments" do

View File

@@ -1317,6 +1317,24 @@ feature 'Proposals' do
it_behaves_like "followable", "proposal", "proposal_path", { "id": "id" } it_behaves_like "followable", "proposal", "proposal_path", { "id": "id" }
it_behaves_like "documentable", "proposal", "proposal_path", { "id": "id" }
it_behaves_like "nested documentable",
"proposal",
"new_proposal_path",
{ },
"fill_new_valid_proposal",
"Create proposal",
"Proposal created successfully"
it_behaves_like "nested documentable",
"proposal",
"edit_proposal_path",
{ "id": "id" },
nil,
"Save changes",
"Proposal updated successfully"
scenario 'Erased author' do scenario 'Erased author' do
user = create(:user) user = create(:user)
proposal = create(:proposal, author: user) proposal = create(:proposal, author: user)

BIN
spec/fixtures/files/empty.pdf vendored Normal file

Binary file not shown.

View File

@@ -12,8 +12,12 @@ describe "Abilities::Administrator" do
let(:debate) { create(:debate) } let(:debate) { create(:debate) }
let(:comment) { create(:comment) } let(:comment) { create(:comment) }
let(:proposal) { create(:proposal) } let(:proposal) { create(:proposal) }
let(:budget_investment) { create(:budget_investment) }
let(:legislation_question) { create(:legislation_question) } let(:legislation_question) { create(:legislation_question) }
let(:proposal_document) { build(:document, documentable: proposal) }
let(:budget_investment_document) { build(:document, documentable: budget_investment) }
let(:hidden_debate) { create(:debate, :hidden) } let(:hidden_debate) { create(:debate, :hidden) }
let(:hidden_comment) { create(:comment, :hidden) } let(:hidden_comment) { create(:comment, :hidden) }
let(:hidden_proposal) { create(:proposal, :hidden) } let(:hidden_proposal) { create(:proposal, :hidden) }
@@ -71,4 +75,12 @@ describe "Abilities::Administrator" do
it { should be_able_to(:valuate, create(:budget_investment, budget: create(:budget, phase: 'valuating'))) } it { should be_able_to(:valuate, create(:budget_investment, budget: create(:budget, phase: 'valuating'))) }
it { should be_able_to(:valuate, create(:budget_investment, budget: create(:budget, phase: 'finished'))) } it { should be_able_to(:valuate, create(:budget_investment, budget: create(:budget, phase: 'finished'))) }
it { should be_able_to(:new, proposal_document) }
it { should be_able_to(:create, proposal_document) }
it { should be_able_to(:destroy, proposal_document) }
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) }
end end

View File

@@ -54,6 +54,11 @@ describe "Abilities::Common" do
let(:incoming_poll_question_from_other_geozone) { create(:poll_question, poll: incoming_poll_from_other_geozone) } let(:incoming_poll_question_from_other_geozone) { create(:poll_question, poll: incoming_poll_from_other_geozone) }
let(:incoming_poll_question_from_all_geozones) { create(:poll_question, poll: incoming_poll) } let(:incoming_poll_question_from_all_geozones) { create(:poll_question, poll: incoming_poll) }
let(:own_proposal_document) { build(:document, documentable: own_proposal) }
let(:proposal_document) { build(:document, documentable: proposal) }
let(:own_budget_investment_document) { build(:document, documentable: own_investment_in_accepting_budget) }
let(:budget_investment_document) { build(:document, documentable: investment_in_accepting_budget) }
it { should be_able_to(:index, Debate) } it { should be_able_to(:index, Debate) }
it { should be_able_to(:show, debate) } it { should be_able_to(:show, debate) }
it { should be_able_to(:vote, debate) } it { should be_able_to(:vote, debate) }
@@ -82,6 +87,25 @@ describe "Abilities::Common" do
it { should_not be_able_to(:create, DirectMessage) } it { should_not be_able_to(:create, DirectMessage) }
it { should_not be_able_to(:show, DirectMessage) } it { should_not be_able_to(:show, DirectMessage) }
it { should be_able_to(:new_nested, Document) }
it { should be_able_to(:destroy_upload, Document) }
it { should be_able_to(:new, own_proposal_document) }
it { should be_able_to(:create, own_proposal_document) }
it { should be_able_to(:destroy, own_proposal_document) }
it { should_not be_able_to(:new, proposal_document) }
it { should_not be_able_to(:create, proposal_document) }
it { should_not be_able_to(:destroy, proposal_document) }
it { should be_able_to(:new, own_budget_investment_document) }
it { should be_able_to(:create, own_budget_investment_document) }
it { should be_able_to(:destroy, own_budget_investment_document) }
it { should_not be_able_to(:new, budget_investment_document) }
it { should_not be_able_to(:create, budget_investment_document) }
it { should_not be_able_to(:destroy, budget_investment_document) }
describe 'flagging content' do describe 'flagging content' do
it { should be_able_to(:flag, debate) } it { should be_able_to(:flag, debate) }
it { should be_able_to(:unflag, debate) } it { should be_able_to(:unflag, debate) }

View File

@@ -0,0 +1,8 @@
require 'rails_helper'
describe Document do
it_behaves_like "document validations", "budget_investment_document"
it_behaves_like "document validations", "proposal_document"
end

View File

@@ -0,0 +1,404 @@
shared_examples "documentable" do |documentable_factory_name, documentable_path, documentable_path_arguments|
include ActionView::Helpers
include DocumentsHelper
include DocumentablesHelper
let!(:administrator) { create(:user) }
let!(:user) { create(:user) }
let!(:arguments) { {} }
let!(:documentable) { create(documentable_factory_name, author: user) }
let!(:documentable_dom_name) { documentable_factory_name.parameterize }
before do
create(:administrator, user: administrator)
documentable_path_arguments.each do |argument_name, path_to_value|
arguments.merge!("#{argument_name}": documentable.send(path_to_value))
end
end
context "Show" do
scenario "Should not display upload document button when there is no logged user" do
visit send(documentable_path, arguments)
within "##{dom_id(documentable)}" do
expect(page).not_to have_link("Upload document")
end
end
scenario "Should not display upload document button when maximum number of documents reached " do
create_list(:document, 3, documentable: documentable)
visit send(documentable_path, arguments)
within "##{dom_id(documentable)}" do
expect(page).not_to have_link("Upload document")
end
end
scenario "Should display upload document button when user is logged in and is documentable owner" do
login_as(user)
visit send(documentable_path, arguments)
within "##{dom_id(documentable)}" do
expect(page).to have_link("Upload document")
end
end
scenario "Should display upload document button when admin is logged in" do
login_as(administrator)
visit send(documentable_path, arguments)
within "##{dom_id(documentable)}" do
expect(page).to have_link("Upload document")
end
end
scenario "Should navigate to new document page when click un upload button" do
login_as(user)
visit send(documentable_path, arguments)
click_link "Upload document"
expect(page).to have_selector("h1", text: "Upload document")
end
describe "Documents tab" do
let!(:document) { create(:document, documentable: documentable, user: documentable.author)}
scenario "Should not display maximum number of documents alert when reached for users without document creation permission" do
create_list(:document, 2, documentable: documentable)
visit send(documentable_path, arguments)
within "#tab-documents" do
expect(page).not_to have_content "You have reached the maximum number of documents allowed! You have to delete one before you can upload another."
end
end
scenario "Should display maximum number of documents alert when reached and when current user has document creation permission" do
login_as documentable.author
create_list(:document, 2, documentable: documentable)
visit send(documentable_path, arguments)
within "#tab-documents" do
expect(page).to have_content "You have reached the maximum number of documents allowed! You have to delete one before you can upload another."
end
end
scenario "Download action should be able to anyone" do
visit send(documentable_path, arguments)
within "#tab-documents" do
expect(page).to have_link("Dowload file")
end
end
scenario "Download file link should have blank target attribute" do
visit send(documentable_path, arguments)
within "#tab-documents" do
expect(page).to have_selector("a[target=_blank]", text: "Dowload file")
end
end
scenario "Download file links should have rel attribute setted to no follow" do
visit send(documentable_path, arguments)
within "#tab-documents" do
expect(page).to have_selector("a[rel=nofollow]", text: "Dowload file")
end
end
describe "Destroy action" do
scenario "Should not be able when no user logged in" do
visit send(documentable_path, arguments)
within "#tab-documents" do
expect(page).not_to have_link("Destroy")
end
end
scenario "Should be able when documentable author is logged in" do
login_as documentable.author
visit send(documentable_path, arguments)
within "#tab-documents" do
expect(page).to have_link("Destroy")
end
end
scenario "Should be able when any administrator logged in" do
login_as administrator
visit send(documentable_path, arguments)
within "#tab-documents" do
expect(page).to have_link("Destroy")
end
end
end
end
end
context "New" do
scenario "Should not be able for unathenticated users" do
visit new_document_path(documentable_type: documentable.class.name,
documentable_id: documentable.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_document_path(documentable_type: documentable.class.name,
documentable_id: documentable.id)
expect(page).to have_content("You do not have permission to carry out the action 'new' on document. ")
end
scenario "Should be able to documentable author" do
login_as documentable.author
visit new_document_path(documentable_type: documentable.class.name,
documentable_id: documentable.id)
expect(page).to have_selector("h1", text: "Upload document")
end
scenario "Should display file name after file selection", :js do
login_as documentable.author
visit new_document_path(documentable_type: documentable.class.name,
documentable_id: documentable.id)
attach_file :document_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 documentable.author
visit new_document_path(documentable_type: documentable.class.name,
documentable_id: documentable.id)
attach_file :document_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 documentable.author
visit new_document_path(documentable_type: documentable.class.name,
documentable_id: documentable.id)
attach_file :document_attachment, "spec/fixtures/files/empty.pdf", 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 documentable.author
visit new_document_path(documentable_type: documentable.class.name,
documentable_id: documentable.id)
attach_file :document_attachment, "spec/fixtures/files/logo_header.png", make_visible: true
sleep 1
expect(page).to have_selector ".loading-bar.errors"
end
scenario "Should update document title with attachment original file name after file selection if no title defined by user", :js do
login_as documentable.author
visit new_document_path(documentable_type: documentable.class.name,
documentable_id: documentable.id)
attach_file :document_attachment, "spec/fixtures/files/empty.pdf", make_visible: true
sleep 1
expect(find("input[name='document[title]']").value).to eq("empty.pdf")
end
scenario "Should not update document title with attachment original file name after file selection when title already defined by user", :js do
login_as documentable.author
visit new_document_path(documentable_type: documentable.class.name,
documentable_id: documentable.id)
fill_in :document_title, with: "My custom title"
attach_file :document_attachment, "spec/fixtures/files/empty.pdf", make_visible: true
sleep 1
expect(find("input[name='document[title]']").value).to eq("My custom title")
end
scenario "Should update document cached_attachment field after valid file upload", :js do
login_as documentable.author
visit new_document_path(documentable_type: documentable.class.name,
documentable_id: documentable.id)
attach_file :document_attachment, "spec/fixtures/files/empty.pdf", make_visible: true
sleep 1
expect(find("input[name='document[cached_attachment]']", visible: false).value).to include("empty.pdf")
end
scenario "Should not update document cached_attachment field after unvalid file upload", :js do
login_as documentable.author
visit new_document_path(documentable_type: documentable.class.name,
documentable_id: documentable.id)
attach_file :document_attachment, "spec/fixtures/files/logo_header.png", make_visible: true
sleep 1
expect(find("input[name='document[cached_attachment]']", visible: false).value).to eq ""
end
scenario "Should show documentable custom recomentations" do
login_as documentable.author
visit new_document_path(documentable_type: documentable.class.name,
documentable_id: documentable.id,
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 files up to #{max_file_size(documentable)} MB."
end
end
context "Create" do
scenario "Should show validation errors" do
login_as documentable.author
visit new_document_path(documentable_type: documentable.class.name,
documentable_id: documentable.id)
click_on "Upload document"
expect(page).to have_content "2 errors prevented this Document 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 document upload" do
login_as documentable.author
visit new_document_path(documentable_type: documentable.class.name,
documentable_id: documentable.id,
from: send(documentable_path, arguments))
attach_file :document_attachment, "spec/fixtures/files/empty.pdf"
sleep 1
click_on "Upload document"
expect(page).to have_content "Cannot create document. Check form errors and try again."
end
scenario "Should show success notice after successfull document upload" do
login_as documentable.author
visit new_document_path(documentable_type: documentable.class.name,
documentable_id: documentable.id,
from: send(documentable_path, arguments))
fill_in :document_title, with: "Document title"
attach_file :document_attachment, "spec/fixtures/files/empty.pdf"
sleep 1
click_on "Upload document"
expect(page).to have_content "Document was created successfully."
end
scenario "Should redirect to documentable path after successfull document upload" do
login_as documentable.author
visit new_document_path(documentable_type: documentable.class.name,
documentable_id: documentable.id,
from: send(documentable_path, arguments))
fill_in :document_title, with: "Document title"
attach_file :document_attachment, "spec/fixtures/files/empty.pdf"
sleep 1
click_on "Upload document"
within "##{dom_id(documentable)}" do
expect(page).to have_selector "h1", text: documentable.title
end
end
scenario "Should show new document on documentable documents tab after successfull document upload" do
login_as documentable.author
visit new_document_path(documentable_type: documentable.class.name,
documentable_id: documentable.id,
from: send(documentable_path, arguments))
fill_in :document_title, with: "Document title"
attach_file :document_attachment, "spec/fixtures/files/empty.pdf"
sleep 1
click_on "Upload document"
expect(page).to have_link "Documents (1)"
within "#tab-documents" do
within "#document_#{Document.last.id}" do
expect(page).to have_content "Document title"
expect(page).to have_link "Dowload file"
expect(page).to have_link "Destroy"
end
end
end
end
context "Destroy" do
let!(:document) { create(:document, documentable: documentable, user: documentable.author) }
scenario "Should show success notice after successfull document upload" do
login_as documentable.author
visit send(documentable_path, arguments)
within "#tab-documents" do
within "#document_#{document.id}" do
click_on "Destroy"
end
end
expect(page).to have_content "Document was deleted successfully."
end
scenario "Should update documents tab count after successful deletion" do
login_as documentable.author
visit send(documentable_path, arguments)
within "#tab-documents" do
within "#document_#{document.id}" do
click_on "Destroy"
end
end
expect(page).to have_link "Documents (0)"
end
scenario "Should redirect to documentable path after successful deletion" do
login_as documentable.author
visit send(documentable_path, arguments)
within "#tab-documents" do
within "#document_#{document.id}" do
click_on "Destroy"
end
end
within "##{dom_id(documentable)}" do
expect(page).to have_selector "h1", text: documentable.title
end
end
end
end

View File

@@ -0,0 +1,265 @@
shared_examples "nested documentable" do |documentable_factory_name, path, documentable_path_arguments, fill_resource_method_name, submit_button, documentable_success_notice|
include ActionView::Helpers
include DocumentsHelper
include DocumentablesHelper
let!(:administrator) { create(:user) }
let!(:user) { create(:user, :level_two) }
let!(:arguments) { {} }
let!(:documentable) { create(documentable_factory_name, author: user) }
before do
create(:administrator, user: administrator)
if documentable_path_arguments
documentable_path_arguments.each do |argument_name, path_to_value|
arguments.merge!("#{argument_name}": documentable.send(path_to_value))
end
end
end
describe "at #{path}" do
scenario "Should show new document link when max documents allowed limit is not reached" do
login_as user
visit send(path, arguments)
expect(page).to have_selector "#new_document_link", visible: true
end
scenario "Should not show new document link when documentable max documents allowed limit is reached", :js do
login_as user
visit send(path, arguments)
click_link "Add new document"
sleep 1
click_link "Add new document"
sleep 1
click_link "Add new document"
expect(page).to have_selector "#new_document_link", visible: false
end
scenario "Should not show max documents warning when no documents added", :js do
login_as user
visit send(path, arguments)
expect(page).to have_selector ".max-documents-notice", visible: false
end
scenario "Should show max documents warning when max documents allowed limit is reached", :js do
login_as user
visit send(path, arguments)
click_link "Add new document"
sleep 1
click_link "Add new document"
sleep 1
click_link "Add new document"
expect(page).to have_selector ".max-documents-notice", visible: true
end
scenario "Should hide max documents warning after any document removal", :js do
login_as user
visit send(path, arguments)
click_link "Add new document"
sleep 1
click_link "Add new document"
sleep 1
click_link "Add new document"
sleep 1
within "#document_0" do
find("a", text: "Remove document").click
end
sleep 1
expect(page).to have_selector ".max-documents-notice", visible: false
end
scenario "Should update nested document file name after choosing a file", :js do
login_as user
visit send(path, arguments)
attach_new_file(documentable_factory_name, 0, "spec/fixtures/files/empty.pdf")
expect(page).to have_selector ".file-name", text: "empty.pdf"
end
scenario "Should update nested document file title with file name after choosing a file when no title defined", :js do
login_as user
visit send(path, arguments)
attach_new_file(documentable_factory_name, 0, "spec/fixtures/files/empty.pdf")
expect(find("##{documentable_factory_name}_documents_attributes_0_title").value).to eq "empty.pdf"
end
scenario "Should not update nested document file title with file name after choosing a file when title already defined", :js do
login_as user
visit send(path, arguments)
click_link "Add new document"
sleep 1
fill_in "#{documentable_factory_name}[documents_attributes][0][title]", with: "Title"
attach_file("#{documentable_factory_name}[documents_attributes][0][attachment]", "spec/fixtures/files/empty.pdf", make_visible: true)
sleep 1
expect(find("##{documentable_factory_name}_documents_attributes_0_title").value).to eq "Title"
end
scenario "Should update loading bar style after valid file upload", :js do
login_as user
visit send(path, arguments)
attach_new_file(documentable_factory_name, 0, "spec/fixtures/files/empty.pdf")
fill_in "#{documentable_factory_name}[documents_attributes][0][title]", with: "Title"
expect(page).to have_selector ".loading-bar.complete"
end
scenario "Should update loading bar style after unvalid file upload", :js do
login_as user
visit send(path, arguments)
attach_new_file(documentable_factory_name, 0, "spec/fixtures/files/logo_header.png")
expect(page).to have_selector ".loading-bar.errors"
end
scenario "Should update document cached_attachment field after valid file upload", :js do
login_as user
visit send(path, arguments)
attach_new_file(documentable_factory_name, 0, "spec/fixtures/files/empty.pdf")
expect(find("input[name='#{documentable_factory_name}[documents_attributes][0][cached_attachment]']", visible: false).value).to include("empty.pdf")
end
scenario "Should not update document cached_attachment field after unvalid file upload", :js do
login_as user
visit send(path, arguments)
attach_new_file(documentable_factory_name, 0, "spec/fixtures/files/logo_header.png")
expect(find("input[name='#{documentable_factory_name}[documents_attributes][0][cached_attachment]']", visible: false).value).to eq ""
end
scenario "Should show document errors after unvalid file upload", :js do
login_as user
visit send(path, arguments)
click_link "Add new document"
sleep 1
click_on submit_button
within "#document_0" do
expect(page).to have_content("can't be blank", count: 2)
end
end
scenario "Should delete document after valid file upload and click on remove button", :js do
login_as user
visit send(path, arguments)
attach_new_file(documentable_factory_name, 0, "spec/fixtures/files/empty.pdf")
within "#document_0" do
click_link "Remove document"
end
expect(page).not_to have_selector("#document_0")
end
scenario "Should delete document after valid file upload and click on remove button", :js do
login_as user
visit send(path, arguments)
attach_new_file(documentable_factory_name, 0, "spec/fixtures/files/empty.pdf")
within "#document_0" do
click_link "Remove document"
end
expect(page).to have_content "Document was deleted successfully."
end
scenario "Should show successful notice when resource filled correctly without any nested documents", :js do
login_as user
visit send(path, arguments)
send(fill_resource_method_name) if fill_resource_method_name
click_on submit_button
expect(page).to have_content documentable_success_notice
end
scenario "Should show successful notice when resource filled correctly and after valid file uploads", :js do
login_as user
visit send(path, arguments)
send(fill_resource_method_name) if fill_resource_method_name
attach_new_file(documentable_factory_name, 0, "spec/fixtures/files/empty.pdf")
click_on submit_button
expect(page).to have_content documentable_success_notice
end
scenario "Should show new document after successful creation with one uploaded file", :js do
login_as user
visit send(path, arguments)
send(fill_resource_method_name) if fill_resource_method_name
attach_new_file(documentable_factory_name, 0, "spec/fixtures/files/empty.pdf")
click_on submit_button
redirected_to_resource_show_or_navigate_to
expect(page).to have_content "Documents (1)"
end
scenario "Should show resource with new document after successful creation with maximum allowed uploaded files", :js do
login_as user
visit send(path, arguments)
send(fill_resource_method_name) if fill_resource_method_name
documentable.class.max_documents_allowed.times.each do |index|
attach_new_file(documentable_factory_name, index , "spec/fixtures/files/empty.pdf")
end
click_on submit_button
redirected_to_resource_show_or_navigate_to
expect(page).to have_content "Documents (#{documentable.class.max_documents_allowed})"
end
end
end
def redirected_to_resource_show_or_navigate_to
find("a", text: "Not now, go to my proposal")
click_on "Not now, go to my proposal"
rescue
return
end
def attach_new_file(documentable_factory_name, index, path)
click_link "Add new document"
sleep 1
attach_file("#{documentable_factory_name}[documents_attributes][#{index}][attachment]", path, make_visible: true)
sleep 1
end
def fill_new_valid_proposal
fill_in :proposal_title, with: "Proposal title"
fill_in :proposal_summary, with: "Proposal summary"
fill_in :proposal_question, with: "Proposal question?"
check :proposal_terms_of_service
end
def fill_new_valid_budget_investment
page.select documentable.heading.name_scoped_by_group, from: :budget_investment_heading_id
fill_in :budget_investment_title, with: "Budget investment title"
fill_in_ckeditor "budget_investment_description", with: "Budget investment description"
check :budget_investment_terms_of_service
end

View File

@@ -0,0 +1,62 @@
shared_examples "document validations" do |documentable_factory|
include DocumentsHelper
include DocumentablesHelper
let!(:document) { build(:document, documentable_factory.to_sym) }
let!(:documentable) { document.documentable }
let!(:maxfilesize) { max_file_size(document.documentable) }
let!(:acceptedcontenttypes) { accepted_content_types(document.documentable) }
it "should be valid" do
expect(document).to be_valid
end
it "should not be valid without a title" do
document.title = nil
expect(document).to_not be_valid
end
it "should not be valid without an attachment" do
document.attachment = nil
expect(document).to_not be_valid
end
it "should be valid for all accepted content types" do
acceptedcontenttypes.each do |content_type|
extension = content_type.split("/").last
document.attachment = File.new("spec/fixtures/files/empty.#{extension}")
expect(document).to be_valid
end
end
it "should not be valid for attachments larger than documentable max_file_size definition" do
document.stub(:attachment_file_size).and_return(maxfilesize.megabytes + 1.byte)
expect(document).to_not be_valid
expect(document.errors[:attachment]).to include "must be in between 0 Bytes and #{maxfilesize} MB"
end
it "should not be valid without a user_id" do
document.user_id = nil
expect(document).to_not be_valid
end
it "should not be valid without a documentable_id" do
document.save
document.documentable_id = nil
expect(document).to_not be_valid
end
it "should not be valid without a documentable_type" do
document.save
document.documentable_type = nil
expect(document).to_not be_valid
end
end