Merge branch 'master' into 1856-legislation_processes_proposals_phase

This commit is contained in:
María Checa
2017-10-09 17:19:07 +02:00
306 changed files with 7018 additions and 2677 deletions

View File

@@ -1,6 +1,8 @@
inherit_from: .rubocop_todo.yml
AllCops:
DisplayCopNames: true
DisplayStyleGuide: true
Include:
- '**/Rakefile'
- '**/config.ru'

View File

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

View File

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

View File

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

Binary file not shown.

View File

@@ -62,4 +62,7 @@
<glyph glyph-name="expand" unicode="&#48;" d="M26 168l-26-158c0-2 1-5 3-7 0 0 0 0 0 0 2-2 5-3 7-3l158 27c3 0 6 3 7 6 1 3 0 7-3 9l-30 31 82 82c4 4 4 9 0 13l-57 57c-3 3-9 3-12 0l-83-83-31 31c-2 2-5 3-9 2-3-1-5-4-6-7z m460 176l26 158c0 2-1 5-3 7 0 0 0 0 0 0-2 2-5 3-7 3l-158-27c-3 0-6-3-7-6-1-3 0-7 3-9l30-31-82-82c-4-4-4-9 0-13l57-57c3-3 9-3 12 0l83 83 31-31c2-2 5-3 9-2 3 1 5 4 6 7z"/>
<glyph glyph-name="telegram" unicode="&#49;" d="M504 509c6-5 9-11 8-18l-73-439c-1-6-4-10-10-13-2-2-5-2-8-2-3 0-5 0-7 1l-130 53-69-84c-3-5-8-7-14-7-2 0-4 0-6 1-4 1-7 4-9 7-2 3-3 6-3 10l0 100 247 303-306-265-113 47c-7 2-10 7-11 15 0 8 3 14 9 17l476 274c2 2 5 3 9 3 4 0 7-1 10-3z"/>
<glyph glyph-name="instagram" unicode="&#50;" d="M426 105l0 185-39 0c4-12 6-25 6-38 0-24-6-46-18-66-13-20-29-36-50-48-21-12-44-18-69-18-37 0-69 13-96 39-27 26-40 57-40 93 0 13 2 26 6 38l-41 0 0-185c0-5 2-10 5-13 4-3 8-5 13-5l305 0c5 0 9 2 13 5 3 3 5 8 5 13z m-81 152c0 23-9 44-26 60-18 17-38 25-63 25-24 0-45-8-62-25-17-16-26-37-26-60 0-24 9-44 26-61 17-16 38-25 62-25 25 0 45 9 63 25 17 17 26 37 26 61z m81 103l0 47c0 5-2 10-6 14-4 4-8 6-14 6l-50 0c-5 0-10-2-14-6-4-4-5-9-5-14l0-47c0-6 1-10 5-14 4-4 9-6 14-6l50 0c6 0 10 2 14 6 4 4 6 8 6 14z m49 59l0-326c0-16-5-29-16-40-11-11-24-16-40-16l-326 0c-16 0-29 5-40 16-11 11-16 24-16 40l0 326c0 16 5 29 16 40 11 11 24 16 40 16l326 0c16 0 29-5 40-16 11-11 16-24 16-40z"/>
<glyph glyph-name="image" unicode="&#51;" d="M165 347c0-15-6-28-16-38-11-11-24-16-39-16-16 0-28 5-39 16-11 10-16 23-16 38 0 16 5 29 16 39 11 11 23 16 39 16 15 0 28-5 39-16 10-10 16-23 16-39z m292-109l0-128-402 0 0 55 91 91 46-46 146 147z m28 201l-458 0c-2 0-4-1-6-3-2-2-3-4-3-6l0-348c0-2 1-4 3-6 2-2 4-3 6-3l458 0c2 0 4 1 6 3 2 2 3 4 3 6l0 348c0 2-1 4-3 6-2 2-4 3-6 3z m45-9l0-348c0-12-4-23-13-32-9-9-20-13-32-13l-458 0c-12 0-23 4-32 13-9 9-13 20-13 32l0 348c0 12 4 23 13 32 9 9 20 13 32 13l458 0c12 0 23-4 32-13 9-9 13-20 13-32z"/>
<glyph glyph-name="search-plus" unicode="&#52;" d="M311 283l0-18c0-2-1-4-3-6-2-2-4-3-6-3l-64 0 0-64c0-2-1-5-3-6-2-2-4-3-6-3l-19 0c-2 0-4 1-6 3-2 1-3 4-3 6l0 64-64 0c-2 0-4 1-6 3-2 2-3 4-3 6l0 18c0 3 1 5 3 7 2 2 4 3 6 3l64 0 0 64c0 2 1 4 3 6 2 2 4 3 6 3l19 0c2 0 4-1 6-3 2-2 3-4 3-6l0-64 64 0c2 0 4-1 6-3 2-2 3-4 3-7z m36-9c0 36-12 66-37 91-25 25-55 37-91 37-35 0-65-12-90-37-25-25-38-55-38-91 0-35 13-65 38-90 25-25 55-38 90-38 36 0 66 13 91 38 25 25 37 55 37 90z m147-237c0-11-4-19-11-26-7-7-16-11-26-11-10 0-19 4-26 11l-98 98c-34-24-72-36-114-36-27 0-53 5-78 16-25 11-46 25-64 43-18 18-32 39-43 64-10 25-16 51-16 78 0 28 6 54 16 78 11 25 25 47 43 65 18 18 39 32 64 43 25 10 51 15 78 15 28 0 54-5 79-15 24-11 46-25 64-43 18-18 32-40 43-65 10-24 16-50 16-78 0-42-12-80-36-114l98-98c7-7 11-15 11-25z"/>
<glyph glyph-name="search-minus" unicode="&#53;" d="M311 283l0-18c0-2-1-4-3-6-2-2-4-3-6-3l-165 0c-2 0-4 1-6 3-2 2-3 4-3 6l0 18c0 3 1 5 3 7 2 2 4 3 6 3l165 0c2 0 4-1 6-3 2-2 3-4 3-7z m36-9c0 36-12 66-37 91-25 25-55 37-91 37-35 0-65-12-90-37-25-25-38-55-38-91 0-35 13-65 38-90 25-25 55-38 90-38 36 0 66 13 91 38 25 25 37 55 37 90z m147-237c0-11-4-19-11-26-7-7-16-11-26-11-10 0-19 4-26 11l-98 98c-34-24-72-36-114-36-27 0-53 5-78 16-25 11-46 25-64 43-18 18-32 39-43 64-10 25-16 51-16 78 0 28 6 54 16 78 11 25 25 47 43 65 18 18 39 32 64 43 25 10 51 15 78 15 28 0 54-5 79-15 24-11 46-25 64-43 18-18 32-40 43-65 10-24 16-50 16-78 0-42-12-80-36-114l98-98c7-7 11-15 11-25z"/>
</font></defs></svg>

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

