diff --git a/CUSTOMIZE_EN.md b/CUSTOMIZE_EN.md index 542d11ee72f..3a97b96deb4 100644 --- a/CUSTOMIZE_EN.md +++ b/CUSTOMIZE_EN.md @@ -1,12 +1,12 @@ # Customization -You can modify your own Consul to have your custom visual style, but first you'll have to create a fork from [https://github.com/consul/consul](https://github.com/consul/consul) using Github's "fork" button on top right corner. You can use any other service like Gitlab, but don't forget to put a reference link back to Consul on the footer to comply with project's license (GPL Affero 3). +You can modify your own CONSUL to have your custom visual style, but first you'll have to create a fork from [https://github.com/consul/consul](https://github.com/consul/consul) using Github's "fork" button on top right corner. You can use any other service like Gitlab, but don't forget to put a reference link back to CONSUL on the footer to comply with project's license (GPL Affero 3). -We've created an specific structure where you can overwrite and customize the application in a way that will let you keep updating it from Consul's main repository, without having conflicts on code merging or risking loosing your customization changes. We try to make Consul as vanilla as possible to help other developers onboard the codebase. +We've created an specific structure where you can overwrite and customize the application in a way that will let you keep updating it from CONSUL's main repository, without having conflicts on code merging or risking loosing your customization changes. We try to make CONSUL as vanilla as possible to help other developers onboard the codebase. ## Special Folders and Files -In order to customize your Consul fork, you'll make use of some `custom` folders on the following paths: +In order to customize your CONSUL fork, you'll make use of some `custom` folders on the following paths: * `config/locales/custom/` * `app/assets/images/custom/` @@ -207,7 +207,7 @@ TODO ## Updating -We recommend you to add consul as remote: +We recommend you to add CONSUL as remote: ``` git remote add consul https://github.com/consul/consul diff --git a/CUSTOMIZE_ES.md b/CUSTOMIZE_ES.md index e5bea19ad93..26204c4e439 100644 --- a/CUSTOMIZE_ES.md +++ b/CUSTOMIZE_ES.md @@ -1,12 +1,12 @@ # Personalización -Puedes modificar consul y ponerle tu propia imagen, para esto debes primero hacer un fork de [https://github.com/consul/consul](https://github.com/consul/consul) creando un repositorio nuevo en Github. Puedes usar otro servicio como Gitlab, pero no te olvides de poner el enlace en el footer a tu repositorio en cumplimiento con la licencia de este proyecto (GPL Affero 3). +Puedes modificar CONSUL y ponerle tu propia imagen, para esto debes primero hacer un fork de [https://github.com/consul/consul](https://github.com/consul/consul) creando un repositorio nuevo en Github. Puedes usar otro servicio como Gitlab, pero no te olvides de poner el enlace en el footer a tu repositorio en cumplimiento con la licencia de este proyecto (GPL Affero 3). -Hemos creado una estructura específica donde puedes sobreescribir y personalizar la aplicación para que puedas actualizar sin que tengas problemas al hacer merge y se sobreescriban por error tus cambios. Intentamos que Consul sea una aplicación Ruby on Rails lo más plain vanilla posible para facilitar el acceso de nuevas desarrolladoras. +Hemos creado una estructura específica donde puedes sobreescribir y personalizar la aplicación para que puedas actualizar sin que tengas problemas al hacer merge y se sobreescriban por error tus cambios. Intentamos que CONSUL sea una aplicación Ruby on Rails lo más plain vanilla posible para facilitar el acceso de nuevas desarrolladoras. ## Ficheros y directorios especiales -Para adaptarlo puedes hacerlo a través de los directorios que están en custom dentro de: +Para adaptar tu fork de CONSUL puedes utilizar alguno de los directorios `custom` que están en las rutas: * `config/locales/custom/` * `app/assets/images/custom/` @@ -207,7 +207,7 @@ TODO ## Actualizar -Te recomendamos que agregues el remote de consul para facilitar este proceso de merge: +Te recomendamos que agregues el remote de CONSUL para facilitar este proceso de merge: ``` git remote add consul https://github.com/consul/consul diff --git a/Gemfile b/Gemfile index 699e321690e..0c315da9ffa 100644 --- a/Gemfile +++ b/Gemfile @@ -33,8 +33,9 @@ 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', '~> 0.21.0' gem 'pg_search', '~> 2.0.1' gem 'rails-assets-markdown-it', '~> 8.2.1', source: 'https://rails-assets.org' gem 'redcarpet', '~> 3.4.0' @@ -71,7 +72,7 @@ end group :test do gem 'capybara', '~> 2.14.0' gem 'coveralls', '~> 0.8.21', require: false - gem 'database_cleaner', '~> 1.5.3' + gem 'database_cleaner', '~> 1.6.1' gem 'email_spec', '~> 2.1.0' gem 'poltergeist', '~> 1.15.0' gem 'rspec-rails', '~> 3.6' @@ -84,7 +85,7 @@ group :development do gem 'capistrano3-delayed-job', '~> 1.7.3' gem 'mdl', '~> 0.4.0', require: false gem 'rvm1-capistrano3', '~> 1.4.0', require: false - gem 'scss_lint', '~> 0.53.0', require: false + gem 'scss_lint', '~> 0.54.0', require: false gem 'web-console', '~> 3.3.0' end diff --git a/Gemfile.lock b/Gemfile.lock index e166df53fb1..7ed9436eb13 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -123,7 +123,7 @@ GEM tins (~> 1.6) daemons (1.2.4) dalli (2.7.6) - database_cleaner (1.5.3) + database_cleaner (1.6.1) debug_inspector (0.0.2) delayed_job (4.1.2) activesupport (>= 3.0, < 5.1) @@ -205,6 +205,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) @@ -302,7 +306,7 @@ GEM activerecord (>= 4.0, < 5.2) parser (2.4.0.0) ast (~> 2.2) - pg (0.20.0) + pg (0.21.0) pg_search (2.0.1) activerecord (>= 4.2) activesupport (>= 4.2) @@ -406,7 +410,7 @@ GEM nokogiri (>= 1.4.0) nori (~> 2.4) wasabi (~> 3.4) - scss_lint (0.53.0) + scss_lint (0.54.0) rake (>= 0.9, < 13) sass (~> 3.4.20) selenium-webdriver (3.0.5) @@ -511,7 +515,7 @@ DEPENDENCIES coveralls (~> 0.8.21) daemons (~> 1.2.4) dalli (~> 2.7.6) - database_cleaner (~> 1.5.3) + database_cleaner (~> 1.6.1) delayed_job_active_record (~> 4.1.0) devise (~> 3.5.7) devise-async (~> 0.10.2) @@ -527,6 +531,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) @@ -541,7 +546,7 @@ DEPENDENCIES omniauth-twitter (~> 1.4.0) paperclip (~> 5.1.0) paranoia (~> 2.3.1) - pg (~> 0.20.0) + pg (~> 0.21.0) pg_search (~> 2.0.1) poltergeist (~> 1.15.0) quiet_assets (~> 1.1.0) @@ -557,7 +562,7 @@ DEPENDENCIES rvm1-capistrano3 (~> 1.4.0) sass-rails (~> 5.0, >= 5.0.4) savon (~> 2.11.1) - scss_lint (~> 0.53.0) + scss_lint (~> 0.54.0) selenium-webdriver (~> 3.0.5) sitemap_generator (~> 5.3.1) social-share-button (~> 0.10) @@ -573,4 +578,4 @@ DEPENDENCIES whenever (~> 0.9.7) BUNDLED WITH - 1.15.1 + 1.15.4 diff --git a/README.md b/README.md index 2027d213d12..5c96ce70f80 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![Logo of Consul](https://raw.githubusercontent.com/consul/consul/master/public/consul_logo.png) -# Consul +# CONSUL Citizen Participation and Open Government Application @@ -21,7 +21,7 @@ Citizen Participation and Open Government Application [![Join the chat at https://gitter.im/consul/consul](https://badges.gitter.im/consul/consul.svg)](https://gitter.im/consul/consul?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://github.com/consul/consul/issues?q=is%3Aissue+is%3Aopen+label%3APRs-welcome) -This is the opensource code repository of the eParticipation website originally developed for the Madrid City government eParticipation website +This is the opensource code repository of the eParticipation website CONSUL, originally developed for the Madrid City government eParticipation website ## Current state @@ -108,3 +108,7 @@ Code published under AFFERO GPL v3 (see [LICENSE-AGPLv3.txt](LICENSE-AGPLv3.txt) ## Contributions See [CONTRIBUTING.md](CONTRIBUTING.md) + +## Brand guidelines + +If you want to use CONSUL logo you can [download the guidelines](https://raw.githubusercontent.com/consul/consul/master/public/consul_brand.zip) which contains a use guide and different versions and sizes of the logo. diff --git a/README_ES.md b/README_ES.md index 202d452b3ff..de2402d1bd0 100644 --- a/README_ES.md +++ b/README_ES.md @@ -2,7 +2,7 @@ ![Logotipo de Consul](https://raw.githubusercontent.com/consul/consul/master/public/consul_logo.png) -# Consul +# CONSUL Aplicación de Participación Ciudadana y Gobierno Abierto @@ -21,7 +21,7 @@ Aplicación de Participación Ciudadana y Gobierno Abierto [![Join the chat at https://gitter.im/consul/consul](https://badges.gitter.im/consul/consul.svg)](https://gitter.im/consul/consul?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://github.com/consul/consul/issues?q=is%3Aissue+is%3Aopen+label%3APRs-welcome) -Este es el repositorio de código abierto de la Aplicación de Participación Ciudadana Consul, creada originariamente por el Ayuntamiento de Madrid. +Este es el repositorio de código abierto de la Aplicación de Participación Ciudadana CONSUL, creada originariamente por el Ayuntamiento de Madrid. ## Estado del proyecto @@ -105,3 +105,7 @@ El código de este proyecto está publicado bajo la licencia AFFERO GPL v3 (ver ## Contribuciones Ver fichero [CONTRIBUTING_ES.md](CONTRIBUTING_ES.md) + +## Guía de estilo + +Si quieres usar el logo de CONSUL puedes [descargar la guía de estilo](https://raw.githubusercontent.com/consul/consul/master/public/consul_brand.zip) que contiene una guía de uso y diferentes versiones y tamaños del logo. diff --git a/app/assets/images/help/help_icon_budgets.png b/app/assets/images/help/help_icon_budgets.png new file mode 100644 index 00000000000..fc5e3022fc4 Binary files /dev/null and b/app/assets/images/help/help_icon_budgets.png differ diff --git a/app/assets/images/help/help_icon_debates.png b/app/assets/images/help/help_icon_debates.png new file mode 100644 index 00000000000..afc72967125 Binary files /dev/null and b/app/assets/images/help/help_icon_debates.png differ diff --git a/app/assets/images/help/help_icon_legislation_processes.png b/app/assets/images/help/help_icon_legislation_processes.png new file mode 100644 index 00000000000..00872c24770 Binary files /dev/null and b/app/assets/images/help/help_icon_legislation_processes.png differ diff --git a/app/assets/images/help/help_icon_polls.png b/app/assets/images/help/help_icon_polls.png new file mode 100644 index 00000000000..b7f7cf47955 Binary files /dev/null and b/app/assets/images/help/help_icon_polls.png differ diff --git a/app/assets/images/help/help_icon_proposals.png b/app/assets/images/help/help_icon_proposals.png new file mode 100644 index 00000000000..e41db1cf8fe Binary files /dev/null and b/app/assets/images/help/help_icon_proposals.png differ diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 2436d8f2a73..cdd5f7db439 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -15,6 +15,7 @@ //= require jquery-ui/widgets/datepicker //= require jquery-ui/i18n/datepicker-es //= require jquery-ui/effects/effect-shake +//= require jquery-fileupload/basic //= require foundation //= require turbolinks //= require ckeditor/loader @@ -62,6 +63,7 @@ //= require legislation_annotatable //= require watch_form_changes //= require followable +//= require documentable //= require tree_navigator //= require custom @@ -97,6 +99,7 @@ var initialize_modules = function() { App.LegislationAnnotatable.initialize(); App.WatchFormChanges.initialize(); App.TreeNavigator.initialize(); + App.Documentable.initialize(); }; $(function(){ @@ -105,4 +108,4 @@ $(function(){ $(document).ready(initialize_modules); $(document).on('page:load', initialize_modules); $(document).on('ajax:complete', initialize_modules); -}); \ No newline at end of file +}); diff --git a/app/assets/javascripts/documentable.js.coffee b/app/assets/javascripts/documentable.js.coffee new file mode 100644 index 00000000000..8683ce5e220 --- /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/javascripts/embed_video.js.coffee b/app/assets/javascripts/embed_video.js.coffee index 85c18ac8e40..0825d1b80dd 100644 --- a/app/assets/javascripts/embed_video.js.coffee +++ b/app/assets/javascripts/embed_video.js.coffee @@ -1,7 +1,6 @@ App.EmbedVideo = - + initialize: -> $('#js-embedded-video').each -> - code = $(this).data("video-code") + code = $(this).data("video-code") $('#js-embedded-video').html(code) - \ No newline at end of file diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index c042c754cec..5a8ef6b003f 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -10,6 +10,7 @@ @import 'pages'; @import 'legislation'; @import 'legislation_process'; +@import 'community'; @import 'custom'; @import 'custom/*'; @import 'c3'; @@ -17,3 +18,4 @@ @import 'annotator_overrides'; @import 'jquery-ui/datepicker'; @import 'datepicker_overrides'; +@import 'documentable'; diff --git a/app/assets/stylesheets/community.scss b/app/assets/stylesheets/community.scss new file mode 100644 index 00000000000..0dddf4cdb58 --- /dev/null +++ b/app/assets/stylesheets/community.scss @@ -0,0 +1,44 @@ +.communities-show { + .button.disabled, .button[disabled] { + pointer-events: none; + } + + .wide-order-selector { + margin-top: 0; + } + + .panel { + min-height: auto; + margin: 0.375rem 0; + + .button { + margin-top: $line-height; + } + } +} + +.communities-participant { + + .comment-body { + display: inline-block; + float: left; + margin-right: $line-height; + margin-bottom: $line-height; + } +} + +.topic-show { + + p, + ul li { + margin-bottom: 0; + } + + .comments { + + .first-comment { + margin-top: $line-height; + margin-bottom: $line-height; + } + } +} diff --git a/app/assets/stylesheets/documentable.scss b/app/assets/stylesheets/documentable.scss new file mode 100644 index 00000000000..2aa015a1406 --- /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 782b5cdb279..5e92ee460b1 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 04283a41861..0b967f85a01 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; } @@ -199,7 +205,7 @@ a { .menu.simple { border-bottom: 1px solid $border; - margin: $line-height 0; + margin-bottom: $line-height; li { padding-bottom: rem-calc(7); @@ -326,6 +332,10 @@ a { text-transform: uppercase; } +.align-top { + vertical-align: top; +} + // 02. Header // ---------- @@ -2137,3 +2147,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/legislation_process.scss b/app/assets/stylesheets/legislation_process.scss index eda341185a8..2ec9f8ee914 100644 --- a/app/assets/stylesheets/legislation_process.scss +++ b/app/assets/stylesheets/legislation_process.scss @@ -13,37 +13,29 @@ // // 01. Utils -// ----------------- +// --------- + +$grey-heading: #e6e6e6; +$border-dark: darken($border, 10%); .grey-heading { - background: #e6e6e6; + background: $grey-heading; } -$epigraph-font-size: rem-calc(15); -$epigraph-line-height: rem-calc(22); - // 02. Hero -// ----------------- -.legislation-hero { - padding-top: 1.5rem; +// -------- - @include breakpoint(medium) { - padding-top: 3.5rem; - } - - h4 { - text-transform: uppercase; - } +.legislation-hero { ul { list-style: none; margin-left: 0; li::before { - vertical-align: text-bottom; - padding-right: 0.5rem; - content: '■'; color: #8aa8be; + content: '■'; + padding-right: $line-height / 4; + vertical-align: text-bottom; } } @@ -52,79 +44,33 @@ $epigraph-line-height: rem-calc(22); } .debate-add-info { - margin-top: 3rem; - padding-top: 4rem; - border-top: 1px solid darken($border, 10%); - - @include breakpoint(medium) { - margin-bottom: 2rem; - } + border-top: 1px solid $border-dark; + margin-top: $line-height; + padding-top: $line-height; - .debate-info-wrapper { - - h2 { - font-size: $lead-font-size; - - @include breakpoint(medium) { - float: left; - } - } - } - } - - .half-gradient { - background: #e6e6e6; - background: linear-gradient(to bottom, #e6e6e6 0%, #e6e6e6 50%, #fff 50%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#e6e6e6', endColorstr='#fff', GradientType=0); } - .text-center .button { - background: $brand; - margin-bottom: 0; - } - - .headline { - margin-bottom: 1rem; - - @include breakpoint(medium) { - margin-bottom: 4rem; - - } + .title { + font-weight: bold; + text-transform: uppercase; } .description { - margin-bottom: 1rem; - - p { - font-size: $epigraph-font-size; - line-height: $epigraph-line-height; - } - - ul { - font-size: $epigraph-font-size; - line-height: $epigraph-line-height; - } li { - margin-bottom: 1rem; p { display: inline; - margin-bottom: 0; } } - - h4 { - font-size: $base-font-size; - } } .button-subscribe { - margin-top: 1rem; + margin-top: $line-height; @include breakpoint(medium) { + margin-top: $line-height * 2; padding: 0.5em 1em; - margin-top: 3rem; } h3 { @@ -132,8 +78,8 @@ $epigraph-line-height: rem-calc(22); } p { - margin-bottom: 0; font-size: $small-font-size; + margin-bottom: 0; } &:hover h3 { @@ -143,41 +89,21 @@ $epigraph-line-height: rem-calc(22); } // 03. Legislation process navigation -// ----------------- +// ---------------------------------- + .legislation-process-categories { position: relative; .legislation-process-list { border-bottom: 1px solid $medium-gray; margin: 0 1rem 1rem; - padding-top: 4rem; - - @include breakpoint(medium) { - margin-left: 0; - } ul { - position: relative; - max-width: 75rem; - margin-left: auto; - margin-right: auto; + list-style: none; - padding-left: 0; + margin: 0 auto; margin-bottom: 0; - - @include breakpoint(medium) { - padding-left: 1rem; - } - - svg { - position: absolute; - top: 1.25rem; - - @include breakpoint(1280px) { - transform: rotate(-6deg); - left: -1rem; - } - } + padding-left: 0; } li { @@ -187,6 +113,10 @@ $epigraph-line-height: rem-calc(22); transition: all 0.4s; border-bottom: 2px solid transparent; + @include breakpoint(medium) { + margin-left: $line-height * 2; + } + &:first-of-type { margin-left: 0; } @@ -197,29 +127,25 @@ $epigraph-line-height: rem-calc(22); border-bottom: 2px solid $brand; } - @media (min-width: 950px) { - margin: 0 0 0 3rem; - } - a, h4 { display: block; color: #6d6d6d; margin-bottom: 0; } + } - a { - &:hover, - &:active { - text-decoration: none; - } + a { + &:hover, + &:active { + text-decoration: none; + } - p { - margin-bottom: 0; + p { + margin-bottom: 0; - @include breakpoint(medium) { - margin-bottom: 1rem; - } + @include breakpoint(medium) { + margin-bottom: 1rem; } } } @@ -231,7 +157,8 @@ $epigraph-line-height: rem-calc(22); } // 04. Debate list -// ----------------- +// ---------------- + .debate-chooser { padding: 2rem 1rem; @@ -277,38 +204,43 @@ $epigraph-line-height: rem-calc(22); } // 05. Debate quiz -// ----------------- +// --------------- + .debate-questions { + .comments { - margin-top: 4rem; + margin-top: $line-height * 2.5; } .quiz-header { - margin-bottom: 2rem; + margin-bottom: $line-height; .quiz-title, .quiz-next { - padding: 1rem; - height: 6rem; + padding: $line-height; + + @include breakpoint(medium) { + height: $line-height * 4; + } } .quiz-title { background: #e5ecf2; .quiz-header-title { + font-size: $small-font-size; + font-weight: 700; margin-bottom: 0; text-transform: uppercase; - font-weight: 700; - font-size: $small-font-size; } } h4 a { color: $brand; - } - h4 a:hover { - text-decoration: none; + &:hover { + text-decoration: none; + } } .quiz-next-link { @@ -318,57 +250,49 @@ $epigraph-line-height: rem-calc(22); &:active { text-decoration: none; } + } - .quiz-next { - background: #ccdbe5; - font-weight: 700; - color: $brand; - font-size: $small-font-size; - text-align: right; - text-transform: uppercase; - transition: background 0.25s ease-out, background 0.25s ease-out; - - .icon-angle-right { - vertical-align: sub; - } + .quiz-next { + background: #ccdbe5; + color: $brand; + font-size: $small-font-size; + font-weight: bold; + text-align: right; + text-transform: uppercase; + transition: background 0.25s ease-out, background 0.25s ease-out; - &:hover, - &:active { - text-decoration: none; - background: $brand; - color: #fff; + .icon-angle-right { + vertical-align: middle; + } - .icon-angle-right { - color: #fff; - } - } + &:hover, + &:active { + background: $brand; + color: #fff; + text-decoration: none; } } } .quiz-question { - margin-bottom: 2rem; + margin-bottom: $line-height; } .debate-questions { position: relative; list-style: none; - .participation-not-allowed { - padding-bottom: 3rem; - } - .control { - position: relative; - display: inline-block; - color: #555; - cursor: pointer; background: #fff; border: 1px solid $border; - border-radius: 4px; - padding: 0.75rem 2.5rem; - margin-right: 1.5rem; - margin-bottom: 0.5rem; + border-radius: rem-calc(4); + color: #555; + cursor: pointer; + display: inline-block; + margin-bottom: $line-height / 2; + margin-right: $line-height; + padding: $line-height / 2 $line-height * 2; + position: relative; } .active { @@ -409,14 +333,15 @@ $epigraph-line-height: rem-calc(22); } // 06. Legislation draft -// ----------------- +// --------------------- + .debate-draft { padding: 10rem 2rem 15rem; display: block; background: #f2f2f2; button { - height: 90px; + height: rem-calc(90); h3 { margin-bottom: 0; @@ -430,7 +355,8 @@ $epigraph-line-height: rem-calc(22); } // 07. Legislation allegations -// ----------------- +// --------------------------- + .legislation-allegation { padding-top: 1rem; @@ -449,12 +375,12 @@ $epigraph-line-height: rem-calc(22); .button-circle { line-height: 0; padding: 0; - width: 30px; - height: 30px; + width: rem-calc(30); + height: rem-calc(30); border-radius: 50%; span { - padding-left: 1px; + padding-left: rem-calc(1); &::before { line-height: 1.55; @@ -580,7 +506,7 @@ $epigraph-line-height: rem-calc(22); .calc-comments { cursor: pointer; background: #f2f2f2; - width: 50px; + width: rem-calc(50); .draft-panel { .panel-title { @@ -733,7 +659,7 @@ $epigraph-line-height: rem-calc(22); .comments-on { .calc-index { - width: 50px; + width: rem-calc(50); background: #f2f2f2; cursor: pointer; @@ -797,11 +723,11 @@ $epigraph-line-height: rem-calc(22); .comments-box-container { position: absolute; - top: 230px; + top: rem-calc(230); } .comment-box { - width: 375px; + width: rem-calc(375); padding: 1rem; background: #f9f9f9; border: 1px solid $border; @@ -852,7 +778,7 @@ $epigraph-line-height: rem-calc(22); .participation-not-allowed { font-size: 0.875rem; - height: 50px; + height: rem-calc(50); padding: 0.85rem 0.75rem; top: -18px; } @@ -891,7 +817,7 @@ $epigraph-line-height: rem-calc(22); border-right: 1px solid #d0d0d0; border-left: 1px solid #d0d0d0; width: 100%; - height: 200px; + height: rem-calc(200); margin-bottom: 0.5rem; } diff --git a/app/assets/stylesheets/pages.scss b/app/assets/stylesheets/pages.scss index d3db7e9b2a1..7114b01d628 100644 --- a/app/assets/stylesheets/pages.scss +++ b/app/assets/stylesheets/pages.scss @@ -44,7 +44,8 @@ // 03. Content // ---------------------- -.more-info-content { +.more-info-content, +.communities-show { h3 { color: $brand; diff --git a/app/assets/stylesheets/participation.scss b/app/assets/stylesheets/participation.scss index 8785aa33930..f21e996b1e0 100644 --- a/app/assets/stylesheets/participation.scss +++ b/app/assets/stylesheets/participation.scss @@ -249,11 +249,15 @@ .debate-form, .proposal-form, .budget-investment-form, -.spending-proposal-form { +.spending-proposal-form, +.document-form, +.topic-new, +.topic-form { .icon-debates, .icon-proposals, - .icon-budget { + .icon-budget, + .icon-documents { font-size: rem-calc(50); line-height: $line-height; opacity: 0.5; @@ -263,7 +267,8 @@ color: $debates; } - .icon-proposals { + .icon-proposals, + .icon-documents { color: $proposals; } @@ -295,7 +300,10 @@ } } -.proposal-form { +.proposal-form, +.topic-form, +.topic-new, +.document-form { .recommendations li::before { color: $proposals; @@ -313,7 +321,9 @@ .debate-quiz, .budget-investment-show, .draft-panels, -.debate-questions { +.debate-questions, +.communities-show, +.topic-show { p { word-wrap: break-word; @@ -347,7 +357,8 @@ .debate-info, .proposal-info, .investment-project-info, - .budget-investment-show { + .budget-investment-show, + .topic-info { clear: both; color: $text-medium; font-size: $small-font-size; @@ -624,7 +635,8 @@ .proposal, .investment-project, .budget-investment, -.legislation { +.legislation, +.communities-show { margin: $line-height / 4 0; .panel { @@ -696,7 +708,8 @@ .debate-info, .proposal-info, .investment-project-info, - .budget-investment-info { + .budget-investment-info, + .topic-info { color: $text-medium; font-size: $small-font-size; margin: rem-calc(6) 0 0; @@ -747,6 +760,12 @@ display: none; } +.document-form{ + max-width: 75rem; + margin-left: auto; + margin-right: auto; +} + .more-info { clear: both; color: $text-medium; @@ -876,6 +895,18 @@ } } +.help-header { + background: #fafafa; + border-bottom: 1px solid #eee; + padding-bottom: $line-height / 2; + padding-top: $line-height; + + h1 { + font-size: rem-calc(24); + text-transform: uppercase; + } +} + // 05. Featured // ------------ @@ -1482,13 +1513,9 @@ .poll, .poll-question { - background: #fff; - border-radius: rem-calc(6); + border: 1px solid $border; margin-bottom: $line-height / 2; -} - -.poll { - padding: $line-height; + padding: $line-height / 2; position: relative; .icon-poll-answer { diff --git a/app/controllers/admin/budget_investment_milestones_controller.rb b/app/controllers/admin/budget_investment_milestones_controller.rb index a4b8acc5d4a..fb5f4238475 100644 --- a/app/controllers/admin/budget_investment_milestones_controller.rb +++ b/app/controllers/admin/budget_investment_milestones_controller.rb @@ -48,7 +48,7 @@ def load_budget_investment end def load_budget_investment_milestone - @milestone = Budget::Investment::Milestone.find params[:id] + @milestone = Budget::Investment::Milestone.find params[:id] end diff --git a/app/controllers/admin/poll/officer_assignments_controller.rb b/app/controllers/admin/poll/officer_assignments_controller.rb index 1f2a7ca2c77..45c9a225a7b 100644 --- a/app/controllers/admin/poll/officer_assignments_controller.rb +++ b/app/controllers/admin/poll/officer_assignments_controller.rb @@ -32,35 +32,6 @@ def search_officers end end - def create - @officer_assignment = ::Poll::OfficerAssignment.new(booth_assignment: @booth_assignment, - officer_id: create_params[:officer_id], - date: create_params[:date]) - @officer_assignment.final = true if @officer_assignment.date > @booth_assignment.poll.ends_at.to_date - - if @officer_assignment.save - notice = t("admin.poll_officer_assignments.flash.create") - else - notice = t("admin.poll_officer_assignments.flash.error_create") - end - - redirect_params = { poll_id: create_params[:poll_id], officer_id: create_params[:officer_id] } - redirect_to by_officer_admin_poll_officer_assignments_path(redirect_params), notice: notice - end - - def destroy - @officer_assignment = ::Poll::OfficerAssignment.includes(:booth_assignment).find(params[:id]) - - if @officer_assignment.destroy - notice = t("admin.poll_officer_assignments.flash.destroy") - else - notice = t("admin.poll_officer_assignments.flash.error_destroy") - end - - redirect_params = { poll_id: @officer_assignment.poll_id, officer_id: @officer_assignment.officer_id } - redirect_to by_officer_admin_poll_officer_assignments_path(redirect_params), notice: notice - end - private def officer_assignment_params diff --git a/app/controllers/admin/poll/shifts_controller.rb b/app/controllers/admin/poll/shifts_controller.rb new file mode 100644 index 00000000000..8a808a7a9aa --- /dev/null +++ b/app/controllers/admin/poll/shifts_controller.rb @@ -0,0 +1,53 @@ +class Admin::Poll::ShiftsController < Admin::BaseController + + before_action :load_booth + before_action :load_polls + + def new + load_officers + load_shifts + @shift = ::Poll::Shift.new + end + + def create + @shift = ::Poll::Shift.new(shift_params) + if @shift.save + notice = t("admin.poll_shifts.flash.create") + redirect_to new_admin_booth_shift_path(@shift.booth), notice: notice + else + load_officers + load_shifts + render :new + end + end + + def destroy + @shift = Poll::Shift.find(params[:id]) + @shift.destroy + notice = t("admin.poll_shifts.flash.destroy") + redirect_to new_admin_booth_shift_path(@booth), notice: notice + end + + private + + def load_booth + @booth = ::Poll::Booth.find(params[:booth_id]) + end + + def load_polls + @polls = ::Poll.current_or_incoming + end + + def load_officers + @officers = ::Poll::Officer.all + end + + def load_shifts + @shifts = @booth.shifts + end + + def shift_params + params.require(:shift).permit(:booth_id, :officer_id, :date) + end + +end \ No newline at end of file diff --git a/app/controllers/budgets/investments_controller.rb b/app/controllers/budgets/investments_controller.rb index 5be50346545..1a7c9988a7a 100644 --- a/app/controllers/budgets/investments_controller.rb +++ b/app/controllers/budgets/investments_controller.rb @@ -44,10 +44,12 @@ def show 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 @@ -114,7 +116,8 @@ def set_random_seed 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/communities_controller.rb b/app/controllers/communities_controller.rb new file mode 100644 index 00000000000..ff74f208bbd --- /dev/null +++ b/app/controllers/communities_controller.rb @@ -0,0 +1,34 @@ +class CommunitiesController < ApplicationController + TOPIC_ORDERS = %w{newest most_commented oldest}.freeze + before_action :set_order, :set_community, :load_topics, :load_participants + + has_orders TOPIC_ORDERS + + skip_authorization_check + + def show + redirect_to root_path unless Setting['feature.community'].present? + end + + private + + def set_order + @order = valid_order? ? params[:order] : "newest" + end + + def set_community + @community = Community.find(params[:id]) + end + + def load_topics + @topics = @community.topics.send("sort_by_#{@order}").page(params[:page]) + end + + def load_participants + @participants = @community.participants + end + + def valid_order? + params[:order].present? && TOPIC_ORDERS.include?(params[:order]) + end +end diff --git a/app/controllers/concerns/commentable_actions.rb b/app/controllers/concerns/commentable_actions.rb index 91dcbc8e36f..dac592fb8fa 100644 --- a/app/controllers/concerns/commentable_actions.rb +++ b/app/controllers/concerns/commentable_actions.rb @@ -62,6 +62,8 @@ def edit 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 @@ -117,4 +119,12 @@ def index_customization def add_predefined_tag @resource.tag_list << params[:tag] if params[:tag].present? 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 00000000000..ceb7d191f04 --- /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/management/sessions_controller.rb b/app/controllers/management/sessions_controller.rb index 85ed59eae76..6db303a3932 100644 --- a/app/controllers/management/sessions_controller.rb +++ b/app/controllers/management/sessions_controller.rb @@ -20,7 +20,7 @@ def destroy def destroy_session session[:manager] = nil - session[:document_type] = nil + session[:document_type] = nil session[:document_number] = nil end diff --git a/app/controllers/proposals_controller.rb b/app/controllers/proposals_controller.rb index af6bb9d7b15..678754f860e 100644 --- a/app/controllers/proposals_controller.rb +++ b/app/controllers/proposals_controller.rb @@ -20,11 +20,13 @@ def show super @notifications = @proposal.notifications load_rank + @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 log_event("proposal", "create") @@ -87,7 +89,8 @@ def new def proposal_params params.require(:proposal).permit(:title, :question, :summary, :description, :external_url, :video_url, - :responsible_name, :tag_list, :terms_of_service, :geozone_id, :proceeding, :sub_proceeding) + :responsible_name, :tag_list, :terms_of_service, :geozone_id, :proceeding, :sub_proceeding, + documents_attributes: [:id, :title, :attachment, :cached_attachment, :user_id] ) end def retired_params diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb new file mode 100644 index 00000000000..0e63685bd58 --- /dev/null +++ b/app/controllers/topics_controller.rb @@ -0,0 +1,60 @@ +class TopicsController < ApplicationController + include CommentableActions + + before_action :load_community + before_action :load_topic, only: [:show, :edit, :update, :destroy] + + has_orders %w{most_voted newest oldest}, only: :show + + skip_authorization_check only: :show + load_and_authorize_resource except: :show + + def new + @topic = Topic.new + end + + def create + @topic = Topic.new(topic_params.merge(author: current_user, community_id: params[:community_id])) + if @topic.save + redirect_to community_path(@community), notice: I18n.t('flash.actions.create.topic') + else + render :new + end + end + + def show + @commentable = @topic + @comment_tree = CommentTree.new(@commentable, params[:page], @current_order) + set_comment_flags(@comment_tree.comments) + end + + def edit + end + + def update + if @topic.update(topic_params) + redirect_to community_path(@community), notice: t('flash.actions.update.topic') + else + render :edit + end + end + + def destroy + @topic.destroy + redirect_to community_path(@community), notice: I18n.t('flash.actions.destroy.topic') + end + + private + + def topic_params + params.require(:topic).permit(:title, :description) + end + + def load_community + @community = Community.find(params[:community_id]) + end + + def load_topic + @topic = Topic.find(params[:id]) + end +end diff --git a/app/controllers/verification/letter_controller.rb b/app/controllers/verification/letter_controller.rb index 8066e5a33ad..a9be02fba92 100644 --- a/app/controllers/verification/letter_controller.rb +++ b/app/controllers/verification/letter_controller.rb @@ -45,7 +45,7 @@ def letter_params end def verify_phone! - unless current_user.confirmed_phone? + unless current_user.sms_verified? redirect_to verified_user_path, alert: t('verification.letter.alert.unconfirmed_code') end end diff --git a/app/helpers/comments_helper.rb b/app/helpers/comments_helper.rb index 05a715d2c49..9553d9ad93e 100644 --- a/app/helpers/comments_helper.rb +++ b/app/helpers/comments_helper.rb @@ -60,6 +60,8 @@ def commentable_path(comment) legislation_process_question_path(commentable.process, commentable) when "Legislation::Annotation" legislation_process_draft_version_annotation_path(commentable.draft_version.process, commentable.draft_version, commentable) + when "Topic" + community_topic_path(commentable.community, commentable) else commentable end diff --git a/app/helpers/communities_helper.rb b/app/helpers/communities_helper.rb new file mode 100644 index 00000000000..bb770a9488f --- /dev/null +++ b/app/helpers/communities_helper.rb @@ -0,0 +1,35 @@ +module CommunitiesHelper + + def community_title(community) + community.from_proposal? ? community.proposal.title : community.investment.title + end + + def community_text(community) + community.from_proposal? ? t("community.show.title.proposal") : t("community.show.title.investment") + end + + def community_description(community) + community.from_proposal? ? t("community.show.description.proposal") : t("community.show.description.investment") + end + + def is_author?(community, participant) + if community.from_proposal? + community.proposal.author_id == participant.id + else + community.investment.author_id == participant.id + end + end + + def community_back_link_path(community) + if community.from_proposal? + proposal_path(community.proposal) + else + budget_investment_path(community.investment.budget_id, community.investment) + end + end + + def community_access_text(community) + community.from_proposal? ? t("community.sidebar.description.proposal") : t("community.sidebar.description.investment") + end + +end diff --git a/app/helpers/documentables_helper.rb b/app/helpers/documentables_helper.rb new file mode 100644 index 00000000000..4fd737908b6 --- /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 00000000000..17d70068b53 --- /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/helpers/shifts_helper.rb b/app/helpers/shifts_helper.rb new file mode 100644 index 00000000000..37f22a3e2a2 --- /dev/null +++ b/app/helpers/shifts_helper.rb @@ -0,0 +1,23 @@ +module ShiftsHelper + + def shift_dates_select_options(polls) + options = [] + (start_date(polls)..end_date(polls)).each do |date| + options << [l(date, format: :long), l(date)] + end + options_for_select(options, params[:date]) + end + + def start_date(polls) + polls.map(&:starts_at).min.to_date + end + + def end_date(polls) + polls.map(&:ends_at).max.to_date + end + + def officer_select_options(officers) + officers.collect { |officer| [officer.name, officer.id] } + end + +end diff --git a/app/helpers/topics_helper.rb b/app/helpers/topics_helper.rb new file mode 100644 index 00000000000..1d5f1964f86 --- /dev/null +++ b/app/helpers/topics_helper.rb @@ -0,0 +1,11 @@ +module TopicsHelper + + def disabled_create_topic + "disabled" unless current_user + end + + def disabled_info_title + t("community.show.sidebar.disabled_info_title") unless current_user + end + +end diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index e7aeb5a2181..111a8026788 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -40,6 +40,26 @@ def current_administrator? current_user && current_user.administrator? end + def current_moderator? + current_user && current_user.moderator? + end + + def current_valuator? + current_user && current_user.valuator? + end + + def current_manager? + current_user && current_user.manager? + end + + def current_poll_officer? + current_user && current_user.poll_officer? + end + + def show_admin_menu? + current_administrator? || current_moderator? || current_valuator? || current_manager? || current_poll_officer? + end + def interests_title_text(user) if current_user == user t('account.show.public_interests_my_title_list') diff --git a/app/models/abilities/administrator.rb b/app/models/abilities/administrator.rb index 795c58a6168..f785962f82e 100644 --- a/app/models/abilities/administrator.rb +++ b/app/models/abilities/administrator.rb @@ -32,8 +32,8 @@ def initialize(user) can :mark_featured, Debate can :unmark_featured, Debate - can :comment_as_administrator, [Debate, Comment, Proposal, SpendingProposal, ProbeOption, Poll::Question, Budget::Investment, - Legislation::Question, Legislation::Annotation] + can :comment_as_administrator, [Debate, Comment, Proposal, Poll::Question, Budget::Investment, + Legislation::Question, Legislation::Annotation, Topic, SpendingProposal, ProbeOption] can [:search, :create, :index, :destroy], ::Administrator can [:search, :create, :index, :destroy], ::Moderator @@ -79,6 +79,7 @@ def initialize(user) 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 fc474ca7af6..1f811e3837c 100644 --- a/app/models/abilities/common.rb +++ b/app/models/abilities/common.rb @@ -38,6 +38,9 @@ def initialize(user) 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 @@ -90,6 +93,9 @@ def initialize(user) can [:create, :read], Answer can :create, Annotation can [:update, :destroy], Annotation, user_id: user.id + + can [:create], Topic + can [:update, :destroy], Topic, author_id: user.id end end end diff --git a/app/models/abilities/moderator.rb b/app/models/abilities/moderator.rb index c29672d6cc8..a0f46f25e61 100644 --- a/app/models/abilities/moderator.rb +++ b/app/models/abilities/moderator.rb @@ -5,8 +5,8 @@ class Moderator def initialize(user) merge Abilities::Moderation.new(user) - can :comment_as_moderator, [Debate, Comment, Proposal, SpendingProposal, ProbeOption, Poll::Question, Budget::Investment, - Legislation::Question, Legislation::Annotation] + can :comment_as_moderator, [Debate, Comment, Proposal, Budget::Investment, Poll::Question, + Legislation::Question, Legislation::Annotation, Topic, SpendingProposal, ProbeOption,] end end end diff --git a/app/models/budget/investment.rb b/app/models/budget/investment.rb index d86afb2b5d2..4a0fb84bae4 100644 --- a/app/models/budget/investment.rb +++ b/app/models/budget/investment.rb @@ -1,12 +1,17 @@ class Budget class Investment < ActiveRecord::Base - include Measurable include Sanitizable include Taggable include Searchable include Reclassification include Followable + include Communitable + 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/comment.rb b/app/models/comment.rb index 91a59456d79..f4648b5e44e 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -3,7 +3,7 @@ class Comment < ActiveRecord::Base include HasPublicAuthor include Graphqlable - COMMENTABLE_TYPES = %w(Debate Proposal SpendingProposal ProbeOption Budget::Investment Poll::Question Legislation::Question Legislation::Annotation).freeze + COMMENTABLE_TYPES = %w(Debate Proposal Budget::Investment Poll::Question Legislation::Question Legislation::Annotation Topic SpendingProposal ProbeOption).freeze acts_as_paranoid column: :hidden_at include ActsAsParanoidAliases diff --git a/app/models/community.rb b/app/models/community.rb new file mode 100644 index 00000000000..3fa1cebaa93 --- /dev/null +++ b/app/models/community.rb @@ -0,0 +1,34 @@ +class Community < ActiveRecord::Base + has_one :proposal + has_one :investment, class_name: Budget::Investment + has_many :topics + + def participants + users_participants = users_who_commented + + users_who_topics_author + + author_from_community + users_participants.uniq + end + + def from_proposal? + self.proposal.present? + end + + private + + def users_who_commented + topics_ids = topics.pluck(:id) + query = "comments.commentable_id IN (?)and comments.commentable_type = 'Topic'" + User.by_comments(query, topics_ids) + end + + def users_who_topics_author + author_ids = topics.pluck(:author_id) + User.by_authors(author_ids) + end + + def author_from_community + from_proposal? ? User.where(id: proposal.author_id) : User.where(id: investment.author_id) + end + +end diff --git a/app/models/concerns/communitable.rb b/app/models/concerns/communitable.rb new file mode 100644 index 00000000000..415a7a3f083 --- /dev/null +++ b/app/models/concerns/communitable.rb @@ -0,0 +1,14 @@ +module Communitable + extend ActiveSupport::Concern + + included do + belongs_to :community + before_create :associate_community + end + + def associate_community + community = Community.create + self.community_id = community.id + end + +end diff --git a/app/models/concerns/documentable.rb b/app/models/concerns/documentable.rb new file mode 100644 index 00000000000..4aeaf6eab9f --- /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 00000000000..7fd82ea332b --- /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/poll.rb b/app/models/poll.rb index 1db4def0ed1..7d16fccc447 100644 --- a/app/models/poll.rb +++ b/app/models/poll.rb @@ -39,6 +39,10 @@ def expired?(timestamp = Date.current.beginning_of_day) ends_at < timestamp end + def self.current_or_incoming + current + incoming + end + def answerable_by?(user) user.present? && user.level_two_or_three_verified? && diff --git a/app/models/poll/booth.rb b/app/models/poll/booth.rb index c7fb63efc9a..9edbcbaf0ff 100644 --- a/app/models/poll/booth.rb +++ b/app/models/poll/booth.rb @@ -2,6 +2,7 @@ class Poll class Booth < ActiveRecord::Base has_many :booth_assignments, class_name: "Poll::BoothAssignment" has_many :polls, through: :booth_assignments + has_many :shifts validates :name, presence: true, uniqueness: true diff --git a/app/models/poll/shift.rb b/app/models/poll/shift.rb new file mode 100644 index 00000000000..8ee646ea420 --- /dev/null +++ b/app/models/poll/shift.rb @@ -0,0 +1,22 @@ +class Poll + class Shift < ActiveRecord::Base + belongs_to :booth + belongs_to :officer + + validates :booth_id, presence: true + validates :officer_id, presence: true + validates :date, presence: true + validates :date, uniqueness: { scope: [:officer_id, :booth_id] } + + after_create :create_officer_assignments + + def create_officer_assignments + booth.booth_assignments.each do |booth_assignment| + attrs = { officer_id: officer_id, + date: date, + booth_assignment_id: booth_assignment.id } + Poll::OfficerAssignment.create!(attrs) + end + end + end + end \ No newline at end of file diff --git a/app/models/proposal.rb b/app/models/proposal.rb index f3e78c55f3d..1239491387a 100644 --- a/app/models/proposal.rb +++ b/app/models/proposal.rb @@ -10,6 +10,12 @@ class Proposal < ActiveRecord::Base include HasPublicAuthor include Graphqlable include Followable + include Communitable + 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/topic.rb b/app/models/topic.rb new file mode 100644 index 00000000000..34abed0e3a5 --- /dev/null +++ b/app/models/topic.rb @@ -0,0 +1,18 @@ +class Topic < ActiveRecord::Base + acts_as_paranoid column: :hidden_at + include ActsAsParanoidAliases + + belongs_to :community + belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' + + has_many :comments, as: :commentable + + validates :title, presence: true + validates :description, presence: true + validates :author, presence: true + + scope :sort_by_newest, -> { order(created_at: :desc) } + scope :sort_by_oldest, -> { order(created_at: :asc) } + scope :sort_by_most_commented, -> { reorder(comments_count: :desc) } + +end diff --git a/app/models/user.rb b/app/models/user.rb index 41939d4bb50..6f0e41f8cc9 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -63,11 +63,13 @@ class User < ActiveRecord::Base scope :officials, -> { where("official_level > 0") } scope :newsletter, -> { where(newsletter: true) } scope :for_render, -> { includes(:organization) } - scope :by_document, ->(document_type, document_number) { where(document_type: document_type, document_number: document_number) } + scope :by_document, -> (document_type, document_number) { where(document_type: document_type, document_number: document_number) } scope :email_digest, -> { where(email_digest: true) } scope :active, -> { where(erased_at: nil) } scope :erased, -> { where.not(erased_at: nil) } scope :public_for_api, -> { all } + scope :by_comments, -> (query, topics_ids) { joins(:comments).where(query, topics_ids).uniq } + scope :by_authors, -> (author_ids) { where("users.id IN (?)", author_ids) } before_validation :clean_document_number diff --git a/app/views/admin/_menu.html.erb b/app/views/admin/_menu.html.erb index 94ee849623d..b3510b3166f 100644 --- a/app/views/admin/_menu.html.erb +++ b/app/views/admin/_menu.html.erb @@ -73,9 +73,13 @@ <%= link_to t('admin.menu.poll_officers'), admin_officers_path %> -<%= t("admin.poll_shifts.new.date") %> | +<%= t("admin.poll_shifts.new.officer") %> | +<%= t("admin.poll_shifts.new.assignment") %> | +
---|---|---|
<%= l(shift.date.to_date, format: :long) %> | +<%= shift.officer.name %> | ++ <%= link_to t("admin.poll_shifts.new.remove_assignment"), + admin_booth_shift_path(@booth, shift), + method: :delete, + class: "button hollow alert" %> + | +
+ <%= t("budgets.index.section_footer.title") %> +
+<%= t("budgets.index.section_footer.description") %>
+<%= t("budgets.index.section_footer.help_text_1") %>
+<%= t("budgets.index.section_footer.help_text_2") %>
+<%= t("budgets.index.section_footer.help_text_3", + org: link_to(setting['org_name'], new_user_registration_path)).html_safe %>
+<%= t("budgets.index.section_footer.help_text_4") %>
+<%= t("budgets.investments.show.code_html", code: investment.id) %> @@ -55,6 +60,7 @@
<%= investment.price_explanation %>
<% end %> ++ <%= community_access_text(community) %> +
+ <%= link_to t("community.sidebar.button_to_access"), community_path(community.id), class: 'button hollow expanded' %> +<% end %> diff --git a/app/views/communities/_participant.html.erb b/app/views/communities/_participant.html.erb new file mode 100644 index 00000000000..5e4f02900bd --- /dev/null +++ b/app/views/communities/_participant.html.erb @@ -0,0 +1,22 @@ +<%= community_title(@community) %>
+<%= community_description(@community) %>
++ <%= t("debates.index.section_footer.title") %> +
+<%= t("debates.index.section_footer.description") %>
+<%= t("debates.index.section_footer.help_text_1") %>
+<%= t("debates.index.section_footer.help_text_2", + org: link_to(setting['org_name'], new_user_registration_path)).html_safe %>
+<%= t("debates.index.section_footer.help_text_3", + proposal: link_to(t("debates.index.section_footer.proposals_link"), proposals_path), + budget: link_to(t("debates.index.section_footer.budget_link"), budgets_path)).html_safe %> +
+<%= documentables_note(documentable) %>
+ + <% documentable.documents.each_with_index do |document, index| %> + <%= render 'documents/nested_fields', document: document, index: index, documentable: documentable %> + <% end %> +<%= document_attachment_file_name(document) %>
+ +<%= document_attachment_file_name(document) %>
+ +<%= t('legislation.processes.header_full.description') %>
<%= markdown process.description %> <% end %>