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("account.show.personal")%>

-
- <% if @account.organization? %> - <%= f.fields_for :organization do |fo| %> - <%= fo.text_field :name, autofocus: true, maxlength: Organization.name_max_length, placeholder: t("account.show.organization_name_label") %> - <%= fo.text_field :responsible_name, autofocus: true, maxlength: Organization.responsible_name_max_length, placeholder: t("account.show.organization_responsible_name_placeholder") %> - <% end %> - <%= f.text_field :phone_number, placeholder: t("account.show.phone_number_label") %> +
+ <% if @account.organization? %> + <%= f.fields_for :organization do |fo| %> + <%= fo.text_field :name, autofocus: true, maxlength: Organization.name_max_length, placeholder: t("account.show.organization_name_label") %> + <%= fo.text_field :responsible_name, autofocus: true, maxlength: Organization.responsible_name_max_length, placeholder: t("account.show.organization_responsible_name_placeholder") %> + <% end %> + <%= f.text_field :phone_number, placeholder: t("account.show.phone_number_label") %> <% else %> - <%= f.text_field :username, maxlength: User.username_max_length, placeholder: t("account.show.username_label") %> - <% end %> + <%= f.text_field :username, maxlength: User.username_max_length, placeholder: t("account.show.username_label") %> + <% end %>

<%= t("account.show.notifications")%>