View File

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

View File

@@ -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('<div class="progress-bar"><div class="loading-bar uploading"></div></div>')
$(wrapper).find('.progress-bar-placeholder').css('display','block')
data.formData = {
"document[title]": $(wrapper).find('input.document-title').val() || data.files[0].name
"index": index,
"nested_document": is_nested_document
}
data = 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 = '<small class="error">' + data.jqXHR.responseJSON.errors + '</small>'
$(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')

View File

@@ -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 = '<small class="error">' + data.jqXHR.responseJSON.errors + '</small>'
$(data.errorContainer).append(errors)
setPreview: (data) ->
image_preview = '<div class="small-12 column text-center image-preview"><figure><img src="' + data.result.attachment_url + '" class="cached-image"/></figure></div>'
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')

View File

@@ -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: '<div class="map-icon"></div>')
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

View File

@@ -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() + "<br><strong>" + @token + "</strong>");
token_message.show()
false

View File

@@ -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();

View File

@@ -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();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,4 +4,12 @@ module DebatesHelper
Debate.all.featured.count > 0
end
end
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -24,7 +24,7 @@ module Graphqlable
end
def graphql_type_description
(model_name.human).to_s
model_name.human.to_s
end
end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

112
app/models/image.rb Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
<div class="admin-sidebar" data-equalizer-watch>
<ul id="admin_menu" data-accordion-menu>
<ul id="admin_menu" data-accordion-menu data-multi-open="false">
<li class="section-title">
<a href="#">
@@ -53,19 +53,20 @@
</li>
<% end %>
<% if feature?(:polls) %>
<li class="section-title">
<a href="#">
<span class="icon-checkmark-circle"></span>
<strong><%= t("admin.menu.title_polls") %></strong>
</a>
<ul id="polls_menu" <%= "class=is-active" if menu_polls? && controller.class.parent == Admin::Poll::QuestionsController %>>
<ul id="polls_menu" <%= "class=is-active" if menu_polls? || controller.class.parent == Admin::Poll::Questions::Answers %>>
<li <%= "class=active" if ["polls", "officer_assignments", "booth_assignments", "recounts", "results"].include? controller_name %>>
<%= link_to t('admin.menu.polls'), admin_polls_path %>
</li>
<li <%= "class=active" if controller_name == "questions" && controller.class.parent == Admin::Poll::QuestionsController %>>
<li <%= "class=active" if controller_name == "questions" ||
controller_name == "answers" ||
controller.class.parent == Admin::Poll::Questions::Answers %>>
<%= link_to t("admin.menu.poll_questions"), admin_questions_path %>
</li>
@@ -158,12 +159,14 @@
<span class="icon-settings"></span>
<strong><%= t("admin.menu.title_site_customization") %></strong>
</a>
<ul <%= "class=is-active" if menu_customization? %>>
<ul <%= "class=is-active" if menu_customization? &&
controller.class.parent != Admin::Poll::Questions::Answers %>>
<li <%= "class=active" if controller_name == "pages" %>>
<%= link_to t("admin.menu.site_customization.pages"), admin_site_customization_pages_path %>
</li>
<li <%= "class=active" if controller_name == "images" %>>
<li <%= "class=active" if controller_name == "images" &&
controller.class.parent != Admin::Poll::Questions::Answers %>>
<%= link_to t("admin.menu.site_customization.images"), admin_site_customization_images_path %>
</li>

