diff --git a/.rubocop.yml b/.rubocop.yml index e2cc2bf93..49de3550b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,6 +1,8 @@ inherit_from: .rubocop_todo.yml AllCops: + DisplayCopNames: true + DisplayStyleGuide: true Include: - '**/Rakefile' - '**/config.ru' diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 288c964c1..f8d4bb811 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -454,12 +454,6 @@ Style/ClassVars: - 'app/models/organization.rb' - 'app/models/user.rb' -# Offense count: 6 -# Cop supports --auto-correct. -Style/ColonMethodCall: - Exclude: - - 'spec/models/budget/investment_spec.rb' - # Offense count: 12 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, SingleLineConditionsOnly, IncludeTernaryExpressions. @@ -480,12 +474,6 @@ Style/DoubleNegation: Exclude: - 'app/models/flag.rb' -# Offense count: 1 -# Cop supports --auto-correct. -Style/EmptyCaseCondition: - Exclude: - - 'app/models/concerns/verification.rb' - # Offense count: 2 # Configuration parameters: ExpectMatchingDefinition, Regex, IgnoreExecutableScripts, AllowedAcronyms. # AllowedAcronyms: CLI, DSL, ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, XSS @@ -527,20 +515,6 @@ Style/Lambda: - 'app/models/vote.rb' - 'lib/graph_ql/api_types_creator.rb' -# Offense count: 1 -# Cop supports --auto-correct. -Style/MethodCallWithoutArgsParentheses: - Exclude: - - 'app/controllers/management/document_verifications_controller.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. -# SupportedStyles: require_parentheses, require_no_parentheses, require_no_parentheses_except_multiline -Style/MethodDefParentheses: - Exclude: - - 'spec/helpers/comments_helper_spec.rb' - # Offense count: 1 Style/MultilineBlockChain: Exclude: @@ -572,25 +546,6 @@ Style/MutableConstant: - 'lib/tag_sanitizer.rb' - 'lib/wysiwyg_sanitizer.rb' -# Offense count: 29 -# Cop supports --auto-correct. -Style/NestedParenthesizedCalls: - Exclude: - - 'spec/features/debates_spec.rb' - - 'spec/features/emails_spec.rb' - - 'spec/features/valuation/budget_investments_spec.rb' - - 'spec/features/valuation/spending_proposals_spec.rb' - - 'spec/helpers/settings_helper_spec.rb' - - 'spec/helpers/verification_helper_spec.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, MinBodyLength, SupportedStyles. -# SupportedStyles: skip_modifier_ifs, always -Style/Next: - Exclude: - - 'app/controllers/officing/results_controller.rb' - # Offense count: 54 # Cop supports --auto-correct. # Configuration parameters: Strict. @@ -625,15 +580,6 @@ Style/ParallelAssignment: - 'lib/active_model/dates.rb' - 'spec/support/common_actions.rb' -# Offense count: 3 -# Cop supports --auto-correct. -# Configuration parameters: AllowSafeAssignment. -Style/ParenthesesAroundCondition: - Exclude: - - 'app/controllers/proposals_controller.rb' - - 'app/models/debate.rb' - - 'app/models/proposal.rb' - # Offense count: 11 # Configuration parameters: NamePrefix, NamePrefixBlacklist, NameWhitelist. # NamePrefix: is_, has_, have_ @@ -668,11 +614,6 @@ Style/RedundantBegin: - 'app/controllers/graphql_controller.rb' - 'app/models/legislation/annotation.rb' -# Offense count: 55 -# Cop supports --auto-correct. -Style/RedundantParentheses: - Enabled: false - # Offense count: 3 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, AllowInnerSlashes. @@ -698,22 +639,6 @@ Style/SafeNavigation: Exclude: - 'app/models/signature.rb' -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. -# SupportedStyles: single_quotes, double_quotes -Style/StringLiteralsInInterpolation: - Exclude: - - 'spec/features/budgets/investments_spec.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyleForMultiline, SupportedStylesForMultiline. -# SupportedStylesForMultiline: comma, consistent_comma, no_comma -Style/TrailingCommaInArguments: - Exclude: - - 'app/controllers/legislation/answers_controller.rb' - # Offense count: 9 # Configuration parameters: SupportedStyles. # SupportedStyles: snake_case, camelCase diff --git a/Gemfile b/Gemfile index a6c1dc71e..7f9579b73 100644 --- a/Gemfile +++ b/Gemfile @@ -53,10 +53,13 @@ gem 'turnout', '~> 2.4.0' gem 'uglifier', '~> 3.2.0' gem 'unicorn', '~> 5.3.0' gem 'whenever', '~> 0.9.7', require: false +source 'https://rails-assets.org' do + gem 'rails-assets-leaflet' +end group :development, :test do gem "bullet", '~> 5.5.1' - gem 'byebug', '~> 9.0.6' + gem 'byebug', '~> 9.1.0' gem 'factory_girl_rails', '~> 4.8.0' gem "faker", '~> 1.7.3' gem 'i18n-tasks', '~> 0.9.15' diff --git a/Gemfile.lock b/Gemfile.lock index f15f0c6e9..b3bdaed90 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -71,7 +71,7 @@ GEM bullet (5.5.1) activesupport (>= 3.0.0) uniform_notifier (~> 1.10.0) - byebug (9.0.6) + byebug (9.1.0) cancancan (1.16.0) capistrano (3.8.2) airbrussh (>= 1.0.0) @@ -251,7 +251,7 @@ GEM mime-types-data (~> 3.2015) mime-types-data (3.2016.0521) mimemagic (0.3.2) - mini_portile2 (2.2.0) + mini_portile2 (2.3.0) minitest (5.10.3) mixlib-cli (1.7.0) mixlib-config (2.2.4) @@ -262,8 +262,8 @@ GEM net-ssh (>= 2.6.5) net-ssh (4.1.0) newrelic_rpm (4.1.0.333) - nokogiri (1.8.0) - mini_portile2 (~> 2.2.0) + nokogiri (1.8.1) + mini_portile2 (~> 2.3.0) nori (2.6.0) oauth (0.5.3) oauth2 (1.4.0) @@ -334,6 +334,7 @@ GEM bundler (>= 1.3.0, < 2.0) railties (= 4.2.9) sprockets-rails + rails-assets-leaflet (1.1.0) rails-assets-markdown-it (8.2.2) rails-deprecated_sanitizer (1.0.3) activesupport (>= 4.2.0.alpha) @@ -493,7 +494,7 @@ DEPENDENCIES ancestry (~> 2.2.2) browser (~> 2.3.0) bullet (~> 5.5.1) - byebug (~> 9.0.6) + byebug (~> 9.1.0) cancancan (~> 1.16.0) capistrano (~> 3.8.1) capistrano-bundler (~> 1.2) @@ -542,6 +543,7 @@ DEPENDENCIES poltergeist (~> 1.15.0) quiet_assets (~> 1.1.0) rails (= 4.2.9) + rails-assets-leaflet! rails-assets-markdown-it (~> 8.2.1)! redcarpet (~> 3.4.0) responders (~> 2.4.0) diff --git a/app/assets/fonts/icons.eot b/app/assets/fonts/icons.eot index c1afd24fd..5564a5c62 100644 Binary files a/app/assets/fonts/icons.eot and b/app/assets/fonts/icons.eot differ diff --git a/app/assets/fonts/icons.svg b/app/assets/fonts/icons.svg index 68d878942..0d94e5751 100644 --- a/app/assets/fonts/icons.svg +++ b/app/assets/fonts/icons.svg @@ -62,4 +62,7 @@ + + + diff --git a/app/assets/fonts/icons.ttf b/app/assets/fonts/icons.ttf index a9443499b..6af0797fb 100644 Binary files a/app/assets/fonts/icons.ttf and b/app/assets/fonts/icons.ttf differ diff --git a/app/assets/fonts/icons.woff b/app/assets/fonts/icons.woff index 09a6f6dc5..7f8f3c95e 100644 Binary files a/app/assets/fonts/icons.woff and b/app/assets/fonts/icons.woff differ diff --git a/app/assets/images/custom/example_horizontal.jpg b/app/assets/images/custom/example_horizontal.jpg new file mode 100644 index 000000000..634e432ac Binary files /dev/null and b/app/assets/images/custom/example_horizontal.jpg differ diff --git a/app/assets/images/custom/example_vertical.jpg b/app/assets/images/custom/example_vertical.jpg new file mode 100644 index 000000000..11743cc18 Binary files /dev/null and b/app/assets/images/custom/example_vertical.jpg differ diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index cf579627b..ce9861547 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -14,6 +14,7 @@ //= require jquery_ujs //= require jquery-ui/widgets/datepicker //= require jquery-ui/i18n/datepicker-es +//= require jquery-ui/widgets/autocomplete //= require jquery-fileupload/basic //= require foundation //= require turbolinks @@ -62,8 +63,14 @@ //= require followable //= require flaggable //= require documentable +//= require imageable //= require tree_navigator //= require custom +//= require tag_autocomplete +//= require polls_admin +//= require leaflet +//= require map +//= require polls var initialize_modules = function() { App.Comments.initialize(); @@ -98,10 +105,15 @@ var initialize_modules = function() { App.WatchFormChanges.initialize(); App.TreeNavigator.initialize(); App.Documentable.initialize(); + App.Imageable.initialize(); + App.TagAutocomplete.initialize(); + App.PollsAdmin.initialize(); + App.Map.initialize(); + App.Polls.initialize(); }; $(function(){ - Turbolinks.enableProgressBar() + Turbolinks.enableProgressBar(); $(document).ready(initialize_modules); $(document).on('page:load', initialize_modules); diff --git a/app/assets/javascripts/documentable.js.coffee b/app/assets/javascripts/documentable.js.coffee index 4c354e5a6..7cc71f39d 100644 --- a/app/assets/javascripts/documentable.js.coffee +++ b/app/assets/javascripts/documentable.js.coffee @@ -1,101 +1,156 @@ App.Documentable = initialize: -> - @initializeDirectUploads() - @initializeInterface() - initializeDirectUploads: -> + inputFiles = $('.js-document-attachment') + $.each inputFiles, (index, input) -> + App.Documentable.initializeDirectUploadInput(input) - $('input.js-document-attachment[type=file]').fileupload + $('#nested-documents').on 'cocoon:after-remove', (e, insertedItem) -> + App.Documentable.unlockUploads() - paramName: "document[attachment]" + $('#nested-documents').on 'cocoon:after-insert', (e, nested_document) -> + input = $(nested_document).find('.js-document-attachment') + App.Documentable.initializeDirectUploadInput(input) + + if $(nested_document).closest('#nested-documents').find('.document:visible').length >= $('#nested-documents').data('max-documents-allowed') + App.Documentable.lockUploads() + + initializeDirectUploadInput: (input) -> + + inputData = @buildData([], input) + + @initializeRemoveCachedDocumentLink(input, inputData) + + $(input).fileupload + + paramName: "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 = App.Documentable.buildFileUploadData(e, data) + App.Documentable.clearProgressBar(data) + App.Documentable.setProgressBar(data, 'uploading') data.submit() change: (e, data) -> - wrapper = $(e.target).parent() - $.each(data.files, (index, file)-> - $(wrapper).find('.file-name').text(file.name) - ) + $.each data.files, (index, file) -> + App.Documentable.setFilename(inputData, file.name) + + fail: (e, data) -> + $(data.cachedAttachmentField).val("") + App.Documentable.clearFilename(data) + App.Documentable.setProgressBar(data, 'errors') + App.Documentable.clearInputErrors(data) + App.Documentable.setInputErrors(data) + $(data.destroyAttachmentLinkContainer).find("a.delete:not(.remove-nested)").remove() + $(data.addAttachmentLabel).addClass('error') + $(data.addAttachmentLabel).show() + + done: (e, data) -> + $(data.cachedAttachmentField).val(data.result.cached_attachment) + App.Documentable.setTitleFromFile(data, data.result.filename) + App.Documentable.setProgressBar(data, 'complete') + App.Documentable.setFilename(data, data.result.filename) + App.Documentable.clearInputErrors(data) + $(data.addAttachmentLabel).hide() + $(data.wrapper).find(".attachment-actions").removeClass('small-12').addClass('small-6 float-right') + $(data.wrapper).find(".attachment-actions .action-remove").removeClass('small-3').addClass('small-12') + + destroyAttachmentLink = $(data.result.destroy_link) + $(data.destroyAttachmentLinkContainer).html(destroyAttachmentLink) + $(destroyAttachmentLink).on 'click', (e) -> + e.preventDefault() + e.stopPropagation() + App.Documentable.doDeleteCachedAttachmentRequest(this.href, data) progress: (e, data) -> progress = parseInt(data.loaded / data.total * 100, 10) $(data.progressBar).find('.loading-bar').css 'width', progress + '%' return - initializeInterface: -> - input_files = $('input.js-document-attachment[type=file]') + buildFileUploadData: (e, data) -> + data = @buildData(data, e.target) + return data - $.each input_files, (index, file) -> - wrapper = $(file).parent() - App.Documentable.watchRemoveDocumentbutton(wrapper) + buildData: (data, input) -> + wrapper = $(input).closest('.direct-upload') + data.input = input + data.wrapper = wrapper + data.progressBar = $(wrapper).find('.progress-bar-placeholder') + data.errorContainer = $(wrapper).find('.attachment-errors') + data.fileNameContainer = $(wrapper).find('p.file-name') + data.destroyAttachmentLinkContainer = $(wrapper).find('.action-remove') + data.addAttachmentLabel = $(wrapper).find('.action-add label') + data.cachedAttachmentField = $(wrapper).find("input[name$='[cached_attachment]']") + data.titleField = $(wrapper).find("input[name$='[title]']") + $(wrapper).find('.progress-bar-placeholder').css('display', 'block') + return data - watchRemoveDocumentbutton: (wrapper) -> - remove_document_button = $(wrapper).find('.remove-document') - $(remove_document_button).on 'click', (e) -> + clearFilename: (data) -> + $(data.fileNameContainer).text('') + $(data.fileNameContainer).hide() + + clearInputErrors: (data) -> + $(data.errorContainer).find('small.error').remove() + + clearProgressBar: (data) -> + $(data.progressBar).find('.loading-bar').removeClass('complete errors uploading').css('width', "0px") + + setFilename: (data, file_name) -> + $(data.fileNameContainer).text(file_name) + $(data.fileNameContainer).show() + + setProgressBar: (data, klass) -> + $(data.progressBar).find('.loading-bar').addClass(klass) + + setTitleFromFile: (data, title) -> + if $(data.titleField).val() == "" + $(data.titleField).val(title) + + setInputErrors: (data) -> + errors = '' + data.jqXHR.responseJSON.errors + '' + $(data.errorContainer).append(errors) + + lockUploads: -> + $('#max-documents-notice').removeClass('hide') + $('#new_document_link').addClass('hide') + + unlockUploads: -> + $('#max-documents-notice').addClass('hide') + $('#new_document_link').removeClass('hide') + + doDeleteCachedAttachmentRequest: (url, data) -> + $.ajax + type: "POST" + url: url + dataType: "json" + data: { "_method": "delete" } + complete: -> + $(data.cachedAttachmentField).val("") + $(data.addAttachmentLabel).show() + + App.Documentable.clearFilename(data) + App.Documentable.clearInputErrors(data) + App.Documentable.clearProgressBar(data) + + App.Documentable.unlockUploads() + $(data.wrapper).find(".attachment-actions").addClass('small-12').removeClass('small-6 float-right') + $(data.wrapper).find(".attachment-actions .action-remove").addClass('small-3').removeClass('small-12') + + if $(data.input).data('nested-document') == true + $(data.wrapper).remove() + else + $(data.wrapper).find('a.remove-cached-attachment').remove() + + initializeRemoveCachedDocumentLink: (input, data) -> + wrapper = $(input).closest(".direct-upload") + remove_document_link = $(wrapper).find('a.remove-cached-attachment') + $(remove_document_link).on 'click', (e) -> e.preventDefault() - $(wrapper).remove() - $('#new_document_link').show() - $('.max-documents-notice').hide() + e.stopPropagation() + App.Documentable.doDeleteCachedAttachmentRequest(this.href, data) - 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) -> + removeDocument: (id) -> $('#' + 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/imageable.js.coffee b/app/assets/javascripts/imageable.js.coffee new file mode 100644 index 000000000..e029dff52 --- /dev/null +++ b/app/assets/javascripts/imageable.js.coffee @@ -0,0 +1,166 @@ +App.Imageable = + + initialize: -> + inputFiles = $('.js-image-attachment') + $.each inputFiles, (index, input) -> + App.Imageable.initializeDirectUploadInput(input) + + $('#nested-image').on 'cocoon:after-remove', (e, item) -> + $("#new_image_link").removeClass('hide') + + $('#nested-image').on 'cocoon:before-insert', (e, nested_image) -> + if $(".js-image-attachment").length > 0 + $(".js-image-attachment").closest('.image').remove() + + $('#nested-image').on 'cocoon:after-insert', (e, nested_image) -> + $("#new_image_link").addClass('hide') + input = $(nested_image).find('.js-image-attachment') + App.Imageable.initializeDirectUploadInput(input) + + initializeDirectUploadInput: (input) -> + + inputData = @buildData([], input) + + @initializeRemoveCachedImageLink(input, inputData) + + $(input).fileupload + + paramName: "attachment" + + formData: null + + add: (e, data) -> + data = App.Imageable.buildFileUploadData(e, data) + App.Imageable.clearProgressBar(data) + App.Imageable.setProgressBar(data, 'uploading') + data.submit() + + change: (e, data) -> + $.each data.files, (index, file) -> + App.Imageable.setFilename(inputData, file.name) + + fail: (e, data) -> + $(data.cachedAttachmentField).val("") + App.Imageable.clearFilename(data) + App.Imageable.setProgressBar(data, 'errors') + App.Imageable.clearInputErrors(data) + App.Imageable.setInputErrors(data) + App.Imageable.clearPreview(data) + $(data.destroyAttachmentLinkContainer).find("a.delete:not(.remove-nested)").remove() + $(data.addAttachmentLabel).addClass('error') + $(data.addAttachmentLabel).show() + + done: (e, data) -> + $(data.cachedAttachmentField).val(data.result.cached_attachment) + App.Imageable.setTitleFromFile(data, data.result.filename) + App.Imageable.setProgressBar(data, 'complete') + App.Imageable.setFilename(data, data.result.filename) + App.Imageable.clearInputErrors(data) + $(data.addAttachmentLabel).hide() + $(data.wrapper).find(".attachment-actions").removeClass('small-12').addClass('small-6 float-right') + $(data.wrapper).find(".attachment-actions .action-remove").removeClass('small-3').addClass('small-12') + App.Imageable.setPreview(data) + + destroyAttachmentLink = $(data.result.destroy_link) + $(data.destroyAttachmentLinkContainer).html(destroyAttachmentLink) + $(destroyAttachmentLink).on 'click', (e) -> + e.preventDefault() + e.stopPropagation() + App.Imageable.doDeleteCachedAttachmentRequest(this.href, data) + + progress: (e, data) -> + progress = parseInt(data.loaded / data.total * 100, 10) + $(data.progressBar).find('.loading-bar').css 'width', progress + '%' + return + + buildFileUploadData: (e, data) -> + data = @buildData(data, e.target) + return data + + buildData: (data, input) -> + wrapper = $(input).closest('.direct-upload') + data.input = input + data.wrapper = wrapper + data.progressBar = $(wrapper).find('.progress-bar-placeholder') + data.preview = $(wrapper).find('.image-preview') + data.errorContainer = $(wrapper).find('.attachment-errors') + data.fileNameContainer = $(wrapper).find('p.file-name') + data.destroyAttachmentLinkContainer = $(wrapper).find('.action-remove') + data.addAttachmentLabel = $(wrapper).find('.action-add label') + data.cachedAttachmentField = $(wrapper).find("input[name$='[cached_attachment]']") + data.titleField = $(wrapper).find("input[name$='[title]']") + $(wrapper).find('.progress-bar-placeholder').css('display', 'block') + return data + + clearFilename: (data) -> + $(data.fileNameContainer).text('') + $(data.fileNameContainer).hide() + + clearInputErrors: (data) -> + $(data.errorContainer).find('small.error').remove() + + clearProgressBar: (data) -> + $(data.progressBar).find('.loading-bar').removeClass('complete errors uploading').css('width', "0px") + + clearPreview: (data) -> + $(data.wrapper).find('.image-preview').remove() + + setFilename: (data, file_name) -> + $(data.fileNameContainer).text(file_name) + $(data.fileNameContainer).show() + + setProgressBar: (data, klass) -> + $(data.progressBar).find('.loading-bar').addClass(klass) + + setTitleFromFile: (data, title) -> + if $(data.titleField).val() == "" + $(data.titleField).val(title) + + setInputErrors: (data) -> + errors = '' + data.jqXHR.responseJSON.errors + '' + $(data.errorContainer).append(errors) + + setPreview: (data) -> + image_preview = '
' + if $(data.preview).length > 0 + $(data.preview).replaceWith(image_preview) + else + $(image_preview).insertBefore($(data.wrapper).find(".attachment-actions")) + data.preview = $(data.wrapper).find('.image-preview') + + doDeleteCachedAttachmentRequest: (url, data) -> + $.ajax + type: "POST" + url: url + dataType: "json" + data: { "_method": "delete" } + complete: -> + $(data.cachedAttachmentField).val("") + $(data.addAttachmentLabel).show() + + App.Imageable.clearFilename(data) + App.Imageable.clearInputErrors(data) + App.Imageable.clearProgressBar(data) + App.Imageable.clearPreview(data) + + $('#new_image_link').removeClass('hide') + + $(data.wrapper).find(".attachment-actions").addClass('small-12').removeClass('small-6 float-right') + $(data.wrapper).find(".attachment-actions .action-remove").addClass('small-3').removeClass('small-12') + + if $(data.input).data('nested-image') == true + $(data.wrapper).remove() + else + $(data.wrapper).find('a.remove-cached-attachment').remove() + + initializeRemoveCachedImageLink: (input, data) -> + wrapper = $(input).closest(".direct-upload") + remove_image_link = $(wrapper).find('a.remove-cached-attachment') + $(remove_image_link).on 'click', (e) -> + e.preventDefault() + e.stopPropagation() + App.Imageable.doDeleteCachedAttachmentRequest(this.href, data) + + removeImage: (id) -> + $('#' + id).remove() + $("#new_image_link").removeClass('hide') diff --git a/app/assets/javascripts/map.js.coffee b/app/assets/javascripts/map.js.coffee new file mode 100644 index 000000000..3f28024a7 --- /dev/null +++ b/app/assets/javascripts/map.js.coffee @@ -0,0 +1,78 @@ +App.Map = + + initialize: -> + maps = $('*[data-map]') + + if maps.length > 0 + $.each maps, (index, map) -> + App.Map.initializeMap map + + initializeMap: (element) -> + + mapCenterLatitude = $(element).data('map-center-latitude') + mapCenterLongitude = $(element).data('map-center-longitude') + markerLatitude = $(element).data('marker-latitude') + markerLongitude = $(element).data('marker-longitude') + zoom = $(element).data('map-zoom') + mapTilesProvider = $(element).data('map-tiles-provider') + mapAttribution = $(element).data('map-tiles-provider-attribution') + latitudeInputSelector = $(element).data('latitude-input-selector') + longitudeInputSelector = $(element).data('longitude-input-selector') + zoomInputSelector = $(element).data('zoom-input-selector') + removeMarkerSelector = $(element).data('marker-remove-selector') + editable = $(element).data('marker-editable') + marker = null; + markerIcon = L.divIcon( + className: 'map-marker' + iconSize: [30, 30] + iconAnchor: [15, 40] + html: '
') + + createMarker = (latitude, longitude) -> + markerLatLng = new (L.LatLng)(latitude, longitude) + marker = L.marker(markerLatLng, { icon: markerIcon, draggable: editable }) + if editable + marker.on 'dragend', updateFormfields + marker.addTo(map) + return marker + + removeMarker = (e) -> + e.preventDefault() + if marker + map.removeLayer(marker) + marker = null; + clearFormfields() + return + + moveOrPlaceMarker = (e) -> + if marker + marker.setLatLng(e.latlng) + else + marker = createMarker(e.latlng.lat, e.latlng.lng) + + updateFormfields() + return + + updateFormfields = -> + $(latitudeInputSelector).val marker.getLatLng().lat + $(longitudeInputSelector).val marker.getLatLng().lng + $(zoomInputSelector).val map.getZoom() + return + + clearFormfields = -> + $(latitudeInputSelector).val '' + $(longitudeInputSelector).val '' + $(zoomInputSelector).val '' + return + + mapCenterLatLng = new (L.LatLng)(mapCenterLatitude, mapCenterLongitude) + map = L.map(element.id).setView(mapCenterLatLng, zoom) + L.tileLayer(mapTilesProvider, attribution: mapAttribution).addTo map + + if markerLatitude && markerLongitude + marker = createMarker(markerLatitude, markerLongitude) + + if editable + $(removeMarkerSelector).on 'click', removeMarker + map.on 'zoomend', updateFormfields + map.on 'click', moveOrPlaceMarker diff --git a/app/assets/javascripts/polls.js.coffee b/app/assets/javascripts/polls.js.coffee new file mode 100644 index 000000000..5cf792ce2 --- /dev/null +++ b/app/assets/javascripts/polls.js.coffee @@ -0,0 +1,28 @@ +App.Polls = + generateToken: -> + token = '' + rand = '' + for n in [0..5] + rand = Math.random().toString(36).substr(2) # remove `0.` + token = token + rand; + + token = token.substring(0, 64) + return token + + replaceToken: -> + for link in $('.js-question-answer') + token_param = link.search.slice(-6) + if token_param == "token=" + link.href = link.href + @token + + initialize: -> + @token = App.Polls.generateToken() + App.Polls.replaceToken() + + $(".js-question-answer").on + click: => + token_message = $(".js-token-message") + if !token_message.is(':visible') + token_message.html(token_message.html() + "
" + @token + ""); + token_message.show() + false diff --git a/app/assets/javascripts/polls_admin.js.coffee b/app/assets/javascripts/polls_admin.js.coffee new file mode 100644 index 000000000..ef1dd44f1 --- /dev/null +++ b/app/assets/javascripts/polls_admin.js.coffee @@ -0,0 +1,12 @@ +App.PollsAdmin = + + initialize: -> + $("select[class='js-poll-shifts']").on + change: -> + switch ($(this).val()) + when 'vote_collection' + $("select[class='js-shift-vote-collection-dates']").show(); + $("select[class='js-shift-recount-scrutiny-dates']").hide(); + when 'recount_scrutiny' + $("select[class='js-shift-recount-scrutiny-dates']").show(); + $("select[class='js-shift-vote-collection-dates']").hide(); diff --git a/app/assets/javascripts/tag_autocomplete.js.coffee b/app/assets/javascripts/tag_autocomplete.js.coffee new file mode 100644 index 000000000..be27cd81c --- /dev/null +++ b/app/assets/javascripts/tag_autocomplete.js.coffee @@ -0,0 +1,34 @@ +App.TagAutocomplete = + + split: ( val ) -> + return (val.split( /,\s*/ )) + + extractLast: ( term ) -> + return (App.TagAutocomplete.split( term ).pop()) + + init_autocomplete: -> + $('.tag-autocomplete').autocomplete + source: (request, response) -> + $.ajax + url: $('.tag-autocomplete').data('js-url'), + data: {search: App.TagAutocomplete.extractLast( request.term )}, + type: 'GET', + dataType: 'json' + success: ( data ) -> + response( data ); + + minLength: 0, + search: -> + App.TagAutocomplete.extractLast( this.value ); + focus: -> + return false; + select: ( event, ui ) -> ( + terms = App.TagAutocomplete.split( this.value ); + terms.pop(); + terms.push( ui.item.value ); + terms.push( "" ); + this.value = terms.join( ", " ); + return false;); + + initialize: -> + App.TagAutocomplete.init_autocomplete(); \ No newline at end of file diff --git a/app/assets/stylesheets/_consul_settings.scss b/app/assets/stylesheets/_consul_settings.scss index 0844f861f..1f03c9be7 100644 --- a/app/assets/stylesheets/_consul_settings.scss +++ b/app/assets/stylesheets/_consul_settings.scss @@ -75,3 +75,5 @@ $accordion-content-color: foreground($accordion-background, $text); $tab-item-font-size: $base-font-size; $tab-item-padding: $line-height / 2 0; $tab-content-border: $border; + +$orbit-bullet-diameter: 0.8rem; diff --git a/app/assets/stylesheets/admin.scss b/app/assets/stylesheets/admin.scss index 6ac728e2c..8f6912716 100644 --- a/app/assets/stylesheets/admin.scss +++ b/app/assets/stylesheets/admin.scss @@ -8,38 +8,105 @@ // 06. Polls // 07. Legislation // 08. CMS +// 09. Map // // 01. Global styles // ----------------- -$admin-color: #cf3638; +$admin-color: #245b80; +$sidebar: #245b80; +$sidebar-hover: #25597c; +$sidebar-active: #f4fcd0; .admin { + h2 { + font-weight: 100; + margin-bottom: $line-height; + + &.title { + text-transform: uppercase; + } + } + + .back { + float: none; + } + .header { border: 0; } .top-links { - background: darken($admin-color, 15%); - } - - .back-web { - padding-top: $line-height / 4; - text-decoration: underline; + background: #000; } .top-bar { - background: $admin-color !important; + background: #fff !important; + border-bottom: 1px solid #eee; + box-shadow: 0 2px 2px #eee; + color: #000; height: auto; + + [class^="icon-"]:not(.icon-circle) { + font-size: $base-font-size; + } } .top-bar-title { h1 { margin-bottom: 0; + margin-top: $line-height / 2; + + @include breakpoint(medium) { + margin-left: $line-height / 2; + } + + small { + color: #000; + text-transform: uppercase; + } } + + a { + color: #000 !important; + line-height: $line-height !important; + + @include breakpoint(medium) { + line-height: $line-height !important; + } + } + } + + .top-bar .menu > li { + + @include breakpoint(medium) { + height: auto !important; + padding-top: $line-height / 2; + } + + a { + color: #000 !important; + } + } + + .menu-icon.dark { + + &::after, + &:hover::after { + background: #000 !important; + box-shadow: 0 7px 0 #000, 0 14px 0 #000 !important; + } + } + + .notifications .icon-circle { + color: $admin-color; + } + + .dropdown.menu > .is-dropdown-submenu-parent > a::after { + border-color: #000 transparent transparent; } .fieldset { @@ -54,7 +121,8 @@ $admin-color: #cf3638; } } - th, td { + th, + td { text-align: left; &.text-center { @@ -81,6 +149,7 @@ $admin-color: #cf3638; } table { + .break { word-break: break-word; } @@ -107,9 +176,19 @@ $admin-color: #cf3638; max-width: none; } - .menu.simple .active { - border-bottom: 2px solid $admin-color; - color: $admin-color; + .menu.simple { + margin-bottom: $line-height / 2; + + h2 { + font-weight: bold; + margin-bottom: $line-height / 3; + } + + .active { + border-bottom: 2px solid $admin-color; + color: $admin-color; + font-weight: bold; + } } .tabs-panel { @@ -195,6 +274,8 @@ $admin-color: #cf3638; // ----------- .admin-sidebar { + background: $sidebar; + background: linear-gradient(to bottom, #245b80 0%, #488fb5 100%); border-right: 1px solid $border; @include breakpoint(medium) { @@ -208,7 +289,7 @@ $admin-color: #cf3638; padding: 0; [class^="icon-"] { - color: $admin-color; + color: #fff; display: inline-block; font-size: rem-calc(24); line-height: $line-height; @@ -219,39 +300,32 @@ $admin-color: #cf3638; } li { - background: #fff; margin: 0; outline: 0; ul { margin-left: $line-height / 1.5; - border-left: 1px solid $border; + border-left: 1px solid $sidebar-hover; padding-left: $line-height / 2; } - &.section-title { - border-bottom: 1px solid $border; - } - &.active a { - background: #f3f6f7; - border-radius: rem-calc(6); - color: $admin-color; + background: $sidebar-hover; + border-left: 2px solid $sidebar-active; font-weight: bold; } } li a { - color: $text; + color: #fff; display: block; line-height: rem-calc(48); padding-left: rem-calc(12); vertical-align: top; &:hover { - background: #f3f6f7; - border-radius: rem-calc(6); - color: $admin-color; + background: $sidebar-hover; + color: #fff; text-decoration: none; } } @@ -259,7 +333,13 @@ $admin-color: #cf3638; .is-accordion-submenu-parent { > a::after { - border-color: $admin-color transparent transparent; + border: 0; + content: '\61' !important; + font-family: "icons" !important; + height: auto; + position: absolute !important; + right: 30px; + top: 6px !important; } } @@ -898,3 +978,51 @@ table { border: 0; } } + +// 09. Map +// -------------- + +.map { + width: 100%; + height: 350px; + + .map-marker { + visibility: visible; + position: absolute; + left: 50%; + top: 50%; + margin-top: -5px; + + .map-icon { + width: 30px; + height: 30px; + border-radius: 50% 50% 50% 0; + background: #00cae9; + transform: rotate(-45deg); + } + + .map-icon::after { + content: ''; + width: 14px; + height: 14px; + margin: 8px 0 0 8px; + background: #fff; + position: absolute; + border-radius: 50%; + } + } + + .map-attributtion { + visibility: visible; + height: auto; + } +} + +.map-marker { + visibility: hidden; +} + +.map-attributtion { + visibility: hidden; + height: 0; +} diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 19c73de32..aff55c88c 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -16,4 +16,6 @@ @import 'annotator_overrides'; @import 'jquery-ui/datepicker'; @import 'datepicker_overrides'; -@import 'documentable'; +@import 'jquery-ui/autocomplete'; +@import 'autocomplete_overrides'; +@import 'leaflet'; diff --git a/app/assets/stylesheets/autocomplete_overrides.scss b/app/assets/stylesheets/autocomplete_overrides.scss new file mode 100644 index 000000000..dd2b939ad --- /dev/null +++ b/app/assets/stylesheets/autocomplete_overrides.scss @@ -0,0 +1,40 @@ +// Overrides styles of jquery-ui/autocomplete +// + +/* Autocomplete +----------------------------------*/ +.ui-autocomplete { + position: absolute; + cursor: default; +} + +/* workarounds */ +* html .ui-autocomplete { + width: 1px; +} /* without this, the menu expands to 100% in IE6 */ + +/* Menu +----------------------------------*/ +.ui-menu { + list-style: none; + padding: $line-height / 4 $line-height / 3; + display: block; + background: #fff; + border: 1px solid $border; + font-size: $small-font-size; + + .ui-menu-item { + + .ui-menu-item-wrapper { + padding: $line-height / 4 $line-height / 3; + position: relative; + } + + .ui-state-hover, + .ui-state-active { + background: #ececec; + border-radius: rem-calc(6); + } + } + +} diff --git a/app/assets/stylesheets/documentable.scss b/app/assets/stylesheets/documentable.scss deleted file mode 100644 index 70629bb27..000000000 --- a/app/assets/stylesheets/documentable.scss +++ /dev/null @@ -1,63 +0,0 @@ -.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; - } - - .js-document-attachment { - 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/foundation_and_overrides.scss b/app/assets/stylesheets/foundation_and_overrides.scss index 0bead10ae..66640f3f2 100644 --- a/app/assets/stylesheets/foundation_and_overrides.scss +++ b/app/assets/stylesheets/foundation_and_overrides.scss @@ -4,6 +4,7 @@ @import 'consul_settings'; @import 'custom_settings'; @import 'foundation'; +@import 'motion-ui/motion-ui'; @include foundation-global-styles; @include foundation-grid; @@ -37,3 +38,7 @@ @include foundation-title-bar; @include foundation-top-bar; @include foundation-menu-icon; +@include foundation-orbit; + +@include motion-ui-transitions; +@include motion-ui-animations; diff --git a/app/assets/stylesheets/icons.scss b/app/assets/stylesheets/icons.scss index 5e92ee460..96eb10a0d 100644 --- a/app/assets/stylesheets/icons.scss +++ b/app/assets/stylesheets/icons.scss @@ -97,10 +97,6 @@ content: '\72'; } -.icon-documents::before { - content: '\68'; -} - .icon-proposals::before { content: '\68'; } @@ -260,3 +256,15 @@ .icon-instagram::before { content: '\32'; } + +.icon-image::before { + content: '\33'; +} + +.icon-search-plus::before { + content: '\34'; +} + +.icon-search-minus::before { + content: '\35'; +} diff --git a/app/assets/stylesheets/layout.scss b/app/assets/stylesheets/layout.scss index 9ecf74442..efa0454db 100644 --- a/app/assets/stylesheets/layout.scss +++ b/app/assets/stylesheets/layout.scss @@ -18,7 +18,8 @@ // 16. Flags // 17. Activity // 18. Banners -// 19. Documents +// 19. Recommended Section Home +// 20. Documents // // 01. Global styles @@ -205,19 +206,40 @@ a { .menu.simple { border-bottom: 1px solid $border; - margin-bottom: $line-height; + clear: both; + margin-bottom: $line-height / 2; li { - padding-bottom: rem-calc(7); + margin-right: $line-height / 2; + + @include breakpoint(medium) { + margin-right: $line-height * 1.5; + } a { - color: $text-medium; + color: $text; + display: inline-block; + font-weight: bold; + position: relative; + text-align: left; + + &:hover { + color: $link; + } } &.active { border-bottom: 2px solid $brand; color: $brand; } + + &:not(.active) { + margin-bottom: $line-height / 3; + } + } + + h2 { + font-size: $base-font-size; } } @@ -281,6 +303,10 @@ a { float: left; } +.back:not([class^="icon-"]) { + text-decoration: underline; +} + .tabs-content { border: 0; } @@ -320,10 +346,28 @@ a { background: $brand; } +.truncate-horizontal-text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .align-top { vertical-align: top; } +.aling-middle { + vertical-align: middle; +} + +.table { + display: table; +} + +.table-cell { + display: table-cell; +} + // 02. Header // ---------- @@ -367,7 +411,11 @@ header { .top-bar-title a { @include logo; - line-height: rem-calc(80) !important; + line-height: rem-calc(80); + + @include breakpoint(medium) { + line-height: rem-calc(80); + } &:hover { text-decoration: none; @@ -587,7 +635,7 @@ header { text-align: left; @include breakpoint(medium) { - margin-right: $line-height * 1.5; + margin-right: $line-height; } &:hover { @@ -2136,68 +2184,146 @@ table { } } -// 19. Documents -.document-form form { +// 19. Recommended Section Home +// ---------------------------- - .radio-buttons { - label { - margin-right: $line-height; - } +.home-page { + + .push { + display: none; + } +} + +.section-recommended { + padding: $line-height * 2 0; + + h2 { + margin-bottom: $line-height * 2; } - .source-option-link { - input { - padding-bottom: 0; + .debates, + .proposals, + .budget-investments { + + @include breakpoint(medium) { + margin-bottom: 0; } - .error { + @include breakpoint(small) { margin-bottom: $line-height; } - label { - &.error { - margin-bottom: 0; - } + .button.hollow { + margin-top: rem-calc(15); } } - .source-option-file { - .file-name { - label { + .card { - @include breakpoint(small medium) { - float: none; - } - - @include breakpoint(large) { - float: left; - } - } + .card-section { + padding: $line-height 0; + max-width: rem-calc(300); + margin: 0 auto; p { + font-size: rem-calc(15); + text-align: left; + } + } - @include breakpoint(small medium) { - float: none; - margin-top: 0; - margin-left: 0; - margin-bottom: 0; - } + .orbit { + height: rem-calc(300); - @include breakpoint(large) { - float: left; - margin-bottom: 0; - margin-top: $line-height / 2; - margin-left: $line-height; - } + .orbit-wrapper { + max-height: rem-calc(250); + overflow: hidden; + position: relative; + } + + .orbit-bullets { + @include orbit-bullets; + width: 100%; } } } - .attachment-errors { - margin-bottom: $line-height; + .card .orbit .orbit-wrapper .truncate { + background: image-url('truncate.png'); + background-repeat: repeat-x; + bottom: 0; + height: rem-calc(20); + position: absolute; + width: 100%; + } + + .debates-inner { + border-top: 4px solid $debates; + } + + .proposals-inner { + border-top: 4px solid $proposals; + } + + .budget-investments-inner { + border-top: 4px solid $budget; + } + + .debates-inner, + .proposals-inner, + .budget-investments-inner { + background: #fff; + max-height: rem-calc(350); + + @include breakpoint(small) { + max-height: rem-calc(400); + } + + h4 { + margin-top: $line-height; + margin-bottom: 0; + font-size: rem-calc(18); + min-height: rem-calc(50); + } + + h5 { + font-size: $small-font-size; + text-align: left; + } + } + + .carousel-image { + + .card .orbit { + height: rem-calc(480); + + .orbit-wrapper { + max-height: rem-calc(450); + } + } + + .debates-inner, + .proposals-inner, + .budget-investments-inner { + max-height: rem-calc(500); + + @include breakpoint(small) { + max-height: rem-calc(600); + } + } + } + + .carousel-image .orbit-wrapper img { + display: block; + + @include breakpoint(small) { + margin: 0 auto; + } } } +// 20. Documents +// ------------- + .documents-list { table { @@ -2254,6 +2380,5 @@ table { } } - } } diff --git a/app/assets/stylesheets/legislation_process.scss b/app/assets/stylesheets/legislation_process.scss index f31d02ef6..e536bb280 100644 --- a/app/assets/stylesheets/legislation_process.scss +++ b/app/assets/stylesheets/legislation_process.scss @@ -456,11 +456,11 @@ $border-dark: darken($border, 10%); span { vertical-align: inherit; font-style: normal; + } - a { - text-decoration: underline; - color: $text-medium; - } + .see-changes { + color: $text-medium; + text-decoration: underline; } } @@ -474,6 +474,7 @@ $border-dark: darken($border, 10%); } .draft-allegation { + @include breakpoint(medium) { display: flex; padding-left: 0.9375rem; @@ -490,7 +491,6 @@ $border-dark: darken($border, 10%); } } - // Panel calcs for desktop @media screen and (min-width: 40em) { .calc-index { width: calc(35% - 25px); @@ -506,6 +506,7 @@ $border-dark: darken($border, 10%); width: rem-calc(50); .draft-panel { + .panel-title { display: none; } @@ -909,19 +910,15 @@ $border-dark: darken($border, 10%); display: inline-block; } } - - .show-for-medium { - .panel-title { - display: none; - } - } } } } // 08. Legislation changes // ----------------- + .legislation-changes { + ul { list-style: none; margin-left: 0; @@ -933,35 +930,36 @@ $border-dark: darken($border, 10%); margin-right: 0.25rem; content: '—'; } + } + } - .changes-link { - display: block; - margin-left: 1rem; - font-size: $small-font-size; + .changes-link { + display: block; + margin-left: 1rem; + font-size: $small-font-size; - @include breakpoint(medium) { - display: inline-block; - } + @include breakpoint(medium) { + display: inline-block; + } - a { - span { - text-decoration: underline; - } + a { - .icon-external { - text-decoration: none; - color: #999; - line-height: 0; - vertical-align: sub; - margin-left: 0.5rem; - } + span { + text-decoration: underline; + } - &:active, - &:focus, - &:hover { - text-decoration: none; - } - } + .icon-external { + text-decoration: none; + color: #999; + line-height: 0; + vertical-align: sub; + margin-left: 0.5rem; + } + + &:active, + &:focus, + &:hover { + text-decoration: none; } } } @@ -969,6 +967,7 @@ $border-dark: darken($border, 10%); // 09. Legislation comments // ----------------- + .legislation-comments { .pull-right { @@ -1017,6 +1016,7 @@ $border-dark: darken($border, 10%); // 10. Legislation draft comment // ----------------- + .legislation-comment { .annotation-share-comment { diff --git a/app/assets/stylesheets/mixins.scss b/app/assets/stylesheets/mixins.scss index 401a2ec81..ac3c0a923 100644 --- a/app/assets/stylesheets/mixins.scss +++ b/app/assets/stylesheets/mixins.scss @@ -1,7 +1,9 @@ // Table of Contents // // 01. Logo -// +// 02. Orbit bullets +// 03. Direct uploads +// ------------------ // 01. Logo // -------- @@ -30,3 +32,108 @@ } } } + +// 02. Orbit bullet +// ---------------- + +@mixin orbit-bullets { + @include disable-mouse-outline; + position: relative; + margin-top: $orbit-bullet-margin-top; + margin-bottom: $orbit-bullet-margin-bottom; + text-align: center; + + button { + width: $orbit-bullet-diameter; + height: $orbit-bullet-diameter; + margin: $orbit-bullet-margin; + + border-radius: 50%; + background-color: $orbit-bullet-background; + + &:hover { + background-color: $orbit-bullet-background-active; + } + + &.is-active { + background-color: $orbit-bullet-background-active; + } + } +} + +// 03. Direct uploads +// ------------------ + +@mixin direct-uploads { + + .cached-image { + max-width: rem-calc(150); + max-height: rem-calc(150); + } + + .progress-bar-placeholder { + display: none; + margin-bottom: $line-height; + } + + .document, + .image { + + .document-attachment, + .image-attachment { + padding-left: 0; + + p { + margin-bottom: 0; + } + } + + .attachment-errors { + + > .js-image-attachment, + > .js-document-attachment { + display: none; + + ~ .error { + display: inline-block; + } + } + } + } + + .button { + font-weight: normal; + } + + .progress-bar { + width: 100%; + background-color: $light-gray; + } + + .file-name { + margin-top: 0; + } + + .loading-bar { + height: 5px; + width: 0; + transition: width 500ms ease-out; + + &.uploading { + background-color: $dark-gray; + } + + &.complete { + background-color: $success-color; + } + + &.errors { + background-color: $alert-color; + margin-top: $line-height / 2; + } + } + + .loading-bar.no-transition { + transition: none; + } +} diff --git a/app/assets/stylesheets/participation.scss b/app/assets/stylesheets/participation.scss index 3d90ee128..7d6a0fff7 100644 --- a/app/assets/stylesheets/participation.scss +++ b/app/assets/stylesheets/participation.scss @@ -249,14 +249,13 @@ .proposal-form, .budget-investment-form, .spending-proposal-form, -.document-form, .topic-new, .topic-form { .icon-debates, .icon-proposals, .icon-budget, - .icon-documents { + .icon-image { font-size: rem-calc(50); line-height: $line-height; opacity: 0.5; @@ -267,7 +266,7 @@ } .icon-proposals, - .icon-documents { + .icon-image { color: $proposals; } @@ -301,14 +300,21 @@ .proposal-form, .topic-form, -.topic-new, -.document-form { +.topic-new { .recommendations li::before { color: $proposals; } } +.budget-investment-new, +.proposal-form, +.proposal-edit, +.polls-form, +.poll-question-form { + @include direct-uploads; +} + // 03. Show participation // ---------------------- @@ -329,8 +335,16 @@ word-wrap: break-word; } - .callout.proposal-retired { - font-size: $base-font-size; + .callout { + &.token-message { + background-color: #fff; + border-color: $info-border; + color: $color-info; + } + + &.proposal-retired { + font-size: $base-font-size; + } } .social-share-full .social-share-button { @@ -349,8 +363,7 @@ width: rem-calc(48); } - .edit-debate, - .edit-proposal { + .edit-debate { margin-bottom: 0; } @@ -640,6 +653,71 @@ } } +.budget-investments-list .budget-investment, +.proposals-list .proposal { + + .no-image { + background: $brand; + } +} + +.budget-investments-list .budget-investment, +.proposals-list .proposal { + + @include breakpoint(small) { + + .no-image { + width: 100%; + max-width: rem-calc(300); + margin: 0 auto; + + &::before { + content: ''; + display: block; + padding-top: 100%; + } + } + } + + @include breakpoint(medium) { + + .panel { + + &.with-image { + padding: 0 $line-height / 2 0 0; + } + + .no-image { + height: 100%; + min-height: rem-calc(245); + width: rem-calc(140); + } + } + + .column:first-child { + overflow: hidden; + } + + .column:nth-child(2) { + float: left; + } + + .column:last-child:not(:first-child) { + padding-top: $line-height / 2; + } + + img { + max-width: 12rem; + } + + .budget-investment-content { + ul { + margin-bottom: 0; + } + } + } +} + .debate, .proposal, .investment-project, @@ -755,7 +833,7 @@ background: image-url('truncate.png'); background-repeat: repeat-x; bottom: 0; - height: 24px; + height: rem-calc(24); position: absolute; width: 100%; } @@ -769,12 +847,6 @@ display: none; } -.document-form { - max-width: 75rem; - margin-left: auto; - margin-right: auto; -} - .more-info { clear: both; color: $text-medium; @@ -859,6 +931,19 @@ } } +.budget-investment-show { + + figure { + margin: rem-calc(10) 0 0; + display: inline-block; + + figcaption { + font-size: $small-font-size; + margin-top: rem-calc(10); + } + } +} + .investment-project-show .supports, .budget-investment-show .supports { border: 0; @@ -1468,18 +1553,8 @@ // 08. Polls // ---------------------- -.dark-heading { - background: #2d3e50; - color: #fff; - - .title { - color: #92ba48; - } - - .button { - background: #fff; - color: $brand; - } +.polls-show-header { + background: #fafafa; .callout { @@ -1495,28 +1570,117 @@ color: $color-alert; } } +} - .info { - background: #314253; - padding: $line-height; +.poll-more-info, +.poll-more-info-answers { + border-top: 1px solid #eee; +} - @include breakpoint(medium) { - border-top: rem-calc(6) solid #92ba48; +.poll-more-info-answers { + background: #fafafa; + border-bottom: 1px solid #eee; + + .column:nth-child(odd) { + border-right: 2px solid $text; + } + + .answer-divider { + border-bottom: 2px solid $text; + border-right: 0 !important; + margin-bottom: $line-height; + padding-bottom: $line-height; + } + + .answer-description { + height: 100%; + + &.short { + height: $line-height * 12; + overflow: hidden; } } +} - a:not(.button) { - color: #fff; - text-decoration: underline; +.orbit-bullets button { + background-color: #ccc; + height: $line-height / 2; + width: $line-height / 2; + + &.is-active { + background-color: $brand; } +} - .back, - .icon-angle-left { - color: #fff; +.orbit-container { + height: 100% !important; + max-height: none !important; + + li { + margin-bottom: 0 !important; } +} - &.polls-show-header { - min-height: $line-height * 8; +.orbit-slide { + max-height: none !important; + + img { + image-rendering: pixelated; + } +} + +.orbit-caption { + background: #eee; + color: $text; +} + +.orbit-next, +.orbit-previous { + background: rgba(34, 34, 34, 0.25); +} + +.zoom-link { + background: #fff; + border-radius: rem-calc(48); + color: #000; + font-size: rem-calc(24); + font-weight: bold; + height: rem-calc(48); + line-height: rem-calc(48); + right: 12px; + padding-top: rem-calc(4); + position: absolute; + text-align: center; + top: 24px; + width: rem-calc(48); + z-index: 9; + + &:hover { + background: $dark; + color: #fff; + text-decoration: none; + } +} + +.image-container { + background: #fafafa; + overflow: hidden; + position: relative; +} + +.poll { + + &.with-image { + + @include breakpoint(medium) { + padding: 0 $line-height / 2 0 0; + } + + img { + height: 100%; + max-width: none; + position: absolute; + } } } @@ -1614,9 +1778,13 @@ } .section-title-divider { - border-bottom: 2px solid $brand; - color: $brand; - margin-bottom: $line-height; + border-bottom: 1px solid #eee; + color: #000; + margin: $line-height 0; + + span { + border-bottom: 1px solid #000; + } } .poll-question { @@ -1637,6 +1805,10 @@ margin-right: $line-height / 4; min-width: rem-calc(168); + @include breakpoint(medium down) { + width: 100%; + } + &.answered { background: #f4f8ec; border: 2px solid #92ba48; diff --git a/app/controllers/admin/poll/booth_assignments_controller.rb b/app/controllers/admin/poll/booth_assignments_controller.rb index 06eb233bd..8ffa455c8 100644 --- a/app/controllers/admin/poll/booth_assignments_controller.rb +++ b/app/controllers/admin/poll/booth_assignments_controller.rb @@ -15,7 +15,7 @@ class Admin::Poll::BoothAssignmentsController < Admin::Poll::BaseController end def show - @booth_assignment = @poll.booth_assignments.includes(:total_results, :voters, + @booth_assignment = @poll.booth_assignments.includes(:recounts, :voters, officer_assignments: [officer: [:user]]).find(params[:id]) @voters_by_date = @booth_assignment.voters.group_by {|v| v.created_at.to_date} end diff --git a/app/controllers/admin/poll/officer_assignments_controller.rb b/app/controllers/admin/poll/officer_assignments_controller.rb index 7fa120aa6..fd62df8b3 100644 --- a/app/controllers/admin/poll/officer_assignments_controller.rb +++ b/app/controllers/admin/poll/officer_assignments_controller.rb @@ -18,7 +18,7 @@ class Admin::Poll::OfficerAssignmentsController < Admin::Poll::BaseController @officer = ::Poll::Officer.includes(:user).find(officer_assignment_params[:officer_id]) @officer_assignments = ::Poll::OfficerAssignment. joins(:booth_assignment). - includes(:total_results, booth_assignment: :booth). + includes(:recounts, booth_assignment: :booth). where("officer_id = ? AND poll_booth_assignments.poll_id = ?", @officer.id, @poll.id). order(:date) end diff --git a/app/controllers/admin/poll/polls_controller.rb b/app/controllers/admin/poll/polls_controller.rb index c95c8ed1f..cce880ed4 100644 --- a/app/controllers/admin/poll/polls_controller.rb +++ b/app/controllers/admin/poll/polls_controller.rb @@ -1,7 +1,7 @@ class Admin::Poll::PollsController < Admin::Poll::BaseController load_and_authorize_resource - before_action :load_search, only: [:search_booths, :search_questions, :search_officers] + before_action :load_search, only: [:search_booths, :search_officers] before_action :load_geozones, only: [:new, :create, :edit, :update] def index @@ -47,25 +47,6 @@ class Admin::Poll::PollsController < Admin::Poll::BaseController redirect_to admin_poll_path(@poll), notice: notice end - def remove_question - question = ::Poll::Question.find(params[:question_id]) - - if @poll.questions.include? question - @poll.questions.delete(question) - notice = t("admin.polls.flash.question_removed") - else - notice = t("admin.polls.flash.error_on_question_removed") - end - redirect_to admin_poll_path(@poll), notice: notice - end - - def search_questions - @questions = ::Poll::Question.where("poll_id IS ? OR poll_id != ?", nil, @poll.id).search(search: @search).order(title: :asc) - respond_to do |format| - format.js - end - end - private def load_geozones @@ -73,7 +54,9 @@ class Admin::Poll::PollsController < Admin::Poll::BaseController end def poll_params - params.require(:poll).permit(:name, :starts_at, :ends_at, :geozone_restricted, geozone_ids: []) + params.require(:poll).permit(:name, :starts_at, :ends_at, :geozone_restricted, :summary, :description, + geozone_ids: [], + image_attributes: [:id, :title, :attachment, :cached_attachment, :user_id, :_destroy]) end def search_params diff --git a/app/controllers/admin/poll/questions/answers/images_controller.rb b/app/controllers/admin/poll/questions/answers/images_controller.rb new file mode 100644 index 000000000..1bf30907d --- /dev/null +++ b/app/controllers/admin/poll/questions/answers/images_controller.rb @@ -0,0 +1,33 @@ +class Admin::Poll::Questions::Answers::ImagesController < Admin::Poll::BaseController + before_action :load_answer + + def index + end + + def new + @answer = ::Poll::Question::Answer.find(params[:answer_id]) + end + + def create + @answer = ::Poll::Question::Answer.find(params[:answer_id]) + @answer.attributes = images_params + + if @answer.save + redirect_to admin_answer_images_path(@answer), + notice: "Image uploaded successfully" + else + render :new + end + end + + private + + def images_params + params.require(:poll_question_answer).permit(:answer_id, + images_attributes: [:id, :title, :attachment, :cached_attachment, :user_id, :_destroy]) + end + + def load_answer + @answer = ::Poll::Question::Answer.find(params[:answer_id]) + end +end diff --git a/app/controllers/admin/poll/questions/answers/videos_controller.rb b/app/controllers/admin/poll/questions/answers/videos_controller.rb new file mode 100644 index 000000000..a231c1a20 --- /dev/null +++ b/app/controllers/admin/poll/questions/answers/videos_controller.rb @@ -0,0 +1,57 @@ +class Admin::Poll::Questions::Answers::VideosController < Admin::Poll::BaseController + before_action :load_answer, only: [:index, :new, :create] + before_action :load_video, only: [:edit, :update, :destroy] + + def index + end + + def new + @video = ::Poll::Question::Answer::Video.new + end + + def create + @video = ::Poll::Question::Answer::Video.new(video_params) + + if @video.save + redirect_to admin_answer_videos_path(@answer), + notice: t("flash.actions.create.poll_question_answer_video") + else + render :new + end + end + + def edit + end + + def update + if @video.update(video_params) + redirect_to admin_answer_videos_path(@video.answer_id), + notice: t("flash.actions.save_changes.notice") + else + render :edit + end + end + + def destroy + if @video.destroy + notice = t("flash.actions.destroy.poll_question_answer_video") + else + notice = t("flash.actions.destroy.error") + end + redirect_to :back, notice: notice + end + + private + + def video_params + params.require(:poll_question_answer_video).permit(:title, :url, :answer_id) + end + + def load_answer + @answer = ::Poll::Question::Answer.find(params[:answer_id]) + end + + def load_video + @video = ::Poll::Question::Answer::Video.find(params[:id]) + end +end diff --git a/app/controllers/admin/poll/questions/answers_controller.rb b/app/controllers/admin/poll/questions/answers_controller.rb new file mode 100644 index 000000000..51c44dd94 --- /dev/null +++ b/app/controllers/admin/poll/questions/answers_controller.rb @@ -0,0 +1,52 @@ +class Admin::Poll::Questions::AnswersController < Admin::Poll::BaseController + before_action :load_answer, only: [:show, :edit, :update, :documents] + + load_and_authorize_resource :question, class: "::Poll::Question" + + def new + @answer = ::Poll::Question::Answer.new + end + + def create + @answer = ::Poll::Question::Answer.new(answer_params) + + if @answer.save + redirect_to admin_question_path(@answer.question), + notice: t("flash.actions.create.poll_question_answer") + else + render :new + end + end + + def show + end + + def edit + end + + def update + if @answer.update(answer_params) + redirect_to admin_question_path(@answer.question), + notice: t("flash.actions.save_changes.notice") + else + redirect_to :back + end + end + + def documents + @documents = @answer.documents + + render 'admin/poll/questions/answers/documents' + end + + private + + def answer_params + params.require(:poll_question_answer).permit(:title, :description, :question_id, documents_attributes: [:id, :title, :attachment, :cached_attachment, :user_id, :_destroy]) + end + + def load_answer + @answer = ::Poll::Question::Answer.find(params[:id] || params[:answer_id]) + end + +end diff --git a/app/controllers/admin/poll/questions_controller.rb b/app/controllers/admin/poll/questions_controller.rb index 8f6c18841..5cf587735 100644 --- a/app/controllers/admin/poll/questions_controller.rb +++ b/app/controllers/admin/poll/questions_controller.rb @@ -22,7 +22,6 @@ class Admin::Poll::QuestionsController < Admin::Poll::BaseController def create @question.author = @question.proposal.try(:author) || current_user - recover_documents_from_cache(@question) if @question.save redirect_to admin_question_path(@question) @@ -32,7 +31,6 @@ class Admin::Poll::QuestionsController < Admin::Poll::BaseController end def show - @document = Document.new(documentable: @question) end def edit @@ -58,8 +56,7 @@ class Admin::Poll::QuestionsController < Admin::Poll::BaseController private def question_params - params.require(:poll_question).permit(:poll_id, :title, :question, :description, :proposal_id, :valid_answers, :video_url, - documents_attributes: [:id, :title, :attachment, :cached_attachment, :user_id]) + params.require(:poll_question).permit(:poll_id, :title, :question, :proposal_id, :valid_answers, :video_url) end def search_params diff --git a/app/controllers/admin/poll/recounts_controller.rb b/app/controllers/admin/poll/recounts_controller.rb index 6d4a9a442..32c533eec 100644 --- a/app/controllers/admin/poll/recounts_controller.rb +++ b/app/controllers/admin/poll/recounts_controller.rb @@ -3,7 +3,7 @@ class Admin::Poll::RecountsController < Admin::Poll::BaseController def index @booth_assignments = @poll.booth_assignments. - includes(:booth, :total_results, :voters). + includes(:booth, :recounts, :voters). order("poll_booths.name"). page(params[:page]).per(50) end diff --git a/app/controllers/admin/poll/shifts_controller.rb b/app/controllers/admin/poll/shifts_controller.rb index 48f540c07..1f60b2b4e 100644 --- a/app/controllers/admin/poll/shifts_controller.rb +++ b/app/controllers/admin/poll/shifts_controller.rb @@ -1,7 +1,6 @@ class Admin::Poll::ShiftsController < Admin::Poll::BaseController before_action :load_booth - before_action :load_polls before_action :load_officer def new @@ -14,10 +13,10 @@ class Admin::Poll::ShiftsController < Admin::Poll::BaseController @officer = @shift.officer if @shift.save - notice = t("admin.poll_shifts.flash.create") - redirect_to new_admin_booth_shift_path(@shift.booth), notice: notice + redirect_to new_admin_booth_shift_path(@shift.booth), notice: t("admin.poll_shifts.flash.create") else load_shifts + flash[:error] = t("admin.poll_shifts.flash.date_missing") render :new end end @@ -30,7 +29,7 @@ class Admin::Poll::ShiftsController < Admin::Poll::BaseController end def search_officers - @officers = User.search(params[:search]).order(username: :asc) + @officers = User.search(params[:search]).order(username: :asc).select { |o| o.poll_officer? == true } end private @@ -39,10 +38,6 @@ class Admin::Poll::ShiftsController < Admin::Poll::BaseController @booth = ::Poll::Booth.find(params[:booth_id]) end - def load_polls - @polls = ::Poll.current_or_incoming - end - def load_shifts @shifts = @booth.shifts end @@ -54,7 +49,7 @@ class Admin::Poll::ShiftsController < Admin::Poll::BaseController end def shift_params - params.require(:shift).permit(:booth_id, :officer_id, :date) + shift_params = params.require(:shift).permit(:booth_id, :officer_id, :task, date:[:vote_collection_date, :recount_scrutiny_date]) + shift_params.merge(date: shift_params[:date]["#{shift_params[:task]}_date".to_sym]) end - end diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb index 63ec8cf52..14dfc6658 100644 --- a/app/controllers/admin/settings_controller.rb +++ b/app/controllers/admin/settings_controller.rb @@ -1,7 +1,7 @@ class Admin::SettingsController < Admin::BaseController def index - all_settings = (Setting.all).group_by { |s| s.type } + all_settings = Setting.all.group_by { |s| s.type } @settings = all_settings['common'] @feature_flags = all_settings['feature'] @banner_styles = all_settings['banner-style'] @@ -14,6 +14,13 @@ class Admin::SettingsController < Admin::BaseController redirect_to admin_settings_path, notice: t("admin.settings.flash.updated") end + def update_map + Setting["map_latitude"] = params[:latitude].to_f + Setting["map_longitude"] = params[:longitude].to_f + Setting["map_zoom"] = params[:zoom].to_i + redirect_to admin_settings_path, notice: t("admin.settings.index.map.flash.update") + end + private def settings_params diff --git a/app/controllers/budgets/investments_controller.rb b/app/controllers/budgets/investments_controller.rb index 799d1f76d..bb00f24af 100644 --- a/app/controllers/budgets/investments_controller.rb +++ b/app/controllers/budgets/investments_controller.rb @@ -44,12 +44,10 @@ module Budgets set_comment_flags(@comment_tree.comments) load_investment_votes(@investment) @investment_ids = [@investment.id] - @document = Document.new(documentable: @investment) end def create @investment.author = current_user - recover_documents_from_cache(@investment) if @investment.save Mailer.budget_investment_created(@investment).deliver_later @@ -105,9 +103,12 @@ module Budgets end def investment_params - params.require(:budget_investment).permit(:title, :description, :external_url, :heading_id, :tag_list, - :organization_name, :location, :terms_of_service, - documents_attributes: [:id, :title, :attachment, :cached_attachment, :user_id]) + params.require(:budget_investment) + .permit(:title, :description, :external_url, :heading_id, :tag_list, + :organization_name, :location, :terms_of_service, + image_attributes: [:id, :title, :attachment, :cached_attachment, :user_id, :_destroy], + documents_attributes: [:id, :title, :attachment, :cached_attachment, :user_id, :_destroy], + map_location_attributes: [:latitude, :longitude, :zoom]) end def load_ballot diff --git a/app/controllers/concerns/commentable_actions.rb b/app/controllers/concerns/commentable_actions.rb index 1c47b0c06..826d90da9 100644 --- a/app/controllers/concerns/commentable_actions.rb +++ b/app/controllers/concerns/commentable_actions.rb @@ -4,17 +4,22 @@ module CommentableActions include Search def index - @resources = @search_terms.present? ? resource_model.search(@search_terms) : resource_model.all - @resources = @advanced_search_terms.present? ? @resources.filter(@advanced_search_terms) : @resources + @resources = resource_model.all + @resources = @current_order == "recommendations" && current_user.present? ? @resources.recommendations(current_user) : @resources.for_render + @resources = @resources.search(@search_terms) if @search_terms.present? + @resources = @advanced_search_terms.present? ? @resources.filter(@advanced_search_terms) : @resources @resources = @resources.tagged_with(@tag_filter) if @tag_filter - @resources = @resources.page(params[:page]).for_render.send("sort_by_#{@current_order}") + + @resources = @resources.page(params[:page]).send("sort_by_#{@current_order}") + index_customization if index_customization.present? @tag_cloud = tag_cloud @banners = Banner.with_active set_resource_votes(@resources) + set_resources_instance end @@ -57,10 +62,7 @@ module CommentableActions end def update - resource.assign_attributes(strong_params) - recover_documents_from_cache(resource) - - if resource.save + if resource.update(strong_params) redirect_to resource, notice: t("flash.actions.update.#{resource_name.underscore}") else load_categories @@ -112,11 +114,4 @@ module CommentableActions nil end - def recover_documents_from_cache(resource) - return false unless resource.try(:documents) - resource.documents = resource.documents.each do |document| - document.set_attachment_from_cached_attachment if document.cached_attachment.present? - end - end - end diff --git a/app/controllers/debates_controller.rb b/app/controllers/debates_controller.rb index b8d6bdf10..cd113d486 100644 --- a/app/controllers/debates_controller.rb +++ b/app/controllers/debates_controller.rb @@ -10,7 +10,7 @@ class DebatesController < ApplicationController invisible_captcha only: [:create, :update], honeypot: :subtitle - has_orders %w{hot_score confidence_score created_at relevance}, only: :index + has_orders ->(c) { Debate.debates_orders(c.current_user) }, only: :index has_orders %w{most_voted newest oldest}, only: :show load_and_authorize_resource diff --git a/app/controllers/direct_uploads_controller.rb b/app/controllers/direct_uploads_controller.rb new file mode 100644 index 000000000..ade7d631f --- /dev/null +++ b/app/controllers/direct_uploads_controller.rb @@ -0,0 +1,49 @@ +class DirectUploadsController < ApplicationController + include DirectUploadsHelper + include ActionView::Helpers::UrlHelper + before_action :authenticate_user! + + load_and_authorize_resource except: :create + skip_authorization_check only: :create + + helper_method :render_destroy_upload_link + + def create + @direct_upload = DirectUpload.new(direct_upload_params.merge(user: current_user, attachment: params[:attachment])) + + if @direct_upload.valid? + @direct_upload.save_attachment + @direct_upload.relation.set_cached_attachment_from_attachment + + render json: { cached_attachment: @direct_upload.relation.cached_attachment, + filename: @direct_upload.relation.attachment.original_filename, + destroy_link: render_destroy_upload_link(@direct_upload).html_safe, + attachment_url: @direct_upload.relation.attachment.url + } + else + @direct_upload.destroy_attachment + render json: { errors: @direct_upload.errors[:attachment].join(", ") }, + status: 422 + end + end + + def destroy + @direct_upload = DirectUpload.new(direct_upload_params.merge(user: current_user) ) + @direct_upload.relation.set_attachment_from_cached_attachment + + if @direct_upload.destroy_attachment + render json: :ok + else + render json: :error + end + end + + private + + def direct_upload_params + params.require(:direct_upload) + .permit(:resource, :resource_type, :resource_id, :resource_relation, + :attachment, :cached_attachment, attachment_attributes: []) + end + +end \ No newline at end of file diff --git a/app/controllers/documents_controller.rb b/app/controllers/documents_controller.rb index 8f085e27f..001d23446 100644 --- a/app/controllers/documents_controller.rb +++ b/app/controllers/documents_controller.rb @@ -1,29 +1,7 @@ class DocumentsController < ApplicationController before_action :authenticate_user! - before_action :find_documentable, except: :destroy - before_action :prepare_new_document, only: [:new, :new_nested] - before_action :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 + load_and_authorize_resource def destroy respond_to do |format| @@ -45,57 +23,4 @@ class DocumentsController < ApplicationController end end - def destroy_upload - @document = Document.new(cached_attachment: params[:path]) - @document.set_attachment_from_cached_attachment - @document.cached_attachment = nil - @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/images_controller.rb b/app/controllers/images_controller.rb new file mode 100644 index 000000000..e273234f2 --- /dev/null +++ b/app/controllers/images_controller.rb @@ -0,0 +1,26 @@ +class ImagesController < ApplicationController + before_action :authenticate_user! + + load_and_authorize_resource + + def destroy + respond_to do |format| + format.html do + if @image.destroy + flash[:notice] = t "images.actions.destroy.notice" + else + flash[:alert] = t "images.actions.destroy.alert" + end + redirect_to params[:from] + end + format.js do + if @image.destroy + flash.now[:notice] = t "images.actions.destroy.notice" + else + flash.now[:alert] = t "images.actions.destroy.alert" + end + end + end + end + +end diff --git a/app/controllers/legislation/annotations_controller.rb b/app/controllers/legislation/annotations_controller.rb index 352fce266..ebbe05b12 100644 --- a/app/controllers/legislation/annotations_controller.rb +++ b/app/controllers/legislation/annotations_controller.rb @@ -30,7 +30,7 @@ class Legislation::AnnotationsController < ApplicationController def create if !@process.allegations_phase.open? || @draft_version.final_version? - render(json: {}, status: :not_found) && (return) + render(json: {}, status: :not_found) && return end existing_annotation = @draft_version.annotations.where( diff --git a/app/controllers/legislation/answers_controller.rb b/app/controllers/legislation/answers_controller.rb index 2e7e058e7..ecaa0037a 100644 --- a/app/controllers/legislation/answers_controller.rb +++ b/app/controllers/legislation/answers_controller.rb @@ -30,7 +30,7 @@ class Legislation::AnswersController < Legislation::BaseController def answer_params params.require(:legislation_answer).permit( - :legislation_question_option_id, + :legislation_question_option_id ) end diff --git a/app/controllers/management/document_verifications_controller.rb b/app/controllers/management/document_verifications_controller.rb index 687d8ee52..f59c00c8f 100644 --- a/app/controllers/management/document_verifications_controller.rb +++ b/app/controllers/management/document_verifications_controller.rb @@ -4,7 +4,7 @@ class Management::DocumentVerificationsController < Management::BaseController before_action :set_document, only: :check def index - @document_verification = Verification::Management::Document.new() + @document_verification = Verification::Management::Document.new end def check diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb index 99f401f7b..16a82de6a 100644 --- a/app/controllers/notifications_controller.rb +++ b/app/controllers/notifications_controller.rb @@ -11,7 +11,7 @@ class NotificationsController < ApplicationController def show @notification = current_user.notifications.find(params[:id]) - redirect_to url_for(@notification.linkable_resource) + redirect_to linkable_resource_path(@notification) end def mark_all_as_read @@ -25,4 +25,13 @@ class NotificationsController < ApplicationController @notification.mark_as_read end + def linkable_resource_path(notification) + case notification.linkable_resource.class.name + when "Budget::Investment" + budget_investment_path @notification.linkable_resource.budget, @notification.linkable_resource + else + url_for @notification.linkable_resource + end + end + end diff --git a/app/controllers/officing/polls_controller.rb b/app/controllers/officing/polls_controller.rb index 46bcf9f37..69b65ad23 100644 --- a/app/controllers/officing/polls_controller.rb +++ b/app/controllers/officing/polls_controller.rb @@ -7,7 +7,9 @@ class Officing::PollsController < Officing::BaseController def final @polls = if current_user.poll_officer? - current_user.poll_officer.final_days_assigned_polls.select {|poll| poll.ends_at > 2.week.ago && poll.expired?} + current_user.poll_officer.final_days_assigned_polls.select do |poll| + poll.ends_at > 2.weeks.ago && poll.expired? || poll.ends_at.today? + end else [] end diff --git a/app/controllers/officing/results_controller.rb b/app/controllers/officing/results_controller.rb index 65a6deac5..23ef0b038 100644 --- a/app/controllers/officing/results_controller.rb +++ b/app/controllers/officing/results_controller.rb @@ -26,9 +26,7 @@ class Officing::ResultsController < Officing::BaseController @partial_results = ::Poll::PartialResult.includes(:question). where(booth_assignment_id: index_params[:booth_assignment_id]). where(date: index_params[:date]) - @whites = ::Poll::WhiteResult.where(booth_assignment_id: @booth_assignment.id, date: index_params[:date]).sum(:amount) - @nulls = ::Poll::NullResult.where(booth_assignment_id: @booth_assignment.id, date: index_params[:date]).sum(:amount) - @total = ::Poll::TotalResult.where(booth_assignment_id: @booth_assignment.id, date: index_params[:date]).sum(:amount) + @recounts = ::Poll::Recount.where(booth_assignment_id: @booth_assignment.id, date: index_params[:date]) end end @@ -52,62 +50,37 @@ class Officing::ResultsController < Officing::BaseController go_back_to_new if question.blank? results.each_pair do |answer_index, count| - if count.present? - answer = question.valid_answers[answer_index.to_i] - go_back_to_new if question.blank? + next if count.blank? + answer = question.valid_answers[answer_index.to_i] + go_back_to_new if question.blank? - partial_result = ::Poll::PartialResult.find_or_initialize_by(booth_assignment_id: @officer_assignment.booth_assignment_id, - date: results_params[:date], - question_id: question_id, - answer: answer) - partial_result.officer_assignment_id = @officer_assignment.id - partial_result.amount = count.to_i - partial_result.author = current_user - partial_result.origin = 'booth' - @results << partial_result - end + partial_result = ::Poll::PartialResult.find_or_initialize_by(booth_assignment_id: @officer_assignment.booth_assignment_id, + date: results_params[:date], + question_id: question_id, + answer: answer) + partial_result.officer_assignment_id = @officer_assignment.id + partial_result.amount = count.to_i + partial_result.author = current_user + partial_result.origin = 'booth' + @results << partial_result end end - build_white_results - build_null_results - build_total_results + build_recounts end - def build_white_results - if results_params[:whites].present? - white_result = ::Poll::WhiteResult.find_or_initialize_by(booth_assignment_id: @officer_assignment.booth_assignment_id, - date: results_params[:date]) - white_result.officer_assignment_id = @officer_assignment.id - white_result.amount = results_params[:whites].to_i - white_result.author = current_user - white_result.origin = 'booth' - @results << white_result - end - end - - def build_null_results - if results_params[:nulls].present? - null_result = ::Poll::NullResult.find_or_initialize_by(booth_assignment_id: @officer_assignment.booth_assignment_id, - date: results_params[:date]) - null_result.officer_assignment_id = @officer_assignment.id - null_result.amount = results_params[:nulls].to_i - null_result.author = current_user - null_result.origin = 'booth' - @results << null_result - end - end - - def build_total_results - if results_params[:total].present? - total_result = ::Poll::TotalResult.find_or_initialize_by(booth_assignment_id: @officer_assignment.booth_assignment_id, - date: results_params[:date]) - total_result.officer_assignment_id = @officer_assignment.id - total_result.amount = results_params[:total].to_i - total_result.author = current_user - total_result.origin = 'booth' - @results << total_result + def build_recounts + recount = ::Poll::Recount.find_or_initialize_by(booth_assignment_id: @officer_assignment.booth_assignment_id, + date: results_params[:date]) + recount.officer_assignment_id = @officer_assignment.id + recount.author = current_user + recount.origin = 'booth' + [:whites, :nulls, :total].each do |recount_type| + if results_params[recount_type].present? + recount["#{recount_type.to_s.singularize}_amount"] = results_params[recount_type].to_i + end end + @results << recount end def go_back_to_new(alert = nil) @@ -120,7 +93,7 @@ class Officing::ResultsController < Officing::BaseController end def load_poll - @poll = ::Poll.expired.includes(:questions).find(params[:poll_id]) + @poll = ::Poll.includes(:questions).find(params[:poll_id]) end def load_officer_assignment diff --git a/app/controllers/officing/voters_controller.rb b/app/controllers/officing/voters_controller.rb index dee1e00bd..2b7ed329f 100644 --- a/app/controllers/officing/voters_controller.rb +++ b/app/controllers/officing/voters_controller.rb @@ -3,7 +3,8 @@ class Officing::VotersController < Officing::BaseController def new @user = User.find(params[:id]) - @polls = Poll.answerable_by(@user) + booths = current_user.poll_officer.shifts.current.vote_collection.pluck(:booth_id).uniq + @polls = Poll.answerable_by(@user).where(id: Poll::BoothAssignment.where(booth: booths).pluck(:poll_id).uniq) end def create @@ -12,7 +13,9 @@ class Officing::VotersController < Officing::BaseController @voter = Poll::Voter.new(document_type: @user.document_type, document_number: @user.document_number, user: @user, - poll: @poll) + poll: @poll, + origin: "booth", + officer: current_user.poll_officer) @voter.save! end diff --git a/app/controllers/polls/questions_controller.rb b/app/controllers/polls/questions_controller.rb index bb1560f54..407e6d984 100644 --- a/app/controllers/polls/questions_controller.rb +++ b/app/controllers/polls/questions_controller.rb @@ -5,25 +5,16 @@ class Polls::QuestionsController < ApplicationController has_orders %w{most_voted newest oldest}, only: :show - def show - @commentable = @question.proposal.present? ? @question.proposal : @question - @comment_tree = CommentTree.new(@commentable, params[:page], @current_order) - set_comment_flags(@comment_tree.comments) - - @document = Document.new(documentable: @question) - - question_answer = @question.answers.where(author_id: current_user.try(:id)).first - @answers_by_question_id = {@question.id => question_answer.try(:answer)} - end - def answer answer = @question.answers.find_or_initialize_by(author: current_user) + token = params[:token] answer.answer = params[:answer] + answer.touch if answer.persisted? answer.save! - answer.record_voter_participation + answer.record_voter_participation(token) - @answers_by_question_id = {@question.id => params[:answer]} + @answers_by_question_id = { @question.id => params[:answer] } end end diff --git a/app/controllers/polls_controller.rb b/app/controllers/polls_controller.rb index 41a038b46..064aa130f 100644 --- a/app/controllers/polls_controller.rb +++ b/app/controllers/polls_controller.rb @@ -1,5 +1,7 @@ class PollsController < ApplicationController + include PollsHelper + load_and_authorize_resource has_filters %w{current expired incoming} @@ -12,7 +14,7 @@ class PollsController < ApplicationController def show @questions = @poll.questions.for_render.sort_for_list - + @token = poll_voter_token(@poll, current_user) @answers_by_question_id = {} poll_answers = ::Poll::Answer.by_question(@poll.question_ids).by_author(current_user.try(:id)) poll_answers.each do |answer| diff --git a/app/controllers/proposals_controller.rb b/app/controllers/proposals_controller.rb index 06e170ee2..e86caf922 100644 --- a/app/controllers/proposals_controller.rb +++ b/app/controllers/proposals_controller.rb @@ -9,7 +9,7 @@ class ProposalsController < ApplicationController invisible_captcha only: [:create, :update], honeypot: :subtitle - has_orders %w{hot_score confidence_score created_at relevance archival_date}, only: :index + has_orders ->(c) { Proposal.proposals_orders(c.current_user) }, only: :index has_orders %w{most_voted newest oldest}, only: :show load_and_authorize_resource @@ -19,13 +19,11 @@ class ProposalsController < ApplicationController def show super @notifications = @proposal.notifications - @document = Document.new(documentable: @proposal) redirect_to proposal_path(@proposal), status: :moved_permanently if request.path != proposal_path(@proposal) end def create @proposal = Proposal.new(proposal_params.merge(author: current_user)) - recover_documents_from_cache(@proposal) if @proposal.save redirect_to share_proposal_path(@proposal), notice: I18n.t('flash.actions.create.proposal') @@ -78,7 +76,9 @@ class ProposalsController < ApplicationController def proposal_params params.require(:proposal).permit(:title, :question, :summary, :description, :external_url, :video_url, :responsible_name, :tag_list, :terms_of_service, :geozone_id, - documents_attributes: [:id, :title, :attachment, :cached_attachment, :user_id]) + image_attributes: [:id, :title, :attachment, :cached_attachment, :user_id, :_destroy], + documents_attributes: [:id, :title, :attachment, :cached_attachment, :user_id, :_destroy], + map_location_attributes: [:latitude, :longitude, :zoom]) end def retired_params @@ -113,7 +113,7 @@ class ProposalsController < ApplicationController end def load_featured - return unless !@advanced_search_terms && @search_terms.blank? && @tag_filter.blank? && params[:retired].blank? + return unless !@advanced_search_terms && @search_terms.blank? && @tag_filter.blank? && params[:retired].blank? && @current_order != "recommendations" @featured_proposals = Proposal.not_archived.sort_by_confidence_score.limit(3) if @featured_proposals.present? set_featured_proposal_votes(@featured_proposals) diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb new file mode 100644 index 000000000..c58067048 --- /dev/null +++ b/app/controllers/tags_controller.rb @@ -0,0 +1,11 @@ +class TagsController < ApplicationController + + load_and_authorize_resource class: ActsAsTaggableOn::Tag + respond_to :json + + def suggest + @tags = ActsAsTaggableOn::Tag.search(params[:search]).map(&:name) + respond_with @tags + end + +end diff --git a/app/controllers/welcome_controller.rb b/app/controllers/welcome_controller.rb index d0c650fa3..73b2b3aba 100644 --- a/app/controllers/welcome_controller.rb +++ b/app/controllers/welcome_controller.rb @@ -1,12 +1,10 @@ class WelcomeController < ApplicationController skip_authorization_check + before_action :set_user_recommendations, only: :index, if: :current_user layout "devise", only: [:welcome, :verification] def index - if current_user - redirect_to :proposals - end end def welcome @@ -16,4 +14,11 @@ class WelcomeController < ApplicationController redirect_to verification_path if signed_in? end + private + + def set_user_recommendations + @recommended_debates = Debate.recommendations(current_user).sort_by_recommendations.limit(3) + @recommended_proposals = Proposal.recommendations(current_user).sort_by_recommendations.limit(3) + end + end diff --git a/app/helpers/admin_helper.rb b/app/helpers/admin_helper.rb index 06fb158a4..765a8d9dd 100644 --- a/app/helpers/admin_helper.rb +++ b/app/helpers/admin_helper.rb @@ -25,7 +25,7 @@ module AdminHelper end def menu_polls? - ["polls", "questions", "officers", "booths", "officer_assignments", "booth_assignments", "recounts", "results", "shifts"].include? controller_name + %w[polls questions officers booths officer_assignments booth_assignments recounts results shifts questions answers].include? controller_name end def menu_profiles? diff --git a/app/helpers/debates_helper.rb b/app/helpers/debates_helper.rb index 8db989f61..e880f0831 100644 --- a/app/helpers/debates_helper.rb +++ b/app/helpers/debates_helper.rb @@ -4,4 +4,12 @@ module DebatesHelper Debate.all.featured.count > 0 end -end \ No newline at end of file + def empty_recommended_debates_message_text(user) + if user.interests.any? + t('debates.index.recommendations.without_results') + else + t('debates.index.recommendations.without_interests') + end + end + +end diff --git a/app/helpers/direct_uploads_helper.rb b/app/helpers/direct_uploads_helper.rb new file mode 100644 index 000000000..87ed630d7 --- /dev/null +++ b/app/helpers/direct_uploads_helper.rb @@ -0,0 +1,16 @@ +module DirectUploadsHelper + + def render_destroy_upload_link(direct_upload) + label = direct_upload.resource_relation == "image" ? "images" : "documents" + link_to t("#{label}.form.delete_button"), + direct_upload_destroy_url("direct_upload[resource_type]": direct_upload.resource_type, + "direct_upload[resource_id]": direct_upload.resource_id, + "direct_upload[resource_relation]": direct_upload.resource_relation, + "direct_upload[cached_attachment]": direct_upload.relation.cached_attachment, + format: :json), + method: :delete, + remote: true, + class: "delete remove-cached-attachment" + end + +end \ No newline at end of file diff --git a/app/helpers/documentables_helper.rb b/app/helpers/documentables_helper.rb index edbf88131..8d9f85578 100644 --- a/app/helpers/documentables_helper.rb +++ b/app/helpers/documentables_helper.rb @@ -8,12 +8,12 @@ module DocumentablesHelper documentable.class.max_documents_allowed end - def max_file_size(documentable) - bytes_to_mega(documentable.class.max_file_size) + def max_file_size(documentable_class) + bytes_to_mega(documentable_class.max_file_size) end - def accepted_content_types(documentable) - documentable.class.accepted_content_types + def accepted_content_types(documentable_class) + documentable_class.accepted_content_types end def accepted_content_types_extensions(documentable_class) @@ -22,16 +22,16 @@ module DocumentablesHelper .join(",") end - def humanized_accepted_content_types(documentable) - documentable.class.accepted_content_types + def documentable_humanized_accepted_content_types(documentable_class) + 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) + accepted_content_types: documentable_humanized_accepted_content_types(documentable.class), + max_file_size: max_file_size(documentable.class) end def max_documents_allowed?(documentable) diff --git a/app/helpers/documents_helper.rb b/app/helpers/documents_helper.rb index cc39f7857..d7e8d1dea 100644 --- a/app/helpers/documents_helper.rb +++ b/app/helpers/documents_helper.rb @@ -4,7 +4,7 @@ module DocumentsHelper document.attachment_file_name end - def errors_on_attachment(document) + def document_errors_on_attachment(document) document.errors[:attachment].join(', ') if document.errors.key?(:attachment) end @@ -12,78 +12,46 @@ module DocumentsHelper 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? + def render_destroy_document_link(builder, document) + if !document.persisted? && document.cached_attachment.present? 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" + direct_upload_destroy_url("direct_upload[resource_type]": document.documentable_type, + "direct_upload[resource_id]": document.documentable_id, + "direct_upload[resource_relation]": "documents", + "direct_upload[cached_attachment]": document.cached_attachment), + method: :delete, + remote: true, + class: "delete remove-cached-attachment" else - link_to t('documents.form.delete_button'), - "#", - class: "delete float-right remove-document" + link_to_remove_association t('documents.form.delete_button'), builder, class: "delete remove-document" end end - def render_attachment(document, index) - html = file_field_tag :attachment, - accept: accepted_content_types_extensions(document.documentable_type.constantize), - class: 'js-document-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 + def render_attachment(builder, document) + klass = document.errors[:attachment].any? ? "error" : "" + klass = document.persisted? || document.cached_attachment.present? ? " hide" : "" + html = builder.label :attachment, + t("documents.form.attachment_label"), + class: "button hollow #{klass}" + html += builder.file_field :attachment, + label: false, + accept: accepted_content_types_extensions(document.documentable_type.constantize), + class: 'js-document-attachment', + data: { + url: document_direct_upload_url(document), + nested_document: true + } html end def document_direct_upload_url(document) - upload_documents_url( - documentable_type: document.documentable_type, - documentable_id: document.documentable_id, - format: :js - ) + direct_uploads_url("direct_upload[resource_type]": document.documentable_type, + "direct_upload[resource_id]": document.documentable_id, + "direct_upload[resource_relation]": "documents") end end diff --git a/app/helpers/imageables_helper.rb b/app/helpers/imageables_helper.rb new file mode 100644 index 000000000..b1c8059ce --- /dev/null +++ b/app/helpers/imageables_helper.rb @@ -0,0 +1,40 @@ +module ImageablesHelper + + def can_destroy_image?(imageable) + imageable.image.present? && can?(:destroy, imageable.image) + end + + def imageable_class(imageable) + imageable.class.name.parameterize('_') + end + + def imageable_max_file_size + bytesToMeg(Image::MAX_IMAGE_SIZE) + end + + def bytesToMeg(bytes) + bytes / Numeric::MEGABYTE + end + + def imageable_accepted_content_types + Image::ACCEPTED_CONTENT_TYPE + end + + def imageable_accepted_content_types_extensions + Image::ACCEPTED_CONTENT_TYPE + .collect{ |content_type| ".#{content_type.split("/").last}" } + .join(",") + end + + def imageable_humanized_accepted_content_types + Image::ACCEPTED_CONTENT_TYPE + .collect{ |content_type| content_type.split("/").last } + .join(", ") + end + + def imageables_note(imageable) + t "images.form.note", accepted_content_types: imageable_humanized_accepted_content_types, + max_file_size: imageable_max_file_size + end + +end \ No newline at end of file diff --git a/app/helpers/images_helper.rb b/app/helpers/images_helper.rb new file mode 100644 index 000000000..4e2bdff0e --- /dev/null +++ b/app/helpers/images_helper.rb @@ -0,0 +1,79 @@ +module ImagesHelper + + def image_absolute_url(image, version) + return "" unless image + if Paperclip::Attachment.default_options[:storage] == :filesystem + URI(request.url) + image.attachment.url(version) + else + investment.image_url(version) + end + end + + def image_first_recommendation(image) + t "images.#{image.imageable.class.name.parameterize.underscore}.recommendation_one_html", + title: image.imageable.title + end + + def image_attachment_file_name(image) + image.attachment_file_name + end + + def image_errors_on_attachment(image) + image.errors[:attachment].join(', ') if image.errors.key?(:attachment) + end + + def image_bytesToMeg(bytes) + bytes / Numeric::MEGABYTE + end + + def image_class(image) + image.persisted? ? "persisted-image" : "cached-image" + end + + def render_destroy_image_link(builder, image) + if !image.persisted? && image.cached_attachment.present? + link_to t('images.form.delete_button'), + direct_upload_destroy_url("direct_upload[resource_type]": image.imageable_type, + "direct_upload[resource_id]": image.imageable_id, + "direct_upload[resource_relation]": "image", + "direct_upload[cached_attachment]": image.cached_attachment), + method: :delete, + remote: true, + class: "delete remove-cached-attachment" + else + link_to_remove_association t('images.form.delete_button'), builder, class: "delete remove-image" + end + end + + def render_image_attachment(builder, imageable, image) + klass = image.errors[:attachment].any? ? "error" : "" + klass = image.persisted? || image.cached_attachment.present? ? " hide" : "" + html = builder.label :attachment, + t("images.form.attachment_label"), + class: "button hollow #{klass}" + html += builder.file_field :attachment, + label: false, + accept: imageable_accepted_content_types_extensions, + class: 'js-image-attachment', + data: { + url: image_direct_upload_url(imageable), + nested_image: true + } + + html + end + + def render_image(image, version, show_caption = true) + version = image.persisted? ? version : :original + render partial: "images/image", locals: { image: image, + version: version, + show_caption: show_caption } + end + + def image_direct_upload_url(imageable) + direct_uploads_url("direct_upload[resource_type]": imageable.class.name, + "direct_upload[resource_id]": imageable.id, + "direct_upload[resource_relation]": "image") + end + +end diff --git a/app/helpers/map_locations_helper.rb b/app/helpers/map_locations_helper.rb new file mode 100644 index 000000000..ae45e7c1d --- /dev/null +++ b/app/helpers/map_locations_helper.rb @@ -0,0 +1,67 @@ +module MapLocationsHelper + + def map_location_available?(map_location) + map_location.present? && map_location.available? + end + + def map_location_latitude(map_location) + map_location.present? && map_location.latitude.present? ? map_location.latitude : Setting["map_latitude"] + end + + def map_location_longitude(map_location) + map_location.present? && map_location.longitude.present? ? map_location.longitude : Setting["map_longitude"] + end + + def map_location_zoom(map_location) + map_location.present? && map_location.zoom.present? ? map_location.zoom : Setting["map_zoom"] + end + + def map_location_input_id(prefix, attribute) + "#{prefix}_map_location_attributes_#{attribute}" + end + + def map_location_remove_marker_link_id(map_location) + "remove-marker-link-#{dom_id(map_location)}" + end + + def render_map(map_location, parent_class, editable, remove_marker_label) + map = content_tag_for :div, + map_location, + class: "map", + data: prepare_map_settings(map_location, editable, parent_class) + map += map_location_remove_marker(map_location, remove_marker_label) if editable + map + end + + def map_location_remove_marker(map_location, text) + content_tag :div, class: "text-right" do + content_tag :a, + id: map_location_remove_marker_link_id(map_location), + href: "#", + class: "location-map-remove-marker-button delete" do + text + end + end + end + + private + + def prepare_map_settings(map_location, editable, parent_class) + options = { + map: "", + map_center_latitude: map_location_latitude(map_location), + map_center_longitude: map_location_longitude(map_location), + map_zoom: map_location_zoom(map_location), + map_tiles_provider: Rails.application.secrets.map_tiles_provider, + map_tiles_provider_attribution: Rails.application.secrets.map_tiles_provider_attribution, + marker_editable: editable, + marker_latitude: map_location.latitude, + marker_longitude: map_location.longitude, + marker_remove_selector: "##{map_location_remove_marker_link_id(map_location)}", + latitude_input_selector: "##{map_location_input_id(parent_class, 'latitude')}", + longitude_input_selector: "##{map_location_input_id(parent_class, 'longitude')}", + zoom_input_selector: "##{map_location_input_id(parent_class, 'zoom')}" + } + end + +end diff --git a/app/helpers/poll_recounts_helper.rb b/app/helpers/poll_recounts_helper.rb index 716b71d85..45eefd696 100644 --- a/app/helpers/poll_recounts_helper.rb +++ b/app/helpers/poll_recounts_helper.rb @@ -1,7 +1,7 @@ module PollRecountsHelper def total_recounts_by_booth(booth_assignment) - booth_assignment.total_results.any? ? booth_assignment.total_results.to_a.sum(&:amount) : nil + booth_assignment.recounts.any? ? booth_assignment.recounts.to_a.sum(&:total_amount) : nil end end diff --git a/app/helpers/polls_helper.rb b/app/helpers/polls_helper.rb index 27d33ea04..dd4018be7 100644 --- a/app/helpers/polls_helper.rb +++ b/app/helpers/polls_helper.rb @@ -41,4 +41,12 @@ module PollsHelper booth.name + location end + def poll_voter_token(poll, user) + Poll::Voter.where(poll: poll, user: user, origin: "web").first&.token || '' + end + + def voted_before_sign_in(question) + question.answers.where(author: current_user).any? { |vote| current_user.current_sign_in_at >= vote.updated_at } + end + end diff --git a/app/helpers/proposals_helper.rb b/app/helpers/proposals_helper.rb index d40f7950c..9d5ac40cc 100644 --- a/app/helpers/proposals_helper.rb +++ b/app/helpers/proposals_helper.rb @@ -12,8 +12,8 @@ module ProposalsHelper percentage = (proposal.total_votes.to_f * 100 / Proposal.votes_needed_for_success) case percentage when 0 then "0%" - when 0..(0.1) then "0.1%" - when (0.1)..100 then number_to_percentage(percentage, strip_insignificant_zeros: true, precision: 1) + when 0..0.1 then "0.1%" + when 0.1..100 then number_to_percentage(percentage, strip_insignificant_zeros: true, precision: 1) else "100%" end end @@ -32,8 +32,12 @@ module ProposalsHelper Proposal::RETIRE_OPTIONS.collect { |option| [ t("proposals.retire_options.#{option}"), option ] } end - def can_create_document?(document, proposal) - can?(:create, document) && proposal.documents.size < Proposal.max_documents_allowed + def empty_recommended_proposals_message_text(user) + if user.interests.any? + t('proposals.index.recommendations.without_results') + else + t('proposals.index.recommendations.without_interests') + end end def author_of_proposal?(proposal) @@ -44,4 +48,4 @@ module ProposalsHelper current_user && proposal.editable_by?(current_user) end -end \ No newline at end of file +end diff --git a/app/helpers/shifts_helper.rb b/app/helpers/shifts_helper.rb index 37f22a3e2..dc9f97a91 100644 --- a/app/helpers/shifts_helper.rb +++ b/app/helpers/shifts_helper.rb @@ -1,11 +1,15 @@ 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]) + def shift_vote_collection_dates(polls) + date_options((start_date(polls)..end_date(polls))) + end + + def shift_recount_scrutiny_dates(polls) + date_options(polls.map(&:ends_at).map(&:to_date).sort.inject([]) { |total, date| total << (date..date + 1.week).to_a }.flatten.uniq) + end + + def date_options(dates) + dates.map { |date| [l(date, format: :long), l(date)] } end def start_date(polls) diff --git a/app/helpers/welcome_helper.rb b/app/helpers/welcome_helper.rb new file mode 100644 index 000000000..9f8c8c13e --- /dev/null +++ b/app/helpers/welcome_helper.rb @@ -0,0 +1,62 @@ +module WelcomeHelper + + def active_class(index) + "is-active is-in" if index == 0 + end + + def slide_display(index) + "display: none;" if index > 0 + end + + def recommended_path(recommended) + case recommended.class.name + when "Debate" + debate_path(recommended) + when "Proposal" + proposal_path(recommended) + else + '#' + end + end + + def render_recommendation_image(recommended, image_default) + image_path = calculate_image_path(recommended, image_default) + image_tag(image_path) if image_path.present? + end + + def calculate_image_path(recommended, image_default) + if recommended.try(:image) && recommended.image.present? && recommended.image.attachment.exists? + recommended.image.attachment.send("url", :medium) + elsif image_default.present? + image_default + end + end + + def calculate_carousel_size(debates, proposals, apply_offset) + offset = calculate_offset(debates, proposals, apply_offset) + centered = calculate_centered(debates, proposals) + "#{offset if offset} #{centered if centered}" + end + + def calculate_centered(debates, proposals) + if (debates.blank? && proposals.any?) || + (debates.any? && proposals.blank?) + centered = "medium-centered large-centered" + end + end + + def calculate_offset(debates, proposals, apply_offset) + if (debates.any? && proposals.any?) + if apply_offset + offset = "medium-offset-2 large-offset-2" + else + offset = "end" + end + end + end + + def highlight_background + (feature?("user.recommendations") && current_user) ? "highlight" : "" + end + +end diff --git a/app/models/abilities/administrator.rb b/app/models/abilities/administrator.rb index 9d2335f9d..795402003 100644 --- a/app/models/abilities/administrator.rb +++ b/app/models/abilities/administrator.rb @@ -51,7 +51,7 @@ module Abilities can [:read, :update, :valuate, :destroy, :summary], SpendingProposal - can [:index, :read, :new, :create, :update, :destroy, :calculate_winners], Budget + can [:index, :read, :new, :create, :update, :destroy, :calculate_winners, :read_results], Budget can [:read, :create, :update, :destroy], Budget::Group can [:read, :create, :update, :destroy], Budget::Heading can [:hide, :update, :toggle_selection], Budget::Investment @@ -62,7 +62,7 @@ module Abilities can [:index, :create, :edit, :update, :destroy], Geozone - can [:read, :create, :update, :destroy, :add_question, :remove_question, :search_booths, :search_questions, :search_officers], Poll + can [:read, :create, :update, :destroy, :add_question, :search_booths, :search_officers], Poll can [:read, :create, :update, :destroy, :available], Poll::Booth can [:search, :create, :index, :destroy], ::Poll::Officer can [:create, :destroy], ::Poll::BoothAssignment @@ -81,6 +81,8 @@ module Abilities cannot :comment_as_moderator, [::Legislation::Question, Legislation::Annotation, ::Legislation::Proposal] can [:create, :destroy], Document + can [:destroy], Image + can [:create, :destroy], DirectUpload end end end diff --git a/app/models/abilities/common.rb b/app/models/abilities/common.rb index 2de8e4c79..310cc743e 100644 --- a/app/models/abilities/common.rb +++ b/app/models/abilities/common.rb @@ -32,6 +32,7 @@ module Abilities can :suggest, Debate can :suggest, Proposal can :suggest, Legislation::Proposal + can :suggest, ActsAsTaggableOn::Tag can [:flag, :unflag], Comment cannot [:flag, :unflag], Comment, user_id: user.id @@ -47,8 +48,11 @@ module Abilities can [:create, :destroy], Follow - can [:create, :destroy, :new], Document, documentable: { author_id: user.id } - can [:new_nested, :upload, :destroy_upload], Document + can [:destroy], Document, documentable: { author_id: user.id } + + can [:destroy], Image, imageable: { author_id: user.id } + + can [:create, :destroy], DirectUpload unless user.organization? can :vote, Debate @@ -68,6 +72,7 @@ module Abilities can :suggest, Budget::Investment, budget: { phase: "accepting" } can :destroy, Budget::Investment, budget: { phase: ["accepting", "reviewing"] }, author_id: user.id can :vote, Budget::Investment, budget: { phase: "selecting" } + can [:show, :create], Budget::Ballot, budget: { phase: "balloting" } can [:create, :destroy], Budget::Ballot::Line, budget: { phase: "balloting" } diff --git a/app/models/budget/investment.rb b/app/models/budget/investment.rb index d658e7ab2..ddf6e819c 100644 --- a/app/models/budget/investment.rb +++ b/app/models/budget/investment.rb @@ -7,11 +7,12 @@ class Budget include Reclassification include Followable include Communitable + include Imageable + include Mappable include Documentable documentable max_documents_allowed: 3, max_file_size: 3.megabytes, accepted_content_types: [ "application/pdf" ] - accepts_nested_attributes_for :documents, allow_destroy: true acts_as_votable acts_as_paranoid column: :hidden_at diff --git a/app/models/concerns/documentable.rb b/app/models/concerns/documentable.rb index 729a0b0f8..36e91ece7 100644 --- a/app/models/concerns/documentable.rb +++ b/app/models/concerns/documentable.rb @@ -3,6 +3,7 @@ module Documentable included do has_many :documents, as: :documentable, dependent: :destroy + accepts_nested_attributes_for :documents, allow_destroy: true end module ClassMethods @@ -15,6 +16,7 @@ module Documentable @max_file_size = options[:max_file_size] @accepted_content_types = options[:accepted_content_types] end + end end diff --git a/app/models/concerns/followable.rb b/app/models/concerns/followable.rb index 84853b8ca..fee1ebd7f 100644 --- a/app/models/concerns/followable.rb +++ b/app/models/concerns/followable.rb @@ -4,6 +4,10 @@ module Followable included do has_many :follows, as: :followable, dependent: :destroy has_many :followers, through: :follows, source: :user + + scope :followed_by_user, -> (user){ + joins(:follows).where("follows.user_id = ?", user.id) + } end def followed_by?(user) diff --git a/app/models/concerns/galleryable.rb b/app/models/concerns/galleryable.rb new file mode 100644 index 000000000..1063c5571 --- /dev/null +++ b/app/models/concerns/galleryable.rb @@ -0,0 +1,12 @@ +module Galleryable + extend ActiveSupport::Concern + + included do + has_many :images, as: :imageable, dependent: :destroy + accepts_nested_attributes_for :images, allow_destroy: true, update_only: true + + def image_url(style) + image.attachment.url(style) if image && image.attachment.exists? + end + end +end \ No newline at end of file diff --git a/app/models/concerns/graphqlable.rb b/app/models/concerns/graphqlable.rb index 228810579..c8b47cece 100644 --- a/app/models/concerns/graphqlable.rb +++ b/app/models/concerns/graphqlable.rb @@ -24,7 +24,7 @@ module Graphqlable end def graphql_type_description - (model_name.human).to_s + model_name.human.to_s end end diff --git a/app/models/concerns/imageable.rb b/app/models/concerns/imageable.rb new file mode 100644 index 000000000..a2a2f537d --- /dev/null +++ b/app/models/concerns/imageable.rb @@ -0,0 +1,14 @@ +# can [:update, :destroy ], Image, :imageable_id => user.id, :imageable_type => 'User' +# and add a feature like forbidden/without_role_images_spec.rb to test it +module Imageable + extend ActiveSupport::Concern + + included do + has_one :image, as: :imageable, dependent: :destroy + accepts_nested_attributes_for :image, allow_destroy: true, update_only: true + + def image_url(style) + image.attachment.url(style) if image && image.attachment.exists? + end + end +end diff --git a/app/models/concerns/mappable.rb b/app/models/concerns/mappable.rb new file mode 100644 index 000000000..c108bdca7 --- /dev/null +++ b/app/models/concerns/mappable.rb @@ -0,0 +1,9 @@ +module Mappable + extend ActiveSupport::Concern + + included do + has_one :map_location, dependent: :destroy + accepts_nested_attributes_for :map_location, allow_destroy: true + end + +end diff --git a/app/models/concerns/verification.rb b/app/models/concerns/verification.rb index a4770117e..b8d23f594 100644 --- a/app/models/concerns/verification.rb +++ b/app/models/concerns/verification.rb @@ -55,10 +55,9 @@ module Verification end def user_type - case - when level_three_verified? + if level_three_verified? :level_3_user - when level_two_verified? + elsif level_two_verified? :level_2_user else :level_1_user diff --git a/app/models/debate.rb b/app/models/debate.rb index 9d74597b8..eb424c308 100644 --- a/app/models/debate.rb +++ b/app/models/debate.rb @@ -37,14 +37,21 @@ class Debate < ActiveRecord::Base scope :sort_by_random, -> { reorder("RANDOM()") } scope :sort_by_relevance, -> { all } scope :sort_by_flags, -> { order(flags_count: :desc, updated_at: :desc) } + scope :sort_by_recommendations, -> { order(cached_votes_total: :desc) } scope :last_week, -> { where("created_at >= ?", 7.days.ago)} scope :featured, -> { where("featured_at is not null")} scope :public_for_api, -> { all } + # Ahoy setup visitable # Ahoy will automatically assign visit_id on create attr_accessor :link_required + def self.recommendations(user) + tagged_with(user.interests, any: true). + where("author_id != ?", user.id) + end + def searchable_values { title => 'A', author.username => 'B', @@ -88,7 +95,7 @@ class Debate < ActiveRecord::Base def register_vote(user, vote_value) if votable_by?(user) - Debate.increment_counter(:cached_anonymous_votes_total, id) if (user.unverified? && !user.voted_for?(self)) + Debate.increment_counter(:cached_anonymous_votes_total, id) if user.unverified? && !user.voted_for?(self) vote_by(voter: user, vote: vote_value) end end @@ -135,4 +142,9 @@ class Debate < ActiveRecord::Base featured_at.present? end + def self.debates_orders(user) + orders = %w{hot_score confidence_score created_at relevance} + orders << "recommendations" if user.present? + orders + end end diff --git a/app/models/direct_upload.rb b/app/models/direct_upload.rb new file mode 100644 index 000000000..e597f9f12 --- /dev/null +++ b/app/models/direct_upload.rb @@ -0,0 +1,66 @@ +class DirectUpload + include ActiveModel::Validations + include ActiveModel::Conversion + extend ActiveModel::Naming + + attr_accessor :resource, :resource_type, :resource_id, + :relation, :resource_relation, + :attachment, :cached_attachment, :user + + validates_presence_of :attachment, :resource_type, :resource_relation, :user + validate :parent_resource_attachment_validations, + if: -> { attachment.present? && resource_type.present? && resource_relation.present? && user.present? } + + def initialize(attributes = {}) + attributes.each do |name, value| + send("#{name}=", value) + end + + if @resource_type.present? && @resource_relation.present? && (@attachment.present? || @cached_attachment.present?) + @resource = @resource_type.constantize.find_or_initialize_by(id: @resource_id) + + #Refactor + if @resource.respond_to?(:images) && + ((@attachment.present? && !@attachment.content_type.match(/pdf/)) || @cached_attachment.present?) + @relation = @resource.images.send("build", relation_attributtes) + elsif @resource.class.reflections[@resource_relation].macro == :has_one + @relation = @resource.send("build_#{resource_relation}", relation_attributtes) + else + @relation = @resource.send(@resource_relation).build(relation_attributtes) + end + + @relation.user = user + end + end + + def save_attachment + @relation.attachment.save + end + + def destroy_attachment + @relation.attachment.destroy + end + + def persisted? + false + end + + private + + def parent_resource_attachment_validations + @relation.valid? + + if @relation.errors.has_key? :attachment + errors[:attachment] = @relation.errors[:attachment] + end + end + + def relation_attributtes + { + attachment: @attachment, + cached_attachment: @cached_attachment, + user: @user + } + end + +end \ No newline at end of file diff --git a/app/models/document.rb b/app/models/document.rb index 3556d4c0a..f680bc04e 100644 --- a/app/models/document.rb +++ b/app/models/document.rb @@ -1,14 +1,17 @@ class Document < ActiveRecord::Base include DocumentsHelper include DocumentablesHelper - has_attached_file :attachment, path: ":rails_root/public/system/:class/:prefix/:style/:filename" + has_attached_file :attachment, url: "/system/:class/:prefix/:style/:hash.:extension", + hash_data: ":class/:style", + use_timestamp: false, + hash_secret: Rails.application.secrets.secret_key_base 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 + # Paperclip do not allow to use Procs on valiations definition do_not_validate_attachment_file_type :attachment validate :attachment_presence validate :validate_attachment_content_type, if: -> { attachment.present? } @@ -18,13 +21,14 @@ class Document < ActiveRecord::Base 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? } + before_save :set_attachment_from_cached_attachment, if: -> { cached_attachment.present? } + after_save :remove_cached_attachment, if: -> { cached_attachment.present? } - def set_cached_attachment_from_attachment(prefix) + def set_cached_attachment_from_attachment self.cached_attachment = if Paperclip::Attachment.default_options[:storage] == :filesystem attachment.path else - prefix + attachment.url + attachment.url end end @@ -40,7 +44,7 @@ class Document < ActiveRecord::Base attachment.instance.prefix(attachment, style) end - def prefix(attachment, _style) + def prefix(attachment, style) if !attachment.instance.persisted? "cached_attachments/user/#{attachment.instance.user_id}" else @@ -50,21 +54,25 @@ class Document < ActiveRecord::Base private + def documentable_class + documentable_type.constantize if documentable_type.present? + end + def validate_attachment_size - if documentable.present? && - attachment_file_size > documentable.class.max_file_size + if documentable_class.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") + max: "#{max_file_size(documentable_class)} MB") end end def validate_attachment_content_type - if documentable.present? && - !accepted_content_types(documentable).include?(attachment_content_type) + if documentable_class && + !accepted_content_types(documentable_class).include?(attachment_content_type) errors[:attachment] = I18n.t("documents.errors.messages.wrong_content_type", content_type: attachment_content_type, - accepted_content_types: humanized_accepted_content_types(documentable)) + accepted_content_types: documentable_humanized_accepted_content_types(documentable_class)) end end @@ -74,8 +82,12 @@ class Document < ActiveRecord::Base end end - def remove_cached_document - File.delete(cached_attachment) if File.exist?(cached_attachment) + def remove_cached_attachment + document = Document.new(documentable: documentable, + cached_attachment: cached_attachment, + user: user) + document.set_attachment_from_cached_attachment + document.attachment.destroy end end diff --git a/app/models/image.rb b/app/models/image.rb new file mode 100644 index 000000000..84b38dd4a --- /dev/null +++ b/app/models/image.rb @@ -0,0 +1,112 @@ +class Image < ActiveRecord::Base + include ImagesHelper + include ImageablesHelper + + TITLE_LEGHT_RANGE = 4..80 + MIN_SIZE = 475 + MAX_IMAGE_SIZE = 1.megabyte + ACCEPTED_CONTENT_TYPE = %w(image/jpeg image/jpg) + + has_attached_file :attachment, styles: { large: "x#{MIN_SIZE}", medium: "300x300#", thumb: "140x245#" }, + url: "/system/:class/:prefix/:style/:hash.:extension", + hash_data: ":class/:style", + use_timestamp: false, + hash_secret: Rails.application.secrets.secret_key_base + attr_accessor :cached_attachment + + belongs_to :user + belongs_to :imageable, polymorphic: true + + # Disable paperclip security validation due to polymorphic configuration + # Paperclip do not allow to use Procs on valiations definition + do_not_validate_attachment_file_type :attachment + validate :attachment_presence + validate :validate_attachment_content_type, if: -> { attachment.present? } + validate :validate_attachment_size, if: -> { attachment.present? } + validates :title, presence: true, length: { in: TITLE_LEGHT_RANGE } + validates :user_id, presence: true + validates :imageable_id, presence: true, if: -> { persisted? } + validates :imageable_type, presence: true, if: -> { persisted? } + validate :validate_image_dimensions, if: -> { attachment.present? && attachment.dirty? } + + before_save :set_attachment_from_cached_attachment, if: -> { cached_attachment.present? } + after_save :remove_cached_attachment, if: -> { cached_attachment.present? } + + def set_cached_attachment_from_attachment + self.cached_attachment = if Paperclip::Attachment.default_options[:storage] == :filesystem + attachment.path + else + 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 imageable_class + imageable_type.constantize if imageable_type.present? + end + + def validate_image_dimensions + if attachment_of_valid_content_type? + dimensions = Paperclip::Geometry.from_file(attachment.queued_for_write[:original].path) + errors.add(:attachment, :min_image_width, required_min_width: MIN_SIZE) if dimensions.width < MIN_SIZE + errors.add(:attachment, :min_image_height, required_min_height: MIN_SIZE) if dimensions.height < MIN_SIZE + end + end + + def validate_attachment_size + if imageable_class && + attachment_file_size > 1.megabytes + errors[:attachment] = I18n.t("images.errors.messages.in_between", + min: "0 Bytes", + max: "#{imageable_max_file_size} MB") + end + end + + def validate_attachment_content_type + if imageable_class && !attachment_of_valid_content_type? + errors[:attachment] = I18n.t("images.errors.messages.wrong_content_type", + content_type: attachment_content_type, + accepted_content_types: imageable_humanized_accepted_content_types) + end + end + + def attachment_presence + if attachment.blank? && cached_attachment.blank? + errors[:attachment] = I18n.t("errors.messages.blank") + end + end + + def attachment_of_valid_content_type? + attachment.present? && imageable_accepted_content_types.include?(attachment_content_type) + end + + def remove_cached_attachment + image = Image.new(imageable: imageable, + cached_attachment: cached_attachment, + user: user) + image.set_attachment_from_cached_attachment + image.attachment.destroy + end + +end diff --git a/app/models/map_location.rb b/app/models/map_location.rb new file mode 100644 index 000000000..14e91d92c --- /dev/null +++ b/app/models/map_location.rb @@ -0,0 +1,10 @@ +class MapLocation < ActiveRecord::Base + + belongs_to :proposal + belongs_to :investment, class_name: Budget::Investment + + def available? + latitude.present? && longitude.present? && zoom.present? + end + +end diff --git a/app/models/poll.rb b/app/models/poll.rb index 3873ac5e0..84349bf0b 100644 --- a/app/models/poll.rb +++ b/app/models/poll.rb @@ -1,10 +1,10 @@ class Poll < ActiveRecord::Base + include Imageable + has_many :booth_assignments, class_name: "Poll::BoothAssignment" has_many :booths, through: :booth_assignments has_many :partial_results, through: :booth_assignments - has_many :white_results, through: :booth_assignments - has_many :null_results, through: :booth_assignments - has_many :total_results, through: :booth_assignments + has_many :recounts, through: :booth_assignments has_many :voters has_many :officer_assignments, through: :booth_assignments has_many :officers, through: :officer_assignments @@ -16,23 +16,23 @@ class Poll < ActiveRecord::Base validate :date_range - scope :current, -> { where('starts_at <= ? and ? <= ends_at', Time.current, Time.current) } - scope :incoming, -> { where('? < starts_at', Time.current) } - scope :expired, -> { where('ends_at < ?', Time.current) } + scope :current, -> { where('starts_at <= ? and ? <= ends_at', Date.current.beginning_of_day, Date.current.beginning_of_day) } + scope :incoming, -> { where('? < starts_at', Date.current.beginning_of_day) } + scope :expired, -> { where('ends_at < ?', Date.current.beginning_of_day) } scope :published, -> { where('published = ?', true) } scope :by_geozone_id, ->(geozone_id) { where(geozones: {id: geozone_id}.joins(:geozones)) } scope :sort_for_list, -> { order(:geozone_restricted, :starts_at, :name) } - def current?(timestamp = DateTime.current) + def current?(timestamp = Date.current.beginning_of_day) starts_at <= timestamp && timestamp <= ends_at end - def incoming?(timestamp = DateTime.current) + def incoming?(timestamp = Date.current.beginning_of_day) timestamp < starts_at end - def expired?(timestamp = DateTime.current) + def expired?(timestamp = Date.current.beginning_of_day) ends_at < timestamp end @@ -61,6 +61,10 @@ class Poll < ActiveRecord::Base voters.where(document_number: document_number, document_type: document_type).exists? end + def voted_in_booth?(user) + Poll::Voter.where(poll: self, user: user, origin: "booth").exists? + end + def date_range unless starts_at.present? && ends_at.present? && starts_at <= ends_at errors.add(:starts_at, I18n.t('errors.messages.invalid_date_range')) diff --git a/app/models/poll/answer.rb b/app/models/poll/answer.rb index a1ff30f0a..4484060dd 100644 --- a/app/models/poll/answer.rb +++ b/app/models/poll/answer.rb @@ -8,12 +8,15 @@ class Poll::Answer < ActiveRecord::Base validates :question, presence: true validates :author, presence: true validates :answer, presence: true - validates :answer, inclusion: {in: ->(a) { a.question.valid_answers }} + + # temporary skipping validation, review when removing valid_answers + # validates :answer, inclusion: { in: ->(a) { a.question.valid_answers }}, + # unless: ->(a) { a.question.blank? } scope :by_author, ->(author_id) { where(author_id: author_id) } scope :by_question, ->(question_id) { where(question_id: question_id) } - def record_voter_participation - Poll::Voter.create!(user: author, poll: poll) + def record_voter_participation(token) + Poll::Voter.find_or_create_by(user: author, poll: poll, origin: "web", token: token) end -end \ No newline at end of file +end diff --git a/app/models/poll/booth_assignment.rb b/app/models/poll/booth_assignment.rb index 8b4b655ef..8489c3cf0 100644 --- a/app/models/poll/booth_assignment.rb +++ b/app/models/poll/booth_assignment.rb @@ -7,8 +7,6 @@ class Poll has_many :officers, through: :officer_assignments has_many :voters has_many :partial_results - has_many :white_results - has_many :null_results - has_many :total_results + has_many :recounts end end diff --git a/app/models/poll/officer.rb b/app/models/poll/officer.rb index bf4c73c36..384202455 100644 --- a/app/models/poll/officer.rb +++ b/app/models/poll/officer.rb @@ -2,6 +2,7 @@ class Poll class Officer < ActiveRecord::Base belongs_to :user has_many :officer_assignments, class_name: "Poll::OfficerAssignment" + has_many :shifts, class_name: "Poll::Shift" has_many :failed_census_calls, foreign_key: :poll_officer_id validates :user_id, presence: true, uniqueness: true diff --git a/app/models/poll/officer_assignment.rb b/app/models/poll/officer_assignment.rb index 8e86d309c..417e6a150 100644 --- a/app/models/poll/officer_assignment.rb +++ b/app/models/poll/officer_assignment.rb @@ -3,14 +3,12 @@ class Poll belongs_to :officer belongs_to :booth_assignment has_many :partial_results - has_many :white_results - has_many :null_results - has_many :total_results + has_many :recounts has_many :voters validates :officer_id, presence: true validates :booth_assignment_id, presence: true - validates :date, presence: true, uniqueness: { scope: [:officer_id, :booth_assignment_id] } + validates :date, presence: true delegate :poll_id, :booth_id, to: :booth_assignment diff --git a/app/models/poll/question.rb b/app/models/poll/question.rb index a46f28a8c..e6ef482a9 100644 --- a/app/models/poll/question.rb +++ b/app/models/poll/question.rb @@ -1,11 +1,6 @@ class Poll::Question < ActiveRecord::Base include Measurable include Searchable - include Documentable - documentable max_documents_allowed: 1, - max_file_size: 3.megabytes, - accepted_content_types: [ "application/pdf" ] - accepts_nested_attributes_for :documents, allow_destroy: true acts_as_paranoid column: :hidden_at include ActsAsParanoidAliases @@ -14,7 +9,8 @@ class Poll::Question < ActiveRecord::Base belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' has_many :comments, as: :commentable - has_many :answers + has_many :answers, class_name: 'Poll::Answer' + has_many :question_answers, class_name: 'Poll::Question::Answer' has_many :partial_results belongs_to :proposal @@ -23,7 +19,6 @@ class Poll::Question < ActiveRecord::Base validates :poll_id, presence: true validates :title, length: { minimum: 4 } - validates :description, length: { maximum: Poll::Question.description_max_length } scope :by_poll_id, ->(poll_id) { where(poll_id: poll_id) } @@ -40,15 +35,10 @@ class Poll::Question < ActiveRecord::Base def searchable_values { title => 'A', proposal.try(:title) => 'A', - description => 'B', author.username => 'C', author_visible_name => 'C' } end - def description - super.try :html_safe - end - def valid_answers (super.try(:split, ',').compact || []).map(&:strip) end @@ -59,7 +49,6 @@ class Poll::Question < ActiveRecord::Base self.author_visible_name = proposal.author.name self.proposal_id = proposal.id self.title = proposal.title - self.description = proposal.description self.valid_answers = I18n.t('poll_questions.default_valid_answers') end end diff --git a/app/models/poll/question/answer.rb b/app/models/poll/question/answer.rb new file mode 100644 index 000000000..b3324e57f --- /dev/null +++ b/app/models/poll/question/answer.rb @@ -0,0 +1,17 @@ +class Poll::Question::Answer < ActiveRecord::Base + include Galleryable + 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 + + belongs_to :question, class_name: 'Poll::Question', foreign_key: 'question_id' + has_many :videos, class_name: 'Poll::Question::Answer::Video' + + validates :title, presence: true + + def description + super.try :html_safe + end +end diff --git a/app/models/poll/question/answer/video.rb b/app/models/poll/question/answer/video.rb new file mode 100644 index 000000000..3d214af96 --- /dev/null +++ b/app/models/poll/question/answer/video.rb @@ -0,0 +1,16 @@ +class Poll::Question::Answer::Video < ActiveRecord::Base + belongs_to :answer, class_name: 'Poll::Question::Answer', foreign_key: 'answer_id' + + VIMEO_REGEX = /vimeo.*(staffpicks\/|channels\/|videos\/|video\/|\/)([^#\&\?]*).*/ + YOUTUBE_REGEX = /youtu.*(be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/ + + validates :title, presence: true + validate :valid_url? + + def valid_url? + return if url.blank? + return if url.match(VIMEO_REGEX) + return if url.match(YOUTUBE_REGEX) + errors.add(:url, :invalid) + end +end diff --git a/app/models/poll/recount.rb b/app/models/poll/recount.rb new file mode 100644 index 000000000..74a6fe937 --- /dev/null +++ b/app/models/poll/recount.rb @@ -0,0 +1,36 @@ +class Poll::Recount < ActiveRecord::Base + + VALID_ORIGINS = %w{web booth letter}.freeze + + belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' + belongs_to :booth_assignment + belongs_to :officer_assignment + + validates :author, presence: true + validates :origin, inclusion: {in: VALID_ORIGINS} + + scope :web, -> { where(origin: 'web') } + scope :booth, -> { where(origin: 'booth') } + scope :letter, -> { where(origin: 'letter') } + + scope :by_author, ->(author_id) { where(author_id: author_id) } + + before_save :update_logs + + def update_logs + amounts_changed = false + + [:white, :null, :total].each do |amount| + next unless send("#{amount}_amount_changed?") && send("#{amount}_amount_was").present? + self["#{amount}_amount_log"] += ":#{send("#{amount}_amount_was")}" + amounts_changed = true + end + + update_officer_author if amounts_changed + end + + def update_officer_author + self.officer_assignment_id_log += ":#{officer_assignment_id_was}" + self.author_id_log += ":#{author_id_was}" + end +end diff --git a/app/models/poll/shift.rb b/app/models/poll/shift.rb index d894df5a4..e3cfd2280 100644 --- a/app/models/poll/shift.rb +++ b/app/models/poll/shift.rb @@ -5,25 +5,41 @@ class Poll validates :booth_id, presence: true validates :officer_id, presence: true - validates :date, presence: true - validates :date, uniqueness: { scope: [:officer_id, :booth_id] } + validates :date, presence: true, uniqueness: { scope: [:officer_id, :booth_id, :task] } + validates :task, presence: true + + enum task: { vote_collection: 0, recount_scrutiny: 1 } + + scope :vote_collection, -> { where(task: 'vote_collection') } + scope :recount_scrutiny, -> { where(task: 'recount_scrutiny') } + scope :current, -> { where(date: Date.current) } before_create :persist_data 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 + before_destroy :destroy_officer_assignments def persist_data self.officer_name = officer.name self.officer_email = officer.email end + def create_officer_assignments + booth.booth_assignments.each do |booth_assignment| + attrs = { + officer_id: officer_id, + date: date, + booth_assignment_id: booth_assignment.id, + final: recount_scrutiny? + } + Poll::OfficerAssignment.create!(attrs) + end + end + + def destroy_officer_assignments + Poll::OfficerAssignment.where(booth_assignment: booth.booth_assignments, + officer: officer, + date: date, + final: recount_scrutiny?).destroy_all + end end end diff --git a/app/models/poll/voter.rb b/app/models/poll/voter.rb index 760096206..bc0118a88 100644 --- a/app/models/poll/voter.rb +++ b/app/models/poll/voter.rb @@ -1,18 +1,26 @@ class Poll class Voter < ActiveRecord::Base + + VALID_ORIGINS = %w{ web booth } + belongs_to :poll belongs_to :user belongs_to :geozone belongs_to :booth_assignment belongs_to :officer_assignment + belongs_to :officer validates :poll_id, presence: true validates :user_id, presence: true validates :document_number, presence: true, uniqueness: { scope: [:poll_id, :document_type], message: :has_voted } + validates :origin, inclusion: { in: VALID_ORIGINS } before_validation :set_demographic_info, :set_document_info + scope :web, -> { where(origin: 'web') } + scope :booth, -> { where(origin: 'booth') } + def set_demographic_info return if user.blank? diff --git a/app/models/proposal.rb b/app/models/proposal.rb index f9eeecc6f..1a688ab30 100644 --- a/app/models/proposal.rb +++ b/app/models/proposal.rb @@ -10,11 +10,12 @@ class Proposal < ActiveRecord::Base include Graphqlable include Followable include Communitable + include Imageable + include Mappable 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 include EmbedVideosHelper acts_as_votable @@ -57,14 +58,27 @@ class Proposal < ActiveRecord::Base scope :sort_by_relevance, -> { all } scope :sort_by_flags, -> { order(flags_count: :desc, updated_at: :desc) } scope :sort_by_archival_date, -> { archived.sort_by_confidence_score } + scope :sort_by_recommendations, -> { order(cached_votes_up: :desc) } scope :archived, -> { where("proposals.created_at <= ?", Setting["months_to_archive_proposals"].to_i.months.ago) } scope :not_archived, -> { where("proposals.created_at > ?", Setting["months_to_archive_proposals"].to_i.months.ago) } scope :last_week, -> { where("proposals.created_at >= ?", 7.days.ago)} scope :retired, -> { where.not(retired_at: nil) } scope :not_retired, -> { where(retired_at: nil) } scope :successful, -> { where("cached_votes_up >= ?", Proposal.votes_needed_for_success) } + scope :unsuccessful, -> { where("cached_votes_up < ?", Proposal.votes_needed_for_success) } scope :public_for_api, -> { all } + def self.recommendations(user) + tagged_with(user.interests, any: true). + where("author_id != ?", user.id). + unsuccessful. + not_followed_by_user(user) + end + + def self.not_followed_by_user(user) + where.not(id: followed_by_user(user).pluck(:id)) + end + def to_param "#{id}-#{title}".parameterize end @@ -88,7 +102,7 @@ class Proposal < ActiveRecord::Base def self.search_by_code(terms) matched_code = match_code(terms) results = where(id: matched_code[1]) if matched_code - return results if (results.present? && results.first.code == terms) + return results if results.present? && results.first.code == terms end def self.match_code(terms) @@ -184,6 +198,12 @@ class Proposal < ActiveRecord::Base (voters + followers).uniq end + def self.proposals_orders(user) + orders = %w{hot_score confidence_score created_at relevance archival_date} + orders << "recommendations" if user.present? + orders + end + protected def set_responsible_name diff --git a/app/models/user.rb b/app/models/user.rb index 88fb1ee18..a212c1b5c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -88,12 +88,12 @@ class User < ActiveRecord::Base end def debate_votes(debates) - voted = votes.for_debates(debates) + voted = votes.for_debates(Array(debates).map(&:id)) voted.each_with_object({}) { |v, h| h[v.votable_id] = v.value } end def proposal_votes(proposals) - voted = votes.for_proposals(proposals) + voted = votes.for_proposals(Array(proposals).map(&:id)) voted.each_with_object({}) { |v, h| h[v.votable_id] = v.value } end diff --git a/app/views/admin/_menu.html.erb b/app/views/admin/_menu.html.erb index 168f8c31b..f0350b30f 100644 --- a/app/views/admin/_menu.html.erb +++ b/app/views/admin/_menu.html.erb @@ -1,5 +1,5 @@
-
    +
    • @@ -53,19 +53,20 @@
    • <% end %> - <% if feature?(:polls) %>
    • <%= t("admin.menu.title_polls") %> -
        > +
          >
        • > <%= link_to t('admin.menu.polls'), admin_polls_path %>
        • -
        • > +
        • > <%= link_to t("admin.menu.poll_questions"), admin_questions_path %>
        • @@ -158,12 +159,14 @@ <%= t("admin.menu.title_site_customization") %> -
            > +
              >
            • > <%= link_to t("admin.menu.site_customization.pages"), admin_site_customization_pages_path %>
            • -
            • > +
            • > <%= link_to t("admin.menu.site_customization.images"), admin_site_customization_images_path %>
            • diff --git a/app/views/admin/dashboard/index.html.erb b/app/views/admin/dashboard/index.html.erb index 2a2ed1c56..ca64da28d 100644 --- a/app/views/admin/dashboard/index.html.erb +++ b/app/views/admin/dashboard/index.html.erb @@ -1,99 +1,9 @@ -<%= link_to admin_settings_path, class: "button float-right" do %> - - <%= t("admin.menu.settings") %> -<% end %> - -<%= link_to admin_stats_path, class: "button float-right" do %> - - <%= t("admin.menu.stats") %> -<% end %> - -

              <%= t("admin.dashboard.index.title") %>

              - -

              Desde aquí puedes administrar el sistema, a través de las siguientes acciones:

              - -
              -
                - -
              • - Temas de debate -
                -

                Los temas (también llamadas tags, o etiquetas) de debate son palabras que definen los usuarios al crear debates, para catalogarlos (ej: sanidad, movilidad, arganzuela, ...). Aquí se pueden eliminar temas inapropiados, o marcarlos para ser propuestos al crear debates (cada usuario puede definir los que quiera, pero se le sugieren algunos que nos parecen útiles como catalogación por defecto; aquí se puede cambiar cuáles se sugieren)

                -
                -
              • - -
              • - Propuestas/Debates/Comentarios ocultos -
                -

                Cuando un moderador o un administrador oculta una Propuesta/Debate/Comentario aparecerá en esta lista. De esta forma los administradores pueden revisar que se ha ocultado el elemento adecuado.

                - -
                  -
                • Al pulsar Confirmar se acepta el que se haya ocultado, se considera que se ha hecho correctamente.
                • -
                • Al pulsar Volver a mostrar se revierte la acción de ocultar y vuelve a ser una Propuesta/Debate/Comentario visible, en el caso de que se considere - que ha sido una acción errónea el haberlo ocultado.
                • -
                - -

                Para facilitar la gestión, arriba encontramos un filtro con las secciones: "pendientes" (los elementos sobre los que todavía no se ha pulsado "confirmar" o "volver a mostrar", que deberían ser revisados todavía), "confirmados" y "todos".

                - -

                Es recomendable revisar regularmente la sección "pendientes".

                -
                -
              • - -
              • - Usuarios bloqueados -
                -

                Cuando un moderador o un administrador bloquea a un usuario aparecerá en esta lista. Al bloquear a un usuario, éste deja de poder utilizarlo para ninguna acción de la web. Los administradores pueden desbloquearlos pulsando el botón al lado del nombre del usuario en la lista.

                -
                -
              • - -
              • - Organizaciones -
                -

                En la web hay dos tipos de usuarios: individuales y organizaciones. Cualquier persona puede crear usuarios de un tipo o de otro en la propia web. Los usuarios de organizaciones pueden ser verificados por parte de los administradores, confirmando que quien gestiona el usuario efectivamente representa a esa organización. Una vez se haya realizado el proceso de verificación, por el proceso externo a la web que se haya definido para ello, se pulsa el botón "Verificar" para confimarlo; lo que hará que al lado del nombre de la organización aparezca una etiqueta señalando que es una organización verificada.

                - -

                En caso de que el proceso de verificación haya sido negativo, se pulsa el botón "Rechazar". Para editar alguno de los datos de la organización, se pulsa el botón "Editar".

                - -

                En caso de que el proceso de verificación haya sido negativo, se pulsa el botón "Rechazar". Para editar alguno de los datos de la organización, se pulsa el botón "Editar".

                - -

                Las organizaciones que no aparecen en la lista pueden ser encontradas para actuar sobre ellas por medio del buscador en la parte superior. Para facilitar la gestión, arriba - encontramos un filtro con las secciones: "pendientes" (las organizaciones que todavía no han sido verificadas o rechazadas), "verificadas", "rechazadas" y "todas".

                -

                Es recomendable revisar regularmente la sección "pendientes".

                -
                -
              • - -
              • - Cargos Públicos -
                -

                En la web, los usuarios individuales pueden ser usuarios normales, o cargos públicos. Estos últimos se diferencian de los primeros únicamente en que al lado de sus nombres aparece una etiqueta que les identifica, y cambia ligeramente el estilo de sus comentarios. Esto permite que los usuarios les identifiquen más fácilmente. Al lado de cada usuario vemos la identificación que aparece en su etiqueta, y su nivel (la manera que internamente usa la web para diferenciar entre un tipo de cargos y otros). Pulsando el botón "Editar" al lado del usuario, se puede modificar su información. Los cargos públicos que no aparecen en la lista pueden ser encontrados para actuar sobre ellos por medio del buscador en la parte superior.

                -
                -
              • - -
              • - Moderadores -
                -

                Mediante el buscador de la parte superior se pueden buscar usuarios, para activarlos o desactivarlos como moderadores de la web. Los moderadores al acceder a la web con su usuario ven en la parte superior una nueva sección llamada "Moderar"

                -
                -
              • - -
              • - Actividad de moderadores -
                -

                En esta sección se va guardando todas las acciones que realizan los moderadores o los administradores respecto a la moderación: ocultar/mostrar Propuestas/Debates/Comentarios y bloquear usuarios. En la columna "Acción" comprobamos si la acción corresponde con ocultar o con volver a mostrar (restaurar) elementos o con bloquear usuarios. En las demás columnas tenemos el tipo de elemento, el contenido del elemento y el moderador o administrador que ha realizado la acción. Esta sección permite que los administradores detecten comportamientos irregulares por parte de moderadores específicos y que por lo tanto puedan corregirlos.

                -
                -
              • - -
              • - Configuración Global -
                -

                Opciones generales de configuración del sistema.

                -
                -
              • - -
              • - Estadísticas -
                -

                Estadísticas generales del sistema.

                -
                -
              • -
              +
              + <%= link_to root_path do %> + <%= t("admin.dashboard.index.back", org: setting['org_name']) %> + <% end %>
              + +

              <%= t("admin.dashboard.index.title") %>

              + +

              <%= t("admin.dashboard.index.description", org: setting['org_name']) %>

              diff --git a/app/views/admin/poll/booth_assignments/show.html.erb b/app/views/admin/poll/booth_assignments/show.html.erb index b6917bca6..fd303d099 100644 --- a/app/views/admin/poll/booth_assignments/show.html.erb +++ b/app/views/admin/poll/booth_assignments/show.html.erb @@ -14,15 +14,15 @@
                -
              • +
              • <%= link_to t("admin.poll_booth_assignments.show.officers"), "#tab-officers" %>
              • -
              • +
              • <%= link_to t("admin.poll_booth_assignments.show.recounts"), "#tab-recounts" %>
              -
              +
              <% if @booth_assignment.officers.empty? %>
              <%= t("admin.poll_booth_assignments.show.no_officers") %> @@ -43,27 +43,36 @@ <% end %>
              -
              +

              <%= t("admin.poll_booth_assignments.show.recounts_list") %>

              + + + + + + + + + + + + + + +
              <%= t("admin.poll_booth_assignments.show.count_final") %><%= t("admin.poll_booth_assignments.show.total_system") %>
              <%= total_recounts_by_booth(@booth_assignment) || '-' %><%= @booth_assignment.voters.count %>
              + - <% (@poll.starts_at.to_date..@poll.ends_at.to_date).each do |voting_date| %> - <% total_recount = @booth_assignment.total_results.where(date: voting_date).first %> <% system_count = @voters_by_date[voting_date].present? ? @voters_by_date[voting_date].size : 0 %> - <% if total_recount.present? %> - - <% else %> - - <% end %> <% end %> diff --git a/app/views/admin/poll/booths/_booth.html.erb b/app/views/admin/poll/booths/_booth.html.erb index 80b1ef38f..921361c4a 100644 --- a/app/views/admin/poll/booths/_booth.html.erb +++ b/app/views/admin/poll/booths/_booth.html.erb @@ -6,11 +6,13 @@ <%= booth.location %> - \ No newline at end of file + diff --git a/app/views/admin/poll/booths/index.html.erb b/app/views/admin/poll/booths/index.html.erb index 05044dd0e..95b248b3f 100644 --- a/app/views/admin/poll/booths/index.html.erb +++ b/app/views/admin/poll/booths/index.html.erb @@ -1,7 +1,8 @@

              <%= t("admin.booths.index.title") %>

              -<%= link_to t("admin.booths.index.add_booth"), new_admin_booth_path, - class: "button success float-right" %> +<% if controller_name == "booths" && action_name != "available" %> + <%= link_to t("admin.booths.index.add_booth"), new_admin_booth_path, class: "button success float-right" %> +<% end %> <% if @booths.empty? %>
              diff --git a/app/views/admin/poll/officer_assignments/_search_officers_results.html.erb b/app/views/admin/poll/officer_assignments/_search_officers_results.html.erb index f665c6c0b..ee009f1fa 100644 --- a/app/views/admin/poll/officer_assignments/_search_officers_results.html.erb +++ b/app/views/admin/poll/officer_assignments/_search_officers_results.html.erb @@ -12,29 +12,19 @@
              - <% @officers.each do |user| %> - <% end %> diff --git a/app/views/admin/poll/officer_assignments/by_officer.html.erb b/app/views/admin/poll/officer_assignments/by_officer.html.erb index 6ea421e2d..c6738eba0 100644 --- a/app/views/admin/poll/officer_assignments/by_officer.html.erb +++ b/app/views/admin/poll/officer_assignments/by_officer.html.erb @@ -21,36 +21,10 @@ <% @officer_assignments.each do |officer_assignment| %> - + <% end %>
              <%= t("admin.poll_booth_assignments.show.date") %><%= t("admin.poll_booth_assignments.show.total_recount") %> <%= t("admin.poll_booth_assignments.show.count_by_system") %>
              <%= l voting_date %><%= total_recount.amount %> - <%= system_count %>
              - <%= link_to t("admin.booths.booth.shifts"), - new_admin_booth_shift_path(booth), - class: "button hollow" %> - <%= link_to t("admin.actions.edit"), - edit_admin_booth_path(booth), - class: "button hollow" %> + <% if controller_name == "shifts" || controller_name == "booths" && action_name == "available" %> + <%= link_to t("admin.booths.booth.shifts"), + new_admin_booth_shift_path(booth), + class: "button hollow" %> + <%= link_to t("admin.actions.edit"), + edit_admin_booth_path(booth), + class: "button hollow" %> + <% end %>
              <%= t("admin.poll_officer_assignments.index.table_name") %> <%= t("admin.poll_officer_assignments.index.table_email") %><%= t("admin.polls.show.table_assignment") %>
              - <%= user.name %> + + <%= link_to user.name, by_officer_admin_poll_officer_assignments_path(@poll, officer_id: user.id) %> + <%= user.email %> - <% if @poll.officer_ids.include?(user.poll_officer.id) %> - <%= link_to t("admin.poll_officer_assignments.index.edit_officer_assignments"), - by_officer_admin_poll_officer_assignments_path(@poll, officer_id: user.poll_officer.id), - class: "button hollow alert" %> - <% else %> - <%= link_to t("admin.poll_officer_assignments.index.add_officer_assignments"), - by_officer_admin_poll_officer_assignments_path(@poll, officer_id: user.poll_officer.id), - class: "button hollow" %> - <% end %> -
              <%= officer_assignment.final? ? t('polls.final_date') : l(officer_assignment.date.to_date) %><%= l(officer_assignment.date.to_date)%> <%= content_tag :strong, t('polls.final_date') if officer_assignment.final %> <%= booth_name_with_location(officer_assignment.booth_assignment.booth) %>
              - -

              <%= t("admin.poll_officer_assignments.by_officer.total_recounts") %>

              - - - - - - - - - - <% @officer_assignments.each do |officer_assignment| %> - - - - - - <% end %> - -
              <%= t("admin.poll_officer_assignments.by_officer.date") %><%= t("admin.poll_officer_assignments.by_officer.booth") %><%= t("admin.poll_officer_assignments.by_officer.total_recount") %>
              <%= l(officer_assignment.date.to_date) %><%= booth_name_with_location(officer_assignment.booth_assignment.booth) %> - <% if officer_assignment.total_results.any? %> - <%= officer_assignment.total_results.to_a.sum(&:amount) %> - <% else %> - - - <% end %> -
              <% end %> diff --git a/app/views/admin/poll/officers/_officer.html.erb b/app/views/admin/poll/officers/_officer.html.erb index 80434f385..3562b4a90 100644 --- a/app/views/admin/poll/officers/_officer.html.erb +++ b/app/views/admin/poll/officers/_officer.html.erb @@ -16,7 +16,7 @@ <% if officer.persisted? %> <%= link_to t('admin.poll_officers.officer.delete'), - admin_poll_officer_path(officer), + admin_officer_path(officer), method: :delete, class: "button hollow alert" %> <% else %> diff --git a/app/views/admin/poll/polls/_form.html.erb b/app/views/admin/poll/polls/_form.html.erb index 0b99ea9f5..aff19d9d7 100644 --- a/app/views/admin/poll/polls/_form.html.erb +++ b/app/views/admin/poll/polls/_form.html.erb @@ -19,6 +19,22 @@
              +
              +
              + <%=f.text_area :summary, rows: 4%> +
              +
              + +
              +
              + <%=f.text_area :description, rows: 8%> +
              +
              + +
              + <%= render 'images/admin_image', imageable: @poll, f: f %> +
              +
              <%= f.check_box :geozone_restricted, data: { checkbox_toggle: "#geozones" } %> diff --git a/app/views/admin/poll/polls/_questions.html.erb b/app/views/admin/poll/polls/_questions.html.erb index e41d2017e..fb92a4e43 100644 --- a/app/views/admin/poll/polls/_questions.html.erb +++ b/app/views/admin/poll/polls/_questions.html.erb @@ -9,7 +9,6 @@ <%= t('admin.polls.show.table_title') %> - <%= t('admin.polls.show.table_assignment') %> <% @poll.questions.each do |question| %> @@ -19,12 +18,6 @@ <%= link_to question.title, admin_question_path(question) %> - - <%= link_to t('admin.polls.show.remove_question'), - remove_question_admin_poll_path(poll_id: @poll.id, question_id: question.id), - class: "button hollow alert", - method: :patch %> - <% end %> diff --git a/app/views/admin/poll/polls/_search_questions.html.erb b/app/views/admin/poll/polls/_search_questions.html.erb deleted file mode 100644 index 659cdcd37..000000000 --- a/app/views/admin/poll/polls/_search_questions.html.erb +++ /dev/null @@ -1,17 +0,0 @@ -
              -
              - <%= form_tag(search_questions_admin_poll_path(@poll), method: :get, remote: true) do |f| %> -
              - <%= text_field_tag :search, - @search, - placeholder: t("admin.shared.poll_questions_search.placeholder"), id: "search-questions" %> - -
              - <%= submit_tag t("admin.shared.poll_questions_search.button"), class: "button" %> -
              -
              - <% end %> -
              -
              - -
              diff --git a/app/views/admin/poll/polls/_search_questions_results.html.erb b/app/views/admin/poll/polls/_search_questions_results.html.erb deleted file mode 100644 index f6f11e1f1..000000000 --- a/app/views/admin/poll/polls/_search_questions_results.html.erb +++ /dev/null @@ -1,33 +0,0 @@ -<% if @questions.blank? %> -
              - <%= t('admin.shared.no_search_results') %> -
              -<% else %> -

              <%= t('admin.shared.search_results') %>

              -<% end %> - -<% if @questions.any? %> - - - - - - - - - <% @questions.each do |question| %> - - - - - <% end %> - -
              <%= t("admin.polls.show.table_name") %><%= t("admin.polls.show.table_assignment") %>
              - <%= question.title %> - - <%= link_to t("admin.polls.show.add_question"), - add_question_admin_poll_path(poll_id: @poll.id, question_id: question.id), - method: :patch, - class: "button hollow" %> -
              -<% end %> diff --git a/app/views/admin/poll/polls/_subnav.html.erb b/app/views/admin/poll/polls/_subnav.html.erb index 249ff407c..24d7a5b1f 100644 --- a/app/views/admin/poll/polls/_subnav.html.erb +++ b/app/views/admin/poll/polls/_subnav.html.erb @@ -1,8 +1,10 @@