diff --git a/app/assets/images/icon_home_debates.png b/app/assets/images/icon_home_debates.png new file mode 100644 index 000000000..bfdaafa36 Binary files /dev/null and b/app/assets/images/icon_home_debates.png differ diff --git a/app/assets/images/icon_home_decides.png b/app/assets/images/icon_home_decides.png new file mode 100644 index 000000000..89630d6c5 Binary files /dev/null and b/app/assets/images/icon_home_decides.png differ diff --git a/app/assets/images/icon_home_propones.png b/app/assets/images/icon_home_propones.png new file mode 100644 index 000000000..ffe3d376a Binary files /dev/null and b/app/assets/images/icon_home_propones.png differ diff --git a/app/assets/images/icon_home_sehace.png b/app/assets/images/icon_home_sehace.png new file mode 100644 index 000000000..97ec84c79 Binary files /dev/null and b/app/assets/images/icon_home_sehace.png differ diff --git a/app/assets/javascripts/moderator_proposals.js.coffee b/app/assets/javascripts/moderator_proposals.js.coffee new file mode 100644 index 000000000..2106b5c65 --- /dev/null +++ b/app/assets/javascripts/moderator_proposals.js.coffee @@ -0,0 +1,8 @@ +App.ModeratorProposals = + + add_class_faded: (id) -> + $("##{id}").addClass("faded") + $("#comments").addClass("faded") + + hide_moderator_actions: (id) -> + $("##{id} .js-moderator-proposals-actions:first").hide() diff --git a/app/assets/javascripts/prevent_double_submission.js.coffee b/app/assets/javascripts/prevent_double_submission.js.coffee index 867e6fa43..5423f2018 100644 --- a/app/assets/javascripts/prevent_double_submission.js.coffee +++ b/app/assets/javascripts/prevent_double_submission.js.coffee @@ -1,26 +1,32 @@ App.PreventDoubleSubmission = - disable_button: (button) -> - unless button.hasClass('disabled') - loading = button.data('loading') ? '...' - button.addClass('disabled').attr('disabled', 'disabled') - button.data('text', button.val()) - button.val(loading) + disable_buttons: (buttons) -> + setTimeout -> + buttons.each -> + button = $(this) + unless button.hasClass('disabled') + loading = button.data('loading') ? '...' + button.addClass('disabled').attr('disabled', 'disabled') + button.data('text', button.val()) + button.val(loading) + , 1 - reset_button: (button) -> - if button.hasClass('disabled') - button_text = button.data('text') - button.removeClass('disabled').attr('disabled', null) - if button_text - button.val(button_text) - button.data('text', null) + reset_buttons: (buttons) -> + buttons.each -> + button = $(this) + if button.hasClass('disabled') + button_text = button.data('text') + button.removeClass('disabled').attr('disabled', null) + if button_text + button.val(button_text) + button.data('text', null) initialize: -> $('form').on('submit', event, -> - button = $(this).find(':button, :submit') - App.PreventDoubleSubmission.disable_button(button) + buttons = $(this).find(':button, :submit') + App.PreventDoubleSubmission.disable_buttons(buttons) ).on('ajax:success', -> - button = $(this).find(':button, :submit') - App.PreventDoubleSubmission.reset_button(button) + buttons = $(this).find(':button, :submit') + App.PreventDoubleSubmission.reset_buttons(buttons) ) false diff --git a/app/assets/javascripts/tags.js.coffee b/app/assets/javascripts/tags.js.coffee index 12c4c3115..adbcad3e8 100644 --- a/app/assets/javascripts/tags.js.coffee +++ b/app/assets/javascripts/tags.js.coffee @@ -1,7 +1,7 @@ App.Tags = initialize: -> - $tag_input = $('input#debate_tag_list') + $tag_input = $('input.js-tag-list') $('body .js-add-tag-link').each -> $this = $(this) diff --git a/app/assets/javascripts/votes.js.coffee b/app/assets/javascripts/votes.js.coffee index 4ccfb2131..09f2a7db0 100644 --- a/app/assets/javascripts/votes.js.coffee +++ b/app/assets/javascripts/votes.js.coffee @@ -12,8 +12,5 @@ App.Votes = initialize: -> App.Votes.hoverize votes for votes in $("div.votes") + App.Votes.hoverize votes for votes in $("div.supports") false - - - - diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 38a90a3fa..ee7ca0e02 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -10,4 +10,5 @@ @import "admin"; @import "participacion"; @import "debates"; +@import "proposals"; @import "c3"; diff --git a/app/assets/stylesheets/debates.scss b/app/assets/stylesheets/debates.scss index a2acbea13..cfd0fc323 100644 --- a/app/assets/stylesheets/debates.scss +++ b/app/assets/stylesheets/debates.scss @@ -352,7 +352,7 @@ @media (min-width: $small-breakpoint) { border-top-left-radius: 3px; border-bottom-left-radius: 3px; - margin: 0 rem-calc(-24) 0 rem-calc(12); + margin: 0 rem-calc(-25) 0 rem-calc(12); } &:after { @@ -487,13 +487,16 @@ font-weight: bold; } - h3 { - border-top: 2px solid $brand; - display: inline-block; - font-size: rem-calc(16); - margin: -1px 0 rem-calc(12); - padding-top: rem-calc(6); - text-transform: uppercase; + aside { + + h3 { + border-top: 2px solid $brand; + display: inline-block; + font-size: rem-calc(16); + margin: -1px 0 rem-calc(12); + padding-top: rem-calc(6); + text-transform: uppercase; + } } .votes { @@ -636,7 +639,7 @@ .comment-votes { color: $text-medium; font-weight: lighter; - margin: rem-calc(15) rem-calc(6) 0; + margin: rem-calc(8) rem-calc(12) rem-calc(6) 0; a { color: $text-light; @@ -650,7 +653,7 @@ [class^="icon-"] { font-size: rem-calc(20); - vertical-align: top; + vertical-align: middle; } } @@ -676,6 +679,7 @@ .comment-user { margin-top: rem-calc(6); padding: rem-calc(6) 0; + overflow: hidden; @each $n in ("1", "2", "3","4", "5") { &.level-#{$n} { @@ -757,6 +761,7 @@ .button { background: none; + margin-bottom: 0; padding: 0; } } diff --git a/app/assets/stylesheets/icons.scss b/app/assets/stylesheets/icons.scss index 2cc97f8e8..7b2a6df3a 100644 --- a/app/assets/stylesheets/icons.scss +++ b/app/assets/stylesheets/icons.scss @@ -85,7 +85,7 @@ .icon-stats:before { content: "r"; } -.icon-initiatives:before { +.icon-proposals:before { content: "h"; } .icon-organizations:before { diff --git a/app/assets/stylesheets/participacion.scss b/app/assets/stylesheets/participacion.scss index 5a7d4c6d7..a14d6f67f 100644 --- a/app/assets/stylesheets/participacion.scss +++ b/app/assets/stylesheets/participacion.scss @@ -329,7 +329,7 @@ header { @media (min-width: $small-breakpoint) { display: inline-block; margin-bottom: 0; - margin-left: rem-calc(24); + margin-left: rem-calc(12); } &:hover { @@ -656,7 +656,7 @@ header { @media (min-width: $small-breakpoint) { line-height: $line-height*3; margin-left: rem-calc(12); - margin-right: rem-calc(72); + margin-right: rem-calc(36); } &:after { @@ -772,7 +772,7 @@ footer { @extend .tags; h3 { - border-top: 1px solid $votes-border; + border-top: 2px solid $brand; display: inline-block; font-family: $font-family-sans-serif; font-size: rem-calc(16); @@ -925,6 +925,18 @@ form { margin-bottom: rem-calc(12); } + .note-marked { + @extend .note; + background: yellow; + display: inline-block; + + em { + background: white; + display: inline-block; + padding-left: rem-calc(6); + } + } + .ckeditor { min-height: rem-calc(312); } @@ -1403,6 +1415,7 @@ table { li { font-size: rem-calc(15); + line-height: rem-calc(30); margin-bottom: rem-calc(12); } } diff --git a/app/assets/stylesheets/proposals.scss b/app/assets/stylesheets/proposals.scss new file mode 100644 index 000000000..6a38a4efe --- /dev/null +++ b/app/assets/stylesheets/proposals.scss @@ -0,0 +1,733 @@ +// Table of Contents +// +// 01. Debates +// 02. Index +// 02.1. Featured +// 02.2. List +// 03. Show +// 04. New +// 05. Comments +// 06. Flags +// + +// 01. Proposals +// - - - - - - - - - - - - - - - - - - - - - - - - - + +.button-proposal { + background: $proposals; + + &:hover { + background: $proposals-border; + } +} + +@mixin supports { + background: $proposals; + border-top: 1px solid $proposals-border; + margin: 0 rem-calc(-12); + padding: rem-calc(14) rem-calc(12); + position: relative; + + .progress { + background-color: rgba(255,255,255,.8); + height: rem-calc(12); + margin-bottom: rem-calc(6); + margin-top: rem-calc(4); + + .meter { + background: $votes-like; + } + } + + abbr { + color: white; + + &[title] { + border-bottom: 1px dotted white; + } + } + + .button-support { + background: white; + color: $proposals; + display: inline-block; + font-size: rem-calc(14); + margin-top: rem-calc(12); + + &:hover { + background: $proposals-border; + color: white; + cursor: pointer; + } + + &:active { + opacity: .75; + } + } + + .total-supports { + color: white; + text-align: center; + font-size: rem-calc(14); + + span { + display: block; + font-size: rem-calc(11); + opacity: .75; + } + } + + .divider { + margin: 0 rem-calc(6); + } + + .not-logged { + background: rgba(255,164,45,.9); + color: white; + height: 100%; + left: 0; + line-height: $line-height*2; + padding-top: rem-calc(12); + position: absolute; + text-align: center; + top: 0; + width: 100%; + filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,startColorstr='#222222', endColorstr='#222222'); /* IE */ + + a { + color: white; + text-decoration: underline; + } + } + + .anonymous-votes, .organizations-votes { + background: $warning-bg; + color: $warning-color; + height: 100%; + left: 0; + line-height: $line-height; + padding-top: rem-calc(12); + position: absolute; + text-align: center; + top: 0; + width: 100%; + + p { + color: $warning-color; + margin: 0 rem-calc(12); + text-align: left; + } + + a { + color: $warning-color; + font-weight: bold; + text-decoration: underline; + } + } + + .supported { + color: white; + margin-top: rem-calc(12); + } +} + +// 02. Index +// - - - - - - - - - - - - - - - - - - - - - - - - - + +// 02.1. Featured +// - - - - - - - - - - - - - + +.proposal-featured { + + .panel { + background: white; + border: 1px solid; + border-color: #e5e6e9 #dfe0e4 #d0d1d5; + border-radius: rem-calc(3); + margin-bottom: rem-calc(24); + padding: rem-calc(24) rem-calc(12) 0 rem-calc(12); + + .proposal-content { + min-height: rem-calc(353); + } + + .label { + background: none; + clear: both; + color: $proposals; + display: block; + font-weight: bold; + text-transform: uppercase; + padding-left: 0; + padding-top: 0; + } + + .icon-proposals { + color: $proposals; + font-size: rem-calc(36); + line-height: $line-height; + position: absolute; + right: rem-calc(18); + top: rem-calc(12); + } + + h3 { + font-weight: bold; + margin: rem-calc(8) 0 0 0; + min-height: rem-calc(65); + + a { + clear: both; + color: $text; + display: block; + font-size: rem-calc(16); + line-height: $line-height; + text-transform: lowercase; + + &:first-letter { + text-transform: uppercase; + } + } + } + + .proposal-info { + color: $text-medium; + font-weight: lighter; + margin-bottom: 0; + + .icon-comments { + font-size: rem-calc(16); + vertical-align: top; + } + + a { + color: $text-medium; + } + } + + .proposal-description { + color: $text; + font-size: rem-calc(13); + height: rem-calc(156); + line-height: $line-height; + margin-bottom: rem-calc(12); + margin-top: rem-calc(24); + overflow: hidden; + position: relative; + + a { + color: $text; + } + + ul, ol { + + li { + font-size: rem-calc(13); + margin-bottom: rem-calc(12); + } + } + } + + .truncate { + background: image-url('truncate.png'); + background-repeat: repeat-x; + bottom: 0; + height: 24px; + position: absolute; + width: 100%; + } + + p { + color: $text; + font-size: rem-calc(14); + line-height: $line-height; + margin-bottom: rem-calc(12); + + &.debate-info { + font-size: rem-calc(13); + } + } + } + + .supports { + @include supports; + } +} + +// 02.2. List +// - - - - - - - - - - - - - + +.proposals-list { + + @media (min-width: $small-breakpoint) { + margin-bottom: rem-calc(48); + } +} + +.proposal { + @extend .proposal-featured; + margin-bottom: 0; + margin-top: 0; + + .panel { + border-radius: 0; + box-shadow: 0px 1px 3px 0 $border; + margin-bottom: rem-calc(12); + min-height: rem-calc(192); + padding-top: rem-calc(12); + + @media (min-width: $small-breakpoint) { + margin-bottom: rem-calc(-1); + padding-bottom: rem-calc(12); + } + + .label { + line-height: $line-height; + padding-bottom: 0; + } + + h3 { + margin-top: 0; + min-height: rem-calc(48); + } + + .proposal-content { + margin: 0; + min-height: rem-calc(180); + + .tags { + display: block; + } + } + + .icon-proposals { + font-size: rem-calc(18); + left: rem-calc(88); + top: 0; + } + + .proposal-description { + height: rem-calc(72); + margin-top: 0; + } + } + + .supports { + border: 1px solid $proposals-border; + margin: 0 rem-calc(-12); + + @media (min-width: $small-breakpoint) { + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; + margin: 0 rem-calc(-25) 0 rem-calc(12); + } + + &:after { + content: none; + position: absolute; + display: block; + border-style: solid; + border-color: #664212 transparent transparent transparent; + bottom: rem-calc(-14); + border-left-width: 0; + border-right-color: transparent; + right: rem-calc(-1); + border-width: 1em 1em 0 0; + + @media (min-width: $small-breakpoint) { + content: ""; + } + } + + .total-supports { + display: inline-block; + line-height: $line-height; + padding-top: rem-calc(12); + vertical-align: top; + + @media (min-width: $small-breakpoint) { + display: block; + float: none; + margin-left: 0; + padding-top: 0; + } + } + + .not-logged { + line-height: $line-height; + padding-top: rem-calc(24); + } + + .anonymous-votes, .organizations-votes { + padding-top: rem-calc(24); + } + + .divider { + display: none; + } + + @media (min-width: $medium-breakpoint) { + .divider { + display: inline-block; + } + } + } +} + +// 03. Show +// - - - - - - - - - - - - - - - - - - - - - - - - - + +.proposal-show { + padding-top: rem-calc(12); + + .back { + @include back; + } + + .icon-angle-left { + @extend .back; + } + + h1 { + clear: both; + font-size: rem-calc(30); + font-weight: bold; + margin: 0; + text-transform: lowercase; + + &:first-letter { + text-transform: uppercase; + } + } + + .edit-proposal { + margin-bottom: 0; + } + + .proposal-info { + clear: both; + color: $text-medium; + font-weight: lighter; + line-height: $line-height*2; + text-align: justify; + + a { + color: $text-medium; + } + + p { + font-size: rem-calc(15); + line-height: $line-height; + margin-bottom: 0; + } + } + + ul, ol { + + li { + font-size: rem-calc(13); + margin-bottom: rem-calc(12); + } + } + + .author-photo { + line-height: $line-height*2; + margin-right: rem-calc(6); + vertical-align: middle; + width: 32px; + } + + .author { + color: $text; + font-weight: bold; + } + + aside { + + h3 { + border-top: 2px solid $brand; + display: inline-block; + font-size: rem-calc(16); + margin: -1px 0 rem-calc(12); + padding-top: rem-calc(6); + text-transform: uppercase; + } + } + + .supports { + @include supports; + border: 0; + border-radius: 0; + margin: 0; + + .total-supports { + display: block; + float: none; + line-height: $line-height; + } + + .not-logged { + line-height: $line-height; + padding: rem-calc(24); + } + + @media (min-width: $small-breakpoint + em-calc(1)) and (max-width:$medium-breakpoint) { + .in-favor, .against { + text-align: left; + width: rem-calc(100); + } + } + + .divider { + display: none; + } + + @media (min-width: $medium-breakpoint) { + .divider { + display: inline-block; + } + } + } + + .leave-comment { + display: inline-block; + margin-top: rem-calc(24); + } + + .tags { + display: block; + margin: rem-calc(24) 0; + + a { + margin-right: rem-calc(6); + } + } +} + +.bullet { + color: $border; +} + +// 04. New +// - - - - - - - - - - - - - - - - - - - - - - - - - + +.proposal-new { + background: white; + padding-top: rem-calc(24); + + .back { + @include back; + } + + h1 { + clear: both; + font-size: rem-calc(36); + font-weight: bold; + line-height: $line-height*2; + margin-bottom: rem-calc(24); + } + + .icon-proposals { + color: $proposals; + font-size: rem-calc(50); + line-height: $line-height; + opacity: .5; + } + + h2 { + clear: both; + font-size: rem-calc(20); + font-weight: bold; + line-height: $line-height; + margin: 0; + } + + .recommendations { + list-style-type: none; + margin-left: 0; + margin-top: rem-calc(24); + + li { + font-size: rem-calc(12); + margin: rem-calc(12) 0; + + &:before { + color: $proposals; + content: "l "; + font-family: "icons" !important; + } + } + } +} + +.proposal-edit { + @extend .proposal-new; +} + +// 05. Comments +// - - - - - - - - - - - - - - - - - - - - - - - - - + +.comments { + background: $white; + background-repeat: repeat-x; + padding-top: rem-calc(24); + padding-bottom: rem-calc(96); + + h2 { + margin: 0; + font-weight: bold; + + span { + font-size: rem-calc(18); + font-weight: normal; + opacity: .8; + } + } + + .comment { + margin: rem-calc(6) 0; + + p { + margin-bottom: 0; + } + + .comment-votes { + color: $text-medium; + font-weight: lighter; + margin: rem-calc(8) rem-calc(12) rem-calc(6) 0; + + a { + color: $text-light; + display: inline-block; + vertical-align: top; + + &:hover { + color: $text-medium; + } + } + + [class^="icon-"] { + font-size: rem-calc(20); + vertical-align: middle; + } + } + + .comment-body { + margin-left: rem-calc(42); + + p { + font-size: rem-calc(14); + } + + .reply { + background: white; + border: 1px solid $border; + font-size: rem-calc(12); + margin: rem-calc(6) 0; + padding: rem-calc(6); + + .divider { + color: $text-light; + } + } + + .comment-user { + margin-top: rem-calc(6); + padding: rem-calc(6) 0; + overflow: hidden; + + @each $n in ("1", "2", "3","4", "5") { + &.level-#{$n} { + @if $n == "5" { + background: $comment-level-5; + padding: rem-calc(6) rem-calc(12); + } + @elseif $n == "1" { + background: none; + padding: rem-calc(6) rem-calc(12); + } + @else { + background: $comment-official; + padding: rem-calc(6) rem-calc(12); + } + } + } + + &.is-author { + background: $comment-author; + padding: rem-calc(6) rem-calc(12); + } + + &.is-admin { + background: $comment-admin; + padding: rem-calc(6) rem-calc(12); + } + + &.is-moderator { + @extend .is-admin; + } + + &.level-5 { + background: $comment-level-5; + padding: rem-calc(6) rem-calc(12); + } + } + } + + .is-deleted { + background: $deleted; + margin-left: rem-calc(42); + padding: rem-calc(6) rem-calc(12); + } + + .comment-children { + border-left: 1px dashed $border; + margin-left: rem-calc(42); + padding-left: rem-calc(6); + + @media only screen and (max-width: 40em) { + margin-left: rem-calc(16); + } + } + + .comment-info { + color: $text-light; + font-size: rem-calc(13); + font-weight: lighter; + margin-top: rem-calc(6); + vertical-align: middle; + + span.user-name { + color: $text; + font-weight: bold; + } + } + } +} + +.faded { + opacity: 0.4; +} + +// 06. Flags +// - - - - - - - - - - - - - - - - - - - - - - - - - + +.flag-content { + + .button { + background: none; + margin-bottom: 0; + padding: 0; + } +} + +.flag-disable { + color: $text-medium; + line-height: rem-calc(24); + vertical-align: middle; +} + +.flag-active { + @extend .flag-disable; + color: $delete; +} diff --git a/app/assets/stylesheets/variables.scss b/app/assets/stylesheets/variables.scss index 1954de5bd..b7077e54b 100644 --- a/app/assets/stylesheets/variables.scss +++ b/app/assets/stylesheets/variables.scss @@ -41,6 +41,9 @@ $votes-unlike-act: #BD6A6A; $delete: #F04124; $check: #46DB91; +$proposals: #FFA42D; +$proposals-border: #CC8425; + // 03. Forms // - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/controllers/account_controller.rb b/app/controllers/account_controller.rb index b0bd18e5b..38bd4bfe5 100644 --- a/app/controllers/account_controller.rb +++ b/app/controllers/account_controller.rb @@ -23,9 +23,9 @@ class AccountController < ApplicationController def account_params if @account.organization? - params.require(:account).permit(:phone_number, :email_on_debate_comment, :email_on_comment_reply, organization_attributes: [:name, :responsible_name]) + params.require(:account).permit(:phone_number, :email_on_comment, :email_on_comment_reply, organization_attributes: [:name, :responsible_name]) else - params.require(:account).permit(:username, :email_on_debate_comment, :email_on_comment_reply) + params.require(:account).permit(:username, :email_on_comment, :email_on_comment_reply) end end diff --git a/app/controllers/admin/proposals_controller.rb b/app/controllers/admin/proposals_controller.rb new file mode 100644 index 000000000..4f399bff7 --- /dev/null +++ b/app/controllers/admin/proposals_controller.rb @@ -0,0 +1,26 @@ +class Admin::ProposalsController < Admin::BaseController + has_filters %w{without_confirmed_hide all with_confirmed_hide}, only: :index + + before_action :load_proposal, only: [:confirm_hide, :restore] + + def index + @proposals = Proposal.only_hidden.send(@current_filter).order(hidden_at: :desc).page(params[:page]) + end + + def confirm_hide + @proposal.confirm_hide + redirect_to request.query_parameters.merge(action: :index) + end + + def restore + @proposal.restore + redirect_to request.query_parameters.merge(action: :index) + end + + private + + def load_proposal + @proposal = Proposal.with_hidden.find(params[:id]) + end + +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index aa6da3a80..d5d351038 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -3,6 +3,7 @@ require "application_responder" class ApplicationController < ActionController::Base include SimpleCaptcha::ControllerHelpers include HasFilters + include HasOrders before_action :authenticate_http_basic, if: :http_basic_auth_site? before_action :authenticate_user!, unless: :devise_controller?, if: :beta_site? @@ -78,6 +79,10 @@ class ApplicationController < ActionController::Base @debate_votes = current_user ? current_user.debate_votes(debates) : {} end + def set_proposal_votes(proposals) + @proposal_votes = current_user ? current_user.proposal_votes(proposals) : {} + end + def set_comment_flags(comments) @comment_flags = current_user ? current_user.comment_flags(comments) : {} end diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb index 1573ed590..35406faf0 100644 --- a/app/controllers/comments_controller.rb +++ b/app/controllers/comments_controller.rb @@ -55,11 +55,11 @@ class CommentsController < ApplicationController end def administrator_comment? - ["1", true].include?(comment_params[:as_administrator]) && can?(:comment_as_administrator, Debate) + ["1", true].include?(comment_params[:as_administrator]) && can?(:comment_as_administrator, @commentable) end def moderator_comment? - ["1", true].include?(comment_params[:as_moderator]) && can?(:comment_as_moderator, Debate) + ["1", true].include?(comment_params[:as_moderator]) && can?(:comment_as_moderator, @commentable) end end diff --git a/app/controllers/concerns/has_orders.rb b/app/controllers/concerns/has_orders.rb new file mode 100644 index 000000000..31a98e850 --- /dev/null +++ b/app/controllers/concerns/has_orders.rb @@ -0,0 +1,12 @@ +module HasOrders + extend ActiveSupport::Concern + + class_methods do + def has_orders(valid_orders, *args) + before_action(*args) do + @valid_orders = valid_orders + @current_order = @valid_orders.include?(params[:order]) ? params[:order] : @valid_orders.first + end + end + end +end diff --git a/app/controllers/debates_controller.rb b/app/controllers/debates_controller.rb index b336a924a..849091afc 100644 --- a/app/controllers/debates_controller.rb +++ b/app/controllers/debates_controller.rb @@ -1,16 +1,17 @@ class DebatesController < ApplicationController - before_action :parse_order, only: :index before_action :parse_tag_filter, only: :index before_action :parse_search_terms, only: :index before_action :authenticate_user!, except: [:index, :show] + has_orders %w{confidence_score hot_score created_at most_commented random}, only: :index load_and_authorize_resource + respond_to :html, :js def index @debates = @search_terms.present? ? Debate.search(@search_terms) : Debate.all @debates = @debates.tagged_with(@tag_filter) if @tag_filter - @debates = @debates.page(params[:page]).for_render.send("sort_by_#{@order}") + @debates = @debates.page(params[:page]).for_render.send("sort_by_#{@current_order}") @tag_cloud = Debate.tag_counts.order(taggings_count: :desc, name: :asc).limit(20) set_debate_votes(@debates) end @@ -82,11 +83,6 @@ class DebatesController < ApplicationController @featured_tags = ActsAsTaggableOn::Tag.where(featured: true) end - def parse_order - @valid_orders = ['confidence_score', 'hot_score', 'created_at', 'most_commented', 'random'] - @order = @valid_orders.include?(params[:order]) ? params[:order] : @valid_orders.first - end - def parse_tag_filter if params[:tag].present? @tag_filter = params[:tag] if ActsAsTaggableOn::Tag.where(name: params[:tag]).exists? diff --git a/app/controllers/moderation/proposals_controller.rb b/app/controllers/moderation/proposals_controller.rb new file mode 100644 index 000000000..52b4222e9 --- /dev/null +++ b/app/controllers/moderation/proposals_controller.rb @@ -0,0 +1,44 @@ +class Moderation::ProposalsController < Moderation::BaseController + + has_filters %w{pending_flag_review all with_ignored_flag}, only: :index + has_orders %w{created_at flags}, only: :index + + before_filter :load_proposals, only: [:index, :moderate] + + load_and_authorize_resource + + def index + @proposals = @proposals.send(@current_filter) + .send("sort_by_#{@current_order}") + .page(params[:page]) + .per(50) + end + + def hide + @proposal.hide + end + + def moderate + @proposals = @proposals.where(id: params[:proposal_ids]) + + if params[:hide_proposals].present? + @proposals.accessible_by(current_ability, :hide).each(&:hide) + + elsif params[:ignore_flags].present? + @proposals.accessible_by(current_ability, :ignore_flag).each(&:ignore_flag) + + elsif params[:block_authors].present? + author_ids = @proposals.pluck(:author_id).uniq + User.where(id: author_ids).accessible_by(current_ability, :block).each(&:block) + end + + redirect_to request.query_parameters.merge(action: :index) + end + + private + + def load_proposals + @proposals = Proposal.accessible_by(current_ability, :moderate) + end + +end diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb index a00c266dc..d761ff4c7 100644 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb @@ -34,6 +34,15 @@ class PagesController < ApplicationController def transparency end + def proposals_info + end + + def participation_facts + end + + def participation_world + end + def blog redirect_to "http://diario.madrid.es/blog/category/gobiernoabierto/" end diff --git a/app/controllers/proposals_controller.rb b/app/controllers/proposals_controller.rb new file mode 100644 index 000000000..f55604c1f --- /dev/null +++ b/app/controllers/proposals_controller.rb @@ -0,0 +1,94 @@ +class ProposalsController < ApplicationController + before_action :parse_tag_filter, only: :index + before_action :parse_search_terms, only: :index + before_action :authenticate_user!, except: [:index, :show] + has_orders %w{confidence_score hot_score created_at most_commented random}, only: :index + + load_and_authorize_resource + respond_to :html, :js + + def index + @proposals = @search_terms.present? ? Proposal.search(@search_terms) : Proposal.all + @proposals = @proposals.tagged_with(@tag_filter) if @tag_filter + @proposals = @proposals.page(params[:page]).for_render.send("sort_by_#{@current_order}") + @tag_cloud = Proposal.tag_counts.order(taggings_count: :desc, name: :asc).limit(20) + set_proposal_votes(@proposals) + end + + def show + set_proposal_votes(@proposal) + @commentable = @proposal + @root_comments = @proposal.comments.roots.recent.page(params[:page]).per(10).for_render + @comments = @root_comments.inject([]){|all, root| all + Comment.descendants_of(root).for_render} + + @all_visible_comments = @root_comments + @comments + set_comment_flags(@all_visible_comments) + end + + def new + @proposal = Proposal.new + load_featured_tags + end + + def create + @proposal = Proposal.new(proposal_params) + @proposal.author = current_user + + if @proposal.save_with_captcha + ahoy.track :proposal_created, proposal_id: @proposal.id + redirect_to @proposal, notice: t('flash.actions.create.notice', resource_name: 'Proposal') + else + load_featured_tags + render :new + end + end + + def edit + load_featured_tags + end + + def update + @proposal.assign_attributes(proposal_params) + if @proposal.save_with_captcha + redirect_to @proposal, notice: t('flash.actions.update.notice', resource_name: 'Proposal') + else + load_featured_tags + render :edit + end + end + + def flag + Flag.flag(current_user, @proposal) + respond_with @proposal, template: 'proposals/_refresh_flag_actions' + end + + def unflag + Flag.unflag(current_user, @proposal) + respond_with @proposal, template: 'proposals/_refresh_flag_actions' + end + + def vote + @proposal.register_vote(current_user, 'yes') + set_proposal_votes(@proposal) + end + + private + + def proposal_params + params.require(:proposal).permit(:title, :question, :summary, :description, :external_url, :video_url, :responsible_name, :tag_list, :terms_of_service, :captcha, :captcha_key) + end + + def load_featured_tags + @featured_tags = ActsAsTaggableOn::Tag.where(featured: true) + end + + def parse_tag_filter + if params[:tag].present? + @tag_filter = params[:tag] if ActsAsTaggableOn::Tag.where(name: params[:tag]).exists? + end + end + + def parse_search_terms + @search_terms = params[:search] if params[:search].present? + end +end diff --git a/app/controllers/verification/letter_controller.rb b/app/controllers/verification/letter_controller.rb index e1e23903b..a6b5125bc 100644 --- a/app/controllers/verification/letter_controller.rb +++ b/app/controllers/verification/letter_controller.rb @@ -13,7 +13,7 @@ class Verification::LetterController < ApplicationController def create @letter = Verification::Letter.new(user: current_user) if @letter.save - redirect_to edit_letter_path, notice: t('verification.letter.create.flash.success') + redirect_to edit_letter_path else flash.now.alert = t('verification.letter.create.alert.failure') render :new diff --git a/app/controllers/welcome_controller.rb b/app/controllers/welcome_controller.rb index 56c609121..9211f1082 100644 --- a/app/controllers/welcome_controller.rb +++ b/app/controllers/welcome_controller.rb @@ -4,11 +4,26 @@ class WelcomeController < ApplicationController layout "devise", only: :welcome def index - @featured_debates = Debate.sort_by_confidence_score.limit(3).for_render - set_debate_votes(@featured_debates) + if current_user + redirect_to :proposals + end end def welcome end + def highlights + debates = Debate.sort_by_hot_score.page(params[:page]).per(10).for_render + set_debate_votes(debates) + + proposals = Proposal.sort_by_hot_score.page(params[:page]).per(10).for_render + set_proposal_votes(proposals) + + @list = (debates.to_a + proposals.to_a).sort{|a, b| b.hot_score <=> a.hot_score} + @paginator = debates.total_pages > proposals.total_pages ? debates : proposals + + render 'highlights' + end + + end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 57c81b4d6..54eadb947 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -6,6 +6,7 @@ module ApplicationHelper end def home_page? + return false if user_signed_in? # Using path because fullpath yields false negatives since it contains # parameters too request.path == '/' diff --git a/app/helpers/cache_keys_helper.rb b/app/helpers/cache_keys_helper.rb index be23439dd..566b3cd81 100644 --- a/app/helpers/cache_keys_helper.rb +++ b/app/helpers/cache_keys_helper.rb @@ -10,7 +10,7 @@ module CacheKeysHelper if user_signed_in? user_status += ":signed" - user_status += ":verified" if current_user.verified_at.present? + user_status += ":verified" if current_user.level_two_or_three_verified? user_status += ":org" if current_user.organization? user_status += ":admin" if current_user.administrator? user_status += ":moderator" if current_user.moderator? diff --git a/app/helpers/proposals_helper.rb b/app/helpers/proposals_helper.rb new file mode 100644 index 000000000..42090a276 --- /dev/null +++ b/app/helpers/proposals_helper.rb @@ -0,0 +1,21 @@ +module ProposalsHelper + + def progress_bar_percentage(proposal) + case proposal.cached_votes_up + when 0 then 0 + when 1..Proposal.votes_needed_for_success then (proposal.cached_votes_up.to_f * 100 / Proposal.votes_needed_for_success).floor + else 100 + end + end + + def supports_percentage(proposal) + percentage = (proposal.cached_votes_up.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) + else "100%" + end + end + +end \ No newline at end of file diff --git a/app/helpers/tags_helper.rb b/app/helpers/tags_helper.rb new file mode 100644 index 000000000..b8476ac0a --- /dev/null +++ b/app/helpers/tags_helper.rb @@ -0,0 +1,14 @@ +module TagsHelper + + def taggable_path(taggable, tag_name) + case taggable + when 'debate' + debates_path(tag: tag_name) + when 'proposal' + proposals_path(tag: tag_name) + else + '#' + end + end + +end diff --git a/app/helpers/text_with_links_helper.rb b/app/helpers/text_with_links_helper.rb index 55281e89e..ca07daf39 100644 --- a/app/helpers/text_with_links_helper.rb +++ b/app/helpers/text_with_links_helper.rb @@ -6,4 +6,9 @@ module TextWithLinksHelper Rinku.auto_link(sanitized, :all, 'target="_blank" rel="nofollow"').html_safe end + def safe_html_with_links(html) + return html unless html.html_safe? + Rinku.auto_link(html, :all, 'target="_blank" rel="nofollow"').html_safe + end + end diff --git a/app/helpers/votes_helper.rb b/app/helpers/votes_helper.rb index dfc1bbda0..dafb108c7 100644 --- a/app/helpers/votes_helper.rb +++ b/app/helpers/votes_helper.rb @@ -1,7 +1,7 @@ module VotesHelper - def css_classes_for_debate_vote(debate_votes, debate) - case debate_votes[debate.id] + def css_classes_for_vote(votes, votable) + case votes[votable.id] when true {in_favor: "voted", against: "no-voted"} when false @@ -11,4 +11,8 @@ module VotesHelper end end + def voted_for?(votes, votable) + votes[votable.id] + end + end diff --git a/app/models/ability.rb b/app/models/ability.rb index 0d2ac3f8b..23c6f9f01 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -9,7 +9,7 @@ class Ability # Not logged in users can :read, Debate - + can :read, Proposal if user # logged-in users can [:read, :update], User, id: user.id @@ -19,8 +19,14 @@ class Ability debate.editable_by?(user) end + can :read, Proposal + can :update, Proposal do |proposal| + proposal.editable_by?(user) + end + can :create, Comment can :create, Debate + can :create, Proposal can [:flag, :unflag], Comment cannot [:flag, :unflag], Comment, user_id: user.id @@ -28,11 +34,18 @@ class Ability can [:flag, :unflag], Debate cannot [:flag, :unflag], Debate, author_id: user.id + can [:flag, :unflag], Proposal + cannot [:flag, :unflag], Proposal, author_id: user.id + unless user.organization? can :vote, Debate can :vote, Comment end + if user.level_two_or_three_verified? + can :vote, Proposal + end + if user.moderator? || user.administrator? can :read, Organization can(:verify, Organization){ |o| !o.verified? } @@ -52,12 +65,24 @@ class Ability can :ignore_flag, Debate, ignored_flag_at: nil, hidden_at: nil cannot :ignore_flag, Debate, author_id: user.id + can :hide, Proposal, hidden_at: nil + cannot :hide, Proposal, author_id: user.id + + can :ignore_flag, Proposal, ignored_flag_at: nil, hidden_at: nil + cannot :ignore_flag, Proposal, author_id: user.id + + can :moderate, Proposal + cannot :moderate, Proposal, author_id: user.id + can :hide, User cannot :hide, User, id: user.id + + can :block, User + cannot :block, User, id: user.id end if user.moderator? - can :comment_as_moderator, [Debate, Comment] + can :comment_as_moderator, [Debate, Comment, Proposal] end if user.administrator? @@ -67,6 +92,9 @@ class Ability can :restore, Debate cannot :restore, Debate, hidden_at: nil + can :restore, Proposal + cannot :restore, Proposal, hidden_at: nil + can :restore, User cannot :restore, User, hidden_at: nil @@ -76,10 +104,13 @@ class Ability can :confirm_hide, Debate cannot :confirm_hide, Debate, hidden_at: nil + can :confirm_hide, Proposal + cannot :confirm_hide, Proposal, hidden_at: nil + can :confirm_hide, User cannot :confirm_hide, User, hidden_at: nil - can :comment_as_administrator, [Debate, Comment] + can :comment_as_administrator, [Debate, Comment, Proposal] can :manage, Moderator end diff --git a/app/models/comment.rb b/app/models/comment.rb index 73bd0b732..227448da2 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -1,4 +1,5 @@ class Comment < ActiveRecord::Base + include Flaggable acts_as_paranoid column: :hidden_at include ActsAsParanoidAliases @@ -9,21 +10,16 @@ class Comment < ActiveRecord::Base validates :body, presence: true validates :user, presence: true - validates_inclusion_of :commentable_type, in: ["Debate"] + validates_inclusion_of :commentable_type, in: ["Debate", "Proposal"] validate :validate_body_length belongs_to :commentable, -> { with_hidden }, polymorphic: true, counter_cache: true belongs_to :user, -> { with_hidden } - has_many :flags, as: :flaggable - scope :recent, -> { order(id: :desc) } scope :sort_for_moderation, -> { order(flags_count: :desc, updated_at: :desc) } - scope :pending_flag_review, -> { where(ignored_flag_at: nil, hidden_at: nil) } - scope :with_ignored_flag, -> { where(hidden_at: nil).where.not(ignored_flag_at: nil) } - scope :flagged, -> { where("flags_count > 0") } scope :for_render, -> { with_hidden.includes(user: :organization) } @@ -68,14 +64,6 @@ class Comment < ActiveRecord::Base cached_votes_down end - def ignored_flag? - ignored_flag_at.present? - end - - def ignore_flag - update(ignored_flag_at: Time.now) - end - def as_administrator? administrator_id.present? end diff --git a/app/models/comment_notifier.rb b/app/models/comment_notifier.rb index 8a82e9df0..3cceb5c15 100644 --- a/app/models/comment_notifier.rb +++ b/app/models/comment_notifier.rb @@ -12,16 +12,16 @@ class CommentNotifier private def send_comment_email - Mailer.comment(@comment).deliver_later if email_on_debate_comment? + Mailer.comment(@comment).deliver_later if email_on_comment? end def send_reply_email Mailer.reply(@comment).deliver_later if email_on_comment_reply? end - def email_on_debate_comment? + def email_on_comment? commentable_author = @comment.commentable.author - commentable_author != @author && commentable_author.email_on_debate_comment? + commentable_author != @author && commentable_author.email_on_comment? end def email_on_comment_reply? diff --git a/app/models/concerns/flaggable.rb b/app/models/concerns/flaggable.rb new file mode 100644 index 000000000..613ce360b --- /dev/null +++ b/app/models/concerns/flaggable.rb @@ -0,0 +1,19 @@ +module Flaggable + extend ActiveSupport::Concern + + included do + has_many :flags, as: :flaggable + scope :flagged, -> { where("flags_count > 0") } + scope :pending_flag_review, -> { where(ignored_flag_at: nil, hidden_at: nil) } + scope :with_ignored_flag, -> { where.not(ignored_flag_at: nil).where(hidden_at: nil) } + end + + def ignored_flag? + ignored_flag_at.present? + end + + def ignore_flag + update(ignored_flag_at: Time.now) + end + +end diff --git a/app/models/concerns/verification.rb b/app/models/concerns/verification.rb new file mode 100644 index 000000000..fe5172c65 --- /dev/null +++ b/app/models/concerns/verification.rb @@ -0,0 +1,47 @@ +module Verification + extend ActiveSupport::Concern + + included do + scope :level_three_verified, -> { where.not(verified_at: nil) } + scope :level_two_verified, -> { where("users.confirmed_phone IS NOT NULL AND users.residence_verified_at IS NOT NULL") } + scope :level_two_or_three_verified, -> { where("users.verified_at IS NOT NULL OR (users.confirmed_phone IS NOT NULL AND users.residence_verified_at IS NOT NULL)") } + scope :unverified, -> { where("users.verified_at IS NULL AND (users.confirmed_phone IS NULL OR users.residence_verified_at IS NOT NULL)") } + end + + def verification_email_sent? + email_verification_token.present? + end + + def verification_sms_sent? + unconfirmed_phone.present? && sms_confirmation_code.present? + end + + def verification_letter_sent? + letter_requested_at.present? && letter_verification_code.present? + end + + def residence_verified? + residence_verified_at.present? + end + + def sms_verified? + confirmed_phone.present? + end + + def level_two_verified? + residence_verified? && sms_verified? + end + + def level_three_verified? + verified_at.present? + end + + def level_two_or_three_verified? + level_two_verified? || level_three_verified? + end + + def unverified? + !level_two_or_three_verified? + end + +end diff --git a/app/models/debate.rb b/app/models/debate.rb index db3133877..d141e4330 100644 --- a/app/models/debate.rb +++ b/app/models/debate.rb @@ -1,5 +1,6 @@ require 'numeric' class Debate < ActiveRecord::Base + include Flaggable apply_simple_captcha acts_as_votable @@ -9,7 +10,6 @@ class Debate < ActiveRecord::Base belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' has_many :comments, as: :commentable - has_many :flags, as: :flaggable validates :title, presence: true validates :description, presence: true @@ -26,9 +26,6 @@ class Debate < ActiveRecord::Base before_save :calculate_hot_score, :calculate_confidence_score scope :sort_for_moderation, -> { order(flags_count: :desc, updated_at: :desc) } - scope :pending_flag_review, -> { where(ignored_flag_at: nil, hidden_at: nil) } - scope :with_ignored_flag, -> { where.not(ignored_flag_at: nil).where(hidden_at: nil) } - scope :flagged, -> { where("flags_count > 0") } scope :for_render, -> { includes(:tags) } scope :sort_by_hot_score , -> { order(hot_score: :desc) } scope :sort_by_confidence_score , -> { order(confidence_score: :desc) } @@ -100,37 +97,20 @@ class Debate < ActiveRecord::Base count < 0 ? 0 : count end - def ignored_flag? - ignored_flag_at.present? - end - - def ignore_flag - update(ignored_flag_at: Time.now) - end - def after_commented save # updates the hot_score because there is a before_save end def calculate_hot_score - start = Time.new(2015, 6, 15) - comments_weight = 1.0/20 # 1 positive vote / x comments - time_unit = 12.hours.to_f - - total = cached_votes_total + comments_weight * comments_count - ups = cached_votes_up + comments_weight * comments_count - downs = total - ups - score = ups - downs - order = Math.log([score.abs, 1].max, 10) - sign = (score <=> 0).to_f - seconds = ((created_at || Time.now) - start).to_f - - self.hot_score = (((order * sign) + (seconds/time_unit)) * 1000000).round + self.hot_score = ScoreCalculator.hot_score(created_at, + cached_votes_total, + cached_votes_up, + comments_count) end def calculate_confidence_score - return unless cached_votes_total > 0 - self.confidence_score = cached_votes_score * (cached_votes_up / cached_votes_total.to_f) * 100 + self.confidence_score = ScoreCalculator.confidence_score(cached_votes_total, + cached_votes_up) end def self.search(terms) diff --git a/app/models/proposal.rb b/app/models/proposal.rb new file mode 100644 index 000000000..24045e8f7 --- /dev/null +++ b/app/models/proposal.rb @@ -0,0 +1,181 @@ +class Proposal < ActiveRecord::Base + include Flaggable + + apply_simple_captcha + acts_as_votable + acts_as_taggable + acts_as_paranoid column: :hidden_at + include ActsAsParanoidAliases + + belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' + has_many :comments, as: :commentable + + validates :title, presence: true + validates :question, presence: true + validates :summary, presence: true + validates :author, presence: true + validates :responsible_name, presence: true + + validate :validate_title_length + validate :validate_question_length + validate :validate_description_length + validate :validate_responsible_length + + validates :terms_of_service, acceptance: { allow_nil: false }, on: :create + + before_validation :sanitize_description + before_validation :sanitize_tag_list + before_validation :set_responsible_name + + before_save :calculate_hot_score, :calculate_confidence_score + + scope :for_render, -> { includes(:tags) } + scope :sort_by_hot_score , -> { order(hot_score: :desc) } + scope :sort_by_confidence_score , -> { order(confidence_score: :desc) } + scope :sort_by_created_at, -> { order(created_at: :desc) } + scope :sort_by_most_commented, -> { order(comments_count: :desc) } + scope :sort_by_random, -> { order("RANDOM()") } + scope :sort_by_flags, -> { order(flags_count: :desc, updated_at: :desc) } + + def total_votes + cached_votes_up + end + + def conflictive? + return false unless flags_count > 0 && cached_votes_up > 0 + cached_votes_up/flags_count.to_f < 5 + end + + def description + super.try :html_safe + end + + def tag_list_with_limit(limit = nil) + return tags if limit.blank? + + tags.sort{|a,b| b.taggings_count <=> a.taggings_count}[0, limit] + end + + def tags_count_out_of_limit(limit = nil) + return 0 unless limit + + count = tags.size - limit + count < 0 ? 0 : count + end + + def description + super.try :html_safe + end + + def editable? + total_votes <= Setting.value_for("max_votes_for_proposal_edit").to_i + end + + def editable_by?(user) + author_id == user.id && editable? + end + + def votable_by?(user) + user.level_two_or_three_verified? + end + + def register_vote(user, vote_value) + if votable_by?(user) + vote_by(voter: user, vote: vote_value) + end + end + + def code + "#{Setting.value_for("proposal_code_prefix")}-#{created_at.strftime('%Y-%M')}-#{id}" + end + + def after_commented + save # updates the hot_score because there is a before_save + end + + def calculate_hot_score + self.hot_score = ScoreCalculator.hot_score(created_at, + cached_votes_up, + cached_votes_up, + comments_count) + end + + def calculate_confidence_score + self.confidence_score = ScoreCalculator.confidence_score(cached_votes_up, + cached_votes_up) + end + + def self.title_max_length + @@title_max_length ||= self.columns.find { |c| c.name == 'title' }.limit || 80 + end + + def self.question_max_length + 140 + end + + def self.description_max_length + 6000 + end + + def self.responsible_name_max_length + 60 + end + + def self.search(terms) + terms.present? ? where("title ILIKE ? OR description ILIKE ? OR question ILIKE ?", "%#{terms}%", "%#{terms}%", "%#{terms}%") : none + end + + def self.votes_needed_for_success + Setting.value_for('votes_for_proposal_success').to_i + end + + protected + + def sanitize_description + self.description = WYSIWYGSanitizer.new.sanitize(description) + end + + def sanitize_tag_list + self.tag_list = TagSanitizer.new.sanitize_tag_list(self.tag_list) + end + + def set_responsible_name + if author && author.level_two_or_three_verified? + self.responsible_name = author.document_number + end + end + + private + + def validate_description_length + validator = ActiveModel::Validations::LengthValidator.new( + attributes: :description, + maximum: Proposal.description_max_length) + validator.validate(self) + end + + def validate_title_length + validator = ActiveModel::Validations::LengthValidator.new( + attributes: :title, + minimum: 4, + maximum: Proposal.title_max_length) + validator.validate(self) + end + + def validate_question_length + validator = ActiveModel::Validations::LengthValidator.new( + attributes: :title, + minimum: 10, + maximum: Proposal.question_max_length) + validator.validate(self) + end + + def validate_responsible_length + validator = ActiveModel::Validations::LengthValidator.new( + attributes: :title, + minimum: 6, + maximum: Proposal.responsible_name_max_length) + validator.validate(self) + end + +end diff --git a/app/models/setting.rb b/app/models/setting.rb index f696ceefd..2d0926418 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -1,7 +1,7 @@ class Setting < ActiveRecord::Base validates :key, presence: true, uniqueness: true - default_scope { order(key: :desc) } + default_scope { order(id: :asc) } def self.value_for(key) where(key: key).pluck(:value).first diff --git a/app/models/user.rb b/app/models/user.rb index 723f92458..657e3123b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -86,6 +86,11 @@ class User < ActiveRecord::Base voted.each_with_object({}) { |v, h| h[v.votable_id] = v.value } end + def proposal_votes(proposals) + voted = votes.for_proposals(proposals) + voted.each_with_object({}) { |v, h| h[v.votable_id] = v.value } + end + def comment_flags(comments) comment_flags = flags.for_comments(comments) comment_flags.each_with_object({}){ |f, h| h[f.flaggable_id] = true } @@ -123,10 +128,13 @@ class User < ActiveRecord::Base def block debates_ids = Debate.where(author_id: id).pluck(:id) comments_ids = Comment.where(user_id: id).pluck(:id) + proposal_ids = Proposal.where(author_id: id).pluck(:id) self.hide + Debate.hide_all debates_ids Comment.hide_all comments_ids + Proposal.hide_all proposal_ids end diff --git a/app/views/account/show.html.erb b/app/views/account/show.html.erb index e43c424de..1df3e408e 100644 --- a/app/views/account/show.html.erb +++ b/app/views/account/show.html.erb @@ -29,25 +29,25 @@
<%= t("debates.comment.deleted") %>
+<%= t("comments.comment.deleted") %>
<%= t("debates.index.select_order") %>
+ <%= render 'shared/order_selector', i18n_namespace: "debates.index" %> +<%= t("debates.index.select_order_long") %>
+ <%= render 'shared/order_selector', i18n_namespace: "debates.index" %> +<%= t("layouts.header.open_city_title") %>
-<%= t("layouts.header.open_city_slogan") %>
- <%= link_to t("layouts.header.see_all_debates"), debates_path, class: "button radius see-more" %> +<%= t("layouts.header.open_city_slogan_html") %>
+ <%= link_to t("layouts.header.see_all"), highlights_path, class: "button radius see-more warning" %> <%= link_to t("layouts.header.more_information"), "/more_information", class: "more-info" %><%= t("moderation.proposals.index.title") %>
+ +<%= render 'shared/filter_subnav', i18n_namespace: "moderation.proposals.index" %> + +<%= page_entries_info @proposals %>
++ <%= t('shared.check') %>: + <%= link_to t('shared.check_all'), '#', data: {check_all: "proposal_ids[]"} %> + | + <%= link_to t('shared.check_none'), '#', data: {check_none: "proposal_ids[]"} %> +
+ ++ <%= l proposal.updated_at.to_date %> + • + <%= proposal.flags_count %> + • + <%= proposal.author.username %> +
+
¿Cómo funciona este Portal de Gobierno Abierto?
-+ + <%= link_to t("debates.show.back_link"), "/more_information", class: 'left back' %> +
-
I. Participación
- I.1. Espacio de debate
+ - I.2. Espacio de propuestas
@@ -39,6 +41,14 @@Tanto los hilos, como los comentarios podrán ser valorados por cualquiera, de tal manera que será la propia ciudadanía, y nadie en su nombre, la que decida cuáles son los temas más importantes en cada momento. Estos serán presentados en la portada del espacio, pudiendo por supuesto accederse a todos los demás temas en páginas posteriores, o usando otros criterios de ordenación (los temas con más comentarios, los más nuevos, los más controvertidos, etc.).
Cada uno de los trabajadores del Ayuntamiento tiene un usuario propio, que será resaltado como tal, permitiendo que participen en los debates al mismo nivel que todos los demás ciudadanos. Esto permitirá crear espacios de comunicación directos entre unos y otros, evitando los inconvenientes que implica la comunicación medidada, y respondiendo a un planteamiento claro por parte del nuevo gobierno de Madrid por el cual el Ayuntamiento trabaja para la ciudadanía, y ante ella debe responder.
+ +I.I. Espacio de propuestas
+ +En este espacio, cualquier persona puede proponer una iniciativa con la intención de recabar los suficientes apoyos como para que la idea pase a ser consultada a toda la ciudadanía con caracter vinculante.
+ +Las propuestas pueden ser apoyadas por ciudadanos empadronados en Madrid que hayan verificado su cuenta en la plataforma de participación, de tal manera que será la propia ciudadanía, y nadie en su nombre, la que decida cuáles son las propuestas que merecen la pena ser llevadas a cabo.
+ +Una vez que una propuesta alcance una cantidad de apoyos equivalente al 2% del censo de Madrid (unos 53000), automaticamente pasa a ser estudiada por un grupo de trabajo del ayuntamiento y pasará a la siguiente fase de consulta popular, en la que la ciudadanía de Madrid votará si se lleva a cabo o no. El plazo máximo para recabar los apoyos necesarios será de 12 meses.
Utilízalo en tu municipio
+ + <%= link_to t("debates.show.back_link"), "/more_information", class: 'left back' %> + +Utilízalo en tu municipio
Utilízalo en tu municipio libremente o ayúdanos a mejorarlo, es software libre.
Este Portal de Gobierno Abierto usa la aplicación Consul que es software libre, con licencia AGPLv3, esto significa en palabras sencillas, que cualquiera puede libremente usar el código, copiarlo, verlo en detalle, modificarlo, y redistribuirlo al mundo con las modificaciones que quiera (manteniendo el que otros puedan a su vez hacer lo mismo). Porque creemos que la cultura es mejor y más rica cuando se libera.
diff --git a/app/views/pages/more_information.html.erb b/app/views/pages/more_information.html.erb index c00e5f108..4b64b2137 100644 --- a/app/views/pages/more_information.html.erb +++ b/app/views/pages/more_information.html.erb @@ -21,7 +21,27 @@ Participación Ciudadana, Transparencia y Gobierno Abierto + +Si tienes problemas con la web contacta con el servicio técnico.
--
- I. Participación
-
- -
- II. Transparencia
-
-
+ + <%= link_to t("debates.show.back_link"), "/more_information", class: 'left back' %> +-- I.1. Propuestas ciudadanas
- - I.2. Presupuestos participativos
- - I.3. Legislacion colaborativa
- - I.4. Co-gobierno ciudadano
- - I.5. Participación sectorial
- - I.6. Inclusión, neutralidad y privacidad
- - I.7. Innovación social
-
--- II.1. Ordenanza de transparencia
- - II.2. Portal de transparencia
- - II.3. Datos abiertos
- - II.4. Agendas públicas
- - II.5. Transparencia del Lobby
-
-+-
+ I. Participación
+
+ -
+ II. Transparencia
+
+
+- I.1. Propuestas ciudadanas
+ - I.2. Presupuestos participativos
+ - I.3. Legislacion colaborativa
+ - I.4. Co-gobierno ciudadano
+ - I.5. Participación sectorial
+ - I.6. Inclusión, neutralidad y privacidad
+ - I.7. Innovación social
+
++- II.1. Ordenanza de transparencia
+ - II.2. Portal de transparencia
+ - II.3. Datos abiertos
+ - II.4. Agendas públicas
+ - II.5. Transparencia del Lobby
+
+Participación y Transparencia en Madrid - Próximas novedades
diff --git a/app/views/pages/participation_facts.html.erb b/app/views/pages/participation_facts.html.erb new file mode 100644 index 000000000..019076022 --- /dev/null +++ b/app/views/pages/participation_facts.html.erb @@ -0,0 +1,33 @@ +Hechos sobre participación ciudadana y democracia directa
++
La democracia directa produce gente más informada y con más cultura política.
+El poner en marcha mecanismos de participación reales hace que la gente se preocupe por las decisiones que tiene que tomar, y que por lo tanto se informe sobre ellas. Esto se observa sistemáticamente al comparar diferentes regiones de un mismo país con diferentes grados de democracia directa, o en procesos particulares como por ejemplo la votación del Tratado por una Constitución para Europa, comparando los países que lo votaron en referéndum y los que no.
+En ocasiones se argumenta que no se deben crear mecanismos de decisión ciudadana, porque un posible bajo nivel cultural o de conocimiento político haría que se tomaran malas decisiones. Esto se ha argumentado tradicionalmente en contra del voto femenino, del voto inmigrante, del voto de la gente sin renta, etc. pero lo que se ha demostrado es que permitir a toda esa gente decidir ha sido precisamente lo que ha permitido que escaparan de su situación de desigualdad cultural y de derechos, o al menos que la mejoraran considerablemente.
+M. Benz / A. Stutzer (2004), «Are voters better informed when they have a larger say in politics?», Public Choice 119, p. 31-59
+ +La democracia directa es más efectiva para tomar decisiones que la representativa. Por ejemplo genera menos deuda.
+Los proyectos disparatados y los gastos desmesurados suelen venir por parte de políticos electos en lugar de por procesos de participación ciudadana. Los estados de Estados Unidos con sistemas de iniciativas populares efectivos generan un 7% menos de deuda que los que no los tienen, y las regiones en Suiza con referéndum obligatorios para gastos públicos importantes gastan un 19% menos que los demás.
+R.K. von Weizsäcker (1992), «Staatsverschuldung und Demokratie», Kyklos 45, p. 51-67
+L.P. Feld / J.G. Matsusaka (2003), »Budget referendums and government spending: evidence from Swiss cantons», Journal of Public Economics 87, p. 2703-2724
+J. G. Matsusaka (2004), «For the Many or the Few. The Initiative, Public Policy, and American Democracy» University of Chicago Press
+ +La democracia directa protege mejor los derechos humanos y los de las minorías que la representativa
+Estudios de todos los referéndum suizos durante treinta años muestran que los referéndum cuando tratan sobre aumentar los derechos de las minorías se aprueban aproximadamente un 25% más que los que tratan de temas generales. En concreto, el 80% de los referéndum de este tipo a nivel federal fueron aprobados.
+Decisiones como eliminar el derecho a la vivienda de manera efectiva, el derecho de sanidad para las personas migrantes, o el iniciar guerras contra otros países, suelen darse a través de gobiernos representativos, y no en referéndum ciudadanos.
+B.S. Frey / M. Goette (1998), «Does the popular vote destroy civil rights?», American Journal of Political Science 42, p. 1343-1348
+ +En cualquier caso, la democracia directa no sustituye ni amenaza a la democracia representativa, la complementa. Y soluciona algunos problemas que no consigue la otra.
+El año que más iniciativas ciudadanas se votaron en los diferentes estados con democracia directa de Estados Unidos, estas decisiones sólo representaron el 0,6% de todas las decisiones que tomaron los políticos en esos estados.
+Las decisiones directas de la ciudadanía no sustituyen en ningún lugar del mundo el sistema político representativo, sólo lo amplían y mejoran. Tampoco pretenden ser la solución a todos los problemas, también se toman decisiones equivocadas a través de las decisiones ciudadanas, pero se reduce el número de las decisiones erróneas que se toman teniendo sólo un sistema representativo.
+M.D. Waters (2002), «Initiative and referendum in the United States: a primer», Washington: Citizen Lawmaker Press
+ +Si los ciudadanos son capaces de elegir entre políticos que toman decisiones buenas o malas para el país, deben ser capaces de elegir directamente entre buenas y malas decisiones.
++-
+ La nueva ola global de participación ciudadana
+
+ -
+ Participación ciudadana directa con experiencia
+
+
+Participación ciudadana directa en el mundo
+En el mundo existen sistemas de participación ciudadana muy similares al que vamos a implementar en Madrid, que llevan funcionando desde hace más de cien años y en países muy diferentes. La experiencia larga y variada de dichos sistemas demuestra que lo que ponemos en marcha en Madrid tendrá un impacto muy positivo en la sociedad, como ha tenido en esos otros países.
+ +Además de los ejemplos clásicos, como Suiza, asistimos a un desarrollo muy fuerte de los sistemas de participación ciudadana en todo el mundo, en particular en los últimos años, gracias especialmente a las nuevas posibilidades que nos brinda Internet. Islandia, Finlandia, Brasil, Estados Unidos, son algunos de los países que más están apostando por una participación directa de la ciudadanía en la toma de decisiones.
+ +La nueva ola global de participación ciudadana
+ +Las nuevas formas de participación se están dirigiendo principalmente a que sean la ciudadanía quien decida qué caminos debe tomar la política de su país, a través de mecanismos de iniciativas ciudadanas. Finlandia es uno de los países donde se están desarrollando nuevas herramientas similares al nuevo portal de gobierno abierto de Madrid. Su plataforma Open Ministry permite a la población presentar y apoyar propuestas, y ha conseguido por ejemplo que se apruebe gracias a él la ley de matrimonio igualitario. Islandia también ha tenido una gran repercusión desde 2011, cuando lanzaron su plataforma Better Reykjavik, que ha permitido que el 58% de la población participe en el proceso de propuestas, seleccionando cada mes las 15 ideas más votadas.
+ +Estonia es uno de los países que gracias a una apuesta clara por las nuevas tecnologías, ha podido situarse en cabeza de Europa en el nivel de uso por parte de la ciudadanía de Internet para la interacción con el gobierno. No sólo los ciudadanos y ciudadanas resuelven diariamente todos sus trámites a través de Internet, sino que han puesto en marcha plataformas como Rahvakogu, donde después de los escándalos políticos de 2012, 50.000 personas (de un total de 1.3 millones) participaron proponiendo medidas para mejorar la situación democrática del país.
+ +Otra de las principales experiencias que se están extendiendo rápidamente por todo el mundo son los presupuestos participativos. Estos consisten en mecanismos, generalmente acompañados de una plataforma en Internet, por los cuales el gobierno reserva parte de sus presupuestos de inversión (los que no están comprometidos ya en cuestiones como limpieza o servicios sociales), para que sea la ciudadanía quien decida en qué se gasta dicho dinero.
+ +Islandia es uno de los países de referencia al respecto, a través de su plataforma Betri Reykjavík-Betri Hverfi (Better District). Cada año se gastan 1.8 millones de euros en alrededor de 200 proyectos propuestos por la población para los distintos barrios de Reykjavík. Las ciudadanas y ciudadanos pueden participar y seguir el proyecto a través de la plataforma digital betrireykjavik, en Facebook o a través de centros en sus distritos o comités distritales.
+ +Otras experiencias de referencia incluyen París, donde los residentes decidirán hasta el 2020 cómo se gastan 426 millones de euros (lo que corresponde al 5% del presupuesto municipal de París); Nueva York, donde el año pasado dedicaron 32 millones de euros a presupuestos participativos; más de 100 ciudades brasileñas donde se han manejado presupuestos participativos que han oscilado entre el 5% y el 15%; entre muchas otras ciudades del mundo.
+ +Participación ciudadana directa con experiencia
+ +En diferentes países del mundo existen sistemas de participación ciudadana directa que vienen funcionando desde hace mucho tiempo sin pasar por las nuevas tecnologías. Aunque estos sistemas no tengan la agilidad de las nuevas plataformas, los procesos que se dan en ellos son básicamente los mismos, y nos aseguran la calidad y los resultados de los mecanismos que se van a poner en marcha. Comentamos a continuación tres de los casos más importantes y resaltamos algunas cifras para entender de lo que hablamos:
+ +Suiza
+Desde 1848 se han votado unos 600 referéndum a nivel federal (compilación en francés), y se celebran cada año en todo el país del orden de 200 referéndum a todos los niveles (municipal, cantonal y federal).
+Aproximadamente una de cada dos leyes aprobadas en el Parlamento directamente, y luego consultada a la ciudadanía, fue anulada. Por otro lado, incluso aunque las iniciativas populares no tengan éxito, el gobierno acaba concediendo parte de las demandas propuestas, y se genera una atención nacional sobre el asunto tratado. Esto hace que aproximadamente la mitad de la gente que lanzó las iniciativas que no tuvieron éxito consideren que mereció la pena el esfuerzo y se obtuvo algo que no hubiera sido posible sin la iniciativa.
+ +Estados Unidos
+En este país no se cuenta con procesos de iniciativas ciudadanas a nivel federal, pero 27 de los 51 estados cuentan con algún tipo sistema de democracia directa (cambiando mucho la regulación y el alcance de un estado a otro). A nivel local, aproximadamente la mitad de las ciudades cuentan con un sistema de iniciativas ciudadanas vinculantes.
+Entre 1904 y 2000 se convocaron casi 2.000 referéndum iniciados por la ciudadanía. En 1996 en los estados con mecanismos de iniciativas ciudadanas se convocaron 96 referéndum, frente a las más de 14.000 leyes y resoluciones aprobadas por los representantes de esos estados.
+ +Alemania
+No existe en Alemania ningún mecanismo de democracia directa a nivel nacional, pero sí a nivel regional y local. Estos sistemas se introdujeron en su mayoría en los años 90. En 1996 el número de iniciativas ciudadanas fue de 318, reduciéndose en años posteriores, una vez tratados tantos temas pendientes anteriores a la introducción de estos mecanismos, a una media de 100 por año.
+ ++-
+ Explicación detallada del proceso
+
+ -
+ Preguntas Frecuentes
+
+ -
+ Hojas de firmas para recoger apoyos
+
+
+¿Cómo funcionan las propuestas ciudadanas?
+El mecanismo de propuestas ciudadanas se resume en cuatro pasos muy sencillos:
++- ¡Propones! Creas una propuesta en esta web.
+ - ¡Apoyas! La gente hace click en el botón de apoyar tu propuesta (necesitas el apoyo del 2% de los empadronados mayores de 16 años para pasar a la siguiente fase).
+ - ¡Decides! Si has conseguido suficientes apoyos, dejamos 45 días para que la gente pueda debatir sobre la propuesta, y después durante una semana se invita a toda la gente de Madrid a decidir si están a favor o en contra de tu propuesta, en esta misma web.
+ - ¡Se hace! Si hay más gente a favor de tu propuesta que en contra, el gobierno del Ayuntamiento de Madrid asumirá como propia la propuesta y la llevará a cabo.
+
+ +Además no hace falta ni que tengas Internet, todos los pasos se pueden hacer en cualquiera de las 26 Oficinas de Atención al Ciudadano que hay por todo Madrid.
+ +Explicación detallada del proceso
+ ++- Creación de una propuesta. Cualquier persona (sin necesidad siquiera de estar empadronada en Madrid) puede crear una propuesta. Lo único que hay que hacer es pulsar el botón “Crear una propuesta” y rellenar los campos requeridos. La propuesta puede ser tan sencilla como una simple frase, pero te recomendamos detallarla todo lo posible, incluso añadiendo material adicional, para que sea más completa e interesante. Una vez creada aparecerá en esta web para que cualquiera pueda apoyarla.
+ - Apoyo de propuestas. Para apoyar una de las propuestas que aparece en la web, pulsamos el botón “apoyar esta propuesta” que aparece en cada una. Para este paso tendremos que estar empadronados en Madrid, así que al llevarlo a cabo por primera vez se nos pedirá que verifiquemos nuestra cuenta para estar seguros de este requerimiento. Se nos va a pedir que introduzcamos algunos datos para comprobar nuestra información de empadronamiento, y se nos enviará un código personal para que el proceso sea seguro. Las propuestas necesitan una cierta cantidad de apoyos para pasar a la siguiente fase; concretamente el 2% de los empadronados mayores de 16 años (que suponen 53.726 apoyos).
+ - Decisión sobre propuestas. Cuando una propuesta consigue los apoyos necesarios, se anuncia en la web. Desde ese momento se dejan 45 días para que todo el mundo pueda debatir e informarse sobre la propuesta. Todas las otras propuestas que hayan conseguido los apoyos necesarios en los primeros 30 días del tiempo de debate se agruparán junto a la primera para decidir sobre ellas al mismo tiempo. Pasados los 45 días se exponen estas propuestas en un espacio especial de votación de la web, donde durante una semana cualquier persona empadronada en Madrid y mayor de 16 años podrá decidir si está a favor o rechaza la propuesta. Para participar en este paso tendrás que tener tu cuenta de usuario verificada completamente, de tal forma que nos aseguremos que cada persona no tiene más de una cuenta y el proceso es seguro.
+ - Realización de las propuestas. En caso de que haya más gente a favor de una propuesta que rechazándola se aceptará como propuesta colectiva de la ciudadanía de Madrid, y el gobierno del Ayuntamiento de Madrid la asumirá como propia y la llevará a cabo. Para ello en un plazo máximo de un mes, se realizarán los informes técnicos correspondientes sobre su legalidad, viabilidad y coste económico, teniendo en cuenta a los sectores afectados y a la persona que haya lanzado la propuesta, para detallar la actuación correspondiente por parte del Ayuntamiento. Se publicarán en la web todos los informes realizados, y un seguimiento de las actuaciones que se lleven a cabo, para asegurar un correcto desarrollo de la propuesta.
+
+ +Todas las acciones relacionadas con el proceso de propuestas ciudadanas pueden realizarse a través del portal de gobierno abierto, o presencialmente en cualquiera de las 26 Oficinas de Atención al Ciudadano existentes en Madrid. Ver la lista completa de oficinas y su ubicación.
+ +El proceso de recogida de apoyos de una propuesta puede realizarse también a través de hojas de firmas, cuyo modelo puede ser descargado en este enlace. Los apoyos recogidos de esta manera se sumarán a los apoyos ya existentes en el portal de gobierno abierto. Las hojas pueden ser entregadas en cualquiera de los Registros del Ayuntamiento, presentes en cada una de las Juntas de Distrito. Ver la lista completa de Oficinas de Registro.
+ + +Preguntas Frecuentes
++- ¿Cuáles son los requisitos que se solicitan para poder apoyar o votar propuestas?
+
+ - ¿Cómo va a controlarse que cada persona vote una única vez por propuesta?
+
+ - ¿Se llevará a cabo cualquier propuesta que se acepte por mayoría en la web? ¿incluso aunque sean ilegales o atenten contra los derechos humanos? ¿y si no hay presupuesto para llevarla a cabo?
+
+ - ¿Qué diferencia existe entre los debates y las propuestas?
+
+ - ¿Vota toda la gente de Madrid en cada propuesta? ¿incluso aunque haga referencia a un único distrito?
+
+ - ¿Y si se presentan varias propuestas iguales? ¿Se plantea la posibilidad de unificarlas para evitar que se diversifiquen los apoyos y/o votos?
+
+ - ¿Durante cuanto tiempo se pueden recoger apoyos?
+
+ - ¿Durante cuanto tiempo se votará cada propuesta?
+
+ - ¿Pueden participar asociaciones, fundaciones y ONG´s? ¿Y empresas?
+
+ - ¿Cuánta gente tiene que votar una propuesta para que sea aprobada? ¿Qué quórum mínimo se necesita para que las votaciones sean vinculantes?
+
+ - ¿Existen mecanismos presenciales para participar? ¿Se ha planteado llegar a los ciudadanos y ciudadanas con dificultades de acceso a Internet o en situación de exclusión?
+
+ - ¿Cómo puede participar la gente que no esté empadronada en Madrid?
+
+ - ¿El voto es secreto? ¿el Ayuntamiento podrá tener acceso a los votos de los ciudadanos?
+
+ - ¿Al ser electrónico el voto no se aumenta el riesgo de fraude?
+ No, incluso lo reducimos. Hay una sensación subjetiva muy fuerte de diferencia de seguridad entre procesos en papel y procesos electrónicos que no se corresponde con la realidad. En las elecciones generales donde el voto es en papel, después de que las mesas hayan hecho el recuento, las papeletas de los votos se tiran a la basura, y la persona responsable se dirige en persona con el recuento en la mano al centro donde se comparten los datos. Y sin embargo pensemos por ejemplo en el sistema de tarjetas de crédito y cuentas bancarias, donde cada día confiamos la seguridad de nuestro dinero a un sistema electrónico. En el caso del Ayuntamiento, la votación está asegurada mediante diferentes entidades externas, y además cualquier votante puede tanto comprobar que su voto particular está en el recuento final, como realizar el recuento completo de todos los votos por su cuenta. Dos garantías de seguridad completamente imposibles en un votación tradicional.
+
+ - ¿Cómo se sabe que una propuesta se ha cumplido?
+
+
+ Estar empadronado en Madrid y ser mayor de 16 años. En caso de que se haga a través de Internet se requerirá que se haya verificado la cuenta de usuario de la web (encontramos el botón de verificación en el apartado “Mi cuenta” en la esquina superior derecha), proporcionando la información del padrón, y un medio de comunicación para obtener un código seguro de participación que introduciremos en la web para validar nuestra cuenta. +
+ Las votaciones presenciales en las Oficinas de Atención al Ciudadano están controladas porque la persona tiene que presentar su DNI. Para la votación a través de Internet se verifica que la cuenta de usuario corresponde a un único ciudadano/a. Para ello se le facilita un código personal seguro de verificación de la cuenta que se comunica a través de canales de comunicación privados, como son teléfonos móviles que constan en el Ayuntamiento que pertenecen a la persona adecuada, o por correo a través del buzón que figura en la dirección de empadronamiento. De esta forma nos aseguramos de que sólo esa persona ha recibido el código. En caso de que dichos medios de comunicación no estén disponibles se solicitará a la persona que acuda presencialmente a alguna de las Oficinas de Atención al Ciudadano, para obtener su código. +
+ Se establecen una serie de criterios objetivos por los que una propuesta no podrá ser aceptada como, por ejemplo, que tenga fines delictivos o que atente contra derechos fundamentales y libertades públicas. Además se excluyen del proceso las propuestas sobre otras modalidades de participación tales como presupuestos participativos, procesos revocatorios, audiencia pública o iniciativas normativas populares. +
Este mecanismo de participación no cambia los límites ya existentes para el Ayuntamiento, lo que hace es trasladar quién toma la decisión de lo que haya que hacer, incluyendo a toda la ciudadanía en el proceso de toma de decisiones. +
En el caso de las propuestas que quedan fuera de las competencias municipales el Ayuntamiento no podrá llevarlas a cabo, pero emprenderá actuaciones alternativas dentro de sus capacidades que intenten cumplir con la decisión de la propuesta. Lo mismo sucede con el presupuesto para llevar a cabo la propuesta; el ayuntamiento utilizará los recursos necesarios en cuanto sea posible, de la misma manera que se costean las decisiones adoptadas sin participación ciudadana. +
También puede ocurrir que la actuación requiera la aprobación del Pleno, por lo que el resultado dependerá del posicionamiento de todas las fuerzas políticas respecto a la actuación presentada.
+ Aunque ambos pueden ser apoyados, los primeros no activan ningún mecanismo de actuación concreto, mientras que las segundas pasan a una fase de decisión colectiva y en caso de que sean aprobadas, son asumidas por el Ayuntamiento.
+ Sí. El mecanismo actual se desarrolla a nivel de toda la ciudad. Lo que no entra en colisión con que a nivel distrital se puedan desarrollar mecanismos de participación ciudadana que afecten únicamente a cada distrito.
+ Propuestas similares pueden ser marcadas como tales en la plataforma, lo que se señala al visualizar cada una de ellas. Más allá de eso no se unifican, dejando que sea la gente la que decida si apoyar a una o a otra.
+ Las propuestas pueden recoger apoyos durante 12 meses. Si en ese tiempo no consiguen alcanzar el 2% de los apoyos necesarios dejan de poder recibir más apoyos.
+ Una vez que las propuestas reciben el apoyo del 2% pasan a la fase de decisión final donde la gente puede votar aceptando o rechazando la propuesta durante un plazo de una semana.
+ Las propuestas tienen que ser presentadas individualmente, sin problema de que la persona que la presenta represente a una organización de cualquier tipo. Al registrar una propuesta en la web existe la posibilidad de señalar este caso como registro de asociaciones/colectivos.
+ El quórum es el mínimo de participación necesaria para considerar una votación vinculante de manera legal. Ningún reglamento del Ayuntamiento puede hacer que este mecanismo sea vinculante juridicamente, porque eso está en contra de la legislación española. La vinculación con el mecanismo es política y se asume de manera personal por los concejales y la Alcaldesa. Por ello no se considerará ningún quórum.
+ Todas las acciones relacionadas con el proceso de propuestas ciudadanas pueden realizarse presencialmente en cualquiera de las 26 Oficinas de Atención al Ciudadano repartidas por todos los distritos de Madrid. Además, el proceso de recogida de apoyos de una propuesta puede realizarse también a través de hojas de firmas, cuyo modelo puede ser descargado en este enlace. +
Adicionalmente se ha creado en el Área de Gobierno de Participación Ciudadana, Transparencia y Gobierno Abierto el Servicio de Inclusión, Neutralidad y Privacidad que pondrá en marcha una mesa de inclusión con personal del Ayuntamiento y asociaciones que trabajan con colectivos en situación de exclusión, para diseñar mecanismos especiales para que puedan participar dichos colectivos.
+ Presentando propuestas, participando en los debates, y difundiendo lo que ocurra en la plataforma.
+ El voto es totalmente secreto. Se encripta de manera conjunta con diferentes autoridades externas al Ayuntamiento, de tal manera que para poder conocer la identidad de un votante todas las autoridades tendrían que realizar fraude conjuntamente.
+ Cuando se aprueba una propuesta, el Ayuntamiento publica en la página web cuáles son los pasos que se van a realizar para ponerla en práctica, incluyendo los informes que las correspondientes áreas redacten y las acciones que se emprenden. La información es completa y transparente para que puedas hacer un seguimiento de cómo evoluciona cada propuesta ciudadana.
Hojas de firmas para recoger apoyos
+ +El proceso de recogida de apoyos de una propuesta, además de en la web, puede realizarse a través de hojas de firmas, cuyo modelo puede ser descargado en este enlace.
+La hoja debe contener en las casillas superiores el código de la propuesta y su título, según figura en la página web específica de la propuesta, dentro del Portal de Gobierno Abierto.
+Los apoyos recogidos de esta manera se sumarán a los apoyos ya existentes en el portal de gobierno abierto. Las hojas pueden ser entregadas en cualquiera de los Registros del Ayuntamiento, presentes en cada una de las Juntas de Distrito. Ver la lista completa de Oficinas de Registro.
+ ++ <%= t("proposals.show.comments_title") %> + (<%= @proposal.comments_count %>) +
+ + <% if user_signed_in? %> + <%= render 'comments/form', {commentable: @proposal, parent_id: nil, toggeable: false} %> + <% else %> ++ +
+-
+ <%= link_to t('shared.flag'), flag_proposal_path(proposal), method: :put, remote: true, id: "flag-proposal-#{ proposal.id }" %>
+
+
+ <% end %> + + <% if show_unflag_action? proposal %> + + + ++-
+ <%= link_to t('shared.unflag'), unflag_proposal_path(proposal), method: :put, remote: true, id: "unflag-proposal-#{ proposal.id }" %>
+
+
+ <% end %> + diff --git a/app/views/proposals/_form.html.erb b/app/views/proposals/_form.html.erb new file mode 100644 index 000000000..1d98a05d6 --- /dev/null +++ b/app/views/proposals/_form.html.erb @@ -0,0 +1,84 @@ +<%= form_for(@proposal) do |f| %> + <%= render 'shared/errors', resource: @proposal %> + +<%= link_to proposal.title, proposal %>
++ + <%= link_to t("proposals.proposal.comments", count: proposal.comments_count), proposal_path(proposal, anchor: "comments") %> + • + <%= l proposal.created_at.to_date %> +
+<%= t("proposals.edit.editing") %>
+ + <%= render "form" %> ++ <%= page_entries_info @proposals %> + <%= t("proposals.index.search_results", count: @proposals.size, search_term: @search_terms) %> +
+ <% elsif @tag_filter %> ++ <%= page_entries_info @proposals %> + <%= t("proposals.index.filter_topic", count: @proposals.size, topic: @tag_filter) %> +
+ <% end %> ++ <%= t("proposals.index.select_order") %> +
+ <%= render 'shared/order_selector', i18n_namespace: "proposals.index" %> ++ <%= t("proposals.index.select_order_long") %> +
+ <%= render 'shared/order_selector', i18n_namespace: "proposals.index" %> +<%= t("proposals.new.start_new") %>
+<%= t("proposals.new.recommendations_title") %>
++- <%= t("proposals.new.recommendation_one") %>
+ - <%= t("proposals.new.recommendation_two") %>
+ - <%= t("proposals.new.recommendation_three") %>
+
+<%= @proposal.title %>
+ <% if @proposal.conflictive? %> +<%= @proposal.question %>
+ + <%= render 'shared/tags', proposal: @proposal %> + +<%= t("shared.search_form.search_title") %>
+<%= t("#{i18n_namespace}.title") %>
<%= t("shared.tags_cloud.tags") %>
<% tag_cloud @tag_cloud, %w[s m l] do |tag, css_class| %> - <%= link_to sanitize("#{tag.name} #{tag.taggings_count}"), debates_path(tag: tag.name), class: css_class %> + <%= link_to sanitize("#{tag.name} #{tag.taggings_count}"), taggable_path(taggable, tag.name), class: css_class %> <% end %>
<%= t("verification.letter.edit.title") %>
+ <%= t("verification.letter.new.explanation_html") %> + + <%= link_to t("verification.letter.new.offices"), t("verification.letter.new.offices_url"), + target: "_blank", class: "button radius inline-block" + %> + +<%= link_to featured_debate.title, featured_debate %>
-- - <%= link_to t("debates.show.comments", count: featured_debate.comments_count), debate_path(featured_debate, anchor: "comments") %> -
-+ <%= t('welcome.signed_in_home_title') %> +
+<%= t("welcome.last_debates") %>
+<%= t("welcome.debates.title") %>
+<%= t("welcome.debates.description") %>
+ +<%= t("welcome.proposal.title") %>
+<%= t("welcome.proposal.description") %>
+<%= t("welcome.decide.title") %>
+<%= t("welcome.decide.description") %>
+<%= t("welcome.do.title") %>
+<%= t("welcome.do.description") %>
#{Faker::Lorem.paragraphs.join('
')}" + description = "#{Faker::Lorem.paragraphs.join('
')}
" debate = Debate.create!(author: author, title: Faker::Lorem.sentence(3), created_at: rand((Time.now - 1.week) .. Time.now), @@ -67,6 +76,26 @@ tags = Faker::Lorem.words(25) puts " #{debate.title}" end +puts "Creating Proposals" + +tags = Faker::Lorem.words(25) + +(1..30).each do |i| + author = User.reorder("RANDOM()").first + description = "#{Faker::Lorem.paragraphs.join('
')}
" + proposal = Proposal.create!(author: author, + title: Faker::Lorem.sentence(3), + question: Faker::Lorem.sentence(3), + summary: Faker::Lorem.sentence(3), + responsible_name: Faker::Name.name, + external_url: Faker::Internet.url, + description: description, + created_at: rand((Time.now - 1.week) .. Time.now), + tag_list: tags.sample(3).join(','), + terms_of_service: "1") + puts " #{proposal.title}" +end + puts "Commenting Debates" @@ -80,9 +109,21 @@ puts "Commenting Debates" end -puts "Commenting Comments" +puts "Commenting Proposals" (1..100).each do |i| + author = User.reorder("RANDOM()").first + proposal = Proposal.reorder("RANDOM()").first + Comment.create!(user: author, + created_at: rand(proposal.created_at .. Time.now), + commentable: proposal, + body: Faker::Lorem.sentence) +end + + +puts "Commenting Comments" + +(1..200).each do |i| author = User.reorder("RANDOM()").first parent = Comment.reorder("RANDOM()").first Comment.create!(user: author, @@ -94,7 +135,7 @@ puts "Commenting Comments" end -puts "Voting Debates & Comments" +puts "Voting Debates, Proposals & Comments" (1..100).each do |i| voter = not_org_users.reorder("RANDOM()").first @@ -110,6 +151,12 @@ end comment.vote_by(voter: voter, vote: vote) end +(1..100).each do |i| + voter = User.level_two_or_three_verified.reorder("RANDOM()").first + proposal = Proposal.reorder("RANDOM()").first + proposal.vote_by(voter: voter, vote: true) +end + puts "Flagging Debates & Comments" @@ -125,23 +172,32 @@ end Flag.flag(flagger, comment) end +(1..40).each do |i| + proposal = Proposal.reorder("RANDOM()").first + flagger = User.where(["users.id <> ?", proposal.author_id]).reorder("RANDOM()").first + Flag.flag(flagger, proposal) +end -puts "Ignoring flags in Debates & comments" + +puts "Ignoring flags in Debates, comments & proposals" Debate.flagged.reorder("RANDOM()").limit(10).each(&:ignore_flag) Comment.flagged.reorder("RANDOM()").limit(30).each(&:ignore_flag) +Proposal.flagged.reorder("RANDOM()").limit(10).each(&:ignore_flag) -puts "Hiding debates & comments" +puts "Hiding debates, comments & proposals" Comment.with_hidden.flagged.reorder("RANDOM()").limit(30).each(&:hide) Debate.with_hidden.flagged.reorder("RANDOM()").limit(5).each(&:hide) +Proposal.with_hidden.flagged.reorder("RANDOM()").limit(10).each(&:hide) -puts "Confirming hiding in debates & comments" +puts "Confirming hiding in debates, comments & proposals" Comment.only_hidden.flagged.reorder("RANDOM()").limit(10).each(&:confirm_hide) Debate.only_hidden.flagged.reorder("RANDOM()").limit(5).each(&:confirm_hide) +Proposal.only_hidden.flagged.reorder("RANDOM()").limit(5).each(&:confirm_hide) diff --git a/db/migrate/20150911171301_create_proposal.rb b/db/migrate/20150911171301_create_proposal.rb new file mode 100644 index 000000000..37dd1bb85 --- /dev/null +++ b/db/migrate/20150911171301_create_proposal.rb @@ -0,0 +1,22 @@ +class CreateProposal < ActiveRecord::Migration + def change + create_table :proposals do |t| + t.string "title", limit: 80 + t.text "description" + t.string "question" + t.string "external_url" + t.integer "author_id" + t.datetime "hidden_at" + t.integer "flags_count", default: 0 + t.datetime "ignored_flag_at" + t.integer "cached_votes_up", default: 0 + t.integer "comments_count", default: 0 + t.datetime "confirmed_hide_at" + t.integer "hot_score", limit: 8, default: 0 + t.integer "confidence_score", default: 0 + + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + end +end diff --git a/db/migrate/20150914113251_add_responsible_to_proposals.rb b/db/migrate/20150914113251_add_responsible_to_proposals.rb new file mode 100644 index 000000000..c565c29df --- /dev/null +++ b/db/migrate/20150914113251_add_responsible_to_proposals.rb @@ -0,0 +1,5 @@ +class AddResponsibleToProposals < ActiveRecord::Migration + def change + add_column :proposals, :responsible_name, :string, limit: 60 + end +end diff --git a/db/migrate/20150914114019_change_email_config_field_in_user.rb b/db/migrate/20150914114019_change_email_config_field_in_user.rb new file mode 100644 index 000000000..90c875d60 --- /dev/null +++ b/db/migrate/20150914114019_change_email_config_field_in_user.rb @@ -0,0 +1,5 @@ +class ChangeEmailConfigFieldInUser < ActiveRecord::Migration + def change + rename_column :users, :email_on_debate_comment, :email_on_comment + end +end diff --git a/db/migrate/20150914173834_add_summary_to_proposals.rb b/db/migrate/20150914173834_add_summary_to_proposals.rb new file mode 100644 index 000000000..7ebe2df9c --- /dev/null +++ b/db/migrate/20150914173834_add_summary_to_proposals.rb @@ -0,0 +1,5 @@ +class AddSummaryToProposals < ActiveRecord::Migration + def change + add_column :proposals, :summary, :text, limit: 280 + end +end diff --git a/db/migrate/20150914181921_add_more_indexes_for_ahoy.rb b/db/migrate/20150914181921_add_more_indexes_for_ahoy.rb new file mode 100644 index 000000000..7fa8bedc4 --- /dev/null +++ b/db/migrate/20150914181921_add_more_indexes_for_ahoy.rb @@ -0,0 +1,7 @@ +class AddMoreIndexesForAhoy < ActiveRecord::Migration + def change + add_index :ahoy_events, [:name, :time] + add_index :visits, [:started_at] + end +end + diff --git a/db/migrate/20150914182652_adds_indexes.rb b/db/migrate/20150914182652_adds_indexes.rb new file mode 100644 index 000000000..a22d0d16e --- /dev/null +++ b/db/migrate/20150914182652_adds_indexes.rb @@ -0,0 +1,19 @@ +class AddsIndexes < ActiveRecord::Migration + def change + add_index :debates, :author_id + add_index :debates, [:author_id, :hidden_at] + + add_index :proposals, :author_id + add_index :proposals, [:author_id, :hidden_at] + add_index :proposals, :cached_votes_up + add_index :proposals, :confidence_score + add_index :proposals, :hidden_at + add_index :proposals, :hot_score + + add_index :settings, :key + + add_index :verified_users, :document_number + add_index :verified_users, :phone + add_index :verified_users, :email + end +end diff --git a/db/migrate/20150914184018_add_indexes_for_searches.rb b/db/migrate/20150914184018_add_indexes_for_searches.rb new file mode 100644 index 000000000..64c46370c --- /dev/null +++ b/db/migrate/20150914184018_add_indexes_for_searches.rb @@ -0,0 +1,11 @@ +class AddIndexesForSearches < ActiveRecord::Migration + def change + add_index :debates, :title + add_index :debates, :description + + add_index :proposals, :title + add_index :proposals, :question + add_index :proposals, :summary + add_index :proposals, :description + end +end diff --git a/db/migrate/20150914191003_add_video_url_to_proposal.rb b/db/migrate/20150914191003_add_video_url_to_proposal.rb new file mode 100644 index 000000000..298038e97 --- /dev/null +++ b/db/migrate/20150914191003_add_video_url_to_proposal.rb @@ -0,0 +1,5 @@ +class AddVideoUrlToProposal < ActiveRecord::Migration + def change + add_column :proposals, :video_url, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index dca1e8354..0bfa35d54 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20150912145218) do +ActiveRecord::Schema.define(version: 20150914191003) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -51,6 +51,7 @@ ActiveRecord::Schema.define(version: 20150912145218) do t.string "ip" end + add_index "ahoy_events", ["name", "time"], name: "index_ahoy_events_on_name_and_time", using: :btree add_index "ahoy_events", ["time"], name: "index_ahoy_events_on_time", using: :btree add_index "ahoy_events", ["user_id"], name: "index_ahoy_events_on_user_id", using: :btree add_index "ahoy_events", ["visit_id"], name: "index_ahoy_events_on_visit_id", using: :btree @@ -104,13 +105,17 @@ ActiveRecord::Schema.define(version: 20150912145218) do t.integer "confidence_score", default: 0 end + add_index "debates", ["author_id", "hidden_at"], name: "index_debates_on_author_id_and_hidden_at", using: :btree + add_index "debates", ["author_id"], name: "index_debates_on_author_id", using: :btree add_index "debates", ["cached_votes_down"], name: "index_debates_on_cached_votes_down", using: :btree add_index "debates", ["cached_votes_score"], name: "index_debates_on_cached_votes_score", using: :btree add_index "debates", ["cached_votes_total"], name: "index_debates_on_cached_votes_total", using: :btree add_index "debates", ["cached_votes_up"], name: "index_debates_on_cached_votes_up", using: :btree add_index "debates", ["confidence_score"], name: "index_debates_on_confidence_score", using: :btree + add_index "debates", ["description"], name: "index_debates_on_description", using: :btree add_index "debates", ["hidden_at"], name: "index_debates_on_hidden_at", using: :btree add_index "debates", ["hot_score"], name: "index_debates_on_hot_score", using: :btree + add_index "debates", ["title"], name: "index_debates_on_title", using: :btree create_table "delayed_jobs", force: :cascade do |t| t.integer "priority", default: 0, null: false @@ -188,11 +193,45 @@ ActiveRecord::Schema.define(version: 20150912145218) do add_index "organizations", ["user_id"], name: "index_organizations_on_user_id", using: :btree + create_table "proposals", force: :cascade do |t| + t.string "title", limit: 80 + t.text "description" + t.string "question" + t.string "external_url" + t.integer "author_id" + t.datetime "hidden_at" + t.integer "flags_count", default: 0 + t.datetime "ignored_flag_at" + t.integer "cached_votes_up", default: 0 + t.integer "comments_count", default: 0 + t.datetime "confirmed_hide_at" + t.integer "hot_score", limit: 8, default: 0 + t.integer "confidence_score", default: 0 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "responsible_name", limit: 60 + t.text "summary" + t.string "video_url" + end + + add_index "proposals", ["author_id", "hidden_at"], name: "index_proposals_on_author_id_and_hidden_at", using: :btree + add_index "proposals", ["author_id"], name: "index_proposals_on_author_id", using: :btree + add_index "proposals", ["cached_votes_up"], name: "index_proposals_on_cached_votes_up", using: :btree + add_index "proposals", ["confidence_score"], name: "index_proposals_on_confidence_score", using: :btree + add_index "proposals", ["description"], name: "index_proposals_on_description", using: :btree + add_index "proposals", ["hidden_at"], name: "index_proposals_on_hidden_at", using: :btree + add_index "proposals", ["hot_score"], name: "index_proposals_on_hot_score", using: :btree + add_index "proposals", ["question"], name: "index_proposals_on_question", using: :btree + add_index "proposals", ["summary"], name: "index_proposals_on_summary", using: :btree + add_index "proposals", ["title"], name: "index_proposals_on_title", using: :btree + create_table "settings", force: :cascade do |t| t.string "key" t.string "value" end + add_index "settings", ["key"], name: "index_settings_on_key", using: :btree + create_table "simple_captcha_data", force: :cascade do |t| t.string "key", limit: 40 t.string "value", limit: 6 @@ -240,7 +279,7 @@ ActiveRecord::Schema.define(version: 20150912145218) do t.datetime "confirmed_at" t.datetime "confirmation_sent_at" t.string "unconfirmed_email" - t.boolean "email_on_debate_comment", default: false + t.boolean "email_on_comment", default: false t.boolean "email_on_comment_reply", default: false t.string "phone_number", limit: 30 t.string "official_position" @@ -275,6 +314,10 @@ ActiveRecord::Schema.define(version: 20150912145218) do t.datetime "updated_at", null: false end + add_index "verified_users", ["document_number"], name: "index_verified_users_on_document_number", using: :btree + add_index "verified_users", ["email"], name: "index_verified_users_on_email", using: :btree + add_index "verified_users", ["phone"], name: "index_verified_users_on_phone", using: :btree + create_table "visits", id: :uuid, default: nil, force: :cascade do |t| t.uuid "visitor_id" t.string "ip" @@ -303,6 +346,7 @@ ActiveRecord::Schema.define(version: 20150912145218) do t.datetime "started_at" end + add_index "visits", ["started_at"], name: "index_visits_on_started_at", using: :btree add_index "visits", ["user_id"], name: "index_visits_on_user_id", using: :btree create_table "votes", force: :cascade do |t| diff --git a/db/seeds.rb b/db/seeds.rb index 7714981c7..27da98e1c 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,6 +1,8 @@ # Default admin user (change password after first deploy to a server!) -admin = User.create!(username: 'admin', email: 'admin@madrid.es', password: '12345678', password_confirmation: '12345678', confirmed_at: Time.now, terms_of_service: "1") -admin.create_administrator +if Administrator.count == 0 + admin = User.create!(username: 'admin', email: 'admin@madrid.es', password: '12345678', password_confirmation: '12345678', confirmed_at: Time.now, terms_of_service: "1") + admin.create_administrator +end # Names for the moderation console, as a hint for moderators # to know better how to assign users with official positions @@ -12,3 +14,12 @@ Setting.create(key: 'official_level_5_name', value: 'Alcaldesa') # Max percentage of allowed anonymous votes on a debate Setting.create(key: 'max_ratio_anon_votes_on_debates', value: '50') + +# Max votes where a proposal is still editable +Setting.create(key: 'max_votes_for_proposal_edit', value: '1000') + +# Prefix for the Proposal codes +Setting.create(key: 'proposal_code_prefix', value: 'MAD') + +# Number of votes needed for proposal success +Setting.create(key: 'votes_for_proposal_success', value: '53726') diff --git a/lib/score_calculator.rb b/lib/score_calculator.rb new file mode 100644 index 000000000..7d77e82d6 --- /dev/null +++ b/lib/score_calculator.rb @@ -0,0 +1,30 @@ +module ScoreCalculator + + EPOC = Time.new(2015, 6, 15) + COMMENT_WEIGHT = 1.0/20 # 1 positive vote / x comments + TIME_UNIT = 12.hours.to_f + + def self.hot_score(date, votes_total, votes_up, comments_count) + total = (votes_total + COMMENT_WEIGHT * comments_count).to_f + ups = (votes_up + COMMENT_WEIGHT * comments_count).to_f + downs = total - ups + score = ups - downs + offset = Math.log([score.abs, 1].max, 10) * (ups / [total, 1].max) + sign = score <=> 0 + seconds = ((date || Time.now) - EPOC).to_f + + (((offset * sign) + (seconds/TIME_UNIT)) * 10000000).round + end + + def self.confidence_score(votes_total, votes_up) + return 0 unless votes_total > 0 + + votes_total = votes_total.to_f + votes_up = votes_up.to_f + votes_down = votes_total - votes_up + score = votes_up - votes_down + + score * (votes_up / votes_total) * 100 + end + +end diff --git a/lib/verification.rb b/lib/verification.rb deleted file mode 100644 index 67b0f4422..000000000 --- a/lib/verification.rb +++ /dev/null @@ -1,36 +0,0 @@ -module Verification - - def verification_email_sent? - email_verification_token.present? - end - - def verification_sms_sent? - unconfirmed_phone.present? && sms_confirmation_code.present? - end - - def verification_letter_sent? - letter_requested_at.present? && letter_verification_code.present? - end - - def residence_verified? - residence_verified_at.present? - end - - def sms_verified? - confirmed_phone.present? - end - - def level_two_verified? - residence_verified? && sms_verified? - end - - def level_three_verified? - verified_at.present? - end - - def unverified? - !level_two_verified? && !level_three_verified? - end - - -end \ No newline at end of file diff --git a/public/docs/formulario_propuestas_ciudadanas.pdf b/public/docs/formulario_propuestas_ciudadanas.pdf new file mode 100644 index 000000000..f8296d9c5 Binary files /dev/null and b/public/docs/formulario_propuestas_ciudadanas.pdf differ diff --git a/spec/controllers/concerns/has_orders_spec.rb b/spec/controllers/concerns/has_orders_spec.rb new file mode 100644 index 000000000..082c4c068 --- /dev/null +++ b/spec/controllers/concerns/has_orders_spec.rb @@ -0,0 +1,37 @@ +require 'rails_helper' + +describe 'HasOrders' do + + class FakeController < ActionController::Base; end + + controller(FakeController) do + include HasOrders + has_orders ['created_at', 'votes_count', 'flags_count'], only: :index + + def index + render text: "#{@current_order} (#{@valid_orders.join(' ')})" + end + end + + it "has the valid orders set up" do + get :index + expect(response.body).to eq('created_at (created_at votes_count flags_count)') + end + + describe "the current order" do + it "defaults to the first one on the list" do + get :index + expect(response.body).to eq('created_at (created_at votes_count flags_count)') + end + + it "can be changed by the order param" do + get :index, order: 'votes_count' + expect(response.body).to eq('votes_count (created_at votes_count flags_count)') + end + + it "defaults to the first one on the list if given a bogus order" do + get :index, order: 'foobar' + expect(response.body).to eq('created_at (created_at votes_count flags_count)') + end + end +end diff --git a/spec/factories.rb b/spec/factories.rb index 8513d4d16..cda9c01a8 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -7,6 +7,17 @@ FactoryGirl.define do terms_of_service '1' confirmed_at { Time.now } + trait :level_two do + residence_verified_at Time.now + confirmed_phone "611111111" + document_number "12345678Z" + end + + trait :level_three do + verified_at Time.now + document_number "12345678Z" + end + trait :hidden do hidden_at Time.now end @@ -101,6 +112,50 @@ FactoryGirl.define do end end + factory :proposal do + sequence(:title) { |n| "Proposal #{n} title" } + summary 'In summary, what we want is...' + description 'Proposal description' + question 'Proposal question' + external_url 'http://external_documention.es' + responsible_name 'John Snow' + terms_of_service '1' + association :author, factory: :user + + trait :hidden do + hidden_at Time.now + end + + trait :with_ignored_flag do + ignored_flag_at Time.now + end + + trait :with_confirmed_hide do + confirmed_hide_at Time.now + end + + trait :flagged do + after :create do |debate| + Flag.flag(FactoryGirl.create(:user), debate) + end + end + + trait :with_hot_score do + before(:save) { |d| d.calculate_hot_score } + end + + trait :with_confidence_score do + before(:save) { |d| d.calculate_confidence_score } + end + + trait :conflictive do + after :create do |debate| + Flag.flag(FactoryGirl.create(:user), debate) + 4.times { create(:vote, votable: debate) } + end + end + end + factory :vote do association :votable, factory: :debate association :voter, factory: :user diff --git a/spec/features/account_spec.rb b/spec/features/account_spec.rb index 93cfe3dd0..e0249072c 100644 --- a/spec/features/account_spec.rb +++ b/spec/features/account_spec.rb @@ -33,7 +33,7 @@ feature 'Account' do visit account_path fill_in 'account_username', with: 'Larry Bird' - check 'account_email_on_debate_comment' + check 'account_email_on_comment' check 'account_email_on_comment_reply' click_button 'Save changes' @@ -42,7 +42,7 @@ feature 'Account' do visit account_path expect(page).to have_selector("input[value='Larry Bird']") - expect(page).to have_selector("input[id='account_email_on_debate_comment'][value='1']") + expect(page).to have_selector("input[id='account_email_on_comment'][value='1']") expect(page).to have_selector("input[id='account_email_on_comment_reply'][value='1']") end @@ -51,7 +51,7 @@ feature 'Account' do visit account_path fill_in 'account_organization_attributes_name', with: 'Google' - check 'account_email_on_debate_comment' + check 'account_email_on_comment' check 'account_email_on_comment_reply' click_button 'Save changes' @@ -60,7 +60,7 @@ feature 'Account' do visit account_path expect(page).to have_selector("input[value='Google']") - expect(page).to have_selector("input[id='account_email_on_debate_comment'][value='1']") + expect(page).to have_selector("input[id='account_email_on_comment'][value='1']") expect(page).to have_selector("input[id='account_email_on_comment_reply'][value='1']") end diff --git a/spec/features/admin/proposals_spec.rb b/spec/features/admin/proposals_spec.rb new file mode 100644 index 000000000..e31b84578 --- /dev/null +++ b/spec/features/admin/proposals_spec.rb @@ -0,0 +1,85 @@ +require 'rails_helper' + +feature 'Admin proposals' do + + background do + admin = create(:administrator) + login_as(admin.user) + end + + scenario 'Restore' do + proposal = create(:proposal, :hidden) + visit admin_proposals_path + + click_link 'Restore' + + expect(page).to_not have_content(proposal.title) + + expect(proposal.reload).to_not be_hidden + end + + scenario 'Confirm hide' do + proposal = create(:proposal, :hidden) + visit admin_proposals_path + + click_link 'Confirm' + + expect(page).to_not have_content(proposal.title) + click_link('Confirmed') + expect(page).to have_content(proposal.title) + + expect(proposal.reload).to be_confirmed_hide + end + + scenario "Current filter is properly highlighted" do + visit admin_proposals_path + expect(page).to_not have_link('Pending') + expect(page).to have_link('All') + expect(page).to have_link('Confirmed') + + visit admin_proposals_path(filter: 'Pending') + expect(page).to_not have_link('Pending') + expect(page).to have_link('All') + expect(page).to have_link('Confirmed') + + visit admin_proposals_path(filter: 'all') + expect(page).to have_link('Pending') + expect(page).to_not have_link('All') + expect(page).to have_link('Confirmed') + + visit admin_proposals_path(filter: 'with_confirmed_hide') + expect(page).to have_link('All') + expect(page).to have_link('Pending') + expect(page).to_not have_link('Confirmed') + end + + scenario "Filtering proposals" do + create(:proposal, :hidden, title: "Unconfirmed proposal") + create(:proposal, :hidden, :with_confirmed_hide, title: "Confirmed proposal") + + visit admin_proposals_path(filter: 'pending') + expect(page).to have_content('Unconfirmed proposal') + expect(page).to_not have_content('Confirmed proposal') + + visit admin_proposals_path(filter: 'all') + expect(page).to have_content('Unconfirmed proposal') + expect(page).to have_content('Confirmed proposal') + + visit admin_proposals_path(filter: 'with_confirmed_hide') + expect(page).to_not have_content('Unconfirmed proposal') + expect(page).to have_content('Confirmed proposal') + end + + scenario "Action links remember the pagination setting and the filter" do + per_page = Kaminari.config.default_per_page + (per_page + 2).times { create(:proposal, :hidden, :with_confirmed_hide) } + + visit admin_proposals_path(filter: 'with_confirmed_hide', page: 2) + + click_on('Restore', match: :first, exact: true) + + expect(current_url).to include('filter=with_confirmed_hide') + expect(current_url).to include('page=2') + end + +end diff --git a/spec/features/admin_spec.rb b/spec/features/admin_spec.rb index a096b2b83..ec991e388 100644 --- a/spec/features/admin_spec.rb +++ b/spec/features/admin_spec.rb @@ -15,7 +15,8 @@ feature 'Admin' do login_as(user) visit admin_root_path - expect(current_path).to eq(root_path) + expect(current_path).not_to eq(admin_root_path) + expect(current_path).to eq(proposals_path) expect(page).to have_content "not authorized" end @@ -23,7 +24,8 @@ feature 'Admin' do login_as(moderator) visit admin_root_path - expect(current_path).to eq(root_path) + expect(current_path).not_to eq(admin_root_path) + expect(current_path).to eq(proposals_path) expect(page).to have_content "not authorized" end diff --git a/spec/features/ckeditor_spec.rb b/spec/features/ckeditor_spec.rb new file mode 100644 index 000000000..7f5edc6e8 --- /dev/null +++ b/spec/features/ckeditor_spec.rb @@ -0,0 +1,19 @@ +require 'rails_helper' + +feature 'CKEditor' do + + scenario 'is present before & after turbolinks update page', :js do + author = create(:user) + login_as(author) + + visit new_debate_path + + expect(page).to have_css "#cke_debate_description" + + click_link 'Debates' + click_link 'Start a debate' + + expect(page).to have_css "#cke_debate_description" + end + +end \ No newline at end of file diff --git a/spec/features/comments_spec.rb b/spec/features/comments/debates.rb similarity index 82% rename from spec/features/comments_spec.rb rename to spec/features/comments/debates.rb index 437286bf7..4b8b23ce8 100644 --- a/spec/features/comments_spec.rb +++ b/spec/features/comments/debates.rb @@ -1,7 +1,7 @@ require 'rails_helper' include ActionView::Helpers::DateHelper -feature 'Comments' do +feature 'Commenting debates' do let(:user) { create :user } let(:debate) { create :debate } @@ -305,4 +305,90 @@ feature 'Comments' do end end + feature 'Voting comments' do + background do + @manuela = create(:user, verified_at: Time.now) + @pablo = create(:user) + @debate = create(:debate) + @comment = create(:comment, commentable: @debate) + + login_as(@manuela) + end + + scenario 'Show' do + create(:vote, voter: @manuela, votable: @comment, vote_flag: true) + create(:vote, voter: @pablo, votable: @comment, vote_flag: false) + + visit debate_path(@debate) + + within("#comment_#{@comment.id}_votes") do + within(".in_favor") do + expect(page).to have_content "1" + end + + within(".against") do + expect(page).to have_content "1" + end + + expect(page).to have_content "2 votes" + end + end + + scenario 'Create', :js do + visit debate_path(@debate) + + within("#comment_#{@comment.id}_votes") do + find(".in_favor a").click + + within(".in_favor") do + expect(page).to have_content "1" + end + + within(".against") do + expect(page).to have_content "0" + end + + expect(page).to have_content "1 vote" + end + end + + scenario 'Update', :js do + visit debate_path(@debate) + + within("#comment_#{@comment.id}_votes") do + find('.in_favor a').click + find('.against a').click + + within('.in_favor') do + expect(page).to have_content "0" + end + + within('.against') do + expect(page).to have_content "1" + end + + expect(page).to have_content "1 vote" + end + end + + scenario 'Trying to vote multiple times', :js do + visit debate_path(@debate) + + within("#comment_#{@comment.id}_votes") do + find('.in_favor a').click + find('.in_favor a').click + + within('.in_favor') do + expect(page).to have_content "1" + end + + within('.against') do + expect(page).to have_content "0" + end + + expect(page).to have_content "1 vote" + end + end + end + end diff --git a/spec/features/comments/proposals.rb b/spec/features/comments/proposals.rb new file mode 100644 index 000000000..9c6b99a6a --- /dev/null +++ b/spec/features/comments/proposals.rb @@ -0,0 +1,395 @@ +require 'rails_helper' +include ActionView::Helpers::DateHelper + +feature 'Commenting proposals' do + let(:user) { create :user } + let(:proposal) { create :proposal } + + scenario 'Index' do + 3.times { create(:comment, commentable: proposal) } + + visit proposal_path(proposal) + + expect(page).to have_css('.comment', count: 3) + + comment = Comment.last + within first('.comment') do + expect(page).to have_content comment.user.name + expect(page).to have_content I18n.l(comment.created_at, format: :datetime) + expect(page).to have_content comment.body + end + end + + scenario 'Turns links into html links' do + create :comment, commentable: proposal, body: 'Built with http://rubyonrails.org/' + + visit proposal_path(proposal) + + within first('.comment') do + expect(page).to have_content 'Built with http://rubyonrails.org/' + expect(page).to have_link('http://rubyonrails.org/', href: 'http://rubyonrails.org/') + expect(find_link('http://rubyonrails.org/')[:rel]).to eq('nofollow') + expect(find_link('http://rubyonrails.org/')[:target]).to eq('_blank') + end + end + + scenario 'Sanitizes comment body for security' do + create :comment, commentable: proposal, body: " click me http://madrid.es" + + visit proposal_path(proposal) + + within first('.comment') do + expect(page).to have_content "click me http://madrid.es" + expect(page).to have_link('http://madrid.es', href: 'http://madrid.es') + expect(page).not_to have_link('click me') + end + end + + scenario 'Paginated comments' do + per_page = 10 + (per_page + 2).times { create(:comment, commentable: proposal)} + + visit proposal_path(proposal) + + expect(page).to have_css('.comment', count: per_page) + within("ul.pagination") do + expect(page).to have_content("1") + expect(page).to have_content("2") + expect(page).to_not have_content("3") + click_link "Next", exact: false + end + + expect(page).to have_css('.comment', count: 2) + end + + feature 'Not logged user' do + scenario 'can not see comments forms' do + create(:comment, commentable: proposal) + visit proposal_path(proposal) + + expect(page).to have_content 'You need to sign in or sign up to comment' + within('#comments') do + expect(page).to_not have_content 'Write a comment' + expect(page).to_not have_content 'Reply' + expect(page).to_not have_css('form') + end + end + end + + scenario 'Create', :js do + login_as(user) + visit proposal_path(proposal) + + fill_in "comment-body-proposal_#{proposal.id}", with: 'Have you thought about...?' + click_button 'Publish comment' + + within "#comments" do + expect(page).to have_content 'Have you thought about...?' + end + end + + scenario 'Errors on create', :js do + login_as(user) + visit proposal_path(proposal) + + click_button 'Publish comment' + + expect(page).to have_content "Can't be blank" + end + + scenario 'Reply', :js do + citizen = create(:user, username: 'Ana') + manuela = create(:user, username: 'Manuela') + comment = create(:comment, commentable: proposal, user: citizen) + + login_as(manuela) + visit proposal_path(proposal) + + click_link "Reply" + + within "#js-comment-form-comment_#{comment.id}" do + fill_in "comment-body-comment_#{comment.id}", with: 'It will be done next week.' + click_button 'Publish reply' + end + + within "#comment_#{comment.id}" do + expect(page).to have_content 'It will be done next week.' + end + + expect(page).to_not have_selector("#js-comment-form-comment_#{comment.id}", visible: true) + end + + scenario 'Errors on reply', :js do + comment = create(:comment, commentable: proposal, user: user) + + login_as(user) + visit proposal_path(proposal) + + click_link "Reply" + + within "#js-comment-form-comment_#{comment.id}" do + click_button 'Publish reply' + expect(page).to have_content "Can't be blank" + end + + end + + scenario "N replies", :js do + parent = create(:comment, commentable: proposal) + + 7.times do + create(:comment, commentable: proposal, parent: parent) + parent = parent.children.first + end + + visit proposal_path(proposal) + expect(page).to have_css(".comment.comment.comment.comment.comment.comment.comment.comment") + end + + scenario "Flagging as inappropriate", :js do + comment = create(:comment, commentable: proposal) + + login_as(user) + visit proposal_path(proposal) + + within "#comment_#{comment.id}" do + page.find("#flag-expand-comment-#{comment.id}").click + page.find("#flag-comment-#{comment.id}").click + + expect(page).to have_css("#unflag-expand-comment-#{comment.id}") + end + + expect(Flag.flagged?(user, comment)).to be + end + + scenario "Undoing flagging as inappropriate", :js do + comment = create(:comment, commentable: proposal) + Flag.flag(user, comment) + + login_as(user) + visit proposal_path(proposal) + + within "#comment_#{comment.id}" do + page.find("#unflag-expand-comment-#{comment.id}").click + page.find("#unflag-comment-#{comment.id}").click + + expect(page).to have_css("#flag-expand-comment-#{comment.id}") + end + + expect(Flag.flagged?(user, comment)).to_not be + end + + scenario "Flagging turbolinks sanity check", :js do + proposal = create(:proposal, title: "Should we change the world?") + comment = create(:comment, commentable: proposal) + + login_as(user) + visit proposals_path + click_link "Should we change the world?" + + within "#comment_#{comment.id}" do + page.find("#flag-expand-comment-#{comment.id}").click + expect(page).to have_selector("#flag-comment-#{comment.id}") + end + end + + feature "Moderators" do + scenario "can create comment as a moderator", :js do + moderator = create(:moderator) + + login_as(moderator.user) + visit proposal_path(proposal) + + fill_in "comment-body-proposal_#{proposal.id}", with: "I am moderating!" + check "comment-as-moderator-proposal_#{proposal.id}" + click_button "Publish comment" + + within "#comments" do + expect(page).to have_content "I am moderating!" + expect(page).to have_content "Moderator ##{moderator.id}" + expect(page).to have_css "p.is-moderator" + expect(page).to have_css "img.moderator-avatar" + end + end + + scenario "can create reply as a moderator", :js do + citizen = create(:user, username: "Ana") + manuela = create(:user, username: "Manuela") + moderator = create(:moderator, user: manuela) + comment = create(:comment, commentable: proposal, user: citizen) + + login_as(manuela) + visit proposal_path(proposal) + + click_link "Reply" + + within "#js-comment-form-comment_#{comment.id}" do + fill_in "comment-body-comment_#{comment.id}", with: "I am moderating!" + check "comment-as-moderator-comment_#{comment.id}" + click_button 'Publish reply' + end + + within "#comment_#{comment.id}" do + expect(page).to have_content "I am moderating!" + expect(page).to have_content "Moderator ##{moderator.id}" + expect(page).to have_css "p.is-moderator" + expect(page).to have_css "img.moderator-avatar" + end + + expect(page).to_not have_selector("#js-comment-form-comment_#{comment.id}", visible: true) + end + + scenario "can not comment as an administrator" do + moderator = create(:moderator) + + login_as(moderator.user) + visit proposal_path(proposal) + + expect(page).to_not have_content "Comment as administrator" + end + end + + feature "Administrators" do + scenario "can create comment as an administrator", :js do + admin = create(:administrator) + + login_as(admin.user) + visit proposal_path(proposal) + + fill_in "comment-body-proposal_#{proposal.id}", with: "I am your Admin!" + check "comment-as-administrator-proposal_#{proposal.id}" + click_button "Publish comment" + + within "#comments" do + expect(page).to have_content "I am your Admin!" + expect(page).to have_content "Administrator ##{admin.id}" + expect(page).to have_css "p.is-admin" + expect(page).to have_css "img.admin-avatar" + end + end + + scenario "can create reply as an administrator", :js do + citizen = create(:user, username: "Ana") + manuela = create(:user, username: "Manuela") + admin = create(:administrator, user: manuela) + comment = create(:comment, commentable: proposal, user: citizen) + + login_as(manuela) + visit proposal_path(proposal) + + click_link "Reply" + + within "#js-comment-form-comment_#{comment.id}" do + fill_in "comment-body-comment_#{comment.id}", with: "Top of the world!" + check "comment-as-administrator-comment_#{comment.id}" + click_button 'Publish reply' + end + + within "#comment_#{comment.id}" do + expect(page).to have_content "Top of the world!" + expect(page).to have_content "Administrator ##{admin.id}" + expect(page).to have_css "p.is-admin" + expect(page).to have_css "img.admin-avatar" + end + + expect(page).to_not have_selector("#js-comment-form-comment_#{comment.id}", visible: true) + end + + scenario "can not comment as a moderator" do + admin = create(:administrator) + + login_as(admin.user) + visit proposal_path(proposal) + + expect(page).to_not have_content "Comment as moderator" + end + end + + feature 'Voting comments' do + + background do + @manuela = create(:user, verified_at: Time.now) + @pablo = create(:user) + @proposal = create(:proposal) + @comment = create(:comment, commentable: @proposal) + + login_as(@manuela) + end + + scenario 'Show' do + create(:vote, voter: @manuela, votable: @comment, vote_flag: true) + create(:vote, voter: @pablo, votable: @comment, vote_flag: false) + + visit proposal_path(@proposal) + + within("#comment_#{@comment.id}_votes") do + within(".in_favor") do + expect(page).to have_content "1" + end + + within(".against") do + expect(page).to have_content "1" + end + + expect(page).to have_content "2 votes" + end + end + + scenario 'Create', :js do + visit proposal_path(@proposal) + + within("#comment_#{@comment.id}_votes") do + find(".in_favor a").click + + within(".in_favor") do + expect(page).to have_content "1" + end + + within(".against") do + expect(page).to have_content "0" + end + + expect(page).to have_content "1 vote" + end + end + + scenario 'Update', :js do + visit proposal_path(@proposal) + + within("#comment_#{@comment.id}_votes") do + find('.in_favor a').click + find('.against a').click + + within('.in_favor') do + expect(page).to have_content "0" + end + + within('.against') do + expect(page).to have_content "1" + end + + expect(page).to have_content "1 vote" + end + end + + scenario 'Trying to vote multiple times', :js do + visit proposal_path(@proposal) + + within("#comment_#{@comment.id}_votes") do + find('.in_favor a').click + find('.in_favor a').click + + within('.in_favor') do + expect(page).to have_content "1" + end + + within('.against') do + expect(page).to have_content "0" + end + + expect(page).to have_content "1 vote" + end + end + end + +end diff --git a/spec/features/debates_spec.rb b/spec/features/debates_spec.rb index 84d7413e0..934a602c9 100644 --- a/spec/features/debates_spec.rb +++ b/spec/features/debates_spec.rb @@ -55,34 +55,20 @@ feature 'Debates' do login_as(author) visit new_debate_path - fill_in 'debate_title', with: 'Acabar con los desahucios' - fill_in 'debate_description', with: 'Esto es un tema muy importante porque...' + fill_in 'debate_title', with: 'End evictions' + fill_in 'debate_description', with: 'This is very important because...' fill_in 'debate_captcha', with: correct_captcha_text check 'debate_terms_of_service' click_button 'Start a debate' expect(page).to have_content 'Debate was successfully created.' - expect(page).to have_content 'Acabar con los desahucios' - expect(page).to have_content 'Esto es un tema muy importante porque...' + expect(page).to have_content 'End evictions' + expect(page).to have_content 'This is very important because...' expect(page).to have_content author.name expect(page).to have_content I18n.l(Debate.last.created_at.to_date) end - scenario 'CKEditor is present before & after turbolinks update page', :js do - author = create(:user) - login_as(author) - - visit new_debate_path - - expect(page).to have_css "#cke_debate_description" - - click_link 'Debates' - click_link 'Start a debate' - - expect(page).to have_css "#cke_debate_description" - end - scenario 'Captcha is required for debate creation' do login_as(create(:user)) @@ -152,6 +138,48 @@ feature 'Debates' do expect(page.html).to_not include '<p>This is' end + scenario 'Autolinking is applied to description' do + author = create(:user) + login_as(author) + + visit new_debate_path + fill_in 'debate_title', with: 'Testing auto link' + fill_in 'debate_description', with: 'This is a link www.example.org
' + fill_in 'debate_captcha', with: correct_captcha_text + check 'debate_terms_of_service' + + click_button 'Start a debate' + + expect(page).to have_content 'Debate was successfully created.' + expect(page).to have_content 'Testing auto link' + expect(page).to have_link('www.example.org', href: 'http://www.example.org') + end + + scenario 'JS injection is prevented but autolinking is respected' do + author = create(:user) + login_as(author) + + visit new_debate_path + fill_in 'debate_title', with: 'Testing auto link' + fill_in 'debate_description', with: " click me http://example.org" + fill_in 'debate_captcha', with: correct_captcha_text + check 'debate_terms_of_service' + + click_button 'Start a debate' + + expect(page).to have_content 'Debate was successfully created.' + expect(page).to have_content 'Testing auto link' + expect(page).to have_link('http://example.org', href: 'http://example.org') + expect(page).not_to have_link('click me') + expect(page.html).to_not include "" + + click_link 'Edit' + + expect(current_path).to eq edit_debate_path(Debate.last) + expect(page).not_to have_link('click me') + expect(page.html).to_not include "" + end + context 'Tagging debates' do let(:author) { create(:user) } @@ -209,7 +237,8 @@ feature 'Debates' do login_as(create(:user)) visit edit_debate_path(debate) - expect(current_path).to eq(root_path) + expect(current_path).not_to eq(edit_debate_path(debate)) + expect(current_path).to eq(proposals_path) expect(page).to have_content 'not authorized' end @@ -221,7 +250,8 @@ feature 'Debates' do visit edit_debate_path(debate) - expect(current_path).to eq(root_path) + expect(current_path).not_to eq(edit_debate_path(debate)) + expect(current_path).to eq(proposals_path) expect(page).to have_content 'not authorized' end @@ -378,7 +408,9 @@ feature 'Debates' do visit debates_path select 'most active', from: 'order-selector' - within '#debates.js-order-hot-score' do + expect(page).to have_selector('.js-order-selector[data-order="hot_score"]') + + within '#debates' do expect('Best').to appear_before('Medium') expect('Medium').to appear_before('Worst') end @@ -395,7 +427,9 @@ feature 'Debates' do visit debates_path select 'most commented', from: 'order-selector' - within '#debates.js-order-most-commented' do + expect(page).to have_selector('.js-order-selector[data-order="most_commented"]') + + within '#debates' do expect('Best').to appear_before('Medium') expect('Medium').to appear_before('Worst') end @@ -412,7 +446,9 @@ feature 'Debates' do visit debates_path select 'newest', from: 'order-selector' - within '#debates.js-order-created-at' do + expect(page).to have_selector('.js-order-selector[data-order="created_at"]') + + within '#debates' do expect('Best').to appear_before('Medium') expect('Medium').to appear_before('Worst') end @@ -426,13 +462,15 @@ feature 'Debates' do visit debates_path select 'random', from: 'order-selector' - debates_first_time = find("#debates.js-order-random").text + expect(page).to have_selector('.js-order-selector[data-order="random"]') + debates_first_time = find("#debates").text select 'most commented', from: 'order-selector' - expect(page).to have_selector('#debates.js-order-most-commented') + expect(page).to have_selector('.js-order-selector[data-order="most_commented"]') select 'random', from: 'order-selector' - debates_second_time = find("#debates.js-order-random").text + expect(page).to have_selector('.js-order-selector[data-order="random"]') + debates_second_time = find("#debates").text expect(debates_first_time).to_not eq(debates_second_time) expect(current_url).to include('page=1') diff --git a/spec/features/emails_spec.rb b/spec/features/emails_spec.rb index b6a9d1db5..1ce4b529f 100644 --- a/spec/features/emails_spec.rb +++ b/spec/features/emails_spec.rb @@ -26,7 +26,7 @@ feature 'Emails' do context 'Debate comments' do scenario "Send email on debate comment", :js do - user = create(:user, email_on_debate_comment: true) + user = create(:user, email_on_comment: true) debate = create(:debate, author: user) comment_on(debate) @@ -37,7 +37,7 @@ feature 'Emails' do end scenario 'Do not send email about own debate comments', :js do - user = create(:user, email_on_debate_comment: true) + user = create(:user, email_on_comment: true) debate = create(:debate, author: user) comment_on(debate, user) @@ -45,7 +45,7 @@ feature 'Emails' do end scenario 'Do not send email about debate comment unless set in preferences', :js do - user = create(:user, email_on_debate_comment: false) + user = create(:user, email_on_comment: false) debate = create(:debate, author: user) comment_on(debate) diff --git a/spec/features/highlights_spec.rb b/spec/features/highlights_spec.rb new file mode 100644 index 000000000..33ade0063 --- /dev/null +++ b/spec/features/highlights_spec.rb @@ -0,0 +1,34 @@ +require 'rails_helper' + +feature "Highlights" do + + scenario 'Debates and proposals order by hot_score' do + create(:debate, title: 'best debate 100').update_column(:hot_score, 100) + create(:debate, title: 'worst debate 50').update_column(:hot_score, 50) + create(:debate, title: 'medium debate 70').update_column(:hot_score, 70) + + create(:proposal, title: 'best proposal 90').update_column(:hot_score, 90) + create(:proposal, title: 'worst proposal 60').update_column(:hot_score, 60) + create(:proposal, title: 'medium proposal 80').update_column(:hot_score, 80) + + login_as(create(:user)) + + visit highlights_path + + expect('best debate 100').to appear_before('best proposal 90') + expect('best proposal 90').to appear_before('medium proposal 80') + expect('medium proposal 80').to appear_before('medium debate 70') + expect('medium debate 70').to appear_before('worst proposal 60') + expect('worst proposal 60').to appear_before('worst debate 50') + end + + scenario 'create debate and create proposal links' do + login_as(create(:user)) + + visit highlights_path + + expect(page).to have_link("Start a proposal") + expect(page).to have_link("Start a debate") + end + +end \ No newline at end of file diff --git a/spec/features/home_spec.rb b/spec/features/home_spec.rb index ff87a294b..2c99a7083 100644 --- a/spec/features/home_spec.rb +++ b/spec/features/home_spec.rb @@ -2,30 +2,21 @@ require 'rails_helper' feature "Home" do - scenario 'featured debates' do - featured_debates = [create(:debate), create(:debate), create(:debate)] + feature "For not logged users" do + scenario 'Welcome message' do + visit root_path - visit root_path - - expect(page).to have_selector('#featured-debates .debate-featured', count: 3) - featured_debates.each do |debate| - within('#featured-debates') do - expect(page).to have_content debate.title - expect(page).to have_css("a[href='#{debate_path(debate)}']", text: debate.description) - end + expect(page).to have_content "we open this digital Puerta del Sol" end - end - scenario "Order by confidence score" do - create(:debate, confidence_score: 100, title: 'best') - create(:debate, confidence_score: -20, title: 'worst') - create(:debate, confidence_score: 50, title: 'medium') + feature "For signed in users" do + scenario 'Redirect to proposals' do + login_as(create(:user)) + visit root_path - visit root_path - - expect('best').to appear_before('medium') - expect('medium').to appear_before('worst') + expect(current_path).to eq proposals_path + end end end diff --git a/spec/features/moderation/proposals_spec.rb b/spec/features/moderation/proposals_spec.rb new file mode 100644 index 000000000..50572c49e --- /dev/null +++ b/spec/features/moderation/proposals_spec.rb @@ -0,0 +1,177 @@ +require 'rails_helper' + +feature 'Moderate proposals' do + + scenario 'Hide', :js do + citizen = create(:user) + moderator = create(:moderator) + + proposal = create(:proposal) + + login_as(moderator.user) + visit proposal_path(proposal) + + within("#proposal_#{proposal.id}") do + click_link 'Hide' + end + + expect(page).to have_css("#proposal_#{proposal.id}.faded") + + login_as(citizen) + visit proposals_path + + expect(page).to have_css('.proposal', count: 0) + end + + scenario 'Can not hide own proposal' do + moderator = create(:moderator) + proposal = create(:proposal, author: moderator.user) + + login_as(moderator.user) + visit proposal_path(proposal) + + within("#proposal_#{proposal.id}") do + expect(page).to_not have_link('Hide') + expect(page).to_not have_link('Block author') + end + end + + feature '/moderation/ screen' do + + background do + moderator = create(:moderator) + login_as(moderator.user) + end + + feature 'moderate in bulk' do + feature "When a proposal has been selected for moderation" do + background do + @proposal = create(:proposal) + visit moderation_proposals_path + + within("#proposal_#{@proposal.id}") do + check "proposal_#{@proposal.id}_check" + end + + expect(page).to_not have_css("proposal_#{@proposal.id}") + end + + scenario 'Hide the proposal' do + click_on "Hide proposals" + expect(page).to_not have_css("proposal_#{@proposal.id}") + expect(@proposal.reload).to be_hidden + expect(@proposal.author).to_not be_hidden + end + + scenario 'Block the author' do + click_on "Block authors" + expect(page).to_not have_css("proposal_#{@proposal.id}") + expect(@proposal.reload).to be_hidden + expect(@proposal.author).to be_hidden + end + + scenario 'Ignore the proposal' do + click_on "Ignore flags" + expect(page).to_not have_css("proposal_#{@proposal.id}") + expect(@proposal.reload).to be_ignored_flag + expect(@proposal.reload).to_not be_hidden + expect(@proposal.author).to_not be_hidden + end + end + + scenario "select all/none", :js do + create_list(:proposal, 20) + + visit moderation_proposals_path + + within('.js-check') { click_on 'All' } + + all('input[type=checkbox]').each do |checkbox| + expect(checkbox).to be_checked + end + + within('.js-check') { click_on 'None' } + + all('input[type=checkbox]').each do |checkbox| + expect(checkbox).to_not be_checked + end + end + + scenario "remembering page, filter and order" do + create_list(:proposal, 55) + + visit moderation_proposals_path(filter: 'all', page: '2', order: 'created_at') + + click_on "Ignore flags" + + expect(page).to have_selector('.js-order-selector[data-order="created_at"]') + + expect(current_url).to include('filter=all') + expect(current_url).to include('page=2') + expect(current_url).to include('order=created_at') + end + end + + scenario "Current filter is properly highlighted" do + visit moderation_proposals_path + expect(page).to_not have_link('Pending') + expect(page).to have_link('All') + expect(page).to have_link('Ignored') + + visit moderation_proposals_path(filter: 'all') + within('.sub-nav') do + expect(page).to_not have_link('All') + expect(page).to have_link('Pending') + expect(page).to have_link('Ignored') + end + + visit moderation_proposals_path(filter: 'pending_flag_review') + within('.sub-nav') do + expect(page).to have_link('All') + expect(page).to_not have_link('Pending') + expect(page).to have_link('Ignored') + end + + visit moderation_proposals_path(filter: 'with_ignored_flag') + within('.sub-nav') do + expect(page).to have_link('All') + expect(page).to have_link('Pending') + expect(page).to_not have_link('Ignored') + end + end + + scenario "Filtering proposals" do + create(:proposal, title: "Pending proposal") + create(:proposal, :hidden, title: "Hidden proposal") + create(:proposal, :with_ignored_flag, title: "Ignored proposal") + + visit moderation_proposals_path(filter: 'all') + expect(page).to have_content('Pending proposal') + expect(page).to_not have_content('Hidden proposal') + expect(page).to have_content('Ignored proposal') + + visit moderation_proposals_path(filter: 'pending_flag_review') + expect(page).to have_content('Pending proposal') + expect(page).to_not have_content('Hidden proposal') + expect(page).to_not have_content('Ignored proposal') + + visit moderation_proposals_path(filter: 'with_ignored_flag') + expect(page).to_not have_content('Pending proposal') + expect(page).to_not have_content('Hidden proposal') + expect(page).to have_content('Ignored proposal') + end + + scenario "sorting proposals" do + create(:proposal, title: "Flagged proposal", created_at: Time.now - 1.day, flags_count: 5) + create(:proposal, title: "Newer proposal", created_at: Time.now) + + visit moderation_proposals_path(order: 'created_at') + + expect("Newer proposal").to appear_before("Flagged proposal") + + visit moderation_proposals_path(order: 'flags') + + expect("Flagged proposal").to appear_before("Newer proposal") + end + end +end diff --git a/spec/features/moderation_spec.rb b/spec/features/moderation_spec.rb index 78df5e56f..9cb2ff19d 100644 --- a/spec/features/moderation_spec.rb +++ b/spec/features/moderation_spec.rb @@ -10,7 +10,8 @@ feature 'Admin' do expect(page).to_not have_link("Moderation") visit moderation_root_path - expect(current_path).to eq(root_path) + expect(current_path).not_to eq(moderation_root_path) + expect(current_path).to eq(proposals_path) expect(page).to have_content "not authorized" end diff --git a/spec/features/proposals_spec.rb b/spec/features/proposals_spec.rb new file mode 100644 index 000000000..a968cc9ea --- /dev/null +++ b/spec/features/proposals_spec.rb @@ -0,0 +1,599 @@ +require 'rails_helper' + +feature 'Proposals' do + + scenario 'Index' do + proposal = [create(:proposal), create(:proposal), create(:proposal)] + + visit proposals_path + + expect(page).to have_selector('#proposals .proposal', count: 3) + proposal.each do |proposal| + within('#proposals') do + expect(page).to have_content proposal.title + expect(page).to have_css("a[href='#{proposal_path(proposal)}']", text: proposal.summary) + end + end + end + + scenario 'Paginated Index' do + per_page = Kaminari.config.default_per_page + (per_page + 2).times { create(:proposal) } + + visit proposals_path + + expect(page).to have_selector('#proposals .proposal', count: per_page) + + within("ul.pagination") do + expect(page).to have_content("1") + expect(page).to have_content("2") + expect(page).to_not have_content("3") + click_link "Next", exact: false + end + + expect(page).to have_selector('#proposals .proposal', count: 2) + end + + scenario 'Show' do + proposal = create(:proposal) + + visit proposal_path(proposal) + + expect(page).to have_content proposal.title + expect(page).to have_content proposal.code + expect(page).to have_content "Proposal question" + expect(page).to have_content "Proposal description" + expect(page).to have_content "http://external_documention.es" + expect(page).to have_content proposal.author.name + expect(page).to have_content I18n.l(proposal.created_at.to_date) + expect(page).to have_selector(avatar(proposal.author.name)) + + within('.social-share-button') do + expect(page.all('a').count).to be(3) # Twitter, Facebook, Google+ + end + end + + scenario 'Create' do + author = create(:user) + login_as(author) + + visit new_proposal_path + fill_in 'proposal_title', with: 'Help refugees' + fill_in 'proposal_question', with: '¿Would you like to give assistance to war refugees?' + fill_in 'proposal_summary', with: 'In summary, what we want is...' + fill_in 'proposal_description', with: 'This is very important because...' + fill_in 'proposal_external_url', with: 'http://rescue.org/refugees' + fill_in 'proposal_video_url', with: 'http://youtube.com' + fill_in 'proposal_responsible_name', with: 'Isabel Garcia' + fill_in 'proposal_captcha', with: correct_captcha_text + check 'proposal_terms_of_service' + + click_button 'Start a proposal' + + expect(page).to have_content 'Proposal was successfully created.' + expect(page).to have_content 'Help refugees' + expect(page).to have_content '¿Would you like to give assistance to war refugees?' + expect(page).to have_content 'In summary, what we want is...' + expect(page).to have_content 'This is very important because...' + expect(page).to have_content 'http://rescue.org/refugees' + expect(page).to have_content 'http://youtube.com' + expect(page).to have_content author.name + expect(page).to have_content I18n.l(Proposal.last.created_at.to_date) + end + + scenario 'Responsible name is stored for anonymous users' do + author = create(:user) + login_as(author) + + visit new_proposal_path + fill_in 'proposal_title', with: 'Help refugees' + fill_in 'proposal_question', with: '¿Would you like to give assistance to war refugees?' + fill_in 'proposal_summary', with: 'In summary, what we want is...' + fill_in 'proposal_description', with: 'This is very important because...' + fill_in 'proposal_external_url', with: 'http://rescue.org/refugees' + fill_in 'proposal_responsible_name', with: 'Isabel Garcia' + fill_in 'proposal_captcha', with: correct_captcha_text + fill_in 'proposal_responsible_name', with: 'Isabel Garcia' + check 'proposal_terms_of_service' + + click_button 'Start a proposal' + + expect(page).to have_content 'Proposal was successfully created.' + expect(Proposal.last.responsible_name).to eq('Isabel Garcia') + end + + scenario 'Responsible name field is not shown for verified users' do + author = create(:user, :level_two) + login_as(author) + + visit new_proposal_path + expect(page).to_not have_selector('#proposal_responsible_name') + + fill_in 'proposal_title', with: 'Help refugees' + fill_in 'proposal_question', with: '¿Would you like to give assistance to war refugees?' + fill_in 'proposal_summary', with: 'In summary, what we want is...' + fill_in 'proposal_description', with: 'This is very important because...' + fill_in 'proposal_external_url', with: 'http://rescue.org/refugees' + fill_in 'proposal_captcha', with: correct_captcha_text + check 'proposal_terms_of_service' + + click_button 'Start a proposal' + + expect(page).to have_content 'Proposal was successfully created.' + end + + scenario 'Captcha is required for proposal creation' do + login_as(create(:user)) + + visit new_proposal_path + fill_in 'proposal_title', with: "Great title" + fill_in 'proposal_question', with: '¿Would you like to give assistance to war refugees?' + fill_in 'proposal_summary', with: 'In summary, what we want is...' + fill_in 'proposal_description', with: 'Very important issue...' + fill_in 'proposal_external_url', with: 'http://rescue.org/refugees' + fill_in 'proposal_responsible_name', with: 'Isabel Garcia' + fill_in 'proposal_captcha', with: "wrongText!" + check 'proposal_terms_of_service' + + click_button "Start a proposal" + + expect(page).to_not have_content "Proposal was successfully created." + expect(page).to have_content "1 error" + + fill_in 'proposal_captcha', with: correct_captcha_text + click_button "Start a proposal" + + expect(page).to have_content "Proposal was successfully created." + end + + scenario 'Failed creation goes back to new showing featured tags' do + featured_tag = create(:tag, :featured) + tag = create(:tag) + login_as(create(:user)) + + visit new_proposal_path + fill_in 'proposal_title', with: "" + fill_in 'proposal_question', with: '¿Would you like to give assistance to war refugees?' + fill_in 'proposal_summary', with: 'In summary, what we want is...' + fill_in 'proposal_description', with: 'Very important issue...' + fill_in 'proposal_external_url', with: 'http://rescue.org/refugees' + fill_in 'proposal_responsible_name', with: 'Isabel Garcia' + fill_in 'proposal_captcha', with: correct_captcha_text + check 'proposal_terms_of_service' + + click_button "Start a proposal" + + expect(page).to_not have_content "Proposal was successfully created." + expect(page).to have_content "error" + within(".tags") do + expect(page).to have_content featured_tag.name + expect(page).to_not have_content tag.name + end + end + + scenario 'Errors on create' do + author = create(:user) + login_as(author) + + visit new_proposal_path + click_button 'Start a proposal' + expect(page).to have_content error_message + end + + scenario 'JS injection is prevented but safe html is respected' do + author = create(:user) + login_as(author) + + visit new_proposal_path + fill_in 'proposal_title', with: 'Testing an attack' + fill_in 'proposal_question', with: '¿Would you like to give assistance to war refugees?' + fill_in 'proposal_summary', with: 'In summary, what we want is...' + fill_in 'proposal_description', with: 'This is
' + fill_in 'proposal_external_url', with: 'http://rescue.org/refugees' + fill_in 'proposal_responsible_name', with: 'Isabel Garcia' + fill_in 'proposal_captcha', with: correct_captcha_text + check 'proposal_terms_of_service' + + click_button 'Start a proposal' + + expect(page).to have_content 'Proposal was successfully created.' + expect(page).to have_content 'Testing an attack' + expect(page.html).to include 'This is alert("an attack");
' + expect(page.html).to_not include '' + expect(page.html).to_not include '<p>This is' + end + + scenario 'Autolinking is applied to description' do + author = create(:user) + login_as(author) + + visit new_proposal_path + fill_in 'proposal_title', with: 'Testing auto link' + fill_in 'proposal_question', with: 'Should I stay or should I go?' + fill_in 'proposal_summary', with: 'In summary, what we want is...' + fill_in 'proposal_description', with: 'This is a link www.example.org
' + fill_in 'proposal_responsible_name', with: 'Isabel Garcia' + fill_in 'proposal_captcha', with: correct_captcha_text + check 'proposal_terms_of_service' + + click_button 'Start a proposal' + + expect(page).to have_content 'Proposal was successfully created.' + expect(page).to have_content 'Testing auto link' + expect(page).to have_link('www.example.org', href: 'http://www.example.org') + end + + scenario 'JS injection is prevented but autolinking is respected' do + author = create(:user) + login_as(author) + + visit new_proposal_path + fill_in 'proposal_title', with: 'Testing auto link' + fill_in 'proposal_question', with: 'Should I stay or should I go?' + fill_in 'proposal_summary', with: 'In summary, what we want is...' + fill_in 'proposal_description', with: " click me http://example.org" + fill_in 'proposal_responsible_name', with: 'Isabel Garcia' + fill_in 'proposal_captcha', with: correct_captcha_text + check 'proposal_terms_of_service' + + click_button 'Start a proposal' + + expect(page).to have_content 'Proposal was successfully created.' + expect(page).to have_content 'Testing auto link' + expect(page).to have_link('http://example.org', href: 'http://example.org') + expect(page).not_to have_link('click me') + expect(page.html).to_not include "" + + click_link 'Edit' + + expect(current_path).to eq edit_proposal_path(Proposal.last) + expect(page).not_to have_link('click me') + expect(page.html).to_not include "" + end + + context 'Tagging proposals' do + let(:author) { create(:user) } + + background do + login_as(author) + end + + scenario 'using featured tags', :js do + ['Medio Ambiente', 'Ciencia'].each do |tag_name| + create(:tag, :featured, name: tag_name) + end + + visit new_proposal_path + + fill_in 'proposal_title', with: 'A test with enough characters' + fill_in 'proposal_question', with: '¿Would you like to give assistance to war refugees?' + fill_in 'proposal_summary', with: 'In summary, what we want is...' + fill_in_ckeditor 'proposal_description', with: 'A description with enough characters' + fill_in 'proposal_external_url', with: 'http://rescue.org/refugees' + fill_in 'proposal_responsible_name', with: 'Isabel Garcia' + fill_in 'proposal_captcha', with: correct_captcha_text + check 'proposal_terms_of_service' + + ['Medio Ambiente', 'Ciencia'].each do |tag_name| + find('.js-add-tag-link', text: tag_name).click + end + + click_button 'Start a proposal' + + expect(page).to have_content 'Proposal was successfully created.' + ['Medio Ambiente', 'Ciencia'].each do |tag_name| + expect(page).to have_content tag_name + end + end + + scenario 'using dangerous strings' do + visit new_proposal_path + + fill_in 'proposal_title', with: 'A test of dangerous strings' + fill_in 'proposal_question', with: '¿Would you like to give assistance to war refugees?' + fill_in 'proposal_summary', with: 'In summary, what we want is...' + fill_in 'proposal_description', with: 'A description suitable for this test' + fill_in 'proposal_external_url', with: 'http://rescue.org/refugees' + fill_in 'proposal_responsible_name', with: 'Isabel Garcia' + fill_in 'proposal_captcha', with: correct_captcha_text + check 'proposal_terms_of_service' + + fill_in 'proposal_tag_list', with: 'user_id=1, &a=3, ' + + click_button 'Start a proposal' + + expect(page).to have_content 'Proposal was successfully created.' + expect(page).to have_content 'user_id1' + expect(page).to have_content 'a3' + expect(page).to have_content 'scriptalert("hey");script' + expect(page.html).to_not include 'user_id=1, &a=3, ' + end + end + + scenario 'Update should not be posible if logged user is not the author' do + proposal = create(:proposal) + expect(proposal).to be_editable + login_as(create(:user)) + + visit edit_proposal_path(proposal) + expect(current_path).not_to eq(edit_proposal_path(proposal)) + expect(current_path).to eq(proposals_path) + expect(page).to have_content 'not authorized' + end + + scenario 'Update should not be posible if proposal is not editable' do + proposal = create(:proposal) + Setting.find_by(key: "max_votes_for_proposal_edit").update(value: 10) + 11.times { create(:vote, votable: proposal) } + + expect(proposal).to_not be_editable + + login_as(proposal.author) + visit edit_proposal_path(proposal) + + expect(current_path).not_to eq(edit_proposal_path(proposal)) + expect(current_path).to eq(proposals_path) + expect(page).to have_content 'not authorized' + end + + scenario 'Update should be posible for the author of an editable proposal' do + proposal = create(:proposal) + login_as(proposal.author) + + visit edit_proposal_path(proposal) + expect(current_path).to eq(edit_proposal_path(proposal)) + + fill_in 'proposal_title', with: "End child poverty" + fill_in 'proposal_question', with: '¿Would you like to give assistance to war refugees?' + fill_in 'proposal_summary', with: 'Basically...' + fill_in 'proposal_description', with: "Let's do something to end child poverty" + fill_in 'proposal_external_url', with: 'http://rescue.org/refugees' + fill_in 'proposal_responsible_name', with: 'Isabel Garcia' + fill_in 'proposal_captcha', with: correct_captcha_text + + click_button "Save changes" + + expect(page).to have_content "Proposal was successfully updated." + expect(page).to have_content "Basically..." + expect(page).to have_content "End child poverty" + expect(page).to have_content "Let's do something to end child poverty" + end + + scenario 'Errors on update' do + proposal = create(:proposal) + login_as(proposal.author) + + visit edit_proposal_path(proposal) + fill_in 'proposal_title', with: "" + click_button "Save changes" + + expect(page).to have_content error_message + end + + scenario 'Captcha is required to update a proposal' do + proposal = create(:proposal) + login_as(proposal.author) + + visit edit_proposal_path(proposal) + expect(current_path).to eq(edit_proposal_path(proposal)) + + fill_in 'proposal_title', with: "New cool title" + fill_in 'proposal_captcha', with: "wrong!" + click_button "Save changes" + + expect(page).to_not have_content "Proposal was successfully updated." + expect(page).to have_content "error" + + fill_in 'proposal_captcha', with: correct_captcha_text + click_button "Save changes" + + expect(page).to have_content "Proposal was successfully updated." + end + + scenario 'Failed update goes back to edit showing featured tags' do + proposal = create(:proposal) + featured_tag = create(:tag, :featured) + tag = create(:tag) + login_as(proposal.author) + + visit edit_proposal_path(proposal) + expect(current_path).to eq(edit_proposal_path(proposal)) + + fill_in 'proposal_title', with: "" + fill_in 'proposal_captcha', with: correct_captcha_text + click_button "Save changes" + + expect(page).to_not have_content "Proposal was successfully updated." + expect(page).to have_content "error" + within(".tags") do + expect(page).to have_content featured_tag.name + expect(page).to_not have_content tag.name + end + end + + describe 'Limiting tags shown' do + scenario 'Index page shows up to 5 tags per proposal' do + tag_list = ["Hacienda", "Economía", "Medio Ambiente", "Corrupción", "Fiestas populares", "Prensa", "Huelgas"] + create :proposal, tag_list: tag_list + + visit proposals_path + + within('.proposal .tags') do + expect(page).to have_content '2+' + end + end + + scenario 'Index page shows 3 tags with no plus link' do + tag_list = ["Medio Ambiente", "Corrupción", "Fiestas populares"] + create :proposal, tag_list: tag_list + + visit proposals_path + + within('.proposal .tags') do + tag_list.each do |tag| + expect(page).to have_content tag + end + expect(page).not_to have_content '+' + end + end + end + + feature 'Proposal index order filters' do + + scenario 'Default order is confidence_score', :js do + create(:proposal, title: 'Best proposal').update_column(:confidence_score, 10) + create(:proposal, title: 'Worst proposal').update_column(:confidence_score, 2) + create(:proposal, title: 'Medium proposal').update_column(:confidence_score, 5) + + visit proposals_path + + expect('Best proposal').to appear_before('Medium proposal') + expect('Medium proposal').to appear_before('Worst proposal') + end + + scenario 'Proposals are ordered by hot_score', :js do + create(:proposal, title: 'Best proposal').update_column(:hot_score, 10) + create(:proposal, title: 'Worst proposal').update_column(:hot_score, 2) + create(:proposal, title: 'Medium proposal').update_column(:hot_score, 5) + + visit proposals_path + select 'most active', from: 'order-selector' + + expect(page).to have_selector('.js-order-selector[data-order="hot_score"]') + + within '#proposals' do + expect('Best proposal').to appear_before('Medium proposal') + expect('Medium proposal').to appear_before('Worst proposal') + end + + expect(current_url).to include('order=hot_score') + expect(current_url).to include('page=1') + end + + scenario 'Proposals are ordered by most commented', :js do + create(:proposal, title: 'Best proposal', comments_count: 10) + create(:proposal, title: 'Medium proposal', comments_count: 5) + create(:proposal, title: 'Worst proposal', comments_count: 2) + + visit proposals_path + select 'most commented', from: 'order-selector' + + expect(page).to have_selector('.js-order-selector[data-order="most_commented"]') + + within '#proposals' do + expect('Best proposal').to appear_before('Medium proposal') + expect('Medium proposal').to appear_before('Worst proposal') + end + + expect(current_url).to include('order=most_commented') + expect(current_url).to include('page=1') + end + + scenario 'Proposals are ordered by newest', :js do + create(:proposal, title: 'Best proposal', created_at: Time.now) + create(:proposal, title: 'Medium proposal', created_at: Time.now - 1.hour) + create(:proposal, title: 'Worst proposal', created_at: Time.now - 1.day) + + visit proposals_path + select 'newest', from: 'order-selector' + + expect(page).to have_selector('.js-order-selector[data-order="created_at"]') + + within '#proposals' do + expect('Best proposal').to appear_before('Medium proposal') + expect('Medium proposal').to appear_before('Worst proposal') + end + + expect(current_url).to include('order=created_at') + expect(current_url).to include('page=1') + end + + scenario 'Proposals are ordered randomly', :js do + create_list(:proposal, 12) + visit proposals_path + + select 'random', from: 'order-selector' + expect(page).to have_selector('.js-order-selector[data-order="random"]') + proposals_first_time = find("#proposals").text + + select 'most commented', from: 'order-selector' + expect(page).to have_selector('.js-order-selector[data-order="most_commented"]') + + select 'random', from: 'order-selector' + expect(page).to have_selector('.js-order-selector[data-order="random"]') + proposals_second_time = find("#proposals").text + + expect(proposals_first_time).to_not eq(proposals_second_time) + expect(current_url).to include('page=1') + end + end + + scenario 'proposal index search' do + proposal1 = create(:proposal, title: "Show me what you got") + proposal2 = create(:proposal, title: "Get Schwifty") + proposal3 = create(:proposal) + proposal4 = create(:proposal, description: "Schwifty in here") + proposal5 = create(:proposal, question: "Schwifty in here") + + visit proposals_path + fill_in "search", with: "Schwifty" + click_button "Search" + + expect(current_path).to eq(proposals_path) + + within("#proposals") do + expect(page).to have_css('.proposal', count: 3) + expect(page).to have_content(proposal2.title) + expect(page).to have_content(proposal4.title) + expect(page).to have_content(proposal5.title) + expect(page).to_not have_content(proposal1.title) + expect(page).to_not have_content(proposal3.title) + end + end + + scenario 'Conflictive' do + good_proposal = create(:proposal) + conflictive_proposal = create(:proposal, :conflictive) + + visit proposal_path(conflictive_proposal) + expect(page).to have_content "This proposal has been flag as innapropiate for some users." + + visit proposal_path(good_proposal) + expect(page).to_not have_content "This proposal has been flag as innapropiate for some users." + end + + scenario "Flagging", :js do + user = create(:user) + proposal = create(:proposal) + + login_as(user) + visit proposal_path(proposal) + + within "#proposal_#{proposal.id}" do + page.find("#flag-expand-proposal-#{proposal.id}").click + page.find("#flag-proposal-#{proposal.id}").click + + expect(page).to have_css("#unflag-expand-proposal-#{proposal.id}") + end + + expect(Flag.flagged?(user, proposal)).to be + end + + scenario "Unflagging", :js do + user = create(:user) + proposal = create(:proposal) + Flag.flag(user, proposal) + + login_as(user) + visit proposal_path(proposal) + + within "#proposal_#{proposal.id}" do + page.find("#unflag-expand-proposal-#{proposal.id}").click + page.find("#unflag-proposal-#{proposal.id}").click + + expect(page).to have_css("#flag-expand-proposal-#{proposal.id}") + end + + expect(Flag.flagged?(user, proposal)).to_not be + end +end diff --git a/spec/features/votes_spec.rb b/spec/features/votes_spec.rb index a3cf2e43a..81995397d 100644 --- a/spec/features/votes_spec.rb +++ b/spec/features/votes_spec.rb @@ -10,53 +10,6 @@ feature 'Votes' do end feature 'Debates' do - scenario "Home shows user votes on featured debates" do - debate1 = create(:debate) - debate2 = create(:debate) - debate3 = create(:debate) - create(:vote, voter: @manuela, votable: debate1, vote_flag: true) - create(:vote, voter: @manuela, votable: debate3, vote_flag: false) - - visit root_path - - within("#featured-debates") do - within("#debate_#{debate1.id}_votes") do - within(".in-favor") do - expect(page).to have_css("a.voted") - expect(page).to_not have_css("a.no-voted") - end - - within(".against") do - expect(page).to have_css("a.no-voted") - expect(page).to_not have_css("a.voted") - end - end - - within("#debate_#{debate2.id}_votes") do - within(".in-favor") do - expect(page).to_not have_css("a.voted") - expect(page).to_not have_css("a.no-voted") - end - - within(".against") do - expect(page).to_not have_css("a.no-voted") - expect(page).to_not have_css("a.voted") - end - end - - within("#debate_#{debate3.id}_votes") do - within(".in-favor") do - expect(page).to have_css("a.no-voted") - expect(page).to_not have_css("a.voted") - end - - within(".against") do - expect(page).to have_css("a.voted") - expect(page).to_not have_css("a.no-voted") - end - end - end - end scenario "Index shows user votes on debates" do debate1 = create(:debate) @@ -202,27 +155,6 @@ feature 'Votes' do expect(page).to have_content "1 vote" end - scenario 'Create in featured', :js do - visit root_path - - within("#featured-debates") do - find('.in-favor a').click - - within('.in-favor') do - expect(page).to have_content "100%" - expect(page).to have_css("a.voted") - end - - within('.against') do - expect(page).to have_content "0%" - expect(page).to have_css("a.no-voted") - end - - expect(page).to have_content "1 vote" - end - expect(current_path).to eq(root_path) - end - scenario 'Create in index', :js do visit debates_path @@ -245,87 +177,167 @@ feature 'Votes' do expect(current_path).to eq(debates_path) end end + + scenario 'Not logged user trying to vote', :js do + debate = create(:debate) + + visit "/" + click_link "Logout" + + visit debates_path + within("#debate_#{debate.id}") do + find("div.votes").hover + expect_message_you_need_to_sign_in + end + + visit debate_path(debate) + within("#debate_#{debate.id}") do + find("div.votes").hover + expect_message_you_need_to_sign_in + end + end + + scenario 'Anonymous user trying to vote', :js do + user = create(:user) + debate = create(:debate) + + Setting.find_by(key: "max_ratio_anon_votes_on_debates").update(value: 50) + debate.update(cached_anonymous_votes_total: 520, cached_votes_total: 1000) + + login_as(user) + + visit debates_path + within("#debate_#{debate.id}") do + find("div.votes").hover + expect_message_to_many_anonymous_votes + end + + visit debate_path(debate) + within("#debate_#{debate.id}") do + find("div.votes").hover + expect_message_to_many_anonymous_votes + end + end end - feature 'Comments' do - background do - @debate = create(:debate) - @comment = create(:comment, commentable: @debate) - end + feature 'Proposals' do - scenario 'Show' do - create(:vote, voter: @manuela, votable: @comment, vote_flag: true) - create(:vote, voter: @pablo, votable: @comment, vote_flag: false) + scenario "Index shows user votes on proposals" do + proposal1 = create(:proposal) + proposal2 = create(:proposal) + proposal3 = create(:proposal) + create(:vote, voter: @manuela, votable: proposal1, vote_flag: true) - visit debate_path(@debate) + visit proposals_path - within("#comment_#{@comment.id}_votes") do - within(".in_favor") do - expect(page).to have_content "1" + within("#proposals") do + within("#proposal_#{proposal1.id}_votes") do + expect(page).to have_content "You already supported this proposal!" end - within(".against") do - expect(page).to have_content "1" + within("#proposal_#{proposal2.id}_votes") do + expect(page).to_not have_content "You already supported this proposal!" end - expect(page).to have_content "2 votes" + within("#proposal_#{proposal3.id}_votes") do + expect(page).to_not have_content "You already supported this proposal!" + end end end - scenario 'Create', :js do - visit debate_path(@debate) + feature 'Single proposal' do + background do + @proposal = create(:proposal) + end - within("#comment_#{@comment.id}_votes") do - find(".in_favor a").click + scenario 'Show no votes' do + visit proposal_path(@proposal) + expect(page).to have_content "No supports" + end - within(".in_favor") do - expect(page).to have_content "1" + scenario 'Trying to vote multiple times', :js do + visit proposal_path(@proposal) + + within('.supports') do + find('.in-favor a').click + find('.in-favor a').click + + expect(page).to have_content "1 support" end + end - within(".against") do - expect(page).to have_content "0" + scenario 'Show' do + create(:vote, voter: @manuela, votable: @proposal, vote_flag: true) + create(:vote, voter: @pablo, votable: @proposal, vote_flag: true) + + visit proposal_path(@proposal) + + within('.supports') do + expect(page).to have_content "2 supports" end + end - expect(page).to have_content "1 vote" + scenario 'Create from proposal show', :js do + visit proposal_path(@proposal) + + within('.supports') do + find('.in-favor a').click + + expect(page).to have_content "1 support" + expect(page).to have_content "You already supported this proposal!" + end + end + + scenario 'Create in index', :js do + visit proposals_path + + within("#proposals") do + find('.in-favor a').click + + expect(page).to have_content "1 support" + expect(page).to have_content "You already supported this proposal!" + end + expect(URI.parse(current_url).path).to eq(proposals_path) end end + end - scenario 'Update', :js do - visit debate_path(@debate) + scenario 'Not logged user trying to vote', :js do + proposal = create(:proposal) - within("#comment_#{@comment.id}_votes") do - find('.in_favor a').click - find('.against a').click + visit "/" + click_link "Logout" + expect(page).to have_content "Signed out successfully." - within('.in_favor') do - expect(page).to have_content "0" - end - - within('.against') do - expect(page).to have_content "1" - end - - expect(page).to have_content "1 vote" - end + visit proposals_path + within("#proposal_#{proposal.id}") do + find("div.supports").hover + expect_message_you_need_to_sign_in end - scenario 'Trying to vote multiple times', :js do - visit debate_path(@debate) + visit proposal_path(proposal) + within("#proposal_#{proposal.id}") do + find("div.supports").hover + expect_message_you_need_to_sign_in + end + end - within("#comment_#{@comment.id}_votes") do - find('.in_favor a').click - find('.in_favor a').click + scenario "Anonymous user trying to vote", :js do + user = create(:user) + proposal = create(:proposal) - within('.in_favor') do - expect(page).to have_content "1" - end + login_as(user) + visit proposals_path - within('.against') do - expect(page).to have_content "0" - end + within("#proposal_#{proposal.id}") do + find("div.supports").hover + expect_message_only_verified_can_vote_proposals + end - expect(page).to have_content "1 vote" - end + visit proposal_path(proposal) + within("#proposal_#{proposal.id}") do + find("div.supports").hover + expect_message_only_verified_can_vote_proposals end end end diff --git a/spec/features/welcome_spec.rb b/spec/features/welcome_spec.rb index 2635ef0df..cef26e035 100644 --- a/spec/features/welcome_spec.rb +++ b/spec/features/welcome_spec.rb @@ -15,7 +15,7 @@ feature "Welcome screen" do login_through_form_as(user) - expect(current_path).to eq(root_path) + expect(current_path).to eq(proposals_path) end scenario 'is not shown to organizations' do @@ -23,7 +23,7 @@ feature "Welcome screen" do login_through_form_as(organization.user) - expect(current_path).to eq(root_path) + expect(current_path).to eq(proposals_path) end scenario 'it is not shown to level-2 users' do @@ -31,7 +31,7 @@ feature "Welcome screen" do login_through_form_as(user) - expect(current_path).to eq(root_path) + expect(current_path).to eq(proposals_path) end scenario 'it is not shown to level-3 users' do @@ -39,7 +39,7 @@ feature "Welcome screen" do login_through_form_as(user) - expect(current_path).to eq(root_path) + expect(current_path).to eq(proposals_path) end end diff --git a/spec/helpers/proposals_helper_spec.rb b/spec/helpers/proposals_helper_spec.rb new file mode 100644 index 000000000..983f43572 --- /dev/null +++ b/spec/helpers/proposals_helper_spec.rb @@ -0,0 +1,44 @@ +require 'rails_helper' + +describe ProposalsHelper do + + describe "#progress_bar_percentage" do + it "should be 0 if no votes" do + proposal = create(:proposal) + expect(progress_bar_percentage(proposal)).to eq 0 + end + + it "should be a between 1 and 100 if there are votes but less than needed" do + proposal = create(:proposal, cached_votes_up: Proposal.votes_needed_for_success/2) + expect(progress_bar_percentage(proposal)).to eq 50 + end + + it "should be 100 if there are more votes than needed" do + proposal = create(:proposal, cached_votes_up: Proposal.votes_needed_for_success*2) + expect(progress_bar_percentage(proposal)).to eq 100 + end + end + + describe "#supports_percentage" do + it "should be 0 if no votes" do + proposal = create(:proposal) + expect(supports_percentage(proposal)).to eq "0%" + end + + it "should be a between 0.1 from 1 to 0.1% of needed votes" do + proposal = create(:proposal, cached_votes_up: 1) + expect(supports_percentage(proposal)).to eq "0.1%" + end + + it "should be a between 1 and 100 if there are votes but less than needed" do + proposal = create(:proposal, cached_votes_up: Proposal.votes_needed_for_success/2) + expect(supports_percentage(proposal)).to eq "50%" + end + + it "should be 100 if there are more votes than needed" do + proposal = create(:proposal, cached_votes_up: Proposal.votes_needed_for_success*2) + expect(supports_percentage(proposal)).to eq "100%" + end + end + +end \ No newline at end of file diff --git a/spec/helpers/votes_helper_spec.rb b/spec/helpers/votes_helper_spec.rb new file mode 100644 index 000000000..96be3b3fe --- /dev/null +++ b/spec/helpers/votes_helper_spec.rb @@ -0,0 +1,21 @@ +require 'rails_helper' + +describe VotesHelper do + + describe "#voted_for?" do + it "should return true if voted for a proposal" do + proposal = create(:proposal) + votes = {proposal.id => true} + + expect(voted_for?(votes, proposal)).to eq(true) + end + + it "should return false if not voted for a proposals" do + proposal = create(:proposal) + votes = {proposal.id => nil} + + expect(voted_for?(votes, proposal)).to eq(nil) + end + end + +end \ No newline at end of file diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb index f106ef14e..a2e6212b5 100644 --- a/spec/models/ability_spec.rb +++ b/spec/models/ability_spec.rb @@ -3,8 +3,17 @@ require 'cancan/matchers' describe Ability do subject(:ability) { Ability.new(user) } - let(:debate) { Debate.new } + let(:debate) { create(:debate) } let(:comment) { create(:comment) } + let(:proposal) { create(:proposal) } + + let(:own_debate) { create(:debate, author: user) } + let(:own_comment) { create(:comment, author: user) } + let(:own_proposal) { create(:proposal, author: user) } + + let(:hidden_debate) { create(:debate, :hidden) } + let(:hidden_comment) { create(:comment, :hidden) } + let(:hidden_proposal) { create(:proposal, :hidden) } describe "Non-logged in user" do let(:user) { nil } @@ -13,6 +22,15 @@ describe Ability do it { should be_able_to(:show, debate) } it { should_not be_able_to(:edit, Debate) } it { should_not be_able_to(:vote, Debate) } + it { should_not be_able_to(:flag, Debate) } + it { should_not be_able_to(:unflag, Debate) } + + it { should be_able_to(:index, Proposal) } + it { should be_able_to(:show, proposal) } + it { should_not be_able_to(:edit, Proposal) } + it { should_not be_able_to(:vote, Proposal) } + it { should_not be_able_to(:flag, Proposal) } + it { should_not be_able_to(:unflag, Proposal) } end describe "Citizen" do @@ -28,8 +46,14 @@ describe Ability do it { should be_able_to(:create, Comment) } it { should be_able_to(:vote, Comment) } + it { should be_able_to(:index, Proposal) } + it { should be_able_to(:show, proposal) } + it { should_not be_able_to(:vote, Proposal) } + it { should_not be_able_to(:comment_as_administrator, debate) } it { should_not be_able_to(:comment_as_moderator, debate) } + it { should_not be_able_to(:comment_as_administrator, proposal) } + it { should_not be_able_to(:comment_as_moderator, proposal) } describe 'flagging content' do it { should be_able_to(:flag, debate) } @@ -38,18 +62,18 @@ describe Ability do it { should be_able_to(:flag, comment) } it { should be_able_to(:unflag, comment) } - describe "own comments" do - let(:own_comment) { create(:comment, author: user) } + it { should be_able_to(:flag, proposal) } + it { should be_able_to(:unflag, proposal) } + describe "own content" do it { should_not be_able_to(:flag, own_comment) } it { should_not be_able_to(:unflag, own_comment) } - end - - describe "own debates" do - let(:own_debate) { create(:debate, author: user) } it { should_not be_able_to(:flag, own_debate) } it { should_not be_able_to(:unflag, own_debate) } + + it { should_not be_able_to(:flag, own_proposal) } + it { should_not be_able_to(:unflag, own_proposal) } end end @@ -60,15 +84,34 @@ describe Ability do end describe "editing debates" do - let(:own_debate) { create(:debate, author: user) } let(:own_debate_non_editable) { create(:debate, author: user) } - before { allow(own_debate_non_editable).to receive(:editable?).and_return(false) } it { should be_able_to(:edit, own_debate) } it { should_not be_able_to(:edit, debate) } # Not his it { should_not be_able_to(:edit, own_debate_non_editable) } end + + describe "editing proposals" do + let(:own_proposal_non_editable) { create(:proposal, author: user) } + before { allow(own_proposal_non_editable).to receive(:editable?).and_return(false) } + + it { should be_able_to(:edit, own_proposal) } + it { should_not be_able_to(:edit, proposal) } # Not his + it { should_not be_able_to(:edit, own_proposal_non_editable) } + end + + describe "when level 2 verified" do + before{ user.update(residence_verified_at: Time.now, confirmed_phone: "1") } + + it { should be_able_to(:vote, Proposal) } + end + + describe "when level 3 verified" do + before{ user.update(verified_at: Time.now) } + + it { should be_able_to(:vote, Proposal) } + end end describe "Organization" do @@ -82,6 +125,10 @@ describe Ability do it { should be_able_to(:show, debate) } it { should_not be_able_to(:vote, debate) } + it { should be_able_to(:index, Proposal) } + it { should be_able_to(:show, proposal) } + it { should_not be_able_to(:vote, Proposal) } + it { should be_able_to(:create, Comment) } it { should_not be_able_to(:vote, Comment) } end @@ -96,6 +143,9 @@ describe Ability do it { should be_able_to(:show, debate) } it { should be_able_to(:vote, debate) } + it { should be_able_to(:index, Proposal) } + it { should be_able_to(:show, proposal) } + it { should be_able_to(:read, Organization) } describe "organizations" do @@ -114,12 +164,9 @@ describe Ability do end describe "hiding, reviewing and restoring" do - let(:own_comment) { create(:comment, author: user) } - let(:own_debate) { create(:debate, author: user) } - let(:hidden_comment) { create(:comment, :hidden) } - let(:hidden_debate) { create(:debate, :hidden) } let(:ignored_comment) { create(:comment, :with_ignored_flag) } let(:ignored_debate) { create(:debate, :with_ignored_flag) } + let(:ignored_proposal) { create(:proposal,:with_ignored_flag) } it { should be_able_to(:hide, comment) } it { should be_able_to(:hide_in_moderation_screen, comment) } @@ -131,6 +178,11 @@ describe Ability do it { should_not be_able_to(:hide, hidden_debate) } it { should_not be_able_to(:hide, own_debate) } + it { should be_able_to(:hide, proposal) } + it { should be_able_to(:hide_in_moderation_screen, proposal) } + it { should_not be_able_to(:hide, hidden_proposal) } + it { should_not be_able_to(:hide, own_proposal) } + it { should be_able_to(:ignore_flag, comment) } it { should_not be_able_to(:ignore_flag, hidden_comment) } it { should_not be_able_to(:ignore_flag, ignored_comment) } @@ -141,15 +193,29 @@ describe Ability do it { should_not be_able_to(:ignore_flag, ignored_debate) } it { should_not be_able_to(:ignore_flag, own_debate) } + it { should be_able_to(:ignore_flag, proposal) } + it { should_not be_able_to(:ignore_flag, hidden_proposal) } + it { should_not be_able_to(:ignore_flag, ignored_proposal) } + it { should_not be_able_to(:ignore_flag, own_proposal) } + + it { should be_able_to(:moderate, proposal) } + it { should_not be_able_to(:moderate, own_proposal) } + it { should_not be_able_to(:hide, user) } it { should be_able_to(:hide, other_user) } + it { should_not be_able_to(:block, user) } + it { should be_able_to(:block, other_user) } + it { should_not be_able_to(:restore, comment) } it { should_not be_able_to(:restore, debate) } + it { should_not be_able_to(:restore, proposal) } it { should_not be_able_to(:restore, other_user) } it { should be_able_to(:comment_as_moderator, debate) } + it { should be_able_to(:comment_as_moderator, proposal) } it { should_not be_able_to(:comment_as_administrator, debate) } + it { should_not be_able_to(:comment_as_administrator, proposal) } end end @@ -160,32 +226,37 @@ describe Ability do let(:other_user) { create(:user) } let(:hidden_user) { create(:user, :hidden) } - let(:hidden_debate) { create(:debate, :hidden) } - let(:hidden_comment) { create(:comment, :hidden) } - let(:own_debate) { create(:debate, author: user)} - let(:own_comment) { create(:comment, author: user)} - it { should be_able_to(:index, Debate) } it { should be_able_to(:show, debate) } it { should be_able_to(:vote, debate) } + it { should be_able_to(:index, Proposal) } + it { should be_able_to(:show, proposal) } + it { should_not be_able_to(:restore, comment) } it { should_not be_able_to(:restore, debate) } + it { should_not be_able_to(:restore, proposal) } it { should_not be_able_to(:restore, other_user) } it { should be_able_to(:restore, hidden_comment) } it { should be_able_to(:restore, hidden_debate) } + it { should be_able_to(:restore, hidden_proposal) } it { should be_able_to(:restore, hidden_user) } it { should_not be_able_to(:confirm_hide, comment) } it { should_not be_able_to(:confirm_hide, debate) } + it { should_not be_able_to(:confirm_hide, proposal) } it { should_not be_able_to(:confirm_hide, other_user) } it { should be_able_to(:confirm_hide, hidden_comment) } it { should be_able_to(:confirm_hide, hidden_debate) } + it { should be_able_to(:confirm_hide, hidden_proposal) } it { should be_able_to(:confirm_hide, hidden_user) } it { should be_able_to(:comment_as_administrator, debate) } it { should_not be_able_to(:comment_as_moderator, debate) } + + it { should be_able_to(:comment_as_administrator, proposal) } + it { should_not be_able_to(:comment_as_moderator, proposal) } end end diff --git a/spec/models/debate_spec.rb b/spec/models/debate_spec.rb index f9af3c149..907bcef5b 100644 --- a/spec/models/debate_spec.rb +++ b/spec/models/debate_spec.rb @@ -78,6 +78,42 @@ describe Debate do end end + describe "#votable_by?" do + let(:debate) { create(:debate) } + + before(:each) do + Setting.find_by(key: "max_ratio_anon_votes_on_debates").update(value: 50) + end + + it "should be true for level two verified users" do + user = create(:user, residence_verified_at: Time.now, confirmed_phone: "666333111") + expect(debate.votable_by?(user)).to be true + end + + it "should be true for level three verified users" do + user = create(:user, verified_at: Time.now) + expect(debate.votable_by?(user)).to be true + end + + it "should be true for anonymous users if allowed anonymous votes" do + debate.update(cached_anonymous_votes_total: 420, cached_votes_total: 1000) + user = create(:user) + expect(debate.votable_by?(user)).to be true + end + + it "should be true for anonymous users if less than 100 votes" do + debate.update(cached_anonymous_votes_total: 90, cached_votes_total: 92) + user = create(:user) + expect(debate.votable_by?(user)).to be true + end + + it "should be false for anonymous users if too many anonymous votes" do + debate.update(cached_anonymous_votes_total: 520, cached_votes_total: 1000) + user = create(:user) + expect(debate.votable_by?(user)).to be false + end + end + describe "#register_vote" do let(:debate) { create(:debate) } @@ -138,42 +174,6 @@ describe Debate do end end - describe "#votable_by?" do - let(:debate) { create(:debate) } - - before(:each) do - Setting.find_by(key: "max_ratio_anon_votes_on_debates").update(value: 50) - end - - it "should be true for level two verified users" do - user = create(:user, residence_verified_at: Time.now, confirmed_phone: "666333111") - expect(debate.votable_by?(user)).to be true - end - - it "should be true for level three verified users" do - user = create(:user, verified_at: Time.now) - expect(debate.votable_by?(user)).to be true - end - - it "should be true for anonymous users if allowed anonymous votes" do - debate.update(cached_anonymous_votes_total: 420, cached_votes_total: 1000) - user = create(:user) - expect(debate.votable_by?(user)).to be true - end - - it "should be true for anonymous users if less than 100 votes" do - debate.update(cached_anonymous_votes_total: 90, cached_votes_total: 92) - user = create(:user) - expect(debate.votable_by?(user)).to be true - end - - it "should be false for anonymous users if too many anonymous votes" do - debate.update(cached_anonymous_votes_total: 520, cached_votes_total: 1000) - user = create(:user) - expect(debate.votable_by?(user)).to be false - end - end - describe '#anonymous_votes_ratio' do it "returns the percentage of anonymous votes of the total votes" do debate = create(:debate, cached_anonymous_votes_total: 25, cached_votes_total: 100) @@ -244,16 +244,16 @@ describe Debate do debate = create(:debate, :with_confidence_score, cached_votes_up: 100, cached_votes_score: 100, cached_votes_total: 100) expect(debate.confidence_score).to eq(10000) - debate = create(:debate, :with_confidence_score, cached_votes_up: 0, cached_votes_score: -100, cached_votes_total: 100) + debate = create(:debate, :with_confidence_score, cached_votes_up: 0, cached_votes_total: 100) expect(debate.confidence_score).to eq(0) - debate = create(:debate, :with_confidence_score, cached_votes_up: 50, cached_votes_score: 50, cached_votes_total: 100) - expect(debate.confidence_score).to eq(2500) + debate = create(:debate, :with_confidence_score, cached_votes_up: 75, cached_votes_total: 100) + expect(debate.confidence_score).to eq(3750) - debate = create(:debate, :with_confidence_score, cached_votes_up: 500, cached_votes_score: 500, cached_votes_total: 1000) - expect(debate.confidence_score).to eq(25000) + debate = create(:debate, :with_confidence_score, cached_votes_up: 750, cached_votes_total: 1000) + expect(debate.confidence_score).to eq(37500) - debate = create(:debate, :with_confidence_score, cached_votes_up: 10, cached_votes_score: -80, cached_votes_total: 100) + debate = create(:debate, :with_confidence_score, cached_votes_up: 10, cached_votes_total: 100) expect(debate.confidence_score).to eq(-800) end diff --git a/spec/models/lock_spec.rb b/spec/models/lock_spec.rb index 0c072e9c8..d3c238484 100644 --- a/spec/models/lock_spec.rb +++ b/spec/models/lock_spec.rb @@ -10,7 +10,7 @@ describe Lock do expect(lock.locked?).to be true end - it "return false if locked_until is before curren time" do + it "return false if locked_until is before current time" do lock.locked_until = 1.day.ago expect(lock.locked?).to be false end diff --git a/spec/models/proposal_spec.rb b/spec/models/proposal_spec.rb new file mode 100644 index 000000000..f713f3ce5 --- /dev/null +++ b/spec/models/proposal_spec.rb @@ -0,0 +1,226 @@ +require 'rails_helper' + +describe Proposal do + let(:proposal) { build(:proposal) } + + it "should be valid" do + expect(proposal).to be_valid + end + + it "should not be valid without an author" do + proposal.author = nil + expect(proposal).to_not be_valid + end + + it "should not be valid without a title" do + proposal.title = nil + expect(proposal).to_not be_valid + end + + it "should not be valid without a question" do + proposal.question = nil + expect(proposal).to_not be_valid + end + + it "should not be valid without a summary" do + proposal.summary = nil + expect(proposal).to_not be_valid + end + + describe "#description" do + it "should be sanitized" do + proposal.description = "" + proposal.valid? + expect(proposal.description).to eq("alert('danger');") + end + end + + describe "#responsible" do + it "should be mandatory" do + proposal.responsible_name = nil + expect(proposal).to_not be_valid + end + + it "should be the document_number if level two user" do + author = create(:user, :level_two, document_number: "12345678Z") + proposal.author = author + proposal.responsible_name = nil + + expect(proposal).to be_valid + proposal.responsible_name = "12345678Z" + end + + it "should be the document_number if level two user" do + author = create(:user, :level_three, document_number: "12345678Z") + proposal.author = author + proposal.responsible_name = nil + + expect(proposal).to be_valid + proposal.responsible_name = "12345678Z" + end + end + + it "should sanitize the tag list" do + proposal.tag_list = "user_id=1" + proposal.valid? + expect(proposal.tag_list).to eq(['user_id1']) + end + + it "should not be valid without accepting terms of service" do + proposal.terms_of_service = nil + expect(proposal).to_not be_valid + end + + it "should have a code" do + Setting.find_by(key: "proposal_code_prefix").update(value: "TEST") + proposal = create(:proposal) + expect(proposal.code).to eq "TEST-#{proposal.created_at.strftime('%Y-%M')}-#{proposal.id}" + end + + describe "#editable?" do + let(:proposal) { create(:proposal) } + before(:each) {Setting.find_by(key: "max_votes_for_proposal_edit").update(value: 100)} + + it "should be true if proposal has no votes yet" do + expect(proposal.total_votes).to eq(0) + expect(proposal.editable?).to be true + end + + it "should be true if proposal has less than limit votes" do + create_list(:vote, 91, votable: proposal) + expect(proposal.total_votes).to eq(91) + expect(proposal.editable?).to be true + end + + it "should be false if proposal has more than limit votes" do + create_list(:vote, 102, votable: proposal) + expect(proposal.total_votes).to eq(102) + expect(proposal.editable?).to be false + end + end + + describe "#votable_by?" do + let(:proposal) { create(:proposal) } + + it "should be true for level two verified users" do + user = create(:user, residence_verified_at: Time.now, confirmed_phone: "666333111") + expect(proposal.votable_by?(user)).to be true + end + + it "should be true for level three verified users" do + user = create(:user, verified_at: Time.now) + expect(proposal.votable_by?(user)).to be true + end + + it "should be false for anonymous users" do + user = create(:user) + expect(proposal.votable_by?(user)).to be false + end + end + + describe "#register_vote" do + let(:proposal) { create(:proposal) } + + describe "from level two verified users" do + it "should register vote" do + user = create(:user, residence_verified_at: Time.now, confirmed_phone: "666333111") + expect {proposal.register_vote(user, 'yes')}.to change{proposal.reload.votes_for.size}.by(1) + end + end + + describe "from level three verified users" do + it "should register vote" do + user = create(:user, verified_at: Time.now) + expect {proposal.register_vote(user, 'yes')}.to change{proposal.reload.votes_for.size}.by(1) + end + end + + describe "from anonymous users" do + it "should not register vote" do + user = create(:user) + expect {proposal.register_vote(user, 'yes')}.to change{proposal.reload.votes_for.size}.by(0) + end + end + end + + describe '#hot_score' do + let(:now) { Time.now } + + it "increases for newer proposals" do + old = create(:proposal, :with_hot_score, created_at: now - 1.day) + new = create(:proposal, :with_hot_score, created_at: now) + expect(new.hot_score).to be > old.hot_score + end + + it "increases for proposals with more comments" do + more_comments = create(:proposal, :with_hot_score, created_at: now, comments_count: 25) + less_comments = create(:proposal, :with_hot_score, created_at: now, comments_count: 1) + expect(more_comments.hot_score).to be > less_comments.hot_score + end + + it "increases for proposals with more positive votes" do + more_likes = create(:proposal, :with_hot_score, created_at: now, cached_votes_up: 5) + less_likes = create(:proposal, :with_hot_score, created_at: now, cached_votes_up: 1) + expect(more_likes.hot_score).to be > less_likes.hot_score + end + + it "increases for proposals with more confidence" do + more_confidence = create(:proposal, :with_hot_score, created_at: now, cached_votes_up: 700) + less_confidence = create(:proposal, :with_hot_score, created_at: now, cached_votes_up: 9) + expect(more_confidence.hot_score).to be > less_confidence.hot_score + end + + it "decays in older proposals, even if they have more votes" do + older_more_voted = create(:proposal, :with_hot_score, created_at: now - 2.days, cached_votes_up: 900) + new_less_voted = create(:proposal, :with_hot_score, created_at: now, cached_votes_up: 9) + expect(new_less_voted.hot_score).to be > older_more_voted.hot_score + end + + describe 'actions which affect it' do + let(:proposal) { create(:proposal, :with_hot_score) } + + it "increases with votes" do + previous = proposal.hot_score + 5.times { proposal.register_vote(create(:user, verified_at: Time.now), true) } + expect(previous).to be < proposal.reload.hot_score + end + + it "increases with comments" do + previous = proposal.hot_score + 25.times{ Comment.create(user: create(:user), commentable: proposal, body: 'foobarbaz') } + expect(previous).to be < proposal.reload.hot_score + end + end + end + + describe "#confidence_score" do + + it "takes into account votes" do + proposal = create(:proposal, :with_confidence_score, cached_votes_up: 100) + expect(proposal.confidence_score).to eq(10000) + + proposal = create(:proposal, :with_confidence_score, cached_votes_up: 0) + expect(proposal.confidence_score).to eq(0) + + proposal = create(:proposal, :with_confidence_score, cached_votes_up: 75) + expect(proposal.confidence_score).to eq(7500) + + proposal = create(:proposal, :with_confidence_score, cached_votes_up: 750) + expect(proposal.confidence_score).to eq(75000) + + proposal = create(:proposal, :with_confidence_score, cached_votes_up: 10) + expect(proposal.confidence_score).to eq(1000) + end + + describe 'actions which affect it' do + let(:proposal) { create(:proposal, :with_confidence_score) } + + it "increases with like" do + previous = proposal.confidence_score + 5.times { proposal.register_vote(create(:user, verified_at: Time.now), true) } + expect(previous).to be < proposal.confidence_score + end + end + + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 756810395..f7ef59121 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -66,9 +66,9 @@ describe User do end describe 'preferences' do - describe 'email_on_debate_comment' do + describe 'email_on_comment' do it 'should be false by default' do - expect(subject.email_on_debate_comment).to eq(false) + expect(subject.email_on_comment).to eq(false) end end diff --git a/spec/support/common_actions.rb b/spec/support/common_actions.rb index 2de06ce92..4e2dcdb18 100644 --- a/spec/support/common_actions.rb +++ b/spec/support/common_actions.rb @@ -134,4 +134,20 @@ module CommonActions expect(page).to have_content 'Correct code' end + + def expect_message_you_need_to_sign_in + expect(page).to have_content 'You need to sign in or sign up before continuing' + expect(page).to have_selector('.in-favor a', visible: false) + end + + def expect_message_to_many_anonymous_votes + expect(page).to have_content 'Too many anonymous votes, verify your account to vote.' + expect(page).to have_selector('.in-favor a', visible: false) + end + + def expect_message_only_verified_can_vote_proposals + expect(page).to have_content 'Proposals can only be voted by verified users, verify your account.' + expect(page).to have_selector('.in-favor a', visible: false) + end + end