View File

@@ -1,99 +1,9 @@
<%= link_to admin_settings_path, class: "button float-right" do %>
<span class="icon-settings"></span>
<%= t("admin.menu.settings") %>
<% end %>
<%= link_to admin_stats_path, class: "button float-right" do %>
<span class="icon-stats"></span>
<%= t("admin.menu.stats") %>
<% end %>
<h2 class="inline-block"><%= t("admin.dashboard.index.title") %></h2>
<p>Desde aquí puedes administrar el sistema, a través de las siguientes acciones:</p>
<div class="small-12 medium-9">
<ul class="accordion" data-accordion data-multi-expand="true" data-allow-all-closed="true">
<li class="accordion-item" data-accordion-item>
<a href="#" class="accordion-title">Temas de debate</a>
<div class="accordion-content" data-tab-content>
<p>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 <strong>marcarlos para ser propuestos al crear debates</strong> (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)</p>
</div>
</li>
<li class="accordion-item" data-accordion-item>
<a href="#" class="accordion-title">Propuestas/Debates/Comentarios ocultos</a>
<div class="accordion-content" data-tab-content>
<p>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.</p>
<ul>
<li>Al pulsar <strong>Confirmar</strong> se acepta el que se haya ocultado, se considera que se ha hecho correctamente.</li>
<li>Al pulsar <strong>Volver a mostrar</strong> 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.</li>
</ul>
<p>Para facilitar la gestión, arriba encontramos un <strong>filtro</strong> 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".</p>
<p><em>Es recomendable revisar regularmente la sección "pendientes".</em></p>
</div>
</li>
<li class="accordion-item" data-accordion-item>
<a href="#" class="accordion-title">Usuarios bloqueados</a>
<div class="accordion-content" data-tab-content>
<p>Cuando un moderador o un administrador bloquea a un usuario aparecerá en esta lista. Al <strong>bloquear a un usuario, éste deja de poder utilizarlo para ninguna acción de la web</strong>. Los administradores pueden desbloquearlos pulsando el botón al lado del nombre del usuario en la lista.</p>
</div>
</li>
<li class="accordion-item" data-accordion-item>
<a href="#" class="accordion-title">Organizaciones</a>
<div class="accordion-content" data-tab-content>
<p>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 <strong>"Verificar"</strong> 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.</p>
<p>En caso de que el proceso de verificación haya sido negativo, se pulsa el botón <strong>"Rechazar"</strong>. Para editar alguno de los datos de la organización, se pulsa el botón <strong>"Editar"</strong>.</p>
<p>En caso de que el proceso de verificación haya sido negativo, se pulsa el botón <strong>"Rechazar"</strong>. Para editar alguno de los datos de la organización, se pulsa el botón <strong>"Editar"</strong>.</p>
<p>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 <strong>filtro</strong> con las secciones: "pendientes" (las organizaciones que todavía no han sido verificadas o rechazadas), "verificadas", "rechazadas" y "todas".</p>
<p><em>Es recomendable revisar regularmente la sección "pendientes".</em></p>
</div>
</li>
<li class="accordion-item" data-accordion-item>
<a href="#" class="accordion-title">Cargos Públicos</a>
<div class="accordion-content" data-tab-content>
<p>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 <strong>etiqueta que les identifica</strong>, 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 <strong>su nivel</strong> (la manera que internamente usa la web para diferenciar entre un tipo de cargos y otros). Pulsando el botón <strong>"Editar"</strong> 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.</p>
</div>
</li>
<li class="accordion-item" data-accordion-item>
<a href="#" class="accordion-title">Moderadores</a>
<div class="accordion-content" data-tab-content>
<p>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 <strong>"Moderar"</strong></p>
</div>
</li>
<li class="accordion-item" data-accordion-item>
<a href="#" class="accordion-title">Actividad de moderadores</a>
<div class="accordion-content" data-tab-content>
<p>En esta sección se va guardando <strong>todas las acciones que realizan los moderadores o los administradores respecto a la moderación</strong>: ocultar/mostrar Propuestas/Debates/Comentarios y bloquear usuarios. En la columna <strong>"Acción"</strong> 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.</p>
</div>
</li>
<li class="accordion-item" data-accordion-item>
<a href="#" class="accordion-title">Configuración Global</a>
<div class="accordion-content" data-tab-content>
<p>Opciones generales de configuración del sistema.</p>
</div>
</li>
<li class="accordion-item" data-accordion-item>
<a href="#" class="accordion-title">Estadísticas</a>
<div class="accordion-content" data-tab-content>
<p>Estadísticas generales del sistema.</p>
</div>
</li>
</ul>
<div class="float-right">
<%= link_to root_path do %>
<%= t("admin.dashboard.index.back", org: setting['org_name']) %>
<% end %>
</div>
<h2 class="title inline-block"><%= t("admin.dashboard.index.title") %></h2>
<p><%= t("admin.dashboard.index.description", org: setting['org_name']) %></p>

