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 4704696e5..f8d4bb811 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -283,7 +283,6 @@ Lint/ParenthesesAsGroupedExpression: # Cop supports --auto-correct. Lint/StringConversionInInterpolation: Exclude: - - 'app/models/poll/final_recount.rb' - 'app/models/poll/null_result.rb' - 'app/models/poll/partial_result.rb' - 'app/models/poll/white_result.rb' @@ -455,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. @@ -481,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 @@ -528,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: @@ -573,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. @@ -626,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_ @@ -669,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. @@ -699,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/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 2ff87100a..0b3ee0201 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 @@ -60,9 +61,12 @@ //= require legislation_annotatable //= require watch_form_changes //= require followable +//= require flaggable //= require documentable +//= require imageable //= require tree_navigator //= require custom +//= require tag_autocomplete var initialize_modules = function() { App.Comments.initialize(); @@ -97,10 +101,12 @@ var initialize_modules = function() { App.WatchFormChanges.initialize(); App.TreeNavigator.initialize(); App.Documentable.initialize(); + App.Imageable.initialize(); + App.TagAutocomplete.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 8683ce5e2..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.document_ajax_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.document_ajax_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/flaggable.js.coffee b/app/assets/javascripts/flaggable.js.coffee new file mode 100644 index 000000000..7ada75686 --- /dev/null +++ b/app/assets/javascripts/flaggable.js.coffee @@ -0,0 +1,4 @@ +App.Flaggable = + + update: (resource_id, button) -> + $("#" + resource_id + " .js-flag-actions").html(button).foundation() 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/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/admin.scss b/app/assets/stylesheets/admin.scss index 6ac728e2c..3dcf4a474 100644 --- a/app/assets/stylesheets/admin.scss +++ b/app/assets/stylesheets/admin.scss @@ -13,33 +13,99 @@ // 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 +120,8 @@ $admin-color: #cf3638; } } - th, td { + th, + td { text-align: left; &.text-center { @@ -110,6 +177,7 @@ $admin-color: #cf3638; .menu.simple .active { border-bottom: 2px solid $admin-color; color: $admin-color; + font-weight: bold; } .tabs-panel { @@ -195,6 +263,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 +278,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 +289,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 +322,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; } } diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 19c73de32..361dcfde3 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -16,4 +16,5 @@ @import 'annotator_overrides'; @import 'jquery-ui/datepicker'; @import 'datepicker_overrides'; -@import 'documentable'; +@import 'jquery-ui/autocomplete'; +@import 'autocomplete_overrides'; 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/community.scss b/app/assets/stylesheets/community.scss index 0dddf4cdb..c06d5c5d2 100644 --- a/app/assets/stylesheets/community.scss +++ b/app/assets/stylesheets/community.scss @@ -1,5 +1,6 @@ .communities-show { - .button.disabled, .button[disabled] { + .button.disabled, + .button[disabled] { pointer-events: none; } diff --git a/app/assets/stylesheets/datepicker_overrides.scss b/app/assets/stylesheets/datepicker_overrides.scss index 79414ad99..190784d15 100644 --- a/app/assets/stylesheets/datepicker_overrides.scss +++ b/app/assets/stylesheets/datepicker_overrides.scss @@ -44,18 +44,18 @@ position: absolute; top: 4px; width: rem-calc(30); - + &:hover { - text-decoration: none; + text-decoration: none; } } - + .ui-datepicker-prev::after { - content: '\62'; + content: '\62'; } - + .ui-datepicker-next::after { - content: '\63'; + content: '\63'; } table { diff --git a/app/assets/stylesheets/documentable.scss b/app/assets/stylesheets/documentable.scss deleted file mode 100644 index 2aa015a14..000000000 --- a/app/assets/stylesheets/documentable.scss +++ /dev/null @@ -1,59 +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; - } - - input.document_ajax_attachment[type=file]{ - display: none; - } - - .file-name { - margin-top: $line-height / 2; - } - - .loading-bar { - height: 5px; - width: 0; - transition: width 500ms ease-out; - - &.uploading { - background-color: $dark-gray; - } - - &.complete { - background-color: $success-color; - width: 100%; - } - - &.errors { - background-color: $alert-color; - width: 100%; - margin-top: $line-height / 2; - } - } - - .loading-bar.no-transition { - transition: none; - } -} diff --git a/app/assets/stylesheets/icons.scss b/app/assets/stylesheets/icons.scss index 5e92ee460..e55832667 100644 --- a/app/assets/stylesheets/icons.scss +++ b/app/assets/stylesheets/icons.scss @@ -197,6 +197,10 @@ content: '\53'; } +.icon-image::before { + content: '\68'; +} + .icon-notification::before { content: '\6e'; } diff --git a/app/assets/stylesheets/layout.scss b/app/assets/stylesheets/layout.scss index 9d7b31a12..d05dbd54b 100644 --- a/app/assets/stylesheets/layout.scss +++ b/app/assets/stylesheets/layout.scss @@ -206,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; } } @@ -313,10 +334,6 @@ a { } } -.no-max-width { - max-width: none; -} - .button.float-right ~ .button.float-right { margin: 0 $line-height / 2; } @@ -380,7 +397,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; @@ -2284,68 +2305,7 @@ table { } } -// 20. Documents -.document-form form { - - .radio-buttons { - label { - margin-right: $line-height; - } - } - - .source-option-link { - input { - padding-bottom: 0; - } - - .error { - margin-bottom: $line-height; - } - - label { - &.error { - margin-bottom: 0; - } - } - } - - .source-option-file { - .file-name { - label { - - @include breakpoint(small medium) { - float: none; - } - - @include breakpoint(large) { - float: left; - } - } - - p { - - @include breakpoint(small medium) { - float: none; - margin-top: 0; - margin-left: 0; - margin-bottom: 0; - } - - @include breakpoint(large) { - float: left; - margin-bottom: 0; - margin-top: $line-height / 2; - margin-left: $line-height; - } - } - } - } - - .attachment-errors { - margin-bottom: $line-height; - } -} - +// 19. Documents .documents-list { table { diff --git a/app/assets/stylesheets/legislation_process.scss b/app/assets/stylesheets/legislation_process.scss index 2ec9f8ee9..0e73321d3 100644 --- a/app/assets/stylesheets/legislation_process.scss +++ b/app/assets/stylesheets/legislation_process.scss @@ -459,11 +459,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; } } @@ -477,6 +477,7 @@ $border-dark: darken($border, 10%); } .draft-allegation { + @include breakpoint(medium) { display: flex; padding-left: 0.9375rem; @@ -493,7 +494,6 @@ $border-dark: darken($border, 10%); } } - // Panel calcs for desktop @media screen and (min-width: 40em) { .calc-index { width: calc(35% - 25px); @@ -509,6 +509,7 @@ $border-dark: darken($border, 10%); width: rem-calc(50); .draft-panel { + .panel-title { display: none; } @@ -912,19 +913,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; @@ -936,35 +933,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; } } } @@ -972,6 +970,7 @@ $border-dark: darken($border, 10%); // 09. Legislation comments // ----------------- + .legislation-comments { .pull-right { @@ -1020,6 +1019,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 70f4c179d..0028683c2 100644 --- a/app/assets/stylesheets/mixins.scss +++ b/app/assets/stylesheets/mixins.scss @@ -32,8 +32,8 @@ } } - -//02. Orbit bullet +// 02. Orbit bullet +// ---------------- @mixin orbit-bullets { @include disable-mouse-outline; position: relative; @@ -58,3 +58,72 @@ } } } + +// 02. Direct uploads +// ------------------ +@mixin direct-uploads { + + .cached-image { + max-width: 150px; + max-height: 150px; + } + + .progress-bar-placeholder { + display: none; + margin-bottom: $line-height; + } + + .document, + .image { + + .document-attachment, + .image-attachment { + padding-left:0; + + p{ + margin-bottom: 0; + } + } + input.js-document-attachment, + input.js-image-attachment{ + display: none; + } + } + + .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 8c6fc5415..fbcda0ebb 100644 --- a/app/assets/stylesheets/participation.scss +++ b/app/assets/stylesheets/participation.scss @@ -249,14 +249,14 @@ .proposal-form, .budget-investment-form, .spending-proposal-form, -.document-form, .topic-new, -.topic-form { +.topic-form { .icon-debates, .icon-proposals, .icon-budget, - .icon-documents { + .icon-documents, + .icon-image { font-size: rem-calc(50); line-height: $line-height; opacity: 0.5; @@ -267,7 +267,8 @@ } .icon-proposals, - .icon-documents { + .icon-documents, + .icon-image { color: $proposals; } @@ -301,14 +302,22 @@ .proposal-form, .topic-form, -.topic-new, -.document-form { +.topic-new { .recommendations li::before { color: $proposals; } } +.budget-investment-new, +.proposal-form, +.proposal-edit, +.new_poll_question, +.edit_poll_question { + @include direct-uploads; +} + + // 03. Show participation // ---------------------- @@ -640,6 +649,77 @@ } } +.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: 300px; + margin: 0 auto; + } + + .no-image::before { + content: ''; + display: block; + padding-top: 100%; + } + + h3 { + font-size: 1.3rem; + } + + .column:first-child { + text-align: center; + } + } + + @include breakpoint(medium) { + .panel { + padding: 0 0.75rem 0 0; + + .no-image { + height: 245px; + width: 140px; + } + } + + h3 { + font-size: 1.4rem; + } + + .column:first-child { + overflow: hidden; + } + + .column:nth-child(2) { + float: left; + } + + .column:last-child:not(:first-child) { + padding-top: 0.75rem; + } + + img { + max-width: 12rem; + } + + .budget-investment-content { + ul { + margin-bottom: 0; + } + } + } +} + .debate, .proposal, .investment-project, @@ -769,12 +849,6 @@ display: none; } -.document-form{ - max-width: 75rem; - margin-left: auto; - margin-right: auto; -} - .more-info { clear: both; color: $text-medium; @@ -859,6 +933,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; diff --git a/app/controllers/admin/poll/base_controller.rb b/app/controllers/admin/poll/base_controller.rb new file mode 100644 index 000000000..c58e4f23c --- /dev/null +++ b/app/controllers/admin/poll/base_controller.rb @@ -0,0 +1,10 @@ +class Admin::Poll::BaseController < Admin::BaseController + helper_method :namespace + + private + + def namespace + "admin" + end + +end diff --git a/app/controllers/admin/poll/booth_assignments_controller.rb b/app/controllers/admin/poll/booth_assignments_controller.rb index 358f26a65..06eb233bd 100644 --- a/app/controllers/admin/poll/booth_assignments_controller.rb +++ b/app/controllers/admin/poll/booth_assignments_controller.rb @@ -1,4 +1,4 @@ -class Admin::Poll::BoothAssignmentsController < Admin::BaseController +class Admin::Poll::BoothAssignmentsController < Admin::Poll::BaseController before_action :load_poll, except: [:create, :destroy] @@ -15,7 +15,7 @@ class Admin::Poll::BoothAssignmentsController < Admin::BaseController end def show - @booth_assignment = @poll.booth_assignments.includes(:final_recounts, :voters, + @booth_assignment = @poll.booth_assignments.includes(:total_results, :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/booths_controller.rb b/app/controllers/admin/poll/booths_controller.rb index 4b322f0b2..7a3a1370d 100644 --- a/app/controllers/admin/poll/booths_controller.rb +++ b/app/controllers/admin/poll/booths_controller.rb @@ -1,4 +1,4 @@ -class Admin::Poll::BoothsController < Admin::BaseController +class Admin::Poll::BoothsController < Admin::Poll::BaseController load_and_authorize_resource class: 'Poll::Booth' def index @@ -41,4 +41,4 @@ class Admin::Poll::BoothsController < Admin::BaseController params.require(:poll_booth).permit(:name, :location) end -end \ No newline at end of file +end diff --git a/app/controllers/admin/poll/officer_assignments_controller.rb b/app/controllers/admin/poll/officer_assignments_controller.rb index 45c9a225a..7fa120aa6 100644 --- a/app/controllers/admin/poll/officer_assignments_controller.rb +++ b/app/controllers/admin/poll/officer_assignments_controller.rb @@ -1,4 +1,4 @@ -class Admin::Poll::OfficerAssignmentsController < Admin::BaseController +class Admin::Poll::OfficerAssignmentsController < Admin::Poll::BaseController before_action :load_poll before_action :redirect_if_blank_required_params, only: [:by_officer] @@ -18,7 +18,7 @@ class Admin::Poll::OfficerAssignmentsController < Admin::BaseController @officer = ::Poll::Officer.includes(:user).find(officer_assignment_params[:officer_id]) @officer_assignments = ::Poll::OfficerAssignment. joins(:booth_assignment). - includes(:final_recounts, booth_assignment: :booth). + includes(:total_results, 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/officers_controller.rb b/app/controllers/admin/poll/officers_controller.rb index 2641a12b5..1d5c19634 100644 --- a/app/controllers/admin/poll/officers_controller.rb +++ b/app/controllers/admin/poll/officers_controller.rb @@ -1,4 +1,4 @@ -class Admin::Poll::OfficersController < Admin::BaseController +class Admin::Poll::OfficersController < Admin::Poll::BaseController load_and_authorize_resource :officer, class: "Poll::Officer", except: [:edit, :show] def index @@ -36,4 +36,4 @@ class Admin::Poll::OfficersController < Admin::BaseController def edit end -end \ No newline at end of file +end diff --git a/app/controllers/admin/poll/polls_controller.rb b/app/controllers/admin/poll/polls_controller.rb index cad91f218..c95c8ed1f 100644 --- a/app/controllers/admin/poll/polls_controller.rb +++ b/app/controllers/admin/poll/polls_controller.rb @@ -1,4 +1,4 @@ -class Admin::Poll::PollsController < Admin::BaseController +class Admin::Poll::PollsController < Admin::Poll::BaseController load_and_authorize_resource before_action :load_search, only: [:search_booths, :search_questions, :search_officers] diff --git a/app/controllers/admin/poll/questions_controller.rb b/app/controllers/admin/poll/questions_controller.rb index 3bbe1c395..97b301d43 100644 --- a/app/controllers/admin/poll/questions_controller.rb +++ b/app/controllers/admin/poll/questions_controller.rb @@ -1,4 +1,4 @@ -class Admin::Poll::QuestionsController < Admin::BaseController +class Admin::Poll::QuestionsController < Admin::Poll::BaseController include CommentableActions load_and_authorize_resource :poll @@ -22,7 +22,6 @@ class Admin::Poll::QuestionsController < Admin::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::BaseController end def show - @document = Document.new(documentable: @question) end def edit @@ -59,7 +57,7 @@ class Admin::Poll::QuestionsController < Admin::BaseController 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]) + documents_attributes: [:id, :title, :attachment, :cached_attachment, :user_id, :_destroy]) end def search_params diff --git a/app/controllers/admin/poll/recounts_controller.rb b/app/controllers/admin/poll/recounts_controller.rb index 57289a207..6d4a9a442 100644 --- a/app/controllers/admin/poll/recounts_controller.rb +++ b/app/controllers/admin/poll/recounts_controller.rb @@ -1,9 +1,9 @@ -class Admin::Poll::RecountsController < Admin::BaseController +class Admin::Poll::RecountsController < Admin::Poll::BaseController before_action :load_poll def index @booth_assignments = @poll.booth_assignments. - includes(:booth, :final_recounts, :voters). + includes(:booth, :total_results, :voters). order("poll_booths.name"). page(params[:page]).per(50) end diff --git a/app/controllers/admin/poll/results_controller.rb b/app/controllers/admin/poll/results_controller.rb index 2c5bbba27..8d1d22d91 100644 --- a/app/controllers/admin/poll/results_controller.rb +++ b/app/controllers/admin/poll/results_controller.rb @@ -1,4 +1,4 @@ -class Admin::Poll::ResultsController < Admin::BaseController +class Admin::Poll::ResultsController < Admin::Poll::BaseController before_action :load_poll def index @@ -10,4 +10,4 @@ class Admin::Poll::ResultsController < Admin::BaseController def load_poll @poll = ::Poll.includes(:questions).find(params[:poll_id]) end -end \ No newline at end of file +end diff --git a/app/controllers/admin/poll/shifts_controller.rb b/app/controllers/admin/poll/shifts_controller.rb index 4294045e5..48f540c07 100644 --- a/app/controllers/admin/poll/shifts_controller.rb +++ b/app/controllers/admin/poll/shifts_controller.rb @@ -1,4 +1,4 @@ -class Admin::Poll::ShiftsController < Admin::BaseController +class Admin::Poll::ShiftsController < Admin::Poll::BaseController before_action :load_booth before_action :load_polls @@ -57,4 +57,4 @@ class Admin::Poll::ShiftsController < Admin::BaseController params.require(:shift).permit(:booth_id, :officer_id, :date) end -end \ No newline at end of file +end diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb index 63ec8cf52..70541a3a0 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'] diff --git a/app/controllers/budgets/investments_controller.rb b/app/controllers/budgets/investments_controller.rb index 799d1f76d..fb82c51db 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,11 @@ 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]) end def load_ballot diff --git a/app/controllers/concerns/commentable_actions.rb b/app/controllers/concerns/commentable_actions.rb index e0a2d7719..826d90da9 100644 --- a/app/controllers/concerns/commentable_actions.rb +++ b/app/controllers/concerns/commentable_actions.rb @@ -62,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 @@ -117,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/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/officing/polls_controller.rb b/app/controllers/officing/polls_controller.rb index 46bcf9f37..5ba94e024 100644 --- a/app/controllers/officing/polls_controller.rb +++ b/app/controllers/officing/polls_controller.rb @@ -7,7 +7,7 @@ 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 {|poll| poll.ends_at > 2.weeks.ago && poll.expired?} else [] end diff --git a/app/controllers/officing/results_controller.rb b/app/controllers/officing/results_controller.rb index 65a6deac5..3ad7a91ea 100644 --- a/app/controllers/officing/results_controller.rb +++ b/app/controllers/officing/results_controller.rb @@ -52,20 +52,19 @@ 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 unless count.present? + 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 diff --git a/app/controllers/polls/questions_controller.rb b/app/controllers/polls/questions_controller.rb index bb1560f54..1849dff97 100644 --- a/app/controllers/polls/questions_controller.rb +++ b/app/controllers/polls/questions_controller.rb @@ -10,8 +10,6 @@ class Polls::QuestionsController < ApplicationController @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 diff --git a/app/controllers/proposals_controller.rb b/app/controllers/proposals_controller.rb index 069445bc9..e33b2357f 100644 --- a/app/controllers/proposals_controller.rb +++ b/app/controllers/proposals_controller.rb @@ -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,8 @@ class ProposalsController < ApplicationController def proposal_params params.require(:proposal).permit(:title, :question, :summary, :description, :external_url, :video_url, :responsible_name, :tag_list, :terms_of_service, :geozone_id, - documents_attributes: [:id, :title, :attachment, :cached_attachment, :user_id]) + image_attributes: [:id, :title, :attachment, :cached_attachment, :user_id, :_destroy], + documents_attributes: [:id, :title, :attachment, :cached_attachment, :user_id, :_destroy] ) end def retired_params 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/helpers/admin_helper.rb b/app/helpers/admin_helper.rb index f9c2e8c69..2cefcdf27 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].include? controller_name end def menu_profiles? @@ -39,7 +39,7 @@ module AdminHelper def menu_customization? ["pages", "images", "content_blocks"].include? controller_name end - + def official_level_options options = [["", 0]] (1..5).each do |i| 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 72ebbf021..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: 'document_ajax_attachment', - data: { - url: document_direct_upload_url(document), - cached_attachment_input_field: document_nested_field_id(document, index, :cached_attachment), - multiple: false, - index: index, - nested_document: true - }, - name: document_nested_field_name(document, index, :attachment), - id: document_nested_field_id(document, index, :attachment) - if document.attachment.blank? && document.cached_attachment.blank? - klass = document.errors[:attachment].any? ? "error" : "" - html += label_tag document_nested_field_id(document, index, :attachment), - t("documents.form.attachment_label"), - class: "button hollow #{klass}" - if document.errors[:attachment].any? - html += content_tag :small, class: "error" do - errors_on_attachment(document) - end - end - end + 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/officing_helper.rb b/app/helpers/officing_helper.rb index 4e824b3f2..3d53acf6d 100644 --- a/app/helpers/officing_helper.rb +++ b/app/helpers/officing_helper.rb @@ -1,13 +1,5 @@ module OfficingHelper - def officer_assignments_select_options(officer_assignments) - options = [] - officer_assignments.each do |oa| - options << ["#{oa.booth_assignment.booth.name}: #{l(oa.date.to_date, format: :long)}", oa.id] - end - options_for_select(options) - end - def booths_for_officer_select_options(officer_assignments) options = [] officer_assignments.each do |oa| @@ -17,10 +9,6 @@ module OfficingHelper options_for_select(options, params[:oa]) end - def system_recount_to_compare_with_final_recount(final_recount) - final_recount.booth_assignment.voters.select {|v| v.created_at.to_date == final_recount.date}.size - end - def answer_result_value(question_id, answer_index) return nil if params.blank? return nil if params[:questions].blank? diff --git a/app/helpers/poll_final_recounts_helper.rb b/app/helpers/poll_final_recounts_helper.rb deleted file mode 100644 index e196cb65b..000000000 --- a/app/helpers/poll_final_recounts_helper.rb +++ /dev/null @@ -1,7 +0,0 @@ -module PollFinalRecountsHelper - - def final_recount_for_date(final_recounts, date) - final_recounts.select {|f| f.date == date}.first - end - -end \ No newline at end of file diff --git a/app/helpers/poll_recounts_helper.rb b/app/helpers/poll_recounts_helper.rb index 95ea813e3..716b71d85 100644 --- a/app/helpers/poll_recounts_helper.rb +++ b/app/helpers/poll_recounts_helper.rb @@ -1,7 +1,7 @@ module PollRecountsHelper - def booth_assignment_sum_final_recounts(ba) - ba.final_recounts.any? ? ba.final_recounts.to_a.sum(&:count) : nil + def total_recounts_by_booth(booth_assignment) + booth_assignment.total_results.any? ? booth_assignment.total_results.to_a.sum(&:amount) : nil end end diff --git a/app/helpers/polls_helper.rb b/app/helpers/polls_helper.rb index 169599a7c..27d33ea04 100644 --- a/app/helpers/polls_helper.rb +++ b/app/helpers/polls_helper.rb @@ -28,11 +28,6 @@ module PollsHelper options_for_select(options, params[:d]) end - def poll_final_recount_option(poll) - final_date = poll.ends_at.to_date + 1.day - options_for_select([[I18n.t("polls.final_date"), l(final_date)]]) - end - def poll_booths_select_options(poll) options = [] poll.booths.each do |booth| @@ -46,4 +41,4 @@ module PollsHelper booth.name + location end -end \ No newline at end of file +end diff --git a/app/helpers/proposals_helper.rb b/app/helpers/proposals_helper.rb index 6ef3be75d..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 @@ -40,11 +40,6 @@ module ProposalsHelper end end - - def can_create_document?(document, proposal) - can?(:create, document) && proposal.documents.size < Proposal.max_documents_allowed - end - def author_of_proposal?(proposal) author_of?(proposal, current_user) end diff --git a/app/helpers/welcome_helper.rb b/app/helpers/welcome_helper.rb index 76ae62d81..9f8c8c13e 100644 --- a/app/helpers/welcome_helper.rb +++ b/app/helpers/welcome_helper.rb @@ -19,14 +19,14 @@ module WelcomeHelper end end - def render_image(recommended, image_field, image_version, image_default) - image_path = calculate_image_path(recommended, image_field, image_version, image_default) + 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_field, image_version, image_default) - if image_field.present? && image_version.present? - recommended.send("#{image_field}", image_version) + 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 diff --git a/app/models/abilities/administrator.rb b/app/models/abilities/administrator.rb index 773dabf34..4278d4f8d 100644 --- a/app/models/abilities/administrator.rb +++ b/app/models/abilities/administrator.rb @@ -74,6 +74,8 @@ module Abilities cannot :comment_as_moderator, [::Legislation::Question, Legislation::Annotation] 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 620cfb212..e4e92abaf 100644 --- a/app/models/abilities/common.rb +++ b/app/models/abilities/common.rb @@ -24,6 +24,7 @@ module Abilities can :suggest, Debate can :suggest, Proposal + can :suggest, ActsAsTaggableOn::Tag can [:flag, :unflag], Comment cannot [:flag, :unflag], Comment, user_id: user.id @@ -36,8 +37,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 @@ -54,6 +58,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..857025375 100644 --- a/app/models/budget/investment.rb +++ b/app/models/budget/investment.rb @@ -7,11 +7,11 @@ class Budget include Reclassification include Followable include Communitable + include Imageable 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/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/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 3488b0519..eb424c308 100644 --- a/app/models/debate.rb +++ b/app/models/debate.rb @@ -95,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 diff --git a/app/models/direct_upload.rb b/app/models/direct_upload.rb new file mode 100644 index 000000000..a6b6ef276 --- /dev/null +++ b/app/models/direct_upload.rb @@ -0,0 +1,62 @@ +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) + + if @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/poll/booth_assignment.rb b/app/models/poll/booth_assignment.rb index 47cb01bfe..8b4b655ef 100644 --- a/app/models/poll/booth_assignment.rb +++ b/app/models/poll/booth_assignment.rb @@ -4,7 +4,6 @@ class Poll belongs_to :poll has_many :officer_assignments, class_name: "Poll::OfficerAssignment", dependent: :destroy - has_many :final_recounts, class_name: "Poll::FinalRecount", dependent: :destroy has_many :officers, through: :officer_assignments has_many :voters has_many :partial_results diff --git a/app/models/poll/final_recount.rb b/app/models/poll/final_recount.rb deleted file mode 100644 index 3647c85ca..000000000 --- a/app/models/poll/final_recount.rb +++ /dev/null @@ -1,19 +0,0 @@ -class Poll - class FinalRecount < ActiveRecord::Base - belongs_to :booth_assignment, class_name: "Poll::BoothAssignment" - belongs_to :officer_assignment, class_name: "Poll::OfficerAssignment" - - validates :booth_assignment_id, presence: true - validates :date, presence: true, uniqueness: {scope: :booth_assignment_id} - validates :count, presence: true, numericality: {only_integer: true} - - before_save :update_logs - - def update_logs - if count_changed? && count_was.present? - self.count_log += ":#{count_was.to_s}" - self.officer_assignment_id_log += ":#{officer_assignment_id_was.to_s}" - end - end - end -end \ No newline at end of file diff --git a/app/models/poll/officer_assignment.rb b/app/models/poll/officer_assignment.rb index 1d7326500..8e86d309c 100644 --- a/app/models/poll/officer_assignment.rb +++ b/app/models/poll/officer_assignment.rb @@ -2,8 +2,10 @@ class Poll class OfficerAssignment < ActiveRecord::Base belongs_to :officer belongs_to :booth_assignment - has_many :final_recounts has_many :partial_results + has_many :white_results + has_many :null_results + has_many :total_results has_many :voters validates :officer_id, presence: true diff --git a/app/models/proposal.rb b/app/models/proposal.rb index e3c7a8c57..344232e26 100644 --- a/app/models/proposal.rb +++ b/app/models/proposal.rb @@ -10,11 +10,11 @@ class Proposal < ActiveRecord::Base include Graphqlable include Followable include Communitable + include Imageable 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 + accepted_content_types: [ "application/pdf" ] include EmbedVideosHelper acts_as_votable @@ -101,7 +101,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) diff --git a/app/views/admin/_menu.html.erb b/app/views/admin/_menu.html.erb index 11d58adc8..bb246cb6c 100644 --- a/app/views/admin/_menu.html.erb +++ b/app/views/admin/_menu.html.erb @@ -1,5 +1,5 @@
-