- <%= f.label :email_on_debate_comment do %> - <%= f.check_box :email_on_debate_comment, label: false %> - <%= t("account.show.email_on_debate_comment_label") %> + <%= f.label :email_on_comment do %> + <%= f.check_box :email_on_comment, label: false %> + <%= t("account.show.email_on_comment_label") %> <% end %>
diff --git a/app/views/admin/_menu.html.erb b/app/views/admin/_menu.html.erb index 89cf9694e..42d979209 100644 --- a/app/views/admin/_menu.html.erb +++ b/app/views/admin/_menu.html.erb @@ -11,6 +11,13 @@ <% end %> +
  • > + <%= link_to admin_proposals_path do %> + + <%= t("admin.menu.hidden_proposals") %> + <% end %> +
  • +
  • > <%= link_to admin_debates_path do %> diff --git a/app/views/admin/proposals/index.html.erb b/app/views/admin/proposals/index.html.erb new file mode 100644 index 000000000..2c6c8fd84 --- /dev/null +++ b/app/views/admin/proposals/index.html.erb @@ -0,0 +1,28 @@ +

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

    + +<%= render 'shared/filter_subnav', i18n_namespace: "admin.proposals.index" %> + +

    <%= page_entries_info @proposals %>

    + + + +<%= paginate @proposals %> diff --git a/app/views/comments/_actions.html.erb b/app/views/comments/_actions.html.erb index 36ec42e01..80302a70a 100644 --- a/app/views/comments/_actions.html.erb +++ b/app/views/comments/_actions.html.erb @@ -11,7 +11,7 @@ <% if can? :hide, comment.user %>  •  - <%= link_to t("admin.actions.hide_author").capitalize, hide_moderation_user_path(comment.user_id, debate_id: @debate.id), + <%= link_to t("admin.actions.hide_author").capitalize, hide_moderation_user_path(comment.user_id), method: :put, data: { confirm: t('admin.actions.confirm') } %> <% end %> diff --git a/app/views/comments/_comment.html.erb b/app/views/comments/_comment.html.erb index 25d7585f3..5a87dde7d 100644 --- a/app/views/comments/_comment.html.erb +++ b/app/views/comments/_comment.html.erb @@ -5,7 +5,7 @@ <% if comment.hidden? || comment.user.hidden? %> <% if select_children(@comments, comment).size > 0 %>
    -

    <%= t("debates.comment.deleted") %>

    +

    <%= t("comments.comment.deleted") %>

    <% end %> <% else %> @@ -28,13 +28,13 @@
    <% if comment.as_administrator? %> - <%= t("debates.comment.admin") %> #<%= comment.administrator_id%> + <%= t("comments.comment.admin") %> #<%= comment.administrator_id%> <% elsif comment.as_moderator? %> - <%= t("debates.comment.moderator") %> #<%= comment.moderator_id%> + <%= t("comments.comment.moderator") %> #<%= comment.moderator_id%> <% else %> <% if comment.user.hidden? %> - <%= t("debates.comment.user_deleted") %> + <%= t("comments.comment.user_deleted") %> <% else %> <%= comment.user.name %> <% if comment.user.official? %> @@ -53,7 +53,7 @@ <% if comment.user_id == @commentable.author_id %>  •  - <%= t("debates.comment.author") %> + <%= t("comments.comment.author") %> <% end %> @@ -80,7 +80,7 @@
    - <%= t("debates.comment.responses", count: select_children(@comments, comment).size) %> + <%= t("comments.comment.responses", count: select_children(@comments, comment).size) %> <% if user_signed_in? %>  |  diff --git a/app/views/comments/_errors.html.erb b/app/views/comments/_errors.html.erb index 777225f64..e70eed258 100644 --- a/app/views/comments/_errors.html.erb +++ b/app/views/comments/_errors.html.erb @@ -1,2 +1,3 @@ -
    <%= t("errors.messages.blank").capitalize %>
    - +
    + <%= t("errors.messages.blank").capitalize %> +
    diff --git a/app/views/comments/_form.html.erb b/app/views/comments/_form.html.erb index d14f582e3..0d2a75eff 100644 --- a/app/views/comments/_form.html.erb +++ b/app/views/comments/_form.html.erb @@ -1,7 +1,7 @@ <% cache [locale_and_user_status, parent_id, commentable_cache_key(commentable)] do %> <% css_id = parent_or_commentable_dom_id(parent_id, commentable) %>
    > - <%= form_for [commentable, Comment.new], remote: true do |f| %> + <%= form_for Comment.new, remote: true do |f| %> <%= label_tag "comment-body-#{css_id}", t("comments.form.leave_comment") %> <%= f.text_area :body, id: "comment-body-#{css_id}", maxlength: Comment.body_max_length, label: false %> <%= f.hidden_field :commentable_type, value: commentable.class.name %> diff --git a/app/views/comments/_votes.html.erb b/app/views/comments/_votes.html.erb index 495837af6..21ee52884 100644 --- a/app/views/comments/_votes.html.erb +++ b/app/views/comments/_votes.html.erb @@ -1,5 +1,5 @@ - <%= t('debates.comment.votes', count: comment.total_votes) %> + <%= t('comments.comment.votes', count: comment.total_votes) %>  |  diff --git a/app/views/debates/_form.html.erb b/app/views/debates/_form.html.erb index 870f5081d..379ea60c2 100644 --- a/app/views/debates/_form.html.erb +++ b/app/views/debates/_form.html.erb @@ -20,7 +20,7 @@ <%= tag.name %> <% end %> - <%= f.text_field :tag_list, value: @debate.tag_list.to_s, label: false, placeholder: t("debates.form.tags_placeholder") %> + <%= f.text_field :tag_list, value: @debate.tag_list.to_s, label: false, placeholder: t("debates.form.tags_placeholder"), class: 'js-tag-list' %>
    diff --git a/app/views/debates/_votes.html.erb b/app/views/debates/_votes.html.erb index 5f38b1e7a..89a03752f 100644 --- a/app/views/debates/_votes.html.erb +++ b/app/views/debates/_votes.html.erb @@ -1,4 +1,4 @@ -<% voted_classes = css_classes_for_debate_vote(@debate_votes, debate) %> +<% voted_classes = css_classes_for_vote(@debate_votes, debate) %>
    <%= link_to vote_debate_path(debate, value: 'yes'), diff --git a/app/views/debates/index.html.erb b/app/views/debates/index.html.erb index 9d576bfc2..38593a359 100644 --- a/app/views/debates/index.html.erb +++ b/app/views/debates/index.html.erb @@ -1,7 +1,13 @@ -
    +<% content_for :header_addon do %> + <%= render "shared/search_form_header", + search_path: debates_path(page: 1), + i18n_namespace: "debates.index.search_form" %> +<% end %> +
    -
    +
    +
    <% if @search_terms %> @@ -16,39 +22,36 @@ <% end %>
    + <% if @tag_filter || @search_terms %>
    <%= t("debates.index.select_order") %>
    + <%= render 'shared/order_selector', i18n_namespace: "debates.index" %> +
    <% else %>

    <%= t("debates.index.select_order_long") %>

    + <%= render 'shared/order_selector', i18n_namespace: "debates.index" %> +
    <% end %> -
    - -
    + +
    + <%= link_to t("debates.index.start_debate"), new_debate_path, class: 'button radius expand' %>
    + + <%= render @debates %> + <%= paginate @debates %>
    -
    - <%= link_to t("debates.index.start_debate"), new_debate_path, class: 'button radius expand' %> -
    - <%= render @debates %> - <%= paginate @debates %>
    +
    diff --git a/app/views/debates/show.html.erb b/app/views/debates/show.html.erb index 83c37964a..70f78ccf1 100644 --- a/app/views/debates/show.html.erb +++ b/app/views/debates/show.html.erb @@ -57,7 +57,7 @@
    - <%= @debate.description %> + <%= safe_html_with_links @debate.description %> <%= render 'shared/tags', debate: @debate %> @@ -81,4 +81,5 @@
    <% end %> + <%= render "comments" %> diff --git a/app/views/layouts/_header.html.erb b/app/views/layouts/_header.html.erb index 9226c048a..62bb45674 100644 --- a/app/views/layouts/_header.html.erb +++ b/app/views/layouts/_header.html.erb @@ -31,10 +31,10 @@ @@ -51,8 +51,8 @@

    <%= 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" %>
    diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb index 4372c2eba..0a40bbf52 100644 --- a/app/views/layouts/mailer.html.erb +++ b/app/views/layouts/mailer.html.erb @@ -14,7 +14,7 @@ - <%= image_tag('logo_email_gobierno_abierto.png', style: "border: 0; display: block; width: 100%;max-width: 370px") %> + <%= image_tag('logo_email_gobierno_abierto.png', style: "border: 0; display: block; width: 100%;max-width: 370px", alt: "Ayuntamiento de Madrid - Participación ciudadana, transparencia y gobierno abierto") %> diff --git a/app/views/moderation/_menu.html.erb b/app/views/moderation/_menu.html.erb index 2f8954290..04bd02f4d 100644 --- a/app/views/moderation/_menu.html.erb +++ b/app/views/moderation/_menu.html.erb @@ -4,6 +4,13 @@ <%= link_to t("moderation.dashboard.index.title"), moderation_root_path %>
  • +
  • > + <%= link_to moderation_proposals_path do %> + + <%= t("moderation.menu.proposals") %> + <% end %> +
  • +
  • > <%= link_to moderation_debates_path do %> diff --git a/app/views/moderation/proposals/hide.js.erb b/app/views/moderation/proposals/hide.js.erb new file mode 100644 index 000000000..447336653 --- /dev/null +++ b/app/views/moderation/proposals/hide.js.erb @@ -0,0 +1,3 @@ +var proposal_id = '<%= dom_id(@proposal) %>'; +App.ModeratorProposals.add_class_faded(proposal_id); +App.ModeratorProposals.hide_moderator_actions(proposal_id); diff --git a/app/views/moderation/proposals/index.html.erb b/app/views/moderation/proposals/index.html.erb new file mode 100644 index 000000000..c7abaf18a --- /dev/null +++ b/app/views/moderation/proposals/index.html.erb @@ -0,0 +1,77 @@ +

    <%= t("moderation.proposals.index.title") %>

    + +<%= render 'shared/filter_subnav', i18n_namespace: "moderation.proposals.index" %> + +
    +

    <%= page_entries_info @proposals %>

    +
    +
    + <%= t("moderation.proposals.index.order") %> + <%= render 'shared/order_selector', i18n_namespace: "moderation.proposals.index" %> +
    +
    +
    + +<%= form_tag moderate_moderation_proposals_path(request.query_parameters), method: :put do %> +

    + <%= 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[]"} %> +

    + + + + + + + <% @proposals.each do |proposal| %> + + + + + <% end %> +
    + <%= t("moderation.proposals.index.headers.proposal") %> + + <%= t("moderation.proposals.index.headers.moderate") %> +
    + <%= link_to proposal.title, proposal, target: "_blank" %> +
    + <%= l proposal.updated_at.to_date %> +  •  + <%= proposal.flags_count %> +  •  + <%= proposal.author.username %> +
    +
    + <%= proposal.description %> +
    +
    + <%= check_box_tag "proposal_ids[]", proposal.id, nil, id: "#{dom_id(proposal)}_check" %> +
    + + + <%= submit_tag t('moderation.proposals.index.block_authors'), + name: "block_authors", + class: "button radius alert", + data: {confirm: t('moderation.proposals.index.confirm')} + %> + +
    + <%= submit_tag t('moderation.proposals.index.hide_proposals'), + name: "hide_proposals", + class: "button radius alert", + data: {confirm: t('moderation.proposals.index.confirm')} + %> + <%= submit_tag t('moderation.proposals.index.ignore_flags'), + name: "ignore_flags", + class: "button radius success", + data: {confirm: t('moderation.proposals.index.confirm')} + %> +
    + + <%= paginate @proposals %> + +<% end %> + diff --git a/app/views/pages/how_it_works.html.erb b/app/views/pages/how_it_works.html.erb index ab6bac050..9ac78b250 100644 --- a/app/views/pages/how_it_works.html.erb +++ b/app/views/pages/how_it_works.html.erb @@ -1,12 +1,14 @@
    diff --git a/app/views/pages/how_to_use.html.erb b/app/views/pages/how_to_use.html.erb index efc755709..b1e44382e 100644 --- a/app/views/pages/how_to_use.html.erb +++ b/app/views/pages/how_to_use.html.erb @@ -1,7 +1,10 @@
    -

    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
  • + +
  • + ¿Cómo funcionan las propuestas ciudadanas? + Crea tus propias propuestas +
  • +
    + +
  • + Participación ciudadana directa en el mundo + Sistemas de participación ciudadana que ya existen en el mundo +
  • +
    + +
  • + Hechos sobre participación ciudadana y democracia directa + Para perderle el miedo +
  • +
    + +

    Si tienes problemas con la web contacta con el servicio técnico.

    diff --git a/app/views/pages/participation.html.erb b/app/views/pages/participation.html.erb index 361d3c4d6..4750e0717 100644 --- a/app/views/pages/participation.html.erb +++ b/app/views/pages/participation.html.erb @@ -1,30 +1,32 @@

    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 @@ +
    +
    +
    +   + <%= link_to t("debates.show.back_link"), "/more_information", class: 'left back' %> + +

    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.

    +
    +
    +
    diff --git a/app/views/pages/participation_world.html.erb b/app/views/pages/participation_world.html.erb new file mode 100644 index 000000000..badda5eec --- /dev/null +++ b/app/views/pages/participation_world.html.erb @@ -0,0 +1,50 @@ +
    +
    + +
    +

    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.

    + +
    +
    +
    diff --git a/app/views/pages/proposals_info.html.erb b/app/views/pages/proposals_info.html.erb new file mode 100644 index 000000000..91ffe684f --- /dev/null +++ b/app/views/pages/proposals_info.html.erb @@ -0,0 +1,105 @@ +
    +
    + +
    +

    ¿Cómo funcionan las propuestas ciudadanas?

    +

    El mecanismo de propuestas ciudadanas se resume en cuatro pasos muy sencillos:

    +
      +
    1. ¡Propones! Creas una propuesta en esta web.
    2. +
    3. ¡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).
    4. +
    5. ¡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.
    6. +
    7. ¡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.
    8. +
    + +

    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

    + +
      +
    1. 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.
    2. +
    3. 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).
    4. +
    5. 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.
    6. +
    7. 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.
    8. +
    + +

    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

    +
      +
    1. ¿Cuáles son los requisitos que se solicitan para poder apoyar o votar propuestas?
      + 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. +
    2. + +
    3. ¿Cómo va a controlarse que cada persona vote una única vez por propuesta?
      + 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. +
    4. + +
    5. ¿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?
      + 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.
    6. + +
    7. ¿Qué diferencia existe entre los debates y las propuestas?
      + 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.
    8. + +
    9. ¿Vota toda la gente de Madrid en cada propuesta? ¿incluso aunque haga referencia a un único distrito?
      + 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.
    10. + +
    11. ¿Y si se presentan varias propuestas iguales? ¿Se plantea la posibilidad de unificarlas para evitar que se diversifiquen los apoyos y/o votos?
      + 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.
    12. + +
    13. ¿Durante cuanto tiempo se pueden recoger apoyos?
      + 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.
    14. + +
    15. ¿Durante cuanto tiempo se votará cada propuesta?
      + 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.
    16. + +
    17. ¿Pueden participar asociaciones, fundaciones y ONG´s? ¿Y empresas?
      + 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.
    18. + +
    19. ¿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?
      + 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.
    20. + +
    21. ¿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?
      + 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.
    22. + +
    23. ¿Cómo puede participar la gente que no esté empadronada en Madrid?
      + Presentando propuestas, participando en los debates, y difundiendo lo que ocurra en la plataforma.
    24. + +
    25. ¿El voto es secreto? ¿el Ayuntamiento podrá tener acceso a los votos de los ciudadanos?
      + 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.
    26. + +
    27. ¿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.
    28. + +
    29. ¿Cómo se sabe que una propuesta se ha cumplido?
      + 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.
    30. + +

      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.

      + +
    +
    +
    diff --git a/app/views/proposals/_actions.html.erb b/app/views/proposals/_actions.html.erb new file mode 100644 index 000000000..b2bc9f111 --- /dev/null +++ b/app/views/proposals/_actions.html.erb @@ -0,0 +1,10 @@ +<% if can? :hide, proposal %> + <%= link_to t("admin.actions.hide").capitalize, hide_moderation_proposal_path(proposal), + method: :put, remote: true, data: { confirm: t('admin.actions.confirm') } %> +<% end %> + +<% if can? :hide, proposal.author %> +  |  + <%= link_to t("admin.actions.hide_author").capitalize, hide_moderation_user_path(proposal.author_id), + method: :put, data: { confirm: t('admin.actions.confirm') } %> +<% end %> diff --git a/app/views/proposals/_comments.html.erb b/app/views/proposals/_comments.html.erb new file mode 100644 index 000000000..f9047d83b --- /dev/null +++ b/app/views/proposals/_comments.html.erb @@ -0,0 +1,29 @@ +<% cache [locale_and_user_status, commentable_cache_key(@proposal), @all_visible_comments, @all_visible_comments.map(&:author), @proposal.comments_count, @comment_flags] do %> +
    +
    +
    +

    + <%= t("proposals.show.comments_title") %> + (<%= @proposal.comments_count %>) +

    + + <% if user_signed_in? %> + <%= render 'comments/form', {commentable: @proposal, parent_id: nil, toggeable: false} %> + <% else %> +
    + +
    + <%= t("proposals.show.login_to_comment", + signin: link_to(t("votes.signin"), new_user_session_path), + signup: link_to(t("votes.signup"), new_user_registration_path)).html_safe %> +
    + <% end %> + + <% @root_comments.each do |comment| %> + <%= render 'comments/comment', comment: comment %> + <% end %> + <%= paginate @root_comments %> +
    +
    +
    +<% end %> \ No newline at end of file diff --git a/app/views/proposals/_flag_actions.html.erb b/app/views/proposals/_flag_actions.html.erb new file mode 100644 index 000000000..3ab259ac8 --- /dev/null +++ b/app/views/proposals/_flag_actions.html.erb @@ -0,0 +1,23 @@ + + <% if show_flag_action? proposal %> + + + <% end %> + + <% if show_unflag_action? proposal %> + + + <% 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 %> + +
    +
    + <%= f.label :title, t("proposals.form.proposal_title") %> + <%= f.text_field :title, maxlength: Proposal.title_max_length, placeholder: t("proposals.form.proposal_title"), label: false %> +
    + +
    + <%= f.label :question, t("proposals.form.proposal_question") %> + + <%= t("proposals.form.proposal_question_example_html") %> + + <%= f.text_field :question, maxlength: Proposal.question_max_length, placeholder: t("proposals.form.proposal_question"), label: false %> +
    + +
    + <%= f.label :summary, t("proposals.form.proposal_summary") %> + <%= t("proposals.form.proposal_summary_note") %> + <%= f.text_area :summary, rows: 4, maxlength: 200, label: false, + placeholder: t('proposals.form.proposal_summary') %> +
    + +
    + <%= f.label :description, t("proposals.form.proposal_text") %> + <%= f.cktext_area :description, maxlength: Proposal.description_max_length, ckeditor: { language: I18n.locale }, label: false %> +
    + + +
    + <%= f.label :video_url, t("proposals.form.proposal_video_url") %> + <%= t("proposals.form.proposal_video_url_note") %> + <%= f.text_field :video_url, placeholder: t("proposals.form.proposal_video_url"), label: false %> +
    + +
    + <%= f.label :external_url, t("proposals.form.proposal_external_url") %> + <%= f.text_field :external_url, placeholder: t("proposals.form.proposal_external_url"), label: false %> +
    + +
    + <%= f.label :tag_list, t("proposals.form.tags_label") %> + <%= t("proposals.form.tags_instructions") %> + + <% @featured_tags.each do |tag| %> + <%= tag.name %> + <% end %> + + <%= f.text_field :tag_list, value: @proposal.tag_list.to_s, label: false, placeholder: t("proposals.form.tags_placeholder"), class: 'js-tag-list' %> +
    + + <% if current_user.unverified? %> +
    + <%= f.label :responsible_name, t("proposals.form.proposal_responsible_name") %> + <%= t("proposals.form.proposal_responsible_name_note") %> + <%= f.text_field :responsible_name, placeholder: t("proposals.form.proposal_responsible_name"), label: false %> +
    + <% end %> + +
    + <% if @proposal.new_record? %> + <%= f.label :terms_of_service do %> + <%= f.check_box :terms_of_service, label: false %> + + <%= t("form.accept_terms", + policy: link_to(t("form.policy"), "/privacy", target: "blank"), + conditions: link_to(t("form.conditions"), "/conditions", target: "blank")).html_safe %> + + <% end %> + <% end %> +
    + +
    + <%= f.simple_captcha input_html: { required: false } %> +
    + +
    + <%= f.submit(class: "button radius", value: t("proposals.#{action_name}.form.submit_button")) %> +
    +
    +<% end %> + + diff --git a/app/views/proposals/_proposal.html.erb b/app/views/proposals/_proposal.html.erb new file mode 100644 index 000000000..21cb53f5b --- /dev/null +++ b/app/views/proposals/_proposal.html.erb @@ -0,0 +1,30 @@ +
    +
    +
    + +
    +
    + <%= t("proposals.proposal.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 %> +

    +
    + <%= link_to proposal.summary, proposal %> +
    +
    + <%= render "shared/tags", proposal: proposal, limit: 5 %> +
    +
    + +
    + <%= render 'proposals/votes', proposal: proposal %> +
    + +
    +
    +
    diff --git a/app/views/proposals/_refresh_flag_actions.js.erb b/app/views/proposals/_refresh_flag_actions.js.erb new file mode 100644 index 000000000..0e20636ae --- /dev/null +++ b/app/views/proposals/_refresh_flag_actions.js.erb @@ -0,0 +1 @@ +$("#<%= dom_id(@proposal) %> .js-flag-actions").html('<%= j render("proposals/flag_actions", proposal: @proposal) %>'); diff --git a/app/views/proposals/_votes.html.erb b/app/views/proposals/_votes.html.erb new file mode 100644 index 000000000..526b114d5 --- /dev/null +++ b/app/views/proposals/_votes.html.erb @@ -0,0 +1,51 @@ +
    + +
    + +
    + + + <%= t("proposals.proposal.supports", count: proposal.total_votes) %>  + (<%= supports_percentage(proposal) %>) + + "> + <%= t("proposals.proposal.supports_necessary") %> + + + + +
    + <% if voted_for?(@proposal_votes, proposal) %> +
    + <%= t("proposals.proposal.already_supported") %> +
    + <% else %> + <%= link_to vote_proposal_path(proposal, value: 'yes'), + class: "button button-support tiny radius expand", + title: t('proposals.proposal.support_title'), method: "post", remote: true do %> + <%= t("proposals.proposal.support") %> + <% end %> + <% end %> +
    + + <% if user_signed_in? && current_user.organization? %> + + <% elsif user_signed_in? && !proposal.votable_by?(current_user)%> + + <% elsif !user_signed_in? %> + + <% end %> +
    diff --git a/app/views/proposals/edit.html.erb b/app/views/proposals/edit.html.erb new file mode 100644 index 000000000..7d7ce413b --- /dev/null +++ b/app/views/proposals/edit.html.erb @@ -0,0 +1,17 @@ +
    + +
    + <%= link_to proposals_path, class: "left back clear" do %> + + <%= t("proposals.edit.back_link") %> + <% end %> + +
    + <%= link_to t("proposals.edit.show_link"), @proposal %> +
    + +

    <%= t("proposals.edit.editing") %>

    + + <%= render "form" %> +
    +
    diff --git a/app/views/proposals/index.html.erb b/app/views/proposals/index.html.erb new file mode 100644 index 000000000..4d9a2de57 --- /dev/null +++ b/app/views/proposals/index.html.erb @@ -0,0 +1,58 @@ +<% content_for :header_addon do %> + <%= render "shared/search_form_header", + search_path: proposals_path(page: 1), + i18n_namespace: "proposals.index.search_form" %> +<% end %> + +
    +
    +
    + +
    +
    + <% if @search_terms %> +

    + <%= 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 %> +
    + + <% if @tag_filter || @search_terms %> +
    +
    + <%= t("proposals.index.select_order") %> +
    + <%= render 'shared/order_selector', i18n_namespace: "proposals.index" %> +
    + <% else %> +
    +

    + <%= t("proposals.index.select_order_long") %> +

    + <%= render 'shared/order_selector', i18n_namespace: "proposals.index" %> +
    + <% end %> + +
    + <%= link_to t("proposals.index.start_proposal"), new_proposal_path, class: 'button radius expand' %> +
    + + <%= render @proposals %> + <%= paginate @proposals %> +
    +
    + +
    + +
    +
    +
    diff --git a/app/views/proposals/new.html.erb b/app/views/proposals/new.html.erb new file mode 100644 index 000000000..0bda9fd56 --- /dev/null +++ b/app/views/proposals/new.html.erb @@ -0,0 +1,26 @@ +
    + +
    + <%= link_to proposals_path, class: "left back" do %> + + <%= t("proposals.new.back_link") %> + <% end %> +

    <%= t("proposals.new.start_new") %>

    +
    + <%= link_to "/proposals_info", target: "_blank" do %> + <%= t("proposals.new.more_info")%> + <% end %> +
    + <%= render "form" %> +
    + +
    + +

    <%= t("proposals.new.recommendations_title") %>

    +
      +
    • <%= t("proposals.new.recommendation_one") %>
    • +
    • <%= t("proposals.new.recommendation_two") %>
    • +
    • <%= t("proposals.new.recommendation_three") %>
    • +
    +
    +
    diff --git a/app/views/proposals/show.html.erb b/app/views/proposals/show.html.erb new file mode 100644 index 000000000..c85f3d2d9 --- /dev/null +++ b/app/views/proposals/show.html.erb @@ -0,0 +1,99 @@ +<% cache [locale_and_user_status(@proposal), @proposal, @proposal.author, Flag.flagged?(current_user, @proposal), @proposal_votes] do %> +
    +
    +
    +   + <%= link_to t("proposals.show.back_link"), proposals_path, class: 'left back' %> + + <% if current_user && @proposal.editable_by?(current_user) %> + <%= link_to edit_proposal_path(@proposal), class: 'edit-proposal button success tiny radius right' do %> + + <%= t("proposals.show.edit_proposal_link") %> + <% end %> + <% end %> + +

    <%= @proposal.title %>

    + <% if @proposal.conflictive? %> +
    + <%= t("proposals.show.flag") %> +
    + <% end %> + + +
    + <%= avatar_image(@proposal.author, seed: @proposal.author_id, size: 32, class: 'author-photo') %> + + <% if @proposal.author.hidden? %> + + + <%= t("proposals.show.author_deleted") %> + + <% else %> + + <%= @proposal.author.name %> + + <% if @proposal.author.official? %> +  •  + + <%= @proposal.author.official_position %> + + <% end %> + <% end %> + + <% if @proposal.author.verified_organization? %> +  •  + + <%= t("shared.collective") %> + + <% end %> + +  •  + <%= l @proposal.created_at.to_date %> +  •  +   + <%= link_to t("proposals.show.comments", count: @proposal.comments_count), "#comments" %> +  •  + <%= @proposal.code %> +  •  + + <%= render 'proposals/flag_actions', proposal: @proposal %> + +
    + +
    <%= @proposal.summary %>
    + + <%= safe_html_with_links @proposal.description %> + + <% if @proposal.external_url.present? %> +
    <%= text_with_links @proposal.external_url %>
    + <% end %> + + <% if @proposal.video_url.present? %> +
    <%= text_with_links @proposal.video_url %>
    + <% end %> + +

    <%= @proposal.question %>

    + + <%= render 'shared/tags', proposal: @proposal %> + +
    + <%= render 'actions', proposal: @proposal %> +
    +
    + + +
    +
    +<% end %> +<%= render "comments" %> diff --git a/app/views/proposals/vote.js.erb b/app/views/proposals/vote.js.erb new file mode 100644 index 000000000..4a5ca6b6b --- /dev/null +++ b/app/views/proposals/vote.js.erb @@ -0,0 +1 @@ +$("#<%= dom_id(@proposal) %>_votes").html('<%= j render("proposals/votes", proposal: @proposal) %>'); \ No newline at end of file diff --git a/app/views/shared/_order_selector.html.erb b/app/views/shared/_order_selector.html.erb new file mode 100644 index 000000000..489977718 --- /dev/null +++ b/app/views/shared/_order_selector.html.erb @@ -0,0 +1,15 @@ +<% # Params: + # + # i18n_namespace: for example "moderation.debates.index" +%> + +
    + +
    diff --git a/app/views/shared/_search_form.html.erb b/app/views/shared/_search_form.html.erb index 1c7a0eb45..1c0344993 100644 --- a/app/views/shared/_search_form.html.erb +++ b/app/views/shared/_search_form.html.erb @@ -1,21 +1,27 @@ +<% # Params: + # + # search_path: for example debates_path + # i18n_namespace: for example "debates.index.search_form" +%> +
    -

    <%= t("shared.search_form.search_title") %>

    +

    <%= t("#{i18n_namespace}.title") %>


    - <%= form_tag debates_path, method: :get do %> + <%= form_tag search_path, method: :get do %>
    - " class="search-form"> + " class="search-form">
    - + ">
    <% end %>
    -
    \ No newline at end of file +
    diff --git a/app/views/shared/_search_form_header.html.erb b/app/views/shared/_search_form_header.html.erb index 343b8d3e6..cf84656e6 100644 --- a/app/views/shared/_search_form_header.html.erb +++ b/app/views/shared/_search_form_header.html.erb @@ -1,14 +1,20 @@ +<% # Params: + # + # i18n_namespace: for example "debates.index.search_form" + # search_path: for example debates_path +%> +
    - <%= form_tag debates_path, method: :get do %> + <%= form_tag search_path, method: :get do %>
    - " class="search-form"> + " class="search-form">
    -
    diff --git a/app/views/shared/_tag_cloud.html.erb b/app/views/shared/_tag_cloud.html.erb index 737fc3a94..171b85b3c 100644 --- a/app/views/shared/_tag_cloud.html.erb +++ b/app/views/shared/_tag_cloud.html.erb @@ -3,6 +3,6 @@

    <%= 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 %>
    diff --git a/app/views/shared/_tags.html.erb b/app/views/shared/_tags.html.erb index ea3f65c6b..6c6d87096 100644 --- a/app/views/shared/_tags.html.erb +++ b/app/views/shared/_tags.html.erb @@ -1,13 +1,15 @@ <%- limit ||= nil %> +<%- taggable = defined?(debate) ? debate : proposal %> -<% if debate.tags.any? %> +<% if taggable.tags.any? %> - <% debate.tag_list_with_limit(limit).each do |tag| %> - <%= link_to sanitize(tag.name), debates_path(tag: tag.name) %> + <% taggable.tag_list_with_limit(limit).each do |tag| %> + <%= link_to sanitize(tag.name), send("#{taggable.class.to_s.downcase.pluralize}_path", tag: tag.name) %> <% end %> - <% if debate.tags_count_out_of_limit(limit) > 0 %> - <%= link_to "#{debate.tags_count_out_of_limit(limit)}+", debate_path(debate) %> + <% if taggable.tags_count_out_of_limit(limit) > 0 %> + <%= link_to "#{taggable.tags_count_out_of_limit(limit)}+", + send("#{taggable.class.to_s.downcase}_path", taggable) %> <% end %> -<% end %> \ No newline at end of file +<% end %> diff --git a/app/views/verification/letter/edit.html.erb b/app/views/verification/letter/edit.html.erb index 3fddf9435..df834ca8b 100644 --- a/app/views/verification/letter/edit.html.erb +++ b/app/views/verification/letter/edit.html.erb @@ -1,4 +1,9 @@