View File

@@ -14,15 +14,15 @@
<div class="tabs-content" data-tabs-content="booths-tabs">
<ul class="tabs" data-tabs id="booths-tabs">
<li class="tabs-title is-active">
<li class="tabs-title">
<%= link_to t("admin.poll_booth_assignments.show.officers"), "#tab-officers" %>
</li>
<li class="tabs-title">
<li class="tabs-title is-active">
<%= link_to t("admin.poll_booth_assignments.show.recounts"), "#tab-recounts" %>
</li>
</ul>
<div class="tabs-panel is-active" id="tab-officers">
<div class="tabs-panel" id="tab-officers">
<% if @booth_assignment.officers.empty? %>
<div class="callout primary margin-top">
<%= t("admin.poll_booth_assignments.show.no_officers") %>
@@ -43,27 +43,36 @@
<% end %>
</div>
<div class="tabs-panel" id="tab-recounts">
<div class="tabs-panel is-active" id="tab-recounts">
<h3><%= t("admin.poll_booth_assignments.show.recounts_list") %></h3>
<table id="totals">
<thead>
<tr>
<th class="text-center"><%= t("admin.poll_booth_assignments.show.count_final") %></th>
<th class="text-center"><%= t("admin.poll_booth_assignments.show.total_system") %></th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center" id="total_final"><%= total_recounts_by_booth(@booth_assignment) || '-' %></td>
<td class="text-center" id="total_system"><%= @booth_assignment.voters.count %></td>
</tr>
</tbody>
</table>
<table id="recounts_list">
<thead>
<tr>
<th><%= t("admin.poll_booth_assignments.show.date") %></th>
<th class="text-center"><%= t("admin.poll_booth_assignments.show.total_recount") %></th>
<th class="text-center"><%= t("admin.poll_booth_assignments.show.count_by_system") %></th>
</tr>
</thead>
<tbody>
<% (@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 %>
<tr id="recounting_<%= voting_date.strftime('%Y%m%d') %>">
<td><%= l voting_date %></td>
<% if total_recount.present? %>
<td class="text-center <%= 'count-error' if total_recount.amount != system_count %>" title="<%= total_recount.officer_assignment.officer.name %>"><%= total_recount.amount %></td>
<% else %>
<td class="text-center" title=""> - </td>
<% end %>
<td class="text-center"><%= system_count %></td>
</tr>
<% end %>

Some files were not shown because too many files have changed in this diff Show More