diff --git a/.gitignore b/.gitignore index 35f5651a4..a6f2826c3 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,5 @@ .DS_Store .ruby-gemset -public/sitemap.xml \ No newline at end of file +public/sitemap.xml +public/system/ diff --git a/Capfile b/Capfile index 1a33b63f0..ea11eb9b6 100644 --- a/Capfile +++ b/Capfile @@ -12,6 +12,10 @@ require 'capistrano/delayed_job' require 'whenever/capistrano' require 'rvm1/capistrano3' +#SCM: Git +require "capistrano/scm/git" +install_plugin Capistrano::SCM::Git + # Load custom tasks from `lib/capistrano/tasks` if you have any defined Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r } Dir.glob('lib/capistrano/tasks/*.cap').each { |r| import r } diff --git a/Gemfile b/Gemfile index c60e91f35..d7f212e86 100644 --- a/Gemfile +++ b/Gemfile @@ -1,20 +1,20 @@ source 'https://rubygems.org' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' -gem 'rails', '4.2.7.1' +gem 'rails', '4.2.8' # Use PostgreSQL -gem 'pg', '~> 0.19.0' +gem 'pg', '~> 0.20.0' # Use SCSS for stylesheets gem 'sass-rails', '~> 5.0', '>= 5.0.4' # Use Uglifier as compressor for JavaScript assets -gem 'uglifier', '>= 3.0.4' +gem 'uglifier', '~> 3.2.0' # Use CoffeeScript for .coffee assets and views gem 'coffee-rails', '~> 4.2.1' # See https://github.com/rails/execjs#readme for more supported runtimes # gem 'therubyracer', platforms: :ruby # Use jquery as the JavaScript library -gem 'jquery-rails', '~> 4.2.2' +gem 'jquery-rails', '~> 4.3.1' gem 'jquery-ui-rails' # Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks gem 'turbolinks' @@ -28,42 +28,44 @@ gem 'devise_security_extension' # gem 'bcrypt', '~> 3.1.7' gem 'omniauth' gem 'omniauth-twitter' -gem 'omniauth-facebook', '~> 3.0.0' +gem 'omniauth-facebook', '~> 4.0.0' gem 'omniauth-google-oauth2', '~> 0.4.0' -gem 'kaminari' +gem 'kaminari', '~> 1.0.1' gem 'ancestry', '~> 2.2.2' gem 'acts-as-taggable-on' -gem 'responders', '~> 2.3.0' +gem 'responders', '~> 2.4.0' gem 'foundation-rails', '~> 6.2.4.0' gem 'foundation_rails_helper', '~> 2.0.0' gem 'acts_as_votable' -gem 'ckeditor', '~> 4.2.2' +gem 'ckeditor', '~> 4.2.3' gem 'invisible_captcha', '~> 0.9.2' -gem 'cancancan' -gem 'social-share-button' +gem 'cancancan', '~> 1.16.0' +gem 'social-share-button', '~> 0.10' gem 'initialjs-rails', '0.2.0.4' -gem 'unicorn', '~> 5.2.0' -gem 'paranoia', '~> 2.2.0' +gem 'unicorn', '~> 5.3.0' +gem 'paranoia', '~> 2.3.1' gem 'rinku', '~> 2.0.2', require: 'rails_rinku' gem 'savon' gem 'dalli' -gem 'rollbar', '~> 2.14.0' +gem 'rollbar', '~> 2.14.1' gem 'delayed_job_active_record', '~> 4.1.0' gem 'daemons' gem 'devise-async' -gem 'newrelic_rpm', '~> 3.17.2.327' +gem 'newrelic_rpm', '~> 4.1.0.333' gem 'whenever', require: false gem 'pg_search' -gem 'sitemap_generator' +gem 'sitemap_generator', '~> 5.3.1' -gem 'ahoy_matey', '~> 1.5.3' -gem 'groupdate', '~> 3.1.0' # group temporary data +gem 'ahoy_matey', '~> 1.6.0' +gem 'groupdate', '~> 3.2.0' # group temporary data gem 'tolk', '~> 2.0.0' # Web interface for translations gem 'browser' gem 'turnout', '~> 2.4.0' -gem 'redcarpet' +gem 'redcarpet', '~> 3.4.0' + +gem 'paperclip' gem 'graphql', '~> 1.3.0' @@ -73,29 +75,29 @@ group :development, :test do # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring gem 'spring' gem 'spring-commands-rspec' - gem 'rspec-rails', '~> 3.5' - gem 'capybara' - gem 'factory_girl_rails' + gem 'rspec-rails', '~> 3.6' + gem 'capybara', '~> 2.14.0' + gem 'factory_girl_rails', '~> 4.8.0' gem 'fuubar' gem 'launchy' gem 'quiet_assets' - gem 'letter_opener_web', '~> 1.3.0' - gem 'i18n-tasks' - gem 'capistrano', '3.5.0', require: false + gem 'letter_opener_web', '~> 1.3.1' + gem 'i18n-tasks', '~> 0.9.15' + gem 'capistrano', '~> 3.8.1', require: false gem 'capistrano-bundler', '~> 1.2', require: false - gem "capistrano-rails", '1.1.8', require: false + gem "capistrano-rails", '~> 1.2.3', require: false gem 'rvm1-capistrano3', require: false - gem 'capistrano3-delayed-job', '~> 1.0' - gem "bullet" - gem "faker" - gem 'rubocop', '~> 0.45.0', require: false + gem 'capistrano3-delayed-job', '~> 1.7.3' + gem "bullet", '~> 5.5.1' + gem "faker", '~> 1.7.3' + gem 'rubocop', '~> 0.48.1', require: false gem 'knapsack' end group :test do gem 'database_cleaner' - gem 'poltergeist' - gem 'coveralls', require: false + gem 'poltergeist', '~> 1.15.0' + gem 'coveralls', '~> 0.8.21', require: false gem 'email_spec' end diff --git a/Gemfile.lock b/Gemfile.lock index efdd101e6..585295501 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,46 +1,46 @@ GEM remote: https://rubygems.org/ specs: - actionmailer (4.2.7.1) - actionpack (= 4.2.7.1) - actionview (= 4.2.7.1) - activejob (= 4.2.7.1) + actionmailer (4.2.8) + actionpack (= 4.2.8) + actionview (= 4.2.8) + activejob (= 4.2.8) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 1.0, >= 1.0.5) - actionpack (4.2.7.1) - actionview (= 4.2.7.1) - activesupport (= 4.2.7.1) + actionpack (4.2.8) + actionview (= 4.2.8) + activesupport (= 4.2.8) rack (~> 1.6) rack-test (~> 0.6.2) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (4.2.7.1) - activesupport (= 4.2.7.1) + actionview (4.2.8) + activesupport (= 4.2.8) builder (~> 3.1) erubis (~> 2.7.0) rails-dom-testing (~> 1.0, >= 1.0.5) - rails-html-sanitizer (~> 1.0, >= 1.0.2) - activejob (4.2.7.1) - activesupport (= 4.2.7.1) + rails-html-sanitizer (~> 1.0, >= 1.0.3) + activejob (4.2.8) + activesupport (= 4.2.8) globalid (>= 0.3.0) - activemodel (4.2.7.1) - activesupport (= 4.2.7.1) + activemodel (4.2.8) + activesupport (= 4.2.8) builder (~> 3.1) - activerecord (4.2.7.1) - activemodel (= 4.2.7.1) - activesupport (= 4.2.7.1) + activerecord (4.2.8) + activemodel (= 4.2.8) + activesupport (= 4.2.8) arel (~> 6.0) - activesupport (4.2.7.1) + activesupport (4.2.8) i18n (~> 0.7) - json (~> 1.7, >= 1.7.7) minitest (~> 5.1) thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) - acts-as-taggable-on (3.5.0) - activerecord (>= 3.2, < 5) + acts-as-taggable-on (4.0.0) + activerecord (>= 4.0) acts_as_votable (0.10.0) - addressable (2.4.0) - ahoy_matey (1.5.3) + addressable (2.5.1) + public_suffix (~> 2.0, >= 2.0.2) + ahoy_matey (1.6.0) addressable browser (~> 2.0) geocoder @@ -51,14 +51,14 @@ GEM safely_block (>= 0.1.1) user_agent_parser uuidtools - airbrussh (1.1.1) + airbrussh (1.2.0) sshkit (>= 1.6.1, != 1.7.0) akami (1.3.1) gyoku (>= 0.4.0) nokogiri ancestry (2.2.2) activerecord (>= 3.0.0) - arel (6.0.3) + arel (6.0.4) ast (2.3.0) babel-source (5.8.35) babel-transpiler (0.7.0) @@ -66,28 +66,27 @@ GEM execjs (~> 2.0) bcrypt (3.1.11) browser (2.3.0) - builder (3.2.2) - bullet (5.4.2) + builder (3.2.3) + bullet (5.5.1) activesupport (>= 3.0.0) uniform_notifier (~> 1.10.0) byebug (9.0.6) - cancancan (1.15.0) - capistrano (3.5.0) + cancancan (1.16.0) + capistrano (3.8.1) airbrussh (>= 1.0.0) - capistrano-harrow i18n rake (>= 10.0.0) sshkit (>= 1.9.0) capistrano-bundler (1.2.0) capistrano (~> 3.1) sshkit (~> 1.2) - capistrano-harrow (0.5.3) - capistrano-rails (1.1.8) + capistrano-rails (1.2.3) capistrano (~> 3.1) capistrano-bundler (~> 1.1) - capistrano3-delayed-job (1.7.2) + capistrano3-delayed-job (1.7.3) capistrano (~> 3.0, >= 3.0.0) - capybara (2.7.1) + daemons (~> 1.2.4) + capybara (2.14.0) addressable mime-types (>= 1.16) nokogiri (>= 1.3.3) @@ -95,11 +94,10 @@ GEM rack-test (>= 0.5.4) xpath (~> 2.0) chronic (0.10.2) - ckeditor (4.2.2) + ckeditor (4.2.3) cocaine orm_adapter (~> 0.5.0) - climate_control (0.0.3) - activesupport (>= 3.0) + climate_control (0.1.0) cliver (0.3.2) cocaine (0.5.8) climate_control (>= 0.0.3, < 1.0) @@ -109,13 +107,13 @@ GEM coffee-script (2.4.1) coffee-script-source execjs - coffee-script-source (1.10.0) - concurrent-ruby (1.0.4) - coveralls (0.8.17) + coffee-script-source (1.12.2) + concurrent-ruby (1.0.5) + coveralls (0.8.21) json (>= 1.8, < 3) - simplecov (~> 0.12.0) + simplecov (~> 0.14.1) term-ansicolor (~> 1.3) - thor (~> 0.19.1) + thor (~> 0.19.4) tins (~> 1.6) daemons (1.2.4) dalli (2.7.6) @@ -138,7 +136,7 @@ GEM devise_security_extension (0.10.0) devise (>= 3.0.0, < 4.0) railties (>= 3.2.6, < 5.0) - diff-lcs (1.2.5) + diff-lcs (1.3) docile (1.1.5) easy_translate (0.5.0) json @@ -151,14 +149,14 @@ GEM errbase (0.0.3) erubis (2.7.0) execjs (2.7.0) - factory_girl (4.7.0) + factory_girl (4.8.0) activesupport (>= 3.0.0) - factory_girl_rails (4.7.0) - factory_girl (~> 4.7.0) + factory_girl_rails (4.8.0) + factory_girl (~> 4.8.0) railties (>= 3.0.0) - faker (1.6.6) + faker (1.7.3) i18n (~> 0.5) - faraday (0.9.2) + faraday (0.11.0) multipart-post (>= 1.2, < 3) foundation-rails (6.2.4.0) railties (>= 3.1.0) @@ -173,23 +171,22 @@ GEM fuubar (2.2.0) rspec-core (~> 3.0) ruby-progressbar (~> 1.4) - geocoder (1.4.1) + geocoder (1.4.3) globalid (0.3.7) activesupport (>= 4.1.0) graphiql-rails (1.3.0) rails graphql (1.3.0) - groupdate (3.1.1) - activesupport (>= 3) + groupdate (3.2.0) gyoku (1.3.1) builder (>= 2.1.2) - hashie (3.4.6) + hashie (3.5.5) highline (1.7.8) htmlentities (4.3.4) httpi (2.4.1) rack - i18n (0.7.0) - i18n-tasks (0.9.6) + i18n (0.8.1) + i18n-tasks (0.9.15) activesupport (>= 4.0.2) ast (>= 2.1.0) easy_translate (>= 0.5.0) @@ -197,32 +194,41 @@ GEM highline (>= 1.7.3) i18n parser (>= 2.2.3.0) - term-ansicolor (>= 1.3.2) + rainbow (~> 2.2) terminal-table (>= 1.5.1) initialjs-rails (0.2.0.4) railties (>= 3.1, < 6.0) invisible_captcha (0.9.2) rails (>= 3.2.0) - jquery-rails (4.2.2) + jquery-rails (4.3.1) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) - jquery-ui-rails (5.0.5) + jquery-ui-rails (6.0.1) railties (>= 3.2.16) - json (1.8.3) - jwt (1.5.4) - kaminari (0.17.0) - actionpack (>= 3.0.0) - activesupport (>= 3.0.0) + json (2.1.0) + jwt (1.5.6) + kaminari (1.0.1) + activesupport (>= 4.1.0) + kaminari-actionview (= 1.0.1) + kaminari-activerecord (= 1.0.1) + kaminari-core (= 1.0.1) + kaminari-actionview (1.0.1) + actionview + kaminari-core (= 1.0.1) + kaminari-activerecord (1.0.1) + activerecord + kaminari-core (= 1.0.1) + kaminari-core (1.0.1) kgio (2.11.0) - knapsack (1.13.1) + knapsack (1.13.3) rake timecop (>= 0.1.0) launchy (2.4.3) addressable (~> 2.3) letter_opener (1.4.1) launchy (~> 2.2) - letter_opener_web (1.3.0) + letter_opener_web (1.3.1) actionmailer (>= 3.2) letter_opener (~> 1.0) railties (>= 3.2) @@ -233,29 +239,30 @@ GEM mime-types (3.1) mime-types-data (~> 3.2015) mime-types-data (3.2016.0521) + mimemagic (0.3.2) mini_portile2 (2.1.0) minitest (5.10.1) multi_json (1.12.1) - multi_xml (0.5.5) + multi_xml (0.6.0) multipart-post (2.0.0) net-scp (1.2.1) net-ssh (>= 2.6.5) - net-ssh (3.2.0) - newrelic_rpm (3.17.2.327) - nokogiri (1.6.8.1) + net-ssh (4.1.0) + newrelic_rpm (4.1.0.333) + nokogiri (1.7.1) mini_portile2 (~> 2.1.0) nori (2.6.0) - oauth (0.5.0) - oauth2 (1.0.0) - faraday (>= 0.8, < 0.10) + oauth (0.5.1) + oauth2 (1.3.1) + faraday (>= 0.8, < 0.12) jwt (~> 1.0) multi_json (~> 1.3) multi_xml (~> 0.5) - rack (~> 1.2) - omniauth (1.3.1) - hashie (>= 1.2, < 4) - rack (>= 1.0, < 3) - omniauth-facebook (3.0.0) + rack (>= 1.2, < 3) + omniauth (1.6.1) + hashie (>= 3.4.6, < 3.6.0) + rack (>= 1.6.2, < 3) + omniauth-facebook (4.0.0) omniauth-oauth2 (~> 1.2) omniauth-google-oauth2 (0.4.1) jwt (~> 1.5.2) @@ -268,24 +275,31 @@ GEM omniauth-oauth2 (1.4.0) oauth2 (~> 1.0) omniauth (~> 1.2) - omniauth-twitter (1.2.1) - json (~> 1.3) + omniauth-twitter (1.4.0) omniauth-oauth (~> 1.1) + rack orm_adapter (0.5.0) - paranoia (2.2.0) - activerecord (>= 4.0, < 5.1) - parser (2.3.3.1) + paperclip (5.1.0) + activemodel (>= 4.2.0) + activesupport (>= 4.2.0) + cocaine (~> 0.5.5) + mime-types + mimemagic (~> 0.3.0) + paranoia (2.3.1) + activerecord (>= 4.0, < 5.2) + parser (2.4.0.0) ast (~> 2.2) - pg (0.19.0) - pg_search (1.0.6) - activerecord (>= 3.1) - activesupport (>= 3.1) - arel - poltergeist (1.10.0) + pg (0.20.0) + pg_search (2.0.1) + activerecord (>= 4.2) + activesupport (>= 4.2) + arel (>= 6) + poltergeist (1.15.0) capybara (~> 2.1) cliver (~> 0.3.1) websocket-driver (>= 0.2.0) powerpack (0.1.1) + public_suffix (2.0.5) quiet_assets (1.1.0) railties (>= 3.1, < 5.0) rack (1.6.5) @@ -295,60 +309,62 @@ GEM rack rack-test (0.6.3) rack (>= 1.0) - rails (4.2.7.1) - actionmailer (= 4.2.7.1) - actionpack (= 4.2.7.1) - actionview (= 4.2.7.1) - activejob (= 4.2.7.1) - activemodel (= 4.2.7.1) - activerecord (= 4.2.7.1) - activesupport (= 4.2.7.1) + rails (4.2.8) + actionmailer (= 4.2.8) + actionpack (= 4.2.8) + actionview (= 4.2.8) + activejob (= 4.2.8) + activemodel (= 4.2.8) + activerecord (= 4.2.8) + activesupport (= 4.2.8) bundler (>= 1.3.0, < 2.0) - railties (= 4.2.7.1) + railties (= 4.2.8) sprockets-rails rails-deprecated_sanitizer (1.0.3) activesupport (>= 4.2.0.alpha) - rails-dom-testing (1.0.7) + rails-dom-testing (1.0.8) activesupport (>= 4.2.0.beta, < 5.0) - nokogiri (~> 1.6.0) + nokogiri (~> 1.6) rails-deprecated_sanitizer (>= 1.0.1) rails-html-sanitizer (1.0.3) loofah (~> 2.0) - railties (4.2.7.1) - actionpack (= 4.2.7.1) - activesupport (= 4.2.7.1) + railties (4.2.8) + actionpack (= 4.2.8) + activesupport (= 4.2.8) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) - rainbow (2.1.0) - raindrops (0.17.0) + rainbow (2.2.2) + rake + raindrops (0.18.0) rake (12.0.0) - redcarpet (3.3.4) + redcarpet (3.4.0) referer-parser (0.3.0) - request_store (1.3.1) - responders (2.3.0) - railties (>= 4.2.0, < 5.1) + request_store (1.3.2) + responders (2.4.0) + actionpack (>= 4.2.0, < 5.3) + railties (>= 4.2.0, < 5.3) rinku (2.0.2) - rollbar (2.14.0) + rollbar (2.14.1) multi_json - rspec-core (3.5.4) - rspec-support (~> 3.5.0) - rspec-expectations (3.5.0) + rspec-core (3.6.0) + rspec-support (~> 3.6.0) + rspec-expectations (3.6.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.5.0) - rspec-mocks (3.5.0) + rspec-support (~> 3.6.0) + rspec-mocks (3.6.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.5.0) - rspec-rails (3.5.2) + rspec-support (~> 3.6.0) + rspec-rails (3.6.0) actionpack (>= 3.0) activesupport (>= 3.0) railties (>= 3.0) - rspec-core (~> 3.5.0) - rspec-expectations (~> 3.5.0) - rspec-mocks (~> 3.5.0) - rspec-support (~> 3.5.0) - rspec-support (3.5.0) - rubocop (0.45.0) - parser (>= 2.3.1.1, < 3.0) + rspec-core (~> 3.6.0) + rspec-expectations (~> 3.6.0) + rspec-mocks (~> 3.6.0) + rspec-support (~> 3.6.0) + rspec-support (3.6.0) + rubocop (0.48.1) + parser (>= 2.3.3.1, < 3.0) powerpack (~> 0.1) rainbow (>= 1.99.1, < 3.0) ruby-progressbar (~> 1.7) @@ -358,9 +374,9 @@ GEM capistrano (~> 3.0) sshkit (>= 1.2) safe_yaml (1.0.4) - safely_block (0.1.1) + safely_block (0.2.0) errbase - sass (3.4.22) + sass (3.4.23) sass-rails (5.0.6) railties (>= 4.0.0, < 6) sass (~> 3.1) @@ -375,17 +391,17 @@ GEM nokogiri (>= 1.4.0) nori (~> 2.4) wasabi (~> 3.4) - simplecov (0.12.0) + simplecov (0.14.1) docile (~> 1.1.0) json (>= 1.8, < 3) simplecov-html (~> 0.10.0) simplecov-html (0.10.0) - sitemap_generator (5.2.0) + sitemap_generator (5.3.1) builder (~> 3.0) - social-share-button (0.3.1) + social-share-button (0.10.0) coffee-rails - sass-rails - spring (1.7.2) + spring (2.0.1) + activesupport (>= 4.2) spring-commands-rspec (1.0.4) spring (>= 0.9.1) sprockets (3.7.1) @@ -399,19 +415,19 @@ GEM actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) - sshkit (1.11.4) + sshkit (1.13.1) net-scp (>= 1.1.2) net-ssh (>= 2.8.0) - term-ansicolor (1.4.0) + term-ansicolor (1.6.0) tins (~> 1.0) terminal-table (1.7.3) unicode-display_width (~> 1.1.1) thor (0.19.4) thread (0.2.2) - thread_safe (0.3.5) - tilt (2.0.5) + thread_safe (0.3.6) + tilt (2.0.7) timecop (0.8.1) - tins (1.13.0) + tins (1.13.2) tolk (2.0.0) rails (>= 4.0) safe_yaml (>= 0.8.6) @@ -422,12 +438,12 @@ GEM rack (>= 1.3, < 3) rack-accept (~> 0.4) tilt (>= 1.4, < 3) - tzinfo (1.2.2) + tzinfo (1.2.3) thread_safe (~> 0.1) - uglifier (3.0.4) + uglifier (3.2.0) execjs (>= 0.3.0, < 3) - unicode-display_width (1.1.1) - unicorn (5.2.0) + unicode-display_width (1.1.3) + unicorn (5.3.0) kgio (~> 2.6) raindrops (~> 0.7) uniform_notifier (1.10.0) @@ -442,7 +458,7 @@ GEM activemodel (>= 4.2) debug_inspector railties (>= 4.2) - websocket-driver (0.6.4) + websocket-driver (0.6.5) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.2) whenever (0.9.7) @@ -456,20 +472,20 @@ PLATFORMS DEPENDENCIES acts-as-taggable-on acts_as_votable - ahoy_matey (~> 1.5.3) + ahoy_matey (~> 1.6.0) ancestry (~> 2.2.2) browser - bullet + bullet (~> 5.5.1) byebug - cancancan - capistrano (= 3.5.0) + cancancan (~> 1.16.0) + capistrano (~> 3.8.1) capistrano-bundler (~> 1.2) - capistrano-rails (= 1.1.8) - capistrano3-delayed-job (~> 1.0) - capybara - ckeditor (~> 4.2.2) + capistrano-rails (~> 1.2.3) + capistrano3-delayed-job (~> 1.7.3) + capybara (~> 2.14.0) + ckeditor (~> 4.2.3) coffee-rails (~> 4.2.1) - coveralls + coveralls (~> 0.8.21) daemons dalli database_cleaner @@ -478,52 +494,56 @@ DEPENDENCIES devise-async devise_security_extension email_spec - factory_girl_rails - faker + factory_girl_rails (~> 4.8.0) + faker (~> 1.7.3) foundation-rails (~> 6.2.4.0) foundation_rails_helper (~> 2.0.0) fuubar graphiql-rails graphql (~> 1.3.0) - groupdate (~> 3.1.0) - i18n-tasks + groupdate (~> 3.2.0) + i18n-tasks (~> 0.9.15) initialjs-rails (= 0.2.0.4) invisible_captcha (~> 0.9.2) - jquery-rails (~> 4.2.2) + jquery-rails (~> 4.3.1) jquery-ui-rails - kaminari + kaminari (~> 1.0.1) knapsack launchy - letter_opener_web (~> 1.3.0) - newrelic_rpm (~> 3.17.2.327) + letter_opener_web (~> 1.3.1) + newrelic_rpm (~> 4.1.0.333) omniauth - omniauth-facebook (~> 3.0.0) + omniauth-facebook (~> 4.0.0) omniauth-google-oauth2 (~> 0.4.0) omniauth-twitter - paranoia (~> 2.2.0) - pg (~> 0.19.0) + paperclip + paranoia (~> 2.3.1) + pg (~> 0.20.0) pg_search - poltergeist + poltergeist (~> 1.15.0) quiet_assets - rails (= 4.2.7.1) - redcarpet - responders (~> 2.3.0) + rails (= 4.2.8) + redcarpet (~> 3.4.0) + responders (~> 2.4.0) rinku (~> 2.0.2) - rollbar (~> 2.14.0) - rspec-rails (~> 3.5) - rubocop (~> 0.45.0) + rollbar (~> 2.14.1) + rspec-rails (~> 3.6) + rubocop (~> 0.48.1) rvm1-capistrano3 sass-rails (~> 5.0, >= 5.0.4) savon - sitemap_generator - social-share-button + sitemap_generator (~> 5.3.1) + social-share-button (~> 0.10) spring spring-commands-rspec sprockets (~> 3.7.1) tolk (~> 2.0.0) turbolinks turnout (~> 2.4.0) - uglifier (>= 3.0.4) - unicorn (~> 5.2.0) + uglifier (~> 3.2.0) + unicorn (~> 5.3.0) web-console (= 3.3.0) whenever + +BUNDLED WITH + 1.14.6 diff --git a/README.md b/README.md index c62fcfeec..20ed21003 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,19 @@ -![Logo of Consul] -(https://raw.githubusercontent.com/consul/consul/master/public/consul_logo.png) +![Logo of Consul](https://raw.githubusercontent.com/consul/consul/master/public/consul_logo.png) # Consul Citizen Participation and Open Government Application -[![Join the chat at https://gitter.im/consul/consul](https://badges.gitter.im/consul/consul.svg)](https://gitter.im/consul/consul?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Build Status](https://travis-ci.org/consul/consul.svg?branch=master)](https://travis-ci.org/consul/consul) [![Code Climate](https://codeclimate.com/github/consul/consul/badges/gpa.svg)](https://codeclimate.com/github/consul/consul) [![Dependency Status](https://gemnasium.com/consul/consul.svg)](https://gemnasium.com/consul/consul) [![Coverage Status](https://coveralls.io/repos/github/consul/consul/badge.svg?branch=master)](https://coveralls.io/github/consul/consul?branch=master) +[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](http://www.gnu.org/licenses/agpl-3.0) + +[![Accessibility conformance](https://img.shields.io/badge/accessibility-WAI:AA-green.svg)](https://www.w3.org/WAI/eval/Overview) +[![A11y issues checked with Rocket Validator](https://rocketvalidator.com/badges/checked_with_rocket_validator.svg?url=https://rocketvalidator.com)](https://rocketvalidator.com/opensource) + +[![Join the chat at https://gitter.im/consul/consul](https://badges.gitter.im/consul/consul.svg)](https://gitter.im/consul/consul?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) This is the opensource code repository of the eParticipation website originally developed for the Madrid City government eParticipation website diff --git a/README_ES.md b/README_ES.md index 1b197e983..2c1e362c3 100644 --- a/README_ES.md +++ b/README_ES.md @@ -1,15 +1,20 @@ -![Logotipo de Consul] -(https://raw.githubusercontent.com/consul/consul/master/public/consul_logo.png) +![Logotipo de Consul](https://raw.githubusercontent.com/consul/consul/master/public/consul_logo.png) # Consul Aplicación de Participación Ciudadana y Gobierno Abierto -[![Join the chat at https://gitter.im/consul/consul](https://badges.gitter.im/consul/consul.svg)](https://gitter.im/consul/consul?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Build Status](https://travis-ci.org/consul/consul.svg?branch=master)](https://travis-ci.org/consul/consul) [![Code Climate](https://codeclimate.com/github/consul/consul/badges/gpa.svg)](https://codeclimate.com/github/consul/consul) [![Dependency Status](https://gemnasium.com/consul/consul.svg)](https://gemnasium.com/consul/consul) [![Coverage Status](https://coveralls.io/repos/github/consul/consul/badge.svg?branch=master)](https://coveralls.io/github/consul/consul?branch=master) +[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](http://www.gnu.org/licenses/agpl-3.0) + +[![Accessibility conformance](https://img.shields.io/badge/accessibility-WAI:AA-green.svg)](https://www.w3.org/WAI/eval/Overview) +[![A11y issues checked with Rocket Validator](https://rocketvalidator.com/badges/checked_with_rocket_validator.svg?url=https://rocketvalidator.com)](https://rocketvalidator.com/opensource) + +[![Join the chat at https://gitter.im/consul/consul](https://badges.gitter.im/consul/consul.svg)](https://gitter.im/consul/consul?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + Este es el repositorio de código abierto de la Aplicación de Participación Ciudadana Consul, creada originariamente por el Ayuntamiento de Madrid. diff --git a/app/assets/fonts/icons.eot b/app/assets/fonts/icons.eot index c3c5d6289..60cfbaddb 100644 Binary files a/app/assets/fonts/icons.eot and b/app/assets/fonts/icons.eot differ diff --git a/app/assets/fonts/icons.svg b/app/assets/fonts/icons.svg index a548d9575..f45adea5a 100644 --- a/app/assets/fonts/icons.svg +++ b/app/assets/fonts/icons.svg @@ -56,6 +56,9 @@ - + + + + diff --git a/app/assets/fonts/icons.ttf b/app/assets/fonts/icons.ttf index 6f938f863..8f791759e 100644 Binary files a/app/assets/fonts/icons.ttf and b/app/assets/fonts/icons.ttf differ diff --git a/app/assets/fonts/icons.woff b/app/assets/fonts/icons.woff index f3c31e804..0eef00837 100644 Binary files a/app/assets/fonts/icons.woff and b/app/assets/fonts/icons.woff differ diff --git a/app/assets/images/more_info/budgets_en.png b/app/assets/images/more_info/budgets_en.png new file mode 100644 index 000000000..1c1002145 Binary files /dev/null and b/app/assets/images/more_info/budgets_en.png differ diff --git a/app/assets/images/more_info/budgets_es.png b/app/assets/images/more_info/budgets_es.png new file mode 100644 index 000000000..89d93947b Binary files /dev/null and b/app/assets/images/more_info/budgets_es.png differ diff --git a/app/assets/images/more_info/budgets_fr.png b/app/assets/images/more_info/budgets_fr.png new file mode 100644 index 000000000..1c1002145 Binary files /dev/null and b/app/assets/images/more_info/budgets_fr.png differ diff --git a/app/assets/images/more_info/budgets_pt-BR.png b/app/assets/images/more_info/budgets_pt-BR.png new file mode 100644 index 000000000..1c1002145 Binary files /dev/null and b/app/assets/images/more_info/budgets_pt-BR.png differ diff --git a/app/assets/images/more_info/debates.png b/app/assets/images/more_info/debates.png new file mode 100644 index 000000000..30b195668 Binary files /dev/null and b/app/assets/images/more_info/debates.png differ diff --git a/app/assets/images/more_info/proposals_en.png b/app/assets/images/more_info/proposals_en.png new file mode 100644 index 000000000..60357f222 Binary files /dev/null and b/app/assets/images/more_info/proposals_en.png differ diff --git a/app/assets/images/more_info/proposals_es.png b/app/assets/images/more_info/proposals_es.png new file mode 100644 index 000000000..3466949bb Binary files /dev/null and b/app/assets/images/more_info/proposals_es.png differ diff --git a/app/assets/images/more_info/proposals_fr.png b/app/assets/images/more_info/proposals_fr.png new file mode 100644 index 000000000..60357f222 Binary files /dev/null and b/app/assets/images/more_info/proposals_fr.png differ diff --git a/app/assets/images/more_info/proposals_pt-BR.png b/app/assets/images/more_info/proposals_pt-BR.png new file mode 100644 index 000000000..60357f222 Binary files /dev/null and b/app/assets/images/more_info/proposals_pt-BR.png differ diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index fba539eeb..928f06edd 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -12,8 +12,8 @@ // //= require jquery //= require jquery_ujs -//= require jquery-ui/datepicker -//= require jquery-ui/datepicker-es +//= require jquery-ui/widgets/datepicker +//= require jquery-ui/i18n/datepicker-es //= require foundation //= require turbolinks //= require ckeditor/loader @@ -49,6 +49,7 @@ //= require fixed_bar //= require banners //= require social_share +//= require checkbox_toggle //= require custom var initialize_modules = function() { @@ -74,6 +75,7 @@ var initialize_modules = function() { App.FixedBar.initialize(); App.Banners.initialize(); App.SocialShare.initialize(); + App.CheckboxToggle.initialize(); }; $(function(){ diff --git a/app/assets/javascripts/checkbox_toggle.js.coffee b/app/assets/javascripts/checkbox_toggle.js.coffee new file mode 100644 index 000000000..096ce7e25 --- /dev/null +++ b/app/assets/javascripts/checkbox_toggle.js.coffee @@ -0,0 +1,12 @@ +App.CheckboxToggle = + + initialize: -> + $('[data-checkbox-toggle]').on 'change', -> + $this = $(this) + $target = $($this.data('checkbox-toggle')) + if $this.is(':checked') + $target.show() + else + $target.hide() + + diff --git a/app/assets/stylesheets/_settings.scss b/app/assets/stylesheets/_settings.scss index cc5bf015e..b804d62ba 100644 --- a/app/assets/stylesheets/_settings.scss +++ b/app/assets/stylesheets/_settings.scss @@ -80,6 +80,7 @@ $budget: #7E328A; $budget-hover: #7571BF; $highlight: #E7F2FC; +$light: #F5F7FA; $featured: #FFDC5C; $footer-border: #BFC1C3; diff --git a/app/assets/stylesheets/admin.scss b/app/assets/stylesheets/admin.scss index 90dee613a..e864d9781 100644 --- a/app/assets/stylesheets/admin.scss +++ b/app/assets/stylesheets/admin.scss @@ -5,6 +5,7 @@ // 03. List elements // 04. Stats // 05. Management +// 06. Polls // // 01. Global styles @@ -20,6 +21,11 @@ body.admin { .top-links { background: darken($admin-color, 15%); } + + .back-web { + padding-top: $line-height/4; + text-decoration: underline; + } } .top-bar { @@ -27,19 +33,37 @@ body.admin { height: auto; } + .top-bar-title { + + h1 { + margin-bottom: 0; + } + } + form { .button { margin-top: 0; + + &.margin-top { + margin-top: $line-height; + } } input[type="text"], textarea { width: 100%; } - .input-group input[type="text"] { - border-radius: 0; - margin-bottom: 0 !important; + .fieldset { + + select { + height: $line-height*2; + } + + .input-group input[type="text"] { + border-radius: 0; + margin-bottom: 0 !important; + } } } @@ -48,6 +72,15 @@ body.admin { th { text-align: left; + &.text-center { + text-align: center; + } + + &.text-right { + padding-right: $line-height; + text-align: right; + } + &.with-button { line-height: $line-height*2; } @@ -62,9 +95,20 @@ body.admin { } } + &.fixed { + table-layout: fixed; + } + input[type="submit"] ~ a, a ~ a { - margin-left: $line-height/2; - margin-right: $line-height/2; + margin-left: 0; + margin-right: 0; + margin-top: $line-height/2; + + @include breakpoint(medium) { + margin-left: $line-height/2; + margin-right: $line-height/2; + margin-top: 0; + } } } @@ -77,6 +121,11 @@ body.admin { color: $admin-color; } + .tabs-panel { + padding-left: 0; + padding-right: 0; + } + #proposals { width: 100% !important; } @@ -148,6 +197,13 @@ body.admin { } } +.input-group { + + .input-group-button { + padding-bottom: rem-calc(16); + } +} + // 02. Sidebar // ----------- @@ -155,7 +211,7 @@ body.admin { border-right: 1px solid $border; @include breakpoint(medium) { - padding-bottom: $line-height*3; + min-height: rem-calc(1100); } ul { @@ -165,10 +221,12 @@ body.admin { padding: 0; [class^="icon-"] { + color: $admin-color; display: inline-block; font-size: rem-calc(24); - padding-right: rem-calc(12); - padding-top: rem-calc(4); + line-height: $line-height; + padding: $line-height/2 $line-height/4; + padding-left: 0; vertical-align: middle; } @@ -177,19 +235,26 @@ body.admin { margin: 0; outline: 0; + ul { + margin-left: $line-height/1.5; + border-left: 1px solid $border; + padding-left: $line-height/2; + } + + &.section-title { + border-bottom: 1px solid $border; + } + &.active a { background: #f3f6f7; + border-radius: rem-calc(6); + -moz-border-radius: rem-calc(6); + -webkit-border-radius: rem-calc(6); color: $admin-color; font-weight: bold; } } - li.section { - border-bottom: 1px dotted #d5d5d5; - border-top: 1px dotted #d5d5d5; - height: $line-height/2; - } - li a { color: $text; display: block; @@ -199,10 +264,38 @@ body.admin { &:hover { background: #f3f6f7; + border-radius: rem-calc(6); + -moz-border-radius: rem-calc(6); + -webkit-border-radius: rem-calc(6); + color: $admin-color; text-decoration: none; } } } + + .is-accordion-submenu-parent { + + & > a::after { + border-color: $admin-color transparent transparent; + } + } + + .submenu { + border-bottom: 0; + margin-left: $line-height; + + li:first-child { + padding-top: $line-height/2; + } + + li:last-child { + padding-bottom: $line-height/2; + } + + a { + font-weight: normal; + } + } } // 03. List elements @@ -403,3 +496,49 @@ table.investment-projects-summary { white-space: nowrap; } } + +body.admin { + + .geozone { + background: #ececec; + border-radius: rem-calc(6); + color: $text; + display: inline-block; + font-size: $small-font-size; + margin-bottom: $line-height/3; + padding: $line-height/4 $line-height/3; + text-decoration: none; + + &:hover { + background: #e0e0e0; + } + } +} + +// 06. Polls +// ----------------- + +.count-error { + background: $alert-bg !important; + color: $color-alert; + font-weight: bold; +} + +table { + + .callout { + height: $line-height*2; + line-height: $line-height*2; + padding: 0 $line-height/2; + } +} + +// 07. CMS +// -------------- +.cms_page_list { + + [class^="icon-"] { + padding-right: $menu-icon-spacing; + vertical-align: middle; + } +} diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 36e055a4e..0720c103f 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -6,6 +6,7 @@ @import "admin"; @import "layout"; @import "participation"; +@import "pages"; @import "custom"; @import "c3"; @import "annotator.min"; diff --git a/app/assets/stylesheets/icons.scss b/app/assets/stylesheets/icons.scss index f3d8c003e..afbcc8bdd 100644 --- a/app/assets/stylesheets/icons.scss +++ b/app/assets/stylesheets/icons.scss @@ -163,6 +163,12 @@ .icon-whatsapp:before { content: "\50"; } +.icon-zip:before { + content: "\4f"; +} +.icon-banner:before { + content: "\51"; +} .icon-arrow-down:before { content: "\52"; } @@ -184,3 +190,6 @@ .icon-checkmark-circle:before { content: "\59"; } +.icon-telegram:before { + content: "\31"; +} diff --git a/app/assets/stylesheets/layout.scss b/app/assets/stylesheets/layout.scss index 7abaee6e2..b1aa65c90 100644 --- a/app/assets/stylesheets/layout.scss +++ b/app/assets/stylesheets/layout.scss @@ -70,6 +70,11 @@ a { } } +.button.hollow { + border: 1px solid $link; + color: $link; +} + .postfix.button { padding: 0; } @@ -139,6 +144,10 @@ a { padding-top: $line-height; } +.light { + background: $light; +} + .highlight { background: $highlight; } @@ -199,6 +208,30 @@ a { } } +.menu.vertical { + background: white; + margin: $line-height 0; + padding: $line-height; + + li { + margin-bottom: $line-height; + + a { + color: $text-medium; + padding: 0; + } + + h2 { + font-size: $base-font-size; + } + + &.active { + border-bottom: 2px solid $brand; + color: $brand; + } + } +} + .small { font-size: $small-font-size; } @@ -374,6 +407,24 @@ header { ul { margin-bottom: 0; + + li { + display: block; + } + + @include breakpoint(medium) { + li { + display: inline-block; + + &:after { + content: "|"; + } + + &:last-child:after { + content: none; + } + } + } } } @@ -439,6 +490,7 @@ header { .input-group-button { line-height: $line-height*1.5; + padding-bottom: 0; button { background: $border; @@ -460,7 +512,6 @@ header { } .submenu { - background: white; border-bottom: 1px solid $border; clear: both; margin-bottom: $line-height/2; @@ -536,12 +587,8 @@ footer { padding-left: 0; } - h2 a { - border-bottom: 1px solid $text-light; - display: block; - font-size: rem-calc(24); - line-height: rem-calc(31); - padding-bottom: $line-height/4; + a.title { + font-weight: bold; text-decoration: none; } } @@ -562,7 +609,8 @@ footer { // 04. Tags // -------- -.tags a , .tag-cloud a, .categories a, .geozone a, .sidebar-links a { +.tags a , .tag-cloud a, .categories a, .geozone a, .sidebar-links a, +.tags span { background: #ececec; border-radius: rem-calc(6); color: $text; @@ -1081,11 +1129,12 @@ img.avatar, img.admin-avatar, img.moderator-avatar, img.initialjs-avatar { // -------------------- [class^="level-"] { - color: white; + color: black; } .is-author { - background: #008CCF; + background: #00A5F1; + color: black; } .is-association { @@ -1143,7 +1192,11 @@ table { .button.button-twitter, .button.button-facebook, -.button.button-google { +.button.button-google, +.button.button-telegram { + background: white; + color: $text; + font-weight: bold; height: $line-height*2; line-height: $line-height*2; padding: 0; @@ -1151,9 +1204,11 @@ table { } .button.button-twitter { - background: #45B0E3; + background: #ECF7FC; + border-left: 3px solid #45B0E3; &:before { + color: #45B0E3; content: "f"; font-family: "icons" !important; font-size: rem-calc(24); @@ -1191,9 +1246,11 @@ table { } .button.button-facebook { - background: #3B5998; + background: #EBEEF4; + border-left: 3px solid #3B5998; &:before { + color: #3B5998; content: "A"; font-family: "icons" !important; font-size: rem-calc(24); @@ -1231,9 +1288,11 @@ table { } .button.button-google { - background: #DE4C34; + background: #FCEDEA; + border-left: 3px solid #DE4C34; &:before { + color: #DE4C34; content: "B"; font-family: "icons" !important; font-size: rem-calc(24); @@ -1270,6 +1329,55 @@ table { } } +.button.button-telegram { + background: #ECF7FC; + border-left: 3px solid #0088cc; + + &:before { + color: #0088cc; + content: "1"; + font-family: "icons" !important; + font-size: rem-calc(24); + left: 0; + line-height: $line-height*2; + padding: 0 rem-calc(20); + position: absolute; + top: 0; + } +} + +.ssb-telegram { + background: #0088cc; + background-image: none !important; + color: white; + height: $line-height*2 !important; + position: relative; + width: $line-height*2 !important; + + &:before { + content: "1"; + font-family: "icons" !important; + font-size: rem-calc(24); + left: 50%; + line-height: $line-height*2; + margin-left: rem-calc(-11); + position: absolute; + top: 0; + } + + &:hover, &:focus { + background: white; + color: #40A2D1; + } +} + +@include breakpoint(medium) { + + .button.button-telegram, .ssb-telegram { + display: none !important; + } +} + .social { a { @@ -1361,6 +1469,30 @@ table { color: #CE3E26; } } + + .ssb-telegram { + background: #0088cc; + color: white; + height: $line-height; + position: relative; + width: $line-height*2; + + &:before { + content: "1"; + font-family: "icons" !important; + font-size: rem-calc(24); + left: 50%; + line-height: $line-height*2; + margin-left: rem-calc(-11); + position: absolute; + top: 0; + } + + &:hover, &:focus { + background: white; + color: #40A2D1; + } + } } // 13. Pages diff --git a/app/assets/stylesheets/pages.scss b/app/assets/stylesheets/pages.scss new file mode 100644 index 000000000..f8bf6c7cd --- /dev/null +++ b/app/assets/stylesheets/pages.scss @@ -0,0 +1,111 @@ +// Table of Contents +// +// 01. Header +// 02. Navigation +// 03. Content +// 04. Sidebar +// + +// 01. Header +// ---------------------- + +.jumbo { + margin-bottom: $line-height; + margin-top: rem-calc(-24); + padding-bottom: $line-height; + padding-top: $line-height; + + &.light { + background: #ECF0F1; + } +} + +.lead { + font-size: rem-calc(24); +} + +// 03. Navigation +// ---------------------- + +.menu-pages { + list-style-type: none; + margin: 0; + + li { + display: block; + + @include breakpoint(medium) { + display: inline-block; + margin-right: $line-height/2; + } + } +} + +// 03. Content +// ---------------------- + +.more-info-content { + + h3 { + color: $brand; + } + + .additional-info { + margin-bottom: $line-height; + } + + a:not(.button) { + text-decoration: underline; + } + + figure { + margin: 0; + text-align: center; + + figcaption { + font-size: $small-font-size; + font-style: italic; + } + } + + ul.features { + list-style-type: circle; + margin-left: $line-height; + + @include breakpoint(medium) { + margin: $line-height 0 $line-height $line-height*2.5; + } + + li { + margin-bottom: $line-height + } + } + + .section-content { + border-top: 1px solid $medium-gray; + padding-bottom: $line-height*2; + padding-top: $line-height*2; + + &:first-child { + border-top: 0; + padding-top: 0; + } + } +} + +// 04. Sidebar +// ---------------------- + +.more-info-sidebar { + + .sidebar-card { + border: 1px solid $border; + margin-bottom: $line-height; + padding: $line-height/2; + + &.light { + background: #ECF0F1; + border: 0; + } + } +} diff --git a/app/assets/stylesheets/participation.scss b/app/assets/stylesheets/participation.scss index c72b981c7..693e45184 100644 --- a/app/assets/stylesheets/participation.scss +++ b/app/assets/stylesheets/participation.scss @@ -6,7 +6,8 @@ // 04. List participation // 05. Featured // 06. Budget -// 07. Proposals successfull +// 07. Proposals successful +// 08. Polls // // 01. Votes and supports @@ -297,7 +298,8 @@ .debate-show, .proposal-show, .investment-project-show, -.budget-investment-show { +.budget-investment-show, +.polls-show { p { word-wrap: break-word; @@ -455,7 +457,7 @@ } .bullet { - color: $border; + color: $text; } .investment-project-show p, .budget-investment-show p { @@ -769,7 +771,7 @@ // ------------ .featured-debates, .featured-proposals, -.proposals-ballot, .proposals-ballot-list { +.enquiries-list { padding: $line-height/2 0; @include breakpoint(medium) { @@ -974,6 +976,10 @@ &.social-share-button-google_plus:hover { color: #CE3E26; } + + &.social-share-button-telegram:hover { + color: #CE3E26; + } } } @@ -1058,13 +1064,16 @@ } } - h3.subtitle { - border-bottom: 3px solid $budget; + .ballot-content { + border: 2px solid #F9F9F9; + border-radius: rem-calc(6); + padding: $line-height/2; + } - span { - font-size: $base-font-size; - font-weight: normal; - } + .subtitle { + border-left: 2px solid $budget; + margin: $line-height/2 0; + padding-left: $line-height/2; } .amount-spent { @@ -1087,7 +1096,7 @@ ul.ballot-list { background: #f9f9f9; line-height: $line-height; margin-bottom: $line-height/4; - padding: $line-height/2; + padding: $line-height $line-height/2; position: relative; a { @@ -1095,7 +1104,6 @@ ul.ballot-list { } span { - color: #9f9f9f; display: block; font-style: italic; } @@ -1202,7 +1210,7 @@ ul.ballot-list { } } -// 07. Proposals successfull +// 07. Proposals successful // ------------------------- .dark-heading { @@ -1235,7 +1243,7 @@ ul.ballot-list { } } -.featured-proposals-ballot-banner { +.featured-proposals-ballot-banner, .sucessfull-proposals-banner { background: #2D3E50 image-url("ballot_tiny.gif") no-repeat; background-position: 75% 0; position: relative; @@ -1259,10 +1267,10 @@ ul.ballot-list { } } -.featured-proposals-ballot-banner, -.successfull .panel { +.sucessfull-proposals-banner, +.successful .panel { - .icon-successfull { + .icon-successful { border-right: 60px solid #FFD200; border-top: 0; border-bottom: 60px solid transparent; @@ -1283,17 +1291,7 @@ ul.ballot-list { } } -.proposals-ballot-list { - - .proposal-sucessfull { - background: white; - border-top: 1px solid $border; - padding: $line-height 0; - position: relative; - } -} - -.successfull { +.successful { .panel { position: relative; @@ -1314,3 +1312,199 @@ ul.ballot-list { } } } + +// 08. Polls +// ---------------------- + +.dark-heading { + background: #2D3E50; + color: white; + + .title { + color: #92BA48; + } + + .button { + background: white; + color: $brand; + } + + .callout { + + &.warning a { + color: $color-warning; + } + + &.primary a { + color: $color-info; + } + + &.alert a { + color: $color-alert; + } + } + + .info { + background: #314253; + padding: $line-height; + + @include breakpoint(medium) { + border-top: rem-calc(6) solid #92BA48; + } + } + + a:not(.button) { + color: white; + text-decoration: underline; + } + + .back, .icon-angle-left { + color: white; + } + + &.polls-show-header { + min-height: $line-height*8; + } +} + +.poll, .poll-question { + background: white; + border-radius: rem-calc(6); + margin-bottom: $line-height/2; +} + +.poll { + padding: $line-height; + position: relative; + + .icon-poll-answer { + border-top: 0; + border-bottom: 60px solid transparent; + height: 0; + position: absolute; + right: 0; + top: 0; + width: 0; + + &.can-answer:after, + &.cant-answer:after, + &.not-logged-in:after, + &.already-answer:after, + &.unverified:after { + font-family: "icons" !important; + left: 34px; + position: absolute; + top: 5px; + } + + &.can-answer { + border-right: 60px solid $info-bg; + + &:after { + color: $color-info; + content: "\6c"; + } + } + + &.cant-answer { + border-right: 60px solid $alert-bg; + + &:after { + color: $color-alert; + content: "\74"; + } + } + + &.not-logged-in { + border-right: 60px solid $info-bg; + + &:after { + color: $color-info; + content: "\6f"; + } + } + + &.unverified { + border-right: 60px solid $warning-bg; + + &:after { + color: $color-warning; + content: "\6f"; + } + } + + &.already-answer { + border-right: 60px solid $success-bg; + + &:after { + color: $color-success; + content: "\59"; + } + } + } + + .dates { + color: $text-medium; + font-size: $small-font-size; + margin-bottom: $line-height/2; + } + + h4 { + font-size: rem-calc(30); + line-height: $line-height*1.5; + + a { + color: $text; + } + } +} + +h2.questions-callout { + font-size: $base-font-size; +} + +h3.section-title-divider { + border-bottom: 2px solid $brand; + color: $brand; + margin-bottom: $line-height; +} + +.poll-question { + padding: 0 $line-height; + + h3 { + padding-top: $line-height; + + a { + color: $text; + } + } +} + +.poll-question-answers { + + .button { + margin-right: $line-height/4; + min-width: rem-calc(168); + + &.answered { + background: #F4F8EC; + border: 2px solid #92BA48; + color: $text; + position: relative; + + &:after { + background: #92BA48; + border-radius: rem-calc(20); + content: "\6c"; + color: white; + font-family: "icons" !important; + font-size: rem-calc(12); + padding: $line-height/4; + position: absolute; + right: -6px; + top: -6px; + } + } + } +} diff --git a/app/controllers/admin/api/stats_controller.rb b/app/controllers/admin/api/stats_controller.rb index e58d9d754..fe8c72cdd 100644 --- a/app/controllers/admin/api/stats_controller.rb +++ b/app/controllers/admin/api/stats_controller.rb @@ -1,9 +1,10 @@ class Admin::Api::StatsController < Admin::Api::BaseController def show - unless params[:events].present? || - params[:visits].present? || - params[:spending_proposals].present? + unless params[:events].present? || + params[:visits].present? || + params[:spending_proposals].present? || + params[:budget_investments].present? return render json: {}, status: :bad_request end @@ -24,6 +25,10 @@ class Admin::Api::StatsController < Admin::Api::BaseController ds.add "Spending proposals", SpendingProposal.group_by_day(:created_at).count end + if params[:budget_investments].present? + ds.add "Budget Investments", Budget::Investment.group_by_day(:created_at).count + end + render json: ds.build end end diff --git a/app/controllers/admin/poll/booth_assignments_controller.rb b/app/controllers/admin/poll/booth_assignments_controller.rb new file mode 100644 index 000000000..f81382bf8 --- /dev/null +++ b/app/controllers/admin/poll/booth_assignments_controller.rb @@ -0,0 +1,66 @@ +class Admin::Poll::BoothAssignmentsController < Admin::BaseController + + before_action :load_poll, except: [:create, :destroy] + + def index + @booth_assignments = @poll.booth_assignments.includes(:booth).order('poll_booths.name').page(params[:page]).per(50) + end + + def search_booths + load_search + @booths = ::Poll::Booth.search(@search) + respond_to do |format| + format.js + end + end + + def show + @booth_assignment = @poll.booth_assignments.includes(:recounts, :final_recounts, :voters, officer_assignments: [officer: [:user]]).find(params[:id]) + @voters_by_date = @booth_assignment.voters.group_by {|v| v.created_at.to_date} + end + + def create + @booth_assignment = ::Poll::BoothAssignment.new(poll_id: booth_assignment_params[:poll_id], booth_id: booth_assignment_params[:booth_id]) + + if @booth_assignment.save + notice = t("admin.poll_booth_assignments.flash.create") + else + notice = t("admin.poll_booth_assignments.flash.error_create") + end + redirect_to admin_poll_booth_assignments_path(@booth_assignment.poll_id), notice: notice + end + + def destroy + @booth_assignment = ::Poll::BoothAssignment.find(params[:id]) + + if @booth_assignment.destroy + notice = t("admin.poll_booth_assignments.flash.destroy") + else + notice = t("admin.poll_booth_assignments.flash.error_destroy") + end + redirect_to admin_poll_booth_assignments_path(@booth_assignment.poll_id), notice: notice + end + + private + + def load_booth_assignment + @booth_assignment = ::Poll::BoothAssignment.find(params[:id]) + end + + def booth_assignment_params + params.permit(:booth_id, :poll_id) + end + + def load_poll + @poll = ::Poll.find(params[:poll_id]) + end + + def search_params + params.permit(:poll_id, :search) + end + + def load_search + @search = search_params[:search] + end + +end \ No newline at end of file diff --git a/app/controllers/admin/poll/booths_controller.rb b/app/controllers/admin/poll/booths_controller.rb new file mode 100644 index 000000000..ff3700436 --- /dev/null +++ b/app/controllers/admin/poll/booths_controller.rb @@ -0,0 +1,39 @@ +class Admin::Poll::BoothsController < Admin::BaseController + load_and_authorize_resource class: 'Poll::Booth' + + def index + @booths = @booths.order(name: :asc).page(params[:page]) + end + + def show + end + + def new + end + + def create + if @booth.save + redirect_to admin_booths_path, notice: t("flash.actions.create.poll_booth") + else + render :new + end + end + + def edit + end + + def update + if @booth.update(booth_params) + redirect_to admin_booth_path(@booth), notice: t("flash.actions.update.poll_booth") + else + render :edit + end + end + + private + + def booth_params + params.require(:poll_booth).permit(:name, :location) + end + +end \ No newline at end of file diff --git a/app/controllers/admin/poll/officer_assignments_controller.rb b/app/controllers/admin/poll/officer_assignments_controller.rb new file mode 100644 index 000000000..f37ef9ab8 --- /dev/null +++ b/app/controllers/admin/poll/officer_assignments_controller.rb @@ -0,0 +1,92 @@ +class Admin::Poll::OfficerAssignmentsController < Admin::BaseController + + before_action :load_poll + before_action :redirect_if_blank_required_params, only: [:by_officer] + before_action :load_booth_assignment, only: [:create] + + def index + @officers = ::Poll::Officer. + includes(:user). + order('users.username'). + where( + id: @poll.officer_assignments.select(:officer_id).distinct.map(&:officer_id) + ).page(params[:page]).per(50) + end + + def by_officer + @poll = ::Poll.includes(:booths).find(params[:poll_id]) + @officer = ::Poll::Officer.includes(:user).find(officer_assignment_params[:officer_id]) + @officer_assignments = ::Poll::OfficerAssignment. + joins(:booth_assignment). + includes(:recount, :final_recounts, booth_assignment: :booth). + where("officer_id = ? AND poll_booth_assignments.poll_id = ?", @officer.id, @poll.id). + order(:date) + end + + def search_officers + load_search + @officers = User.joins(:poll_officer).search(@search).order(username: :asc) + + respond_to do |format| + format.js + end + end + + def create + @officer_assignment = ::Poll::OfficerAssignment.new(booth_assignment: @booth_assignment, + officer_id: create_params[:officer_id], + date: create_params[:date]) + @officer_assignment.final = true if @officer_assignment.date > @booth_assignment.poll.ends_at.to_date + + if @officer_assignment.save + notice = t("admin.poll_officer_assignments.flash.create") + else + notice = t("admin.poll_officer_assignments.flash.error_create") + end + redirect_to by_officer_admin_poll_officer_assignments_path(poll_id: create_params[:poll_id], officer_id: create_params[:officer_id]), notice: notice + end + + def destroy + @officer_assignment = ::Poll::OfficerAssignment.includes(:booth_assignment).find(params[:id]) + + if @officer_assignment.destroy + notice = t("admin.poll_officer_assignments.flash.destroy") + else + notice = t("admin.poll_officer_assignments.flash.error_destroy") + end + redirect_to by_officer_admin_poll_officer_assignments_path(poll_id: @officer_assignment.poll_id, officer_id: @officer_assignment.officer_id), notice: notice + end + + private + + def officer_assignment_params + params.permit(:officer_id) + end + + def create_params + params.permit(:poll_id, :booth_id, :date, :officer_id) + end + + def load_booth_assignment + @booth_assignment = ::Poll::BoothAssignment.includes(:poll).find_by(poll_id: create_params[:poll_id], booth_id: create_params[:booth_id]) + end + + def load_poll + @poll = ::Poll.find(params[:poll_id]) + end + + def redirect_if_blank_required_params + if officer_assignment_params[:officer_id].blank? + redirect_to admin_poll_path(@poll) + end + end + + def search_params + params.permit(:poll_id, :search) + end + + def load_search + @search = search_params[:search] + end + +end \ No newline at end of file diff --git a/app/controllers/admin/poll/officers_controller.rb b/app/controllers/admin/poll/officers_controller.rb new file mode 100644 index 000000000..2641a12b5 --- /dev/null +++ b/app/controllers/admin/poll/officers_controller.rb @@ -0,0 +1,39 @@ +class Admin::Poll::OfficersController < Admin::BaseController + load_and_authorize_resource :officer, class: "Poll::Officer", except: [:edit, :show] + + def index + @officers = @officers.page(params[:page]) + end + + def search + @user = User.find_by(email: params[:email]) + + respond_to do |format| + if @user + @officer = Poll::Officer.find_or_initialize_by(user: @user) + format.js + else + format.js { render "user_not_found" } + end + end + end + + def create + @officer.user_id = params[:user_id] + @officer.save + + redirect_to admin_officers_path + end + + def destroy + @officer.destroy + redirect_to admin_officers_path + end + + def show + end + + def edit + end + +end \ No newline at end of file diff --git a/app/controllers/admin/poll/polls_controller.rb b/app/controllers/admin/poll/polls_controller.rb new file mode 100644 index 000000000..eda5736b0 --- /dev/null +++ b/app/controllers/admin/poll/polls_controller.rb @@ -0,0 +1,86 @@ +class Admin::Poll::PollsController < Admin::BaseController + load_and_authorize_resource + + before_action :load_search, only: [:search_booths, :search_questions, :search_officers] + before_action :load_geozones, only: [:new, :create, :edit, :update] + + def index + end + + def show + @poll = Poll.includes(:questions). + order('poll_questions.title'). + find(params[:id]) + end + + def new + end + + def create + if @poll.save + redirect_to [:admin, @poll], notice: t("flash.actions.create.poll") + else + render :new + end + end + + def edit + end + + def update + if @poll.update(poll_params) + redirect_to [:admin, @poll], notice: t("flash.actions.update.poll") + else + render :edit + end + end + + def add_question + question = ::Poll::Question.find(params[:question_id]) + + if question.present? + @poll.questions << question + notice = t("admin.polls.flash.question_added") + else + notice = t("admin.polls.flash.error_on_question_added") + end + redirect_to admin_poll_path(@poll), notice: notice + end + + def remove_question + question = ::Poll::Question.find(params[:question_id]) + + if @poll.questions.include? question + @poll.questions.delete(question) + notice = t("admin.polls.flash.question_removed") + else + notice = t("admin.polls.flash.error_on_question_removed") + end + redirect_to admin_poll_path(@poll), notice: notice + end + + def search_questions + @questions = ::Poll::Question.where("poll_id IS ? OR poll_id != ?", nil, @poll.id).search({search: @search}).order(title: :asc) + respond_to do |format| + format.js + end + end + + private + def load_geozones + @geozones = Geozone.all.order(:name) + end + + def poll_params + params.require(:poll).permit(:name, :starts_at, :ends_at, :geozone_restricted, geozone_ids: []) + end + + def search_params + params.permit(:poll_id, :search) + end + + def load_search + @search = search_params[:search] + end + +end diff --git a/app/controllers/admin/poll/questions_controller.rb b/app/controllers/admin/poll/questions_controller.rb new file mode 100644 index 000000000..ab7297a95 --- /dev/null +++ b/app/controllers/admin/poll/questions_controller.rb @@ -0,0 +1,64 @@ +class Admin::Poll::QuestionsController < Admin::BaseController + load_and_authorize_resource :poll + load_and_authorize_resource :question, class: 'Poll::Question' + + def index + @polls = Poll.all + @search = search_params[:search] + + @questions = @questions.search(search_params).page(params[:page]).order("created_at DESC") + + @proposals = Proposal.successful.sort_by_confidence_score + end + + def new + @polls = Poll.all + @question.valid_answers = I18n.t('poll_questions.default_valid_answers') + proposal = Proposal.find(params[:proposal_id]) if params[:proposal_id].present? + @question.copy_attributes_from_proposal(proposal) + end + + def create + @question.author = @question.proposal.try(:author) || current_user + + if @question.save + redirect_to admin_question_path(@question) + else + render :new + end + end + + def show + end + + def edit + end + + def update + if @question.update(question_params) + redirect_to admin_question_path(@question), notice: t("flash.actions.save_changes.notice") + else + render :edit + end + end + + def destroy + if @question.destroy + notice = "Question destroyed succesfully" + else + notice = t("flash.actions.destroy.error") + end + redirect_to admin_questions_path, notice: notice + end + + private + + def question_params + params.require(:poll_question).permit(:poll_id, :title, :question, :description, :proposal_id, :valid_answers) + end + + def search_params + params.permit(:poll_id, :search) + end + +end diff --git a/app/controllers/admin/poll/recounts_controller.rb b/app/controllers/admin/poll/recounts_controller.rb new file mode 100644 index 000000000..fec546d79 --- /dev/null +++ b/app/controllers/admin/poll/recounts_controller.rb @@ -0,0 +1,16 @@ +class Admin::Poll::RecountsController < Admin::BaseController + before_action :load_poll + + def index + @booth_assignments = @poll.booth_assignments. + includes(:booth, :recounts, :final_recounts, :voters). + order("poll_booths.name"). + page(params[:page]).per(50) + end + + private + + def load_poll + @poll = ::Poll.find(params[:poll_id]) + end +end \ No newline at end of file diff --git a/app/controllers/admin/poll/results_controller.rb b/app/controllers/admin/poll/results_controller.rb new file mode 100644 index 000000000..2c5bbba27 --- /dev/null +++ b/app/controllers/admin/poll/results_controller.rb @@ -0,0 +1,13 @@ +class Admin::Poll::ResultsController < Admin::BaseController + before_action :load_poll + + def index + @partial_results = @poll.partial_results + end + + private + + def load_poll + @poll = ::Poll.includes(:questions).find(params[:poll_id]) + end +end \ No newline at end of file diff --git a/app/controllers/admin/site_customization/base_controller.rb b/app/controllers/admin/site_customization/base_controller.rb new file mode 100644 index 000000000..18422f66e --- /dev/null +++ b/app/controllers/admin/site_customization/base_controller.rb @@ -0,0 +1,10 @@ +class Admin::SiteCustomization::BaseController < Admin::BaseController + helper_method :namespace + + private + + def namespace + "admin" + end + +end diff --git a/app/controllers/admin/site_customization/content_blocks_controller.rb b/app/controllers/admin/site_customization/content_blocks_controller.rb new file mode 100644 index 000000000..2f0843ccc --- /dev/null +++ b/app/controllers/admin/site_customization/content_blocks_controller.rb @@ -0,0 +1,40 @@ +class Admin::SiteCustomization::ContentBlocksController < Admin::SiteCustomization::BaseController + load_and_authorize_resource :content_block, class: "SiteCustomization::ContentBlock" + + def index + @content_blocks = SiteCustomization::ContentBlock.order(:name, :locale) + end + + def create + if @content_block.save + redirect_to admin_site_customization_content_blocks_path, notice: t('admin.site_customization.content_blocks.create.notice') + else + flash.now[:error] = t('admin.site_customization.content_blocks.create.error') + render :new + end + end + + def update + if @content_block.update(content_block_params) + redirect_to admin_site_customization_content_blocks_path, notice: t('admin.site_customization.content_blocks.update.notice') + else + flash.now[:error] = t('admin.site_customization.content_blocks.update.error') + render :edit + end + end + + def destroy + @content_block.destroy + redirect_to admin_site_customization_content_blocks_path, notice: t('admin.site_customization.content_blocks.destroy.notice') + end + + private + + def content_block_params + params.require(:site_customization_content_block).permit( + :name, + :locale, + :body + ) + end +end diff --git a/app/controllers/admin/site_customization/images_controller.rb b/app/controllers/admin/site_customization/images_controller.rb new file mode 100644 index 000000000..c9f318f41 --- /dev/null +++ b/app/controllers/admin/site_customization/images_controller.rb @@ -0,0 +1,43 @@ +class Admin::SiteCustomization::ImagesController < Admin::SiteCustomization::BaseController + load_and_authorize_resource :image, class: "SiteCustomization::Image" + + def index + @images = SiteCustomization::Image.all_images + end + + def update + if params[:site_customization_image].nil? + redirect_to admin_site_customization_images_path + return + end + + if @image.update(image_params) + redirect_to admin_site_customization_images_path, notice: t('admin.site_customization.images.update.notice') + else + flash.now[:error] = t('admin.site_customization.images.update.error') + + @images = SiteCustomization::Image.all_images + idx = @images.index {|e| e.name == @image.name } + @images[idx] = @image + + render :index + end + end + + def destroy + @image.image = nil + if @image.save + redirect_to admin_site_customization_images_path, notice: t('admin.site_customization.images.destroy.notice') + else + redirect_to admin_site_customization_images_path, notice: t('admin.site_customization.images.destroy.error') + end + end + + private + + def image_params + params.require(:site_customization_image).permit( + :image + ) + end +end diff --git a/app/controllers/admin/site_customization/pages_controller.rb b/app/controllers/admin/site_customization/pages_controller.rb new file mode 100644 index 000000000..4d92a6a1e --- /dev/null +++ b/app/controllers/admin/site_customization/pages_controller.rb @@ -0,0 +1,44 @@ +class Admin::SiteCustomization::PagesController < Admin::SiteCustomization::BaseController + load_and_authorize_resource :page, class: "SiteCustomization::Page" + + def index + @pages = SiteCustomization::Page.order('slug').page(params[:page]) + end + + def create + if @page.save + redirect_to admin_site_customization_pages_path, notice: t('admin.site_customization.pages.create.notice') + else + flash.now[:error] = t('admin.site_customization.pages.create.error') + render :new + end + end + + def update + if @page.update(page_params) + redirect_to admin_site_customization_pages_path, notice: t('admin.site_customization.pages.update.notice') + else + flash.now[:error] = t('admin.site_customization.pages.update.error') + render :edit + end + end + + def destroy + @page.destroy + redirect_to admin_site_customization_pages_path, notice: t('admin.site_customization.pages.destroy.notice') + end + + private + + def page_params + params.require(:site_customization_page).permit( + :slug, + :title, + :subtitle, + :content, + :more_info_flag, + :print_content_flag, + :status + ) + end +end diff --git a/app/controllers/admin/stats_controller.rb b/app/controllers/admin/stats_controller.rb index 3624174a5..9989da796 100644 --- a/app/controllers/admin/stats_controller.rb +++ b/app/controllers/admin/stats_controller.rb @@ -21,6 +21,9 @@ class Admin::StatsController < Admin::BaseController @user_ids_who_voted_proposals = ActsAsVotable::Vote.where(votable_type: 'Proposal').distinct.count(:voter_id) @user_ids_who_didnt_vote_proposals = @verified_users - @user_ids_who_voted_proposals @spending_proposals = SpendingProposal.count + budgets_ids = Budget.where.not(phase: 'finished').pluck(:id) + @budgets = budgets_ids.size + @investments = Budget::Investment.where(budget_id: budgets_ids).count end def proposal_notifications diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 3e1b78d31..495a25314 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -114,4 +114,10 @@ class ApplicationController < ActionController::Base store_location_for(:user, request.path) end end + + def set_default_budget_filter + if @budget.try(:balloting?) + params[:filter] ||= "selected" + end + end end diff --git a/app/controllers/budgets/ballot/lines_controller.rb b/app/controllers/budgets/ballot/lines_controller.rb index 2feb83e5e..522589dfe 100644 --- a/app/controllers/budgets/ballot/lines_controller.rb +++ b/app/controllers/budgets/ballot/lines_controller.rb @@ -7,8 +7,8 @@ module Budgets before_action :load_ballot before_action :load_tag_cloud before_action :load_categories - before_action :load_investments + before_action :load_ballot_referer load_and_authorize_resource :budget load_and_authorize_resource :ballot, class: "Budget::Ballot", through: :budget @@ -73,6 +73,10 @@ module Budgets @categories = ActsAsTaggableOn::Tag.where("kind = 'category'").order(:name) end + def load_ballot_referer + @ballot_referer = session[:ballot_referer] + end + end end end diff --git a/app/controllers/budgets/ballots_controller.rb b/app/controllers/budgets/ballots_controller.rb index ce531e145..b5b63b4aa 100644 --- a/app/controllers/budgets/ballots_controller.rb +++ b/app/controllers/budgets/ballots_controller.rb @@ -3,9 +3,11 @@ module Budgets before_action :authenticate_user! load_and_authorize_resource :budget before_action :load_ballot + after_action :store_referer, only: [:show] def show authorize! :show, @ballot + session[:ballot_referer] = request.referer render template: "budgets/ballot/show" end @@ -16,5 +18,9 @@ module Budgets @ballot = @budget.balloting? ? query.first_or_create : query.first_or_initialize end + def store_referer + session[:ballot_referer] = request.referer + end + end end diff --git a/app/controllers/budgets/groups_controller.rb b/app/controllers/budgets/groups_controller.rb index e55974a6a..d84eb2fdd 100644 --- a/app/controllers/budgets/groups_controller.rb +++ b/app/controllers/budgets/groups_controller.rb @@ -3,6 +3,9 @@ module Budgets load_and_authorize_resource :budget load_and_authorize_resource :group, class: "Budget::Group" + before_action :set_default_budget_filter, only: :show + has_filters %w{not_unfeasible feasible unfeasible unselected selected}, only: [:show] + def show end diff --git a/app/controllers/budgets/investments_controller.rb b/app/controllers/budgets/investments_controller.rb index ebf14d94b..7eb1b1291 100644 --- a/app/controllers/budgets/investments_controller.rb +++ b/app/controllers/budgets/investments_controller.rb @@ -14,18 +14,20 @@ module Budgets before_action :load_heading, only: [:index, :show] before_action :set_random_seed, only: :index before_action :load_categories, only: [:index, :new, :create] + before_action :set_default_budget_filter, only: :index feature_flag :budgets has_orders %w{most_voted newest oldest}, only: :show has_orders ->(c) { c.instance_variable_get(:@budget).investments_orders }, only: :index + has_filters %w{not_unfeasible feasible unfeasible unselected selected}, only: [:index, :show] invisible_captcha only: [:create, :update], honeypot: :subtitle, scope: :budget_investment respond_to :html, :js def index - @investments = @investments.apply_filters_and_search(@budget, params).send("sort_by_#{@current_order}").page(params[:page]).per(10).for_render + @investments = @investments.apply_filters_and_search(@budget, params, @current_filter).send("sort_by_#{@current_order}").page(params[:page]).per(10).for_render @investment_ids = @investments.pluck(:id) load_investment_votes(@investments) @tag_cloud = tag_cloud @@ -55,7 +57,7 @@ module Budgets end def destroy - investment.destroy + @investment.destroy redirect_to user_path(current_user, filter: 'budget_investments'), notice: t('flash.actions.destroy.budget_investment') end @@ -77,7 +79,8 @@ module Budgets def set_random_seed if params[:order] == 'random' || params[:order].blank? params[:random_seed] ||= rand(99)/100.0 - Budget::Investment.connection.execute "select setseed(#{params[:random_seed]})" + seed = Float(params[:random_seed]) rescue 0 + Budget::Investment.connection.execute("select setseed(#{seed})") else params[:random_seed] = nil end @@ -106,6 +109,7 @@ module Budgets def tag_cloud TagCloud.new(Budget::Investment, params[:search]) end + end end diff --git a/app/controllers/budgets_controller.rb b/app/controllers/budgets_controller.rb index ee26f3223..2a53c410b 100644 --- a/app/controllers/budgets_controller.rb +++ b/app/controllers/budgets_controller.rb @@ -3,6 +3,9 @@ class BudgetsController < ApplicationController feature_flag :budgets load_and_authorize_resource + before_action :set_default_budget_filter, only: :show + has_filters %w{not_unfeasible feasible unfeasible unselected selected}, only: :show + respond_to :html, :js def show diff --git a/app/controllers/concerns/commentable_actions.rb b/app/controllers/concerns/commentable_actions.rb index ef5e60dcb..287f2d418 100644 --- a/app/controllers/concerns/commentable_actions.rb +++ b/app/controllers/concerns/commentable_actions.rb @@ -1,6 +1,7 @@ module CommentableActions extend ActiveSupport::Concern include Polymorphic + include Search def index @resources = @search_terms.present? ? resource_model.search(@search_terms) : resource_model.all @@ -100,53 +101,6 @@ module CommentableActions end end - def parse_search_terms - @search_terms = params[:search] if params[:search].present? - end - - def parse_advanced_search_terms - @advanced_search_terms = params[:advanced_search] if params[:advanced_search].present? - parse_search_date - end - - def parse_search_date - return unless search_by_date? - params[:advanced_search][:date_range] = search_date_range - end - - def search_by_date? - params[:advanced_search] && params[:advanced_search][:date_min].present? - end - - def search_start_date - case params[:advanced_search][:date_min] - when '1' - 24.hours.ago - when '2' - 1.week.ago - when '3' - 1.month.ago - when '4' - 1.year.ago - else - Date.parse(params[:advanced_search][:date_min]) rescue nil - end - end - - def search_finish_date - params[:advanced_search][:date_max].try(:to_date) || Date.today - end - - def search_date_range - search_start_date.beginning_of_day..search_finish_date.end_of_day - end - - def set_search_order - if params[:search].present? && params[:order].blank? - params[:order] = 'relevance' - end - end - def set_resource_votes(instance) send("set_#{resource_name}_votes", instance) end diff --git a/app/controllers/concerns/search.rb b/app/controllers/concerns/search.rb new file mode 100644 index 000000000..922ad93cc --- /dev/null +++ b/app/controllers/concerns/search.rb @@ -0,0 +1,57 @@ +module Search + extend ActiveSupport::Concern + + included do + before_action :parse_search_terms, only: [:index, :suggest] + before_action :parse_advanced_search_terms, only: :index + before_action :set_search_order, only: :index + end + + def parse_search_terms + @search_terms = params[:search] if params[:search].present? + end + + def parse_advanced_search_terms + @advanced_search_terms = params[:advanced_search] if params[:advanced_search].present? + parse_search_date + end + + def parse_search_date + return unless search_by_date? + params[:advanced_search][:date_range] = search_date_range + end + + def search_by_date? + params[:advanced_search] && params[:advanced_search][:date_min].present? + end + + def search_start_date + case params[:advanced_search][:date_min] + when '1' + 24.hours.ago + when '2' + 1.week.ago + when '3' + 1.month.ago + when '4' + 1.year.ago + else + Date.parse(params[:advanced_search][:date_min]) rescue 100.years.ago + end + end + + def search_finish_date + (params[:advanced_search][:date_max].to_date rescue Date.today) || Date.today + end + + def search_date_range + [100.years.ago, search_start_date].max.beginning_of_day..[search_finish_date, Date.today].min.end_of_day + end + + def set_search_order + if params[:search].present? && params[:order].blank? + params[:order] = 'relevance' + end + end + +end \ No newline at end of file diff --git a/app/controllers/debates_controller.rb b/app/controllers/debates_controller.rb index 8262f014a..9b29080dd 100644 --- a/app/controllers/debates_controller.rb +++ b/app/controllers/debates_controller.rb @@ -3,10 +3,7 @@ class DebatesController < ApplicationController include CommentableActions include FlagActions - before_action :parse_search_terms, only: [:index, :suggest] - before_action :parse_advanced_search_terms, only: :index before_action :parse_tag_filter, only: :index - before_action :set_search_order, only: :index before_action :authenticate_user!, except: [:index, :show, :map] feature_flag :debates @@ -22,7 +19,7 @@ class DebatesController < ApplicationController def index_customization @featured_debates = @debates.featured - @proposal_successfull_exists = Proposal.successfull.exists? + @proposal_successfull_exists = Proposal.successful.exists? end def show diff --git a/app/controllers/management/budgets_controller.rb b/app/controllers/management/budgets_controller.rb index c5cfdee76..dd7259c99 100644 --- a/app/controllers/management/budgets_controller.rb +++ b/app/controllers/management/budgets_controller.rb @@ -7,6 +7,11 @@ class Management::BudgetsController < Management::BaseController def create_investments @budgets = Budget.accepting.order(created_at: :desc).page(params[:page]) + + if current_manager_administrator? + @budgets += Budget.reviewing.order(created_at: :desc) + + Budget.selecting.order(created_at: :desc) + end end def support_investments @@ -23,4 +28,8 @@ class Management::BudgetsController < Management::BaseController check_verified_user t("management.budget_investments.alert.unverified_user") end + def current_manager_administrator? + session[:manager]["login"].match("admin") + end + end diff --git a/app/controllers/management/users_controller.rb b/app/controllers/management/users_controller.rb index 53004a838..8d3d148fa 100644 --- a/app/controllers/management/users_controller.rb +++ b/app/controllers/management/users_controller.rb @@ -32,7 +32,7 @@ class Management::UsersController < Management::BaseController private def user_params - params.require(:user).permit(:document_type, :document_number, :username, :email) + params.require(:user).permit(:document_type, :document_number, :username, :email, :date_of_birth) end def destroy_session diff --git a/app/controllers/officing/base_controller.rb b/app/controllers/officing/base_controller.rb new file mode 100644 index 000000000..97ef23d30 --- /dev/null +++ b/app/controllers/officing/base_controller.rb @@ -0,0 +1,12 @@ +class Officing::BaseController < ApplicationController + layout 'admin' + + before_action :authenticate_user! + before_action :verify_officer + + skip_authorization_check + + def verify_officer + raise CanCan::AccessDenied unless current_user.try(:poll_officer?) || current_user.try(:administrator?) + end +end \ No newline at end of file diff --git a/app/controllers/officing/dashboard_controller.rb b/app/controllers/officing/dashboard_controller.rb new file mode 100644 index 000000000..4d80a974a --- /dev/null +++ b/app/controllers/officing/dashboard_controller.rb @@ -0,0 +1,6 @@ +class Officing::DashboardController < Officing::BaseController + + def index + end + +end diff --git a/app/controllers/officing/final_recounts_controller.rb b/app/controllers/officing/final_recounts_controller.rb new file mode 100644 index 000000000..e381240e7 --- /dev/null +++ b/app/controllers/officing/final_recounts_controller.rb @@ -0,0 +1,47 @@ +class Officing::FinalRecountsController < Officing::BaseController + before_action :load_poll + before_action :load_officer_assignment, only: :create + + def new + @officer_assignments = ::Poll::OfficerAssignment. + includes(:final_recounts, booth_assignment: [:booth]). + joins(:booth_assignment). + final. + where(id: current_user.poll_officer.officer_assignment_ids). + where("poll_booth_assignments.poll_id = ?", @poll.id). + order(date: :asc) + + @final_recounts = @officer_assignments.select {|oa| oa.final_recounts.any?}.map(&:final_recounts).flatten + end + + def create + @final_recount = ::Poll::FinalRecount.find_or_initialize_by(booth_assignment_id: @officer_assignment.booth_assignment_id, date: final_recount_params[:date]) + @final_recount.officer_assignment_id = @officer_assignment.id + @final_recount.count = final_recount_params[:count] + + if @final_recount.save + msg = { notice: t("officing.final_recounts.flash.create") } + else + msg = { alert: t("officing.final_recounts.flash.error_create") } + end + redirect_to new_officing_poll_final_recount_path(@poll), msg + end + + private + def load_poll + @poll = Poll.expired.find(params[:poll_id]) + end + + def load_officer_assignment + @officer_assignment = current_user.poll_officer. + officer_assignments.final.find_by(id: final_recount_params[:officer_assignment_id]) + if @officer_assignment.blank? + redirect_to new_officing_poll_final_recount_path(@poll), alert: t("officing.final_recounts.flash.error_create") + end + end + + def final_recount_params + params.permit(:officer_assignment_id, :count, :date) + end + +end \ No newline at end of file diff --git a/app/controllers/officing/polls_controller.rb b/app/controllers/officing/polls_controller.rb new file mode 100644 index 000000000..e122284ec --- /dev/null +++ b/app/controllers/officing/polls_controller.rb @@ -0,0 +1,15 @@ +class Officing::PollsController < Officing::BaseController + + def index + @polls = current_user.poll_officer? ? current_user.poll_officer.voting_days_assigned_polls : [] + @polls = @polls.select {|poll| poll.current?(Time.current) || poll.current?(1.day.ago)} + end + + def final + @polls = current_user.poll_officer? ? current_user.poll_officer.final_days_assigned_polls : [] + return unless current_user.poll_officer? + + @polls = @polls.select {|poll| poll.ends_at > 1.week.ago && poll.expired?} + end + +end \ No newline at end of file diff --git a/app/controllers/officing/recounts_controller.rb b/app/controllers/officing/recounts_controller.rb new file mode 100644 index 000000000..1c66e57fd --- /dev/null +++ b/app/controllers/officing/recounts_controller.rb @@ -0,0 +1,46 @@ +class Officing::RecountsController < Officing::BaseController + before_action :load_poll + before_action :load_officer_assignment, only: :create + + def new + @officer_assignments = ::Poll::OfficerAssignment. + includes(:recount, booth_assignment: :booth). + joins(:booth_assignment). + voting_days. + where(id: current_user.poll_officer.officer_assignment_ids). + where("poll_booth_assignments.poll_id = ?", @poll.id). + order(date: :asc) + @recounted = @officer_assignments.select {|oa| oa.recount.present?}.reverse + end + + def create + @recount = ::Poll::Recount.find_or_initialize_by(booth_assignment_id: @officer_assignment.booth_assignment_id, date: @officer_assignment.date) + @recount.officer_assignment_id = @officer_assignment.id + @recount.count = recount_params[:count] + + if @recount.save + msg = { notice: t("officing.recounts.flash.create") } + else + msg = { alert: t("officing.recounts.flash.error_create") } + end + redirect_to new_officing_poll_recount_path(@poll), msg + end + + private + def load_poll + @poll = Poll.find(params[:poll_id]) + end + + def load_officer_assignment + @officer_assignment = current_user.poll_officer. + officer_assignments.find_by(id: recount_params[:officer_assignment_id]) + if @officer_assignment.blank? + redirect_to new_officing_poll_recount_path(@poll), alert: t("officing.recounts.flash.error_create") + end + end + + def recount_params + params.permit(:officer_assignment_id, :count) + end + +end \ No newline at end of file diff --git a/app/controllers/officing/residence_controller.rb b/app/controllers/officing/residence_controller.rb new file mode 100644 index 000000000..9f24d3a90 --- /dev/null +++ b/app/controllers/officing/residence_controller.rb @@ -0,0 +1,37 @@ +class Officing::ResidenceController < Officing::BaseController + + before_action :load_officer_assignment + before_action :validate_officer_assignment, only: :create + + def new + @residence = Officing::Residence.new + end + + def create + @residence = Officing::Residence.new(residence_params.merge(officer: current_user.poll_officer)) + if @residence.save + redirect_to new_officing_voter_path(id: @residence.user.id), notice: t("officing.residence.flash.create") + else + render :new + end + end + + private + + def residence_params + params.require(:residence).permit(:document_number, :document_type, :year_of_birth) + end + + def load_officer_assignment + @officer_assignments = current_user.poll_officer. + officer_assignments. + voting_days. + where(date: Time.current.to_date) + end + + def validate_officer_assignment + if @officer_assignments.blank? + redirect_to officing_root_path, notice: t("officing.residence.flash.not_allowed") + end + end +end diff --git a/app/controllers/officing/results_controller.rb b/app/controllers/officing/results_controller.rb new file mode 100644 index 000000000..b6377e25b --- /dev/null +++ b/app/controllers/officing/results_controller.rb @@ -0,0 +1,141 @@ +class Officing::ResultsController < Officing::BaseController + before_action :load_poll + + before_action :load_officer_assignments, only: :new + before_action :load_partial_results, only: :new + + before_action :load_officer_assignment, only: :create + before_action :check_booth_and_date, only: :create + before_action :build_results, only: :create + + def new + end + + def create + @results.each { |result| result.save! } + + notice = t("officing.results.flash.create") + redirect_to new_officing_poll_result_path(@poll), notice: notice + end + + def index + @booth_assignment = ::Poll::BoothAssignment.includes(:booth).find(index_params[:booth_assignment_id]) + if current_user.poll_officer.officer_assignments.final. + where(booth_assignment_id: @booth_assignment.id).exists? + + @partial_results = ::Poll::PartialResult.includes(:question). + where(booth_assignment_id: index_params[:booth_assignment_id]). + where(date: index_params[:date]) + @whites = ::Poll::WhiteResult.where(booth_assignment_id: @booth_assignment.id, date: index_params[:date]).sum(:amount) + @nulls = ::Poll::NullResult.where(booth_assignment_id: @booth_assignment.id, date: index_params[:date]).sum(:amount) + end + end + + private + + def check_booth_and_date + if @officer_assignment.blank? + go_back_to_new(t("officing.results.flash.error_wrong_booth")) + elsif results_params[:date].blank? || + Date.parse(results_params[:date]) < @poll.starts_at.to_date || + Date.parse(results_params[:date]) > @poll.ends_at.to_date + go_back_to_new(t("officing.results.flash.error_wrong_date")) + end + end + + def build_results + @results = [] + + params[:questions].each_pair do |question_id, results| + question = @poll.questions.find(question_id) + go_back_to_new if question.blank? + + results.each_pair do |answer_index, count| + if count.present? + answer = question.valid_answers[answer_index.to_i] + go_back_to_new if question.blank? + + partial_result = ::Poll::PartialResult.find_or_initialize_by(booth_assignment_id: @officer_assignment.booth_assignment_id, + date: results_params[:date], + question_id: question_id, + answer: answer) + partial_result.officer_assignment_id = @officer_assignment.id + partial_result.amount = count.to_i + partial_result.author = current_user + partial_result.origin = 'booth' + @results << partial_result + end + end + end + + build_white_results + build_null_results + end + + def build_white_results + if results_params[:whites].present? + white_result = ::Poll::WhiteResult.find_or_initialize_by(booth_assignment_id: @officer_assignment.booth_assignment_id, + date: results_params[:date]) + white_result.officer_assignment_id = @officer_assignment.id + white_result.amount = results_params[:whites].to_i + white_result.author = current_user + white_result.origin = 'booth' + @results << white_result + end + end + + def build_null_results + if results_params[:nulls].present? + null_result = ::Poll::NullResult.find_or_initialize_by(booth_assignment_id: @officer_assignment.booth_assignment_id, + date: results_params[:date]) + null_result.officer_assignment_id = @officer_assignment.id + null_result.amount = results_params[:nulls].to_i + null_result.author = current_user + null_result.origin = 'booth' + @results << null_result + end + end + + def go_back_to_new(alert = nil) + params[:d] = results_params[:date] + params[:oa] = results_params[:officer_assignment_id] + flash.now[:alert] = (alert || t("officing.results.flash.error_create")) + load_officer_assignments + load_partial_results + render :new + end + + def load_poll + @poll = ::Poll.expired.includes(:questions).find(params[:poll_id]) + end + + def load_officer_assignment + @officer_assignment = current_user.poll_officer. + officer_assignments.final.find_by(id: results_params[:officer_assignment_id]) + end + + def load_officer_assignments + @officer_assignments = ::Poll::OfficerAssignment. + includes(booth_assignment: [:booth]). + joins(:booth_assignment). + final. + where(id: current_user.poll_officer.officer_assignment_ids). + where("poll_booth_assignments.poll_id = ?", @poll.id). + order(date: :asc) + end + + def load_partial_results + if @officer_assignments.present? + @partial_results = ::Poll::PartialResult.where(officer_assignment_id: @officer_assignments.map(&:id)).order(:booth_assignment_id, :date) + end + end + + def results_params + params.permit(:officer_assignment_id, :date, :questions, :whites, :nulls) + end + + def index_params + params.permit(:booth_assignment_id, :date) + end + +end diff --git a/app/controllers/officing/voters_controller.rb b/app/controllers/officing/voters_controller.rb new file mode 100644 index 000000000..dee1e00bd --- /dev/null +++ b/app/controllers/officing/voters_controller.rb @@ -0,0 +1,25 @@ +class Officing::VotersController < Officing::BaseController + respond_to :html, :js + + def new + @user = User.find(params[:id]) + @polls = Poll.answerable_by(@user) + end + + def create + @poll = Poll.find(voter_params[:poll_id]) + @user = User.find(voter_params[:user_id]) + @voter = Poll::Voter.new(document_type: @user.document_type, + document_number: @user.document_number, + user: @user, + poll: @poll) + @voter.save! + end + + private + + def voter_params + params.require(:voter).permit(:poll_id, :user_id) + end + +end diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb index 03f62925a..81d6e8dbd 100644 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb @@ -2,7 +2,11 @@ class PagesController < ApplicationController skip_authorization_check def show - render action: params[:id] + if @custom_page = SiteCustomization::Page.published.find_by(slug: params[:id]) + render action: :custom_page + else + render action: params[:id] + end rescue ActionView::MissingTemplate head 404 end diff --git a/app/controllers/polls/questions_controller.rb b/app/controllers/polls/questions_controller.rb new file mode 100644 index 000000000..1849dff97 --- /dev/null +++ b/app/controllers/polls/questions_controller.rb @@ -0,0 +1,27 @@ +class Polls::QuestionsController < ApplicationController + + load_and_authorize_resource :poll + load_and_authorize_resource :question, class: 'Poll::Question' + + has_orders %w{most_voted newest oldest}, only: :show + + def show + @commentable = @question.proposal.present? ? @question.proposal : @question + @comment_tree = CommentTree.new(@commentable, params[:page], @current_order) + set_comment_flags(@comment_tree.comments) + + question_answer = @question.answers.where(author_id: current_user.try(:id)).first + @answers_by_question_id = {@question.id => question_answer.try(:answer)} + end + + def answer + answer = @question.answers.find_or_initialize_by(author: current_user) + + answer.answer = params[:answer] + answer.save! + answer.record_voter_participation + + @answers_by_question_id = {@question.id => params[:answer]} + end + +end diff --git a/app/controllers/polls_controller.rb b/app/controllers/polls_controller.rb new file mode 100644 index 000000000..41a038b46 --- /dev/null +++ b/app/controllers/polls_controller.rb @@ -0,0 +1,23 @@ +class PollsController < ApplicationController + + load_and_authorize_resource + + has_filters %w{current expired incoming} + + ::Poll::Answer # trigger autoload + + def index + @polls = @polls.send(@current_filter).includes(:geozones).sort_for_list.page(params[:page]) + end + + def show + @questions = @poll.questions.for_render.sort_for_list + + @answers_by_question_id = {} + poll_answers = ::Poll::Answer.by_question(@poll.question_ids).by_author(current_user.try(:id)) + poll_answers.each do |answer| + @answers_by_question_id[answer.question_id] = answer.answer + end + end + +end diff --git a/app/controllers/proposal_ballots_controller.rb b/app/controllers/proposal_ballots_controller.rb deleted file mode 100644 index 4171fcda8..000000000 --- a/app/controllers/proposal_ballots_controller.rb +++ /dev/null @@ -1,8 +0,0 @@ -class ProposalBallotsController < ApplicationController - skip_authorization_check - - def index - @proposal_ballots = Proposal.successfull.sort_by_confidence_score - end - -end \ No newline at end of file diff --git a/app/controllers/proposals_controller.rb b/app/controllers/proposals_controller.rb index d0ff9551e..d1707a8a7 100644 --- a/app/controllers/proposals_controller.rb +++ b/app/controllers/proposals_controller.rb @@ -2,10 +2,7 @@ class ProposalsController < ApplicationController include CommentableActions include FlagActions - before_action :parse_search_terms, only: [:index, :suggest] - before_action :parse_advanced_search_terms, only: :index before_action :parse_tag_filter, only: :index - before_action :set_search_order, only: :index before_action :load_categories, only: [:index, :new, :edit, :map, :summary] before_action :load_geozones, only: [:edit, :map, :summary] before_action :authenticate_user!, except: [:index, :show, :map, :summary] @@ -28,8 +25,8 @@ class ProposalsController < ApplicationController def index_customization discard_archived load_retired - load_proposal_ballots - load_featured unless @proposal_successfull_exists + load_successful_proposals + load_featured unless @proposal_successful_exists end def vote @@ -103,8 +100,8 @@ class ProposalsController < ApplicationController end end - def load_proposal_ballots - @proposal_successfull_exists = Proposal.successfull.exists? + def load_successful_proposals + @proposal_successful_exists = Proposal.successful.exists? end end diff --git a/app/controllers/valuation/budget_investments_controller.rb b/app/controllers/valuation/budget_investments_controller.rb index 666fac74a..ae747464e 100644 --- a/app/controllers/valuation/budget_investments_controller.rb +++ b/app/controllers/valuation/budget_investments_controller.rb @@ -26,6 +26,7 @@ class Valuation::BudgetInvestmentsController < Valuation::BaseController @investment.send_unfeasible_email end + Activity.log(current_user, :valuate, @investment) redirect_to valuation_budget_budget_investment_path(@budget, @investment), notice: t('valuation.budget_investments.notice.valuate') else render action: :edit diff --git a/app/helpers/admin_helper.rb b/app/helpers/admin_helper.rb index 62d31cd05..3f0d4db4b 100644 --- a/app/helpers/admin_helper.rb +++ b/app/helpers/admin_helper.rb @@ -4,6 +4,38 @@ module AdminHelper render "/#{namespace}/menu" end + def namespaced_root_path + "/#{namespace}" + end + + def namespaced_header_title + t("#{namespace}.header.title") + end + + def menu_tags? + ["tags"].include? controller_name + end + + def menu_moderated_content? + ["proposals", "debates", "comments", "users"].include? controller_name + end + + def menu_budget? + ["spending_proposals"].include? controller_name + end + + def menu_polls? + ["polls", "questions", "officers", "booths", "officer_assignments", "booth_assignments", "recounts", "results"].include? controller_name + end + + def menu_profiles? + ["organizations", "officials", "moderators", "valuators", "managers"].include? controller_name + end + + def menu_banners? + ["banners"].include? controller_name + end + def official_level_options options = [["", 0]] (1..5).each do |i| @@ -16,10 +48,14 @@ module AdminHelper Administrator.all.order('users.username asc').includes(:user).collect { |v| [ v.name, v.id ] } end + def admin_submit_action(resource) + resource.persisted? ? "edit" : "new" + end + private def namespace - controller.class.parent.name.downcase + controller.class.parent.name.downcase.gsub("::", "/") end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 208ed3d3d..0c23c0d22 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -47,4 +47,12 @@ module ApplicationHelper "".html_safe + t("shared.back") end end + + def image_path_for(filename) + SiteCustomization::Image.image_path_for(filename) || filename + end + + def content_block(name, locale) + SiteCustomization::ContentBlock.block_for(name, locale) + end end diff --git a/app/helpers/banners_helper.rb b/app/helpers/banners_helper.rb index a1505a39e..b8b5864c9 100644 --- a/app/helpers/banners_helper.rb +++ b/app/helpers/banners_helper.rb @@ -1,7 +1,7 @@ module BannersHelper - def has_banners - @banners.count > 0 + def has_banners? + @banners.present? && @banners.count > 0 end end \ No newline at end of file diff --git a/app/helpers/budgets_helper.rb b/app/helpers/budgets_helper.rb index 84f04139c..3a07f0393 100644 --- a/app/helpers/budgets_helper.rb +++ b/app/helpers/budgets_helper.rb @@ -10,7 +10,7 @@ module BudgetsHelper def namespaced_budget_investment_path(investment, options={}) case namespace - when "management::budgets" + when "management/budgets" management_budget_investment_path(investment.budget, investment, options) else budget_investment_path(investment.budget, investment, options) @@ -19,7 +19,7 @@ module BudgetsHelper def namespaced_budget_investment_vote_path(investment, options={}) case namespace - when "management::budgets" + when "management/budgets" vote_management_budget_investment_path(investment.budget, investment, options) else vote_budget_investment_path(investment.budget, investment, options) @@ -38,4 +38,8 @@ module BudgetsHelper def current_ballot Budget::Ballot.where(user: current_user, budget: @budget).first end + + def investment_tags_select_options + Budget::Investment.tags_on(:valuation).order(:name).select(:name).distinct + end end diff --git a/app/helpers/comments_helper.rb b/app/helpers/comments_helper.rb index b9e69ddf9..df6e57b61 100644 --- a/app/helpers/comments_helper.rb +++ b/app/helpers/comments_helper.rb @@ -23,6 +23,8 @@ module CommentsHelper def commentable_path(comment) if comment.commentable_type == "Budget::Investment" budget_investment_path(comment.commentable.budget_id, comment.commentable) + elsif comment.commentable_type == "Poll::Question" + question_path(comment.commentable) else comment.commentable end diff --git a/app/helpers/embed_videos_helper.rb b/app/helpers/embed_videos_helper.rb index 8633549f4..22b1d878f 100644 --- a/app/helpers/embed_videos_helper.rb +++ b/app/helpers/embed_videos_helper.rb @@ -2,6 +2,7 @@ module EmbedVideosHelper def embedded_video_code link = @proposal.video_url + title = t('proposals.show.embed_video_title', proposal: @proposal.title) if link.match(/vimeo.*/) server = "Vimeo" elsif link.match(/youtu*.*/) @@ -21,7 +22,7 @@ module EmbedVideosHelper end if match and match[2] - '' + '' else '' end diff --git a/app/helpers/officers_helper.rb b/app/helpers/officers_helper.rb new file mode 100644 index 000000000..f29bcf728 --- /dev/null +++ b/app/helpers/officers_helper.rb @@ -0,0 +1,7 @@ +module OfficersHelper + + def officer_label(officer) + truncate([officer.name, officer.email].compact.join(' - '), length: 100) + end + +end \ No newline at end of file diff --git a/app/helpers/officing_helper.rb b/app/helpers/officing_helper.rb new file mode 100644 index 000000000..2cfebf832 --- /dev/null +++ b/app/helpers/officing_helper.rb @@ -0,0 +1,36 @@ +module OfficingHelper + + def officer_assignments_select_options(officer_assignments) + options = [] + officer_assignments.each do |oa| + options << ["#{oa.booth_assignment.booth.name}: #{l(oa.date.to_date, format: :long)}", oa.id] + end + options_for_select(options) + end + + def booths_for_officer_select_options(officer_assignments) + options = [] + officer_assignments.each do |oa| + options << ["#{oa.booth_assignment.booth.name}", oa.id] + end + options.sort! {|x,y| x[0]<=>y[0]} + options_for_select(options, params[:oa]) + end + + def recount_to_compare_with_final_recount(final_recount) + recount = final_recount.booth_assignment.recounts.select {|r| r.date == final_recount.date}.first + recount.present? ? recount.count : "-" + end + + def system_recount_to_compare_with_final_recount(final_recount) + final_recount.booth_assignment.voters.select {|v| v.created_at.to_date == final_recount.date}.size + end + + def answer_result_value(question_id, answer_index) + return nil if params.blank? + return nil if params[:questions].blank? + return nil if params[:questions][question_id.to_s].blank? + params[:questions][question_id.to_s][answer_index.to_s] + end + +end \ No newline at end of file diff --git a/app/helpers/poll_final_recounts_helper.rb b/app/helpers/poll_final_recounts_helper.rb new file mode 100644 index 000000000..e196cb65b --- /dev/null +++ b/app/helpers/poll_final_recounts_helper.rb @@ -0,0 +1,7 @@ +module PollFinalRecountsHelper + + def final_recount_for_date(final_recounts, date) + final_recounts.select {|f| f.date == date}.first + end + +end \ No newline at end of file diff --git a/app/helpers/poll_recounts_helper.rb b/app/helpers/poll_recounts_helper.rb new file mode 100644 index 000000000..c47402163 --- /dev/null +++ b/app/helpers/poll_recounts_helper.rb @@ -0,0 +1,15 @@ +module PollRecountsHelper + + def recount_for_date(recounts, date) + recounts.select {|r| r.date == date}.first + end + + def booth_assignment_sum_recounts(ba) + ba.recounts.any? ? ba.recounts.to_a.sum(&:count) : nil + end + + def booth_assignment_sum_final_recounts(ba) + ba.final_recounts.any? ? ba.final_recounts.to_a.sum(&:count) :nil + end + +end \ No newline at end of file diff --git a/app/helpers/polls_helper.rb b/app/helpers/polls_helper.rb new file mode 100644 index 000000000..5425a6b52 --- /dev/null +++ b/app/helpers/polls_helper.rb @@ -0,0 +1,49 @@ +module PollsHelper + + def poll_select_options(include_all=nil) + options = @polls.collect {|poll| + [poll.name, current_path_with_query_params(poll_id: poll.id)] + } + options << all_polls if include_all + options_for_select(options, request.fullpath) + end + + def all_polls + [I18n.t("polls.all"), admin_questions_path] + end + + def poll_dates(poll) + if poll.starts_at.blank? || poll.ends_at.blank? + I18n.t("polls.no_dates") + else + I18n.t("polls.dates", open_at: l(poll.starts_at.to_date), closed_at: l(poll.ends_at.to_date)) + end + end + + def poll_dates_select_options(poll) + options = [] + (poll.starts_at.to_date..poll.ends_at.to_date).each do |date| + options << [l(date, format: :long), l(date)] + end + options_for_select(options, params[:d]) + end + + def poll_final_recount_option(poll) + final_date = poll.ends_at.to_date + 1.day + options_for_select([[I18n.t("polls.final_date"), l(final_date)]]) + end + + def poll_booths_select_options(poll) + options = [] + poll.booths.each do |booth| + options << [booth_name_with_location(booth), booth.id] + end + options_for_select(options) + end + + def booth_name_with_location(booth) + location = booth.location.blank? ? "" : " (#{booth.location})" + booth.name + location + end + +end \ No newline at end of file diff --git a/app/helpers/signature_sheets_helper.rb b/app/helpers/signature_sheets_helper.rb index acd75a5ab..8e75dccfd 100644 --- a/app/helpers/signature_sheets_helper.rb +++ b/app/helpers/signature_sheets_helper.rb @@ -2,7 +2,7 @@ module SignatureSheetsHelper def signable_options [[t("activerecord.models.proposal", count: 1), Proposal], - [t("activerecord.models.spending_proposal", count: 1), SpendingProposal]] + [t("activerecord.models.budget/investment", count: 1), Budget::Investment]] end end \ No newline at end of file diff --git a/app/helpers/stats_helper.rb b/app/helpers/stats_helper.rb index e481ef7e1..d8eff0ac2 100644 --- a/app/helpers/stats_helper.rb +++ b/app/helpers/stats_helper.rb @@ -21,4 +21,11 @@ module StatsHelper content_tag :div, "", opt end + def budget_investments_chart_tag(opt={}) + events = events.join(',') if events.is_a? Array + opt[:data] ||= {} + opt[:data][:graph] = admin_api_stats_path(budget_investments: true) + content_tag :div, "", opt + end + end diff --git a/app/models/abilities/administrator.rb b/app/models/abilities/administrator.rb index 8e721b2b8..8df52e6eb 100644 --- a/app/models/abilities/administrator.rb +++ b/app/models/abilities/administrator.rb @@ -32,7 +32,7 @@ module Abilities can :mark_featured, Debate can :unmark_featured, Debate - can :comment_as_administrator, [Debate, Comment, Proposal, Budget::Investment] + can :comment_as_administrator, [Debate, Comment, Proposal, Poll::Question, Budget::Investment] can [:search, :create, :index, :destroy], ::Moderator can [:search, :create, :index, :summary], ::Valuator @@ -50,7 +50,20 @@ module Abilities can :create, Budget::ValuatorAssignment can [:search, :edit, :update, :create, :index, :destroy], Banner + can [:index, :create, :edit, :update, :destroy], Geozone + + can [:read, :create, :update, :destroy, :add_question, :remove_question, :search_booths, :search_questions, :search_officers], Poll + can [:read, :create, :update, :destroy], Poll::Booth + can [:search, :create, :index, :destroy], ::Poll::Officer + can [:create, :destroy], ::Poll::BoothAssignment + can [:create, :destroy], ::Poll::OfficerAssignment + can [:read, :create, :update], Poll::Question + can :destroy, Poll::Question # , comments_count: 0, votes_up: 0 + + can :manage, SiteCustomization::Page + can :manage, SiteCustomization::Image + can :manage, SiteCustomization::ContentBlock end end end diff --git a/app/models/abilities/common.rb b/app/models/abilities/common.rb index e324540d2..6a8ef594c 100644 --- a/app/models/abilities/common.rb +++ b/app/models/abilities/common.rb @@ -46,12 +46,19 @@ module Abilities can :create, SpendingProposal can :create, Budget::Investment, budget: { phase: "accepting" } + can :destroy, Budget::Investment, budget: { phase: ["accepting", "reviewing"] }, author_id: user.id can :vote, Budget::Investment, budget: { phase: "selecting" } can [:show, :create], Budget::Ballot, budget: { phase: "balloting" } can [:create, :destroy], Budget::Ballot::Line, budget: { phase: "balloting" } can :create, DirectMessage can :show, DirectMessage, sender_id: user.id + can :answer, Poll do |poll| + poll.answerable_by?(user) + end + can :answer, Poll::Question do |question| + question.answerable_by?(user) + end end can [:create, :show], ProposalNotification, proposal: { author_id: user.id } diff --git a/app/models/abilities/everyone.rb b/app/models/abilities/everyone.rb index 98080aa4c..c2b6cd6d7 100644 --- a/app/models/abilities/everyone.rb +++ b/app/models/abilities/everyone.rb @@ -6,6 +6,8 @@ module Abilities can [:read, :map], Debate can [:read, :map, :summary], Proposal can :read, Comment + can :read, Poll + can :read, Poll::Question can [:read, :welcome], Budget can :read, Budget::Investment can :read, SpendingProposal diff --git a/app/models/abilities/moderator.rb b/app/models/abilities/moderator.rb index 52d838f9c..5740e302e 100644 --- a/app/models/abilities/moderator.rb +++ b/app/models/abilities/moderator.rb @@ -5,7 +5,7 @@ module Abilities def initialize(user) self.merge Abilities::Moderation.new(user) - can :comment_as_moderator, [Debate, Comment, Proposal, Budget::Investment] + can :comment_as_moderator, [Debate, Comment, Proposal, Budget::Investment, Poll::Question] end end end diff --git a/app/models/activity.rb b/app/models/activity.rb index 047ccb7dd..0fc35ad11 100644 --- a/app/models/activity.rb +++ b/app/models/activity.rb @@ -2,7 +2,7 @@ class Activity < ActiveRecord::Base belongs_to :actionable, -> { with_hidden }, polymorphic: true belongs_to :user, -> { with_hidden } - VALID_ACTIONS = %w( hide block restore ) + VALID_ACTIONS = %w( hide block restore valuate ) validates :action, inclusion: {in: VALID_ACTIONS} @@ -10,6 +10,7 @@ class Activity < ActiveRecord::Base scope :on_debates, -> { where(actionable_type: 'Debate') } scope :on_users, -> { where(actionable_type: 'User') } scope :on_comments, -> { where(actionable_type: 'Comment') } + scope :on_budget_investments, -> { where(actionable_type: 'Budget::Investment') } scope :for_render, -> { includes(user: [:moderator, :administrator]).includes(:actionable) } def self.log(user, action, actionable) diff --git a/app/models/budget.rb b/app/models/budget.rb index 2741ac014..5b2cdc558 100644 --- a/app/models/budget.rb +++ b/app/models/budget.rb @@ -35,6 +35,10 @@ class Budget < ActiveRecord::Base 2000 end + def self.title_max_length + 80 + end + def accepting? phase == "accepting" end @@ -63,6 +67,10 @@ class Budget < ActiveRecord::Base phase == "finished" end + def balloting_or_later? + balloting? || reviewing_ballots? || finished? + end + def on_hold? reviewing? || valuating? || reviewing_ballots? end diff --git a/app/models/budget/ballot.rb b/app/models/budget/ballot.rb index 8355232a8..6a4047aff 100644 --- a/app/models/budget/ballot.rb +++ b/app/models/budget/ballot.rb @@ -66,7 +66,8 @@ class Budget end def heading_for_group(group) - self.headings.where(group: group).first + return nil unless has_lines_in_group?(group) + self.investments.where(group: group).first.heading end end diff --git a/app/models/budget/ballot/line.rb b/app/models/budget/ballot/line.rb index 23c6aaef2..0b56716c6 100644 --- a/app/models/budget/ballot/line.rb +++ b/app/models/budget/ballot/line.rb @@ -2,7 +2,7 @@ class Budget class Ballot class Line < ActiveRecord::Base belongs_to :ballot - belongs_to :investment + belongs_to :investment, counter_cache: :ballot_lines_count belongs_to :heading belongs_to :group belongs_to :budget diff --git a/app/models/budget/investment.rb b/app/models/budget/investment.rb index 100433707..64adbaddf 100644 --- a/app/models/budget/investment.rb +++ b/app/models/budget/investment.rb @@ -25,6 +25,7 @@ class Budget validates :description, presence: true validates :heading_id, presence: true validates_presence_of :unfeasibility_explanation, if: :unfeasibility_explanation_required? + validates_presence_of :price, if: :price_required? validates :title, length: { in: 4..Budget::Investment.title_max_length } validates :description, length: { maximum: Budget::Investment.description_max_length } @@ -45,7 +46,8 @@ class Budget scope :not_unfeasible, -> { where.not(feasibility: "unfeasible") } scope :undecided, -> { where(feasibility: "undecided") } scope :with_supports, -> { where('cached_votes_up > 0') } - scope :selected, -> { where(selected: true) } + scope :selected, -> { feasible.where(selected: true) } + scope :unselected, -> { feasible.where(selected: false) } scope :last_week, -> { where("created_at >= ?", 7.days.ago)} scope :by_group, -> (group_id) { where(group_id: group_id) } @@ -75,34 +77,6 @@ class Budget results.includes(:heading, :group, :budget, administrator: :user, valuators: :user) end - def self.limit_results(results, budget, max_per_heading, max_for_no_heading) - return results if max_per_heading <= 0 && max_for_no_heading <= 0 - - ids = [] - if max_per_heading > 0 - budget.headings.pluck(:id).each do |hid| - ids += Investment.where(heading_id: hid).order(confidence_score: :desc).limit(max_per_heading).pluck(:id) - end - end - - if max_for_no_heading > 0 - ids += Investment.no_heading.order(confidence_score: :desc).limit(max_for_no_heading).pluck(:id) - end - - conditions = ["investments.id IN (?)"] - values = [ids] - - if max_per_heading == 0 - conditions << "investments.heading_id IS NOT ?" - values << nil - elsif max_for_no_heading == 0 - conditions << "investments.heading_id IS ?" - values << nil - end - - results.where(conditions.join(' OR '), *values) - end - def searchable_values { title => 'A', author.username => 'B', @@ -136,6 +110,10 @@ class Budget unfeasible? && valuation_finished? end + def price_required? + feasible? && valuation_finished? + end + def unfeasible_email_pending? unfeasible_email_sent_at.blank? && unfeasible? && valuation_finished? end @@ -225,7 +203,7 @@ class Budget def should_show_aside? (budget.selecting? && !unfeasible?) || (budget.balloting? && feasible?) || - (budget.valuating? && feasible?) + (budget.valuating? && !unfeasible?) end def should_show_votes? @@ -240,17 +218,25 @@ class Budget budget.balloting? end + def should_show_price? + feasible? && + selected? && + (budget.reviewing_ballots? || budget.finished?) + end + + def should_show_price_info? + feasible? && + price_explanation.present? && + (budget.balloting? || budget.reviewing_ballots? || budget.finished?) + end + def formatted_price budget.formatted_amount(price) end - def self.apply_filters_and_search(budget, params) + def self.apply_filters_and_search(budget, params, current_filter=nil) investments = all - if budget.balloting? - investments = investments.selected - else - investments = params[:unfeasible].present? ? investments.unfeasible : investments.not_unfeasible - end + investments = investments.send(current_filter) if current_filter.present? investments = investments.by_heading(params[:heading_id]) if params[:heading_id].present? investments = investments.search(params[:search]) if params[:search].present? investments @@ -259,8 +245,9 @@ class Budget private def set_denormalized_ids - self.group_id ||= self.heading.try(:group_id) + self.group_id = self.heading.try(:group_id) if self.heading_id_changed? self.budget_id ||= self.heading.try(:group).try(:budget_id) end + end end diff --git a/app/models/comment.rb b/app/models/comment.rb index 16bb207d3..ec90553aa 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -12,7 +12,8 @@ class Comment < ActiveRecord::Base validates :body, presence: true validates :user, presence: true - validates_inclusion_of :commentable_type, in: ["Debate", "Proposal", "Budget::Investment"] + + validates_inclusion_of :commentable_type, in: ["Debate", "Proposal", "Budget::Investment", "Poll::Question"] validate :validate_body_length diff --git a/app/models/concerns/searchable.rb b/app/models/concerns/searchable.rb index 4d717959e..147a37fbc 100644 --- a/app/models/concerns/searchable.rb +++ b/app/models/concerns/searchable.rb @@ -12,7 +12,7 @@ module Searchable }, ignoring: :accents, ranked_by: '(:tsearch)', - order_within_rank: "#{self.table_name}.cached_votes_up DESC" + order_within_rank: (self.column_names.include?('cached_votes_up') ? "#{self.table_name}.cached_votes_up DESC" : nil) } end diff --git a/app/models/failed_census_call.rb b/app/models/failed_census_call.rb index ac792d7b7..b7d60e63a 100644 --- a/app/models/failed_census_call.rb +++ b/app/models/failed_census_call.rb @@ -1,3 +1,4 @@ class FailedCensusCall < ActiveRecord::Base belongs_to :user, counter_cache: true + belongs_to :poll_officer, class_name: 'Poll::Officer', counter_cache: true end diff --git a/app/models/geozone.rb b/app/models/geozone.rb index 55121d0ee..9ef4065ef 100644 --- a/app/models/geozone.rb +++ b/app/models/geozone.rb @@ -12,6 +12,10 @@ class Geozone < ActiveRecord::Base Geozone.pluck(:name) end + def self.city + where(name: 'city').first + end + def safe_to_destroy? Geozone.reflect_on_all_associations(:has_many).all? do |association| association.klass.where(geozone: self).empty? diff --git a/app/models/officing/residence.rb b/app/models/officing/residence.rb new file mode 100644 index 000000000..6343fc5f7 --- /dev/null +++ b/app/models/officing/residence.rb @@ -0,0 +1,126 @@ +class Officing::Residence + include ActiveModel::Model + include ActiveModel::Validations::Callbacks + + attr_accessor :user, :officer, :document_number, :document_type, :year_of_birth + + before_validation :call_census_api + + validates_presence_of :document_number + validates_presence_of :document_type + validates_presence_of :year_of_birth + + validate :allowed_age + validate :residence_in_madrid + + def initialize(attrs={}) + super + clean_document_number + end + + def save + return false unless valid? + + if user_exists? + self.user = find_user_by_document + self.user.update(verified_at: Time.current) + else + user_params = { + document_number: document_number, + document_type: document_type, + geozone: self.geozone, + date_of_birth: date_of_birth.to_datetime, + gender: gender, + residence_verified_at: Time.current, + verified_at: Time.current, + erased_at: Time.current, + password: random_password, + terms_of_service: '1', + email: nil + } + self.user = User.create!(user_params) + end + end + + def store_failed_census_call + FailedCensusCall.create({ + user: user, + document_number: document_number, + document_type: document_type, + year_of_birth: year_of_birth, + poll_officer: officer + }) + + end + + def user_exists? + find_user_by_document.present? + end + + def find_user_by_document + User.where(document_number: document_number, + document_type: document_type).first + end + + def residence_in_madrid + return if errors.any? + + unless residency_valid? + store_failed_census_call + errors.add(:residence_in_madrid, false) + end + end + + def allowed_age + return if errors[:year_of_birth].any? + return unless @census_api_response.valid? + + unless allowed_age? + errors.add(:year_of_birth, I18n.t('verification.residence.new.error_not_allowed_age')) + end + end + + def allowed_age? + Age.in_years(date_of_birth) >= User.minimum_required_age + end + + def geozone + Geozone.where(census_code: district_code).first + end + + def district_code + @census_api_response.district_code + end + + def gender + @census_api_response.gender + end + + def date_of_birth + @census_api_response.date_of_birth + end + + private + + def call_census_api + @census_api_response = CensusApi.new.call(document_type, document_number) + end + + def residency_valid? + @census_api_response.valid? && + @census_api_response.date_of_birth.year.to_s == year_of_birth.to_s + end + + def census_year_of_birth + @census_api_response.date_of_birth.year + end + + def clean_document_number + self.document_number = self.document_number.gsub(/[^a-z0-9]+/i, "").upcase unless self.document_number.blank? + end + + def random_password + (0...20).map { ('a'..'z').to_a[rand(26)] }.join + end + +end diff --git a/app/models/poll.rb b/app/models/poll.rb new file mode 100644 index 000000000..c6be3073a --- /dev/null +++ b/app/models/poll.rb @@ -0,0 +1,65 @@ +class Poll < ActiveRecord::Base + has_many :booth_assignments, class_name: "Poll::BoothAssignment" + has_many :booths, through: :booth_assignments + has_many :partial_results, through: :booth_assignments + has_many :white_results, through: :booth_assignments + has_many :null_results, through: :booth_assignments + has_many :voters + has_many :officer_assignments, through: :booth_assignments + has_many :officers, through: :officer_assignments + has_many :questions + + has_and_belongs_to_many :geozones + + validates :name, presence: true + + validate :date_range + + scope :current, -> { where('starts_at <= ? and ? <= ends_at', Time.current, Time.current) } + scope :incoming, -> { where('? < starts_at', Time.current) } + scope :expired, -> { where('ends_at < ?', Time.current) } + scope :published, -> { where('published = ?', true) } + scope :by_geozone_id, ->(geozone_id) { where(geozones: {id: geozone_id}.joins(:geozones)) } + + scope :sort_for_list, -> { order(:geozone_restricted, :starts_at, :name) } + + def current?(timestamp = DateTime.current) + starts_at <= timestamp && timestamp <= ends_at + end + + def incoming?(timestamp = DateTime.current) + timestamp < starts_at + end + + def expired?(timestamp = DateTime.current) + ends_at < timestamp + end + + def answerable_by?(user) + user.present? && + user.level_two_or_three_verified? && + current? && + (!geozone_restricted || geozone_ids.include?(user.geozone_id)) + end + + def self.answerable_by(user) + return none if user.nil? || user.unverified? + current.joins('LEFT JOIN "geozones_polls" ON "geozones_polls"."poll_id" = "polls"."id"') + .where('geozone_restricted = ? OR geozones_polls.geozone_id = ?', false, user.geozone_id) + end + + def votable_by?(user) + !document_has_voted?(user.document_number, user.document_type) + end + + def document_has_voted?(document_number, document_type) + voters.where(document_number: document_number, document_type: document_type).exists? + end + + def date_range + unless starts_at.present? && ends_at.present? && starts_at <= ends_at + errors.add(:starts_at, I18n.t('errors.messages.invalid_date_range')) + end + end + +end diff --git a/app/models/poll/answer.rb b/app/models/poll/answer.rb new file mode 100644 index 000000000..52fb11469 --- /dev/null +++ b/app/models/poll/answer.rb @@ -0,0 +1,19 @@ +class Poll::Answer < ActiveRecord::Base + + belongs_to :question, -> { with_hidden } + belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' + + delegate :poll, :poll_id, to: :question + + validates :question, presence: true + validates :author, presence: true + validates :answer, presence: true + validates :answer, inclusion: {in: ->(a) { a.question.valid_answers }} + + scope :by_author, -> (author_id) { where(author_id: author_id) } + scope :by_question, -> (question_id) { where(question_id: question_id) } + + def record_voter_participation + Poll::Voter.create!(user: author, poll: poll) + end +end \ No newline at end of file diff --git a/app/models/poll/booth.rb b/app/models/poll/booth.rb new file mode 100644 index 000000000..c7fb63efc --- /dev/null +++ b/app/models/poll/booth.rb @@ -0,0 +1,13 @@ +class Poll + class Booth < ActiveRecord::Base + has_many :booth_assignments, class_name: "Poll::BoothAssignment" + has_many :polls, through: :booth_assignments + + validates :name, presence: true, uniqueness: true + + def self.search(terms) + return Booth.none if terms.blank? + Booth.where("name ILIKE ? OR location ILIKE ?", "%#{terms}%", "%#{terms}%") + end + end +end \ No newline at end of file diff --git a/app/models/poll/booth_assignment.rb b/app/models/poll/booth_assignment.rb new file mode 100644 index 000000000..0519fffa6 --- /dev/null +++ b/app/models/poll/booth_assignment.rb @@ -0,0 +1,15 @@ +class Poll + class BoothAssignment < ActiveRecord::Base + belongs_to :booth + belongs_to :poll + + has_many :officer_assignments, class_name: "Poll::OfficerAssignment", dependent: :destroy + has_many :recounts, class_name: "Poll::Recount", dependent: :destroy + has_many :final_recounts, class_name: "Poll::FinalRecount", dependent: :destroy + has_many :officers, through: :officer_assignments + has_many :voters + has_many :partial_results + has_many :white_results + has_many :null_results + end +end diff --git a/app/models/poll/final_recount.rb b/app/models/poll/final_recount.rb new file mode 100644 index 000000000..6ebf5eede --- /dev/null +++ b/app/models/poll/final_recount.rb @@ -0,0 +1,19 @@ +class Poll + class FinalRecount < ActiveRecord::Base + belongs_to :booth_assignment, class_name: "Poll::BoothAssignment" + belongs_to :officer_assignment, class_name: "Poll::OfficerAssignment" + + validates :booth_assignment_id, presence: true + validates :date, presence: true, uniqueness: {scope: :booth_assignment_id} + validates :count, presence: true, numericality: {only_integer: true} + + before_save :update_logs + + def update_logs + if self.count_changed? && self.count_was.present? + self.count_log += ":#{self.count_was.to_s}" + self.officer_assignment_id_log += ":#{self.officer_assignment_id_was.to_s}" + end + end + end +end \ No newline at end of file diff --git a/app/models/poll/null_result.rb b/app/models/poll/null_result.rb new file mode 100644 index 000000000..222432c7f --- /dev/null +++ b/app/models/poll/null_result.rb @@ -0,0 +1,23 @@ +class Poll::NullResult < ActiveRecord::Base + + VALID_ORIGINS = %w{ web booth } + + belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' + belongs_to :booth_assignment + belongs_to :officer_assignment + + validates :author, presence: true + validates :origin, inclusion: {in: VALID_ORIGINS} + + scope :by_author, -> (author_id) { where(author_id: author_id) } + + before_save :update_logs + + def update_logs + if self.amount_changed? && self.amount_was.present? + self.amount_log += ":#{self.amount_was.to_s}" + self.officer_assignment_id_log += ":#{self.officer_assignment_id_was.to_s}" + self.author_id_log += ":#{self.author_id_was.to_s}" + end + end +end \ No newline at end of file diff --git a/app/models/poll/officer.rb b/app/models/poll/officer.rb new file mode 100644 index 000000000..bf4c73c36 --- /dev/null +++ b/app/models/poll/officer.rb @@ -0,0 +1,26 @@ +class Poll + class Officer < ActiveRecord::Base + belongs_to :user + has_many :officer_assignments, class_name: "Poll::OfficerAssignment" + has_many :failed_census_calls, foreign_key: :poll_officer_id + + validates :user_id, presence: true, uniqueness: true + + delegate :name, :email, to: :user + + def voting_days_assigned_polls + officer_assignments.voting_days.includes(booth_assignment: :poll). + map(&:booth_assignment). + map(&:poll).uniq.compact. + sort {|x, y| y.ends_at <=> x.ends_at} + end + + def final_days_assigned_polls + officer_assignments.final.includes(booth_assignment: :poll). + map(&:booth_assignment). + map(&:poll).uniq.compact. + sort {|x, y| y.ends_at <=> x.ends_at} + end + + end +end diff --git a/app/models/poll/officer_assignment.rb b/app/models/poll/officer_assignment.rb new file mode 100644 index 000000000..cd4f53266 --- /dev/null +++ b/app/models/poll/officer_assignment.rb @@ -0,0 +1,25 @@ +class Poll + class OfficerAssignment < ActiveRecord::Base + belongs_to :officer + belongs_to :booth_assignment + has_one :recount + has_many :final_recounts + has_many :partial_results + has_many :voters + + validates :officer_id, presence: true + validates :booth_assignment_id, presence: true + validates :date, presence: true, uniqueness: { scope: [:officer_id, :booth_assignment_id] } + + delegate :poll_id, :booth_id, to: :booth_assignment + + scope :voting_days, -> { where(final: false) } + scope :final, -> { where(final: true) } + + before_create :log_user_data + + def log_user_data + self.user_data_log = "#{officer.user_id} - #{officer.user.name_and_email}" + end + end +end diff --git a/app/models/poll/partial_result.rb b/app/models/poll/partial_result.rb new file mode 100644 index 000000000..e42589a03 --- /dev/null +++ b/app/models/poll/partial_result.rb @@ -0,0 +1,28 @@ +class Poll::PartialResult < ActiveRecord::Base + + VALID_ORIGINS = %w{ web booth } + + belongs_to :question, -> { with_hidden } + belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' + belongs_to :booth_assignment + belongs_to :officer_assignment + + validates :question, presence: true + validates :author, presence: true + validates :answer, presence: true + validates :answer, inclusion: {in: ->(a) { a.question.valid_answers }} + validates :origin, inclusion: {in: VALID_ORIGINS} + + scope :by_author, -> (author_id) { where(author_id: author_id) } + scope :by_question, -> (question_id) { where(question_id: question_id) } + + before_save :update_logs + + def update_logs + if self.amount_changed? && self.amount_was.present? + self.amount_log += ":#{self.amount_was.to_s}" + self.officer_assignment_id_log += ":#{self.officer_assignment_id_was.to_s}" + self.author_id_log += ":#{self.author_id_was.to_s}" + end + end +end \ No newline at end of file diff --git a/app/models/poll/question.rb b/app/models/poll/question.rb new file mode 100644 index 000000000..dd4eae3bc --- /dev/null +++ b/app/models/poll/question.rb @@ -0,0 +1,70 @@ +class Poll::Question < ActiveRecord::Base + include Measurable + include Searchable + + acts_as_paranoid column: :hidden_at + include ActsAsParanoidAliases + + belongs_to :poll + belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' + + has_many :comments, as: :commentable + has_many :answers + has_many :partial_results + belongs_to :proposal + + validates :title, presence: true + validates :author, presence: true + + validates :title, length: { minimum: 4 } + validates :description, length: { maximum: Poll::Question.description_max_length } + + scope :by_poll_id, ->(poll_id) { where(poll_id: poll_id) } + + scope :sort_for_list, -> { order('poll_questions.proposal_id IS NULL', :created_at)} + scope :for_render, -> { includes(:author, :proposal) } + + def self.search(params) + results = self.all + results = results.by_poll_id(params[:poll_id]) if params[:poll_id].present? + results = results.pg_search(params[:search]) if params[:search].present? + results + end + + def searchable_values + { title => 'A', + proposal.try(:title) => 'A', + description => 'B', + author.username => 'C', + author_visible_name => 'C' } + end + + def description + super.try :html_safe + end + + def valid_answers + (super.try(:split, ',').compact || []).map(&:strip) + end + + def copy_attributes_from_proposal(proposal) + if proposal.present? + self.author = proposal.author + self.author_visible_name = proposal.author.name + self.proposal_id = proposal.id + self.title = proposal.title + self.description = proposal.description + self.valid_answers = I18n.t('poll_questions.default_valid_answers') + end + end + + def answerable_by?(user) + poll.answerable_by?(user) + end + + def self.answerable_by(user) + return none if user.nil? || user.unverified? + where(poll_id: Poll.answerable_by(user).pluck(:id)) + end + +end diff --git a/app/models/poll/recount.rb b/app/models/poll/recount.rb new file mode 100644 index 000000000..b4e28583e --- /dev/null +++ b/app/models/poll/recount.rb @@ -0,0 +1,20 @@ +class Poll + class Recount < ActiveRecord::Base + belongs_to :booth_assignment, class_name: "Poll::BoothAssignment" + belongs_to :officer_assignment, class_name: "Poll::OfficerAssignment" + + validates :booth_assignment_id, presence: true + validates :date, presence: true, uniqueness: {scope: :booth_assignment_id} + validates :officer_assignment_id, presence: true, uniqueness: {scope: :booth_assignment_id} + validates :count, presence: true, numericality: {only_integer: true} + + before_save :update_logs + + def update_logs + if self.count_changed? && self.count_was.present? + self.count_log += ":#{self.count_was.to_s}" + self.officer_assignment_id_log += ":#{self.officer_assignment_id_was.to_s}" + end + end + end +end \ No newline at end of file diff --git a/app/models/poll/voter.rb b/app/models/poll/voter.rb new file mode 100644 index 000000000..8fe612151 --- /dev/null +++ b/app/models/poll/voter.rb @@ -0,0 +1,59 @@ +class Poll + class Voter < ActiveRecord::Base + belongs_to :poll + belongs_to :user + belongs_to :geozone + belongs_to :booth_assignment + belongs_to :officer_assignment + + validates :poll_id, presence: true + validates :user_id, presence: true + + validates :document_number, presence: true, uniqueness: { scope: [:poll_id, :document_type], message: :has_voted } + + before_validation :set_demographic_info, :set_document_info + + def set_demographic_info + return unless user.present? + + self.gender = user.gender + self.age = user.age + self.geozone = user.geozone + end + + def set_document_info + return unless user.present? + + self.document_type = user.document_type + self.document_number = user.document_number + end + + private + + def in_census? + census_api_response.valid? + end + + def census_api_response + @census_api_response ||= CensusApi.new.call(document_type, document_number) + end + + def fill_stats_fields + if in_census? + self.gender = census_api_response.gender + self.geozone_id = Geozone.select(:id).where(census_code: census_api_response.district_code).first.try(:id) + self.age = voter_age(census_api_response.date_of_birth) + end + end + + def voter_age(dob) + if dob.blank? + nil + else + now = Time.now.utc.to_date + now.year - dob.year - ((now.month > dob.month || (now.month == dob.month && now.day >= dob.day)) ? 0 : 1) + end + end + + end +end \ No newline at end of file diff --git a/app/models/poll/white_result.rb b/app/models/poll/white_result.rb new file mode 100644 index 000000000..5b0aa4966 --- /dev/null +++ b/app/models/poll/white_result.rb @@ -0,0 +1,23 @@ +class Poll::WhiteResult < ActiveRecord::Base + + VALID_ORIGINS = %w{ web booth } + + belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' + belongs_to :booth_assignment + belongs_to :officer_assignment + + validates :author, presence: true + validates :origin, inclusion: {in: VALID_ORIGINS} + + scope :by_author, -> (author_id) { where(author_id: author_id) } + + before_save :update_logs + + def update_logs + if self.amount_changed? && self.amount_was.present? + self.amount_log += ":#{self.amount_was.to_s}" + self.officer_assignment_id_log += ":#{self.officer_assignment_id_was.to_s}" + self.author_id_log += ":#{self.author_id_was.to_s}" + end + end +end \ No newline at end of file diff --git a/app/models/proposal.rb b/app/models/proposal.rb index c69fe4194..9ed84f9d9 100644 --- a/app/models/proposal.rb +++ b/app/models/proposal.rb @@ -47,12 +47,12 @@ class Proposal < ActiveRecord::Base scope :sort_by_relevance, -> { all } scope :sort_by_flags, -> { order(flags_count: :desc, updated_at: :desc) } scope :sort_by_archival_date, -> { archived.sort_by_confidence_score } - scope :archived, -> { where("proposals.created_at <= ?", Setting["months_to_archive_proposals"].to_i.months.ago)} - scope :not_archived, -> { where("proposals.created_at > ?", Setting["months_to_archive_proposals"].to_i.months.ago)} + scope :archived, -> { where("proposals.created_at <= ?", Setting["months_to_archive_proposals"].to_i.months.ago) } + scope :not_archived, -> { where("proposals.created_at > ?", Setting["months_to_archive_proposals"].to_i.months.ago) } scope :last_week, -> { where("proposals.created_at >= ?", 7.days.ago)} scope :retired, -> { where.not(retired_at: nil) } scope :not_retired, -> { where(retired_at: nil) } - scope :successfull, -> { where("cached_votes_up >= ?", Proposal.votes_needed_for_success)} + scope :successful, -> { where("cached_votes_up >= ?", Proposal.votes_needed_for_success) } def to_param "#{id}-#{title}".parameterize @@ -157,7 +157,7 @@ class Proposal < ActiveRecord::Base Setting['votes_for_proposal_success'].to_i end - def successfull? + def successful? total_votes >= Proposal.votes_needed_for_success end diff --git a/app/models/signature.rb b/app/models/signature.rb index 06ade968c..543965aed 100644 --- a/app/models/signature.rb +++ b/app/models/signature.rb @@ -12,35 +12,30 @@ class Signature < ActiveRecord::Base before_validation :clean_document_number - def verified? - user_exists? || in_census? - end - def verify - if verified? - assign_vote - mark_as_verified - end - end - - def assign_vote if user_exists? assign_vote_to_user - else + mark_as_verified + elsif in_census? create_user assign_vote_to_user + mark_as_verified end end def assign_vote_to_user set_user - signable.register_vote(user, "yes") + if signable.is_a? Budget::Investment + signable.vote_by(voter: user, vote: 'yes') if [nil, :no_selecting_allowed].include?(signable.reason_for_not_being_selectable_by(user)) + else + signable.register_vote(user, "yes") + end assign_signature_to_vote end def assign_signature_to_vote vote = Vote.where(votable: signable, voter: user).first - vote.update(signature: self) + vote.update(signature: self) if vote end def user_exists? @@ -51,11 +46,14 @@ class Signature < ActiveRecord::Base user_params = { document_number: document_number, created_from_signature: true, - verified_at: Time.now, - erased_at: Time.now, + verified_at: Time.current, + erased_at: Time.current, password: random_password, terms_of_service: '1', - email: nil + email: nil, + date_of_birth: @census_api_response.date_of_birth, + gender: @census_api_response.gender, + geozone: Geozone.where(census_code: @census_api_response.district_code).first } User.create!(user_params) end @@ -70,10 +68,17 @@ class Signature < ActiveRecord::Base end def in_census? - response = document_types.detect do |document_type| - CensusApi.new.call(document_type, document_number).valid? + document_types.detect do |document_type| + response = CensusApi.new.call(document_type, document_number) + if response.valid? + @census_api_response = response + true + else + false + end end - response.present? + + @census_api_response.present? end def set_user diff --git a/app/models/signature_sheet.rb b/app/models/signature_sheet.rb index 9720c891f..1434143ac 100644 --- a/app/models/signature_sheet.rb +++ b/app/models/signature_sheet.rb @@ -2,7 +2,7 @@ class SignatureSheet < ActiveRecord::Base belongs_to :signable, polymorphic: true belongs_to :author, class_name: 'User', foreign_key: 'author_id' - VALID_SIGNABLES = %w( Proposal SpendingProposal ) + VALID_SIGNABLES = %w( Proposal Budget::Investment SpendingProposal ) has_many :signatures diff --git a/app/models/site_customization.rb b/app/models/site_customization.rb new file mode 100644 index 000000000..e5d2f2137 --- /dev/null +++ b/app/models/site_customization.rb @@ -0,0 +1,5 @@ +module SiteCustomization + def self.table_name_prefix + 'site_customization_' + end +end diff --git a/app/models/site_customization/content_block.rb b/app/models/site_customization/content_block.rb new file mode 100644 index 000000000..c08beb52e --- /dev/null +++ b/app/models/site_customization/content_block.rb @@ -0,0 +1,11 @@ +class SiteCustomization::ContentBlock < ActiveRecord::Base + VALID_BLOCKS = %w(top_links footer) + + validates :locale, presence: true, inclusion: { in: I18n.available_locales.map(&:to_s) } + validates :name, presence: true, uniqueness: { scope: :locale }, inclusion: { in: VALID_BLOCKS } + + def self.block_for(name, locale) + locale ||= I18n.default_locale + find_by(name: name, locale: locale).try(:body) + end +end diff --git a/app/models/site_customization/image.rb b/app/models/site_customization/image.rb new file mode 100644 index 000000000..2230a96ce --- /dev/null +++ b/app/models/site_customization/image.rb @@ -0,0 +1,48 @@ +class SiteCustomization::Image < ActiveRecord::Base + VALID_IMAGES = { + "icon_home" => [330, 240], + "logo_header" => [80, 80], + "social-media-icon" => [200, 200], + "apple-touch-icon-200" => [200, 200] + } + + has_attached_file :image + + validates :name, presence: true, uniqueness: true, inclusion: { in: VALID_IMAGES.keys } + validates_attachment_content_type :image, :content_type => ["image/png"] + validate :check_image + + def self.all_images + VALID_IMAGES.keys.map do |image_name| + find_by(name: image_name) || create!(name: image_name.to_s) + end + end + + def self.image_path_for(filename) + image_name = filename.split(".").first + + if i = find_by(name: image_name) + i.image.exists? ? i.image.url : nil + end + end + + def required_width + VALID_IMAGES[name].try(:first) + end + + def required_height + VALID_IMAGES[name].try(:second) + end + + private + + def check_image + return unless image? + + dimensions = Paperclip::Geometry.from_file(image.queued_for_write[:original].path) + + errors.add(:image, :image_width, required_width: required_width) unless dimensions.width == required_width + errors.add(:image, :image_height, required_height: required_height) unless dimensions.height == required_height + end + +end diff --git a/app/models/site_customization/page.rb b/app/models/site_customization/page.rb new file mode 100644 index 000000000..c2a9b1467 --- /dev/null +++ b/app/models/site_customization/page.rb @@ -0,0 +1,16 @@ +class SiteCustomization::Page < ActiveRecord::Base + VALID_STATUSES = %w(draft published) + + validates :slug, presence: true, + uniqueness: { case_sensitive: false }, + format: { with: /\A[0-9a-zA-Z\-_]*\Z/, message: :slug_format } + validates :title, presence: true + validates :status, presence: true, inclusion: { in: VALID_STATUSES } + + scope :published, -> { where(status: 'published').order('id DESC') } + scope :with_more_info_flag, -> { where(status: 'published', more_info_flag: true).order('id ASC') } + + def url + "/#{slug}" + end +end diff --git a/app/models/user.rb b/app/models/user.rb index b32614f86..1c6d3d2eb 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -15,6 +15,7 @@ class User < ActiveRecord::Base has_one :moderator has_one :valuator has_one :manager + has_one :poll_officer, class_name: "Poll::Officer" has_one :organization has_one :lock has_many :flags @@ -57,6 +58,7 @@ class User < ActiveRecord::Base scope :by_document, -> (document_type, document_number) { where(document_type: document_type, document_number: document_number) } scope :email_digest, -> { where(email_digest: true) } scope :active, -> { where(erased_at: nil) } + scope :erased, -> { where.not(erased_at: nil) } before_validation :clean_document_number @@ -125,6 +127,10 @@ class User < ActiveRecord::Base manager.present? end + def poll_officer? + poll_officer.present? + end + def organization? organization.present? end @@ -190,6 +196,22 @@ class User < ActiveRecord::Base erased_at.present? end + def take_votes_if_erased_document(document_number, document_type) + erased_user = User.erased.where(document_number: document_number).where(document_type: document_type).first + if erased_user.present? + self.take_votes_from(erased_user) + erased_user.update(document_number: nil, document_type: nil) + end + end + + def take_votes_from(other_user) + return if other_user.blank? + Poll::Voter.where(user_id: other_user.id).update_all(user_id: self.id) + Budget::Ballot.where(user_id: other_user.id).update_all(user_id: self.id) + Vote.where("voter_id = ? AND voter_type = ?", other_user.id, "User").update_all(voter_id: self.id) + self.update(former_users_data_log: "#{self.former_users_data_log} | id: #{other_user.id} - #{Time.current.strftime('%Y-%m-%d %H:%M:%S')}") + end + def locked? Lock.find_or_create_by(user: self).locked? end @@ -243,6 +265,10 @@ class User < ActiveRecord::Base "#{name} (#{email})" end + def age + Age.in_years(date_of_birth) + end + def save_requiring_finish_signup begin self.registering_with_oauth = true diff --git a/app/models/verification/management/document.rb b/app/models/verification/management/document.rb index 4264154d3..420dcf49c 100644 --- a/app/models/verification/management/document.rb +++ b/app/models/verification/management/document.rb @@ -32,7 +32,7 @@ class Verification::Management::Document end def under_age?(response) - User.minimum_required_age.years.ago.beginning_of_day < response.date_of_birth.beginning_of_day + response.date_of_birth.blank? || Age.in_years(response.date_of_birth) < User.minimum_required_age end def verified? diff --git a/app/models/verification/residence.rb b/app/models/verification/residence.rb index eda562671..ea000677f 100644 --- a/app/models/verification/residence.rb +++ b/app/models/verification/residence.rb @@ -26,6 +26,9 @@ class Verification::Residence def save return false unless valid? + + user.take_votes_if_erased_document(document_number, document_type) + user.update(document_number: document_number, document_type: document_type, geozone: self.geozone, @@ -36,11 +39,11 @@ class Verification::Residence def allowed_age return if errors[:date_of_birth].any? - errors.add(:date_of_birth, I18n.t('verification.residence.new.error_not_allowed_age')) unless self.date_of_birth <= User.minimum_required_age.years.ago + errors.add(:date_of_birth, I18n.t('verification.residence.new.error_not_allowed_age')) unless Age.in_years(self.date_of_birth) >= User.minimum_required_age end def document_number_uniqueness - errors.add(:document_number, I18n.t('errors.messages.taken')) if User.where(document_number: document_number).any? + errors.add(:document_number, I18n.t('errors.messages.taken')) if User.active.where(document_number: document_number).any? end def store_failed_attempt diff --git a/app/views/admin/_menu.html.erb b/app/views/admin/_menu.html.erb index d603dbf16..0b9929f46 100644 --- a/app/views/admin/_menu.html.erb +++ b/app/views/admin/_menu.html.erb @@ -1,126 +1,166 @@ - + diff --git a/app/views/admin/banners/edit.html.erb b/app/views/admin/banners/edit.html.erb index 9f2ad0ac0..17cb11405 100644 --- a/app/views/admin/banners/edit.html.erb +++ b/app/views/admin/banners/edit.html.erb @@ -1,10 +1,7 @@ diff --git a/app/views/budgets/ballot/_ballot.html.erb b/app/views/budgets/ballot/_ballot.html.erb index b83005185..51f2750e9 100644 --- a/app/views/budgets/ballot/_ballot.html.erb +++ b/app/views/budgets/ballot/_ballot.html.erb @@ -1,6 +1,6 @@
- <%= render 'shared/back_link' %> + <%= back_link_to @ballot_referer %>

<%= t("budgets.ballots.show.title") %>

@@ -19,13 +19,18 @@
-
- <% @ballot.groups.each do |group| %> -
-

- <%= group.name %> - <%= group.headings.first.name %> -

+ <% ballot_groups = @ballot.groups.order(name: :asc) %> + <% ballot_groups.each do |group| %> +
+
+
+

+ <%= group.name %> - <%= @ballot.heading_for_group(group).name %> +

+ <%= link_to t("budgets.ballots.show.remaining", + amount: @ballot.formatted_amount_available(@ballot.heading_for_group(group))).html_safe, + budget_group_path(@budget, group) %> +
<% if @ballot.has_lines_in_group?(group) %>

<%= t("budgets.ballots.show.amount_spent") %> @@ -43,12 +48,22 @@ <%= render partial: 'budgets/ballot/investment', collection: @ballot.investments.by_group(group.id) %> - -

- <%= t("budgets.ballots.show.remaining", - amount: @ballot.formatted_amount_available(@ballot.heading_for_group(group))).html_safe %> -

- <% end %> -
+
+ <% end %> + + <% no_balloted_groups = @budget.groups.order(name: :asc) - ballot_groups %> + <% no_balloted_groups.each do |group| %> +
+
+
+

+ <%= group.name %> +

+ <%= link_to t("budgets.ballots.show.no_balloted_group_yet"), budget_group_path(@budget, group) %> +
+
+
+ <% end %> +
diff --git a/app/views/budgets/ballot/_investment_for_sidebar.html.erb b/app/views/budgets/ballot/_investment_for_sidebar.html.erb index 059951253..5dc288aaa 100644 --- a/app/views/budgets/ballot/_investment_for_sidebar.html.erb +++ b/app/views/budgets/ballot/_investment_for_sidebar.html.erb @@ -10,7 +10,7 @@ method: :delete, remote: true do %> <%= t('budgets.ballots.show.remove') %> - + <% end %> <% end %> diff --git a/app/views/budgets/groups/show.html.erb b/app/views/budgets/groups/show.html.erb index 73fb3288f..27d747c64 100644 --- a/app/views/budgets/groups/show.html.erb +++ b/app/views/budgets/groups/show.html.erb @@ -7,15 +7,21 @@
-<% if params[:unfeasible] %> +<% if @current_filter == "unfeasible" %>

<%= t("budgets.groups.show.unfeasible_title") %>

+<% elsif @current_filter == "unselected" %> +
+
+

<%= t("budgets.groups.show.unselected_title") %>

+
+
<% end %> -
+
<% @group.headings.each_slice(7) do |slice| %> @@ -25,7 +31,7 @@ class="<%= css_for_ballot_heading(heading) %>"> <%= link_to heading.name, budget_investments_path(heading_id: heading.id, - unfeasible: params[:unfeasible]), + filter: @current_filter), data: { no_turbolink: true } %>
<% end %> @@ -39,11 +45,26 @@
-<% if params[:unfeasible].blank? %> -
-
- <%= link_to t("budgets.groups.show.unfeasible"), - budget_path(@budget, unfeasible: 1) %> +<% if @budget.balloting_or_later? %> + <% unless @current_filter == "unfeasible" %> +
+
+ + <%= link_to t("budgets.groups.show.unfeasible"), + budget_path(@budget, filter: "unfeasible") %> + +
-
+ <% end %> + + <% unless @current_filter == "unselected" %> +
+
+ + <%= link_to t("budgets.groups.show.unselected"), + budget_path(@budget, filter: "unselected") %> + +
+
+ <% end %> <% end %> diff --git a/app/views/budgets/index.html.erb b/app/views/budgets/index.html.erb index 73fc3a697..9f5ab0ab2 100644 --- a/app/views/budgets/index.html.erb +++ b/app/views/budgets/index.html.erb @@ -1,3 +1,7 @@ +<% content_for :canonical do %> + <%= render "shared/canonical", href: budgets_url %> +<% end %> +
@@ -10,8 +14,10 @@
- - + + + + <% @budgets.each do |budget| %> diff --git a/app/views/budgets/investments/_header.html.erb b/app/views/budgets/investments/_header.html.erb index b315f4ac1..2ed01d989 100644 --- a/app/views/budgets/investments/_header.html.erb +++ b/app/views/budgets/investments/_header.html.erb @@ -40,7 +40,7 @@ <%= t("budgets.investments.header.different_heading_assigned_html", heading_link: link_to( @assigned_heading.name, - budget_investments_path(@budget, heading: @assigned_heading)) + budget_investments_path(@budget, heading_id: @assigned_heading.try(:id))) ) %> diff --git a/app/views/budgets/investments/_investment.html.erb b/app/views/budgets/investments/_investment.html.erb index c9391cbe5..3155853cd 100644 --- a/app/views/budgets/investments/_investment.html.erb +++ b/app/views/budgets/investments/_investment.html.erb @@ -73,6 +73,13 @@ ballot: ballot } %> + <% elsif investment.should_show_price? %> +
+

+ <%= investment.formatted_price %> +

+
<% end %> <% end %> diff --git a/app/views/budgets/investments/_investment_show.html.erb b/app/views/budgets/investments/_investment_show.html.erb index 17abf4faf..f9a371585 100644 --- a/app/views/budgets/investments/_investment_show.html.erb +++ b/app/views/budgets/investments/_investment_show.html.erb @@ -47,7 +47,7 @@

<%= investment.unfeasibility_explanation %>

<% end %> - <% if investment.feasible? && investment.price_explanation.present? %> + <% if investment.should_show_price_info? %>

<%= t('budgets.investments.show.price_explanation') %>

<%= investment.price_explanation %>

<% end %> @@ -98,15 +98,24 @@
<% end %> + <% if investment.should_show_price? %> + +

<%= t("budgets.investments.show.price") %>

+
+

+ <%= investment.formatted_price %> +

+
+ <% end %>

<%= t("budgets.investments.show.share") %>

diff --git a/app/views/budgets/investments/_sidebar.html.erb b/app/views/budgets/investments/_sidebar.html.erb index 830b60634..2776a823f 100644 --- a/app/views/budgets/investments/_sidebar.html.erb +++ b/app/views/budgets/investments/_sidebar.html.erb @@ -13,6 +13,14 @@ <% end %> <% end %> +<% if @heading && can?(:show, @ballot) %> +

+ <%= t("budgets.investments.index.sidebar.voted_info", + link: link_to(t("budgets.investments.index.sidebar.voted_info_link"), + budget_ballot_path(@budget))).html_safe %> +

+<% end %> + <%= render "shared/tag_cloud", taggable: 'budget/investment' %> <%= render 'budgets/investments/categories' %> @@ -45,8 +53,4 @@ <% end %> <% end %> - -

- <%= t("budgets.investments.index.sidebar.voted_info") %> -

<% end %> diff --git a/app/views/budgets/investments/index.html.erb b/app/views/budgets/investments/index.html.erb index 50a2866f9..95b60f55b 100644 --- a/app/views/budgets/investments/index.html.erb +++ b/app/views/budgets/investments/index.html.erb @@ -16,7 +16,7 @@
- <% if params[:unfeasible].present? %> + <% if @current_filter == "unfeasible" %>

<%= t("budgets.investments.index.unfeasible") %>: <%= @heading.name %>

<%= t("budgets.investments.index.unfeasible_text", @@ -34,7 +34,7 @@ <% end %>
- <%= render('shared/order_links', i18n_namespace: "budgets.investments.index") unless params[:unfeasible].present? %> + <%= render('shared/order_links', i18n_namespace: "budgets.investments.index") unless @current_filter == "unfeasible" %> <% @investments.each do |investment| %> <%= render partial: '/budgets/investments/investment', locals: { diff --git a/app/views/budgets/show.html.erb b/app/views/budgets/show.html.erb index e49cd4252..84b7691f3 100644 --- a/app/views/budgets/show.html.erb +++ b/app/views/budgets/show.html.erb @@ -35,10 +35,12 @@
-
+
- <% if params[:unfeasible] %> + <% if @current_filter == "unfeasible" %>

<%= t("budgets.show.unfeasible_title") %>

+ <% elsif @current_filter == "unselected" %> +

<%= t("budgets.show.unselected_title") %>

<% end %>
<%= Budget.human_attribute_name(:name) %><%= Budget.human_attribute_name(:phase) %>
<%= Budget.human_attribute_name(:name) %><%= Budget.human_attribute_name(:phase) %>
@@ -52,12 +54,12 @@ <%= link_to group.name, budget_investments_path(@budget, heading_id: group.headings.first.id, - unfeasible: params[:unfeasible]), + filter: @current_filter), data: { no_turbolink: true } %> <% else %> <%= link_to group.name, budget_group_path(@budget, group, - unfeasible: params[:unfeasible]) %> + filter: @current_filter) %> <% end %>
@@ -68,11 +70,26 @@ -<% unless params[:unfeasible] %> -
-
- <%= link_to t("budgets.show.unfeasible"), - budget_path(@budget, unfeasible: 1) %> +<% if @budget.balloting_or_later? %> + <% unless @current_filter == "unfeasible" %> +
+
+ + <%= link_to t("budgets.show.unfeasible"), + budget_path(@budget, filter: "unfeasible") %> + +
-
+ <% end %> + + <% unless @current_filter == "unselected" %> +
+
+ + <%= link_to t("budgets.show.unselected"), + budget_path(@budget, filter: "unselected") %> + +
+
+ <% end %> <% end %> diff --git a/app/views/debates/index.html.erb b/app/views/debates/index.html.erb index 45d27bd56..b7796f0ba 100644 --- a/app/views/debates/index.html.erb +++ b/app/views/debates/index.html.erb @@ -4,6 +4,9 @@ search_path: debates_path(page: 1), i18n_namespace: "debates.index.search_form" %> <% end %> +<% content_for :canonical do %> + <%= render "shared/canonical", href: debates_url %> +<% end %>

<%= t("shared.outline.debates") %>

@@ -27,15 +30,15 @@ <% end %>
- <% if has_banners %> + <% if has_banners? %> <%= render "shared/banner" %> <% end %> - <% if @proposal_successfull_exists %> - <%= render "proposals/proposal_ballots_banner" %> + <% if @proposal_successful_exists %> + <%= render "proposals/successful_banner" %> <% end %> - <% unless @tag_filter || @search_terms || !has_featured? || @proposal_ballots.present? || @proposal_successfull_exists %> + <% unless @tag_filter || @search_terms || !has_featured? || @proposal_successful_exists %> <%= render "featured_debates" %> <% end %> diff --git a/app/views/debates/new.html.erb b/app/views/debates/new.html.erb index cd4a92cc6..ccd11bec2 100644 --- a/app/views/debates/new.html.erb +++ b/app/views/debates/new.html.erb @@ -7,7 +7,7 @@
<%= t("debates.new.info", info_link: link_to(t("debates.new.info_link"), new_proposal_path )).html_safe %> - <%= link_to "/more_information", title: t('shared.target_blank_html'), target: "_blank" do %> + <%= link_to more_info_path, title: t('shared.target_blank_html'), target: "_blank" do %> <%= t("debates.new.more_info") %> <% end %>
diff --git a/app/views/debates/show.html.erb b/app/views/debates/show.html.erb index 363a3bab3..532181928 100644 --- a/app/views/debates/show.html.erb +++ b/app/views/debates/show.html.erb @@ -52,11 +52,11 @@

<%= t("debates.show.share") %>

diff --git a/app/views/devise/confirmations/new.html.erb b/app/views/devise/confirmations/new.html.erb index fe2cf0dc5..51f6cf1eb 100644 --- a/app/views/devise/confirmations/new.html.erb +++ b/app/views/devise/confirmations/new.html.erb @@ -19,6 +19,7 @@ <%= f.submit(t("devise_views.confirmations.new.submit"), class: "button expanded") %> + <% end %> <%= render "devise/shared/links" %> diff --git a/app/views/devise/menu/_login_items.html.erb b/app/views/devise/menu/_login_items.html.erb index 0ab28e2ee..5d493131d 100644 --- a/app/views/devise/menu/_login_items.html.erb +++ b/app/views/devise/menu/_login_items.html.erb @@ -1,6 +1,6 @@ <% if user_signed_in? %>
  • - <%= link_to notifications_path, class: "notifications", accesskey: "n" do %> + <%= link_to notifications_path, rel: "nofollow", class: "notifications" do %> <%= t("layouts.header.notifications") %> <% if current_user.notifications_count > 0 %> @@ -12,19 +12,24 @@ <% end %>
  • - <%= link_to(t("layouts.header.my_activity_link"), user_path(current_user), accesskey: "a") %> + <%= link_to t("layouts.header.my_activity_link"), + user_path(current_user), rel: "nofollow" %>
  • - <%= link_to(t("layouts.header.my_account_link"), account_path, accesskey: "m") %> + <%= link_to t("layouts.header.my_account_link"), + account_path, rel: "nofollow" %>
  • - <%= link_to(t("devise_views.menu.login_items.logout"), destroy_user_session_path, method: :delete) %> + <%= link_to t("devise_views.menu.login_items.logout"), + destroy_user_session_path, rel: "nofollow", method: :delete %>
  • <% else %>
  • - <%= link_to(t("devise_views.menu.login_items.login"), new_user_session_path) %> + <%= link_to t("devise_views.menu.login_items.login"), + new_user_session_path, rel: "nofollow" %>
  • - <%= link_to(t("devise_views.menu.login_items.signup"), new_user_registration_path, class: "button") %> + <%= link_to t("devise_views.menu.login_items.signup"), + new_user_registration_path, rel: "nofollow", class: "button" %>
  • <% end %> diff --git a/app/views/devise/passwords/new.html.erb b/app/views/devise/passwords/new.html.erb index 72dd70fb9..b06c9ff25 100644 --- a/app/views/devise/passwords/new.html.erb +++ b/app/views/devise/passwords/new.html.erb @@ -11,6 +11,7 @@ <%= f.submit t("devise_views.passwords.new.send_submit"), class: "button expanded" %> + <% end %> <%= render "devise/shared/links" %> diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb index ad375e772..910135853 100644 --- a/app/views/devise/sessions/new.html.erb +++ b/app/views/devise/sessions/new.html.erb @@ -11,18 +11,18 @@ <%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
    - <%= f.email_field :email, autofocus: true, placeholder: t("devise_views.sessions.new.email_label"), tabindex: "1" %> + <%= f.email_field :email, autofocus: true, placeholder: t("devise_views.sessions.new.email_label") %>
    - <%= link_to t("devise_views.shared.links.new_password"), new_password_path(resource_name), class: "float-right", tabindex: "3" %> - <%= f.password_field :password, autocomplete: "off", placeholder: t("devise_views.sessions.new.password_label"), tabindex: "2" %> + <%= f.password_field :password, autocomplete: "off", placeholder: t("devise_views.sessions.new.password_label") %> + <%= link_to t("devise_views.shared.links.new_password"), new_password_path(resource_name), class: "float-right" %>
    <% if devise_mapping.rememberable? -%>
    <%= f.label :remember_me do %> - <%= f.check_box :remember_me, title: t('devise_views.sessions.new.remember_me'), label: false, tabindex: "4" %> + <%= f.check_box :remember_me, title: t('devise_views.sessions.new.remember_me'), label: false %> <%= t("devise_views.sessions.new.remember_me") %> <% end %>
    @@ -35,5 +35,3 @@ <% end %> <%= render "devise/shared/links" %> - - diff --git a/app/views/layouts/_admin_header.html.erb b/app/views/layouts/_admin_header.html.erb index 095e351d6..8597b41de 100644 --- a/app/views/layouts/_admin_header.html.erb +++ b/app/views/layouts/_admin_header.html.erb @@ -1,14 +1,16 @@
    - +
    - <%= link_to setting['org_name'], admin_root_path, class: "logo show-for-small-only" %> + <%= link_to setting['org_name'], namespaced_root_path, class: "logo show-for-small-only" %> @@ -17,11 +19,13 @@
    - <%= link_to admin_root_path, class: "hide-for-small-only" do %> - <%= image_tag('logo_header.png', class: 'hide-for-small-only float-left', size: '80x80', alt: t("layouts.header.logo")) %> - <%= setting['org_name'] %> -  | <%= t("admin.dashboard.index.title") %> - <% end %> +

    + <%= link_to namespaced_root_path, class: "hide-for-small-only" do %> + <%= image_tag(image_path_for('logo_header.png'), class: 'hide-for-small-only float-left', size: '80x80', alt: t("layouts.header.logo")) %> + <%= setting['org_name'] %> +  | <%= namespaced_header_title %> + <% end %> +

    @@ -34,4 +38,4 @@
    - \ No newline at end of file + diff --git a/app/views/layouts/_footer.html.erb b/app/views/layouts/_footer.html.erb index 4f8545fb5..2e2f5c063 100644 --- a/app/views/layouts/_footer.html.erb +++ b/app/views/layouts/_footer.html.erb @@ -1,5 +1,5 @@
    -
    +

    <%= link_to t("layouts.header.open_gov", open: "#{t('layouts.header.open')}").html_safe %> @@ -11,29 +11,23 @@ consul: link_to(t("layouts.footer.consul"), t("layouts.footer.consul_url"), target: "blank")).html_safe %> <%= t("layouts.footer.contact_us") %> - <%= link_to t("layouts.footer.faq"), page_path('faq') %> + <%= link_to t("layouts.footer.faq"), faq_path %>

    @@ -43,7 +37,6 @@
    <%= t("layouts.footer.copyright", year: Time.current.year) %> |
      -
    • <%= link_to t("layouts.footer.more_info"), page_path('more_information') %> |
    • <%= link_to t("layouts.footer.privacy"), page_path('privacy') %> |
    • <%= link_to t("layouts.footer.conditions"), page_path('conditions') %> |
    • <%= link_to t("layouts.footer.accessibility"), page_path('accessibility') %>
    • @@ -55,7 +48,7 @@
        <% if setting['twitter_handle'] %>
      • - <%= link_to "https://twitter.com/#{setting['twitter_handle']}", target: "_blank", title: t("social.twitter") + t('shared.target_blank_html') do %> + <%= link_to "https://twitter.com/#{setting['twitter_handle']}", target: "_blank", title: t("social.twitter") + t('shared.target_blank_html'), rel: "nofollow" do %> <%= t("social.twitter") %> <% end %> @@ -63,7 +56,7 @@ <% end %> <% if setting['facebook_handle'] %>
      • - <%= link_to "https://www.facebook.com/#{setting['facebook_handle']}/", target: "_blank", title: t("social.facebook") + t('shared.target_blank_html') do %> + <%= link_to "https://www.facebook.com/#{setting['facebook_handle']}/", target: "_blank", title: t("social.facebook") + t('shared.target_blank_html'), rel: "nofollow" do %> <%= t("social.facebook") %> <% end %> @@ -71,7 +64,7 @@ <% end %> <% if setting['blog_url'] %>
      • - <%= link_to setting['blog_url'], target: "_blank", title: t("social.blog") + t('shared.target_blank_html') do %> + <%= link_to setting['blog_url'], target: "_blank", title: t("social.blog") + t('shared.target_blank_html'), rel: "nofollow" do %> <%= t("social.blog") %> <% end %> @@ -79,14 +72,26 @@ <% end %> <% if setting['youtube_handle'] %>
      • - <%= link_to "https://www.youtube.com/#{setting['youtube_handle']}", target: "_blank", title: t("social.youtube") + t('shared.target_blank_html') do %> + <%= link_to "https://www.youtube.com/#{setting['youtube_handle']}", target: "_blank", title: t("social.youtube") + t('shared.target_blank_html'), rel: "nofollow" do %> <%= t("social.youtube") %> <% end %>
      • <% end %> + <% if setting['telegram_handle'] %> +
      • + <%= link_to "https://www.telegram.me/#{setting['telegram_handle']}", target: "_blank", title: t("social.telegram") + t('shared.target_blank_html'), rel: "nofollow" do %> + <%= t("social.telegram") %> + + <% end %> +
      • + <% end %>
    + +
    + <%= raw content_block("footer", I18n.locale) %> +
    diff --git a/app/views/layouts/_header.html.erb b/app/views/layouts/_header.html.erb index 6de8ed89e..a3162bb8c 100644 --- a/app/views/layouts/_header.html.erb +++ b/app/views/layouts/_header.html.erb @@ -1,6 +1,13 @@
    - <%= render "shared/top_links" %> +
    @@ -14,8 +21,8 @@
    - <%= link_to root_path, class: "hide-for-small-only", accesskey: "/" do %> - <%= image_tag('logo_header.png', class: 'hide-for-small-only float-left', size: '80x80', alt: t("layouts.header.logo")) %> + <%= link_to root_path, class: "hide-for-small-only", accesskey: "0" do %> + <%= image_tag(image_path_for('logo_header.png'), class: 'hide-for-small-only float-left', size: '80x80', alt: t("layouts.header.logo")) %> <%= setting['org_name'] %> <% end %>
    @@ -29,6 +36,9 @@
    diff --git a/app/views/layouts/admin.html.erb b/app/views/layouts/admin.html.erb index 95bcae3e3..198bf3adf 100644 --- a/app/views/layouts/admin.html.erb +++ b/app/views/layouts/admin.html.erb @@ -30,7 +30,7 @@
    <%= render 'layouts/admin_header' %> -
    +
    @@ -42,11 +42,11 @@
    -
    +
    <%= render 'layouts/flash' %> <%= yield %>
    -
    +
    diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 7a1dae4d9..d410140e5 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -7,6 +7,7 @@ <%= render "layouts/tracking_data" %> <%= render "layouts/meta_tags" %> <%= content_for?(:title) ? yield(:title) : setting['org_name'] %> + <%= content_for :canonical %> <%= stylesheet_link_tag "application" %> +
    +
    + + <%= select_tag :officer_assignment_id, + officer_assignments_select_options(@officer_assignments), + { prompt: t("officing.recounts.new.select_booth_date"), + label: false } %> +
    +
    + +
    +
    + + <%= text_field_tag :count, nil, placeholder: t("officing.recounts.new.count_placeholder") %> +
    +
    + +
    +
    + <%= submit_tag t("officing.recounts.new.submit"), class: "button expanded" %> +
    +
    + <% end %> +<% else %> +

    <%= @poll.name %>

    +
    + <%= t("officing.recounts.new.not_allowed") %> +
    +<% end %> + + +<% if @recounted.any? %> +
    +

    <%= t("officing.recounts.new.recount_list") %>

    + +
    + + + + + + + <% @recounted.each do |oa| %> + + + + + + <% end %> + +
    <%= t("officing.recounts.new.date") %><%= t("officing.recounts.new.booth") %><%= t("officing.recounts.new.count") %>
    + <%= l(oa.date.to_date, format: :long) %> + + <%= oa.booth_assignment.booth.name %> + + <%= oa.recount.count %> +
    +<% end %> diff --git a/app/views/officing/residence/_errors.html.erb b/app/views/officing/residence/_errors.html.erb new file mode 100644 index 000000000..4f8895a35 --- /dev/null +++ b/app/views/officing/residence/_errors.html.erb @@ -0,0 +1,14 @@ +<% if @residence.errors[:residence_in_madrid].present? %> + +
    + + <%= t("officing.residence.new.error_verifying_census") %> +
    + +<% else %> + <%= render "shared/errors", + resource: @residence, + message: t("officing.residence.new.form_errors") %> +<% end %> diff --git a/app/views/officing/residence/new.html.erb b/app/views/officing/residence/new.html.erb new file mode 100644 index 000000000..a508fae52 --- /dev/null +++ b/app/views/officing/residence/new.html.erb @@ -0,0 +1,32 @@ +

    <%= t("officing.residence.new.title") %>

    + +<% if @officer_assignments.present? %> + +<% else %> +
    + <%= t("officing.residence.new.no_assignments") %> +
    +<% end %> diff --git a/app/views/officing/results/index.html.erb b/app/views/officing/results/index.html.erb new file mode 100644 index 000000000..70da6d8d3 --- /dev/null +++ b/app/views/officing/results/index.html.erb @@ -0,0 +1,57 @@ +<%= back_link_to new_officing_poll_result_path(@poll) %> +

    <%= @poll.name %> - <%= t("officing.results.index.results") %>

    + +<% if @partial_results.present? %> +
    +

    + <%= @booth_assignment.booth.name %> - <%= l @partial_results.first.date, format: :long %> +

    +
    + + +
    +
    + + + + + + + + + + + + + +
    <%= t("officing.results.index.table_whites") %><%= t("officing.results.index.table_nulls") %>
    <%= @whites %><%= @nulls %>
    + + <% by_question = @partial_results.group_by(&:question_id) %> + <% @poll.questions.each do |question| %> +

    <%= question.title %>

    + + + + + + + + + + <% question.valid_answers.each_with_index do |answer, i| %> + <% by_answer = by_question[question.id].present? ? by_question[question.id].group_by(&:answer) : {} %> + + + + + <% end %> + +
    <%= t("officing.results.index.table_answer") %><%= t("officing.results.index.table_votes") %>
    <%= answer %><%= by_answer[answer].present? ? by_answer[answer].first.amount : 0 %>
    + <% end %> +
    +
    +<% else %> +
    + <%= t("officing.results.index.no_results") %> +
    +<% end %> \ No newline at end of file diff --git a/app/views/officing/results/new.html.erb b/app/views/officing/results/new.html.erb new file mode 100644 index 000000000..74af8af10 --- /dev/null +++ b/app/views/officing/results/new.html.erb @@ -0,0 +1,99 @@ +<% if @officer_assignments.any? %> +

    <%= t("officing.results.new.title", poll: @poll.name) %>

    + + <%= form_tag(officing_poll_results_path(@poll), {id: "officer_assignment_form"}) do %> +
    +
    + + <%= select_tag :officer_assignment_id, + booths_for_officer_select_options(@officer_assignments), + { prompt: t("officing.results.new.select_booth"), + label: false } %> +
    +
    + +
    +
    + + <%= select_tag :date, + poll_dates_select_options(@poll), + { prompt: t("officing.results.new.select_date"), + label: false } %> +
    +
    + + <% @poll.questions.each do |question| %> +
    +
    +

    <%= question.title %>

    +
    + <% question.valid_answers.each_with_index do |answer, i| %> +
    + + <%= text_field_tag "questions[#{question.id}][#{i}]", answer_result_value(question.id, i), placeholder: "0" %> +
    + <% end %> +
    +
    + <% end %> + +
    +
    +

    <%= t("officing.results.new.ballots_white") %>

    + <%= text_field_tag :whites, params[:whites].presence, placeholder: "0" %> +
    + +
    +

    <%= t("officing.results.new.ballots_null") %>

    + <%= text_field_tag :nulls, params[:nulls].presence, placeholder: "0" %> +
    +
    +
    + +
    +
    + <%= submit_tag t("officing.results.new.submit"), class: "button expanded" %> +
    +
    + <% end %> + +<% else %> +

    <%= @poll.name %>

    +
    + <%= t("officing.results.new.not_allowed") %> +
    +<% end %> + +<% if @partial_results.present? %> + +
    +

    <%= t("officing.results.new.results_list") %>

    + + + + + + + + + <% results_by_booth = @partial_results.group_by(&:booth_assignment_id) %> + <% results_by_booth.keys.each do |booth_assignment| %> + <% results_by_booth[booth_assignment].group_by(&:date).keys.each do |date| %> + + + + + + <% end %> + <% end %> + +
    <%= t("officing.results.new.date") %><%= t("officing.results.new.booth") %> 
    + <%= l(date, format: :long) %> + + <%= results_by_booth[booth_assignment].first.booth_assignment.booth.name %> + + <%= link_to t("officing.results.new.see_results"), officing_poll_results_path(@poll, date: l(date), booth_assignment_id: booth_assignment) %> +
    + +<% end %> + diff --git a/app/views/officing/voters/_already_voted.html.erb b/app/views/officing/voters/_already_voted.html.erb new file mode 100644 index 000000000..1476913f9 --- /dev/null +++ b/app/views/officing/voters/_already_voted.html.erb @@ -0,0 +1,7 @@ +
    +
    +
    + <%= t("officing.voters.show.error_already_voted") %> +
    +
    +
    diff --git a/app/views/officing/voters/_can_vote.html.erb b/app/views/officing/voters/_can_vote.html.erb new file mode 100644 index 000000000..27c5e9dbc --- /dev/null +++ b/app/views/officing/voters/_can_vote.html.erb @@ -0,0 +1,14 @@ +
    +
    +
    + <%= t("officing.voters.show.can_vote") %> +
    +
    +
    + <%= form_for @user, as: :voter, url: officing_voters_path, method: :post, remote: true do |f| %> + <%= f.hidden_field :poll_id, value: poll.id %> + <%= f.hidden_field :user_id, value: @user.id %> + <%= f.submit t("officing.voters.show.submit"), class: "button success expanded" %> + <% end %> +
    +
    diff --git a/app/views/officing/voters/_voted.html.erb b/app/views/officing/voters/_voted.html.erb new file mode 100644 index 000000000..edd098403 --- /dev/null +++ b/app/views/officing/voters/_voted.html.erb @@ -0,0 +1,7 @@ +
    +
    +
    + <%= t("officing.voters.show.success") %> +
    +
    +
    diff --git a/app/views/officing/voters/create.js.erb b/app/views/officing/voters/create.js.erb new file mode 100644 index 000000000..ee280faa2 --- /dev/null +++ b/app/views/officing/voters/create.js.erb @@ -0,0 +1 @@ +$("#<%= dom_id(@poll) %> #actions").html('<%= j render("voted") %>'); \ No newline at end of file diff --git a/app/views/officing/voters/new.html.erb b/app/views/officing/voters/new.html.erb new file mode 100644 index 000000000..0f8fa2958 --- /dev/null +++ b/app/views/officing/voters/new.html.erb @@ -0,0 +1,30 @@ +<%= back_link_to new_officing_residence_path %> + +

    <%= t("officing.voters.new.title") %>

    + +<% if @polls.any? %> + + + + + + + + + <% @polls.each do |poll| %> + + + + + <% end %> + +
    <%= t("officing.voters.new.table_poll") %><%= t("officing.voters.new.table_actions") %>
    + <%= poll.name %> + + <% if poll.votable_by?(@user) %> + <%= render "can_vote", poll: poll %> + <% else %> + <%= render "already_voted" %> + <% end %> +
    +<% end %> \ No newline at end of file diff --git a/app/views/pages/accessibility.html.erb b/app/views/pages/accessibility.html.erb index a2c2eff30..a0f54760d 100644 --- a/app/views/pages/accessibility.html.erb +++ b/app/views/pages/accessibility.html.erb @@ -19,45 +19,34 @@
    + - - + + - + - + - + - + + + + + - - - - - - - - - - - - - - - -
    Atajos de teclado para el menú de navegación
    TeclaPáginaTeclaPágina
    /0 Inicio
    D1 Debates
    P2 Propuestas
    S3Votaciones
    4 Presupuestos participativos
    IMás información
    NNotificaciones
    AMi actividad
    MMi cuenta
    @@ -66,10 +55,11 @@
    + - - + + @@ -104,8 +94,8 @@
    Combinación de teclas dependiendo del sistema operativo y navegador
    NavegadorCombinación de teclasNavegadorCombinación de teclas
    - - + + diff --git a/app/views/pages/custom_page.html.erb b/app/views/pages/custom_page.html.erb new file mode 100644 index 000000000..f14eba7d9 --- /dev/null +++ b/app/views/pages/custom_page.html.erb @@ -0,0 +1,15 @@ +<% provide :title do %><%= @custom_page.title %><% end %> + +
    + +
    +

    <%= @custom_page.title %>

    +

    <%= @custom_page.subtitle %>

    + + <%= raw @custom_page.content %> +
    + +
    + <%= render '/shared/print' if @custom_page.print_content_flag %> +
    +
    diff --git a/app/views/pages/how_it_works.html.erb b/app/views/pages/how_it_works.html.erb deleted file mode 100644 index 67fb38371..000000000 --- a/app/views/pages/how_it_works.html.erb +++ /dev/null @@ -1,20 +0,0 @@ -
    - - -
    -

    <%= t('pages.more_information.titles.how_it_works') %>

    - <%= markdown t('pages.more_information.how_it_works.text') %> -
    -
    diff --git a/app/views/pages/how_to_use.html.erb b/app/views/pages/how_to_use.html.erb deleted file mode 100644 index a594961a8..000000000 --- a/app/views/pages/how_to_use.html.erb +++ /dev/null @@ -1,9 +0,0 @@ -
    -
    - <%= render "shared/back_link" %> - -

    <%= t('pages.more_information.titles.how_to_use') %>

    - - <%= markdown t('pages.more_information.how_to_use.text') %> -
    -
    diff --git a/app/views/pages/more_info/_budgets.html.erb b/app/views/pages/more_info/_budgets.html.erb new file mode 100644 index 000000000..6450bdffe --- /dev/null +++ b/app/views/pages/more_info/_budgets.html.erb @@ -0,0 +1,23 @@ +
    +
    +

    + <%= t("pages.more_info.budgets.title") %> +

    +

    <%= t("pages.more_info.budgets.description") %>

    +
      +
    • + <%= t("pages.more_info.budgets.feature_1", + link: link_to(t("pages.more_info.budgets.feature_1_link", org_name: setting['org_name']), + new_user_registration_path)).html_safe %> +
    • +
    • <%= t("pages.more_info.budgets.feature_2_html") %>
    • +
    • <%= t("pages.more_info.budgets.feature_3_html") %>
    • +
    • <%= t("pages.more_info.budgets.feature_4_html") %>
    • +
    + +
    + <%= image_tag "more_info/budgets_#{I18n.locale}.png", alt: t("pages.more_info.budgets.image_alt") %> +
    <%= t("pages.more_info.budgets.figcaption_html") %>
    +
    +
    +
    diff --git a/app/views/pages/more_info/_debates.html.erb b/app/views/pages/more_info/_debates.html.erb new file mode 100644 index 000000000..a8e76ba91 --- /dev/null +++ b/app/views/pages/more_info/_debates.html.erb @@ -0,0 +1,21 @@ +
    +
    +

    + <%= t("pages.more_info.debates.title") %> +

    +

    <%= t("pages.more_info.debates.description") %>

    +
      +
    • + <%= t("pages.more_info.debates.feature_1", + link: link_to(t("pages.more_info.debates.feature_1_link", org_name: setting['org_name']), + new_user_registration_path)).html_safe %> +
    • +
    • <%= t("pages.more_info.debates.feature_2_html") %>
    • +
    + +
    + <%= image_tag "more_info/debates.png", alt: t("pages.more_info.debates.image_alt") %> +
    <%= t("pages.more_info.debates.figcaption") %>
    +
    +
    +
    diff --git a/app/views/pages/more_info/_menu.html.erb b/app/views/pages/more_info/_menu.html.erb new file mode 100644 index 000000000..85097e6a8 --- /dev/null +++ b/app/views/pages/more_info/_menu.html.erb @@ -0,0 +1,22 @@ +
    +
    + +
    +
    diff --git a/app/views/pages/more_info/_other.html.erb b/app/views/pages/more_info/_other.html.erb new file mode 100644 index 000000000..5501c9e31 --- /dev/null +++ b/app/views/pages/more_info/_other.html.erb @@ -0,0 +1,11 @@ +

    Otra información de interés

    + +
      +
    • <%= link_to t("pages.more_info.other.how_to_use", org_name: setting['org_name']), faq_path %>
    • +
    • <%= link_to t("pages.more_info.other.facts"), participation_facts_path %>
    • +
    • <%= link_to t("pages.more_info.other.world"), participation_world_path %>
    • + + <% SiteCustomization::Page.with_more_info_flag.each do |custom_page| %> +
    • <%= link_to custom_page.title, page_path(custom_page.slug) %>
    • + <% end %> +
    diff --git a/app/views/pages/more_info/_polls.html.erb b/app/views/pages/more_info/_polls.html.erb new file mode 100644 index 000000000..360558d99 --- /dev/null +++ b/app/views/pages/more_info/_polls.html.erb @@ -0,0 +1,15 @@ +
    +
    +

    <%= t("pages.more_info.polls.title") %>

    +

    <%= t("pages.more_info.polls.description") %>

    +
      +
    • + <%= t("pages.more_info.polls.feature_1", + link: link_to(t("pages.more_info.polls.feature_1_link", org_name: setting['org_name']), + new_user_registration_path)).html_safe %> +
    • +
    • <%= t("pages.more_info.polls.feature_2") %>
    • +
    • <%= t("pages.more_info.polls.feature_3") %>
    • +
    +
    +
    diff --git a/app/views/pages/more_info/_proposals.html.erb b/app/views/pages/more_info/_proposals.html.erb new file mode 100644 index 000000000..cf1822142 --- /dev/null +++ b/app/views/pages/more_info/_proposals.html.erb @@ -0,0 +1,22 @@ +
    +
    +

    + <%= t("pages.more_info.proposals.title") %> +

    +

    <%= t("pages.more_info.proposals.description") %>

    +
      +
    • + <%= t("pages.more_info.proposals.feature_1", + link: link_to(t("pages.more_info.proposals.feature_1_link", org_name: setting['org_name']), + new_user_registration_path)).html_safe %> +
    • +
    • <%= t("pages.more_info.proposals.feature_2_html") %>
    • +
    • <%= t("pages.more_info.proposals.feature_3_html") %>
    • +
    + +
    + <%= image_tag "more_info/proposals_#{I18n.locale}.png", alt: t("pages.more_info.proposals.image_alt") %> +
    <%= t("pages.more_info.proposals.figcaption_html") %>
    +
    +
    +
    diff --git a/app/views/pages/more_info/_sidebar.html.erb b/app/views/pages/more_info/_sidebar.html.erb new file mode 100644 index 000000000..09131685b --- /dev/null +++ b/app/views/pages/more_info/_sidebar.html.erb @@ -0,0 +1,5 @@ + diff --git a/app/views/pages/faq.html.erb b/app/views/pages/more_info/faq/index.html similarity index 100% rename from app/views/pages/faq.html.erb rename to app/views/pages/more_info/faq/index.html diff --git a/app/views/pages/more_info/how_to_use/index.html.erb b/app/views/pages/more_info/how_to_use/index.html.erb new file mode 100644 index 000000000..9c6ccfb00 --- /dev/null +++ b/app/views/pages/more_info/how_to_use/index.html.erb @@ -0,0 +1,9 @@ +
    +
    + <%= render "shared/back_link" %> + +

    <%= t('pages.more_info.titles.how_to_use') %>

    + + <%= markdown t('pages.more_info.how_to_use.text') %> +
    +
    diff --git a/app/views/pages/more_info/index.html.erb b/app/views/pages/more_info/index.html.erb new file mode 100644 index 000000000..a005672a3 --- /dev/null +++ b/app/views/pages/more_info/index.html.erb @@ -0,0 +1,46 @@ +<% provide :title do %><%= t("pages.titles.more_info", org_name: setting['org_name']) %><% end %> +<% content_for :canonical do %> + <%= render "shared/canonical", href: more_info_url %> +<% end %> + +
    +
    +
    +

    <%= t("pages.more_info.title", org_name: setting['org_name']) %>

    +

    <%= t("pages.more_info.subtitle") %>

    +

    <%= t("pages.more_info.guide", org_name: setting['org_name']) %>

    +
    +
    + + <%= render "pages/more_info/menu" %> +
    + +
    +
    +
    + <% if feature?(:debates) %> + <%= render "pages/more_info/debates" %> + <% end %> + + <%= render "pages/more_info/proposals" %> + + <% if feature?(:budgets) %> + <%= render "pages/more_info/budgets" %> + <% end %> + + <% if feature?(:polls) %> + <%= render "pages/more_info/polls" %> + <% end %> +
    +
    + <%= render "pages/more_info/sidebar" %> +
    +
    + +
    +
    +
    + <%= render "pages/more_info/other" %> +
    +
    +
    diff --git a/app/views/pages/participation_facts.html.erb b/app/views/pages/more_info/participation/facts.html.erb similarity index 100% rename from app/views/pages/participation_facts.html.erb rename to app/views/pages/more_info/participation/facts.html.erb diff --git a/app/views/pages/participation_world.html.erb b/app/views/pages/more_info/participation/world.html.erb similarity index 100% rename from app/views/pages/participation_world.html.erb rename to app/views/pages/more_info/participation/world.html.erb diff --git a/app/views/pages/more_information.html.erb b/app/views/pages/more_information.html.erb deleted file mode 100644 index 07105ef0f..000000000 --- a/app/views/pages/more_information.html.erb +++ /dev/null @@ -1,71 +0,0 @@ -<% provide :title do %><%= t('pages.titles.more_information') %><% end %> -
    -
    -

    <%= t('pages.titles.more_information') %>

    -
      -
    • - <%= link_to page_path('how_it_works') do %> - <%= t('pages.more_information.titles.how_it_works') %> -
      - <%= t('pages.more_information.description.how_it_works') %> - <% end %> -
    • -
    • - <%= link_to page_path('how_to_use') do %> - <%= t('pages.more_information.titles.how_to_use') %> -
      - <%= t('pages.more_information.description.how_to_use') %> - <% end %> -
    • -
    • - <%= link_to page_path('participation') do %> - <%= t('pages.more_information.titles.participation') %> -
      - <%= t('pages.more_information.description.participation') %> - <% end %> -
    • -
    • - <%= link_to page_path('proposals_info') do %> - <%= t('pages.more_information.titles.proposals_info') %> -
      - <%= t('pages.more_information.description.proposals_info') %> - <% end %> -
    • -
    • - <%= link_to page_path('spending_proposals_info') do %> - <%= t('pages.more_information.titles.spending_proposals_info') %> -
      - <%= t('pages.more_information.description.spending_proposals_info') %> - <% end %> -
    • -
    • - <%= link_to page_path('participation_world') do %> - <%= t('pages.more_information.titles.participation_world') %> -
      - <%= t('pages.more_information.description.participation_world') %> - <% end %> -
    • -
    • - <%= link_to page_path('participation_facts') do %> - <%= t('pages.more_information.titles.participation_facts') %> -
      - <%= t('pages.more_information.description.participation_facts') %> - <% end %> -
    • -
    • - <%= link_to page_path('faq') do %> - <%= t('pages.more_information.titles.faq') %> -
      - <%= t('pages.more_information.description.faq') %> - <% end %> -
    • -
    • - <%= link_to page_path('proposals_info', :anchor => "iii") do %> - <%= t('pages.more_information.titles.signature_sheet') %> -
      - <%= t('pages.more_information.description.signature_sheet') %> - <% end %> -
    • -
    -
    -
    diff --git a/app/views/pages/opendata.html.erb b/app/views/pages/opendata.html.erb deleted file mode 100644 index aeee6b465..000000000 --- a/app/views/pages/opendata.html.erb +++ /dev/null @@ -1,14 +0,0 @@ -<% provide :title do %><%= t('pages.titles.opendata') %><% end %> - -
    -
    -
    -

    <%= t("pages.opendata.title") %>

    -

    <%= t("pages.opendata.slogan") %>

    -
    - -
    - <%= image_tag("icon_home.png", size: "330x240", alt:"") %> -
    -
    -
    diff --git a/app/views/pages/participation.html.erb b/app/views/pages/participation.html.erb deleted file mode 100644 index 9d92140fb..000000000 --- a/app/views/pages/participation.html.erb +++ /dev/null @@ -1,46 +0,0 @@ -
    - - -
    -

    Participación y Transparencia

    - -

    Página de información sobre Participación y Transparencia.

    - -

    I. Participación.

    - -
      -
    • I.1. Participación. Información sobre Participación
    • -
    • I.1. Participación. Información sobre Participación
    • -
    • I.1. Participación. Información sobre Participación
    • -
    - -

    II. Transparencia

    - -
      -
    • II.1. Transparencia Información sobre Transparencia
    • -
    • II.1. Transparencia Información sobre Transparencia
    • -
    • II.1. Transparencia Información sobre Transparencia
    • -
    -
    -
    diff --git a/app/views/pages/proposals_info.html.erb b/app/views/pages/proposals_info.html.erb deleted file mode 100644 index 09bf8585e..000000000 --- a/app/views/pages/proposals_info.html.erb +++ /dev/null @@ -1,28 +0,0 @@ -
    -
    - <%= render "shared/back_link" %> - - -
    - -
    -

    ¿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 clic 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 empadronada 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 asumirá como propia la propuesta y la llevará a cabo.
    8. -
    - -

    Explicación detallada del proceso

    -
      -
    1. Creación de una propuesta. Cualquier persona (sin necesidad siquiera de estar empadronada) 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, 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.
    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 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, y el gobierno del Ayuntamiento 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. -
    -
    -
    diff --git a/app/views/pages/spending_proposals_info.html.erb b/app/views/pages/spending_proposals_info.html.erb deleted file mode 100644 index 41332bb96..000000000 --- a/app/views/pages/spending_proposals_info.html.erb +++ /dev/null @@ -1,15 +0,0 @@ -
    -
    - <%= render "shared/back_link" %> - - -
    - -
    -

    ¿Cómo funcionan los Presupuestos participativos?

    -

    Explicación detallada del proceso

    -

    Descripción del proceso de participación ciudadana en los presupuestos.

    -
    -
    diff --git a/app/views/polls/_callout.html.erb b/app/views/polls/_callout.html.erb new file mode 100644 index 000000000..2fad7d07d --- /dev/null +++ b/app/views/polls/_callout.html.erb @@ -0,0 +1,22 @@ +<% unless can?(:answer, @poll) %> + <% if current_user.nil? %> +
    + <%= t("polls.show.cant_answer_not_logged_in", + signin: link_to(t("polls.show.signin"), new_user_session_path, class: "probe-message"), + signup: link_to(t("polls.show.signup"), new_user_registration_path, class: "probe-message")).html_safe %> +
    + <% elsif current_user.unverified? %> +
    + <%= t('polls.show.cant_answer_verify_html', + verify_link: link_to(t('polls.show.verify_link'), verification_path)) %> +
    + <% elsif @poll.incoming? %> +
    + <%= t('polls.show.cant_answer_incoming') %> +
    + <% elsif @poll.expired? %> +
    + <%= t('polls.show.cant_answer_expired') %> +
    + <% end %> +<% end %> diff --git a/app/views/polls/_poll_group.html.erb b/app/views/polls/_poll_group.html.erb new file mode 100644 index 000000000..f4dcc5423 --- /dev/null +++ b/app/views/polls/_poll_group.html.erb @@ -0,0 +1,75 @@ +<% poll_group.each do |poll| %> +
    + <% if poll.answerable_by?(current_user) && poll.votable_by?(current_user) %> + <%= link_to poll, + class: "icon-poll-answer can-answer", + title: t("polls.index.can_answer") do %> + + <%= t("polls.index.can_answer") %> + + <% end %> + <% elsif current_user.nil? %> + <%= link_to new_user_session_path, + class: "icon-poll-answer not-logged-in", + title: t("polls.index.cant_answer_not_logged_in") do %> + + <%= t("polls.index.cant_answer_not_logged_in") %> + + <% end %> + <% elsif current_user.unverified? %> + <%= link_to verification_path, + class: "icon-poll-answer unverified", + title: t("polls.index.cant_answer_verify") do %> + + <%= t("polls.index.cant_answer_verify") %> + + <% end %> + <% elsif !poll.votable_by?(current_user) %> +
    "> + <%= t("polls.index.already_answer") %> +
    + <% else %> +
    "> + <%= t("polls.index.cant_answer") %> +
    + <% end %> +
    +
    +
    <%= poll_dates(poll) %>
    + <% if poll.questions.count == 1 %> + <% poll.questions.each do |question| %> +

    <%= link_to question.title, poll %>

    + <% end %> + <% else %> +

    <%= link_to poll.name, poll %>

    +
      + <% poll.questions.each do |question| %> +
    • <%= link_to question.title, question_path(question) %>
    • + <% end %> +
    + <% end %> + <% if poll.geozones.any? %> +

    + <%= t("polls.index.geozone_info") %> +

    + <% end %> +
      + <% poll.geozones.each do |g| %> +
    • <%= g.name %>
    • + <% end %> +
    +
    +
    + <%= link_to poll, class: "button expanded" do %> + <% if poll.expired? %> + <%= t("polls.index.participate_button_expired") %> + <% elsif poll.incoming? %> + <%= t("polls.index.participate_button_incoming") %> + <% else %> + <%= t("polls.index.participate_button") %> + <% end %> + <% end %> +
    +
    +
    +<% end %> diff --git a/app/views/polls/_reasons_for_not_answering.html.erb b/app/views/polls/_reasons_for_not_answering.html.erb new file mode 100644 index 000000000..bd0bab3ad --- /dev/null +++ b/app/views/polls/_reasons_for_not_answering.html.erb @@ -0,0 +1,24 @@ +<% if poll.incoming? %> +
    + <%= t('poll_questions.show.cant_answer_incoming') %> +
    +<% elsif poll.expired? %> +
    + <%= t('poll_questions.show.cant_answer_expired') %> +
    +<% elsif current_user.nil? %> +
    + <%= t("poll_questions.show.not_logged_in", + signin: link_to(t("poll_questions.show.signin"), new_user_session_path, class: "probe-message"), + signup: link_to(t("poll_questions.show.signup"), new_user_registration_path, class: "probe-message")).html_safe %> +
    +<% elsif current_user.unverified? %> +
    + <%= t('poll_questions.show.cant_answer_verify_html', + verify_link: link_to(t('poll_questions.show.verify_link'), verification_path)) %> +
    +<% else %> +
    + <%= t('poll_questions.show.cant_answer_wrong_geozone') %> +
    +<% end %> diff --git a/app/views/polls/index.html.erb b/app/views/polls/index.html.erb new file mode 100644 index 000000000..71cd2492a --- /dev/null +++ b/app/views/polls/index.html.erb @@ -0,0 +1,35 @@ +<% provide :title do %><%= t("polls.index.title") %><% end %> +<% content_for :wrapper_class, "light" %> +<% content_for :canonical do %> + <%= render "shared/canonical", href: polls_url %> +<% end %> + +
    +
    +
    +

    <%= t("polls.index.title") %>

    +
    +
    +
    + +
    +
    + <%= render 'shared/filter_subnav_vertical', i18n_namespace: "polls.index" %> +
    + +
    + <% polls_by_geozone_restriction = @polls.group_by(&:geozone_restricted) %> + + <% if polls_by_geozone_restriction[false].present? %> +

    <%= t("polls.index.no_geozone_restricted") %>

    + <%= render partial: 'poll_group', locals: {poll_group: polls_by_geozone_restriction[false]} %> + <% end %> + + <% if polls_by_geozone_restriction[true].present? %> +

    <%= t("polls.index.geozone_restricted") %>

    + <%= render partial: 'poll_group', locals: {poll_group: polls_by_geozone_restriction[true]} %> + <% end %> + + <%= paginate @polls %> +
    +
    diff --git a/app/views/polls/questions/_answers.html.erb b/app/views/polls/questions/_answers.html.erb new file mode 100644 index 000000000..c88156b68 --- /dev/null +++ b/app/views/polls/questions/_answers.html.erb @@ -0,0 +1,22 @@ +
    + <% if can? :answer, question %> + <% question.valid_answers.each do |answer| %> + <% if @answers_by_question_id[question.id] == answer %> + "> + <%= answer %> + + <% else %> + <%= link_to answer, + answer_question_path(question, answer: answer), + method: :post, + remote: true, + title: t("poll_questions.show.vote_answer", answer: answer), + class: "button secondary hollow" %> + <% end %> + <% end %> + <% else %> + <% question.valid_answers.each do |answer| %> + <%= answer %> + <% end %> + <% end %> +
    diff --git a/app/views/polls/questions/_comments.html.erb b/app/views/polls/questions/_comments.html.erb new file mode 100644 index 000000000..a967406aa --- /dev/null +++ b/app/views/polls/questions/_comments.html.erb @@ -0,0 +1,31 @@ +<% cache [locale_and_user_status, @current_order, commentable_cache_key(@commentable), @comment_tree.comments, @comment_tree.comment_authors, @commentable.comments_count, @comment_flags] do %> +
    +
    +
    +

    + <%= t("shared.comments.title") %> + (<%= @commentable.comments_count %>) +

    + + <%= render 'shared/wide_order_selector', i18n_namespace: "comments" %> + + <% if user_signed_in? %> + <%= render 'comments/form', {commentable: @commentable, parent_id: nil, toggeable: false} %> + <% else %> +
    + +
    + <%= t("shared.comments.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 %> + + <% @comment_tree.root_comments.each do |comment| %> + <%= render 'comments/comment', comment: comment %> + <% end %> + <%= paginate @comment_tree.root_comments %> +
    +
    +
    +<% end %> \ No newline at end of file diff --git a/app/views/polls/questions/_question.html.erb b/app/views/polls/questions/_question.html.erb new file mode 100644 index 000000000..f0958b6e4 --- /dev/null +++ b/app/views/polls/questions/_question.html.erb @@ -0,0 +1,9 @@ +
    +

    + <%= link_to question.title, question_path(question) %> +

    + +
    + <%= render 'polls/questions/answers', question: question %> +
    +
    diff --git a/app/views/polls/questions/answer.js.erb b/app/views/polls/questions/answer.js.erb new file mode 100644 index 000000000..aabbd8d89 --- /dev/null +++ b/app/views/polls/questions/answer.js.erb @@ -0,0 +1 @@ +$("#<%= dom_id(@question) %>_answers").html('<%= j render("polls/questions/answers", question: @question) %>'); diff --git a/app/views/polls/questions/show.html.erb b/app/views/polls/questions/show.html.erb new file mode 100644 index 000000000..2b7225f9f --- /dev/null +++ b/app/views/polls/questions/show.html.erb @@ -0,0 +1,65 @@ +<% provide :title do %><%= @question.title %><% end %> + +
    +
    +
    + <%= render "shared/back_link" %> + +

    <%= @question.title %>

    + + <% if @question.proposal.present? %> +
    + <%= link_to t('poll_questions.show.original_proposal'), @question.proposal %> +
    + <% end %> + + <% if can? :answer, @question %> + <%= link_to t('poll_questions.show.answer_this_question'), + @question.poll, + class: 'large button' %> + <% else %> + <%= render 'polls/reasons_for_not_answering', poll: @question.poll %> + <% end %> +
    + +
    +

    + + <%= t('poll_questions.show.author') %> + +
    + <% if @question.author_visible_name.present? %> + <%= @question.author_visible_name %> + <% else %> + <%= link_to @question.author.name, @question.author %> + <% end %> + +

    + +

    + + <%= t('poll_questions.show.poll') %> + +
    + <%= link_to @question.poll.name, @question.poll %> +

    + +

    + + <%= t('poll_questions.show.dates_title') %> + +
    + <%= poll_dates(@question.poll) %> +

    +
    +
    +
    + +
    +
    +

    <%= t('poll_questions.show.more_info') %>

    + <%= @question.description %> +
    +
    + +<%= render "comments" %> diff --git a/app/views/polls/show.html.erb b/app/views/polls/show.html.erb new file mode 100644 index 000000000..8178928b6 --- /dev/null +++ b/app/views/polls/show.html.erb @@ -0,0 +1,53 @@ +<% provide :title do %><%= @poll.name %><% end %> +<% content_for :wrapper_class, "light" %> + +
    +
    +
    +
    + <%= back_link_to polls_path %> + +

    <%= @poll.name %>

    +
      + <% @poll.geozones.each do |g| %> +
    • <%= g.name %>
    • + <% end %> +
    + + <%= render "callout" %> +
    +
    +
    +

    + + <%= t("polls.show.dates_title") %> + +
    + <%= poll_dates(@poll) %> +

    +
    +
    +
    +
    + +
    +
    + <% @questions.each do |question| %> + <%= render 'polls/questions/question', question: question %> + <% end %> +
    + + +
    +
    diff --git a/app/views/proposal_ballots/_successfull_proposal.html.erb b/app/views/proposal_ballots/_successfull_proposal.html.erb deleted file mode 100644 index 870e61605..000000000 --- a/app/views/proposal_ballots/_successfull_proposal.html.erb +++ /dev/null @@ -1,13 +0,0 @@ -
    -

    <%= link_to proposal.title, proposal %>

    -
    - <% if proposal.author.hidden? || proposal.author.erased? %> - <%= t("proposals.show.author_deleted") %> - <% else %> - <%= proposal.author.name %> - <% end %> - -  •  - <%= l proposal.created_at.to_date %> -
    -
    diff --git a/app/views/proposal_ballots/index.html.erb b/app/views/proposal_ballots/index.html.erb deleted file mode 100644 index fe5bdef70..000000000 --- a/app/views/proposal_ballots/index.html.erb +++ /dev/null @@ -1,36 +0,0 @@ -<% provide :title do %><%= t("proposal_ballots.title") %><% end %> -
    -
    -
    -

    - <%= t("proposal_ballots.title") %> -

    -

    - <%= t("proposal_ballots.description_html").html_safe %> -

    -
    - -
    -

    <%= t("proposal_ballots.date_title") %>

    -

    <%= t("proposal_ballots.date") %>

    -
    -
    -
    - -
    -
    -
    - <% if @proposal_ballots.present? %> -
    - <% @proposal_ballots.each do |proposal_for_vote| %> - <%= render "successfull_proposal", proposal: proposal_for_vote %> - <% end %> -
    - <% else %> -

    - <%= t("proposal_ballots.nothing_to_vote") %> -

    - <% end %> -
    -
    -
    diff --git a/app/views/proposals/_filter_subnav.html.erb b/app/views/proposals/_filter_subnav.html.erb index fe875c9a9..6347d56b0 100644 --- a/app/views/proposals/_filter_subnav.html.erb +++ b/app/views/proposals/_filter_subnav.html.erb @@ -1,6 +1,6 @@
    -
      +
      • <%= link_to "#tab-comments" do %>

        diff --git a/app/views/proposals/_proposal.html.erb b/app/views/proposals/_proposal.html.erb index 69bf8d164..735e8bcec 100644 --- a/app/views/proposals/_proposal.html.erb +++ b/app/views/proposals/_proposal.html.erb @@ -1,8 +1,8 @@
        Proposal.votes_needed_for_success) %>" + class="proposal clear <%= ("successful" if proposal.total_votes > Proposal.votes_needed_for_success) %>" data-type="proposal">
        -
        +
        @@ -53,13 +53,21 @@
        - <% if proposal.successfull? %> + <% if proposal.successful? %>
        +

        - <%= t("proposal_ballots.successfull", - voting: link_to(t("proposal_ballots.voting"), proposal_ballots_path)).html_safe %> + <%= t("proposals.proposal.successful", + voting: link_to(t("proposals.proposal.voting"), polls_path)).html_safe %>

        + <% if can? :create, Poll::Question %> +

        + <%= link_to t('poll_questions.create_question'), + new_admin_question_path(proposal_id: proposal.id), + class: "button hollow" %> +

        + <% end %> <% elsif proposal.archived? %>
        <%= t("proposals.proposal.supports", count: proposal.total_votes) %> diff --git a/app/views/proposals/_proposal_ballots_banner.html.erb b/app/views/proposals/_proposal_ballots_banner.html.erb deleted file mode 100644 index df210a535..000000000 --- a/app/views/proposals/_proposal_ballots_banner.html.erb +++ /dev/null @@ -1,9 +0,0 @@ - diff --git a/app/views/proposals/_successful_banner.html.erb b/app/views/proposals/_successful_banner.html.erb new file mode 100644 index 000000000..3bde0e097 --- /dev/null +++ b/app/views/proposals/_successful_banner.html.erb @@ -0,0 +1,9 @@ +
        + <%= link_to polls_path do %> +
        +
        +

        <%= t("poll_questions.banner.featured_title") %>

        +

        <%= t("poll_questions.banner.info") %>

        +
        + <% end %> +
        diff --git a/app/views/proposals/index.html.erb b/app/views/proposals/index.html.erb index fd781c04b..264be2471 100644 --- a/app/views/proposals/index.html.erb +++ b/app/views/proposals/index.html.erb @@ -4,6 +4,9 @@ search_path: proposals_path(page: 1), i18n_namespace: "proposals.index.search_form" %> <% end %> +<% content_for :canonical do %> + <%= render "shared/canonical", href: proposals_url %> +<% end %>

        <%= t("shared.outline.proposals") %>

        @@ -29,12 +32,12 @@ <% end %>
        - <% if has_banners %> + <% if has_banners? %> <%= render "shared/banner" %> <% end %> - <% if @proposal_successfull_exists %> - <%= render "proposal_ballots_banner" %> + <% if @proposal_successful_exists %> + <%= render "successful_banner" %> <% elsif @featured_proposals.present? %>

    NavegadorAcción a realizar NavegadorAcción a realizar
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NombreCreadaÚltima actualizaciónEstado
    + Política de privacidad + + Hace 20 días + + Hace 15 días + + Publicada + + + Ver página + + + Borrar página +
    + FAQ + + Hace 6 días + + Hace 3 días + + + Borrador + + + Ver página + + + Borrar página +
    diff --git a/app/views/sandbox/admin_cms_page.html.erb b/app/views/sandbox/admin_cms_page.html.erb new file mode 100644 index 000000000..ea65176b8 --- /dev/null +++ b/app/views/sandbox/admin_cms_page.html.erb @@ -0,0 +1,61 @@ + + + + Volver + + + + +
    + +
    +

    Editar Política de privacidad

    + +
    + + + + + + + + + +
    + + +
    +
    +
    +
    +
    +

    Opciones

    + + + +
    + +

    Estado

    + + +
    + + +
    + +
    + + +
    + + +
    + +
    +
    + + diff --git a/app/views/shared/_admin_login_items.html.erb b/app/views/shared/_admin_login_items.html.erb index 013320f38..13bbb9d01 100644 --- a/app/views/shared/_admin_login_items.html.erb +++ b/app/views/shared/_admin_login_items.html.erb @@ -11,7 +11,8 @@ <% end %> - <% if feature?(:spending_proposals) && (current_user.administrator? || current_user.valuator?) %> + <% if (feature?(:spending_proposals) || feature?(:budgets)) && + (current_user.administrator? || current_user.valuator?) %>
  • <%= link_to t("layouts.header.valuation"), valuation_root_path %>
  • @@ -22,4 +23,10 @@ <%= link_to t("layouts.header.management"), management_sign_in_path %> <% end %> + + <% if current_user.administrator? || current_user.poll_officer? %> +
  • + <%= link_to t("layouts.header.officing"), officing_root_path %> +
  • + <% end %> <% end %> diff --git a/app/views/shared/_advanced_search.html.erb b/app/views/shared/_advanced_search.html.erb index 7ce6108b4..558adc806 100644 --- a/app/views/shared/_advanced_search.html.erb +++ b/app/views/shared/_advanced_search.html.erb @@ -36,8 +36,6 @@ <%= text_field_tag 'advanced_search[date_min]', params[:advanced_search].try(:[], :date_min), - type: "date", - placeholder: t("shared.advanced_search.date_placeholder"), class: 'js-calendar' %>
    @@ -46,8 +44,6 @@ <%= text_field_tag 'advanced_search[date_max]', params[:advanced_search].try(:[], :date_max), - type: "date", - placeholder: t("shared.advanced_search.date_placeholder"), class: 'js-calendar' %>
    diff --git a/app/views/shared/_banner.html.erb b/app/views/shared/_banner.html.erb index 6776e24a5..0ba7a2bd1 100644 --- a/app/views/shared/_banner.html.erb +++ b/app/views/shared/_banner.html.erb @@ -1,9 +1,9 @@ -<% banner = @banners.sample%> +<% banner = @banners.sample %>
    <%= link_to banner.target_url do %> -

    <%=banner.title%>

    -

    <%=banner.description%>

    - <% end %> +

    <%=banner.title %>

    +

    <%=banner.description %>

    + <% end %>
    -
    \ No newline at end of file +
    diff --git a/app/views/shared/_canonical.html.erb b/app/views/shared/_canonical.html.erb new file mode 100644 index 000000000..1c3361a0e --- /dev/null +++ b/app/views/shared/_canonical.html.erb @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/views/shared/_filter_subnav_vertical.html.erb b/app/views/shared/_filter_subnav_vertical.html.erb new file mode 100644 index 000000000..b58ecaccc --- /dev/null +++ b/app/views/shared/_filter_subnav_vertical.html.erb @@ -0,0 +1,14 @@ + diff --git a/app/views/shared/_social_media_meta_tags.html.erb b/app/views/shared/_social_media_meta_tags.html.erb index 50a2efec9..c0e6a5e8a 100644 --- a/app/views/shared/_social_media_meta_tags.html.erb +++ b/app/views/shared/_social_media_meta_tags.html.erb @@ -3,7 +3,7 @@ - + <% if setting['url'] %> @@ -14,7 +14,7 @@ <% end %> - + diff --git a/app/views/shared/_subnavigation.html.erb b/app/views/shared/_subnavigation.html.erb index ed5694f27..991d771ce 100644 --- a/app/views/shared/_subnavigation.html.erb +++ b/app/views/shared/_subnavigation.html.erb @@ -5,21 +5,23 @@ <%= layout_menu_link_to t("layouts.header.debates"), debates_path, controller_name == 'debates', - accesskey: "d" %> + accesskey: "1" %> <% end %>
  • <%= layout_menu_link_to t("layouts.header.proposals"), proposals_path, controller_name == 'proposals', - accesskey: "p" %> -
  • -
  • - <%= layout_menu_link_to t("layouts.header.proposal_ballot"), - proposal_ballots_path, - controller_name == 'proposal_ballots', - accesskey: "v" %> + accesskey: "2" %>
  • + <% if feature?(:polls) %> +
  • + <%= layout_menu_link_to t("layouts.header.poll_questions"), + polls_path, + controller_name == "polls", + accesskey: "3" %> +
  • + <% end %> <% if feature?(:spending_proposals) %>
  • <%= layout_menu_link_to t("layouts.header.spending_proposals"), @@ -33,8 +35,14 @@ <%= layout_menu_link_to t("layouts.header.budgets"), budgets_path, controller_name == "budgets" || controller_name == "investments", - accesskey: "b" %> + accesskey: "4" %>
  • <% end %> +
  • + <%= link_to t("layouts.header.more_info"), + more_info_path, + accesskey: "5", + class: ("active" if current_page?(more_info_path)) %> +
  • diff --git a/app/views/shared/_tags.html.erb b/app/views/shared/_tags.html.erb index 3cdb3c40e..ec2b7a1b1 100644 --- a/app/views/shared/_tags.html.erb +++ b/app/views/shared/_tags.html.erb @@ -5,8 +5,7 @@ <% taggable.tag_list_with_limit(limit).each do |tag| %>
  • <%= link_to sanitize(tag.name), - taggables_path(taggable.class.name.underscore, tag.name), - search: tag.name %>
  • + taggables_path(taggable.class.name.underscore, tag.name) %> <% end %> <% if taggable.tags_count_out_of_limit(limit) > 0 %> diff --git a/app/views/shared/_top_links.html.erb b/app/views/shared/_top_links.html.erb index f57deeebd..ae77cb1c2 100644 --- a/app/views/shared/_top_links.html.erb +++ b/app/views/shared/_top_links.html.erb @@ -1,34 +1,28 @@ - + diff --git a/app/views/spending_proposals/show.html.erb b/app/views/spending_proposals/show.html.erb index 0da045860..b6a1f6e83 100644 --- a/app/views/spending_proposals/show.html.erb +++ b/app/views/spending_proposals/show.html.erb @@ -44,11 +44,11 @@

    <%= t("spending_proposals.show.share") %>

    diff --git a/app/views/users/_budget_investments.html.erb b/app/views/users/_budget_investments.html.erb index b5773cbdf..64cad676d 100644 --- a/app/views/users/_budget_investments.html.erb +++ b/app/views/users/_budget_investments.html.erb @@ -4,6 +4,12 @@ <%= link_to budget_investment.title, budget_investment_path(budget_investment.budget, budget_investment) %> + + <% if can? :destroy, budget_investment %> + <%= link_to t('shared.delete'), budget_investment_path(budget_investment.budget, budget_investment), + method: :delete, class: "button hollow alert" %> + <% end %> + <% end %> diff --git a/app/views/users/registrations/new.html.erb b/app/views/users/registrations/new.html.erb index 1c6f81455..3ae3156f7 100644 --- a/app/views/users/registrations/new.html.erb +++ b/app/views/users/registrations/new.html.erb @@ -19,7 +19,7 @@ <%= f.label :username %>

    <%= t("devise_views.users.registrations.new.username_note") %>

    - <%= f.text_field :username, maxlength: User.username_max_length, placeholder: t("devise_views.users.registrations.new.username_label"), label: false %> + <%= f.text_field :username, autofocus: true, maxlength: User.username_max_length, placeholder: t("devise_views.users.registrations.new.username_label"), label: false %> <%= f.invisible_captcha :family_name %> diff --git a/app/views/valuation/budget_investments/edit.html.erb b/app/views/valuation/budget_investments/edit.html.erb index 16dc3b59d..fa748b098 100644 --- a/app/views/valuation/budget_investments/edit.html.erb +++ b/app/views/valuation/budget_investments/edit.html.erb @@ -8,7 +8,7 @@ <%= form_for(@investment, url: valuate_valuation_budget_budget_investment_path(@budget, @investment), html: {id: "valuation_budget_investment_edit_form"}) do |f| %> <%= render 'shared/errors', resource: @investment %>
    -
    +
    <%= t('valuation.budget_investments.edit.feasibility') %>
    @@ -38,7 +38,7 @@
    -
    +
    <%= f.label :unfeasibility_explanation, t("valuation.budget_investments.edit.feasible_explanation_html") %> <%= f.text_area :unfeasibility_explanation, label: false, rows: 3 %>
    @@ -49,26 +49,26 @@
    -
    +
    <%= f.label :price, "#{t('valuation.budget_investments.edit.price_html', currency: @budget.currency_symbol)}" %> <%= f.number_field :price, label: false, max: 1000000000000000 %>
    -
    +
    <%= f.label :price_first_year, "#{t('valuation.budget_investments.edit.price_first_year_html', currency: @budget.currency_symbol)}" %> <%= f.number_field :price_first_year, label: false, max: 1000000000000000 %>
    -
    +
    <%= f.label :price_explanation, t("valuation.budget_investments.edit.price_explanation_html") %> <%= f.text_area :price_explanation, label: false, rows: 3 %>
    -
    +
    <%= f.label :duration, t("valuation.budget_investments.edit.duration_html") %> <%= f.text_field :duration, label: false %>
    @@ -86,7 +86,7 @@
    -
    +
    <%= f.label :internal_comments, t("valuation.budget_investments.edit.internal_comments_html") %> <%= f.text_area :internal_comments, label: false, rows: 3 %>
    diff --git a/app/views/welcome/index.html.erb b/app/views/welcome/index.html.erb index 978ff489c..1eb6cf38c 100644 --- a/app/views/welcome/index.html.erb +++ b/app/views/welcome/index.html.erb @@ -4,7 +4,7 @@

    <%= t("layouts.header.open_city_title") %>

    <%= t("layouts.header.open_city_slogan_html") %>  - <%= link_to t("layouts.header.more_information"), page_path('more_information') %> + <%= link_to t("layouts.header.more_info"), more_info_path %>

    <%= link_to t("layouts.header.see_all"), proposals_path, class: "button see-more expanded" %> @@ -12,7 +12,7 @@
    - <%= image_tag("icon_home.png", size: "330x240", alt:"") %> + <%= image_tag(image_path_for("icon_home.png"), size: "330x240", alt:"") %>
    diff --git a/config/deploy.rb b/config/deploy.rb index 52fa7a255..82b70efa1 100644 --- a/config/deploy.rb +++ b/config/deploy.rb @@ -1,5 +1,5 @@ # config valid only for current version of Capistrano -lock '3.5.0' +lock '3.8.1' def deploysecret(key) @deploy_secrets_yml ||= YAML.load_file('config/deploy-secrets.yml')[fetch(:stage).to_s] @@ -15,7 +15,6 @@ set :full_app_name, deploysecret(:full_app_name) set :server_name, deploysecret(:server_name) set :repo_url, 'https://github.com/consul/consul.git' -set :scm, :git set :revision, `git rev-parse --short #{fetch(:branch)}`.strip set :log_level, :info @@ -56,4 +55,4 @@ task :install_bundler_gem do on roles(:app) do execute "rvm use #{fetch(:rvm1_ruby_version)}; gem install bundler" end -end \ No newline at end of file +end diff --git a/config/deploy/sample_config_files/apache_passenger.conf b/config/deploy/sample_config_files/apache_passenger.conf deleted file mode 100644 index f97fc9b2a..000000000 --- a/config/deploy/sample_config_files/apache_passenger.conf +++ /dev/null @@ -1,19 +0,0 @@ - - - ServerName yourdomain.com - ServerAlias www.yourdomain.com - ServerAdmin webmaster@localhost - - DocumentRoot /path/to/deploy_to/current/public - - # RailsEnv production - - ErrorLog ${APACHE_LOG_DIR}/yourdomain.error.log - CustomLog ${APACHE_LOG_DIR}/yourdomain.access.log combined - - - Options FollowSymLinks - Require all granted - - - diff --git a/config/deploy/sample_config_files/apache_unicorn.conf b/config/deploy/sample_config_files/apache_unicorn.conf deleted file mode 100644 index 93aafec23..000000000 --- a/config/deploy/sample_config_files/apache_unicorn.conf +++ /dev/null @@ -1,31 +0,0 @@ - - - ServerAdmin admin@yourdomain.com - ServerName yourdomain.com - ServerAlias www.yourdomain.com - - DocumentRoot /path/to/deploy_to/current/public - - RewriteEngine On - - - BalancerMember http://127.0.0.1:5000 - - - # Redirect all non-static requests to thin - RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_FILENAME} !-f - RewriteRule ^/(.*)$ balancer://unicornservers%{REQUEST_URI} [P,QSA,L] - - ProxyPass / balancer://unicornservers/ - ProxyPassReverse / balancer://unicornservers/ - ProxyPreserveHost on - - - Order deny,allow - Allow from all - - - ErrorLog ${APACHE_LOG_DIR}/yourdomain.error.log - CustomLog ${APACHE_LOG_DIR}/yourdomain.access.log combined - - diff --git a/config/deploy/sample_config_files/unicorn_init.sh b/config/deploy/sample_config_files/unicorn_init.sh deleted file mode 100644 index 1cef9110b..000000000 --- a/config/deploy/sample_config_files/unicorn_init.sh +++ /dev/null @@ -1,96 +0,0 @@ -#!/bin/sh - -### BEGIN INIT INFO -# Provides: unicorn -# Required-Start: $local_fs $remote_fs $network $syslog -# Required-Stop: $local_fs $remote_fs $network $syslog -# Default-Start: 2 3 4 5 -# Default-Stop: 0 1 6 -# Short-Description: starts the unicorn web server -# Description: starts unicorn -### END INIT INFO - -set -e - -TIMEOUT=${TIMEOUT-60} -APP_ROOT=<%= current_path %> -PID_DIR=$APP_ROOT/tmp/pids -PID=$PID_DIR/unicorn.pid -CMD="cd $APP_ROOT; bundle exec unicorn -D -c /path/to/shared/config/unicorn.rb -E production" -AS_USER=deploy -set -u - -OLD_PIN="$PID.oldbin" - -sig () { - test -s "$PID" && kill -$1 `cat $PID` -} - -oldsig () { - test -s $OLD_PIN && kill -$1 `cat $OLD_PIN` -} - -workersig () { - workerpid="$APP_ROOT/tmp/pids/unicorn.$2.pid" - - test -s "$workerpid" && kill -$1 `cat $workerpid` -} - -run () { - if [ "$(id -un)" = "$AS_USER" ]; then - eval $1 - else - su -c "$1" - $AS_USER - fi -} - -case "$1" in -start) - sig 0 && echo >&2 "Already running" && exit 0 - run "$CMD" - ;; -stop) - sig QUIT && exit 0 - echo >&2 "Not running" - ;; -force-stop) - sig TERM && exit 0 - echo >&2 "Not running" - ;; -kill_worker) - workersig QUIT $2 && exit 0 - echo >&2 "Worker not running" - ;; -restart|reload) - sig USR2 && echo reloaded OK && exit 0 - echo >&2 "Couldn't reload, starting '$CMD' instead" - run "$CMD" - ;; -upgrade) - if sig USR2 && sleep 2 && sig 0 && oldsig QUIT - then - n=$TIMEOUT - while test -s $OLD_PIN && test $n -ge 0 - do - printf '.' && sleep 1 && n=$(( $n - 1 )) - done - echo - - if test $n -lt 0 && test -s $OLD_PIN - then - echo >&2 "$OLD_PIN still exists after $TIMEOUT seconds" - exit 1 - fi - exit 0 - fi - echo >&2 "Couldn't upgrade, starting '$CMD' instead" - run "$CMD" - ;; -reopen-logs) - sig USR1 - ;; -*) - echo >&2 "Usage: $0 " - exit 1 - ;; -esac diff --git a/config/deploy/shared/database.yml.erb b/config/deploy/shared/database.yml.erb deleted file mode 100644 index 2449ae6f8..000000000 --- a/config/deploy/shared/database.yml.erb +++ /dev/null @@ -1,10 +0,0 @@ -<%= fetch(:rails_env) %>: - adapter: postgresql - timeout: 5000 - encoding: unicode - reconnect: false - database: <%= "#{fetch(:application)}" %> - pool: 5 - username: - password: - host: <%= fetch(:db_server) %> diff --git a/config/deploy/shared/log_rotation.erb b/config/deploy/shared/log_rotation.erb deleted file mode 100755 index 7507de184..000000000 --- a/config/deploy/shared/log_rotation.erb +++ /dev/null @@ -1,11 +0,0 @@ -<%= fetch(:deploy_to) %>/shared/log/*.log { - daily - missingok - rotate 52 - compress - delaycompress - notifempty - sharedscripts - endscript - copytruncate -} diff --git a/config/deploy/shared/secrets.yml.erb b/config/deploy/shared/secrets.yml.erb deleted file mode 100644 index 8f82c5dca..000000000 --- a/config/deploy/shared/secrets.yml.erb +++ /dev/null @@ -1,9 +0,0 @@ -<%= fetch(:rails_env) %>: - secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> - twitter_key: <%= ENV["TWITTER_KEY"] %> - twitter_secret: <%= ENV["TWITTER_SECRET"] %> - facebook_key: <%= ENV["FACEBOOK_KEY"] %> - facebook_secret: <%= ENV["FACEBOOK_SECRET"] %> - google_oauth2_key: <%= ENV["GOOGLE_KEY"] %> - google_oauth2_secret: <%= ENV["GOOGLE_SECRET"] %> - server_name: <%= fetch(:server_name) %> diff --git a/config/deploy/shared/sidekiq.yml.erb b/config/deploy/shared/sidekiq.yml.erb deleted file mode 100644 index 1d4747933..000000000 --- a/config/deploy/shared/sidekiq.yml.erb +++ /dev/null @@ -1,2 +0,0 @@ -<%= fetch(:rails_env) %>: - :concurrency: <%= fetch(:sidekiq_concurrency, 5) %> diff --git a/config/deploy/shared/unicorn.rb.erb b/config/deploy/shared/unicorn.rb.erb deleted file mode 100755 index 805734276..000000000 --- a/config/deploy/shared/unicorn.rb.erb +++ /dev/null @@ -1,42 +0,0 @@ -root = "<%= current_path %>" -working_directory root -pid "#{root}/tmp/pids/unicorn.pid" -stderr_path "#{root}/log/unicorn.log" -stdout_path "#{root}/log/unicorn.log" - -listen 5000 -#listen "/tmp/unicorn.<%= fetch(:application) %>.sock" -worker_processes 4 -timeout 40 -preload_app true - -# Force unicorn to look at the Gemfile in the current_path -# otherwise once we've first started a master process, it -# will always point to the first one it started. -before_exec do |server| - ENV['BUNDLE_GEMFILE'] = "<%= current_path %>/Gemfile" -end - -before_fork do |server, worker| - defined?(ActiveRecord::Base) and - ActiveRecord::Base.connection.disconnect! - # Quit the old unicorn process - old_pid = "#{server.config[:pid]}.oldbin" - if File.exists?(old_pid) && server.pid != old_pid - puts "We've got an old pid and server pid is not the old pid" - begin - Process.kill("QUIT", File.read(old_pid).to_i) - puts "killing master process (good thing tm)" - rescue Errno::ENOENT, Errno::ESRCH - puts "unicorn master already killed" - end - end -end - -after_fork do |server, worker| - port = 5000 + worker.nr - child_pid = server.config[:pid].sub('.pid', ".#{port}.pid") - system("echo #{Process.pid} > #{child_pid}") - defined?(ActiveRecord::Base) and - ActiveRecord::Base.establish_connection -end diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index 85c391314..ca6eae46e 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -22,6 +22,8 @@ data: ## Another gem (replace %#= with %=): # - "<%#= %x[bundle show vagrant].chomp %>/templates/locales/%{locale}.yml" - config/locales/%{locale}.yml + - config/locales/activerecord.%{locale}.yml + - config/locales/activemodel.%{locale}.yml - config/locales/admin.%{locale}.yml - config/locales/moderation.%{locale}.yml - config/locales/valuation.%{locale}.yml @@ -32,6 +34,7 @@ data: - config/locales/devise_views.%{locale}.yml - config/locales/responders.%{locale}.yml - config/locales/kaminari.%{locale}.yml + - config/locales/officing.%{locale}.yml - config/locales/budgets.%{locale}.yml # Locale files to write new keys to, based on a list of key pattern => file rules. Matched from top to bottom: @@ -126,6 +129,8 @@ ignore_unused: - 'admin.activity.show.filter*' - 'admin.comments.index.hidden_*' - 'admin.settings.index.features.*' + - 'admin.polls.*.submit_button' + - 'admin.booths.*.submit_button' - 'moderation.comments.index.filter*' - 'moderation.comments.index.order*' - 'moderation.debates.index.filter*' @@ -137,6 +142,7 @@ ignore_unused: - 'valuation.budgets.index.filter*' - 'valuation.budget_investments.index.filter*' - 'users.show.filters.*' + - 'polls.index.filters.*' - 'debates.index.select_order' - 'debates.index.orders.*' - 'proposals.index.select_order' @@ -150,6 +156,7 @@ ignore_unused: - 'views.pagination.*' # kaminari - 'shared.suggest.*' - 'invisible_captcha.*' + - 'admin.site_customization.pages.page.status_*' # - '{devise,kaminari,will_paginate}.*' # - 'simple_form.{yes,no}' # - 'simple_form.{placeholders,hints,labels}.*' diff --git a/config/initializers/age.rb b/config/initializers/age.rb new file mode 100644 index 000000000..2e90c0a3b --- /dev/null +++ b/config/initializers/age.rb @@ -0,0 +1 @@ +require 'age' diff --git a/config/initializers/kaminari_config.rb b/config/initializers/kaminari_config.rb index 45c2b70e4..b1d87b01b 100644 --- a/config/initializers/kaminari_config.rb +++ b/config/initializers/kaminari_config.rb @@ -8,50 +8,3 @@ Kaminari.configure do |config| # config.page_method_name = :page # config.param_name = :page end - - -# Overrides for making Kaminari handle i18n pluralization correctly -# -# Remove everything below once https://github.com/amatsuda/kaminari/pull/694 is -# merged in Kaminari and we have updated -module Kaminari - - module ActionViewExtension - def page_entries_info(collection, options = {}) - entry_name = if options[:entry_name] - options[:entry_name].pluralize(collection.size) - else - collection.entry_name(:count => collection.size).downcase - end - - if collection.total_pages < 2 - t('helpers.page_entries_info.one_page.display_entries', entry_name: entry_name, count: collection.total_count) - else - first = collection.offset_value + 1 - last = collection.last_page? ? collection.total_count : collection.offset_value + collection.limit_value - t('helpers.page_entries_info.more_pages.display_entries', entry_name: entry_name, first: first, last: last, total: collection.total_count) - end.html_safe - end - end - - module ActiveRecordRelationMethods - def entry_name(options = {}) - model_name.human(options.reverse_merge(default: model_name.human.pluralize(options[:count]))) - end - end - - module MongoidCriteriaMethods - def entry_name(options = {}) - model_name.human(options.reverse_merge(default: model_name.human.pluralize(options[:count]))) - end - end - - class PaginatableArray < Array - ENTRY = 'entry'.freeze - - def entry_name(options = {}) - I18n.t('helpers.page_entries_info.entry', options.reverse_merge(default: ENTRY.pluralize(options[:count]))) - end - end - -end diff --git a/config/initializers/social_share_button.rb b/config/initializers/social_share_button.rb index d8b6e5254..6ee0fa0ed 100644 --- a/config/initializers/social_share_button.rb +++ b/config/initializers/social_share_button.rb @@ -1,3 +1,3 @@ SocialShareButton.configure do |config| - config.allow_sites = %w(twitter facebook google_plus) + config.allow_sites = %w(twitter facebook google_plus telegram) end diff --git a/config/locales/activemodel.en.yml b/config/locales/activemodel.en.yml index f62fd1ec6..fd378e705 100644 --- a/config/locales/activemodel.en.yml +++ b/config/locales/activemodel.en.yml @@ -15,4 +15,8 @@ en: phone: "Telephone" confirmation_code: "Confirmation code" email: - recipient: "Email" \ No newline at end of file + recipient: "Email" + officing/residence: + document_type: "Document type" + document_number: "Document number (including letters)" + year_of_birth: "Year born" \ No newline at end of file diff --git a/config/locales/activemodel.es.yml b/config/locales/activemodel.es.yml index e45eade16..dafc45a87 100644 --- a/config/locales/activemodel.es.yml +++ b/config/locales/activemodel.es.yml @@ -15,4 +15,8 @@ es: phone: "Teléfono" confirmation_code: "Código de confirmación" email: - recipient: "Email" \ No newline at end of file + recipient: "Email" + officing/residence: + document_type: "Tipo documento" + document_number: "Numero de documento (incluida letra)" + year_of_birth: "Año de nacimiento" diff --git a/config/locales/activerecord.en.yml b/config/locales/activerecord.en.yml index 04c5cc9d0..608803579 100644 --- a/config/locales/activerecord.en.yml +++ b/config/locales/activerecord.en.yml @@ -34,12 +34,27 @@ en: organization: one: "Organisation" other: "Organisations" + poll/booth: + one: "booth" + other: "booths" + poll/officer: + one: "officer" + other: "officers" proposal: one: "Citizen proposal" other: "Citizen proposals" spending_proposal: one: "Spending proposal" other: "Spending proposals" + site_customization/page: + one: Custom page + other: Custom pages + site_customization/image: + one: Custom image + other: Custom images + site_customization/content_block: + one: Custom content block + other: Custom content blocks attributes: budget: name: "Name" @@ -99,10 +114,38 @@ en: external_url: "Link to additional documentation" geozone_id: "Scope of operation" title: "Title" + poll: + name: "Name" + starts_at: "Start Date" + ends_at: "Closing Date" + geozone_restricted: "Restricted by geozone" + poll/question: + title: "Question" + valid_answers: "Posibles answers" + summary: "Summary" + description: "Description" + external_url: "Link to additional documentation" signature_sheet: signable_type: "Signable type" signable_id: "Signable ID" document_numbers: "Documents numbers" + site_customization/page: + content: Content + created_at: Created at + subtitle: Subtitle + slug: Slug + status: Status + title: Title + updated_at: Updated at + more_info_flag: Show in more information page + print_content_flag: Print content button + site_customization/image: + name: Name + image: Image + site_customization/content_block: + name: Name + locale: locale + body: Body errors: models: user: @@ -117,6 +160,11 @@ en: attributes: max_per_day: invalid: "You have reached the maximum number of private messages per day" + poll/voter: + attributes: + document_number: + not_in_census: "Document not in census" + has_voted: "User has already voted" proposal: attributes: tag_list: @@ -134,3 +182,12 @@ en: document_number: not_in_census: 'Not verified by Census' already_voted: 'Already voted this proposal' + site_customization/page: + attributes: + slug: + slug_format: "must be letters, numbers, _ and -" + site_customization/image: + attributes: + image: + image_width: "Width must be %{required_width}px" + image_height: "Height must be %{required_height}px" diff --git a/config/locales/activerecord.es.yml b/config/locales/activerecord.es.yml index c71a34178..71e166026 100644 --- a/config/locales/activerecord.es.yml +++ b/config/locales/activerecord.es.yml @@ -8,8 +8,8 @@ es: one: "Presupuesto participativo" other: "Presupuestos participativos" budget/investment: - one: "Propuesta de inversión" - other: "Propuestas de inversión" + one: "Proyecto de inversión" + other: "Proyectos de inversión" comment: one: "Comentario" other: "Comentarios" @@ -37,19 +37,34 @@ es: proposal: one: "Propuesta ciudadana" other: "Propuestas ciudadanas" + poll/booth: + one: "urna" + other: "urnas" + poll/officer: + one: "presidente de mesa" + other: "presidentes de mesa" spending_proposal: one: "Propuesta de inversión" other: "Propuestas de inversión" + site_customization/page: + one: Página + other: Páginas + site_customization/image: + one: Imagen + other: Imágenes + site_customization/content_block: + one: Bloque + other: Bloques attributes: budget: name: "Nombre" - description_accepting: "Descripción durante la fase de aceptación" - description_reviewing: "Descripción durante la fase de revisión" - description_selecting: "Descripción durante la fase de selección" + description_accepting: "Descripción durante la fase de presentación de proyectos" + description_reviewing: "Descripción durante la fase de revisión interna" + description_selecting: "Descripción durante la fase de apoyos" description_valuating: "Descripción durante la fase de evaluación" description_balloting: "Descripción durante la fase de votación" description_reviewing_ballots: "Descripción durante la fase de revisión de votos" - description_finished: "Descripción cuando el presupuesto ha finalizado" + description_finished: "Descripción cuando el presupuesto ha finalizado / Resultados" phase: "Fase" currency_symbol: "Divisa" budget/investment: @@ -94,10 +109,38 @@ es: external_url: "Enlace a documentación adicional" geozone_id: "Ámbito de actuación" title: "Título" + poll: + name: "Nombre" + starts_at: "Fecha de apertura" + ends_at: "Fecha de cierre" + geozone_restricted: "Restringida por zonas" + poll/question: + title: "Pregunta" + valid_answers: "Posibles respuestas" + summary: "Resumen" + description: "Descripción" + external_url: "Enlace a documentación adicional" signature_sheet: signable_type: "Tipo de hoja de firmas" signable_id: "ID Propuesta ciudadana/Propuesta inversión" document_numbers: "Números de documentos" + site_customization/page: + content: Contenido + created_at: Creada + subtitle: Subtítulo + slug: Slug + status: Estado + title: Título + updated_at: última actualización + more_info_flag: Mostrar en la página de más información + print_content_flag: Botón de imprimir contenido + site_customization/image: + name: Nombre + image: Imagen + site_customization/content_block: + name: Nombre + locale: Idioma + body: Contenido errors: models: user: @@ -112,10 +155,19 @@ es: attributes: max_per_day: invalid: "Has llegado al número máximo de mensajes privados por día" + poll/voter: + attributes: + document_number: + not_in_census: "Este documento no aparece en el censo" + has_voted: "Este usuario ya ha votado" proposal: attributes: tag_list: less_than_or_equal_to: "los temas deben ser menor o igual que %{count}" + budget/investment: + attributes: + tag_list: + less_than_or_equal_to: "los temas deben ser menor o igual que %{count}" proposal_notification: attributes: minimum_interval: @@ -125,3 +177,12 @@ es: document_number: not_in_census: 'No verificado por Padrón' already_voted: 'Ya ha votado esta propuesta' + site_customization/page: + attributes: + slug: + slug_format: "deber ser letras, números, _ y -" + site_customization/image: + attributes: + image: + image_width: "Debe tener %{required_width}px de ancho" + image_height: "Debe tener %{required_height}px de alto" diff --git a/config/locales/admin.en.yml b/config/locales/admin.en.yml index dadcb7745..1d1bf9cb0 100755 --- a/config/locales/admin.en.yml +++ b/config/locales/admin.en.yml @@ -1,7 +1,10 @@ --- en: admin: + header: + title: Administration actions: + actions: Actions confirm: Are you sure? confirm_hide: Confirm hide: Hide @@ -9,6 +12,8 @@ en: restore: Restore mark_featured: Featured unmark_featured: Unmark featured + edit: Edit + configure: Configure banners: index: title: Banners @@ -19,6 +24,7 @@ en: all: All with_active: Active with_inactive: Inactive + preview: Preview banner: title: Title description: Description @@ -28,7 +34,6 @@ en: post_started_at: Post started at post_ended_at: Post ended at edit: - back: Back editing: Edit banner form: submit_button: Save changes @@ -38,7 +43,6 @@ en: one: "prevented this banner from being saved" other: 'prevented this banner from being saved' new: - back: Back creating: Create banner activity: show: @@ -124,6 +128,7 @@ en: select: "Select" table_id: "ID" table_title: "Title" + table_supports: "Supports" table_admin: "Administrator" table_valuator: "Valuator" table_geozone: "Scope of operation" @@ -153,6 +158,7 @@ en: tags: Tags tags_placeholder: "Write the tags you want separated by commas (,)" undefined: Undefined + search_unfeasible: Search unfeasible comments: index: filter: Filter @@ -189,23 +195,37 @@ en: activity: Moderator activity admin: Admin menu banner: Manage banners + poll_questions: Poll questions + proposals_topics: Proposals topics budgets: Participatory budgets - debate_topics: Debate topics geozones: Manage geozones hidden_comments: Hidden comments hidden_debates: Hidden debates hidden_proposals: Hidden proposals hidden_users: Hidden users - incomplete_verifications: Incomplete verifications managers: Managers moderators: Moderators valuators: Valuators + poll_officers: Poll officers + polls: Polls + poll_booths: Booths location officials: Officials organizations: Organisations settings: Configuration settings spending_proposals: Spending proposals stats: Statistics signature_sheets: Signature Sheets + site_customization: + pages: Custom Pages + images: Custom Images + content_blocks: Custom content blocks + title_categories: Categories + title_moderated_content: Moderated content + title_budgets: Budgets + title_polls: Polls + title_profiles: Profiles + title_banners: Banners + title_site_customization: Site customization moderators: index: title: Moderators @@ -236,6 +256,162 @@ en: in_evaluation_count: In evaluation total_count: Total cost: Cost + poll_officers: + index: + title: Poll officers + officer: + add: Add + delete: Delete position + name: Name + email: Email + entry_name: officer + search: + email_placeholder: Search user by email + search: Search + user_not_found: User not found + poll_officer_assignments: + flash: + destroy: "Officing shift removed" + create: "Officing shift added" + error_destroy: "An error ocurred when removing officer assignment" + error_create: "An error ocurred when adding officer assignment" + index: + officers_title: "List of officers" + no_officers: "There are no officers assigned to this poll." + table_name: "Name" + table_email: "Email" + add_officer_assignments: "Add shifts as officer" + edit_officer_assignments: "Edit officing shifts" + by_officer: + new_assignment: "New shift" + date: "Date" + booth: "Booth" + assignment: "Assignment" + select_date: "Select day" + select_booth: "Select booth" + add_assignment: "Add shift" + remove_assignment: "Remove" + assignments: "Officing shifts in this poll" + no_assignments: "This user has no officing shifts in this poll." + recounts: "Daily recounts" + recount: "Daily recount (by officer)" + final_recounts: "Final recounts" + final_recount: "Final recount (by officer)" + poll_booth_assignments: + flash: + destroy: "Booth not assigned anymore" + create: "Booth assigned" + error_destroy: "An error ocurred when removing booth assignment" + error_create: "An error ocurred when assigning booth to the poll" + show: + location: "Location" + officers: "Officers" + officers_list: "Officer list for this booth" + no_officers: "There are no officers for this booth" + recounts: "Recounts" + recounts_list: "Recount list for this booth" + no_recounts: "There are not daily recounts of this booth yet" + date: "Date" + count_by_officer: "Daily recount (by officer)" + count_final: "Final recount (by officer)" + count_by_system: "Votes (automatic)" + index: + booths_title: "List of booths" + no_booths: "There are no booths assigned to this poll." + table_name: "Name" + table_location: "Location" + table_assignment: "Assignment" + remove_booth: "Remove booth from poll" + add_booth: "Assign booth" + polls: + index: + title: "List of polls" + no_polls: "There are no polls." + create: "Create poll" + name: "Name" + dates: "Dates" + new: + title: "New poll" + submit_button: "Create poll" + edit: + title: "Edit poll" + submit_button: "Update poll" + show: + questions_tab: Questions + booths_tab: Booths + officers_tab: Officers + recounts_tab: Recounting + results_tab: Results + no_questions: "There are no questions assigned to this poll." + questions_title: "List of questions" + remove_question: "Remove question from poll" + add_question: "Include question" + table_title: "Title" + table_assignment: "Assignment" + table_name: "Name" + flash: + question_added: "Question added to this poll" + error_on_question_added: "Question could not be assigned to this poll" + question_removed: "Question removed from this poll" + error_on_question_removed: "Question could not be removed from this poll" + questions: + index: + title: "Questions" + create: "Create question" + no_questions: "There are no questions." + filter_poll: Filter by Poll + select_poll: Select Poll + questions_tab: "Questions" + successful_proposals_tab: "Successful proposals" + create_question: "Create question" + table_proposal: "Proposal" + table_question: "Question" + edit: + title: "Edit Question" + new: + title: "Create Question" + poll_label: "Poll" + valid_answers_note: "Enter the answers separated by commas (,)" + show: + proposal: Original proposal + author: Author + title: Title + valid_answers: Valid answers + description: Description + preview: View on website + recounts: + index: + title: "Recounts" + no_recounts: "There is nothing to be recounted" + table_booth_name: "Booth" + table_recounts: "Accumulated daily recounts (by officer)" + table_final_recount: "Final recount (by officer)" + table_system_count: "Votes (automatic)" + results: + index: + title: "Results" + no_results: "There are no results" + table_whites: "Blank ballots" + table_nulls: "Invalid ballots" + table_answer: Answer + table_votes: Votes + booths: + index: + title: "List of booths" + no_booths: "There are no booths." + add_booth: "Add booth" + name: "Name" + location: "Location" + new: + title: "New booth" + name: "Name" + location: "Location" + submit_button: "Create booth" + edit: + title: "Edit booth" + submit_button: "Update booth" + show: + location: "Location" officials: edit: destroy: Remove 'Official' status @@ -298,6 +474,15 @@ en: enable: "Enable" disable: "Disable" shared: + booths_search: + button: Search + placeholder: Search booth by name + poll_officers_search: + button: Search + placeholder: Search poll officers + poll_questions_search: + button: Search + placeholder: Search poll questions proposal_search: button: Search placeholder: Search proposals by title, code, description or question @@ -307,6 +492,8 @@ en: user_search: button: Search placeholder: Search user by name or email' + search_results: "Search results" + no_search_results: "No results found." spending_proposals: index: geozone_filter_all: All zones @@ -425,6 +612,8 @@ en: debates: Debates proposal_votes: Proposal votes proposals: Proposals + budgets: Open budgets + budget_investments: Investment projects spending_proposals: Spending Proposals unverified_users: Unverified users user_level_three: Level three users @@ -435,9 +624,11 @@ en: visits: Visits votes: Total votes spending_proposals_title: Spending Proposals + budgets_title: Participatory budgeting visits_title: Visits direct_messages: Direct messages proposal_notifications: Proposal notifications + incomplete_verifications: Incomplete verifications direct_messages: title: Direct messages total: Total @@ -450,9 +641,9 @@ en: create: Create Topic destroy: Destroy Topic index: - add_tag: Add a new debate topic - title: Debate topics - mark_as_featured: Propose topic upon creating debate + add_tag: Add a new proposal topic + title: Proposal topics + mark_as_featured: Propose topic upon creating proposal name: placeholder: Type the name of the topic update: Update Topic @@ -474,3 +665,74 @@ en: phone_not_given: Phone not given sms_code_not_confirmed: Has not confirmed the sms code title: Incomplete verifications + site_customization: + content_blocks: + form: + content_blocks_information: Information about content blocks + content_block_about: You can create HTML content blocks to be inserted in the header or the footer of your Consul. + content_block_top_links_html: "Header blocks (top_links) are blocks of links that must have this format:" + content_block_footer_html: "Footer blocks can have any format and can be used to insert Javascript, CSS or custom HTML." + create: + notice: Content block created successfully + error: Content block couldn't be created + update: + notice: Content block updated successfully + error: Content block couldn't be updated + destroy: + notice: Content block deleted successfully + edit: + title: Editing content block + errors: + form: + error: Error + index: + create: Create new content block + delete: Delete block + title: Content blocks + new: + title: Create new content block + content_block: + body: Body + name: Name + images: + index: + title: Custom images + update: Update + delete: Delete + image: Image + update: + notice: Image updated successfully + error: Image couldn't be updated + destroy: + notice: Image deleted successfully + error: Image couldn't be deleted + pages: + create: + notice: Page created successfully + error: Page couldn't be created + update: + notice: Page updated successfully + error: Page couldn't be updated + destroy: + notice: Page deleted successfully + edit: + title: Editing %{page_title} + errors: + form: + error: Error + form: + options: Options + index: + create: Create new page + delete: Delete page + title: Custom Pages + see_page: See page + new: + title: Create new custom page + page: + created_at: Created at + status: Status + title: Title + updated_at: Updated at + status_draft: Draft + status_published: Published diff --git a/config/locales/admin.es.yml b/config/locales/admin.es.yml index 09959cfde..b42843d70 100644 --- a/config/locales/admin.es.yml +++ b/config/locales/admin.es.yml @@ -1,7 +1,10 @@ --- es: admin: + header: + title: Administración actions: + actions: Acciones confirm: "¿Estás seguro?" confirm_hide: Confirmar hide: Ocultar @@ -9,6 +12,8 @@ es: restore: Volver a mostrar mark_featured: Destacar unmark_featured: Quitar destacado + edit: Editar + configure: Configurar banners: index: title: Banners @@ -19,6 +24,7 @@ es: all: Todos with_active: Activos with_inactive: Inactivos + preview: Vista previa banner: title: Título description: Descripción @@ -28,7 +34,6 @@ es: post_started_at: Inicio de publicación post_ended_at: Fin de publicación edit: - back: Volver editing: Editar el banner form: submit_button: Guardar Cambios @@ -38,7 +43,6 @@ es: one: "error impidió guardar el banner" other: "errores impidieron guardar el banner." new: - back: Volver creating: Crear banner activity: show: @@ -124,6 +128,7 @@ es: select: "Seleccionar" table_id: "ID" table_title: "Título" + table_supports: "Apoyos" table_admin: "Administrador" table_valuator: "Evaluador" table_geozone: "Ámbito de actuación" @@ -153,6 +158,7 @@ es: tags: Etiquetas tags_placeholder: "Escribe las etiquetas que desees separadas por comas (,)" undefined: Sin definir + search_unfeasible: Buscar inviables comments: index: filter: Filtro @@ -189,23 +195,37 @@ es: activity: Actividad de moderadores admin: Menú de administración banner: Gestionar banners + proposals_topics: Temas de propuestas + poll_questions: Preguntas ciudadanas budgets: Presupuestos participativos - debate_topics: Temas de debate geozones: Gestionar distritos hidden_comments: Comentarios ocultos hidden_debates: Debates ocultos hidden_proposals: Propuestas ocultas hidden_users: Usuarios bloqueados - incomplete_verifications: Verificaciones incompletas managers: Gestores moderators: Moderadores valuators: Evaluadores + poll_officers: Presidentes de mesa + polls: Votaciones + poll_booths: Ubicación de urnas officials: Cargos públicos organizations: Organizaciones settings: Configuración global spending_proposals: Propuestas de inversión stats: Estadísticas signature_sheets: Hojas de firmas + site_customization: + pages: Personalizar páginas + images: Personalizar imágenes + content_blocks: Personalizar bloques + title_categories: Categorías + title_moderated_content: Contenido moderado + title_budgets: Presupuestos + title_polls: Votaciones + title_profiles: Perfiles + title_banners: Banners + title_site_customization: Personalizar sitio moderators: index: title: Moderadores @@ -236,6 +256,162 @@ es: in_evaluation_count: En evaluación total_count: Total cost: Coste total + poll_officers: + index: + title: Presidentes de mesa + officer: + add: Añadir como Presidente de mesa + delete: Eliminar cargo + name: Nombre + email: Email + entry_name: presidente de mesa + search: + email_placeholder: Buscar usuario por email + search: Buscar + user_not_found: Usuario no encontrado + poll_officer_assignments: + flash: + destroy: "Eliminado turno de presidente de mesa" + create: "Añadido turno de presidente de mesa" + error_destroy: "Se ha producido un error al eliminar el turno" + error_create: "Se ha producido un error al intentar crear el turno" + index: + officers_title: "Listado de presidentes de mesa asignados" + no_officers: "No hay presidentes de mesa asignados a esta votación." + table_name: "Nombre" + table_email: "Email" + add_officer_assignments: "Añadir turnos como presidente de mesa" + edit_officer_assignments: "Editar turnos" + by_officer: + new_assignment: "Nuevo turno" + date: "Fecha" + booth: "Urna" + assignment: "Asignación" + select_date: "Seleccionar día" + select_booth: "Seleccionar urna" + add_assignment: "Añadir turno" + remove_assignment: "Eliminar turno" + assignments: "Turnos como presidente de mesa en esta votación" + no_assignments: "No tiene turnos como presidente de mesa en esta votación." + recounts: "Recuentos diarios" + recount: "Recuento diario (presidente de mesa)" + final_recounts: "Recuentos finales" + final_recount: "Recuento final (presidente de mesa)" + poll_booth_assignments: + flash: + destroy: "Urna desasignada" + create: "Urna asignada" + error_destroy: "Se ha producido un error al desasignar la urna" + error_create: "Se ha producido un error al intentar asignar la urna" + show: + location: "Ubicación" + officers: "Presidentes de mesa" + officers_list: "Lista de presidentes de mesa asignados a esta urna" + no_officers: "No hay presidentes de mesa para esta urna" + recounts: "Recuentos" + recounts_list: "Lista de recuentos de esta urna" + no_recounts: "No hay recuentos diarios de esta urna" + date: "Fecha" + count_by_officer: "Recuento diario (presidente de mesa)" + count_final: "Recuento final (presidente de mesa)" + count_by_system: "Votos (automático)" + index: + booths_title: "Listado de urnas asignadas" + no_booths: "No hay urnas asignadas a esta votación." + table_name: "Nombre" + table_location: "Ubicación" + table_assignment: "Asignación" + remove_booth: "Desasignar urna" + add_booth: "Asignar urna" + polls: + index: + title: "Listado de votaciones" + no_polls: "No hay ninguna votación." + create: "Crear votación" + name: "Nombre" + dates: "Fechas" + new: + title: "Nueva votación" + submit_button: "Crear votación" + edit: + title: "Editar votación" + submit_button: "Actualizar votación" + show: + questions_tab: Preguntas + booths_tab: Urnas + officers_tab: Presidentes de mesa + recounts_tab: Recuentos + results_tab: Resultados + no_questions: "No hay preguntas asignadas a esta votación." + questions_title: "Listado de preguntas asignadas" + remove_question: "Desasignar pregunta" + add_question: "Incluir pregunta" + table_title: "Título" + table_assignment: "Asignación" + table_name: "Nombre" + flash: + question_added: "Pregunta añadida a esta votación" + error_on_question_added: "No se pudo asignar la pregunta" + question_removed: "Pregunta eliminada de esta votación" + error_on_question_removed: "No se pudo quitar la pregunta" + questions: + index: + title: "Preguntas ciudadanas" + create: "Crear pregunta ciudadana" + no_questions: "No hay ninguna pregunta ciudadana." + filter_poll: "Filtrar por votación" + select_poll: "Seleccionar votación" + questions_tab: "Preguntas ciudadanas" + successful_proposals_tab: "Propuestas que han superado el umbral" + create_question: "Crear pregunta para votación" + table_proposal: "Propuesta" + table_question: "Pregunta" + edit: + title: "Editar pregunta ciudadana" + new: + title: "Crear pregunta ciudadana" + poll_label: "Votación" + valid_answers_note: "Escribe las respuestas separadas por comas (,)" + show: + proposal: Propuesta ciudadana original + author: Autor + title: Título + valid_answers: Respuestas válidas + description: Descripción + preview: Ver en la web + recounts: + index: + title: "Recuentos" + no_recounts: "No hay nada de lo que hacer recuento" + table_booth_name: "Urna" + table_recounts: "Recuentos diarios acumulados (presidente de mesa)" + table_final_recount: "Recuento final (presidente de mesa)" + table_system_count: "Votos (automático)" + results: + index: + title: "Resultados" + no_results: "No hay resultados" + table_whites: Papeletas en blanco + table_nulls: Papeletas nulas + table_answer: Respuesta + table_votes: Votos + booths: + index: + title: "Lista de urnas" + no_booths: "No hay urnas." + add_booth: "Añadir urna" + name: "Nombre" + location: "Ubicación" + new: + title: "Nueva urna" + name: "Nombre" + location: "Ubicación" + submit_button: "Crear urna" + edit: + title: "Editar urna" + submit_button: "Actualizar urna" + show: + location: "Ubicación" officials: edit: destroy: Eliminar condición de 'Cargo Público' @@ -298,6 +474,15 @@ es: enable: "Activar" disable: "Desactivar" shared: + booths_search: + button: Buscar + placeholder: Buscar urna por nombre + poll_officers_search: + button: Buscar + placeholder: Buscar presidentes de mesa + poll_questions_search: + button: Buscar + placeholder: Buscar preguntas proposal_search: button: Buscar placeholder: Buscar propuestas por título, código, descripción o pregunta @@ -307,6 +492,8 @@ es: user_search: button: Buscar placeholder: Buscar usuario por nombre o email + search_results: "Resultados de la búsqueda" + no_search_results: "No se han encontrado resultados." spending_proposals: index: geozone_filter_all: Todos los ámbitos de actuación @@ -425,6 +612,8 @@ es: debates: Debates proposal_votes: Votos en propuestas proposals: Propuestas + budgets: Presupuestos abiertos + budget_investments: Propuestas de inversión spending_proposals: Propuestas de inversión unverified_users: Usuarios sin verificar user_level_three: Usuarios de nivel tres @@ -435,9 +624,11 @@ es: visits: Visitas votes: Votos spending_proposals_title: Propuestas de inversión + budgets_title: Presupuestos participativos visits_title: Visitas direct_messages: Mensajes directos proposal_notifications: Notificaciones de propuestas + incomplete_verifications: Verificaciones incompletas direct_messages: title: Mensajes directos total: Total @@ -450,9 +641,9 @@ es: create: Crear Tema destroy: Eliminar Tema index: - add_tag: Añade un nuevo tema de debate - title: Temas de debate - mark_as_featured: Proponer tema al crear debate + add_tag: Añade un nuevo tema de propuesta + title: Temas de propuesta + mark_as_featured: Proponer tema al crear propuesta name: placeholder: Escribe el nombre del tema update: Actualizar Tema @@ -474,3 +665,74 @@ es: phone_not_given: No ha dado su teléfono sms_code_not_confirmed: No ha introducido su código de seguridad title: Verificaciones incompletas + site_customization: + content_blocks: + form: + content_blocks_information: Información sobre los bloques de texto + content_block_about: Puedes crear bloques de HTML que se incrustarán en la cabecera o el pie de tu Cónsul. + content_block_top_links_html: "Los bloques de la cabecera (top_links) son bloques de enlaces que deben crearse con este formato:" + content_block_footer_html: "Los bloques del pie (footer) pueden tener cualquier formato y se pueden utilizar para guardar huellas Javascript, contenido CSS o contenido HTML personalizado." + create: + notice: Bloque creado correctamente + error: No se ha podido crear el bloque + update: + notice: Bloque actualizado correctamente + error: No se ha podido actualizar el bloque + destroy: + notice: Bloque borrado correctamente + edit: + title: Editar bloque + errors: + form: + error: Error + index: + create: Crear nuevo bloque + delete: Borrar bloque + title: Bloques + new: + title: Crear nuevo bloque + content_block: + body: Contenido + name: Nombre + images: + index: + title: Personalizar imágenes + update: Actualizar + delete: Borrar + image: Imagen + update: + notice: Imagen actualizada correctamente + error: No se ha podido actualizar la imagen + destroy: + notice: Imagen borrada correctamente + error: No se ha podido borrar la imagen + pages: + create: + notice: Página creada correctamente + error: No se ha podido crear la página + update: + notice: Página actualizada correctamente + error: No se ha podido actualizar la página + destroy: + notice: Página eliminada correctamente + edit: + title: Editar %{page_title} + errors: + form: + error: Error + form: + options: Opciones + index: + create: Crear nueva página + delete: Borrar página + title: Páginas + see_page: Ver página + new: + title: Página nueva + page: + created_at: Creada + status: Estado + title: Título + updated_at: Última actualización + status_draft: Borrador + status_published: Publicada diff --git a/config/locales/budgets.en.yml b/config/locales/budgets.en.yml index 5e4861a16..41f6fbe8f 100644 --- a/config/locales/budgets.en.yml +++ b/config/locales/budgets.en.yml @@ -5,15 +5,16 @@ en: title: Your ballot amount_spent: Amount spent remaining: "You still have %{amount} to invest." + no_balloted_group_yet: "You have not voted on this group yet, go vote!" remove: Remove vote voted_html: - one: "You have voted one proposal." - other: "You have voted %{count} proposals." - voted_info_html: "You can change your vote at any time until the close of this phase.
    No need to invest all the money available." + one: "You have voted one investment." + other: "You have voted %{count} investments." + voted_info_html: "You can change your vote at any time until the close of this phase.
    No need to spend all the money available." zero: You have not voted any investment project. reasons_for_not_balloting: not_logged_in: You must %{signin} or %{signup} to continue. - not_verified: Only verified users can vote on proposals; %{verify_account}. + not_verified: Only verified users can vote on investments; %{verify_account}. organization: Organizations are not permitted to vote not_selected: Unselected investment projects can not be supported not_enough_money: "Price is higher than the available amount left." @@ -24,12 +25,14 @@ en: title: Select an option unfeasible_title: Unfeasible investments unfeasible: See unfeasible investments + unselected_title: Investments not selected for balloting phase + unselected: See investments not selected for balloting phase phase: - accepting: Accepting proposals - reviewing: Reviewing proposals - selecting: Selecting proposals - valuating: Valuating proposals - balloting: Balloting proposals + accepting: Accepting projects + reviewing: Reviewing projects + selecting: Selecting projects + valuating: Valuating projects + balloting: Balloting projects reviewing_ballots: Reviewing Ballots finished: Finished budget index: @@ -58,7 +61,8 @@ en: voted_html: one: "You voted one proposal with a cost of %{amount_spent}" other: "You voted %{count} proposals with a cost of %{amount_spent}" - voted_info: You can change your vote at any time until the close of this phase. No need to spend all the money available. + voted_info: You can %{link} at any time until the close of this phase. No need to spend all the money available. + voted_info_link: change your vote zero: You have not voted any investment project. verified_only: "To create a new budget investment %{verify}." verify_account: "verify your account" @@ -84,10 +88,11 @@ en: title: Investment project supports: Supports votes: Votes + price: Price wrong_price_format: Only integer numbers investment: title: Investment project - add: Add + add: Vote already_added: You have already added this investment project already_supported: You have already supported this. Share it! support_title: Support this project @@ -106,4 +111,6 @@ en: group: Group phase: Actual phase unfeasible_title: Unfeasible investments - unfeasible: See unfeasible investments \ No newline at end of file + unfeasible: See unfeasible investments + unselected_title: Investments not selected for balloting phase + unselected: See investments not selected for balloting phase \ No newline at end of file diff --git a/config/locales/budgets.es.yml b/config/locales/budgets.es.yml index 08a33bde8..e5fc416e9 100644 --- a/config/locales/budgets.es.yml +++ b/config/locales/budgets.es.yml @@ -5,11 +5,12 @@ es: title: Mis votos amount_spent: Coste total remaining: "Te quedan %{amount} para invertir" + no_balloted_group_yet: "Todavía no has votado proyectos de este grupo, ¡vota!" remove: Quitar voto voted_html: one: "Has votado una propuesta." other: "Has votado %{count} propuestas." - voted_info_html: "Puedes cambiar tus votos en cualquier momento hasta el cierre de esta fase.
    No hace falta que inviertas todo el dinero disponible." + voted_info_html: "Puedes cambiar tus votos en cualquier momento hasta el cierre de esta fase.
    No hace falta que gastes todo el dinero disponible." zero: "Todavía no has votado ninguna propuesta de inversión." reasons_for_not_balloting: not_logged_in: Necesitas %{signin} o %{signup} para continuar. @@ -24,14 +25,16 @@ es: title: Selecciona una opción unfeasible_title: Propuestas inviables unfeasible: Ver propuestas inviables + unselected_title: Propuestas que no pasan a la votación final + unselected: Ver las propuestas que no pasan a la votación final phase: - accepting: Aceptando propuestas - reviewing: Revisando propuestas - selecting: Selección de propuestas - valuating: Evaluación de propuestas - balloting: Votación de propuestas - reviewing_ballots: Contando resultados - finished: Presupuesto terminado + accepting: Presentación de proyectos + reviewing: Revisión interna de proyectos + selecting: Fase de apoyos + valuating: Evaluación de proyectos + balloting: Votación final + reviewing_ballots: Votación finalizada + finished: Resultados index: title: Presupuestos participativos investments: @@ -58,7 +61,8 @@ es: voted_html: one: "Has votado una propuesta por un valor de %{amount_spent}" other: "Has votado %{count} propuestas por un valor de %{amount_spent}" - voted_info: Puedes cambiar tus votos en cualquier momento hasta el cierre de esta fase. No hace falta que gastes todo el dinero disponible. + voted_info: Puedes %{link} en cualquier momento hasta el cierre de esta fase. No hace falta que gastes todo el dinero disponible. + voted_info_link: cambiar tus votos zero: "Todavía no has votado ninguna propuesta de inversión." verified_only: "Para crear una nueva propuesta de inversión %{verify}." verify_account: "verifica tu cuenta" @@ -84,10 +88,11 @@ es: title: Propuesta de inversión supports: Apoyos votes: Votos + price: Coste wrong_price_format: Solo puede incluir caracteres numéricos investment: title: Propuesta de inversión - add: Añadir + add: Votar already_added: "Ya has añadido esta propuesta de inversión" already_supported: Ya has apoyado esta propuesta. ¡Compártelo! support_title: Apoyar esta propuesta @@ -107,3 +112,5 @@ es: phase: Fase actual unfeasible_title: Propuestas inviables unfeasible: Ver las propuestas inviables + unselected_title: Propuestas que no pasan a la votación final + unselected: Ver las propuestas que no pasan a la votación final \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index dd9624188..b6001a744 100755 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -139,6 +139,7 @@ en: errors: messages: user_not_found: User not found + invalid_date_range: "Invalid date range" form: accept_terms: I agree to the %{policy} and the %{conditions} accept_terms_title: I agree to the Privacy Policy and the Terms and conditions of use @@ -174,7 +175,6 @@ en: copyright: Consul, %{year} description: This portal uses the %{consul} which is %{open_source}. faq: technical assistance - more_info: More information open_data_text: Every detail about the City Council is yours to access. open_data_title: Open data open_source: open-source software @@ -191,15 +191,16 @@ en: debates: Debates external_link_blog: Blog external_link_opendata: Open data - external_link_opendata_url: "/opendata" + external_link_opendata_url: https://opendata.consul external_link_transparency: Transparency external_link_transparency_url: https://transparency.consul locale: 'Language:' - logo: Consul + logo: Consul logo management: Management moderation: Moderation valuation: Valuation - more_information: More information + officing: Polling officers + more_info: More information my_account_link: My account my_activity_link: My activity notifications: Notifications @@ -212,7 +213,7 @@ en: open_city_title: Love the city, and it will become a city you love. open_gov: Open government proposals: Proposals - proposal_ballot: Voting + poll_questions: Voting see_all: See proposals budgets: Participatory budgeting spending_proposals: Spending Proposals @@ -366,6 +367,8 @@ en: supports_necessary: "%{number} supports needed" total_percent: 100% archived: "This proposal has been archived and can't collect supports." + successful: "This proposal has reached the required supports and will be voted in the %{voting}." + voting: "next voting" show: author_deleted: User deleted code: 'Proposal code:' @@ -384,20 +387,64 @@ en: share: Share send_notification: Send notification no_notifications: "This proposal has any notifications." + embed_video_title: "Video on %{proposal}" update: form: submit_button: Save changes - proposal_ballots: - title: "Votings" - description_html: "The following citizen proposals that have reached the required supports and will be voted." - date_title: "Dates of participation" - date: "Soon we'll announce the date of vote these proposals." - successfull: "This proposal has reached the required supports and will be voted in the %{voting}." - voting: "next voting" - featured_title: "#NextVoting" - nothing_to_vote: "There is nothing to vote at the moment." - info: "New proposals that have reached the voting phase." - button: "I want to decide" + polls: + all: "All" + no_dates: "no date assigned" + dates: "From %{open_at} to %{closed_at}" + final_date: "Final recounts/Results" + index: + filters: + current: "Open" + incoming: "Incoming" + expired: "Expired" + title: "Polls" + participate_button: "Participate in this poll" + participate_button_incoming: "More information" + participate_button_expired: "Poll ended" + no_geozone_restricted: "All city" + geozone_restricted: "Districts" + geozone_info: "Can participate people in the Census of: " + can_answer: "You can participate in this poll!" + cant_answer: "This poll is not available on your geozone" + cant_answer_not_logged_in: "You must sign in or sign up to participate" + cant_answer_verify: "You must verify your account in order to answer" + already_answer: "You already have participated in this poll" + show: + dates_title: "Participation dates" + cant_answer_not_logged_in: "You must %{signin} or %{signup} to participate." + signin: Sign in + signup: Sign up + cant_answer_verify_html: "You must %{verify_link} in order to answer." + verify_link: "verify your account" + cant_answer_incoming: "This poll has not yet started." + cant_answer_expired: "This poll has finished." + poll_questions: + create_question: "Create question" + default_valid_answers: "Yes, No" + show: + answer_this_question: "Answer this question" + original_proposal: "Original proposal" + author: "Created by" + dates_title: "Participation dates" + more_info: "More information" + not_logged_in: "You must %{signin} or %{signup} to participate." + signin: Sign in + signup: Sign up + cant_answer_verify_html: "You must %{verify_link} in order to answer." + verify_link: "verify your account" + cant_answer_incoming: "This poll has not yet started." + cant_answer_expired: "This poll has finished." + cant_answer_wrong_geozone: "This question is not available on your geozone." + vote_answer: "Vote %{answer}" + voted: "You have voted %{answer}" + poll: "Poll" + banner: + featured_title: "#NextVoting" + info: "New proposals that have reached the voting phase." proposal_notifications: new: title: "Send message" @@ -409,6 +456,12 @@ en: show: back: "Go back to my activity" shared: + edit: 'Edit' + save: 'Save' + delete: 'Delete' + comments: + title: 'Comments' + login_to_comment: 'You must %{signin} or %{signup} to leave a comment.' "yes": "Yes" "no": "No" advanced_search: @@ -428,6 +481,7 @@ en: search: 'Filter' title: 'Advanced search' to: 'To' + delete: Delete author_info: author_deleted: User deleted back: Go back @@ -472,6 +526,8 @@ en: facebook: Facebook twitter: Twitter youtube: YouTube + whatsapp: WhatsApp + telegram: Telegram spending_proposals: form: association_name_label: 'If you propose in name of an assocation or collective add the name here' diff --git a/config/locales/es.yml b/config/locales/es.yml index 0b8fa4b1c..bcf6e1b0f 100755 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -139,6 +139,7 @@ es: errors: messages: user_not_found: No se encontró el usuario + invalid_date_range: "El rango de fechas no es válido" form: accept_terms: Acepto la %{policy} y las %{conditions} accept_terms_title: Acepto la Política de privacidad y las Condiciones de uso @@ -174,7 +175,6 @@ es: copyright: Consul, %{year} description: Este portal usa la %{consul} que es %{open_source}. faq: Asistencia técnica - more_info: Más información open_data_text: Todos los datos del Ayuntamiento son tuyos. open_data_title: Datos Abiertos open_source: software libre @@ -191,15 +191,16 @@ es: debates: Debates external_link_blog: Blog external_link_opendata: Datos abiertos - external_link_opendata_url: "/opendata" + external_link_opendata_url: https://opendata.consul external_link_transparency: Transparencia external_link_transparency_url: https://transparency.consul locale: 'Idioma:' - logo: Consul + logo: Consul logo management: Gestión moderation: Moderar valuation: Evaluación - more_information: Más información + officing: Presidentes de mesa + more_info: Más información my_account_link: Mi cuenta my_activity_link: Mi actividad notifications: Notificaciones @@ -212,9 +213,9 @@ es: open_city_title: La ciudad que quieres será la ciudad que quieras. open_gov: Gobierno %{open} proposals: Propuestas - proposal_ballot: Votaciones + poll_questions: Votaciones see_all: Ver propuestas - budgets: Presupuestos ciudadanos + budgets: Presupuestos participativos spending_proposals: "Propuestas de inversión" legislation: help: @@ -366,6 +367,8 @@ es: supports_necessary: "%{number} apoyos necesarios" total_percent: 100% archived: "Esta propuesta ha sido archivada y ya no puede recoger apoyos." + successful: "Esta propuesta ha alcanzado los apoyos necesarios y pasará a la %{voting}." + voting: "próxima votación" show: author_deleted: Usuario eliminado code: 'Código de la propuesta:' @@ -384,20 +387,64 @@ es: send_notification: Enviar notificación share: Compartir no_notifications: "Esta propuesta no tiene notificaciones." + embed_video_title: "Vídeo en %{proposal}" update: form: submit_button: Guardar cambios - proposal_ballots: - title: "Votaciones" - description_html: "Las siguientes propuestas ciudadanas han alcanzado el número de apoyos necesarios y pasarán a votación." - date_title: "Fechas de participación" - date: "En breve anunciaremos la fecha de votación de estas propuestas." - successfull: "Esta propuesta ha alcanzado los apoyos necesarios y pasará a la %{voting}." - voting: "próxima votación" - featured_title: "#PróximaVotación" - nothing_to_vote: "No hay nada que votar en este momento." - info: "Nuevas propuestas ciudadanas han llegado a la fase de votación." - button: "Quiero decidir" + polls: + all: "Todas" + no_dates: "sin fecha asignada" + dates: "Desde el %{open_at} hasta el %{closed_at}" + final_date: "Recuento final/Resultados" + index: + filters: + current: "Abiertas" + incoming: "Próximamente" + expired: "Terminadas" + title: "Votaciones" + participate_button: "Participar en esta votación" + participate_button_incoming: "Más información" + participate_button_expired: "Votación terminada" + no_geozone_restricted: "Toda la ciudad" + geozone_restricted: "Distritos" + geozone_info: "Pueden participar las personas empadronadas en: " + can_answer: "¡Puedes participar en esta votación!" + cant_answer: "Esta votación no está disponible en tu zona" + cant_answer_not_logged_in: "Necesitas iniciar sesión o registrarte para participar" + cant_answer_verify: "Por favor verifica tu cuenta para poder responder" + already_answer: "Ya has participado en esta votación" + show: + dates_title: "Fechas de participación" + cant_answer_not_logged_in: "Necesitas %{signin} o %{signup} para participar." + signin: iniciar sesión + signup: registrarte + cant_answer_verify_html: "Por favor %{verify_link} para poder responder." + verify_link: "verifica tu cuenta" + cant_answer_incoming: "Esta votación todavía no ha comenzado." + cant_answer_expired: "Esta votación ha terminado." + poll_questions: + create_question: "Crear pregunta para votación" + default_valid_answers: "Sí, No" + show: + answer_this_question: "Responder a esta pregunta" + original_proposal: "Propuesta original" + author: "Creado por" + dates_title: "Fechas de participación" + more_info: "Más información" + not_logged_in: "Necesitas %{signin} o %{signup} para participar." + signin: iniciar sesión + signup: registrarte + cant_answer_verify_html: "Por favor %{verify_link} para poder responder." + verify_link: "verifica tu cuenta" + cant_answer_incoming: "Esta votación todavía no ha comenzado." + cant_answer_expired: "Esta votación ha terminado." + cant_answer_wrong_geozone: "Esta votación no está disponible en tu zona." + vote_answer: "Votar %{answer}" + voted: "Has votado %{answer}" + poll: "Votación" + banner: + featured_title: "#PróximaVotación" + info: "Nuevas propuestas ciudadanas han llegado a la fase de votación." proposal_notifications: new: title: "Enviar mensaje" @@ -409,6 +456,12 @@ es: show: back: "Volver a mi actividad" shared: + edit: 'Editar' + save: 'Guardar' + delete: 'Borrar' + comments: + title: 'Comentarios' + login_to_comment: 'Necesitas %{signin} o %{signup} para comentar.' "yes": "Sí" "no": "No" advanced_search: @@ -428,6 +481,7 @@ es: search: 'Filtrar' title: 'Búsqueda avanzada' to: 'Hasta' + delete: 'Borrar' author_info: author_deleted: Usuario eliminado back: Volver @@ -472,6 +526,8 @@ es: facebook: Facebook twitter: Twitter youtube: YouTube + whatsapp: WhatsApp + telegram: Telegram spending_proposals: form: association_name_label: 'Si propones en nombre de una asociación o colectivo añade el nombre aquí' @@ -567,8 +623,8 @@ es: one: 1 Propuesta other: "%{count} Propuestas" budget_investments: - one: 1 Propuesta de inversión - other: "%{count} Propuestas de inversión" + one: 1 Proyecto de presupuestos participativos + other: "%{count} Proyectos de presupuestos participativos" no_activity: Usuario sin actividad pública no_private_messages: "Este usuario no acepta mensajes privados." private_activity: Este usuario ha decidido mantener en privado su lista de actividades diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 596531c0d..74a65a916 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -854,7 +854,6 @@ fr: description: Ce portail utiliser %{consul} qui est %{open_source}. De Madrid ouvert sur le monde. faq: assistance technique - more_info: Plus d'information open_data_text: Tout les détails de la Mairie sont vôtre pour accès open_data_title: Open data open_source: logiciel libre @@ -874,9 +873,9 @@ fr: external_link_transparency: Transparence external_link_transparency_url: https://transparency.consul locale: 'Langue :' - logo: Consul + logo: Consul logo moderation: Modération - more_information: Plus d'information + more_info: Plus d'information my_account_link: Mon compte my_activity_link: Mon activité new_notifications: @@ -1167,7 +1166,7 @@ fr: mentionnées dans les conditions d'utilisation de ce portail." conditions: Conditions d'utilisation general_terms: Conditions d'utilisation - more_information: + more_info: description: faq: Questions fréquentes sur les problèmes techniques how_it_works: Découvrez tout ce qui est possible de faire avec ce site @@ -1232,7 +1231,7 @@ fr: titles: accessibility: Accessibilité conditions: Conditions d'utilisation - more_information: Plus d'informations + more_info: Plus d'informations opendata: Open Data privacy: Vie privée verify: @@ -1354,7 +1353,8 @@ fr: official_level_3_name: Niveau 3 personne officielle official_level_4_name: Niveau 4 personne officielle official_level_5_name: Niveau 5 personne officielle - per_page_code: Code à inclure dans chaque page + per_page_code_head: "Code à inclure dans chaque page ()" + per_page_code_body: "Code à inclure dans chaque page ()" proposal_code_prefix: Préfixe pour les codes de propositions votes_for_proposal_success: Nombre de votes nécessaires pour l'approbation d'une proposition @@ -1395,6 +1395,8 @@ fr: facebook: Facebook twitter: Twitter youtube: YouTube + whatsapp: WhatsApp + telegram: Telegram social_share_button: baidu: Baidu.com delicious: Delicious @@ -1413,6 +1415,7 @@ fr: tumblr: Tumblr twitter: Twitter weibo: Sina Weibo + telegram: Telegram spending_proposals: form: description: Description @@ -1629,7 +1632,7 @@ fr: link: Voir les propositions text: Créez vos propres propositions et soutenez celles d'autres citoyens. title: Propositions citoyennes - more_information: Plus d'information sur cette page + more_info: Plus d'information sur cette page proposal: alt: Icône Proposez description: Espace ouvert aux propositions sur le type de ville que nous voulons diff --git a/config/locales/management.en.yml b/config/locales/management.en.yml index 25032010e..d8aaf826c 100644 --- a/config/locales/management.en.yml +++ b/config/locales/management.en.yml @@ -32,6 +32,7 @@ en: under_age: "You don't have the required age to verify your account." verify: Verify email_label: Email + date_of_birth: Date of birth email_verifications: already_verified: This user account is already verified. choose_options: 'Please choose one of the following options:' diff --git a/config/locales/management.es.yml b/config/locales/management.es.yml index 9ac276f3a..10543798a 100644 --- a/config/locales/management.es.yml +++ b/config/locales/management.es.yml @@ -32,6 +32,7 @@ es: under_age: No tienes edad suficiente para verificar tu cuenta. verify: Verificar usuario email_label: Email + date_of_birth: Fecha de nacimiento email_verifications: already_verified: Esta cuenta de usuario ya está verificada. choose_options: 'Elige una de las opciones siguientes:' diff --git a/config/locales/moderation.en.yml b/config/locales/moderation.en.yml index 6304e8d62..66697f5e4 100755 --- a/config/locales/moderation.en.yml +++ b/config/locales/moderation.en.yml @@ -1,6 +1,8 @@ --- en: moderation: + header: + title: Moderation comments: index: block_authors: Block authors diff --git a/config/locales/moderation.es.yml b/config/locales/moderation.es.yml index 2d7c3aea5..b41ba0fbf 100644 --- a/config/locales/moderation.es.yml +++ b/config/locales/moderation.es.yml @@ -1,6 +1,8 @@ --- es: moderation: + header: + title: Moderación comments: index: block_authors: Bloquear autores diff --git a/config/locales/officing.en.yml b/config/locales/officing.en.yml new file mode 100644 index 000000000..ffda9d326 --- /dev/null +++ b/config/locales/officing.en.yml @@ -0,0 +1,103 @@ +--- +en: + officing: + header: + title: Polling + dashboard: + index: + title: Poll officing + info: Here you can validate user documents and store voting results + menu: + voters: Validate document + recounts: Store recount + final_recounts: Final recounts and results + polls: + index: + title: Poll list + no_polls: You are not officing in any active poll + select_poll: Select poll + add_recount: Add recount + final: + title: Polls ready for final recounting + no_polls: You are not officing final recounts in any active poll + select_poll: Select poll + add_recount: Add final recount + add_results: Add results + recounts: + flash: + create: "Data added" + error_create: "Count NOT added. Error in data." + new: + title: "%{poll} - Add daily recount" + not_allowed: "You are not a poll officer for this poll" + booth_date: "Booth and date" + select_booth_date: "Select booth and date" + count: "Vote count" + count_placeholder: "Vote count" + submit: Save + recount_list: "Your recounts" + booth: "Booth" + date: "Date" + final_recounts: + flash: + create: "Data added" + error_create: "Final counts NOT added. Error in data." + new: + title: "%{poll} - Add final recount" + not_allowed: "You are allowed to add final recounts for this poll" + booth: "Booth" + date: "Date" + select_booth: "Select booth" + select_date: "Select date" + count: "Final vote count" + count_placeholder: "Final vote count" + submit: Save + final_recount_list: "Your final recounts" + system_count: "System recount" + add_results: "Add results" + results: + flash: + create: "Results saved" + error_create: "Results NOT saved. Error in data." + error_wrong_booth: "Wrong booth. Results NOT saved." + error_wrong_date: "Wrong date. Results NOT saved." + new: + title: "%{poll} - Add results" + not_allowed: "You are allowed to add results for this poll" + booth: "Booth" + date: "Date" + select_booth: "Select booth" + select_date: "Select date" + ballots_white: "Blank ballots" + ballots_null: "Invalid ballots" + submit: "Save" + results_list: "Your results" + see_results: "See results" + index: + no_results: "No results" + results: Results + table_answer: Answer + table_votes: Votes + table_whites: "Blank ballots" + table_nulls: "Invalid ballots" + residence: + flash: + create: "Document verified with Census" + not_allowed: "You don't have officing shifts today" + new: + title: Validate document + document_number: "Document number (including letters)" + submit: Validate document + error_verifying_census: "The Census was unable to verify this document." + form_errors: prevented the verification of this document + no_assignments: "You don't have officing shifts today" + voters: + new: + title: Polls + table_poll: Poll + table_actions: Polls status + show: + can_vote: Can vote + error_already_voted: Has already participated in this poll + submit: Confirm vote + success: "Vote introduced!" diff --git a/config/locales/officing.es.yml b/config/locales/officing.es.yml new file mode 100644 index 000000000..7958b64b0 --- /dev/null +++ b/config/locales/officing.es.yml @@ -0,0 +1,103 @@ +--- +es: + officing: + header: + title: Votaciones + dashboard: + index: + title: Presidir mesa de votaciones + info: Aquí puedes validar documentos de ciudadanos y guardar los resultados de las urnas + menu: + voters: "Validar documento y votar" + recounts: "Recuento diario" + final_recounts: "Recuento final y escrutinio" + polls: + index: + title: "Listado de votaciones" + no_polls: "No eres presidente de mesa en ninguna votación activa" + select_poll: "Selecciona votación" + add_recount: "Añadir recuento" + final: + title: "Listado de votaciones finalizadas" + no_polls: "No tienes permiso para recuento final en ninguna votación reciente" + select_poll: "Selecciona votación" + add_recount: "Añadir recuentos finales" + add_results: "Añadir resultados" + recounts: + flash: + create: "Datos añadidos" + error_create: "Recuento NO añadido. Error en los datos" + new: + title: "%{poll} - Añadir recuento diario" + not_allowed: "No eres presidente de mesa en esta votación" + booth_date: "Fecha y urna" + select_booth_date: "Elige fecha y urna" + count: "Número de votos" + count_placeholder: "Número de votos" + submit: "Guardar" + recount_list: "Tus recuentos" + booth: "Urna" + date: "Fecha" + final_recounts: + flash: + create: "Datos añadidos" + error_create: "Recuento final NO añadido. Error en los datos" + new: + title: "%{poll} - Añadir recuento final" + not_allowed: "No tienes permiso para introducir recountos finales" + booth: "Urna" + date: "Día" + select_booth: "Elige urna" + select_date: "Elige día" + count: "Recuento final" + count_placeholder: "Número final de votos" + submit: "Guardar" + final_recount_list: "Tus recuentos finales" + system_count: "Recuento del sistema" + add_results: "Añadir resultados" + results: + flash: + create: "Datos guardados" + error_create: "Resultados NO añadidos. Error en los datos" + error_wrong_booth: "Urna incorrecta. Resultados NO guardados." + error_wrong_date: "Fecha incorrecta. Resultados NO guardados." + new: + title: "%{poll} - Añadir resultados" + not_allowed: "No tienes permiso para introducir resultados" + booth: "Urna" + date: "Día" + select_booth: "Elige urna" + select_date: "Elige día" + ballots_white: "Papeletas en blanco" + ballots_null: "Papeletas nulas" + submit: "Guardar" + results_list: "Tus resultados" + see_results: "Ver resultados" + index: + no_results: "No hay resultados" + results: "Resultados" + table_answer: Respuesta + table_votes: Votos + table_whites: Papeletas en blanco + table_nulls: Papeletas nulas + residence: + flash: + create: "Documento verificado con el Padrón" + not_allowed: "Hoy no tienes turno de presidente de mesa" + new: + title: Validar documento + document_number: "Número de documento (incluida letra)" + submit: Validar documento + error_verifying_census: El Padrón no pudo verificar este documento. + form_errors: evitaron verificar este documento + no_assignments: "Hoy no tienes turno de presidente de mesa" + voters: + new: + title: Votaciones + table_poll: Votación + table_actions: Estado de las votaciones + show: + can_vote: Puede votar + error_already_voted: "Ya ha participado en esta votación." + submit: Confirmar voto + success: "¡Voto introducido!" diff --git a/config/locales/pages.en.yml b/config/locales/pages.en.yml index 4ac76b1d3..ab086bdbd 100755 --- a/config/locales/pages.en.yml +++ b/config/locales/pages.en.yml @@ -4,42 +4,57 @@ en: census_terms: To confirm the account, you must be 16 or older and be registered, having provided the information requested previously, will verify. By accepting the verification process, you also consent to the verification of this information, as well as the contact methods featuring in said files. The data provided will be acquired and processed in a file mentioned previously in the terms and conditions of use for the Portal. conditions: Terms and conditions of use general_terms: Terms and Conditions - more_information: - description: - faq: Frecuently asked question about tecnical problems - how_it_works: Find out all you can do with this web site - how_to_use: Use it freely or help us to improve it, it is free software - participation: Citizen participation, transparency and open government - participation_facts: To lose your fear - participation_world: Systems of citizen participation that exist in the world - proposals_info: Create your own proposals - signature_sheet: Sheet collecting signatures for support - spending_proposals_info: Create your own spending proposals - how_it_works: - i: Participation - i1: Debates area - i2: Proposals area - text: |- - The new Open Government Portal is divided in three parts: Participation, Transparency and Open Data (you can see the links in the upper right-hand part). - >**I. Participation:** In this part we can decide which city we want (using citizen proposals, debate areas, participatory budgets , collaborative legislation, and many others we will implement). - **II. Transparency:** In this part information about how the city is managed will be published every day: name, position, salary, contracts, agenda,... of the persons in charge. It is also the place where exercise your right to access information, having the chance to request any information about City Council easily and rapidly. - **III. Open data:** In this part the city council databases are posted, so that anyone can use information directly, without having to ask. It can also be requested the publication of more databases. - - We have opened the new Participation portal and we will open the new Transparency and Open Data seccion shortly (meanwhile the links to existing sites are kept). - ## I. Participation - The participation section will have different ways to participate: citizen proposals, debate areas, participatory budgets , collaborative legislation, and many others. You can currently visit the debate area and the citizen proposals will be ready very soon. - ## I.I. Debates area - In the debates area everyone can open a discussion thread about any topic, creating an independent space where people could debate about the proposed topic. In this way, in this digital space there will be hundreds of different debate spaces which citizens could see, joining the ones they find more interesting. - - Both threads and comments could be valued by anyone, so the citizenship, and not someone in its name, will decide which are the most important issues in every moment. These will be showed in the main page of the space, being able to access also the rest of issues in following pages, or using others order criteria (the most commented, the newst, the most controversial, etc.). - - Every city council employee has its own user, which will be designated as such, allowing them to participate in debates at the same level than the rest of citizens. That will allow creating direct communication spaces between them, avoiding the problems that implies the measured communication, and following clear approach of the new City Council by virtue of which the city council works for the citizenship. - ## I.I. Proposals area - In the proposals area everyone can propose an initiative with the intention of collecting support enough for the idea being consulting to the whole citizenship with binding effect. - - The proposals can be supported by every citizen registered that has verified their account in the participation platform. In this way, the citizenship, and not someone in its name, will decide which are the proposals that are worthwhile to carry out. - - One that the proposal has achive support of 1% census, it will be studied by a city council group and to move beyond the popular referendum phase, in which citizenship will vote if it is carried out or not. The maximum period to obtain support enough is 12 months. + more_info: + title: "Discover %{org_name}" + subtitle: "Learn everything you can do on this website." + guide: 'This guide explains each section of %{org_name}. You can expand the information on "Detailed information" links.' + menu: + debates: "Debates" + proposals: "Proposals" + budgets: "Participatory budgets" + other: "Other information of interest" + debates: + title: "Debates" + description: "Create a thread where you can discuss any topic you want to share with other people in your city." + feature_1: "To create a debate you have to %{link}" + feature_1_link: "sign up on %{org_name}" + feature_2_html: "Debates can be rated using the I agree or I disagree buttons you'll find on each of them." + image_alt: "Buttons to rate the debates" + figcaption: '"I agree" and "I disagree" buttons to rate the debates.' + proposals: + title: "Proposals" + description: "Propose what you want the City Council to do and support proposals from other people." + feature_1: "To create a proposal you have to %{link}, in addition to support you must verify your account." + feature_1_link: "sign up on %{org_name}" + feature_2_html: "Proposals that get support from 1% of people will be voted on." + feature_3_html: "If there are more people in favor than against, the City Council will publish their plan to accomplish the proposal and you can track its progress." + image_alt: "Button to support a proposal" + figcaption_html: 'Button to "Support" a proposal.
    When it reaches the number of supports will go to vote.' + budgets: + title: "Participatory Budgeting" + description: "The first six months of each year you can decide how to spend part of the budget." + feature_1: "To submit an investment project you have to %{link} and verify your account." + feature_1_link: "sign up on %{org_name}" + feature_2_html: "The first is the submit phase of investment projects." + feature_3_html: "Then there is a support phase to prioritize the most interesting, the most supported are evaluated by the City to see if they are viable and how much they are worth." + feature_4_html: "At the end there is a vote phase and you decide which of the approved projects to spend the budget on." + image_alt: "Different phases of a participatory budget" + figcaption_html: '"Support phase" and "Voting" phase of participatory budgets.' + polls: + title: "Polls" + description: "Citizen proposals that reach 1% of support will be put to a vote." + feature_1: "To participate in the next poll you have to %{link} and verify your account." + feature_1_link: "sign up on %{org_name}" + feature_2: "All verified users over 16 years old can vote." + feature_3: "The results of all votes shall be binding on the government." + faq: + title: "Technical problems?" + description: "Read the FAQs and solve your questions." + button: "View frequently asked questions" + other: + how_to_use: "Use %{org_name} in your city" + world: "Citizen participation in the world" + facts: "Facts about citizen participation and direct democracy" how_to_use: text: |- Use it in your local government or help us to improve it, it is free software. @@ -48,24 +63,12 @@ en: If you are a programmer, you can see the code and help us to improve it at [Consul app](https://github.com/ayuntamientomadrid 'consul github'). titles: - faq: Solution to tecnical problemas (FAQ) - how_it_works: How does this Open Government Portal work? how_to_use: Use it in your local government - participation: Participation and Transparency y Transparencia - coming news - participation_facts: Facts about citizen participation and direct democracy - participation_world: Direct citizen participation in the world - proposals_info: How does citizen proposals work? - signature_sheet: Signature sheet - spending_proposals_info: How does participatory budgeting work? - opendata: - slogan: "Information about Open Data." - title: Open Data privacy: Privacy Policy titles: accessibility: Accessibility conditions: Terms of use - more_information: More information - opendata: Open Data + more_info: "More information about %{org_name}" privacy: Privacy Policy verify: code: Code you received in letter @@ -74,4 +77,4 @@ en: info_code: 'Now introduce the code you received in letter:' password: Password submit: Verify my account - title: Verify your account + title: Verify your account \ No newline at end of file diff --git a/config/locales/pages.es.yml b/config/locales/pages.es.yml index e3e82effa..27f1afc16 100644 --- a/config/locales/pages.es.yml +++ b/config/locales/pages.es.yml @@ -4,42 +4,57 @@ es: census_terms: Para verificar la cuenta hay que tener 16 años o más y estar empadronado aportando los datos indicados anteriormente, los cuales serán contrastados. Aceptando el proceso de verificación acepta dar su consentimiento para contrastar dicha información, así como medios de contacto que figuren en dichos ficheros. Los datos aportados serán incorporados y tratados en un fichero indicado anteriormente en las condiciones de uso del portal. conditions: Condiciones de uso general_terms: Términos y Condiciones - more_information: - description: - faq: Preguntas frecuentes sobre problemas técnicos - how_it_works: Descubre todo lo que puedes hacer en esta web - how_to_use: Utilízalo libremente o ayúdanos a mejorarlo, es software libre - participation: Participación Ciudadana, Transparencia y Gobierno Abierto - participation_facts: Para perderle el miedo - participation_world: Sistemas de participación ciudadana que ya existen en el mundo - proposals_info: Crea tus propias propuestas - signature_sheet: Hojas de firmas para recoger apoyos - spending_proposals_info: Envía tus propuestas de gasto - how_it_works: - i: Participación - i1: Espacio de debate - i2: Espacio de propuestas - text: |- - El nuevo Portal de Gobierno Abierto está dividido en tres partes: Participación, Transparencia y Datos Abiertos (verás los enlaces en la parte superior derecha). - >**I. Participación:** Donde poder decidir qué ciudad queremos tener (a través de propuestas ciudadanas, espacios de debate, presupuestos participativos, legislación colaborativa, y muchos otros procesos que iremos implementando). - **II. Transparencia:** En este espacio se publicarán todos los datos relativos a quién y cómo se gestiona la ciudad (nombres de los responsables, cargos, sueldos, planes de gobierno, contratos, agendas de los responsables...). Además es el espacio donde ejercer el derecho de acceso a la información, pudiendo solicitar cualquier información sobre el Ayuntamiento de manera fácil y rápida. - **III. Datos Abiertos:** En este espacio se cuelgan las bases de datos que tiene el Ayuntamiento, para que cualquiera pueda usar toda la información directamente, sin necesidad ni siquiera de preguntar. También se puede solicitar la publicación de más bases de datos. - - Inauguramos el nuevo portal con la nueva sección de Participación, y en breve añadiremos las nuevas secciones de Transparencia y Datos Abiertos (así que mantenemos por el momento los enlaces a las páginas al respecto que ya existían). - ## I. Participación - El apartado de participación comprenderá diferentes mecanismos de participación: propuestas ciudadanas, espacios de debate, presupuestos participativos, legislación colaborativa, entre muchos otros. Por el momento presentamos el espacio de debate, y muy pronto las propuestas ciudadanas. - ## I.I. Espacio de debate - En este espacio, cualquier persona puede abrir un hilo de discusión sobre cualquier tema, creando un espacio independiente donde la gente podrá debatir sobre el tema propuesto. De esta manera, en este espacio digital convivirán cada día cientos de espacios de debate distintos entre los que la ciudadanía podrá pasearse, sumándose a los que más les interese. - - Tanto los hilos, como los comentarios podrán ser valorados por cualquiera, de tal manera que será la propia ciudadanía, y nadie en su nombre, la que decida cuáles son los temas más importantes en cada momento. Estos serán presentados en la portada del espacio, pudiendo por supuesto accederse a todos los demás temas en páginas posteriores, o usando otros criterios de ordenación (los temas con más comentarios, los más nuevos, los más controvertidos, etc.). - - Cada uno de los trabajadores del Ayuntamiento tiene un usuario propio, que será resaltado como tal, permitiendo que participen en los debates al mismo nivel que todos los demás ciudadanos. Esto permitirá crear espacios de comunicación directos entre unos y otros, evitando los inconvenientes que implica la comunicación mediada, y respondiendo a un planteamiento claro por parte del nuevo gobierno por el cual el Ayuntamiento trabaja para la ciudadanía, y ante ella debe responder. - ## I.I. Espacio de propuestas - En este espacio, cualquier persona puede proponer una iniciativa con la intención de recabar los suficientes apoyos como para que la idea pase a ser consultada a toda la ciudadanía con carácter vinculante. - - Las propuestas pueden ser apoyadas por ciudadanos empadronados que hayan verificado su cuenta en la plataforma de participación, de tal manera que será la propia ciudadanía, y nadie en su nombre, la que decida cuáles son las propuestas que merecen la pena ser llevadas a cabo. - - Una vez que una propuesta alcance una cantidad de apoyos equivalente al 1% del censo, automáticamente pasa a ser estudiada por un grupo de trabajo del Ayuntamiento y pasará a la siguiente fase de consulta popular, en la que la ciudadanía votará si se lleva a cabo o no. El plazo máximo para recabar los apoyos necesarios será de 12 meses. + more_info: + title: "Descubre %{org_name}" + subtitle: "Aprende todo lo que puedes hacer en esta web." + guide: 'Esta guía explica cada una de las secciones de %{org_name}. Puedes ampliar la información en los enlaces de "Información detallada".' + menu: + debates: "Debates" + proposals: "Propuestas" + budgets: "Presupuestos participativos" + other: "Otra información de interés" + debates: + title: "Debates" + description: "Crea un hilo en el que debatir sobre cualquier tema que quieras compartir con el resto de gente de tu ciudad." + feature_1: "Para crear un debate tienes que %{link}" + feature_1_link: "registrarte en %{org_name}" + feature_2_html: "Los debates pueden ser valorados utilizando los botones de Estoy de acuerdo o No estoy de acuerdo que encontrarás en cada uno de ellos." + image_alt: "Botones para valorar los debates" + figcaption: 'Botones "Estoy de acuerdo" y "No estoy de acuerdo" para valorar los debates.' + proposals: + title: "Propuestas" + description: "Propón lo que quieres que el Ayuntamiento lleve a cabo y apoya propuestas de otras personas." + feature_1: "Para crear una propuesta tienes que %{link}, además para apoyarlas debes verificar tu cuenta." + feature_1_link: "registrarte en %{org_name}" + feature_2_html: "Las propuestas que consigan el apoyo del 1% de la gente (mayor de 16 años empadronada en Madrid; 27.064 apoyos) pasan a votación." + feature_3_html: "Si hay más gente a favor que en contra en la votación, el Ayuntamiento asume la propuesta y se hace." + image_alt: "Botón para apoyar una propuesta" + figcaption_html: 'Botón para "Apoyar" una propuesta.
    Cuando alcance el número de apoyos pasará a votación.' + budgets: + title: "Presupuestos participativos" + description: "Los primeros seis meses de cada año puedes decidir cómo gastar parte del presupuesto." + feature_1: "Para crear un proyecto de gasto tienes que %{link} y verificar tu cuenta." + feature_1_link: "registrarte en %{org_name}" + feature_2_html: "En primer lugar empieza la fase de aceptación de proyectos de gasto." + feature_3_html: "Después hay una fase de apoyos para priorizar lo más interesante, las más apoyadas son evaluadas por el Ayuntamiento para ver si son viables y cuánto valen." + feature_4_html: "Al final hay una fase de votación donde se decide en cuáles se gasta esa parte del presupuesto." + image_alt: "Diferentes fases de un presupuesto participativo" + figcaption_html: 'Fase de "Apoyos" y fase de "Votación" de los presupuestos participativos.' + polls: + title: "Votaciones" + description: "Las propuestas ciudadanas que alcancen el 1% de apoyos pasarán a votación." + feature_1: "Para participar en la próxima votación tienes que %{link} y verificar tu cuenta." + feature_1_link: "registrarte en %{org_name}" + feature_2: "Pueden votar todas las personas empadronadas en la ciudad mayores de 16 años." + feature_3: "Los resultados de todas las votaciones serán vinculantes para el gobierno." + faq: + title: "¿Problemas técnicos?" + description: "Lee las preguntas frecuentes y resuelve tus dudas." + button: "Ver preguntas frecuentes" + other: + how_to_use: "Utiliza %{org_name} en tu municipio" + world: "Participación ciudadana directa en el mundo" + facts: "Hechos sobre participación ciudadana y democracia directa" how_to_use: text: |- Utilízalo en tu municipio libremente o ayúdanos a mejorarlo, es software libre. @@ -48,24 +63,12 @@ es: Si eres programador, puedes ver el código y ayudarnos a mejorarlo en [aplicación Consul](https://github.com/ayuntamientomadrid 'github consul'). titles: - faq: Soluciones a problemas técnicos (FAQ) - how_it_works: "¿Cómo funciona este Portal de Gobierno Abierto?" how_to_use: Utilízalo en tu municipio - participation: Participación y Transparencia - Próximas novedades - participation_facts: Hechos sobre participación ciudadana y democracia directa - participation_world: Participación ciudadana directa en el mundo - proposals_info: "¿Cómo funcionan las propuestas ciudadanas?" - signature_sheet: Hojas de firmas - spending_proposals_info: "¿Cómo funcionan los presupuestos participativos?" - opendata: - slogan: "Información sobre Datos abiertos." - title: Datos abiertos privacy: Política de Privacidad titles: accessibility: Accesibilidad conditions: Condiciones de uso - more_information: Más información - opendata: Datos abiertos + more_info: "Más información sobre %{org_name}" privacy: Política de Privacidad verify: code: Código que has recibido en tu carta @@ -74,4 +77,4 @@ es: info_code: 'Ahora introduce el código que has recibido en tu carta:' password: Contraseña submit: Verificar mi cuenta - title: Verifica tu cuenta + title: Verifica tu cuenta \ No newline at end of file diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index ace7406a7..298d536ab 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -29,8 +29,7 @@ pt-BR: user_permission_support_proposal: Apoiar propostas user_permission_title: Participação user_permission_verify: Para realizar todas as ações verifique sua conta. - user_permission_verify_info: "* Somente para usuários no Censo da Cidade de - Madrid." + user_permission_verify_info: "* Somente para usuários no Censo da Cidade de Madrid." user_permission_votes: Participar da votação final verified_account: Conta verificada verify_my_account: Verificar minha conta @@ -363,6 +362,7 @@ pt-BR: - Sex - Sab abbr_month_names: + - - Jan - Fev - Mar @@ -384,9 +384,9 @@ pt-BR: - Sexta - Sábado formats: - default: "%Y-%m-%d" - long: "%B %d, %Y" - short: "%b %d" + default: "%d/%m/%Y" + long: "%d de %B de %Y" + short: "%d de %b" month_names: - Janeiro - Fevereiro @@ -401,9 +401,9 @@ pt-BR: - Novembro - Dezembro order: - - ano - - mês - - dia + - :day + - :month + - :year datetime: distance_in_words: about_x_hours: @@ -628,8 +628,7 @@ pt-BR: email. info_text: Sua senha não será alterada, a menos que você acesse o link e edite a mesma. - text: 'Nós recebemos uma solicitação para alteração de sua senha. Você poderá - realizar isto, seguindo este link:' + text: 'Nós recebemos uma solicitação para alteração de sua senha. Você poderá realizar isto, seguindo este link:' title: Alterar sua senha unlock_instructions: hello: Olá @@ -652,20 +651,15 @@ pt-BR: password_label: Senha phone_number_label: Número de telefone responsible_name_label: Nome completo da pessoa responsável pelo coletivo - responsible_name_note: Esta será a pessoa representando a associação/coletivo - na qual será identificada as propostas apresentadas + responsible_name_note: Esta será a pessoa representando a associação/coletivo na qual será identificada as propostas apresentadas submit: Registrar title: Registrar como uma organização ou coletivo success: back_to_index: Eu compreendo; voltar para a página principal - instructions_1_html: "Entraremos em contato brevemente para verificar - que você de fato representa este coletivo." - instructions_2_html: Enquanto seu email é verificado, nós lhe enviamos - um link para confirmar sua conta. - instructions_3_html: Depois de confirmado, você poderá iniciar a participação - como um coletivo sem verificação - thank_you_html: Obrigado por registrar seu coletivo no website. Ele está - agora pendente de verificação. + instructions_1_html: "Entraremos em contato brevemente para verificar que você de fato representa este coletivo." + instructions_2_html: Enquanto seu email é verificado, nós lhe enviamos um link para confirmar sua conta. + instructions_3_html: Depois de confirmado, você poderá iniciar a participação como um coletivo sem verificação + thank_you_html: Obrigado por registrar seu coletivo no website. Ele está agora pendente de verificação. title: Registro de organização/coletivo passwords: edit: @@ -847,7 +841,6 @@ pt-BR: description: Este portal usa o %{consul} que é %{open_source}. De Madrid para o mundo. faq: suporte técnico - more_info: Mais informações open_data_text: Todos os detalhes sobre a Câmara estão disponíveis para acesso. open_data_title: Dados abertos open_source: programa de código aberto @@ -866,9 +859,9 @@ pt-BR: external_link_transparency: Transparência external_link_transparency_url: https://transparency.consul locale: 'Idioma:' - logo: Consul + logo: Consul logotipo moderation: Moderação - more_information: Mais informações + more_info: Mais informações my_account_link: Minha conta my_activity_link: Minha atividade new_notifications: @@ -925,8 +918,7 @@ pt-BR: document_type_label: Tipo do documento document_verifications: already_verified: Esta conta de usuário já foi verificada. - has_no_account_html: No sentido de criar uma conta, vá para %{link} e clique - em 'Register' na parte superior esquerda de sua tela/janela. + has_no_account_html: No sentido de criar uma conta, vá para %{link} e clique em 'Register' na parte superior esquerda de sua tela/janela. in_census_has_following_permissions: 'Este usuário pode participar no website com as seguintes permissões:' not_in_census: Este documento não está registrado. @@ -1156,7 +1148,7 @@ pt-BR: mencionado previamente nos termos e condições de uso deste Portal. conditions: Termos e condições de uso general_terms: Termos e condições - more_information: + more_info: description: faq: Perguntas frequentes feitas sobre problemas técnicos how_it_works: Descubra tudo que você pode fazer com este website. @@ -1255,7 +1247,7 @@ pt-BR: titles: accessibility: Acessibilidade conditions: Termos de uso - more_information: Mais informações + more_info: Mais informações opendata: Dados abertos privacy: Política de privacidade verify: @@ -1377,7 +1369,8 @@ pt-BR: official_level_3_name: Nível 3 coordenador público official_level_4_name: Nível 4 coordenador público official_level_5_name: Nível 5 coordenador público - per_page_code: Código a ser incluído em cada página + per_page_code_head: "Código a ser incluído em cada página ()" + per_page_code_body: "Código a ser incluído em cada página ()" proposal_code_prefix: Prefixo para códigos de Proposta votes_for_proposal_success: Número de votos necessários para aprovar uma Proposta shared: @@ -1449,8 +1442,7 @@ pt-BR: new: back_link: Voltar more_info: Como funciona o orçamento participativo? - recommendation_one: "É mandatório que a proposta faça referência a uma ação - orçamentária." + recommendation_one: "É mandatório que a proposta faça referência a uma ação orçamentária." recommendations_title: Como criar uma proposta de despesa recommendation_three: Tente descer aos detalhes quando descrever sua proposta de despesa de maneira a ajudar a equipe de revisão entender seu argumento. @@ -1465,15 +1457,15 @@ pt-BR: time: am: am formats: - datetime: "%Y-%m-%d %H:%M:%S" - default: "%a, %d %b %Y %H:%M:%S %z" - long: "%B %d, %Y %H:%M" - short: "%d %b %H:%M" + default: "%A, %d de %B de %Y %H:%M:%S %z" + long: "%d de %B de %Y %H:%M" + short: "%d de %b %H:%M" + datetime: "%d/%m/%Y %H:%M:%S" pm: pm unauthorized: default: Você não possui permissão de acesso a esta página. manage: - all: Você não possui permissão para executar esta ação '%{action}' em %{subject}. + all: "Você não possui permissão para executar esta ação '%{action}' em %{subject}." users: show: deleted: Apagado @@ -1606,8 +1598,7 @@ pt-BR: submit_button: Enviar código show: email_title: Emails - explanation: 'Nós atualmente possuímos os seguintes detalhes no Registro: - por favor selecione um método para o envio de seu código de confirmação.' + explanation: 'Nós atualmente possuímos os seguintes detalhes no Registro: por favor selecione um método para o envio de seu código de confirmação.' phone_title: Números de telefone title: Informação disponível use_another_phone: Usar outro telefone @@ -1649,11 +1640,10 @@ pt-BR: link: Mostrar propostas text: Criar suas próprias propostas e apoiar aquelas de outros cidadãos title: Propostas cidadãs - more_information: Mais informações sobre esta página + more_info: Mais informações sobre esta página proposal: alt: "Ícone proposta" - description: Espaço aberto para propostas cidadãs sobre o tipo de cidade dentro - da qual nós queremos viver. + description: Espaço aberto para propostas cidadãs sobre o tipo de cidade dentro da qual nós queremos viver. title: Sua proposta welcome: go_to_index: Veja propostas e debates diff --git a/config/locales/responders.en.yml b/config/locales/responders.en.yml index 100ae5f41..099751345 100755 --- a/config/locales/responders.en.yml +++ b/config/locales/responders.en.yml @@ -6,6 +6,8 @@ en: notice: "%{resource_name} created successfully." debate: "Debate created successfully." direct_message: "You message has been sent successfully." + poll: "Poll created successfully." + poll_booth: "Booth created successfully." proposal: "Proposal created successfully." proposal_notification: "Your message has been sent correctly." spending_proposal: "Spending proposal created successfully. You can access it from %{activity}" @@ -16,9 +18,12 @@ en: update: notice: "%{resource_name} updated successfully." debate: "Debate updated successfully." + poll: "Poll updated successfully." + poll_booth: "Booth updated successfully." proposal: "Proposal updated successfully." spending_proposal: "Investment project updated succesfully." budget_investment: "Investment project updated succesfully." destroy: spending_proposal: "Spending proposal deleted succesfully." budget_investment: "Investment project deleted succesfully." + error: "Could not delete" diff --git a/config/locales/responders.es.yml b/config/locales/responders.es.yml index 43eb00669..166809af9 100644 --- a/config/locales/responders.es.yml +++ b/config/locales/responders.es.yml @@ -6,6 +6,8 @@ es: notice: "%{resource_name} creado correctamente." debate: "Debate creado correctamente." direct_message: "Tu mensaje ha sido enviado correctamente." + poll: "Votación creada correctamente." + poll_booth: "Urna creada correctamente." proposal: "Propuesta creada correctamente." proposal_notification: "Tu message ha sido enviado correctamente." spending_proposal: "Propuesta de inversión creada correctamente. Puedes acceder a ella desde %{activity}" @@ -17,8 +19,11 @@ es: notice: "%{resource_name} actualizado correctamente." debate: "Debate actualizado correctamente." proposal: "Propuesta actualizada correctamente." + poll: "Votación actualizada correctamente." + poll_booth: "Urna actualizada correctamente." spending_proposal: "Propuesta de inversión actualizada correctamente." budget_investment: "Propuesta de inversión actualizada correctamente" destroy: spending_proposal: "Propuesta de inversión eliminada." budget_investment: "Propuesta de inversión eliminada." + error: "No se pudo borrar" diff --git a/config/locales/settings.en.yml b/config/locales/settings.en.yml index 811f10557..d96b9db27 100755 --- a/config/locales/settings.en.yml +++ b/config/locales/settings.en.yml @@ -13,12 +13,16 @@ en: votes_for_proposal_success: "Number of votes necessary for approval of a Proposal" months_to_archive_proposals: "Months to archive Proposals" email_domain_for_officials: "Email domain for public officials" - per_page_code: "Code to be included on every page" + per_page_code_head: "Code to be included on every page ()" + per_page_code_body: "Code to be included on every page ()" twitter_handle: "Twitter handle" twitter_hashtag: "Twitter hashtag" facebook_handle: "Facebook handle" youtube_handle: "Youtube handle" + telegram_handle: "Telegram handle" blog_url: "Blog URL" + transparency_url: "Transparency URL" + opendata_url: "Open Data URL" url: "Main URL" org_name: "Organization" place_name: "Place" @@ -28,6 +32,7 @@ en: facebook_login: Facebook login google_login: Google login debates: Debates + polls: Polls signature_sheets: Signature sheets spending_proposals: Investment projects spending_proposal_features: diff --git a/config/locales/settings.es.yml b/config/locales/settings.es.yml index b4575432e..97ab56dc9 100644 --- a/config/locales/settings.es.yml +++ b/config/locales/settings.es.yml @@ -13,12 +13,16 @@ es: votes_for_proposal_success: "Número de votos necesarios para aprobar una Propuesta" months_to_archive_proposals: "Meses para archivar las Propuestas" email_domain_for_officials: "Dominio de email para cargos públicos" - per_page_code: "Código a incluir en cada página" + per_page_code_head: "Código a incluir en cada página ()" + per_page_code_body: "Código a incluir en cada página ()" twitter_handle: "Usuario de Twitter" twitter_hashtag: "Hashtag para Twitter" facebook_handle: "Identificador de Facebook" youtube_handle: "Usuario de Youtube" + telegram_handle: "Usuario de Telegram" blog_url: "URL del blog" + transparency_url: "URL de transparencia" + opendata_url: "URL de open data" url: "URL general de la web" org_name: "Nombre de la organización" place_name: "Nombre del lugar" @@ -28,6 +32,7 @@ es: facebook_login: Registro con Facebook google_login: Registro con Google debates: Debates + polls: Votaciones signature_sheets: Hojas de firmas spending_proposals: Propuestas de inversión spending_proposal_features: diff --git a/config/locales/social_share_button.en.yml b/config/locales/social_share_button.en.yml index b06cb5ad3..2715c46ab 100644 --- a/config/locales/social_share_button.en.yml +++ b/config/locales/social_share_button.en.yml @@ -17,3 +17,4 @@ en: plurk: "Plurk" pinterest: "Pinterest" email: "Email" + telegram: "Telegram" diff --git a/config/locales/social_share_button.es.yml b/config/locales/social_share_button.es.yml index 45e4ab2a5..1c49ef4f5 100644 --- a/config/locales/social_share_button.es.yml +++ b/config/locales/social_share_button.es.yml @@ -17,3 +17,4 @@ es: plurk: "Plurk" pinterest: "Pinterest" email: "Correo electrónico" + telegram: "Telegram" diff --git a/config/locales/valuation.en.yml b/config/locales/valuation.en.yml index de19fea09..ecfbc113f 100644 --- a/config/locales/valuation.en.yml +++ b/config/locales/valuation.en.yml @@ -1,6 +1,8 @@ --- en: valuation: + header: + title: Valuation menu: title: Valuation budgets: Participatory budgets @@ -59,7 +61,7 @@ en: edit: dossier: Dossier price_html: "Price (%{currency})" - price_first_year_html: "Cost during the first year (%{currency})" + price_first_year_html: "Cost during the first year (%{currency}) (optional, data not public)" price_explanation_html: Price explanation feasibility: Feasibility feasible: Feasible diff --git a/config/locales/valuation.es.yml b/config/locales/valuation.es.yml index b588c8521..b3a5c32ca 100644 --- a/config/locales/valuation.es.yml +++ b/config/locales/valuation.es.yml @@ -1,6 +1,8 @@ --- es: valuation: + header: + title: Evaluación menu: title: Evaluación budgets: Presupuestos participativos @@ -59,7 +61,7 @@ es: edit: dossier: Informe price_html: "Coste (%{currency}) (dato público)" - price_first_year_html: "Coste en el primer año (%{currency}) (opcional, privado)" + price_first_year_html: "Coste en el primer año (%{currency}) (opcional, dato no público)" price_explanation_html: "Informe de coste (opcional, dato público)" feasibility: Viabilidad feasible: Viable diff --git a/config/routes.rb b/config/routes.rb index 35db84630..cd2e82aaf 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -61,8 +61,6 @@ Rails.application.routes.draw do end end - resources :proposal_ballots, only: [:index] - resources :comments, only: [:create, :show], shallow: true do member do post :vote @@ -95,6 +93,12 @@ Rails.application.routes.draw do get :search, on: :collection end + resources :polls, only: [:show, :index] do + resources :questions, only: [:show], controller: 'polls/questions', shallow: true do + post :answer, on: :member + end + end + resources :users, only: [:show] do resources :direct_messages, only: [:new, :create, :show] end @@ -202,6 +206,33 @@ Rails.application.routes.draw do get :search, on: :collection end + scope module: :poll do + resources :polls do + get :search_questions, on: :member + patch :add_question, on: :member + patch :remove_question, on: :member + + resources :booth_assignments, only: [:index, :show, :create, :destroy] do + get :search_booths, on: :collection + end + + resources :officer_assignments, only: [:index, :create, :destroy] do + get :search_officers, on: :collection + get :by_officer, on: :collection + end + + resources :recounts, only: :index + resources :results, only: :index + end + + resources :officers do + get :search, on: :collection + end + + resources :booths + resources :questions + end + resources :verifications, controller: :verifications, only: :index do get :search, on: :collection end @@ -217,6 +248,12 @@ Rails.application.routes.draw do end resources :geozones, only: [:index, :new, :create, :edit, :update, :destroy] + + namespace :site_customization do + resources :pages, except: [:show] + resources :images, only: [:index, :update, :destroy] + resources :content_blocks, except: [:show] + end end namespace :moderation do @@ -298,13 +335,26 @@ Rails.application.routes.draw do get :support_investments get :print_investments end - resources :investments, only: [:index, :new, :create, :show], controller: 'budgets/investments' do + resources :investments, only: [:index, :new, :create, :show, :destroy], controller: 'budgets/investments' do post :vote, on: :member get :print, on: :collection end end end + namespace :officing do + resources :polls, only: [:index] do + get :final, on: :collection + + resources :recounts, only: [:new, :create] + resources :final_recounts, only: [:new, :create] + resources :results, only: [:new, :create, :index] + end + resource :residence, controller: "residence", only: [:new, :create] + resources :voters, only: [:new, :create] + root to: "dashboard#index" + end + # GraphQL get '/graphql', to: 'graphql#query' post '/graphql', to: 'graphql#query' @@ -316,6 +366,13 @@ Rails.application.routes.draw do mount Tolk::Engine => '/translate', :as => 'tolk' + # more info pages + get 'more-information', to: 'pages#show', id: 'more_info/index', as: 'more_info' + get 'more-information/how-to-use', to: 'pages#show', id: 'more_info/how_to_use/index', as: 'how_to_use' + get 'more-information/faq', to: 'pages#show', id: 'more_info/faq/index', as: 'faq' + get 'more-information/participation/facts', to: 'pages#show', id: 'more_info/participation/facts', as: 'participation_facts' + get 'more-information/participation/world', to: 'pages#show', id: 'more_info/participation/world', as: 'participation_world' + # static pages get '/blog' => redirect("http://blog.consul/") resources :pages, path: '/', only: [:show] diff --git a/config/sitemap.rb b/config/sitemap.rb index 11ec9fb09..76ba7cec9 100644 --- a/config/sitemap.rb +++ b/config/sitemap.rb @@ -9,13 +9,20 @@ SitemapGenerator::Sitemap.default_host = Setting["url"] # sitemap generator SitemapGenerator::Sitemap.create do - pages = Dir.entries(File.join(Rails.root,"app","views","pages")) + pages = ["accessibility", + "census_terms", + "conditions", + "general_terms", + "privacy"] pages.each do |page| - page_name = page.split(".").first - add page_name if page_name.present? + add page_path(id: page) end - add "help_translate" + add more_info_path + add how_to_use_path + add faq_path + add participation_facts_path + add participation_world_path add debates_path, priority: 0.7, changefreq: "daily" Debate.find_each do |debate| @@ -27,10 +34,18 @@ SitemapGenerator::Sitemap.create do add proposal_path(proposal), lastmod: proposal.updated_at end - add proposal_ballots_path - add spending_proposals_path, priority: 0.7, changefreq: "daily" SpendingProposal.find_each do |spending_proposal| add spending_proposal_path(spending_proposal), lastmod: spending_proposal.updated_at end + + add budgets_path, priority: 0.7, changefreq: "daily" + Budget.find_each do |budget| + add budget_path(budget), lastmod: budget.updated_at + end + + add polls_path, priority: 0.7, changefreq: "daily" + Poll.find_each do |poll| + add poll_path(poll), lastmod: poll.starts_at + end end diff --git a/db/dev_seeds.rb b/db/dev_seeds.rb index 96b31e4cb..59e0d90d6 100644 --- a/db/dev_seeds.rb +++ b/db/dev_seeds.rb @@ -2,7 +2,7 @@ require 'database_cleaner' DatabaseCleaner.clean_with :truncation -puts "Creating Settings" +print "Creating Settings" Setting.create(key: 'official_level_1_name', value: 'Empleados públicos') Setting.create(key: 'official_level_2_name', value: 'Organización Municipal') Setting.create(key: 'official_level_3_name', value: 'Directores generales') @@ -20,11 +20,13 @@ Setting.create(key: 'twitter_handle', value: '@consul_dev') Setting.create(key: 'twitter_hashtag', value: '#consul_dev') Setting.create(key: 'facebook_handle', value: 'consul') Setting.create(key: 'youtube_handle', value: 'consul') +Setting.create(key: 'telegram_handle', value: 'consul') Setting.create(key: 'blog_url', value: '/blog') Setting.create(key: 'url', value: 'http://localhost:3000') Setting.create(key: 'org_name', value: 'Consul') Setting.create(key: 'place_name', value: 'City') Setting.create(key: 'feature.debates', value: "true") +Setting.create(key: 'feature.polls', value: "true") Setting.create(key: 'feature.spending_proposals', value: nil) Setting.create(key: 'feature.spending_proposal_features.voting_allowed', value: nil) Setting.create(key: 'feature.budgets', value: "true") @@ -32,7 +34,8 @@ Setting.create(key: 'feature.twitter_login', value: "true") Setting.create(key: 'feature.facebook_login', value: "true") Setting.create(key: 'feature.google_login', value: "true") Setting.create(key: 'feature.signature_sheets', value: "true") -Setting.create(key: 'per_page_code', value: "") +Setting.create(key: 'per_page_code_head', value: "") +Setting.create(key: 'per_page_code_body', value: "") Setting.create(key: 'comments_body_max_length', value: '1000') Setting.create(key: 'mailer_from_name', value: 'Consul') Setting.create(key: 'mailer_from_address', value: 'noreply@consul.dev') @@ -41,18 +44,23 @@ Setting.create(key: 'meta_keywords', value: 'citizen participation, open governm Setting.create(key: 'verification_offices_url', value: 'http://oficinas-atencion-ciudadano.url/') Setting.create(key: 'min_age_to_participate', value: '16') -puts "Creating Geozones" +puts " ✅" +print "Creating Geozones" + +Geozone.create(name: "city") +Geozone.create(name: "Existent District", census_code: "01") ('A'..'Z').each { |i| Geozone.create(name: "District #{i}", external_code: i.ord, census_code: i.ord) } -puts "Creating Users" +puts " ✅" +print "Creating Users" def create_user(email, username = Faker::Name.name) - puts " #{username}" + pwd = '12345678' User.create!( username: username, email: email, - password: '12345678', - password_confirmation: '12345678', + password: pwd, + password_confirmation: pwd, confirmed_at: Time.current, terms_of_service: "1", gender: ['Male', 'Female'].sample, @@ -74,6 +82,9 @@ manager.create_manager valuator = create_user('valuator@consul.dev', 'valuator') valuator.create_valuator +poll_officer = create_user('poll_officer@consul.dev', 'Paul O. Fisher') +poll_officer.create_poll_officer + level_2 = create_user('leveltwo@consul.dev', 'level 2') level_2.update(residence_verified_at: Time.current, confirmed_phone: Faker::PhoneNumber.phone_number, document_number: "2222222222", document_type: "1" ) @@ -113,7 +124,8 @@ end org_user_ids = User.organizations.pluck(:id) not_org_users = User.where(['users.id NOT IN(?)', org_user_ids]) -puts "Creating Tags Categories" +puts " ✅" +print "Creating Tags Categories" ActsAsTaggableOn::Tag.create!(name: "Asociaciones", featured: true, kind: "category") ActsAsTaggableOn::Tag.create!(name: "Cultura", featured: true, kind: "category") @@ -131,7 +143,8 @@ ActsAsTaggableOn::Tag.create!(name: "Transparencia", featured: true, kind: "cat ActsAsTaggableOn::Tag.create!(name: "Seguridad y Emergencias", featured: true, kind: "category") ActsAsTaggableOn::Tag.create!(name: "Medio Ambiente", featured: true, kind: "category") -puts "Creating Debates" +puts " ✅" +print "Creating Debates" tags = Faker::Lorem.words(25) (1..30).each do @@ -144,7 +157,6 @@ tags = Faker::Lorem.words(25) tag_list: tags.sample(3).join(','), geozone: Geozone.reorder("RANDOM()").first, terms_of_service: "1") - puts " #{debate.title}" end @@ -159,11 +171,11 @@ tags = ActsAsTaggableOn::Tag.where(kind: 'category') tag_list: tags.sample(3).join(','), geozone: Geozone.reorder("RANDOM()").first, terms_of_service: "1") - puts " #{debate.title}" end -puts "Creating Proposals" +puts " ✅" +print "Creating Proposals" tags = Faker::Lorem.words(25) (1..30).each do |i| @@ -180,13 +192,33 @@ tags = Faker::Lorem.words(25) tag_list: tags.sample(3).join(','), geozone: Geozone.reorder("RANDOM()").first, terms_of_service: "1") - puts " #{proposal.title}" end -puts "Creating Archived Proposals" +puts " ✅" +print "Creating Archived Proposals" tags = Faker::Lorem.words(25) (1..5).each do + author = User.reorder("RANDOM()").first + description = "

    #{Faker::Lorem.paragraphs.join('

    ')}

    " + proposal = Proposal.create!(author: author, + title: Faker::Lorem.sentence(3).truncate(60), + question: Faker::Lorem.sentence(3) + "?", + summary: Faker::Lorem.sentence(3), + responsible_name: Faker::Name.name, + external_url: Faker::Internet.url, + description: description, + tag_list: tags.sample(3).join(','), + geozone: Geozone.reorder("RANDOM()").first, + terms_of_service: "1", + created_at: Setting["months_to_archive_proposals"].to_i.months.ago) +end + +puts " ✅" +print "Creating Successful Proposals" + +tags = Faker::Lorem.words(25) +(1..10).each do |i| author = User.reorder("RANDOM()").first description = "

    #{Faker::Lorem.paragraphs.join('

    ')}

    " proposal = Proposal.create!(author: author, @@ -200,8 +232,7 @@ tags = Faker::Lorem.words(25) tag_list: tags.sample(3).join(','), geozone: Geozone.reorder("RANDOM()").first, terms_of_service: "1", - created_at: Setting["months_to_archive_proposals"].to_i.months.ago) - puts " #{proposal.title}" + cached_votes_up: Setting["votes_for_proposal_success"]) end @@ -220,11 +251,11 @@ tags = ActsAsTaggableOn::Tag.where(kind: 'category') tag_list: tags.sample(3).join(','), geozone: Geozone.reorder("RANDOM()").first, terms_of_service: "1") - puts " #{proposal.title}" end -puts "Commenting Debates" +puts " ✅" +print "Commenting Debates" (1..100).each do author = User.reorder("RANDOM()").first @@ -236,7 +267,8 @@ puts "Commenting Debates" end -puts "Commenting Proposals" +puts " ✅" +print "Commenting Proposals" (1..100).each do |i| author = User.reorder("RANDOM()").first @@ -248,7 +280,8 @@ puts "Commenting Proposals" end -puts "Commenting Comments" +puts " ✅" +print "Commenting Comments" (1..200).each do author = User.reorder("RANDOM()").first @@ -262,7 +295,8 @@ puts "Commenting Comments" end -puts "Voting Debates, Proposals & Comments" +puts " ✅" +print "Voting Debates, Proposals & Comments" (1..100).each do voter = not_org_users.level_two_or_three_verified.reorder("RANDOM()").first @@ -285,7 +319,8 @@ end end -puts "Flagging Debates & Comments" +puts " ✅" +print "Flagging Debates & Comments" (1..40).each do debate = Debate.reorder("RANDOM()").first @@ -305,7 +340,8 @@ end Flag.flag(flagger, proposal) end -puts "Creating Spending Proposals" +puts " ✅" +print "Creating Spending Proposals" tags = Faker::Lorem.words(10) @@ -328,17 +364,18 @@ tags = Faker::Lorem.words(10) tag_list: tags.sample(3).join(','), price: rand(1000000), terms_of_service: "1") - puts " #{spending_proposal.title}" end -puts "Creating Valuation Assignments" +puts " ✅" +print "Creating Valuation Assignments" (1..17).to_a.sample.times do SpendingProposal.reorder("RANDOM()").first.valuators << valuator.valuator end -puts "Creating Budgets" +puts " ✅" +print "Creating Budgets" Budget::PHASES.each_with_index do |phase, i| descriptions = Hash[Budget::PHASES.map{ |p| ["description_#{p}", @@ -351,8 +388,6 @@ Budget::PHASES.each_with_index do |phase, i| ) ) - puts budget.name - (1..([1, 2, 3].sample)).each do group = budget.groups.create!(name: Faker::StarWars.planet) @@ -363,13 +398,12 @@ Budget::PHASES.each_with_index do |phase, i| price: rand(1 .. 100) * 100000) end - print "#{group.name} " end - puts "" end -puts "Creating Investments" +puts " ✅" +print "Creating Investments" tags = Faker::Lorem.words(10) (1..100).each do |i| heading = Budget::Heading.reorder("RANDOM()").first @@ -389,47 +423,53 @@ tags = Faker::Lorem.words(10) tag_list: tags.sample(3).join(','), price: rand(1 .. 100) * 100000, terms_of_service: "1") - puts " #{investment.title}" end -puts "Selecting Investments" -Budget.balloting.reorder("RANDOM()").limit(3).each do |budget| - budget.investments.feasible.reorder("RANDOM()").limit(10).update_all(selected: true) +puts " ✅" +print "Balloting Investments" +Budget.balloting.last.investments.each do |investment| + investment.update(selected: true, feasibility: "feasible") end -puts "Creating Valuation Assignments" +puts " ✅" +print "Creating Valuation Assignments" -(1..17).to_a.sample.times do +(1..50).to_a.sample.times do Budget::Investment.reorder("RANDOM()").first.valuators << valuator.valuator end -puts "Creating Legislation" +puts " ✅" +print "Creating Legislation" Legislation.create!(title: 'Participatory Democracy', body: 'In order to achieve...') -puts "Ignoring flags in Debates, comments & proposals" +puts " ✅" +print "Ignoring flags in Debates, comments & proposals" Debate.flagged.reorder("RANDOM()").limit(10).each(&:ignore_flag) Comment.flagged.reorder("RANDOM()").limit(30).each(&:ignore_flag) Proposal.flagged.reorder("RANDOM()").limit(10).each(&:ignore_flag) -puts "Hiding debates, comments & proposals" +puts " ✅" +print "Hiding debates, comments & proposals" Comment.with_hidden.flagged.reorder("RANDOM()").limit(30).each(&:hide) Debate.with_hidden.flagged.reorder("RANDOM()").limit(5).each(&:hide) Proposal.with_hidden.flagged.reorder("RANDOM()").limit(10).each(&:hide) -puts "Confirming hiding in debates, comments & proposals" +puts " ✅" +print "Confirming hiding in debates, comments & proposals" Comment.only_hidden.flagged.reorder("RANDOM()").limit(10).each(&:confirm_hide) Debate.only_hidden.flagged.reorder("RANDOM()").limit(5).each(&:confirm_hide) Proposal.only_hidden.flagged.reorder("RANDOM()").limit(5).each(&:confirm_hide) -puts "Creating banners" +puts " ✅" +print "Creating banners" Proposal.last(3).each do |proposal| title = Faker::Lorem.sentence(word_count = 3) @@ -444,9 +484,9 @@ Proposal.last(3).each do |proposal| post_started_at: rand((Time.current - 1.week) .. (Time.current - 1.day)), post_ended_at: rand((Time.current - 1.day) .. (Time.current + 1.week)), created_at: rand((Time.current - 1.week) .. Time.current)) - puts " #{banner.title}" end +puts " ✅" puts "Creating proposal notifications" 100.times do |i| @@ -455,3 +495,135 @@ puts "Creating proposal notifications" author: User.reorder("RANDOM()").first, proposal: Proposal.reorder("RANDOM()").first) end + +puts " ✅" +print "Creating polls" + +puts " ✅" +print "Active Polls" +(1..3).each do |i| + poll = Poll.create(name: "Active Poll #{i}", + starts_at: 1.month.ago, + ends_at: 1.month.from_now, + geozone_restricted: false) +end +(4..5).each do |i| + poll = Poll.create(name: "Active Poll #{i}", + starts_at: 1.month.ago, + ends_at: 1.month.from_now, + geozone_restricted: true, + geozones: Geozone.reorder("RANDOM()").limit(3) + ) +end + + + + + +puts " ✅" +print "Upcoming Poll" +poll = Poll.create(name: "Upcoming Poll", + starts_at: 1.month.from_now, + ends_at: 2.months.from_now) + +puts " ✅" +print "Expired Poll" +poll = Poll.create(name: "Expired Poll", + starts_at: 2.months.ago, + ends_at: 1.months.ago) + +puts " ✅" +print "Creating Poll Questions" + +(1..50).each do |i| + poll = Poll.reorder("RANDOM()").first + author = User.reorder("RANDOM()").first + description = "

    #{Faker::Lorem.paragraphs.join('

    ')}

    " + open_at = rand(2.months.ago .. 2.months.from_now) + question = Poll::Question.create!(author: author, + title: Faker::Lorem.sentence(3).truncate(60), + description: description, + valid_answers: Faker::Lorem.words((2..7).to_a.sample).join(', '), + poll: poll) +end + +puts " ✅" +print "Creating Poll Booths" +30.times.each_with_index do |i| + Poll::Booth.create(name: "Booth #{i}", polls: [Poll.all.sample]) +end + +puts " ✅" +print "Creating Booth Assignments" +Poll::Booth.all.each do |booth| + Poll::BoothAssignment.create(booth: booth, poll: Poll.all.sample) +end + +puts " ✅" +print "Creating Poll Officer Assignments" +(1..15).to_a.sample.times do |i| + Poll::BoothAssignment.all.sample(i).each do |booth_assignment| + Poll::OfficerAssignment.create(officer: poll_officer.poll_officer, + booth_assignment: booth_assignment, + date: booth_assignment.poll.starts_at) + end +end + +puts " ✅" +print "Creating Poll Recounts" do +(1..15).to_a.sample.times do |i| + poll_officer.poll_officer.officer_assignments.all.sample(i).each do |officer_assignment| + Poll::Recount.create(officer_assignment: officer_assignment, + booth_assignment: officer_assignment.booth_assignment, + date: officer_assignment.date, + count: (1..5000).to_a.sample) + end +end + +end + +puts " ✅" +print "Creating Poll Questions from Proposals" + +(1..3).each do + proposal = Proposal.reorder("RANDOM()").first + poll = Poll.current.first + question = Poll::Question.create(valid_answers: "Yes, No") + question.copy_attributes_from_proposal(proposal) + question.save! +end + +puts " ✅" +print "Creating Successful Proposals" + +(1..10).each do + proposal = Proposal.reorder("RANDOM()").first + poll = Poll.current.first + question = Poll::Question.create(valid_answers: "Yes, No") + question.copy_attributes_from_proposal(proposal) + question.save! +end + +puts " ✅" +print "Commenting Poll Questions" + +(1..30).each do + author = User.reorder("RANDOM()").first + question = Poll::Question.reorder("RANDOM()").first + Comment.create!(user: author, + created_at: rand(question.created_at .. Time.current), + commentable: question, + body: Faker::Lorem.sentence) +end + +puts " ✅" +print "Creating Poll Voters" + +(1..10).each do + poll = Poll.all.sample + user = User.level_two_verified.sample + Poll::Voter.create(poll: poll, user: user) +end + +puts " ✅" +puts "All dev seeds created successfuly 👍" diff --git a/db/migrate/20160914110004_create_polls.rb b/db/migrate/20160914110004_create_polls.rb new file mode 100644 index 000000000..9dd67d9ba --- /dev/null +++ b/db/migrate/20160914110004_create_polls.rb @@ -0,0 +1,7 @@ +class CreatePolls < ActiveRecord::Migration + def change + create_table :polls do |t| + t.string :name + end + end +end diff --git a/db/migrate/20160914110039_create_poll_officers.rb b/db/migrate/20160914110039_create_poll_officers.rb new file mode 100644 index 000000000..329220d40 --- /dev/null +++ b/db/migrate/20160914110039_create_poll_officers.rb @@ -0,0 +1,7 @@ +class CreatePollOfficers < ActiveRecord::Migration + def change + create_table :poll_officers do |t| + t.integer :user_id + end + end +end diff --git a/db/migrate/20160914172016_create_poll_voters.rb b/db/migrate/20160914172016_create_poll_voters.rb new file mode 100644 index 000000000..df5bb4585 --- /dev/null +++ b/db/migrate/20160914172016_create_poll_voters.rb @@ -0,0 +1,9 @@ +class CreatePollVoters < ActiveRecord::Migration + def change + create_table :poll_voters do |t| + t.integer :booth_id + t.string :document_number + t.string :document_type + end + end +end diff --git a/db/migrate/20160914172535_create_poll_booths.rb b/db/migrate/20160914172535_create_poll_booths.rb new file mode 100644 index 000000000..a474aba49 --- /dev/null +++ b/db/migrate/20160914172535_create_poll_booths.rb @@ -0,0 +1,8 @@ +class CreatePollBooths < ActiveRecord::Migration + def change + create_table :poll_booths do |t| + t.string :name + t.integer :poll_id + end + end +end diff --git a/db/migrate/20160926090107_add_location_to_booths.rb b/db/migrate/20160926090107_add_location_to_booths.rb new file mode 100644 index 000000000..146f55d41 --- /dev/null +++ b/db/migrate/20160926090107_add_location_to_booths.rb @@ -0,0 +1,5 @@ +class AddLocationToBooths < ActiveRecord::Migration + def change + add_column :poll_booths, :location, :string + end +end diff --git a/db/migrate/20160928113143_create_officing_booths.rb b/db/migrate/20160928113143_create_officing_booths.rb new file mode 100644 index 000000000..00483fe00 --- /dev/null +++ b/db/migrate/20160928113143_create_officing_booths.rb @@ -0,0 +1,9 @@ +class CreateOfficingBooths < ActiveRecord::Migration + def change + create_table :poll_officing_booths do |t| + t.belongs_to :officer + t.belongs_to :booth + t.timestamps null: false + end + end +end diff --git a/db/migrate/20161020112156_add_dates_to_polls.rb b/db/migrate/20161020112156_add_dates_to_polls.rb new file mode 100644 index 000000000..d504930ae --- /dev/null +++ b/db/migrate/20161020112156_add_dates_to_polls.rb @@ -0,0 +1,6 @@ +class AddDatesToPolls < ActiveRecord::Migration + def change + add_column :polls, :starts_at, :datetime + add_column :polls, :ends_at, :datetime + end +end diff --git a/db/migrate/20161028104156_create_poll_questions.rb b/db/migrate/20161028104156_create_poll_questions.rb new file mode 100644 index 000000000..13c8ac1e8 --- /dev/null +++ b/db/migrate/20161028104156_create_poll_questions.rb @@ -0,0 +1,21 @@ +class CreatePollQuestions < ActiveRecord::Migration + def change + create_table :poll_questions do |t| + t.references :proposal, index: true, foreign_key: true + t.references :poll, index: true, foreign_key: true + t.references :author, index: true # foreign key added later due to rails 4 + t.string :author_visible_name + t.string :title + t.string :question + t.string :summary + t.string :valid_answers + t.text :description + t.integer :comments_count + t.datetime :hidden_at + + t.timestamps + end + + add_foreign_key :poll_questions, :users, column: :author_id + end +end diff --git a/db/migrate/20161028143204_create_geozones_poll_questions.rb b/db/migrate/20161028143204_create_geozones_poll_questions.rb new file mode 100644 index 000000000..68077c510 --- /dev/null +++ b/db/migrate/20161028143204_create_geozones_poll_questions.rb @@ -0,0 +1,10 @@ +class CreateGeozonesPollQuestions < ActiveRecord::Migration + def change + create_table :geozones_poll_questions do |t| + t.references :geozone, index: true, foreign_key: true + t.integer :question_id, index: true + end + + add_foreign_key :geozones_poll_questions, :poll_questions, column: :question_id + end +end diff --git a/db/migrate/20161107124207_add_all_geozones_to_poll_questions.rb b/db/migrate/20161107124207_add_all_geozones_to_poll_questions.rb new file mode 100644 index 000000000..15ed41ba5 --- /dev/null +++ b/db/migrate/20161107124207_add_all_geozones_to_poll_questions.rb @@ -0,0 +1,5 @@ +class AddAllGeozonesToPollQuestions < ActiveRecord::Migration + def change + add_column :poll_questions, :all_geozones, :boolean, default: false + end +end diff --git a/db/migrate/20161107174423_create_poll_partial_result.rb b/db/migrate/20161107174423_create_poll_partial_result.rb new file mode 100644 index 000000000..a4f91a4f9 --- /dev/null +++ b/db/migrate/20161107174423_create_poll_partial_result.rb @@ -0,0 +1,14 @@ +class CreatePollPartialResult < ActiveRecord::Migration + def change + create_table :poll_partial_results do |t| + t.integer :question_id, index: true + t.integer :author_id, index: true + t.string :answer, index: true + t.integer :amount + t.string :origin, index: true + end + + add_foreign_key(:poll_partial_results, :users, column: :author_id) + add_foreign_key(:poll_partial_results, :poll_questions, column: :question_id) + end +end diff --git a/db/migrate/20161122101702_remove_question_from_poll_questions.rb b/db/migrate/20161122101702_remove_question_from_poll_questions.rb new file mode 100644 index 000000000..2532b2948 --- /dev/null +++ b/db/migrate/20161122101702_remove_question_from_poll_questions.rb @@ -0,0 +1,5 @@ +class RemoveQuestionFromPollQuestions < ActiveRecord::Migration + def change + remove_column :poll_questions, :question + end +end diff --git a/db/migrate/20161129011737_add_tsv_to_poll_questions.rb b/db/migrate/20161129011737_add_tsv_to_poll_questions.rb new file mode 100644 index 000000000..a6a44f13d --- /dev/null +++ b/db/migrate/20161129011737_add_tsv_to_poll_questions.rb @@ -0,0 +1,11 @@ +class AddTsvToPollQuestions < ActiveRecord::Migration + def up + add_column :poll_questions, :tsv, :tsvector + add_index :poll_questions, :tsv, using: "gin" + end + + def down + remove_index :poll_questions, :tsv + remove_column :poll_questions, :tsv + end +end diff --git a/db/migrate/20161130122604_remove_poll_id_from_booth.rb b/db/migrate/20161130122604_remove_poll_id_from_booth.rb new file mode 100644 index 000000000..a84b1a70a --- /dev/null +++ b/db/migrate/20161130122604_remove_poll_id_from_booth.rb @@ -0,0 +1,5 @@ +class RemovePollIdFromBooth < ActiveRecord::Migration + def change + remove_column :poll_booths, :poll_id, :integer + end +end diff --git a/db/migrate/20161130123347_create_poll_booth_assignments.rb b/db/migrate/20161130123347_create_poll_booth_assignments.rb new file mode 100644 index 000000000..36a2b9d64 --- /dev/null +++ b/db/migrate/20161130123347_create_poll_booth_assignments.rb @@ -0,0 +1,9 @@ +class CreatePollBoothAssignments < ActiveRecord::Migration + def change + create_table :poll_booth_assignments do |t| + t.integer :booth_id + t.integer :poll_id + t.timestamps null: false + end + end +end diff --git a/db/migrate/20161206130836_delete_officing_booths.rb b/db/migrate/20161206130836_delete_officing_booths.rb new file mode 100644 index 000000000..2ef1180ab --- /dev/null +++ b/db/migrate/20161206130836_delete_officing_booths.rb @@ -0,0 +1,8 @@ +class DeleteOfficingBooths < ActiveRecord::Migration + def self.up + drop_table :poll_officing_booths + end + + def self.down + end +end diff --git a/db/migrate/20161206131125_create_poll_officer_assignments.rb b/db/migrate/20161206131125_create_poll_officer_assignments.rb new file mode 100644 index 000000000..f3a40017c --- /dev/null +++ b/db/migrate/20161206131125_create_poll_officer_assignments.rb @@ -0,0 +1,9 @@ +class CreatePollOfficerAssignments < ActiveRecord::Migration + def change + create_table :poll_officer_assignments do |t| + t.integer :booth_assignment_id + t.integer :officer_id + t.timestamps null: false + end + end +end diff --git a/db/migrate/20161206132126_rename_booth_id_to_booth_assignment_id.rb b/db/migrate/20161206132126_rename_booth_id_to_booth_assignment_id.rb new file mode 100644 index 000000000..a1b59c749 --- /dev/null +++ b/db/migrate/20161206132126_rename_booth_id_to_booth_assignment_id.rb @@ -0,0 +1,10 @@ +class RenameBoothIdToBoothAssignmentId < ActiveRecord::Migration + def change + remove_column :poll_voters, :booth_id, :integer + + add_column :poll_voters, :booth_assignment_id, :integer, null: false + add_column :poll_voters, :poll_id, :integer, null: false + add_column :poll_voters, :created_at, :datetime, null: false + add_column :poll_voters, :updated_at, :datetime, null: false + end +end diff --git a/db/migrate/20161207181001_remove_poll_id_from_voter.rb b/db/migrate/20161207181001_remove_poll_id_from_voter.rb new file mode 100644 index 000000000..5a7012460 --- /dev/null +++ b/db/migrate/20161207181001_remove_poll_id_from_voter.rb @@ -0,0 +1,5 @@ +class RemovePollIdFromVoter < ActiveRecord::Migration + def change + remove_column :poll_voters, :poll_id, :integer + end +end diff --git a/db/migrate/20161227140109_add_date_to_officer_assignment.rb b/db/migrate/20161227140109_add_date_to_officer_assignment.rb new file mode 100644 index 000000000..b7c88a37b --- /dev/null +++ b/db/migrate/20161227140109_add_date_to_officer_assignment.rb @@ -0,0 +1,5 @@ +class AddDateToOfficerAssignment < ActiveRecord::Migration + def change + add_column :poll_officer_assignments, :date, :datetime + end +end diff --git a/db/migrate/20170102114446_create_poll_recounts.rb b/db/migrate/20170102114446_create_poll_recounts.rb new file mode 100644 index 000000000..9643f5b0a --- /dev/null +++ b/db/migrate/20170102114446_create_poll_recounts.rb @@ -0,0 +1,15 @@ +class CreatePollRecounts < ActiveRecord::Migration + def change + create_table :poll_recounts do |t| + t.integer :booth_assignment_id + t.integer :officer_assignment_id + t.integer :count + t.text :count_log, default: "" + + t.timestamps null: false + end + + add_index :poll_recounts, :booth_assignment_id + add_index :poll_recounts, :officer_assignment_id + end +end diff --git a/db/migrate/20170102170125_add_published_to_polls.rb b/db/migrate/20170102170125_add_published_to_polls.rb new file mode 100644 index 000000000..51da03f34 --- /dev/null +++ b/db/migrate/20170102170125_add_published_to_polls.rb @@ -0,0 +1,5 @@ +class AddPublishedToPolls < ActiveRecord::Migration + def change + add_column :polls, :published, :boolean, default: false + end +end diff --git a/db/migrate/20170103191711_add_date_and_officer_assignment_log_to_poll_recounts.rb b/db/migrate/20170103191711_add_date_and_officer_assignment_log_to_poll_recounts.rb new file mode 100644 index 000000000..e0f813500 --- /dev/null +++ b/db/migrate/20170103191711_add_date_and_officer_assignment_log_to_poll_recounts.rb @@ -0,0 +1,6 @@ +class AddDateAndOfficerAssignmentLogToPollRecounts < ActiveRecord::Migration + def change + add_column :poll_recounts, :date, :datetime + add_column :poll_recounts, :officer_assignment_id_log, :text, default: "" + end +end diff --git a/db/migrate/20170105215410_add_failed_email_digests_count_to_users.rb b/db/migrate/20170105215410_add_failed_email_digests_count_to_users.rb new file mode 100644 index 000000000..44744ed98 --- /dev/null +++ b/db/migrate/20170105215410_add_failed_email_digests_count_to_users.rb @@ -0,0 +1,5 @@ +class AddFailedEmailDigestsCountToUsers < ActiveRecord::Migration + def change + add_column :users, :failed_email_digests_count, :integer, default: 0 + end +end diff --git a/db/migrate/20170120153244_move_geozones_from_poll_questions_to_polls.rb b/db/migrate/20170120153244_move_geozones_from_poll_questions_to_polls.rb new file mode 100644 index 000000000..a2606c370 --- /dev/null +++ b/db/migrate/20170120153244_move_geozones_from_poll_questions_to_polls.rb @@ -0,0 +1,10 @@ +class MoveGeozonesFromPollQuestionsToPolls < ActiveRecord::Migration + def change + drop_table :geozones_poll_questions + + create_table :geozones_polls do |t| + t.references :geozone, index: true, foreign_key: true + t.references :poll, index: true, foreign_key: true + end + end +end diff --git a/db/migrate/20170120161058_add_geozone_restricted_to_polls.rb b/db/migrate/20170120161058_add_geozone_restricted_to_polls.rb new file mode 100644 index 000000000..d6ed3a2c7 --- /dev/null +++ b/db/migrate/20170120161058_add_geozone_restricted_to_polls.rb @@ -0,0 +1,5 @@ +class AddGeozoneRestrictedToPolls < ActiveRecord::Migration + def change + add_column :polls, :geozone_restricted, :boolean, default: false, index: true + end +end diff --git a/db/migrate/20170120164547_remove_all_geozones_from_poll_questions.rb b/db/migrate/20170120164547_remove_all_geozones_from_poll_questions.rb new file mode 100644 index 000000000..b4fcecd20 --- /dev/null +++ b/db/migrate/20170120164547_remove_all_geozones_from_poll_questions.rb @@ -0,0 +1,5 @@ +class RemoveAllGeozonesFromPollQuestions < ActiveRecord::Migration + def change + remove_column :poll_questions, :all_geozones, :boolean + end +end diff --git a/db/migrate/20170125104542_add_poll_id_and_stats_fields_to_poll_voter.rb b/db/migrate/20170125104542_add_poll_id_and_stats_fields_to_poll_voter.rb new file mode 100644 index 000000000..84e7de5d9 --- /dev/null +++ b/db/migrate/20170125104542_add_poll_id_and_stats_fields_to_poll_voter.rb @@ -0,0 +1,12 @@ +class AddPollIdAndStatsFieldsToPollVoter < ActiveRecord::Migration + def change + add_column :poll_voters, :poll_id, :integer, null: false + + remove_column :poll_voters, :booth_assignment_id, :integer, null: false + add_column :poll_voters, :booth_assignment_id, :integer + + add_column :poll_voters, :age, :integer + add_column :poll_voters, :gender, :string + add_column :poll_voters, :geozone_id, :integer + end +end diff --git a/db/migrate/20170125112017_create_poll_answers.rb b/db/migrate/20170125112017_create_poll_answers.rb new file mode 100644 index 000000000..7ea930d0c --- /dev/null +++ b/db/migrate/20170125112017_create_poll_answers.rb @@ -0,0 +1,15 @@ +class CreatePollAnswers < ActiveRecord::Migration + def change + create_table :poll_answers do |t| + t.integer :question_id + t.integer :author_id + t.string :answer + + t.timestamps + end + + add_index :poll_answers, :question_id + add_index :poll_answers, :author_id + add_index :poll_answers, [:question_id, :answer] + end +end diff --git a/db/migrate/20170125114952_add_answer_id_to_poll_voters.rb b/db/migrate/20170125114952_add_answer_id_to_poll_voters.rb new file mode 100644 index 000000000..34d5bbc54 --- /dev/null +++ b/db/migrate/20170125114952_add_answer_id_to_poll_voters.rb @@ -0,0 +1,9 @@ +class AddAnswerIdToPollVoters < ActiveRecord::Migration + def change + add_column :poll_voters, :answer_id, :integer, default: nil + + add_index :poll_voters, :document_number + add_index :poll_voters, :poll_id + add_index :poll_voters, [:poll_id, :document_number, :document_type], name: 'doc_by_poll' + end +end diff --git a/db/migrate/20170127173553_add_officer_assignment_to_votes.rb b/db/migrate/20170127173553_add_officer_assignment_to_votes.rb new file mode 100644 index 000000000..1942ef08f --- /dev/null +++ b/db/migrate/20170127173553_add_officer_assignment_to_votes.rb @@ -0,0 +1,5 @@ +class AddOfficerAssignmentToVotes < ActiveRecord::Migration + def change + add_column :poll_voters, :officer_assignment_id, :integer, default: nil + end +end diff --git a/db/migrate/20170128214244_adds_user_id_to_poll_voters.rb b/db/migrate/20170128214244_adds_user_id_to_poll_voters.rb new file mode 100644 index 000000000..edf635cfc --- /dev/null +++ b/db/migrate/20170128214244_adds_user_id_to_poll_voters.rb @@ -0,0 +1,5 @@ +class AddsUserIdToPollVoters < ActiveRecord::Migration + def change + add_reference :poll_voters, :user, index: true + end +end diff --git a/db/migrate/20170130101121_create_poll_final_recount.rb b/db/migrate/20170130101121_create_poll_final_recount.rb new file mode 100644 index 000000000..07723b708 --- /dev/null +++ b/db/migrate/20170130101121_create_poll_final_recount.rb @@ -0,0 +1,15 @@ +class CreatePollFinalRecount < ActiveRecord::Migration + def change + create_table :poll_final_recounts do |t| + t.integer :booth_assignment_id + t.integer :officer_assignment_id + t.integer :count + t.text :count_log, default: "" + t.datetime :created_at, null: false + t.datetime :updated_at, null: false + t.text :officer_assignment_id_log, default: "" + end + + add_index :poll_final_recounts, :booth_assignment_id + end +end diff --git a/db/migrate/20170130103550_add_final_to_poll_officer_assignments.rb b/db/migrate/20170130103550_add_final_to_poll_officer_assignments.rb new file mode 100644 index 000000000..4cafb5ffb --- /dev/null +++ b/db/migrate/20170130103550_add_final_to_poll_officer_assignments.rb @@ -0,0 +1,5 @@ +class AddFinalToPollOfficerAssignments < ActiveRecord::Migration + def change + add_column :poll_officer_assignments, :final, :boolean, default: false + end +end diff --git a/db/migrate/20170130133736_add_date_to_final_recount.rb b/db/migrate/20170130133736_add_date_to_final_recount.rb new file mode 100644 index 000000000..14d0f564c --- /dev/null +++ b/db/migrate/20170130133736_add_date_to_final_recount.rb @@ -0,0 +1,5 @@ +class AddDateToFinalRecount < ActiveRecord::Migration + def change + add_column :poll_final_recounts, :date, :datetime, null: false + end +end diff --git a/db/migrate/20170130163030_change_datetimes_to_date_in_recounts_and_assignments.rb b/db/migrate/20170130163030_change_datetimes_to_date_in_recounts_and_assignments.rb new file mode 100644 index 000000000..2ef6d0d5a --- /dev/null +++ b/db/migrate/20170130163030_change_datetimes_to_date_in_recounts_and_assignments.rb @@ -0,0 +1,13 @@ +class ChangeDatetimesToDateInRecountsAndAssignments < ActiveRecord::Migration + def up + change_column :poll_recounts, :date, :date, null: false + change_column :poll_final_recounts, :date, :date, null: false + change_column :poll_officer_assignments, :date, :date, null: false + end + + def down + change_column :poll_recounts, :date, :datetime, null: false + change_column :poll_final_recounts, :date, :datetime, null: false + change_column :poll_officer_assignments, :date, :datetime, null: false + end +end diff --git a/db/migrate/20170130171322_remove_summary_from_poll_question.rb b/db/migrate/20170130171322_remove_summary_from_poll_question.rb new file mode 100644 index 000000000..02c5fb68e --- /dev/null +++ b/db/migrate/20170130171322_remove_summary_from_poll_question.rb @@ -0,0 +1,5 @@ +class RemoveSummaryFromPollQuestion < ActiveRecord::Migration + def change + remove_column :poll_questions, :summary + end +end diff --git a/db/migrate/20170201113206_adds_fields_to_poll_partial_results.rb b/db/migrate/20170201113206_adds_fields_to_poll_partial_results.rb new file mode 100644 index 000000000..3755dc700 --- /dev/null +++ b/db/migrate/20170201113206_adds_fields_to_poll_partial_results.rb @@ -0,0 +1,9 @@ +class AddsFieldsToPollPartialResults < ActiveRecord::Migration + def change + add_column :poll_partial_results, :date, :date + add_column :poll_partial_results, :booth_assignment_id, :integer + add_column :poll_partial_results, :officer_assignment_id, :integer + + add_index :poll_partial_results, [:booth_assignment_id, :date] + end +end diff --git a/db/migrate/20170202151151_add_log_fields_to_poll_partial_results.rb b/db/migrate/20170202151151_add_log_fields_to_poll_partial_results.rb new file mode 100644 index 000000000..86966aed4 --- /dev/null +++ b/db/migrate/20170202151151_add_log_fields_to_poll_partial_results.rb @@ -0,0 +1,7 @@ +class AddLogFieldsToPollPartialResults < ActiveRecord::Migration + def change + add_column :poll_partial_results, :amount_log, :text, default: "" + add_column :poll_partial_results, :officer_assignment_id_log, :text, default: "" + add_column :poll_partial_results, :author_id_log, :text, default: "" + end +end diff --git a/db/migrate/20170203163304_create_poll_white_results.rb b/db/migrate/20170203163304_create_poll_white_results.rb new file mode 100644 index 000000000..2ebfc93b1 --- /dev/null +++ b/db/migrate/20170203163304_create_poll_white_results.rb @@ -0,0 +1,18 @@ +class CreatePollWhiteResults < ActiveRecord::Migration + def change + create_table :poll_white_results do |t| + t.integer :author_id + t.integer :amount + t.string :origin + t.date :date + t.integer :booth_assignment_id + t.integer :officer_assignment_id + t.text :amount_log, default: "" + t.text :officer_assignment_id_log, default: "" + t.text :author_id_log, default: "" + end + + add_index :poll_white_results, :officer_assignment_id + add_index :poll_white_results, :booth_assignment_id + end +end diff --git a/db/migrate/20170203163317_create_poll_null_results.rb b/db/migrate/20170203163317_create_poll_null_results.rb new file mode 100644 index 000000000..9c138cf65 --- /dev/null +++ b/db/migrate/20170203163317_create_poll_null_results.rb @@ -0,0 +1,18 @@ +class CreatePollNullResults < ActiveRecord::Migration + def change + create_table :poll_null_results do |t| + t.integer :author_id + t.integer :amount + t.string :origin + t.date :date + t.integer :booth_assignment_id + t.integer :officer_assignment_id + t.text :amount_log, default: "" + t.text :officer_assignment_id_log, default: "" + t.text :author_id_log, default: "" + end + + add_index :poll_null_results, :officer_assignment_id + add_index :poll_null_results, :booth_assignment_id + end +end diff --git a/db/migrate/20170208110146_add_officer_id_to_failed_census_calls.rb b/db/migrate/20170208110146_add_officer_id_to_failed_census_calls.rb new file mode 100644 index 000000000..6ce289bec --- /dev/null +++ b/db/migrate/20170208110146_add_officer_id_to_failed_census_calls.rb @@ -0,0 +1,6 @@ +class AddOfficerIdToFailedCensusCalls < ActiveRecord::Migration + def change + add_column :failed_census_calls, :poll_officer_id, :integer, index: true + add_foreign_key :failed_census_calls, :poll_officers + end +end diff --git a/db/migrate/20170208111639_add_failed_census_calls_count_to_poll_officers.rb b/db/migrate/20170208111639_add_failed_census_calls_count_to_poll_officers.rb new file mode 100644 index 000000000..664c51749 --- /dev/null +++ b/db/migrate/20170208111639_add_failed_census_calls_count_to_poll_officers.rb @@ -0,0 +1,5 @@ +class AddFailedCensusCallsCountToPollOfficers < ActiveRecord::Migration + def change + add_column :poll_officers, :failed_census_calls_count, :integer, default: 0 + end +end diff --git a/db/migrate/20170208112814_add_year_of_birth_to_failed_census_calls.rb b/db/migrate/20170208112814_add_year_of_birth_to_failed_census_calls.rb new file mode 100644 index 000000000..a3fb5c96a --- /dev/null +++ b/db/migrate/20170208112814_add_year_of_birth_to_failed_census_calls.rb @@ -0,0 +1,5 @@ +class AddYearOfBirthToFailedCensusCalls < ActiveRecord::Migration + def change + add_column :failed_census_calls, :year_of_birth, :integer + end +end diff --git a/db/migrate/20170208114548_add_user_data_log_to_poll_officer_assignments.rb b/db/migrate/20170208114548_add_user_data_log_to_poll_officer_assignments.rb new file mode 100644 index 000000000..93c2efd8b --- /dev/null +++ b/db/migrate/20170208114548_add_user_data_log_to_poll_officer_assignments.rb @@ -0,0 +1,5 @@ +class AddUserDataLogToPollOfficerAssignments < ActiveRecord::Migration + def change + add_column :poll_officer_assignments, :user_data_log, :string, default: "" + end +end diff --git a/db/migrate/20170208160130_add_former_users_data_log_to_users.rb b/db/migrate/20170208160130_add_former_users_data_log_to_users.rb new file mode 100644 index 000000000..2cd3ca956 --- /dev/null +++ b/db/migrate/20170208160130_add_former_users_data_log_to_users.rb @@ -0,0 +1,5 @@ +class AddFormerUsersDataLogToUsers < ActiveRecord::Migration + def change + add_column :users, :former_users_data_log, :text, default: "" + end +end diff --git a/db/migrate/20170212123435_add_polls_related_indexes.rb b/db/migrate/20170212123435_add_polls_related_indexes.rb new file mode 100644 index 000000000..f1a667432 --- /dev/null +++ b/db/migrate/20170212123435_add_polls_related_indexes.rb @@ -0,0 +1,36 @@ +class AddPollsRelatedIndexes < ActiveRecord::Migration + def change + + add_index :poll_booth_assignments, :booth_id + add_index :poll_booth_assignments, :poll_id + + add_index :poll_final_recounts, :officer_assignment_id + + add_index :poll_officer_assignments, :booth_assignment_id + add_index :poll_officer_assignments, :officer_id + add_index :poll_officer_assignments, [:officer_id, :date] + + add_index :poll_officers, :user_id + + add_index :poll_voters, :booth_assignment_id + add_index :poll_voters, :officer_assignment_id + + add_index :polls, [:starts_at, :ends_at] + + + add_foreign_key :poll_answers, :poll_questions, column: :question_id + add_foreign_key :poll_booth_assignments, :polls + add_foreign_key :poll_final_recounts, :poll_booth_assignments, column: :booth_assignment_id + add_foreign_key :poll_final_recounts, :poll_officer_assignments, column: :officer_assignment_id + add_foreign_key :poll_null_results, :poll_booth_assignments, column: :booth_assignment_id + add_foreign_key :poll_null_results, :poll_officer_assignments, column: :officer_assignment_id + add_foreign_key :poll_white_results, :poll_booth_assignments, column: :booth_assignment_id + add_foreign_key :poll_white_results, :poll_officer_assignments, column: :officer_assignment_id + add_foreign_key :poll_officer_assignments, :poll_booth_assignments, column: :booth_assignment_id + add_foreign_key :poll_partial_results, :poll_booth_assignments, column: :booth_assignment_id + add_foreign_key :poll_partial_results, :poll_officer_assignments, column: :officer_assignment_id + add_foreign_key :poll_voters, :polls + add_foreign_key :poll_recounts, :poll_booth_assignments, column: :booth_assignment_id + add_foreign_key :poll_recounts, :poll_officer_assignments, column: :officer_assignment_id + end +end diff --git a/db/migrate/20170316174351_create_site_customization_pages.rb b/db/migrate/20170316174351_create_site_customization_pages.rb new file mode 100644 index 000000000..3c4fbe846 --- /dev/null +++ b/db/migrate/20170316174351_create_site_customization_pages.rb @@ -0,0 +1,15 @@ +class CreateSiteCustomizationPages < ActiveRecord::Migration + def change + create_table :site_customization_pages do |t| + t.string :slug, null: false + t.string :title, null: false + t.string :subtitle + t.text :content + t.boolean :more_info_flag + t.boolean :print_content_flag + t.string :status, default: 'draft' + + t.timestamps null: false + end + end +end diff --git a/db/migrate/20170322145702_create_site_customization_images.rb b/db/migrate/20170322145702_create_site_customization_images.rb new file mode 100644 index 000000000..4f980b566 --- /dev/null +++ b/db/migrate/20170322145702_create_site_customization_images.rb @@ -0,0 +1,11 @@ +class CreateSiteCustomizationImages < ActiveRecord::Migration + def change + create_table :site_customization_images do |t| + t.string :name, null: false + t.attachment :image + t.timestamps null: false + end + + add_index :site_customization_images, :name, unique: true + end +end diff --git a/db/migrate/20170324101716_create_site_customization_content_blocks.rb b/db/migrate/20170324101716_create_site_customization_content_blocks.rb new file mode 100644 index 000000000..d0e4c3822 --- /dev/null +++ b/db/migrate/20170324101716_create_site_customization_content_blocks.rb @@ -0,0 +1,13 @@ +class CreateSiteCustomizationContentBlocks < ActiveRecord::Migration + def change + create_table :site_customization_content_blocks do |t| + t.string :name + t.string :locale + t.text :body + + t.timestamps null: false + end + + add_index :site_customization_content_blocks, [:name, :locale], unique: true + end +end diff --git a/db/migrate/20170427145845_change_budget_name.rb b/db/migrate/20170427145845_change_budget_name.rb new file mode 100644 index 000000000..cbf5e12db --- /dev/null +++ b/db/migrate/20170427145845_change_budget_name.rb @@ -0,0 +1,9 @@ +class ChangeBudgetName < ActiveRecord::Migration + def up + change_column :budgets, :name, :string, limit: 80 + end + + def down + change_column :budgets, :name, :string + end +end diff --git a/db/migrate/20170428111355_add_ballot_line_count_to_investments.rb b/db/migrate/20170428111355_add_ballot_line_count_to_investments.rb new file mode 100644 index 000000000..a864075a8 --- /dev/null +++ b/db/migrate/20170428111355_add_ballot_line_count_to_investments.rb @@ -0,0 +1,5 @@ +class AddBallotLineCountToInvestments < ActiveRecord::Migration + def change + add_column :budget_investments, :ballot_lines_count, :integer, default: 0 + end +end diff --git a/db/migrate/20170503163330_add_uniq_index_for_ballot_lines.rb b/db/migrate/20170503163330_add_uniq_index_for_ballot_lines.rb new file mode 100644 index 000000000..dec90f329 --- /dev/null +++ b/db/migrate/20170503163330_add_uniq_index_for_ballot_lines.rb @@ -0,0 +1,5 @@ +class AddUniqIndexForBallotLines < ActiveRecord::Migration + def change + add_index :budget_ballot_lines, [:ballot_id, :investment_id], unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index b332f05d7..5d1550258 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170114154421) do +ActiveRecord::Schema.define(version: 20170503163330) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -88,6 +88,7 @@ ActiveRecord::Schema.define(version: 20170114154421) do t.integer "heading_id" end + add_index "budget_ballot_lines", ["ballot_id", "investment_id"], name: "index_budget_ballot_lines_on_ballot_id_and_investment_id", unique: true, using: :btree add_index "budget_ballot_lines", ["ballot_id"], name: "index_budget_ballot_lines_on_ballot_id", using: :btree add_index "budget_ballot_lines", ["investment_id"], name: "index_budget_ballot_lines_on_investment_id", using: :btree @@ -144,6 +145,7 @@ ActiveRecord::Schema.define(version: 20170114154421) do t.string "location" t.string "organization_name" t.datetime "unfeasible_email_sent_at" + t.integer "ballot_lines_count", default: 0 end add_index "budget_investments", ["administrator_id"], name: "index_budget_investments_on_administrator_id", using: :btree @@ -161,7 +163,7 @@ ActiveRecord::Schema.define(version: 20170114154421) do add_index "budget_valuator_assignments", ["investment_id"], name: "index_budget_valuator_assignments_on_investment_id", using: :btree create_table "budgets", force: :cascade do |t| - t.string "name", limit: 30 + t.string "name", limit: 80 t.string "currency_symbol", limit: 10 t.string "phase", limit: 40, default: "accepting" t.datetime "created_at", null: false @@ -282,6 +284,8 @@ ActiveRecord::Schema.define(version: 20170114154421) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.string "district_code" + t.integer "poll_officer_id" + t.integer "year_of_birth" end add_index "failed_census_calls", ["user_id"], name: "index_failed_census_calls_on_user_id", using: :btree @@ -307,6 +311,14 @@ ActiveRecord::Schema.define(version: 20170114154421) do t.string "census_code" end + create_table "geozones_polls", force: :cascade do |t| + t.integer "geozone_id" + t.integer "poll_id" + end + + add_index "geozones_polls", ["geozone_id"], name: "index_geozones_polls_on_geozone_id", using: :btree + add_index "geozones_polls", ["poll_id"], name: "index_geozones_polls_on_poll_id", using: :btree + create_table "identities", force: :cascade do |t| t.integer "user_id" t.string "provider" @@ -366,6 +378,184 @@ ActiveRecord::Schema.define(version: 20170114154421) do add_index "organizations", ["user_id"], name: "index_organizations_on_user_id", using: :btree + create_table "poll_answers", force: :cascade do |t| + t.integer "question_id" + t.integer "author_id" + t.string "answer" + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "poll_answers", ["author_id"], name: "index_poll_answers_on_author_id", using: :btree + add_index "poll_answers", ["question_id", "answer"], name: "index_poll_answers_on_question_id_and_answer", using: :btree + add_index "poll_answers", ["question_id"], name: "index_poll_answers_on_question_id", using: :btree + + create_table "poll_booth_assignments", force: :cascade do |t| + t.integer "booth_id" + t.integer "poll_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "poll_booth_assignments", ["booth_id"], name: "index_poll_booth_assignments_on_booth_id", using: :btree + add_index "poll_booth_assignments", ["poll_id"], name: "index_poll_booth_assignments_on_poll_id", using: :btree + + create_table "poll_booths", force: :cascade do |t| + t.string "name" + t.string "location" + end + + create_table "poll_final_recounts", force: :cascade do |t| + t.integer "booth_assignment_id" + t.integer "officer_assignment_id" + t.integer "count" + t.text "count_log", default: "" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.text "officer_assignment_id_log", default: "" + t.date "date", null: false + end + + add_index "poll_final_recounts", ["booth_assignment_id"], name: "index_poll_final_recounts_on_booth_assignment_id", using: :btree + add_index "poll_final_recounts", ["officer_assignment_id"], name: "index_poll_final_recounts_on_officer_assignment_id", using: :btree + + create_table "poll_null_results", force: :cascade do |t| + t.integer "author_id" + t.integer "amount" + t.string "origin" + t.date "date" + t.integer "booth_assignment_id" + t.integer "officer_assignment_id" + t.text "amount_log", default: "" + t.text "officer_assignment_id_log", default: "" + t.text "author_id_log", default: "" + end + + add_index "poll_null_results", ["booth_assignment_id"], name: "index_poll_null_results_on_booth_assignment_id", using: :btree + add_index "poll_null_results", ["officer_assignment_id"], name: "index_poll_null_results_on_officer_assignment_id", using: :btree + + create_table "poll_officer_assignments", force: :cascade do |t| + t.integer "booth_assignment_id" + t.integer "officer_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.date "date", null: false + t.boolean "final", default: false + t.string "user_data_log", default: "" + end + + add_index "poll_officer_assignments", ["booth_assignment_id"], name: "index_poll_officer_assignments_on_booth_assignment_id", using: :btree + add_index "poll_officer_assignments", ["officer_id", "date"], name: "index_poll_officer_assignments_on_officer_id_and_date", using: :btree + add_index "poll_officer_assignments", ["officer_id"], name: "index_poll_officer_assignments_on_officer_id", using: :btree + + create_table "poll_officers", force: :cascade do |t| + t.integer "user_id" + t.integer "failed_census_calls_count", default: 0 + end + + add_index "poll_officers", ["user_id"], name: "index_poll_officers_on_user_id", using: :btree + + create_table "poll_partial_results", force: :cascade do |t| + t.integer "question_id" + t.integer "author_id" + t.string "answer" + t.integer "amount" + t.string "origin" + t.date "date" + t.integer "booth_assignment_id" + t.integer "officer_assignment_id" + t.text "amount_log", default: "" + t.text "officer_assignment_id_log", default: "" + t.text "author_id_log", default: "" + end + + add_index "poll_partial_results", ["answer"], name: "index_poll_partial_results_on_answer", using: :btree + add_index "poll_partial_results", ["author_id"], name: "index_poll_partial_results_on_author_id", using: :btree + add_index "poll_partial_results", ["booth_assignment_id", "date"], name: "index_poll_partial_results_on_booth_assignment_id_and_date", using: :btree + add_index "poll_partial_results", ["origin"], name: "index_poll_partial_results_on_origin", using: :btree + add_index "poll_partial_results", ["question_id"], name: "index_poll_partial_results_on_question_id", using: :btree + + create_table "poll_questions", force: :cascade do |t| + t.integer "proposal_id" + t.integer "poll_id" + t.integer "author_id" + t.string "author_visible_name" + t.string "title" + t.string "valid_answers" + t.text "description" + t.integer "comments_count" + t.datetime "hidden_at" + t.datetime "created_at" + t.datetime "updated_at" + t.tsvector "tsv" + end + + add_index "poll_questions", ["author_id"], name: "index_poll_questions_on_author_id", using: :btree + add_index "poll_questions", ["poll_id"], name: "index_poll_questions_on_poll_id", using: :btree + add_index "poll_questions", ["proposal_id"], name: "index_poll_questions_on_proposal_id", using: :btree + add_index "poll_questions", ["tsv"], name: "index_poll_questions_on_tsv", using: :gin + + create_table "poll_recounts", force: :cascade do |t| + t.integer "booth_assignment_id" + t.integer "officer_assignment_id" + t.integer "count" + t.text "count_log", default: "" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.date "date", null: false + t.text "officer_assignment_id_log", default: "" + end + + add_index "poll_recounts", ["booth_assignment_id"], name: "index_poll_recounts_on_booth_assignment_id", using: :btree + add_index "poll_recounts", ["officer_assignment_id"], name: "index_poll_recounts_on_officer_assignment_id", using: :btree + + create_table "poll_voters", force: :cascade do |t| + t.string "document_number" + t.string "document_type" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "poll_id", null: false + t.integer "booth_assignment_id" + t.integer "age" + t.string "gender" + t.integer "geozone_id" + t.integer "answer_id" + t.integer "officer_assignment_id" + t.integer "user_id" + end + + add_index "poll_voters", ["booth_assignment_id"], name: "index_poll_voters_on_booth_assignment_id", using: :btree + add_index "poll_voters", ["document_number"], name: "index_poll_voters_on_document_number", using: :btree + add_index "poll_voters", ["officer_assignment_id"], name: "index_poll_voters_on_officer_assignment_id", using: :btree + add_index "poll_voters", ["poll_id", "document_number", "document_type"], name: "doc_by_poll", using: :btree + add_index "poll_voters", ["poll_id"], name: "index_poll_voters_on_poll_id", using: :btree + add_index "poll_voters", ["user_id"], name: "index_poll_voters_on_user_id", using: :btree + + create_table "poll_white_results", force: :cascade do |t| + t.integer "author_id" + t.integer "amount" + t.string "origin" + t.date "date" + t.integer "booth_assignment_id" + t.integer "officer_assignment_id" + t.text "amount_log", default: "" + t.text "officer_assignment_id_log", default: "" + t.text "author_id_log", default: "" + end + + add_index "poll_white_results", ["booth_assignment_id"], name: "index_poll_white_results_on_booth_assignment_id", using: :btree + add_index "poll_white_results", ["officer_assignment_id"], name: "index_poll_white_results_on_officer_assignment_id", using: :btree + + create_table "polls", force: :cascade do |t| + t.string "name" + t.datetime "starts_at" + t.datetime "ends_at" + t.boolean "published", default: false + t.boolean "geozone_restricted", default: false + end + + add_index "polls", ["starts_at", "ends_at"], name: "index_polls_on_starts_at_and_ends_at", using: :btree + create_table "proposal_notifications", force: :cascade do |t| t.string "title" t.text "body" @@ -439,6 +629,40 @@ ActiveRecord::Schema.define(version: 20170114154421) do t.datetime "updated_at" end + create_table "site_customization_content_blocks", force: :cascade do |t| + t.string "name" + t.string "locale" + t.text "body" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "site_customization_content_blocks", ["name", "locale"], name: "index_site_customization_content_blocks_on_name_and_locale", unique: true, using: :btree + + create_table "site_customization_images", force: :cascade do |t| + t.string "name", null: false + t.string "image_file_name" + t.string "image_content_type" + t.integer "image_file_size" + t.datetime "image_updated_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "site_customization_images", ["name"], name: "index_site_customization_images_on_name", unique: true, using: :btree + + create_table "site_customization_pages", force: :cascade do |t| + t.string "slug", null: false + t.string "title", null: false + t.string "subtitle" + t.text "content" + t.boolean "more_info_flag" + t.boolean "print_content_flag" + t.string "status", default: "draft" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "spending_proposals", force: :cascade do |t| t.string "title" t.text "description" @@ -580,6 +804,8 @@ ActiveRecord::Schema.define(version: 20170114154421) do t.boolean "official_position_badge", default: false t.datetime "password_changed_at", default: '2016-12-21 17:55:08', null: false t.boolean "created_from_signature", default: false + t.integer "failed_email_digests_count", default: 0 + t.text "former_users_data_log", default: "" end add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree @@ -670,14 +896,36 @@ ActiveRecord::Schema.define(version: 20170114154421) do add_foreign_key "administrators", "users" add_foreign_key "annotations", "legislations" add_foreign_key "annotations", "users" + add_foreign_key "failed_census_calls", "poll_officers" add_foreign_key "failed_census_calls", "users" add_foreign_key "flags", "users" + add_foreign_key "geozones_polls", "geozones" + add_foreign_key "geozones_polls", "polls" add_foreign_key "identities", "users" add_foreign_key "locks", "users" add_foreign_key "managers", "users" add_foreign_key "moderators", "users" add_foreign_key "notifications", "users" add_foreign_key "organizations", "users" + add_foreign_key "poll_answers", "poll_questions", column: "question_id" + add_foreign_key "poll_booth_assignments", "polls" + add_foreign_key "poll_final_recounts", "poll_booth_assignments", column: "booth_assignment_id" + add_foreign_key "poll_final_recounts", "poll_officer_assignments", column: "officer_assignment_id" + add_foreign_key "poll_null_results", "poll_booth_assignments", column: "booth_assignment_id" + add_foreign_key "poll_null_results", "poll_officer_assignments", column: "officer_assignment_id" + add_foreign_key "poll_officer_assignments", "poll_booth_assignments", column: "booth_assignment_id" + add_foreign_key "poll_partial_results", "poll_booth_assignments", column: "booth_assignment_id" + add_foreign_key "poll_partial_results", "poll_officer_assignments", column: "officer_assignment_id" + add_foreign_key "poll_partial_results", "poll_questions", column: "question_id" + add_foreign_key "poll_partial_results", "users", column: "author_id" + add_foreign_key "poll_questions", "polls" + add_foreign_key "poll_questions", "proposals" + add_foreign_key "poll_questions", "users", column: "author_id" + add_foreign_key "poll_recounts", "poll_booth_assignments", column: "booth_assignment_id" + add_foreign_key "poll_recounts", "poll_officer_assignments", column: "officer_assignment_id" + add_foreign_key "poll_voters", "polls" + add_foreign_key "poll_white_results", "poll_booth_assignments", column: "booth_assignment_id" + add_foreign_key "poll_white_results", "poll_officer_assignments", column: "officer_assignment_id" add_foreign_key "users", "geozones" add_foreign_key "valuators", "users" end diff --git a/db/seeds.rb b/db/seeds.rb index 41261d9d7..0f1a4f7a3 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -38,15 +38,21 @@ Setting["months_to_archive_proposals"] = 12 # Emails under the domain's subdomains will also be included Setting["email_domain_for_officials"] = '' -# Code to be included at the top (header) of every page (useful for tracking) -Setting['per_page_code'] = '' +# Code to be included at the top (inside ) of every page (useful for tracking) +Setting['per_page_code_head'] = '' + +# Code to be included at the top (inside ) of every page +Setting['per_page_code_body'] = '' # Social settings Setting["twitter_handle"] = nil Setting["twitter_hashtag"] = nil Setting["facebook_handle"] = nil Setting["youtube_handle"] = nil +Setting["telegram_handle"] = nil Setting["blog_url"] = nil +Setting["transparency_url"] = nil +Setting["opendata_url"] = "/opendata" # Public-facing URL of the app. Setting["url"] = "http://example.com" @@ -64,6 +70,7 @@ Setting["meta_keywords"] = nil # Feature flags Setting['feature.debates'] = true Setting['feature.spending_proposals'] = nil +Setting['feature.polls'] = true Setting['feature.twitter_login'] = true Setting['feature.facebook_login'] = true Setting['feature.google_login'] = true diff --git a/doc/README.md b/doc/README.md index c588b82b8..c3aa2467e 100644 --- a/doc/README.md +++ b/doc/README.md @@ -1,15 +1,23 @@ +# Índice + +* [Funcionalidades](#funcionalidades) +* [Registro de usuario](#registro-de-usuario) +* [Perfil de usuario](#perfil-de-usuario) +* [Perfiles de administrador, moderador y gestor](#perfiles-de-administrador,-moderador-y-gestor) +* [Perfiles de evaluador y presidente de mesa](#perfiles-de-evaluador-y-presidente-de-mesa) + # Funcionalidades Actualmente Consul soporta: * Registro y verificación de usuarios tanto en la misma aplicación como con distintos proveedores (Twitter, Facebook, Google). -* Distintos perfiles de usuario, tanto ciudadanos individuales como organizaciones. -* Distintos perfiles de administración, gestión y moderación. +* Distintos perfiles de usuario, tanto ciudadanos individuales como organizaciones y cargos públicos. +* Distintos perfiles de administración, gestión, evaluación y moderación. * Espacio permanente de debates y propuestas. * Comentarios anidados en debates y propuestas. * Presupuestos participativos a través de distintas fases. -# Usuario +# Registro de usuario Para registrar un usuario nuevo es posible hacerlo en la propia aplicación, dando un nombre de usuario (nombre público que aparecerá en tus publicaciones), un correo electrónico y una contraseña con la que se accederá a la web. Se deben aceptar las condiciones de uso. El usuario debe confirmar su correo electrónico para poder iniciar sesión. @@ -36,7 +44,7 @@ Para esta funcionalidad hace falta que el padrón municipal soporte la posibilid # Perfil de usuario -Dentro de su perfil cada usuario puede configurar si quiere mostrar públicamente su lista de actividades, así como las notificaciones que le enviará la aplicación a través de correo electrónico. Estas notificiaciones pueden ser: +Dentro de su perfil ("Mi cuenta" en el menú superior) cada usuario puede configurar si quiere mostrar públicamente su lista de actividades, así como las notificaciones que le enviará la aplicación a través de correo electrónico. Estas notificiaciones pueden ser: * Recibir un email cuando alguien comenta en sus propuestas o debates. * Recibir un email cuando alguien contesta a sus comentarios. @@ -44,36 +52,84 @@ Dentro de su perfil cada usuario puede configurar si quiere mostrar públicament * Recibir resumen de notificaciones sobre propuestas. * Recibir emails con mensajes privados. -# Paneles de administración, gestión y moderación +# Perfiles de administrador, moderador y gestor -Consul cuenta con tres perfiles de usuario diferenciados para hacer tareas de revisión y moderación de los contenidos. Se detallan a continuación: +Consul cuenta con tres perfiles de usuario para administrar contenidos de la web: administrador, moderador y gestor. Además tiene otros dos perfiles para gestión de procesos participativos: [evaluador y presidente de mesa](#perfiles_de_evaluador,_gestor_y_presidente_de_mesa), que se detallan más abajo. -## Administración +Los usuarios con perfil de administrador pueden asignar cualquier tipo de perfil a cualquier tipo de usuario. Sin embargo, todos los perfiles tienen que ser usuarios verificados (contrastados con el padrón municipal) para poder realizar ciertas acciones (por ejemplo, los gestores necesitan estar verificados para crear propuestas de inversión). + +## Panel Administrar ![Panel de administración](imgs/panel_administration.png?raw=true "Panel de administración") -Desde aquí puedes administrar el sistema, a través de las siguientes acciones: +Desde aquí puedes administrar el sistema, a través de los siguientes menús: -### Temas de debate +### Categorías ![categorias](imgs/icon_categories.png?raw=true "categorías") -Los temas (también llamadas tags, o etiquetas) de debate son palabras que definen los usuarios al crear debates, para catalogarlos (ej: sanidad, movilidad, arganzuela, ...). Aquí se pueden eliminar temas inapropiados, o marcarlos para ser propuestos al crear debates (cada usuario puede definir los que quiera, pero se le sugieren algunos que nos parecen útiles como catalogación por defecto; aquí se puede cambiar cuáles se sugieren). +#### Temas de debates/propuestas -### Propuestas/Debates/Comentarios ocultos +Los temas (también llamados tags, o etiquetas) son palabras que definen los usuarios al crear debates o propuestas para facilitar su catalogación (ej: sanidad, movilidad, arganzuela, ...). Desde aquí el administrador tiene las siguientes opciones: -Cuando un moderador o un administrador oculta una Propuesta/Debate/Comentario aparecerá en esta lista. De esta forma los administradores pueden revisar que se ha ocultado el elemento adecuado. -* Al pulsar Confirmar se acepta el que se haya ocultado, se considera que se ha hecho correctamente. -* Al pulsar Volver a mostrar se revierte la acción de ocultar y vuelve a ser una Propuesta/Debate/Comentario visible, en el caso de que se considere que ha sido una acción errónea el haberlo ocultado. -Para facilitar la gestión, arriba encontramos un filtro con las secciones: "pendientes" (los elementos sobre los que todavía no se ha pulsado "confirmar" o "volver a mostrar", que deberían ser revisados todavía), "confirmados" y "todos". +* Crear temas nuevos +* Eliminar temas inapropiados +* Marcar temas para que aparezcan como sugerencia al crear debates/propuestas. Cada usuario puede crear los que quiera, pero el administrador puede sugerir algunos que le parezcan útiles como catalogación por defecto. Marcando "Proponer tema al crear la propuesta" en cada tema, establece cuáles se sugieren. + + +### Contenido moderado ![contenido moderado](imgs/icon_moderated_content.png?raw=true "contenido moderado") + +#### Propuestas/Debates/Comentarios ocultos + +Cuando un administrador o moderador oculta una Propuesta/Debate/Comentario desde la web, aparecerá en esta lista. De esta forma los administradores pueden revisar los elementos que se han ocultado y subsanar posibles errores. + +* Al pulsar "Confirmar" se acepta el que se haya ocultado; se considera que se ha hecho correctamente. +* Si se considera que la ocultación ha sido errónea, al pulsar "Volver a mostrar" se revierte la acción de ocultar y vuelve a ser una Propuesta/Debate/Comentario visible en la web. + +Para facilitar la gestión, arriba encontramos un filtro con las secciones: "Pendientes" (los elementos sobre los que todavía no se ha pulsado "Confirmar" o "Volver a mostrar", que deberían ser revisados todavía), "Confirmadas" y "Todas". + +Es recomendable revisar regularmente la sección "Pendientes". -Es recomendable revisar regularmente la sección "pendientes". ### Usuarios bloqueados -Cuando un moderador o un administrador bloquea a un usuario aparecerá en esta lista. Al bloquear a un usuario, éste deja de poder utilizarlo para ninguna acción de la web. Los administradores pueden desbloquearlos pulsando el botón al lado del nombre del usuario en la lista. +Cuando un moderador o un administrador bloquea a un usuario desde la web aparecerá en esta lista. Cuando un usuario está bloqueado no puede realizar acciones en la web, y todas sus Propuestas/Debates/Comentarios dejaran de ser visibles. +* Al pulsar "Confirmar" se acepta el bloqueo; se considera que se ha hecho correctamente. +* Si se considera que el bloqueo ha sido erróneo, al pulsar "Volver a mostrar" se revierte el bloqueo y el usuario vuelve a estar activo. -### Organizaciones +### Votaciones ![votaciones](imgs/icon_polls.png?raw=true "votaciones") -En la web hay dos tipos de usuarios: individuales y organizaciones. Cualquier persona puede crear usuarios de un tipo o de otro en la propia web. Los usuarios de organizaciones pueden ser verificados por parte de los administradores, confirmando que quien gestiona el usuario efectivamente representa a esa organización. Una vez se haya realizado el proceso de verificación, por el proceso externo a la web que se haya definido para ello, se pulsa el botón "Verificar" para confimarlo; lo que hará que al lado del nombre de la organización aparezca una etiqueta señalando que es una organización verificada. +Se puede crear una votación pulsando "Crear votación" y definiendo un nombre, fecha de apertura y de cierre. Adicionalmente se puede restringir la votación a unas zonas determinadas marcando "Restringir por zonas". Las zonas disponibles se definen en el menú [Gestionar distritos](#gestionar-distritos). + +Los usuarios tienen que estar verificados para poder participar en la votación. + +Una vez que se ha creado la votación, se definen y se agregan sus componentes. Las votaciones tienen tres componentes: Preguntas ciudadanas, Presidentes de mesa y Ubicación de las urnas. + + +#### Preguntas ciudadanas + +Se puede crear una pregunta ciudadana o buscar una existente. Al crear la pregunta se puede asignar a una votación determinada. También se puede modificar la asignación a una pregunta existente pulsando el ella y seleccionando "Editar". + +Desde el apartado de Preguntas ciudadanas también se pueden asignar a una votación aquellas Propuestas ciudadanas que han superado el umbral de apoyos. Se pueden seleccionar desde la pestaña "Propuestas que han superado el umbral". + + +#### Presidentes de mesa + +Cualquier usuario registrado en la web puede convertirse en Presidente de mesa. Para asignarle ese rol se introduce su email en el campo de búsqueda y una vez encontrado se asigna con "Añadir como Presidente de mesa". Cuando los presidentes acceden a la web con su usuario ven en la parte superior una nueva sección llamada "Presidentes de mesa". + + +#### Ubicación de las urnas + +Para añadir una urna a la lista, seleccionar "Añadir urna" y a continuación completar los datos de nombre de la urna y ubicación. + + +### Presupuestos participativos ![presupuestos participativos](imgs/icon_participatory_budgeting.png?raw=true "presupuestos participativos") + +Desde esta sección se puede crear un presupuesto participativo seleccionando "Crear nuevo presupuesto" o editar uno existente. Al editar se puede cambiar la fase en la que se encuentra el proceso; este cambio se reflejará en la web. También se pueden crear grupos de partidas presupuestarias y agregar propuestas de inversión que hayan sido creadas previamente por un [gestor](#panel-gestión). + +### Perfiles ![perfiles](imgs/icon_profiles.png?raw=true "perfiles") + +#### Organizaciones + +En la web cualquier usuario se puede registrar con un perfil individual o como una organización. Los usuarios de organizaciones pueden ser verificados por parte de los administradores, confirmando que quien gestiona el usuario efectivamente representa a esa organización. Una vez se haya realizado el proceso de verificación, por el proceso externo a la web que se haya definido para ello, se pulsa el botón "Verificar" para confimarlo; lo que hará que al lado del nombre de la organización aparezca una etiqueta señalando que es una organización verificada. En caso de que el proceso de verificación haya sido negativo, se pulsa el botón "Rechazar". Para editar alguno de los datos de la organización, se pulsa el botón "Editar". @@ -81,27 +137,77 @@ Las organizaciones que no aparecen en la lista pueden ser encontradas para actua Es recomendable revisar regularmente la sección "pendientes". -### Cargos Públicos -En la web, los usuarios individuales pueden ser usuarios normales, o cargos públicos. Estos últimos se diferencian de los primeros únicamente en que al lado de sus nombres aparece una etiqueta que les identifica, y cambia ligeramente el estilo de sus comentarios. Esto permite que los usuarios les identifiquen más fácilmente. Al lado de cada usuario vemos la identificación que aparece en su etiqueta, y su nivel (la manera que internamente usa la web para diferenciar entre un tipo de cargos y otros). Pulsando el botón "Editar" al lado del usuario, se puede modificar su información. Los cargos públicos que no aparecen en la lista pueden ser encontrados para actuar sobre ellos por medio del buscador en la parte superior. +#### Cargos Públicos -### Moderadores +La condición de cargo público no se puede elegir en el registro que se hace desde la web: se asigna directamente desde esta sección. El administrador busca un usuario introduciendo su email en el campo de búsqueda y le asigna el rol de Cargo público. -Mediante el buscador de la parte superior se pueden buscar usuarios, para activarlos o desactivarlos como moderadores de la web. Los moderadores al acceder a la web con su usuario ven en la parte superior una nueva sección llamada "Moderar". +El cargo público se diferencia del usuario individual únicamente en que al lado de su nombre aparece una etiqueta que le identifica, y cambia ligeramente el estilo de sus comentarios. Esto permite que los usuarios le identifiquen más fácilmente. Al lado de cada cargo vemos la identificación que aparece en su etiqueta, y su nivel (la manera que internamente usa la web para diferenciar entre un tipo de cargos y otros). Pulsando el botón "Editar" al lado del usuario, se puede modificar su información. Los cargos públicos que no aparecen en la lista pueden ser encontrados para actuar sobre ellos por medio del buscador en la parte superior. -### Actividad de moderadores -En esta sección se va guardando todas las acciones que realizan los moderadores o los administradores respecto a la moderación: ocultar/mostrar Propuestas/Debates/Comentarios y bloquear usuarios. En la columna "Acción" comprobamos si la acción corresponde con ocultar o con volver a mostrar (restaurar) elementos o con bloquear usuarios. En las demás columnas tenemos el tipo de elemento, el contenido del elemento y el moderador o administrador que ha realizado la acción. Esta sección permite que los administradores detecten comportamientos irregulares por parte de moderadores específicos y que por lo tanto puedan corregirlos. +#### Moderadores -### Configuración Global +Cualquier usuario registrado en la web puede convertirse en moderador. Para asignarle ese rol se introduce su email en el campo de búsqueda y una vez encontrado se asigna con "Añadir como Moderador". Cuando los moderadores acceden a la web con su usuario ven en la parte superior una nueva sección llamada "Moderar". -Opciones generales de configuración del sistema. +Al seleccionar "Actividad de moderadores" aparece un listado de todas las acciones que realizan los moderadores: ocultar/mostrar Propuestas/Debates/Comentarios y bloquear usuarios. En la columna "Acción" comprobamos si la acción corresponde con ocultar o con volver a mostrar (restaurar) elementos o con bloquear usuarios. En las demás columnas tenemos el tipo de elemento, el contenido del elemento y el moderador o administrador que ha realizado la acción. + +Esta sección permite que los administradores detecten comportamientos irregulares por parte de moderadores específicos y que por lo tanto puedan corregirlos. + +#### Evaluadores + +Cualquier usuario registrado en la web puede convertirse en evaluador. Para asignarle ese rol se introduce su email en el campo de búsqueda y una vez encontrado se asigna con "Añadir como evaluador". Cuando los evaluadores acceden a la web con su usuario ven en la parte superior una nueva sección llamada "Evaluación". + +#### Gestores + +Cualquier usuario registrado en la web puede convertirse en gestor. Para asignarle ese rol se introduce su email en el campo de búsqueda y una vez encontrado se asigna con "Añadir como gestor". Cuando los gestores acceden a la web con su usuario ven en la parte superior una nueva sección llamada "Gestión". + +### Banners ![banners](imgs/icon_banners.png?raw=true "banners") + +Desde el menú "Gestionar banners" se pueden crear banners para hacer anuncios especiales que aparecerán siempre en la parte superior de la web, tanto en el apartado de debates como en el de propuestas. Para crearlo hay que seleccionar "Crear banner" e introducir sus datos y fechas de inicio y fin de publicación en formato ```dd/mm/aaa```. + +Por defecto, en la web sólo aparecerá un banner. Si existen varios banners cuyas fechas indican que deberían estar activos, sólo se visualizará aquel cuya fecha de inicio de publicación sea más antigua. + +### Personalizar sitio ![personalizar sitio](imgs/icon_customize_site.png?raw=true "personalizar sitio") + +#### Personalizar páginas + +Las páginas sirven para mostrar cualquier tipo de contenido estático relativo a los procesos de participación. Al crear o editar una página se debe introducir un _slug_ para definir el _permalink_ de esa página en cuestión. Una vez creada, podemos acceder a ella desde el listado, seleccionando "Ver página". + +#### Personalizar imágenes + +Desde este panel se definen las imágenes de los elementos corporativos de tu Consul. + +#### Personalizar bloques + +Puedes crear bloques de HTML que se incrustarán en la cabecera o el pie de tu Cónsul. + +Los bloques de la cabecera (top_links) son bloques de enlaces que deben crearse con este formato: + +``` +
  • Site 1
  • +
  • Site 2
  • +
  • Site 3
  • +``` + +Los bloques del pie (footer) pueden tener cualquier formato y se pueden utilizar para guardar huellas Javascript, contenido CSS o contenido HTML personalizado. + +### Gestionar distritos + +Desde este menú se pueden crear los distintos distritos de un municipio con su nombre, coordenadas, código externo y código del censo. + +### Hojas de firmas + +Con el fin de registrar apoyos externos a la plataforma, se pueden crear hojas de firmas de Propuestas ciudadanas o Proyectos de inversión introduciendo el ID de la propuesta en cuestión e introduciendo los números de los documentos separados por comas(,). ### Estadísticas Estadísticas generales del sistema. -## Moderación +### Configuración global + +Opciones generales de configuración del sistema. + +## Panel Moderar ![Panel de moderación](imgs/panel_moderation.png?raw=true "Panel de moderación") @@ -129,7 +235,7 @@ Es recomendable revisar regularmente la sección "pendientes". Un buscador nos permite encontrar cualquier usuario introduciendo su nombre de usuario o correo electrónico, y bloquearlo una vez encontrado. Al bloquearlo, el usuario no podrá volver a acceder a la web, y todas sus Propuestas/Debates/Comentarios serán ocultados y dejarán de ser visibles en la web. -## Gestión +## Panel Gestión ![Panel de gestión](imgs/panel_management.png?raw=true "Panel de gestión") @@ -144,3 +250,9 @@ Desde aquí puedes gestionar usuarios a través de las siguientes acciones: * Imprimir propuestas. * Imprimir propts. de inversión. * Invitaciones para usuarios. + +# Perfiles de evaluador y presidente de mesa + +## Panel Evaluación + +## Panel Presidentes de mesa diff --git a/doc/imgs/button_global_configuration.png b/doc/imgs/button_global_configuration.png new file mode 100644 index 000000000..3e71e6812 Binary files /dev/null and b/doc/imgs/button_global_configuration.png differ diff --git a/doc/imgs/button_statistics.png b/doc/imgs/button_statistics.png new file mode 100644 index 000000000..611af884f Binary files /dev/null and b/doc/imgs/button_statistics.png differ diff --git a/doc/imgs/icon_banners.png b/doc/imgs/icon_banners.png new file mode 100644 index 000000000..e778cd3dc Binary files /dev/null and b/doc/imgs/icon_banners.png differ diff --git a/doc/imgs/icon_categories.png b/doc/imgs/icon_categories.png new file mode 100644 index 000000000..0f2c9dfe5 Binary files /dev/null and b/doc/imgs/icon_categories.png differ diff --git a/doc/imgs/icon_customize_site.png b/doc/imgs/icon_customize_site.png new file mode 100644 index 000000000..5779ee9dc Binary files /dev/null and b/doc/imgs/icon_customize_site.png differ diff --git a/doc/imgs/icon_moderated_content.png b/doc/imgs/icon_moderated_content.png new file mode 100644 index 000000000..4097818a1 Binary files /dev/null and b/doc/imgs/icon_moderated_content.png differ diff --git a/doc/imgs/icon_participatory_budgeting.png b/doc/imgs/icon_participatory_budgeting.png new file mode 100644 index 000000000..7b2ac9f86 Binary files /dev/null and b/doc/imgs/icon_participatory_budgeting.png differ diff --git a/doc/imgs/icon_polls.png b/doc/imgs/icon_polls.png new file mode 100644 index 000000000..01825f673 Binary files /dev/null and b/doc/imgs/icon_polls.png differ diff --git a/doc/imgs/icon_profiles.png b/doc/imgs/icon_profiles.png new file mode 100644 index 000000000..2c57c2b45 Binary files /dev/null and b/doc/imgs/icon_profiles.png differ diff --git a/doc/imgs/panel_administration.png b/doc/imgs/panel_administration.png index cdb89caeb..294f7bef4 100644 Binary files a/doc/imgs/panel_administration.png and b/doc/imgs/panel_administration.png differ diff --git a/doc/imgs/panel_management.png b/doc/imgs/panel_management.png index c3e9c7280..f2ba569b5 100644 Binary files a/doc/imgs/panel_management.png and b/doc/imgs/panel_management.png differ diff --git a/doc/imgs/panel_moderation.png b/doc/imgs/panel_moderation.png index 11fa4f759..6c54c7c0c 100644 Binary files a/doc/imgs/panel_moderation.png and b/doc/imgs/panel_moderation.png differ diff --git a/doc/imgs/user_preverification.png b/doc/imgs/user_preverification.png index d6330637a..a6727cfa3 100644 Binary files a/doc/imgs/user_preverification.png and b/doc/imgs/user_preverification.png differ diff --git a/doc/imgs/user_registration.png b/doc/imgs/user_registration.png index 92a129308..d9f6e8932 100644 Binary files a/doc/imgs/user_registration.png and b/doc/imgs/user_registration.png differ diff --git a/doc/imgs/user_verification.png b/doc/imgs/user_verification.png index ddfaa27fd..f05cf843e 100644 Binary files a/doc/imgs/user_verification.png and b/doc/imgs/user_verification.png differ diff --git a/lib/age.rb b/lib/age.rb new file mode 100644 index 000000000..c6ae6fe18 --- /dev/null +++ b/lib/age.rb @@ -0,0 +1,7 @@ +module Age + def self.in_years(dob, now = Time.now.utc.to_date) + return nil unless dob.present? + # reference: http://stackoverflow.com/questions/819263/get-persons-age-in-ruby#comment21200772_819263 + now.year - dob.year - ((now.month > dob.month || (now.month == dob.month && now.day >= dob.day)) ? 0 : 1) + end +end diff --git a/lib/capistrano/substitute_strings.rb b/lib/capistrano/substitute_strings.rb deleted file mode 100755 index d4b244323..000000000 --- a/lib/capistrano/substitute_strings.rb +++ /dev/null @@ -1,12 +0,0 @@ -# we often want to refer to variables which -# are defined in subsequent stage files. This -# let's us use the {{var}} to represent fetch(:var) -# in strings which are only evaluated at runtime. - -def sub_strings(input_string) - output_string = input_string - input_string.scan(/{{(\w*)}}/).each do |var| - output_string.gsub!("{{#{var[0]}}}", fetch(var[0].to_sym)) - end - output_string -end diff --git a/lib/capistrano/tasks/apache.cap b/lib/capistrano/tasks/apache.cap deleted file mode 100755 index 3dfbced57..000000000 --- a/lib/capistrano/tasks/apache.cap +++ /dev/null @@ -1,30 +0,0 @@ -namespace :apache do - %w(start stop restart reload).each do |task_name| - desc "#{task } Apache" - task task_name do - on roles(:app), in: :sequence, wait: 5 do - sudo "/etc/init.d/apache2 #{task_name}" - end - end - end - - desc "Enable site virual host" - task "enable_virtual_host" do - on roles(:app) do - "cd /etc/apache2/sites-available/" - sudo "a2ensite #{fetch(:server_name)}" - end - end - - desc "Remove default Apache Virtual Host" - task "remove_default_vhost" do - on roles(:app) do - if test("[ -f /etc/apache2/sites-enabled/000-default.conf ]") - sudo "rm /etc/apache2/sites-enabled/000-default.conf" - puts "removed default Apache Virtualhost" - else - puts "No default Apache Virtualhost to remove" - end - end - end -end diff --git a/lib/capistrano/tasks/cache.cap b/lib/capistrano/tasks/cache.cap deleted file mode 100644 index ce5bf88a2..000000000 --- a/lib/capistrano/tasks/cache.cap +++ /dev/null @@ -1,12 +0,0 @@ -namespace :cache do - desc "clears the cache in the servers" - task :clear do - on roles(:app) do |role| - within release_path do - with rails_env: fetch(:rails_env) do - execute :rake, "cache:clear" - end - end - end - end -end diff --git a/lib/capistrano/tasks/check_revision.cap b/lib/capistrano/tasks/check_revision.cap deleted file mode 100755 index 12cf3390d..000000000 --- a/lib/capistrano/tasks/check_revision.cap +++ /dev/null @@ -1,14 +0,0 @@ -namespace :deploy do - desc "checks whether the currently checkout out revision matches the - remote one we're trying to deploy from" - task :check_revision do - branch = fetch(:branch) - unless `git rev-parse HEAD` == `git rev-parse origin/#{branch}` - puts "WARNING: HEAD is not the same as origin/#{branch}" - puts "Run `git push` to sync changes or make sure you've" - puts "checked out the branch: #{branch} as you can only deploy" - puts "if you've got the target branch checked out" - exit - end - end -end diff --git a/lib/capistrano/tasks/compile_assets_locally.cap b/lib/capistrano/tasks/compile_assets_locally.cap deleted file mode 100755 index a39295451..000000000 --- a/lib/capistrano/tasks/compile_assets_locally.cap +++ /dev/null @@ -1,17 +0,0 @@ -namespace :deploy do - desc "compiles assets locally then rsyncs" - task :compile_assets_locally do - run_locally do - execute "RAILS_ENV=#{fetch(:rails_env)} bundle exec rake assets:precompile" - end - on roles(:app) do |role| - run_locally do - execute"rsync -av --delete ./public/assets/ #{role.user}@#{role.hostname}:#{release_path}/public/assets/;" - end - "chmod -R 755 #{release_path}/public/assets/" - end - run_locally do - execute "rm -rf ./public/assets" - end - end -end diff --git a/lib/capistrano/tasks/logs.cap b/lib/capistrano/tasks/logs.cap deleted file mode 100755 index 7bbfac2e4..000000000 --- a/lib/capistrano/tasks/logs.cap +++ /dev/null @@ -1,14 +0,0 @@ -namespace :logs do - task :tail, :file do |t, args| - if args[:file] - on roles(:app) do - execute "tail -f #{shared_path}/log/#{args[:file]}.log" - end - else - puts "please specify a logfile e.g: 'rake logs:tail[logfile]" - puts "will tail 'shared_path/log/logfile.log'" - puts "remember if you use zsh you'll need to format it as:" - puts "rake 'logs:tail[logfile]' (single quotes)" - end - end -end diff --git a/lib/capistrano/tasks/run_tests.cap b/lib/capistrano/tasks/run_tests.cap deleted file mode 100755 index 32f26f9ce..000000000 --- a/lib/capistrano/tasks/run_tests.cap +++ /dev/null @@ -1,18 +0,0 @@ -namespace :deploy do - desc "Runs test before deploying, can't deploy unless they pass" - task :run_tests do - test_log = "log/capistrano.test.log" - tests = fetch(:tests) - tests.each do |test| - puts "--> Running tests: '#{test}', please wait ..." - unless system "bundle exec rspec #{test} > #{test_log} 2>&1" - puts "--> Tests: '#{test}' failed. Results in: #{test_log} and below:" - system "cat #{test_log}" - exit; - end - puts "--> '#{test}' passed" - end - puts "--> All tests passed" - system "rm #{test_log}" - end -end diff --git a/lib/capistrano/tasks/setup_config.cap b/lib/capistrano/tasks/setup_config.cap deleted file mode 100755 index 27ba96a12..000000000 --- a/lib/capistrano/tasks/setup_config.cap +++ /dev/null @@ -1,27 +0,0 @@ -namespace :deploy do - task :setup_config do - on roles(:app) do - # make the config dir - execute :mkdir, "-p #{shared_path}/config" - full_app_name = fetch(:full_app_name) - - # config files to be uploaded to shared/config, see the - # definition of smart_template for details of operation. - # Essentially looks for #{filename}.erb in deploy/#{full_app_name}/ - # and if it isn't there, falls back to deploy/#{shared}. Generally - # everything should be in deploy/shared with params which differ - # set in the stage files - config_files = fetch(:config_files) - config_files.each do |file| - smart_template file - end - - # symlink stuff which should be... symlinked - symlinks = fetch(:symlinks) - - symlinks.each do |symlink| - sudo "ln -nfs #{shared_path}/config/#{symlink[:source]} #{sub_strings(symlink[:link])}" - end - end - end -end diff --git a/lib/capistrano/tasks/upload_secrets.cap b/lib/capistrano/tasks/upload_secrets.cap deleted file mode 100644 index e8b75bfcf..000000000 --- a/lib/capistrano/tasks/upload_secrets.cap +++ /dev/null @@ -1,12 +0,0 @@ -namespace :deploy do - desc "rsyncs local config/secrets.yml file" - task :upload_secrets do - on roles(:app) do |role| - run_locally do - execute"rsync -av ./config/secrets.yml #{role.user}@#{role.hostname}:#{shared_path}/config/secrets.yml;" - end - "chmod -R 755 #{shared_path}/config/secrets.yml" - execute "ln -nfs #{shared_path}/config/secrets.yml #{current_path}/config/secrets.yml" - end - end -end diff --git a/lib/capistrano/template.rb b/lib/capistrano/template.rb deleted file mode 100755 index 07bd0be37..000000000 --- a/lib/capistrano/template.rb +++ /dev/null @@ -1,32 +0,0 @@ -# will first try and copy the file: -# config/deploy/#{full_app_name}/#{from}.erb -# to: -# shared/config/to -# if the original source path doesn exist then it will -# search in: -# config/deploy/shared/#{from}.erb -# this allows files which are common to all enviros to -# come from a single source while allowing specific -# ones to be over-ridden -# if the target file name is the same as the source then -# the second parameter can be left out -def smart_template(from, to=nil) - to ||= from - full_to_path = "#{shared_path}/config/#{to}" - if from_erb_path = template_file(from) - from_erb = StringIO.new(ERB.new(File.read(from_erb_path)).result(binding)) - upload! from_erb, full_to_path - info "copying: #{from_erb} to: #{full_to_path}" - else - error "error #{from} not found" - end -end - -def template_file(name) - if File.exist?((file = "config/deploy/#{fetch(:full_app_name)}/#{name}.erb")) - return file - elsif File.exist?((file = "config/deploy/shared/#{name}.erb")) - return file - end - return nil -end diff --git a/lib/census_api.rb b/lib/census_api.rb index 7f88557f2..1d945d904 100644 --- a/lib/census_api.rb +++ b/lib/census_api.rb @@ -1,3 +1,4 @@ +include DocumentParser class CensusApi def call(document_type, document_number) @@ -9,25 +10,6 @@ class CensusApi response end - def get_document_number_variants(document_type, document_number) - # Delete all non-alphanumerics - document_number = document_number.to_s.gsub(/[^0-9A-Za-z]/i, '') - variants = [] - - if is_dni?(document_type) - document_number, letter = split_letter_from(document_number) - number_variants = get_number_variants_with_leading_zeroes_from(document_number) - letter_variants = get_letter_variants(number_variants, letter) - - variants += number_variants - variants += letter_variants - else # if not a DNI, just use the document_number, with no variants - variants << document_number - end - - variants - end - class Response def initialize(body) @body = body @@ -61,6 +43,10 @@ class CensusApi end end + def name + "#{data[:datos_habitante][:item][:nombre]} #{data[:datos_habitante][:item][:apellido1]}" + end + private def data @@ -117,42 +103,4 @@ class CensusApi document_type.to_s == "1" end - def split_letter_from(document_number) - letter = document_number.last - if letter[/[A-Za-z]/] == letter - document_number = document_number[0..-2] - else - letter = nil - end - return document_number, letter - end - - # if the number has less digits than it should, pad with zeros to the left and add each variant to the list - # For example, if the initial document_number is 1234, and digits=8, the result is - # ['1234', '01234', '001234', '0001234'] - def get_number_variants_with_leading_zeroes_from(document_number, digits=8) - document_number = document_number.to_s.last(digits) # Keep only the last x digits - document_number = document_number.gsub(/^0+/, '') # Removes leading zeros - - variants = [] - variants << document_number unless document_number.blank? - while document_number.size < digits - document_number = "0#{document_number}" - variants << document_number - end - variants - end - - # Generates uppercase and lowercase variants of a series of numbers, if the letter is present - # If number_variants == ['1234', '01234'] & letter == 'A', the result is - # ['1234a', '1234A', '01234a', '01234A'] - def get_letter_variants(number_variants, letter) - variants = [] - if letter.present? then - number_variants.each do |number| - variants << number + letter.downcase << number + letter.upcase - end - end - variants - end end diff --git a/lib/document_parser.rb b/lib/document_parser.rb new file mode 100644 index 000000000..c6cd53c92 --- /dev/null +++ b/lib/document_parser.rb @@ -0,0 +1,60 @@ +module DocumentParser + + def get_document_number_variants(document_type, document_number) + # Delete all non-alphanumerics + document_number = document_number.to_s.gsub(/[^0-9A-Za-z]/i, '') + variants = [] + + if is_dni?(document_type) + document_number, letter = split_letter_from(document_number) + number_variants = get_number_variants_with_leading_zeroes_from(document_number) + letter_variants = get_letter_variants(number_variants, letter) + + variants += number_variants + variants += letter_variants + else # if not a DNI, just use the document_number, with no variants + variants << document_number + end + + variants + end + + def split_letter_from(document_number) + letter = document_number.last + if letter[/[A-Za-z]/] == letter + document_number = document_number[0..-2] + else + letter = nil + end + return document_number, letter + end + + # if the number has less digits than it should, pad with zeros to the left and add each variant to the list + # For example, if the initial document_number is 1234, and digits=8, the result is + # ['1234', '01234', '001234', '0001234'] + def get_number_variants_with_leading_zeroes_from(document_number, digits=8) + document_number = document_number.to_s.last(digits) # Keep only the last x digits + document_number = document_number.gsub(/^0+/, '') # Removes leading zeros + + variants = [] + variants << document_number unless document_number.blank? + while document_number.size < digits + document_number = "0#{document_number}" + variants << document_number + end + variants + end + + # Generates uppercase and lowercase variants of a series of numbers, if the letter is present + # If number_variants == ['1234', '01234'] & letter == 'A', the result is + # ['1234a', '1234A', '01234a', '01234A'] + def get_letter_variants(number_variants, letter) + variants = [] + if letter.present? then + number_variants.each do |number| + variants << number + letter.downcase << number + letter.upcase + end + end + variants + end +end diff --git a/lib/email_digest.rb b/lib/email_digest.rb index 2936a5db5..511d137a6 100644 --- a/lib/email_digest.rb +++ b/lib/email_digest.rb @@ -22,6 +22,7 @@ class EmailDigest def mark_as_emailed notifications.update_all(emailed_at: Time.current) + user.update(failed_email_digests_count: 0) end end diff --git a/lib/spending_proposals_importer.rb b/lib/migrate_spending_proposals_to_investments.rb similarity index 98% rename from lib/spending_proposals_importer.rb rename to lib/migrate_spending_proposals_to_investments.rb index 72a3922b8..5db10e2bd 100644 --- a/lib/spending_proposals_importer.rb +++ b/lib/migrate_spending_proposals_to_investments.rb @@ -1,4 +1,4 @@ -class SpendingProposalsImporter +class MigrateSpendingProposalsToInvestments def import(sp) budget = Budget.last || Budget.create!(name: Date.today.year.to_s, currency_symbol: "€") diff --git a/lib/tasks/cache.rake b/lib/tasks/cache.rake deleted file mode 100644 index 1d4b181d6..000000000 --- a/lib/tasks/cache.rake +++ /dev/null @@ -1,6 +0,0 @@ -namespace :cache do - desc "Clears memcached" - task clear: :environment do - Rails.cache.clear - end -end diff --git a/lib/tasks/comments.rake b/lib/tasks/comments.rake deleted file mode 100644 index 67ca1ab7d..000000000 --- a/lib/tasks/comments.rake +++ /dev/null @@ -1,16 +0,0 @@ -namespace :comments do - - desc "Recalculates all the comment counters for debates and proposals" - task count: :environment do - Debate.all.pluck(:id).each{ |id| Debate.reset_counters(id, :comments) } - Proposal.all.pluck(:id).each{ |id| Proposal.reset_counters(id, :comments) } - end - - desc "Recalculates all the comment confidence scores (used for sorting comments)" - task confidence_score: :environment do - Comment.with_hidden.find_in_batches do |comments| - comments.each(&:save) - end - end - -end \ No newline at end of file diff --git a/lib/tasks/debates.rake b/lib/tasks/debates.rake deleted file mode 100644 index 57902dc8f..000000000 --- a/lib/tasks/debates.rake +++ /dev/null @@ -1,9 +0,0 @@ -namespace :debates do - desc "Updates all debates by recalculating their hot_score" - task touch: :environment do - Debate.find_in_batches do |debates| - debates.each(&:save) - end - end - -end diff --git a/lib/tasks/emails.rake b/lib/tasks/emails.rake index ffadebf05..3afa81449 100644 --- a/lib/tasks/emails.rake +++ b/lib/tasks/emails.rake @@ -4,8 +4,13 @@ namespace :emails do task digest: :environment do User.email_digest.find_each do |user| email_digest = EmailDigest.new(user) - email_digest.deliver - email_digest.mark_as_emailed + begin + email_digest.deliver + email_digest.mark_as_emailed + rescue + user.increment_counter(:failed_email_digests_count) + user.save + end end end diff --git a/lib/tasks/proposals.rake b/lib/tasks/proposals.rake deleted file mode 100644 index 447876cc2..000000000 --- a/lib/tasks/proposals.rake +++ /dev/null @@ -1,10 +0,0 @@ -namespace :proposals do - - desc "Updates all proposals by recalculating their hot_score" - task touch: :environment do - Proposal.find_in_batches do |proposals| - proposals.each(&:save) - end - end - -end \ No newline at end of file diff --git a/lib/tasks/spending_proposals.rake b/lib/tasks/spending_proposals.rake deleted file mode 100644 index 6e11b13e7..000000000 --- a/lib/tasks/spending_proposals.rake +++ /dev/null @@ -1,22 +0,0 @@ -namespace :spending_proposals do - - desc "Sends an email to the authors of unfeasible spending proposals" - task send_unfeasible_emails: :environment do - SpendingProposal.find_each do |spending_proposal| - if spending_proposal.unfeasible_email_pending? - spending_proposal.send_unfeasible_email - puts "email sent for proposal #{spending_proposal.title}" - else - puts "this proposal is feasible: #{spending_proposal.title}" - end - end - end - - desc "Updates all spending proposals to recalculate their tsv and responsible_name columns" - task touch: :environment do - SpendingProposal.find_in_batches do |spending_proposal| - spending_proposal.each(&:save) - end - end - -end \ No newline at end of file diff --git a/lib/tasks/tags.rake b/lib/tasks/tags.rake deleted file mode 100644 index c4af963cc..000000000 --- a/lib/tasks/tags.rake +++ /dev/null @@ -1,11 +0,0 @@ -namespace :tags do - desc "Recalculates the debate and proposals counters" - task custom_count: :environment do - ActsAsTaggableOn::Tag.find_in_batches do |tasks| - tasks.each do |task| - task.recalculate_custom_counter_for('Debate') - task.recalculate_custom_counter_for('Proposal') - end - end - end -end diff --git a/lib/tasks/users.rake b/lib/tasks/users.rake deleted file mode 100644 index a45d7fb34..000000000 --- a/lib/tasks/users.rake +++ /dev/null @@ -1,87 +0,0 @@ -namespace :users do - - desc "Recalculates all the failed census calls counters for users" - task count_failed_census_calls: :environment do - User.find_each{ |user| User.reset_counters(user.id, :failed_census_calls)} - end - - desc "Assigns official level to users with the officials' email domain" - task check_for_official_emails: :environment do - domain = Setting['email_domain_for_officials'] - - # We end the task if there is no email domain configured - if !domain.blank? - # We filter the mail addresses with SQL to speed up the process - # The real check will be done by check_if_official_email, however. - User.where('official_level = 0 and email like ?', "%#{domain}").find_each do |user| - if user.has_official_email? - user.add_official_position! (Setting['official_level_1_name']), 1 - puts "#{user.username} (#{user.email}) is now a level-1 official." - end - end - end - end - - desc "Associates a geozone to each user who doesn't have it already but has validated his residence using the census API" - task assign_geozones: :environment do - User.residence_verified.where(geozone_id: nil).find_each do |u| - begin - response = CensusApi.new.call(u.document_type, u.document_number) - if response.valid? - u.geozone = Geozone.where(census_code: response.district_code).first - u.save - print "." - else - print "X" - end - rescue - puts "Could not assign geozone for user: #{u.id}" - end - end - end - - desc "Associates demographic information to users" - task assign_demographic: :environment do - User.residence_verified.where(gender: nil).find_each do |u| - begin - response = CensusApi.new.call(u.document_type, u.document_number) - if response.valid? - u.gender = response.gender - u.date_of_birth = response.date_of_birth.to_datetime - u.save - print "." - else - print "X" - end - rescue - puts "Could not assign gender/dob for user: #{u.id}" - end - end - end - - desc "Makes duplicate username users change their username" - task social_network_reset: :environment do - duplicated_usernames = User.all.select(:username).group(:username).having('count(username) > 1').pluck(:username) - - duplicated_usernames.each do |username| - print "." - user_ids = User.where(username: username).order(created_at: :asc).pluck(:id) - user_ids_to_review = Identity.where(user_id: user_ids).pluck(:user_id) - user_ids_to_review.shift if user_ids.size == user_ids_to_review.size - user_ids_to_review.each { |id| User.find(id).update(registering_with_oauth: true) } - end - end - - desc "Removes identities associated to erased users" - task remove_erased_identities: :environment do - Identity.joins(:user).where('users.erased_at IS NOT NULL').destroy_all - end - - desc "Update password changed at for existing users" - task update_password_changed_at: :environment do - User.all.each do |user| - user.update(password_changed_at: user.created_at) - end - end - -end \ No newline at end of file diff --git a/public/404.html b/public/404.html index cc6820bba..5b5362192 100644 --- a/public/404.html +++ b/public/404.html @@ -1,5 +1,5 @@ - + Error 404 | Not found diff --git a/public/422.html b/public/422.html index e68d2956f..cca90e5a3 100644 --- a/public/422.html +++ b/public/422.html @@ -1,5 +1,5 @@ - + Error 422 | The change you wanted was rejected diff --git a/public/500.html b/public/500.html index 71978cdce..0b563452a 100644 --- a/public/500.html +++ b/public/500.html @@ -1,5 +1,5 @@ - + Error 500 | Internal server error diff --git a/public/robots.txt b/public/robots.txt index 3c9c7c01f..93780f8d1 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -3,3 +3,10 @@ # To ban all spiders from the entire site uncomment the next two lines: # User-agent: * # Disallow: / + +Disallow: /users/ +Disallow: /comments/ + +Disallow: /*?*locale +Disallow: /*?*order +Disallow: /*?*search diff --git a/spec/controllers/admin/api/stats_controller_spec.rb b/spec/controllers/admin/api/stats_controller_spec.rb index e28bd3591..7ba63ffb3 100644 --- a/spec/controllers/admin/api/stats_controller_spec.rb +++ b/spec/controllers/admin/api/stats_controller_spec.rb @@ -91,5 +91,24 @@ describe Admin::Api::StatsController do expect(data).to eq "x"=>["2015-01-01", "2015-01-02"], "Foo"=>[1, 2], "Visits"=>[2, 1] end end + + context 'budget investments present' do + it 'should return budget investments formated for working with c3.js' do + time_1 = DateTime.parse("2017-04-01") + time_2 = DateTime.parse("2017-04-02") + + budget_investment1 = create(:budget_investment, budget: @budget, created_at: time_1) + budget_investment2 = create(:budget_investment, budget: @budget, created_at: time_2) + budget_investment3 = create(:budget_investment, budget: @budget, created_at: time_2) + + sign_in user + get :show, budget_investments: true + + expect(response).to be_ok + + data = JSON.parse(response.body) + expect(data).to eq "x"=>["2017-04-01", "2017-04-02"], "Budget Investments"=>[1, 2] + end + end end end diff --git a/spec/controllers/pages_controller_spec.rb b/spec/controllers/pages_controller_spec.rb index eed930a57..b72bf032e 100644 --- a/spec/controllers/pages_controller_spec.rb +++ b/spec/controllers/pages_controller_spec.rb @@ -22,38 +22,37 @@ describe PagesController do get :show, id: :census_terms expect(response).to be_ok end - end - describe 'Provisional pages' do - it 'should include a opendata page' do - get :show, id: :opendata + it 'should include a accessibility page' do + get :show, id: :accessibility expect(response).to be_ok end end - describe 'Info pages' do - it 'should include a how_it_works page' do - get :show, id: :how_it_works + describe 'More info pages' do + + it 'should include a more info page' do + get :show, id: 'more_info/index' expect(response).to be_ok end it 'should include a how_to_use page' do - get :show, id: :how_to_use + get :show, id: 'more_info/how_to_use/index' expect(response).to be_ok end - it 'should include a more_information page' do - get :show, id: :more_information + it 'should include a faq page' do + get :show, id: 'more_info/faq/index' expect(response).to be_ok end - it 'should include a participation page' do - get :show, id: :participation + it 'should include a participation facts page' do + get :show, id: 'more_info/participation/facts' expect(response).to be_ok end - it 'should include a accessibility page' do - get :show, id: :accessibility + it 'should include a participation world page' do + get :show, id: 'more_info/participation/world' expect(response).to be_ok end end diff --git a/spec/factories.rb b/spec/factories.rb index cb569d39b..ec3ab3eb0 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -23,6 +23,9 @@ FactoryGirl.define do sms_confirmation_code "1234" document_type "1" document_number + date_of_birth Date.new(1980, 12, 31) + gender "female" + geozone end trait :level_three do @@ -38,6 +41,7 @@ FactoryGirl.define do trait :with_confirmed_hide do confirmed_hide_at Time.current end + end factory :identity do @@ -182,6 +186,10 @@ FactoryGirl.define do 4.times { create(:vote, votable: debate) } end end + + trait :successful do + cached_votes_up { Proposal.votes_needed_for_success + 100 } + end end factory :spending_proposal do @@ -277,6 +285,12 @@ FactoryGirl.define do feasibility "feasible" valuation_finished true end + + trait :unselected do + selected false + feasibility "feasible" + valuation_finished true + end end factory :budget_ballot, class: 'Budget::Ballot' do @@ -360,6 +374,127 @@ FactoryGirl.define do user end + factory :poll_officer, class: 'Poll::Officer' do + user + end + + factory :poll do + sequence(:name) { |n| "Poll #{n}" } + + starts_at { 1.month.ago } + ends_at { 1.month.from_now } + + trait :incoming do + starts_at { 2.days.from_now } + ends_at { 1.month.from_now } + end + + trait :expired do + starts_at { 1.month.ago } + ends_at { 15.days.ago } + end + + trait :published do + published true + end + end + + factory :poll_question, class: 'Poll::Question' do + poll + association :author, factory: :user + sequence(:title) { |n| "Question title #{n}" } + sequence(:description) { |n| "Question description #{n}" } + valid_answers { Faker::Lorem.words(3).join(', ') } + end + + factory :poll_booth, class: 'Poll::Booth' do + sequence(:name) { |n| "Booth #{n}" } + sequence(:location) { |n| "Street #{n}" } + end + + factory :poll_booth_assignment, class: 'Poll::BoothAssignment' do + poll + association :booth, factory: :poll_booth + end + + factory :poll_officer_assignment, class: 'Poll::OfficerAssignment' do + association :officer, factory: :poll_officer + association :booth_assignment, factory: :poll_booth_assignment + date Time.current.to_date + + trait :final do + final true + end + end + + factory :poll_recount, class: 'Poll::Recount' do + association :officer_assignment, factory: :poll_officer_assignment + association :booth_assignment, factory: :poll_booth_assignment + count (1..100).to_a.sample + date (1.month.ago.to_datetime..1.month.from_now.to_datetime).to_a.sample + end + + factory :poll_final_recount, class: 'Poll::FinalRecount' do + association :officer_assignment, factory: [:poll_officer_assignment, :final] + association :booth_assignment, factory: :poll_booth_assignment + count (1..100).to_a.sample + date (1.month.ago.to_datetime..1.month.from_now.to_datetime).to_a.sample + end + + factory :poll_voter, class: 'Poll::Voter' do + poll + association :user, :level_two + + trait :from_booth do + association :booth_assignment, factory: :poll_booth_assignment + end + + trait :valid_document do + document_type "1" + document_number "12345678Z" + end + + trait :invalid_document do + document_type "1" + document_number "99999999A" + end + end + + factory :poll_answer, class: 'Poll::Answer' do + association :question, factory: :poll_question + association :author, factory: [:user, :level_two] + answer { question.valid_answers.sample } + end + + factory :poll_partial_result, class: 'Poll::PartialResult' do + association :question, factory: :poll_question + association :author, factory: :user + origin { 'web' } + answer { question.valid_answers.sample } + end + + factory :poll_white_result, class: 'Poll::WhiteResult' do + association :author, factory: :user + origin { 'web' } + end + + factory :poll_null_result, class: 'Poll::NullResult' do + association :author, factory: :user + origin { 'web' } + end + + factory :officing_residence, class: 'Officing::Residence' do + user + association :officer, factory: :poll_officer + document_number + document_type "1" + year_of_birth "1980" + + trait :invalid do + year_of_birth Time.current.year + end + end + factory :organization do user responsible_name "Johnny Utah" @@ -416,6 +551,10 @@ FactoryGirl.define do sequence(:name) { |n| "District #{n}" } sequence(:external_code) { |n| "#{n}" } sequence(:census_code) { |n| "#{n}" } + + trait :in_census do + census_code "01" + end end factory :banner do @@ -451,4 +590,28 @@ FactoryGirl.define do signature_sheet sequence(:document_number) { |n| "#{n}A" } end + + factory :site_customization_page, class: 'SiteCustomization::Page' do + slug "example-page" + title "Example page" + subtitle "About an example" + content "This page is about..." + more_info_flag false + print_content_flag false + status 'draft' + + trait :published do + status "published" + end + + trait :display_in_more_info do + more_info_flag true + end + end + + factory :site_customization_content_block, class: 'SiteCustomization::ContentBlock' do + name "top_links" + locale "en" + body "Some top links content" + end end diff --git a/spec/features/account_spec.rb b/spec/features/account_spec.rb index d1c547185..ef24226f1 100644 --- a/spec/features/account_spec.rb +++ b/spec/features/account_spec.rb @@ -112,8 +112,13 @@ feature 'Account' do end scenario 'Errors editing credentials' do - visit account_path + visit root_path + click_link 'My account' + + expect(current_path).to eq(account_path) + + expect(page).to have_link('Change my credentials') click_link 'Change my credentials' click_button 'Update' diff --git a/spec/features/admin/banners_spec.rb b/spec/features/admin/banners_spec.rb index 72814b3c3..70d9f7621 100644 --- a/spec/features/admin/banners_spec.rb +++ b/spec/features/admin/banners_spec.rb @@ -114,6 +114,7 @@ feature 'Admin banners magement' do visit admin_root_path within('#side_menu') do + click_link "Banners" click_link "Manage banners" end diff --git a/spec/features/admin/budget_investments_spec.rb b/spec/features/admin/budget_investments_spec.rb index ede09ec4f..cfbfcc8b0 100644 --- a/spec/features/admin/budget_investments_spec.rb +++ b/spec/features/admin/budget_investments_spec.rb @@ -21,9 +21,12 @@ feature 'Admin budget investments' do context "Index" do scenario 'Displaying investmentss' do - budget_investment = create(:budget_investment, budget: @budget) + budget_investment = create(:budget_investment, budget: @budget, cached_votes_up: 77) visit admin_budget_budget_investments_path(budget_id: @budget.id) expect(page).to have_content(budget_investment.title) + expect(page).to have_content(budget_investment.heading.name) + expect(page).to have_content(budget_investment.id) + expect(page).to have_content(budget_investment.total_votes) end scenario 'Displaying assignments info' do @@ -252,6 +255,21 @@ feature 'Admin budget investments' do expect(page).to have_content("More schools") end + scenario "Filtering by tag, display only valuation tags" do + investment1 = create(:budget_investment, budget: @budget, tag_list: 'Education') + investment2 = create(:budget_investment, budget: @budget, tag_list: 'Health') + + investment1.set_tag_list_on(:valuation, 'Teachers') + investment2.set_tag_list_on(:valuation, 'Hospitals') + + investment1.save + investment2.save + + visit admin_budget_budget_investments_path(budget_id: @budget.id) + + expect(page).to have_select("tag_name", options: ["All tags", "Hospitals", "Teachers"]) + end + end scenario 'Show' do diff --git a/spec/features/admin/budgets_spec.rb b/spec/features/admin/budgets_spec.rb index 3b34878af..c93aebea1 100644 --- a/spec/features/admin/budgets_spec.rb +++ b/spec/features/admin/budgets_spec.rb @@ -84,7 +84,7 @@ feature 'Admin budgets' do fill_in 'budget_name', with: 'M30 - Summer campaign' fill_in 'budget_description_accepting', with: 'Budgeting for summer 2017 maintenance and improvements of the road M-30' - select 'Accepting proposals', from: 'budget[phase]' + select 'Accepting projects', from: 'budget[phase]' click_button 'Create Participatory budget' diff --git a/spec/features/admin/feature_flags_spec.rb b/spec/features/admin/feature_flags_spec.rb index 18b92eed5..d4500d2f9 100644 --- a/spec/features/admin/feature_flags_spec.rb +++ b/spec/features/admin/feature_flags_spec.rb @@ -31,6 +31,7 @@ feature 'Admin feature flags' do visit admin_root_path within('#side_menu') do + expect(page).not_to have_link "Budgets" expect(page).not_to have_link "Spending proposals" end @@ -45,6 +46,7 @@ feature 'Admin feature flags' do visit admin_root_path within('#side_menu') do + expect(page).not_to have_link "Budgets" expect(page).not_to have_link "Spending proposals" end diff --git a/spec/features/admin/poll/booth_assigments_spec.rb b/spec/features/admin/poll/booth_assigments_spec.rb new file mode 100644 index 000000000..7a07eab82 --- /dev/null +++ b/spec/features/admin/poll/booth_assigments_spec.rb @@ -0,0 +1,181 @@ +require 'rails_helper' + +feature 'Admin booths assignments' do + + background do + admin = create(:administrator) + login_as(admin.user) + end + + scenario 'Assign booth to poll', :js do + poll = create(:poll) + booth = create(:poll_booth) + + visit admin_poll_path(poll) + within('#poll-resources') do + click_link 'Booths (0)' + end + + expect(page).to have_content 'There are no booths assigned to this poll.' + + fill_in 'search-booths', with: booth.name + click_button 'Search' + expect(page).to have_content(booth.name) + + within('#search-booths-results') do + click_link 'Assign booth' + end + + expect(page).to have_content 'Booth assigned' + + visit admin_poll_path(poll) + within('#poll-resources') do + click_link 'Booths (1)' + end + + expect(page).to_not have_content 'There are no booths assigned to this poll.' + expect(page).to have_content booth.name + end + + scenario 'Remove booth from poll', :js do + poll = create(:poll) + booth = create(:poll_booth) + assignment = create(:poll_booth_assignment, poll: poll, booth: booth) + + visit admin_poll_path(poll) + within('#poll-resources') do + click_link 'Booths (1)' + end + + expect(page).to_not have_content 'There are no booths assigned to this poll.' + expect(page).to have_content booth.name + + within("#poll_booth_assignment_#{assignment.id}") do + click_link 'Remove booth from poll' + end + + expect(page).to have_content 'Booth not assigned anymore' + + visit admin_poll_path(poll) + within('#poll-resources') do + click_link 'Booths (0)' + end + + expect(page).to have_content 'There are no booths assigned to this poll.' + expect(page).to_not have_content booth.name + end + + feature 'Show' do + scenario 'Lists all assigned poll oficers' do + poll = create(:poll) + booth = create(:poll_booth) + booth_assignment = create(:poll_booth_assignment, poll: poll, booth: booth) + officer_assignment = create(:poll_officer_assignment, booth_assignment: booth_assignment) + officer = officer_assignment.officer + + booth_assignment_2 = create(:poll_booth_assignment, poll: poll) + officer_assignment_2 = create(:poll_officer_assignment, booth_assignment: booth_assignment_2) + officer_2 = officer_assignment_2.officer + + visit admin_poll_path(poll) + click_link 'Booths (2)' + + within('#assigned_booths_list') { click_link booth.name } + + click_link 'Officers' + within('#officers_list') do + expect(page).to have_content officer.name + expect(page).to_not have_content officer_2.name + end + end + + scenario 'Lists all recounts for the booth assignment' do + poll = create(:poll, starts_at: 2.weeks.ago, ends_at: 1.week.ago) + booth = create(:poll_booth) + booth_assignment = create(:poll_booth_assignment, poll: poll, booth: booth) + officer_assignment_1 = create(:poll_officer_assignment, booth_assignment: booth_assignment, date: poll.starts_at) + officer_assignment_2 = create(:poll_officer_assignment, booth_assignment: booth_assignment, date: poll.ends_at) + final_officer_assignment = create(:poll_officer_assignment, :final, booth_assignment: booth_assignment, date: poll.ends_at) + + recount_1 = create(:poll_recount, + booth_assignment: booth_assignment, + officer_assignment: officer_assignment_1, + date: officer_assignment_1.date, + count: 33) + recount_2 = create(:poll_recount, + booth_assignment: booth_assignment, + officer_assignment: officer_assignment_2, + date: officer_assignment_2.date, + count: 78) + final_recount = create(:poll_final_recount, + booth_assignment: booth_assignment, + officer_assignment: final_officer_assignment, + date: final_officer_assignment.date, + count: 5678) + + booth_assignment_2 = create(:poll_booth_assignment, poll: poll) + other_recount = create(:poll_recount, booth_assignment: booth_assignment_2, count: 100) + + visit admin_poll_path(poll) + click_link 'Booths (2)' + + within('#assigned_booths_list') { click_link booth.name } + + click_link 'Recounts' + within('#recounts_list') do + expect(page).to_not have_content other_recount.count + + within("#recounting_#{recount_1.date.strftime('%Y%m%d')}") do + expect(page).to have_content recount_1.count + end + + within("#recounting_#{recount_2.date.strftime('%Y%m%d')}") do + expect(page).to have_content recount_2.count + end + + within("#recounting_#{final_recount.date.strftime('%Y%m%d')}") do + expect(page).to have_content final_recount.count + end + + end + end + + scenario 'Marks recount values with count-errors' do + poll = create(:poll) + booth = create(:poll_booth) + booth_assignment = create(:poll_booth_assignment, poll: poll, booth: booth) + today = Time.current.to_date + officer_assignment = create(:poll_officer_assignment, booth_assignment: booth_assignment, date: today) + + recount = create(:poll_recount, + booth_assignment: booth_assignment, + officer_assignment: officer_assignment, + date: officer_assignment.date, + count: 1) + + visit admin_poll_booth_assignment_path(poll, booth_assignment) + click_link 'Recounts' + + within('#recounts_list') do + expect(page).to have_css("#recounting_#{recount.date.strftime('%Y%m%d')} td.count-error") + within("#recounting_#{recount.date.strftime('%Y%m%d')}") do + expect(page).to have_content recount.count + expect(page).to have_content 0 + end + end + + create(:poll_voter, :valid_document, poll: poll, booth_assignment: booth_assignment) + + visit admin_poll_booth_assignment_path(poll, booth_assignment) + click_link 'Recounts' + + within('#recounts_list') do + expect(page).to_not have_css('.count-error') + within("#recounting_#{recount.date.strftime('%Y%m%d')}") do + expect(page).to have_content(recount.count) + end + end + end + + end +end diff --git a/spec/features/admin/poll/booths_spec.rb b/spec/features/admin/poll/booths_spec.rb new file mode 100644 index 000000000..ec8c10d58 --- /dev/null +++ b/spec/features/admin/poll/booths_spec.rb @@ -0,0 +1,86 @@ +require 'rails_helper' + +feature 'Admin booths' do + + background do + admin = create(:administrator) + login_as(admin.user) + end + + scenario 'Index empty' do + visit admin_root_path + + within('#side_menu') do + click_link "Booths location" + end + + expect(page).to have_content "There are no booths" + end + + scenario 'Index' do + 3.times { create(:poll_booth) } + + visit admin_root_path + + within('#side_menu') do + click_link "Booths location" + end + + booths = Poll::Booth.all + booths.each do |booth| + within("#booth_#{booth.id}") do + expect(page).to have_content booth.name + expect(page).to have_content booth.location + end + end + expect(page).to_not have_content "There are no booths" + end + + scenario 'Show' do + booth = create(:poll_booth) + + visit admin_booths_path + + expect(page).to have_content booth.name + expect(page).to have_content booth.location + end + + scenario "Create" do + visit admin_booths_path + click_link "Add booth" + + fill_in "poll_booth_name", with: "Upcoming booth" + fill_in "poll_booth_location", with: "39th Street, number 2, ground floor" + click_button "Create booth" + + expect(page).to have_content "Booth created successfully" + + visit admin_booths_path + expect(page).to have_content "Upcoming booth" + expect(page).to have_content "39th Street, number 2, ground floor" + end + + scenario "Edit" do + booth = create(:poll_booth) + + visit admin_booths_path + + within("#booth_#{booth.id}") do + click_link "Edit" + end + + fill_in "poll_booth_name", with: "Next booth" + fill_in "poll_booth_location", with: "40th Street, number 1, firts floor" + click_button "Update booth" + + expect(page).to have_content "Booth updated successfully" + + visit admin_booths_path + + within("#booth_#{booth.id}") do + expect(page).to have_content "Next booth" + expect(page).to have_content "40th Street, number 1, firts floor" + end + end + +end \ No newline at end of file diff --git a/spec/features/admin/poll/officer_assignments_spec.rb b/spec/features/admin/poll/officer_assignments_spec.rb new file mode 100644 index 000000000..6cbe43d14 --- /dev/null +++ b/spec/features/admin/poll/officer_assignments_spec.rb @@ -0,0 +1,97 @@ +require 'rails_helper' + +feature 'Admin officer assignments in poll' do + + background do + admin = create(:administrator) + login_as(admin.user) + end + + scenario 'Assign officer to poll', :js do + booth_assignment = create(:poll_booth_assignment) + officer = create(:poll_officer) + + visit admin_poll_path(booth_assignment.poll) + within('#poll-resources') do + click_link 'Officers (0)' + end + + expect(page).to have_content 'There are no officers assigned to this poll' + + fill_in 'search-officers', with: officer.name + click_button 'Search' + + within('#search-officers-results') do + click_link 'Add shifts as officer' + end + + expect(page).to have_content 'This user has no officing shifts in this poll' + expect(page).to have_content officer.name + expect(page).to have_content booth_assignment.poll.name + + within('#officer_assignment_form') do + select I18n.l(booth_assignment.poll.ends_at.to_date, format: :long), from: 'date' + select "#{booth_assignment.booth.name} (#{booth_assignment.booth.location})", from: 'booth_id' + click_button 'Add shift' + end + + expect(page).to have_content 'Officing shift added' + expect(page).to_not have_content 'This user has no officing shifts in this poll' + + visit admin_poll_path(booth_assignment.poll) + within('#poll-resources') do + click_link 'Officers (1)' + end + + expect(page).to_not have_content 'There are no officers in this poll' + expect(page).to have_content officer.name + expect(page).to have_content officer.email + end + + scenario 'Remove officer assignment from poll' do + officer_assignment = create(:poll_officer_assignment) + poll = officer_assignment.booth_assignment.poll + booth = officer_assignment.booth_assignment.booth + officer = officer_assignment.officer + + visit by_officer_admin_poll_officer_assignments_path(poll, officer_id: officer.id) + + expect(page).to_not have_content 'This user has no officing shifts in this poll' + within("#poll_officer_assignment_#{officer_assignment.id}") do + expect(page).to have_content booth.name + click_link 'Remove' + end + + expect(page).to have_content 'Officing shift removed' + expect(page).to have_content 'This user has no officing shifts in this poll' + end + + scenario 'Index view shows recounts info for officer' do + booth_assignment = create(:poll_booth_assignment) + poll = booth_assignment.poll + officer = create(:poll_officer) + officer_assignment = create(:poll_officer_assignment, + booth_assignment: booth_assignment, + officer: officer, + date: poll.starts_at) + final_officer_assignment = create(:poll_officer_assignment, :final, + booth_assignment: booth_assignment, + officer: officer, + date: poll.ends_at + 1.day) + recount = create(:poll_recount, + booth_assignment: booth_assignment, + officer_assignment: officer_assignment, + date: officer_assignment.date, + count: 77) + final_recount = create(:poll_final_recount, + booth_assignment: booth_assignment, + officer_assignment: final_officer_assignment, + date: poll.ends_at, + count: 9876) + + visit by_officer_admin_poll_officer_assignments_path(poll, officer_id: officer.id) + + within('#recount_list') { expect(page).to have_content('77') } + within('#final_recount_list') { expect(page).to have_content('9876') } + end +end \ No newline at end of file diff --git a/spec/features/admin/poll/officers_spec.rb b/spec/features/admin/poll/officers_spec.rb new file mode 100644 index 000000000..d9ea89638 --- /dev/null +++ b/spec/features/admin/poll/officers_spec.rb @@ -0,0 +1,36 @@ +require 'rails_helper' + +feature 'Admin poll officers' do + + background do + @admin = create(:administrator) + @user = create(:user, username: 'Pedro Jose Garcia') + @officer = create(:poll_officer) + login_as(@admin.user) + visit admin_officers_path + end + + scenario 'Index' do + expect(page).to have_content @officer.name + expect(page).to have_content @officer.email + expect(page).to_not have_content @user.name + end + + scenario 'Create', :js do + fill_in 'email', with: @user.email + click_button 'Search' + + expect(page).to have_content @user.name + click_link 'Add' + within("#officers") do + expect(page).to have_content @user.name + end + end + + scenario 'Delete' do + click_link 'Delete position' + + expect(page).to_not have_css '#officers' + end + +end \ No newline at end of file diff --git a/spec/features/admin/poll/polls_spec.rb b/spec/features/admin/poll/polls_spec.rb new file mode 100644 index 000000000..cb5a34265 --- /dev/null +++ b/spec/features/admin/poll/polls_spec.rb @@ -0,0 +1,366 @@ +require 'rails_helper' + +feature 'Admin polls' do + + background do + admin = create(:administrator) + login_as(admin.user) + end + + scenario 'Index empty', :js do + visit admin_root_path + + click_link "Polls" + within('#polls_menu') do + click_link "Polls" + end + + expect(page).to have_content "There are no polls" + end + + scenario 'Index', :js do + 3.times { create(:poll) } + + visit admin_root_path + + click_link "Polls" + within('#polls_menu') do + click_link "Polls" + end + + expect(page).to have_css ".poll", count: 3 + + polls = Poll.all + polls.each do |poll| + within("#poll_#{poll.id}") do + expect(page).to have_content poll.name + end + end + expect(page).to_not have_content "There are no polls" + end + + scenario 'Show' do + poll = create(:poll) + + visit admin_polls_path + click_link poll.name + + expect(page).to have_content poll.name + end + + scenario "Create" do + visit admin_polls_path + click_link "Create poll" + + start_date = 1.week.from_now + end_date = 2.weeks.from_now + + fill_in "poll_name", with: "Upcoming poll" + fill_in 'poll_starts_at', with: start_date.strftime("%d/%m/%Y") + fill_in 'poll_ends_at', with: end_date.strftime("%d/%m/%Y") + click_button "Create poll" + + expect(page).to have_content "Poll created successfully" + expect(page).to have_content "Upcoming poll" + expect(page).to have_content I18n.l(start_date.to_date) + expect(page).to have_content I18n.l(end_date.to_date) + end + + scenario "Edit" do + poll = create(:poll) + + visit admin_poll_path(poll) + click_link "Edit" + + end_date = 1.year.from_now + + fill_in "poll_name", with: "Next Poll" + fill_in 'poll_ends_at', with: end_date.strftime("%d/%m/%Y") + click_button "Update poll" + + expect(page).to have_content "Poll updated successfully" + expect(page).to have_content "Next Poll" + expect(page).to have_content I18n.l(end_date.to_date) + end + + scenario 'Edit from index' do + poll = create(:poll) + visit admin_polls_path + + within("#poll_#{poll.id}") do + click_link "Edit" + end + + expect(current_path).to eq(edit_admin_poll_path(poll)) + end + + context "Booths" do + + context "Poll show" do + + scenario "No booths" do + poll = create(:poll) + visit admin_poll_path(poll) + click_link "Booths (0)" + + expect(page).to have_content "There are no booths assigned to this poll." + end + + scenario "Booth list" do + poll = create(:poll) + 3.times { create(:poll_booth, polls: [poll]) } + + visit admin_poll_path(poll) + click_link "Booths (3)" + + expect(page).to have_css ".booth", count: 3 + + poll.booth_assignments.each do |ba| + within("#poll_booth_assignment_#{ba.id}") do + expect(page).to have_content ba.booth.name + expect(page).to have_content ba.booth.location + end + end + expect(page).to_not have_content "There are no booths assigned to this poll." + end + end + end + + context "Officers" do + + context "Poll show" do + + scenario "No officers", :js do + poll = create(:poll) + visit admin_poll_path(poll) + click_link "Officers (0)" + + expect(page).to have_content "There are no officers assigned to this poll" + end + + scenario "Officer list", :js do + poll = create(:poll) + booth = create(:poll_booth, polls: [poll]) + + booth.booth_assignments.each do |booth_assignment| + 3.times {create(:poll_officer_assignment, booth_assignment: booth_assignment) } + end + + visit admin_poll_path(poll) + + click_link "Officers (3)" + + expect(page).to have_css ".officer", count: 3 + + officers = Poll::Officer.all + officers.each do |officer| + within("#officer_#{officer.id}") do + expect(page).to have_content officer.name + expect(page).to have_content officer.email + end + end + expect(page).to_not have_content "There are no officers assigned to this poll" + end + end + end + + context "Questions" do + + context "Poll show" do + + scenario "Question list", :js do + poll = create(:poll) + question = create(:poll_question, poll: poll) + other_question = create(:poll_question) + + visit admin_poll_path(poll) + + expect(page).to have_content "Questions (1)" + expect(page).to have_content question.title + expect(page).to_not have_content other_question.title + expect(page).to_not have_content "There are no questions assigned to this poll" + end + + scenario 'Add question to poll', :js do + poll = create(:poll) + question = create(:poll_question, poll: nil, title: 'Should we rebuild the city?') + + visit admin_poll_path(poll) + + expect(page).to have_content 'Questions (0)' + expect(page).to have_content 'There are no questions assigned to this poll' + + fill_in 'search-questions', with: 'rebuild' + click_button 'Search' + + within('#search-questions-results') do + click_link 'Include question' + end + + expect(page).to have_content 'Question added to this poll' + + visit admin_poll_path(poll) + + expect(page).to have_content 'Questions (1)' + expect(page).to_not have_content 'There are no questions assigned to this poll' + expect(page).to have_content question.title + end + + scenario 'Remove question from poll', :js do + poll = create(:poll) + question = create(:poll_question, poll: poll) + + visit admin_poll_path(poll) + + expect(page).to have_content 'Questions (1)' + expect(page).to_not have_content 'There are no questions assigned to this poll' + expect(page).to have_content question.title + + within("#poll_question_#{question.id}") do + click_link 'Remove question from poll' + end + + expect(page).to have_content 'Question removed from this poll' + + visit admin_poll_path(poll) + + expect(page).to have_content 'Questions (0)' + expect(page).to have_content 'There are no questions assigned to this poll' + expect(page).to_not have_content question.title + end + + end + end + + context "Recounting" do + context "Poll show" do + scenario "No recounts", :js do + poll = create(:poll) + visit admin_poll_path(poll) + click_link "Recounting" + + expect(page).to have_content "There is nothing to be recounted" + end + + scenario "Recounts list", :js do + poll = create(:poll) + booth_assignment = create(:poll_booth_assignment, poll: poll) + booth_assignment_recounted = create(:poll_booth_assignment, poll: poll) + booth_assignment_final_recounted = create(:poll_booth_assignment, poll: poll) + + 3.times { |i| create(:poll_recount, + booth_assignment: booth_assignment, + date: poll.starts_at + i.days, + count: 33) } + + 3.times { |i| create(:poll_final_recount, + booth_assignment: booth_assignment, + date: poll.starts_at + i.days, + count: 21) } + + 2.times { create(:poll_voter, + booth_assignment: booth_assignment_final_recounted) } + + create(:poll_recount, + booth_assignment: booth_assignment_recounted, + date: poll.ends_at, + count: 777) + + create(:poll_final_recount, + booth_assignment: booth_assignment_final_recounted, + date: poll.ends_at, + count: 55555) + + visit admin_poll_path(poll) + + click_link "Recounting" + + expect(page).to have_css ".booth_recounts", count: 3 + + within("#poll_booth_assignment_#{booth_assignment.id}_recounts") do + expect(page).to have_content(booth_assignment.booth.name) + expect(page).to have_content('99') + expect(page).to have_content('63') + end + + within("#poll_booth_assignment_#{booth_assignment_recounted.id}_recounts") do + expect(page).to have_content(booth_assignment_recounted.booth.name) + expect(page).to have_content('777') + expect(page).to have_content('-') + end + + within("#poll_booth_assignment_#{booth_assignment_final_recounted.id}_recounts") do + expect(page).to have_content(booth_assignment_final_recounted.booth.name) + expect(page).to have_content('-') + expect(page).to have_content('55555') + expect(page).to have_content('2') + end + end + end + end + + context "Results" do + context "Poll show" do + scenario "No results", :js do + poll = create(:poll) + visit admin_poll_path(poll) + click_link "Results" + + expect(page).to have_content "There are no results" + end + + scenario "Results by answer", :js do + poll = create(:poll) + booth_assignment_1 = create(:poll_booth_assignment, poll: poll) + booth_assignment_2 = create(:poll_booth_assignment, poll: poll) + booth_assignment_3 = create(:poll_booth_assignment, poll: poll) + + question_1 = create(:poll_question, poll: poll, valid_answers: "Yes,No") + question_2 = create(:poll_question, poll: poll, valid_answers: "Today,Tomorrow") + + [booth_assignment_1, booth_assignment_2, booth_assignment_3].each do |ba| + create(:poll_partial_result, + booth_assignment: ba, + question: question_1, + answer: 'Yes', + amount: 11) + create(:poll_partial_result, + booth_assignment: ba, + question: question_2, + answer: 'Tomorrow', + amount: 5) + end + create(:poll_white_result, + booth_assignment: booth_assignment_1, + amount: 21) + create(:poll_null_result, + booth_assignment: booth_assignment_3, + amount: 44) + + visit admin_poll_path(poll) + + click_link "Results" + + expect(page).to have_content(question_1.title) + question_1.valid_answers.each_with_index do |answer, i| + within("#question_#{question_1.id}_#{i}_result") do + expect(page).to have_content(answer) + expect(page).to have_content([33, 0][i]) + end + end + + expect(page).to have_content(question_2.title) + question_2.valid_answers.each_with_index do |answer, i| + within("#question_#{question_2.id}_#{i}_result") do + expect(page).to have_content(answer) + expect(page).to have_content([0,15][i]) + end + end + + within('#white_results') { expect(page).to have_content('21') } + within('#null_results') { expect(page).to have_content('44') } + end + end + end + +end \ No newline at end of file diff --git a/spec/features/admin/poll/questions_spec.rb b/spec/features/admin/poll/questions_spec.rb new file mode 100644 index 000000000..2a1e100f0 --- /dev/null +++ b/spec/features/admin/poll/questions_spec.rb @@ -0,0 +1,116 @@ +require 'rails_helper' + +feature 'Admin poll questions' do + + background do + login_as(create(:administrator).user) + end + + scenario 'Index' do + question1 = create(:poll_question) + question2 = create(:poll_question) + + visit admin_questions_path + + expect(page).to have_content(question1.title) + expect(page).to have_content(question2.title) + end + + scenario 'Show' do + geozone = create(:geozone) + poll = create(:poll, geozone_restricted: true, geozone_ids: [geozone.id]) + question = create(:poll_question, poll: poll) + + visit admin_question_path(question) + + expect(page).to have_content(question.title) + expect(page).to have_content(question.description) + expect(page).to have_content(question.author.name) + expect(page).to have_content(question.valid_answers.join(" ")) + end + + scenario 'Create' do + poll = create(:poll, name: 'Movies') + title = "Star Wars: Episode IV - A New Hope" + description = %{ + During the battle, Rebel spies managed to steal secret plans to the Empire's ultimate weapon, the DEATH STAR, an armored space station with enough power to destroy an entire planet. + Pursued by the Empire's sinister agents, Princess Leia races home aboard her starship, custodian of the stolen plans that can save her people and restore freedom to the galaxy.... + } + + visit admin_questions_path + click_link "Create question" + + select 'Movies', from: 'poll_question_poll_id' + fill_in 'poll_question_title', with: title + fill_in 'poll_question_description', with: description + + click_button 'Save' + + expect(page).to have_content(title) + expect(page).to have_content(description) + end + + scenario 'Create from successful proposal index' do + poll = create(:poll, name: 'Proposals') + proposal = create(:proposal, :successful) + + visit proposals_path + click_link "Create question" + + expect(current_path).to eq(new_admin_question_path) + expect(page).to have_field('poll_question_title', with: proposal.title) + expect(page).to have_field('poll_question_description', with: proposal.description) + expect(page).to have_field('poll_question_valid_answers', with: "Yes, No") + + select 'Proposals', from: 'poll_question_poll_id' + + click_button 'Save' + + expect(page).to have_content(proposal.title) + expect(page).to have_content(proposal.description) + expect(page).to have_link(proposal.title, href: proposal_path(proposal)) + expect(page).to have_link(proposal.author.name, href: user_path(proposal.author)) + end + + pending "Create from successul proposal show" + + scenario 'Update' do + question1 = create(:poll_question) + + visit admin_questions_path + within("#poll_question_#{question1.id}") do + click_link "Edit" + end + + old_title = question1.title + new_title = "Potatoes are great and everyone should have one" + fill_in 'poll_question_title', with: new_title + + click_button 'Save' + + expect(page).to have_content "Changes saved" + expect(page).to have_content new_title + + visit admin_questions_path + + expect(page).to have_content(new_title) + expect(page).to_not have_content(old_title) + end + + scenario 'Destroy' do + question1 = create(:poll_question) + question2 = create(:poll_question) + + visit admin_questions_path + + within("#poll_question_#{question1.id}") do + click_link "Delete" + end + + expect(page).to_not have_content(question1.title) + expect(page).to have_content(question2.title) + end + + pending "Mark all city by default when creating a poll question from a successful proposal" + +end diff --git a/spec/features/admin/signature_sheets_spec.rb b/spec/features/admin/signature_sheets_spec.rb index 8243dd913..bcde7bfc0 100644 --- a/spec/features/admin/signature_sheets_spec.rb +++ b/spec/features/admin/signature_sheets_spec.rb @@ -19,20 +19,42 @@ feature 'Signature sheets' do end end - scenario 'Create' do - proposal = create(:proposal) - visit new_admin_signature_sheet_path + context 'Create' do + scenario 'Proposal' do + proposal = create(:proposal) + visit new_admin_signature_sheet_path - select "Citizen proposal", from: "signature_sheet_signable_type" - fill_in "signature_sheet_signable_id", with: proposal.id - fill_in "signature_sheet_document_numbers", with: "12345678Z, 99999999Z" - click_button "Create signature sheet" + select "Citizen proposal", from: "signature_sheet_signable_type" + fill_in "signature_sheet_signable_id", with: proposal.id + fill_in "signature_sheet_document_numbers", with: "12345678Z, 99999999Z" + click_button "Create signature sheet" - expect(page).to have_content "Signature sheet created successfully" + expect(page).to have_content "Signature sheet created successfully" - visit proposal_path(proposal) + visit proposal_path(proposal) + + expect(page).to have_content "1 support" + end + + scenario 'Budget Investment' do + investment = create(:budget_investment) + budget = investment.budget + budget.update(phase: 'selecting') + + visit new_admin_signature_sheet_path + + select "Investment", from: "signature_sheet_signable_type" + fill_in "signature_sheet_signable_id", with: investment.id + fill_in "signature_sheet_document_numbers", with: "12345678Z, 99999999Z" + click_button "Create signature sheet" + + expect(page).to have_content "Signature sheet created successfully" + + visit budget_investment_path(budget, investment) + + expect(page).to have_content "1 support" + end - expect(page).to have_content "1 support" end scenario 'Errors on create' do diff --git a/spec/features/admin/site_customization/content_blocks_spec.rb b/spec/features/admin/site_customization/content_blocks_spec.rb new file mode 100644 index 000000000..de2155737 --- /dev/null +++ b/spec/features/admin/site_customization/content_blocks_spec.rb @@ -0,0 +1,107 @@ +require 'rails_helper' + +feature "Admin custom content blocks" do + + background do + admin = create(:administrator) + login_as(admin.user) + end + + scenario "Index" do + block = create(:site_customization_content_block) + visit admin_site_customization_content_blocks_path + + expect(page).to have_content(block.name) + expect(page).to have_content(block.body) + end + + context "Create" do + scenario "Valid custom block" do + visit admin_root_path + + within("#side_menu") do + click_link "Custom content blocks" + end + + expect(page).to_not have_content "footer (es)" + + click_link "Create new content block" + + select "footer", from: "site_customization_content_block_name" + select "es", from: "site_customization_content_block_locale" + fill_in "site_customization_content_block_body", with: "Some custom content" + + click_button "Create Custom content block" + + expect(page).to have_content "footer (es)" + expect(page).to have_content "Some custom content" + end + + scenario "Invalid custom block" do + block = create(:site_customization_content_block) + + visit admin_root_path + + within("#side_menu") do + click_link "Custom content blocks" + end + + expect(page).to have_content "top_links (en)" + + click_link "Create new content block" + + select "top_links", from: "site_customization_content_block_name" + select "en", from: "site_customization_content_block_locale" + fill_in "site_customization_content_block_body", with: "Some custom content" + + click_button "Create Custom content block" + + expect(page).to have_content "Content block couldn't be created" + expect(page).to have_content "has already been taken" + end + end + + context "Update" do + scenario "Valid custom block" do + block = create(:site_customization_content_block) + visit admin_root_path + + within("#side_menu") do + click_link "Custom content blocks" + end + + click_link "top_links (en)" + + fill_in "site_customization_content_block_body", with: "Some other custom content" + click_button "Update Custom content block" + + expect(page).to have_content "Content block updated successfully" + expect(page).to have_content "Some other custom content" + end + end + + context "Delete" do + scenario "From index page" do + block = create(:site_customization_content_block) + visit admin_site_customization_content_blocks_path + + expect(page).to have_content("#{block.name} (#{block.locale})") + expect(page).to have_content(block.body) + + click_button "Delete block" + + expect(page).to_not have_content("#{block.name} (#{block.locale})") + expect(page).to_not have_content(block.body) + end + + scenario "From edit page" do + block = create(:site_customization_content_block) + visit edit_admin_site_customization_content_block_path(block) + + click_button "Delete block" + + expect(page).to_not have_content("#{block.name} (#{block.locale})") + expect(page).to_not have_content(block.body) + end + end +end diff --git a/spec/features/admin/site_customization/images_spec.rb b/spec/features/admin/site_customization/images_spec.rb new file mode 100644 index 000000000..094ac19bf --- /dev/null +++ b/spec/features/admin/site_customization/images_spec.rb @@ -0,0 +1,62 @@ +require 'rails_helper' + +feature "Admin custom images" do + + background do + admin = create(:administrator) + login_as(admin.user) + end + + scenario "Upload valid image" do + visit admin_root_path + + within("#side_menu") do + click_link "Custom Images" + end + + within("tr.logo_header") do + attach_file "site_customization_image_image", "spec/fixtures/files/logo_header.png" + click_button "Update" + end + + expect(page).to have_css("tr.logo_header img[src*='logo_header.png']") + expect(page).to have_css("img[src*='logo_header.png']", count: 2) # one in the admin form an one in the page header + end + + scenario "Upload invalid image" do + visit admin_root_path + + within("#side_menu") do + click_link "Custom Images" + end + + within("tr.icon_home") do + attach_file "site_customization_image_image", "spec/fixtures/files/logo_header.png" + click_button "Update" + end + + expect(page).to have_content("Width must be 330px") + expect(page).to have_content("Height must be 240px") + end + + scenario "Delete image" do + visit admin_root_path + + within("#side_menu") do + click_link "Custom Images" + end + + within("tr.social-media-icon") do + attach_file "site_customization_image_image", "spec/fixtures/files/social-media-icon.png" + click_button "Update" + end + + expect(page).to have_css("img[src*='social-media-icon.png']") + + within("tr.social-media-icon") do + click_link "Delete" + end + + expect(page).to_not have_css("img[src*='social-media-icon.png']") + end +end diff --git a/spec/features/admin/site_customization/pages_spec.rb b/spec/features/admin/site_customization/pages_spec.rb new file mode 100644 index 000000000..3e940ea69 --- /dev/null +++ b/spec/features/admin/site_customization/pages_spec.rb @@ -0,0 +1,69 @@ +require 'rails_helper' + +feature "Admin custom pages" do + + background do + admin = create(:administrator) + login_as(admin.user) + end + + scenario "Index" do + custom_page = create(:site_customization_page) + visit admin_site_customization_pages_path + + expect(page).to have_content(custom_page.title) + end + + context "Create" do + scenario "Valid custom page" do + visit admin_root_path + + within("#side_menu") do + click_link "Custom Pages" + end + + expect(page).to_not have_content "An example custom page" + + click_link "Create new page" + + fill_in "site_customization_page_title", with: "An example custom page" + fill_in "site_customization_page_subtitle", with: "Page subtitle" + fill_in "site_customization_page_slug", with: "example-page" + fill_in "site_customization_page_content", with: "This page is about..." + + click_button "Create Custom page" + + expect(page).to have_content "An example custom page" + end + end + + context "Update" do + scenario "Valid custom page" do + create(:site_customization_page, title: "An example custom page") + visit admin_root_path + + within("#side_menu") do + click_link "Custom Pages" + end + + click_link "An example custom page" + + expect(page).to have_selector("h2", text: "An example custom page") + + fill_in "site_customization_page_title", with: "Another example custom page" + click_button "Update Custom page" + + expect(page).to have_content "Page updated successfully" + expect(page).to have_content "Another example custom page" + end + end + + scenario "Delete" do + custom_page = create(:site_customization_page, title: "An example custom page") + visit edit_admin_site_customization_page_path(custom_page) + + click_button "Delete page" + + expect(page).to_not have_content("An example custom page") + end +end diff --git a/spec/features/admin/spending_proposals_spec.rb b/spec/features/admin/spending_proposals_spec.rb index 5539b5ead..953cdc4f6 100644 --- a/spec/features/admin/spending_proposals_spec.rb +++ b/spec/features/admin/spending_proposals_spec.rb @@ -93,11 +93,11 @@ feature 'Admin spending proposals' do expect(page).to have_button("Update") click_link("Back") expect(page).to_not have_button("Update") + expect(page).to have_link("Back") click_link("Back") expect(page).to_not have_link("Destroy the city") expect(page).to have_link("Realocate visitors") - end scenario "Filtering by admin", :js do @@ -135,8 +135,10 @@ feature 'Admin spending proposals' do click_link("Realocate visitors") click_link("Edit classification") expect(page).to have_button("Update") + expect(page).to have_link("Back") click_link("Back") expect(page).to_not have_button("Update") + expect(page).to have_link("Back") click_link("Back") expect(page).to have_content('There is 1 spending proposal') @@ -182,8 +184,10 @@ feature 'Admin spending proposals' do click_link("Realocate visitors") click_link("Edit classification") expect(page).to have_button("Update") + expect(page).to have_link("Back") click_link("Back") expect(page).to_not have_button("Update") + expect(page).to have_link("Back") click_link("Back") expect(page).to have_content('There is 1 spending proposal') diff --git a/spec/features/admin_spec.rb b/spec/features/admin_spec.rb index 161b01999..064021ab8 100644 --- a/spec/features/admin_spec.rb +++ b/spec/features/admin_spec.rb @@ -16,7 +16,7 @@ feature 'Admin' do expect(page).to have_content "You do not have permission to access this page" end - scenario 'Access as a moderator is not authorized' do + scenario 'Access as moderator is not authorized' do create(:moderator, user: user) login_as(user) visit admin_root_path @@ -26,7 +26,7 @@ feature 'Admin' do expect(page).to have_content "You do not have permission to access this page" end - scenario 'Access as a valuator is not authorized' do + scenario 'Access as valuator is not authorized' do create(:valuator, user: user) login_as(user) visit admin_root_path @@ -36,7 +36,7 @@ feature 'Admin' do expect(page).to have_content "You do not have permission to access this page" end - scenario 'Access as a manager is not authorized' do + scenario 'Access as manager is not authorized' do create(:manager, user: user) login_as(user) visit admin_root_path @@ -46,7 +46,17 @@ feature 'Admin' do expect(page).to have_content "You do not have permission to access this page" end - scenario 'Access as an administrator is authorized' do + scenario 'Access as poll officer is not authorized' do + create(:poll_officer, user: user) + login_as(user) + visit admin_root_path + + expect(current_path).not_to eq(admin_root_path) + expect(current_path).to eq(proposals_path) + expect(page).to have_content "You do not have permission to access this page" + end + + scenario 'Access as administrator is authorized' do login_as(administrator) visit admin_root_path diff --git a/spec/features/budgets/ballots_spec.rb b/spec/features/budgets/ballots_spec.rb index c80ec55f4..b7471ee41 100644 --- a/spec/features/budgets/ballots_spec.rb +++ b/spec/features/budgets/ballots_spec.rb @@ -101,7 +101,7 @@ feature 'Ballots' do context "Adding and Removing Investments" do - scenario "Add a proposal", :js do + scenario "Add a investment", :js do investment1 = create(:budget_investment, :selected, heading: new_york, price: 10000) investment2 = create(:budget_investment, :selected, heading: new_york, price: 20000) @@ -130,7 +130,7 @@ feature 'Ballots' do end end - scenario "Removing a proposal", :js do + scenario "Removing a investment", :js do investment = create(:budget_investment, :selected, heading: new_york, price: 10000) ballot = create(:budget_ballot, user: user, budget: budget) ballot.investments << investment @@ -297,7 +297,7 @@ feature 'Ballots' do expect(page).to_not have_css "#progressbar" expect(page).to have_content "You have active votes in another heading:" - expect(page).to have_link california.name, href: budget_investments_path(budget, heading: california) + expect(page).to have_link california.name, href: budget_investments_path(budget, heading_id: california.id) end end @@ -313,12 +313,14 @@ feature 'Ballots' do expect(current_path).to eq(budget_investments_path(budget)) end - scenario 'Displaying the correct count & amount' do + scenario 'Displaying the correct group, heading, count & amount' do group1 = create(:budget_group, budget: budget) group2 = create(:budget_group, budget: budget) + create(:budget_heading, name: "District A", group: group1, price: 100) heading1 = create(:budget_heading, name: "District 1", group: group1, price: 100) heading2 = create(:budget_heading, name: "District 2", group: group2, price: 50) + create(:budget_heading, name: "District Z", group: group1, price: 100) investment1 = create(:budget_investment, :selected, price: 10, heading: heading1) investment2 = create(:budget_investment, :selected, price: 10, heading: heading1) @@ -333,12 +335,12 @@ feature 'Ballots' do login_as(user) visit budget_ballot_path(budget) - expect(page).to have_content("You have voted 5 proposals") + expect(page).to have_content("You have voted 5 investments") within("#budget_group_#{group1.id}") do expect(page).to have_content "#{group1.name} - #{heading1.name}" expect(page).to have_content "Amount spent €20" - expect(page).to have_content "You still have €80 to invest" + expect(page).to have_link "You still have €80 to invest.", href: budget_group_path(budget, group1) end within("#budget_group_#{group2.id}") do @@ -348,9 +350,21 @@ feature 'Ballots' do end end + scenario 'Display links to vote on groups with no investments voted yet' do + group = create(:budget_group, budget: budget) + heading = create(:budget_heading, name: "District 1", group: group, price: 100) + + ballot = create(:budget_ballot, user: user, budget: budget) + + login_as(user) + visit budget_ballot_path(budget) + + expect(page).to have_link "You have not voted on this group yet, go vote!", href: budget_group_path(budget, group) + end + end - scenario 'Removing spending proposals from ballot', :js do + scenario 'Removing investments from ballot', :js do investment = create(:budget_investment, :selected, price: 10, heading: new_york) ballot = create(:budget_ballot, user: user, budget: budget) ballot.investments << investment @@ -358,17 +372,17 @@ feature 'Ballots' do login_as(user) visit budget_ballot_path(budget) - expect(page).to have_content("You have voted one proposal") + expect(page).to have_content("You have voted one investment") within("#budget_investment_#{investment.id}") do find(".remove-investment-project").trigger('click') end expect(current_path).to eq(budget_ballot_path(budget)) - expect(page).to have_content("You have voted 0 proposals") + expect(page).to have_content("You have voted 0 investments") end - scenario 'Removing spending proposals from ballot (sidebar)', :js do + scenario 'Removing investments from ballot (sidebar)', :js do investment1 = create(:budget_investment, :selected, price: 10000, heading: new_york) investment2 = create(:budget_investment, :selected, price: 20000, heading: new_york) @@ -405,6 +419,28 @@ feature 'Ballots' do end end + scenario 'Back link after removing an investment from Ballot', :js do + investment = create(:budget_investment, :selected, heading: new_york, price: 10) + + login_as(user) + visit budget_investments_path(budget, heading_id: new_york.id) + add_to_ballot(investment) + + click_link "Check my ballot" + + expect(page).to have_content("You have voted one investment") + + within("#budget_investment_#{investment.id}") do + find(".remove-investment-project").trigger('click') + end + + expect(page).to have_content("You have voted 0 investments") + + click_link "Go back" + + expect(page).to have_current_path(budget_investments_path(budget, heading_id: new_york.id)) + end + context 'Permissions' do scenario 'User not logged in', :js do @@ -428,7 +464,7 @@ feature 'Ballots' do within("#budget_investment_#{investment.id}") do find("div.ballot").hover - expect(page).to have_content 'Only verified users can vote on proposals' + expect(page).to have_content 'Only verified users can vote on investments' expect(page).to have_selector('.in-favor a', visible: false) end end @@ -447,10 +483,12 @@ feature 'Ballots' do end scenario 'Unselected investments' do - investment = create(:budget_investment, heading: new_york) + investment = create(:budget_investment, heading: new_york, title: "WTF asdfasfd") login_as(user) - visit budget_investments_path(budget, heading_id: new_york.id, unfeasible: 1) + visit budget_path(budget) + click_link states.name + click_link new_york.name expect(page).to_not have_css("#budget_investment_#{investment.id}") end @@ -459,7 +497,9 @@ feature 'Ballots' do investment = create(:budget_investment, feasibility: "undecided", heading: new_york) login_as(user) - visit budget_investments_path(budget, heading_id: new_york.id) + visit budget_path(budget) + click_link states.name + click_link new_york.name within("#budget-investments") do expect(page).to_not have_css("div.ballot") diff --git a/spec/features/budgets/budgets_spec.rb b/spec/features/budgets/budgets_spec.rb index 4d4d603f3..81530a43d 100644 --- a/spec/features/budgets/budgets_spec.rb +++ b/spec/features/budgets/budgets_spec.rb @@ -8,14 +8,57 @@ feature 'Budgets' do budgets.each {|budget| expect(page).to have_link(budget.name)} end - scenario 'Show' do - budget = create(:budget) - group1 = create(:budget_group, budget: budget) - group2 = create(:budget_group, budget: budget) + context 'Show' do - visit budget_path(budget) + scenario "List all groups" do + budget = create(:budget) + group1 = create(:budget_group, budget: budget) + group2 = create(:budget_group, budget: budget) + + visit budget_path(budget) + + budget.groups.each {|group| expect(page).to have_link(group.name)} + end + + scenario "Links to unfeasible and selected if balloting or later" do + budget = create(:budget, :selecting) + group = create(:budget_group, budget: budget) + + visit budget_path(budget) + + expect(page).to_not have_link "See unfeasible investments" + expect(page).to_not have_link "See investments not selected for balloting phase" + + click_link group.name + + expect(page).to_not have_link "See unfeasible investments" + expect(page).to_not have_link "See investments not selected for balloting phase" + + budget.update(phase: :balloting) + + visit budget_path(budget) + + expect(page).to have_link "See unfeasible investments" + expect(page).to have_link "See investments not selected for balloting phase" + + click_link group.name + + expect(page).to have_link "See unfeasible investments" + expect(page).to have_link "See investments not selected for balloting phase" + + budget.update(phase: :finished) + + visit budget_path(budget) + + expect(page).to have_link "See unfeasible investments" + expect(page).to have_link "See investments not selected for balloting phase" + + click_link group.name + + expect(page).to have_link "See unfeasible investments" + expect(page).to have_link "See investments not selected for balloting phase" + end - budget.groups.each {|group| expect(page).to have_link(group.name)} end context 'Accepting' do diff --git a/spec/features/budgets/investments_spec.rb b/spec/features/budgets/investments_spec.rb index a6feb6f44..37ab50fda 100644 --- a/spec/features/budgets/investments_spec.rb +++ b/spec/features/budgets/investments_spec.rb @@ -8,10 +8,14 @@ feature 'Budget Investments' do let!(:heading) { create(:budget_heading, name: "More hospitals", group: group) } scenario 'Index' do - investments = [create(:budget_investment, heading: heading), create(:budget_investment, heading: heading), create(:budget_investment, :feasible, heading: heading)] + investments = [create(:budget_investment, heading: heading), + create(:budget_investment, heading: heading), + create(:budget_investment, :feasible, heading: heading)] + unfeasible_investment = create(:budget_investment, :unfeasible, heading: heading) - visit budget_investments_path(budget, heading_id: heading.id) + visit budget_path(budget) + click_link "Health" expect(page).to have_selector('#budget-investments .budget-investment', count: 3) investments.each do |investment| @@ -56,7 +60,7 @@ feature 'Budget Investments' do investment3 = create(:budget_investment, heading: heading) investment4 = create(:budget_investment, :feasible, heading: heading) - visit budget_investments_path(budget_id: budget.id, heading_id: heading.id, unfeasible: 1) + visit budget_investments_path(budget_id: budget.id, heading_id: heading.id, filter: "unfeasible") within("#budget-investments") do expect(page).to have_css('.budget-investment', count: 1) @@ -69,6 +73,7 @@ feature 'Budget Investments' do end scenario "by unfeasibilty link for group with one heading" do + budget.update(phase: :balloting) group = create(:budget_group, name: 'All City', budget: budget) heading = create(:budget_heading, name: "Madrid", group: group) @@ -77,11 +82,12 @@ feature 'Budget Investments' do click_link "All City" - expected_path = budget_investments_path(budget, heading_id: heading.id, unfeasible: 1) + expected_path = budget_investments_path(budget, heading_id: heading.id, filter: "unfeasible") expect(page).to have_current_path(expected_path) end scenario "by unfeasibilty link for group with many headings" do + budget.update(phase: :balloting) group = create(:budget_group, name: 'Districts', budget: budget) heading1 = create(:budget_heading, name: 'Carabanchel', group: group) heading2 = create(:budget_heading, name: 'Barajas', group: group) @@ -93,7 +99,7 @@ feature 'Budget Investments' do click_link 'Districts' click_link 'Carabanchel' - expected_path = budget_investments_path(budget, heading_id: heading1.id, unfeasible: 1) + expected_path = budget_investments_path(budget, heading_id: heading1.id, filter: "unfeasible") expect(page).to have_current_path(expected_path) end end @@ -188,7 +194,7 @@ feature 'Budget Investments' do expect(current_path).to eq(budget_investments_path(budget_id: budget.id)) end - scenario 'Create spending proposal too fast' do + scenario 'Create budget investment too fast' do allow(InvisibleCaptcha).to receive(:timestamp_threshold).and_return(Float::INFINITY) login_as(author) @@ -263,26 +269,40 @@ feature 'Budget Investments' do end end - scenario "Show (feasible spending proposal)" do - user = create(:user) - login_as(user) + context "Show (feasible budget investment)" do + let(:investment) { create(:budget_investment, + :feasible, + :finished, + budget: budget, + group: group, + heading: heading, + price: 16, + price_explanation: 'Every wheel is 4 euros, so total is 16')} - investment = create(:budget_investment, - :feasible, - :finished, - budget: budget, - group: group, - heading: heading, - price: 16, - price_explanation: 'Every wheel is 4 euros, so total is 16') + background do + user = create(:user) + login_as(user) + end - visit budget_investment_path(budget_id: budget.id, id: investment.id) + scenario "Budget in selecting phase" do + budget.update(phase: "selecting") + visit budget_investment_path(budget_id: budget.id, id: investment.id) - expect(page).to have_content("Price explanation") - expect(page).to have_content(investment.price_explanation) + expect(page).to_not have_content("Unfeasibility explanation") + expect(page).to_not have_content("Price explanation") + expect(page).to_not have_content(investment.price_explanation) + end + + scenario "Budget in balloting phase" do + budget.update(phase: "balloting") + visit budget_investment_path(budget_id: budget.id, id: investment.id) + + expect(page).to have_content("Price explanation") + expect(page).to have_content(investment.price_explanation) + end end - scenario "Show (unfeasible spending proposal)" do + scenario "Show (unfeasible budget investment)" do user = create(:user) login_as(user) @@ -302,7 +322,7 @@ feature 'Budget Investments' do context "Destroy" do - scenario "Admin cannot destroy spending proposals" do + scenario "Admin cannot destroy budget investments" do admin = create(:administrator) user = create(:user, :level_two) investment = create(:budget_investment, heading: heading, author: user) @@ -315,6 +335,20 @@ feature 'Budget Investments' do end end + scenario "Author can destroy while on the accepting phase" do + user = create(:user, :level_two) + sp1 = create(:budget_investment, heading: heading, price: 10000, author: user) + + login_as(user) + visit user_path(user, tab: :budget_investments) + + within("#budget_investment_#{sp1.id}") do + expect(page).to have_content(sp1.title) + click_link('Delete') + end + + visit user_path(user, tab: :budget_investments) + end end context "Selecting Phase" do @@ -391,7 +425,7 @@ feature 'Budget Investments' do budget.update(phase: "valuating") end - scenario "Sidebar in show should display supports text and supports" do + scenario "Sidebar in show should display support text and count" do investment = create(:budget_investment, :selected, budget: budget) create(:vote, votable: investment) @@ -403,8 +437,8 @@ feature 'Budget Investments' do end end - scenario "Index should display supports" do - investment = create(:budget_investment, :selected, budget: budget, heading: heading) + scenario "Index should display support count" do + investment = create(:budget_investment, budget: budget, heading: heading) create(:vote, votable: investment) visit budget_investments_path(budget, heading_id: heading.id) @@ -414,6 +448,18 @@ feature 'Budget Investments' do end end + scenario "Show should display support text and count" do + investment = create(:budget_investment, budget: budget, heading: heading) + create(:vote, votable: investment) + + visit budget_investment_path(budget, investment) + + within("#budget_investment_#{investment.id}") do + expect(page).to have_content "Supports" + expect(page).to have_content "1 support" + end + end + end context "Balloting Phase" do @@ -495,7 +541,6 @@ feature 'Budget Investments' do carabanchel_heading = create(:budget_heading, group: group, name: "Carabanchel") new_york_heading = create(:budget_heading, group: group, name: "New York") - sp1 = create(:budget_investment, :selected, price: 1, heading: global_heading) sp2 = create(:budget_investment, :selected, price: 10, heading: global_heading) sp3 = create(:budget_investment, :selected, price: 100, heading: global_heading) @@ -560,5 +605,52 @@ feature 'Budget Investments' do end end + scenario 'Show unselected budget investments' do + investment1 = create(:budget_investment, :unselected, :feasible, heading: heading, valuation_finished: true) + investment2 = create(:budget_investment, :selected, :feasible, heading: heading, valuation_finished: true) + investment3 = create(:budget_investment, :selected, :feasible, heading: heading, valuation_finished: true) + investment4 = create(:budget_investment, :selected, :feasible, heading: heading, valuation_finished: true) + + visit budget_investments_path(budget_id: budget.id, heading_id: heading.id, filter: "unselected") + + within("#budget-investments") do + expect(page).to have_css('.budget-investment', count: 1) + + expect(page).to have_content(investment1.title) + expect(page).to_not have_content(investment2.title) + expect(page).to_not have_content(investment3.title) + expect(page).to_not have_content(investment4.title) + end + end + + scenario "Shows unselected link for group with one heading" do + group = create(:budget_group, name: 'All City', budget: budget) + heading = create(:budget_heading, name: "Madrid", group: group) + + visit budget_path(budget) + click_link 'See investments not selected for balloting phase' + + click_link "All City" + + expected_path = budget_investments_path(budget, heading_id: heading.id, filter: "unselected") + expect(page).to have_current_path(expected_path) + end + + scenario "Shows unselected link for group with many headings" do + group = create(:budget_group, name: 'Districts', budget: budget) + heading1 = create(:budget_heading, name: 'Carabanchel', group: group) + heading2 = create(:budget_heading, name: 'Barajas', group: group) + + visit budget_path(budget) + + click_link 'See investments not selected for balloting phase' + + click_link 'Districts' + click_link 'Carabanchel' + + expected_path = budget_investments_path(budget, heading_id: heading1.id, filter: "unselected") + expect(page).to have_current_path(expected_path) + end + end -end \ No newline at end of file +end diff --git a/spec/features/comments/poll_questions_spec.rb b/spec/features/comments/poll_questions_spec.rb new file mode 100644 index 000000000..6e5cffe97 --- /dev/null +++ b/spec/features/comments/poll_questions_spec.rb @@ -0,0 +1 @@ +#refactor specs and test as one more commmentable \ No newline at end of file diff --git a/spec/features/debates_spec.rb b/spec/features/debates_spec.rb index f16f416bf..f6efceea2 100644 --- a/spec/features/debates_spec.rb +++ b/spec/features/debates_spec.rb @@ -54,7 +54,7 @@ feature 'Debates' do expect(page.html).to include "#{debate.title}" within('.social-share-button') do - expect(page.all('a').count).to be(3) # Twitter, Facebook, Google+ + expect(page.all('a').count).to be(4) # Twitter, Facebook, Google+, Telegram end end @@ -640,6 +640,28 @@ feature 'Debates' do end end + scenario "Search by custom invalid date range", :js do + debate1 = create(:debate, created_at: 2.years.ago) + debate2 = create(:debate, created_at: 3.days.ago) + debate3 = create(:debate, created_at: 9.days.ago) + + visit debates_path + + click_link "Advanced search" + select "Customized", from: "js-advanced-search-date-min" + fill_in "advanced_search_date_min", with: "9" + fill_in "advanced_search_date_max", with: "444444444" + click_button "Filter" + + within("#debates") do + expect(page).to have_css('.debate', count: 3) + + expect(page).to have_content(debate1.title) + expect(page).to have_content(debate2.title) + expect(page).to have_content(debate3.title) + end + end + scenario "Search by multiple filters", :js do ana = create :user, official_level: 1 john = create :user, official_level: 1 diff --git a/spec/features/home_spec.rb b/spec/features/home_spec.rb index 708b1c883..4deed36bb 100644 --- a/spec/features/home_spec.rb +++ b/spec/features/home_spec.rb @@ -43,7 +43,7 @@ feature "Home" do end def ie_alert_box_xpath - "/html/body/div[@class='wrapper']/comment()[contains(.,'ie-callout')]" + "/html/body/div[@class='wrapper ']/comment()[contains(.,'ie-callout')]" end end end diff --git a/spec/features/management/budget_investments_spec.rb b/spec/features/management/budget_investments_spec.rb index 5b3961306..c4d7a8797 100644 --- a/spec/features/management/budget_investments_spec.rb +++ b/spec/features/management/budget_investments_spec.rb @@ -137,6 +137,61 @@ feature 'Budget Investments' do end end + scenario "Listing - managers can see budgets in accepting phase" do + accepting_budget = create(:budget, phase: "accepting") + reviewing_budget = create(:budget, phase: "reviewing") + selecting_budget = create(:budget, phase: "selecting") + valuating_budget = create(:budget, phase: "valuating") + balloting_budget = create(:budget, phase: "balloting") + reviewing_ballots_budget = create(:budget, phase: "reviewing_ballots") + finished = create(:budget, phase: "finished") + + user = create(:user, :level_two) + login_managed_user(user) + + click_link "Create budget investment" + + expect(page).to have_content(accepting_budget.name) + + expect(page).to_not have_content(reviewing_budget.name) + expect(page).to_not have_content(selecting_budget.name) + expect(page).to_not have_content(valuating_budget.name) + expect(page).to_not have_content(balloting_budget.name) + expect(page).to_not have_content(reviewing_ballots_budget.name) + expect(page).to_not have_content(finished.name) + end + + scenario "Listing - admins can see budgets in accepting, reviewing and selecting phases" do + accepting_budget = create(:budget, phase: "accepting") + reviewing_budget = create(:budget, phase: "reviewing") + selecting_budget = create(:budget, phase: "selecting") + valuating_budget = create(:budget, phase: "valuating") + balloting_budget = create(:budget, phase: "balloting") + reviewing_ballots_budget = create(:budget, phase: "reviewing_ballots") + finished = create(:budget, phase: "finished") + + visit root_path + click_link "Sign out" + + admin = create(:administrator) + login_as(admin.user) + + user = create(:user, :level_two) + login_managed_user(user) + visit management_sign_in_path + + click_link "Create budget investment" + + expect(page).to have_content(accepting_budget.name) + expect(page).to have_content(reviewing_budget.name) + expect(page).to have_content(selecting_budget.name) + + expect(page).to_not have_content(valuating_budget.name) + expect(page).to_not have_content(balloting_budget.name) + expect(page).to_not have_content(reviewing_ballots_budget.name) + expect(page).to_not have_content(finished.name) + end + context "Supporting" do scenario 'Supporting budget investments on behalf of someone in index view', :js do diff --git a/spec/features/management/users_spec.rb b/spec/features/management/users_spec.rb index 694720cc0..5d08ce711 100644 --- a/spec/features/management/users_spec.rb +++ b/spec/features/management/users_spec.rb @@ -18,6 +18,7 @@ feature 'Users' do fill_in 'user_username', with: 'pepe' fill_in 'user_email', with: 'pepe@gmail.com' + select_date '31-December-1980', from: 'user_date_of_birth' click_button 'Create user' @@ -28,6 +29,7 @@ feature 'Users' do expect(user).to be_level_three_verified expect(user).to be_residence_verified expect(user).to_not be_confirmed + expect(user.date_of_birth).to have_content (Date.new(1980,12,31)) sent_token = /.*confirmation_token=(.*)".*/.match(ActionMailer::Base.deliveries.last.body.to_s)[1] visit user_confirmation_path(confirmation_token: sent_token) diff --git a/spec/features/moderation_spec.rb b/spec/features/moderation_spec.rb index 6607ec48a..fedcb105d 100644 --- a/spec/features/moderation_spec.rb +++ b/spec/features/moderation_spec.rb @@ -43,6 +43,20 @@ feature 'Moderation' do expect(page).to have_content "You do not have permission to access this page" end + scenario 'Access as poll officer is not authorized' do + create(:poll_officer, user: user) + + login_as(user) + visit root_path + + expect(page).to_not have_link("Moderation") + visit moderation_root_path + + expect(current_path).not_to eq(moderation_root_path) + expect(current_path).to eq(proposals_path) + expect(page).to have_content "You do not have permission to access this page" + end + scenario 'Access as a moderator is authorized' do create(:moderator, user: user) diff --git a/spec/features/officing/final_recount_spec.rb b/spec/features/officing/final_recount_spec.rb new file mode 100644 index 000000000..1fcfa48a1 --- /dev/null +++ b/spec/features/officing/final_recount_spec.rb @@ -0,0 +1,147 @@ +require 'rails_helper' + +feature 'Officing Final Recount' do + + background do + @poll_officer = create(:poll_officer) + @officer_assignment = create(:poll_officer_assignment, :final, officer: @poll_officer) + @poll = @officer_assignment.booth_assignment.poll + @poll.update(ends_at: 1.day.ago) + login_as(@poll_officer.user) + end + + scenario 'Only polls where user is officer for final recounts are accessible' do + regular_officer_assignment_1 = create(:poll_officer_assignment, officer: @poll_officer) + regular_officer_assignment_2 = create(:poll_officer_assignment, officer: @poll_officer) + + not_allowed_poll_1 = create(:poll, :expired) + not_allowed_poll_2 = regular_officer_assignment_1.booth_assignment.poll + not_allowed_poll_2.update(ends_at: 1.day.ago) + not_allowed_poll_3 = regular_officer_assignment_2.booth_assignment.poll + + visit root_path + click_link 'Polling officers' + + expect(page).to have_content('Poll officing') + within('#side_menu') do + click_link 'Final recounts and results' + end + + expect(page).to_not have_content(not_allowed_poll_1.name) + expect(page).to_not have_content(not_allowed_poll_2.name) + expect(page).to_not have_content(not_allowed_poll_3.name) + expect(page).to have_content(@poll.name) + + visit new_officing_poll_final_recount_path(not_allowed_poll_1) + expect(page).to have_content('You are allowed to add final recounts for this poll') + end + + scenario 'Add final recount' do + visit officing_root_path + + within('#side_menu') do + click_link 'Final recounts and results' + end + + within("#poll_#{@poll.id}") do + expect(page).to have_content(@poll.name) + click_link 'Add final recount' + end + + expect(page).to_not have_content('Your recounts') + + booth_name = @officer_assignment.booth_assignment.booth.name + date = I18n.l(@poll.starts_at.to_date, format: :long) + select booth_name, from: 'officer_assignment_id' + select date, from: 'date' + fill_in :count, with: '33' + click_button 'Save' + + expect(page).to have_content('Your final recounts') + + within("#poll_final_recount_#{@officer_assignment.booth_assignment.final_recounts.first.id}") do + expect(page).to have_content(date) + expect(page).to have_content(booth_name) + expect(page).to have_content('33') + end + end + + scenario 'Edit final recount' do + final_recount = create(:poll_final_recount, + officer_assignment: @officer_assignment, + booth_assignment: @officer_assignment.booth_assignment, + date: @poll.starts_at, + count: 100) + + booth_name = @officer_assignment.booth_assignment.booth.name + date = I18n.l(final_recount.date.to_date, format: :long) + + visit new_officing_poll_final_recount_path(@poll) + + expect(page).to have_content('Your final recounts') + + within("#poll_final_recount_#{final_recount.id}") do + expect(page).to have_content(date) + expect(page).to have_content(booth_name) + expect(page).to have_content('100') + end + + select booth_name, from: 'officer_assignment_id' + select date, from: 'date' + fill_in :count, with: '42' + click_button 'Save' + + expect(page).to have_content "Data added" + + within("#poll_final_recount_#{final_recount.id}") do + expect(page).to have_content(date) + expect(page).to have_content(booth_name) + expect(page).to have_content('42') + end + expect(page).to_not have_content('100') + end + + scenario 'Show final and system recounts to compare' do + final_officer_assignment = create(:poll_officer_assignment, :final, officer: @poll_officer) + poll = final_officer_assignment.booth_assignment.poll + poll.update(ends_at: 1.day.ago) + final_recount = create(:poll_final_recount, + officer_assignment: final_officer_assignment, + booth_assignment: final_officer_assignment.booth_assignment, + date: 7.days.ago, + count: 100) + 33.times { create(:poll_voter, :valid_document, + poll: poll, + booth_assignment: final_officer_assignment.booth_assignment, + created_at: final_recount.date) } + + visit new_officing_poll_final_recount_path(poll) + within("#poll_final_recount_#{final_recount.id}") do + expect(page).to have_content(I18n.l(final_recount.date.to_date, format: :long)) + expect(page).to have_content(final_officer_assignment.booth_assignment.booth.name) + expect(page).to have_content('100') + expect(page).to have_content('33') + end + end + + scenario "Show link to add results for same booth/date" do + final_officer_assignment = create(:poll_officer_assignment, :final, officer: @poll_officer) + poll = final_officer_assignment.booth_assignment.poll + poll.update(ends_at: 1.day.ago) + final_recount = create(:poll_final_recount, + officer_assignment: final_officer_assignment, + booth_assignment: final_officer_assignment.booth_assignment, + date: 7.days.ago, + count: 100) + visit new_officing_poll_final_recount_path(poll) + within("#poll_final_recount_#{final_recount.id}") do + click_link "Add results" + end + + expected_path = new_officing_poll_result_path(poll, oa: final_recount.officer_assignment.id, d: I18n.l(final_recount.date.to_date)) + expect(page).to have_current_path(expected_path) + expect(page).to have_select('officer_assignment_id', selected: final_recount.booth_assignment.booth.name) + expect(page).to have_select('date', selected: I18n.l(final_recount.date.to_date, format: :long)) + end + +end \ No newline at end of file diff --git a/spec/features/officing/recount_spec.rb b/spec/features/officing/recount_spec.rb new file mode 100644 index 000000000..a7d74d0be --- /dev/null +++ b/spec/features/officing/recount_spec.rb @@ -0,0 +1,87 @@ +require 'rails_helper' + +feature 'Officing Recount' do + + background do + @poll_officer = create(:poll_officer) + @officer_assignment = create(:poll_officer_assignment, officer: @poll_officer) + @poll = @officer_assignment.booth_assignment.poll + login_as(@poll_officer.user) + end + + scenario 'Only polls where user is officer are accessible' do + not_allowed_poll = create(:poll) + + visit root_path + click_link 'Polling officers' + + expect(page).to have_content('Poll officing') + within('#side_menu') do + click_link 'Store recount' + end + + expect(page).to_not have_content(not_allowed_poll.name) + expect(page).to have_content(@poll.name) + + visit new_officing_poll_recount_path(not_allowed_poll) + expect(page).to have_content('You are not a poll officer for this poll') + end + + scenario 'Add recount' do + visit officing_root_path + + within('#side_menu') do + click_link 'Store recount' + end + + click_link @poll.name + + expect(page).to_not have_content('Your recounts') + + booth_name = @officer_assignment.booth_assignment.booth.name + date = I18n.l(@officer_assignment.date.to_date, format: :long) + select "#{booth_name}: #{date}", from: 'officer_assignment_id' + fill_in :count, with: '33' + click_button 'Save' + + expect(page).to have_content('Your recounts') + + within("#poll_recount_#{@officer_assignment.booth_assignment.recounts.first.id}") do + expect(page).to have_content(date) + expect(page).to have_content(booth_name) + expect(page).to have_content('33') + end + end + + scenario 'Edit recount' do + recount = create(:poll_recount, + officer_assignment: @officer_assignment, + booth_assignment: @officer_assignment.booth_assignment, + date: @officer_assignment.date, + count: 100) + + booth_name = @officer_assignment.booth_assignment.booth.name + date = I18n.l(@officer_assignment.date.to_date, format: :long) + + visit new_officing_poll_recount_path(@poll) + + expect(page).to have_content('Your recounts') + + within("#poll_recount_#{recount.id}") do + expect(page).to have_content(date) + expect(page).to have_content(booth_name) + expect(page).to have_content('100') + end + + select "#{booth_name}: #{date}", from: 'officer_assignment_id' + fill_in :count, with: '42' + click_button 'Save' + + within("#poll_recount_#{recount.id}") do + expect(page).to have_content(date) + expect(page).to have_content(booth_name) + expect(page).to have_content('42') + end + expect(page).to_not have_content('100') + end +end \ No newline at end of file diff --git a/spec/features/officing/residence_spec.rb b/spec/features/officing/residence_spec.rb new file mode 100644 index 000000000..69ffd9d21 --- /dev/null +++ b/spec/features/officing/residence_spec.rb @@ -0,0 +1,96 @@ +require 'rails_helper' + +feature 'Residence' do + let(:officer) { create(:poll_officer) } + + feature "Officers without assignments" do + + scenario "Can not access residence verification" do + login_as(officer.user) + visit officing_root_path + + within("#side_menu") do + click_link "Validate document" + end + + expect(page).to have_content("You don't have officing shifts today") + + create(:poll_officer_assignment, officer: officer, date: 1.day.from_now) + + visit new_officing_residence_path + + expect(page).to have_content("You don't have officing shifts today") + end + + end + + feature "Assigned officers" do + + background do + create(:poll_officer_assignment, officer: officer) + login_as(officer.user) + visit officing_root_path + end + + scenario "Verify voter" do + within("#side_menu") do + click_link "Validate document" + end + + select 'DNI', from: 'residence_document_type' + fill_in 'residence_document_number', with: "12345678Z" + fill_in 'residence_year_of_birth', with: '1980' + + click_button 'Validate document' + + expect(page).to have_content 'Document verified with Census' + end + + scenario "Error on verify" do + within("#side_menu") do + click_link "Validate document" + end + + click_button 'Validate document' + expect(page).to have_content(/\d errors? prevented the verification of this document/) + end + + scenario "Error on Census (document number)" do + initial_failed_census_calls_count = officer.failed_census_calls_count + within("#side_menu") do + click_link "Validate document" + end + + select 'DNI', from: 'residence_document_type' + fill_in 'residence_document_number', with: "9999999A" + fill_in 'residence_year_of_birth', with: '1980' + + click_button 'Validate document' + + expect(page).to have_content 'The Census was unable to verify this document' + + officer.reload + fcc = FailedCensusCall.last + expect(fcc).to be + expect(fcc.poll_officer).to eq(officer) + expect(officer.failed_census_calls.last).to eq(fcc) + expect(officer.failed_census_calls_count).to eq(initial_failed_census_calls_count + 1) + end + + scenario "Error on Census (year of birth)" do + within("#side_menu") do + click_link "Validate document" + end + + select 'DNI', from: 'residence_document_type' + fill_in 'residence_document_number', with: "12345678Z" + fill_in 'residence_year_of_birth', with: '1981' + + click_button 'Validate document' + + expect(page).to have_content 'The Census was unable to verify this document' + end + + end + +end diff --git a/spec/features/officing/results_spec.rb b/spec/features/officing/results_spec.rb new file mode 100644 index 000000000..1de3f4bdb --- /dev/null +++ b/spec/features/officing/results_spec.rb @@ -0,0 +1,159 @@ +require 'rails_helper' + +feature 'Officing Results' do + + background do + @poll_officer = create(:poll_officer) + @officer_assignment = create(:poll_officer_assignment, :final, officer: @poll_officer) + @poll = @officer_assignment.booth_assignment.poll + @poll.update(ends_at: 1.day.ago) + @question_1 = create(:poll_question, poll: @poll, valid_answers: "Yes,No") + @question_2 = create(:poll_question, poll: @poll, valid_answers: "Today,Tomorrow") + login_as(@poll_officer.user) + end + + scenario 'Only polls where user is officer for results are accessible' do + regular_officer_assignment_1 = create(:poll_officer_assignment, officer: @poll_officer) + regular_officer_assignment_2 = create(:poll_officer_assignment, officer: @poll_officer) + + not_allowed_poll_1 = create(:poll, :expired) + not_allowed_poll_2 = regular_officer_assignment_1.booth_assignment.poll + not_allowed_poll_2.update(ends_at: 1.day.ago) + not_allowed_poll_3 = regular_officer_assignment_2.booth_assignment.poll + + visit root_path + click_link 'Polling officers' + + expect(page).to have_content('Poll officing') + within('#side_menu') do + click_link 'Final recounts and results' + end + + expect(page).to_not have_content(not_allowed_poll_1.name) + expect(page).to_not have_content(not_allowed_poll_2.name) + expect(page).to_not have_content(not_allowed_poll_3.name) + expect(page).to have_content(@poll.name) + + visit new_officing_poll_result_path(not_allowed_poll_1) + expect(page).to have_content('You are allowed to add results for this poll') + end + + scenario 'Add results' do + visit officing_root_path + + within('#side_menu') do + click_link 'Final recounts and results' + end + + within("#poll_#{@poll.id}") do + expect(page).to have_content(@poll.name) + click_link 'Add results' + end + + expect(page).to_not have_content('Your results') + + booth_name = @officer_assignment.booth_assignment.booth.name + date = I18n.l(@poll.starts_at.to_date, format: :long) + select booth_name, from: 'officer_assignment_id' + select date, from: 'date' + + fill_in "questions[#{@question_1.id}][0]", with: '100' + fill_in "questions[#{@question_1.id}][1]", with: '200' + + fill_in "questions[#{@question_2.id}][0]", with: '333' + fill_in "questions[#{@question_2.id}][1]", with: '444' + + fill_in "whites", with: '66' + fill_in "nulls", with: '77' + + click_button 'Save' + + expect(page).to have_content('Your results') + + within("#results_#{@officer_assignment.booth_assignment_id}_#{@poll.starts_at.to_date.strftime('%Y%m%d')}") do + expect(page).to have_content(date) + expect(page).to have_content(booth_name) + end + end + + scenario 'Edit result' do + partial_result = create(:poll_partial_result, + officer_assignment: @officer_assignment, + booth_assignment: @officer_assignment.booth_assignment, + date: @poll.starts_at, + question: @question_1, + answer: @question_1.valid_answers[0], + author: @poll_officer.user, + amount: 7777) + + visit officing_poll_results_path(@poll, date: I18n.l(partial_result.date), booth_assignment_id: partial_result.booth_assignment_id) + + within("#question_#{@question_1.id}_0_result") { expect(page).to have_content('7777') } + + visit new_officing_poll_result_path(@poll) + + booth_name = partial_result.booth_assignment.booth.name + date = I18n.l(partial_result.date, format: :long) + select booth_name, from: 'officer_assignment_id' + select date, from: 'date' + + fill_in "questions[#{@question_1.id}][0]", with: '5555' + fill_in "questions[#{@question_1.id}][1]", with: '200' + fill_in "whites", with: '6' + fill_in "nulls", with: '7' + + click_button 'Save' + + within("#results_#{partial_result.booth_assignment_id}_#{partial_result.date.strftime('%Y%m%d')}") do + expect(page).to have_content(I18n.l(partial_result.date, format: :long)) + expect(page).to have_content(partial_result.booth_assignment.booth.name) + click_link "See results" + end + + expect(page).to_not have_content('7777') + within("#white_results") { expect(page).to have_content('6') } + within("#null_results") { expect(page).to have_content('7') } + within("#question_#{@question_1.id}_0_result") { expect(page).to have_content('5555') } + within("#question_#{@question_1.id}_1_result") { expect(page).to have_content('200') } + end + + scenario 'Index lists all questions and answers' do + partial_result = create(:poll_partial_result, + officer_assignment: @officer_assignment, + booth_assignment: @officer_assignment.booth_assignment, + date: @poll.ends_at, + question: @question_1, + amount: 33) + white_result = create(:poll_white_result, + officer_assignment: @officer_assignment, + booth_assignment: @officer_assignment.booth_assignment, + date: @poll.ends_at, + amount: 21) + null_result = create(:poll_null_result, + officer_assignment: @officer_assignment, + booth_assignment: @officer_assignment.booth_assignment, + date: @poll.ends_at, + amount: 44) + + visit officing_poll_results_path(@poll, + date: I18n.l(@poll.ends_at.to_date), + booth_assignment_id: @officer_assignment.booth_assignment_id) + + expect(page).to have_content(I18n.l(@poll.ends_at.to_date, format: :long)) + expect(page).to have_content(@officer_assignment.booth_assignment.booth.name) + + expect(page).to have_content(@question_1.title) + @question_1.valid_answers.each_with_index do |answer, i| + within("#question_#{@question_1.id}_#{i}_result") { expect(page).to have_content(answer) } + end + + expect(page).to have_content(@question_2.title) + @question_2.valid_answers.each_with_index do |answer, i| + within("#question_#{@question_2.id}_#{i}_result") { expect(page).to have_content(answer) } + end + + within('#white_results') { expect(page).to have_content('21') } + within('#null_results') { expect(page).to have_content('44') } + end + +end \ No newline at end of file diff --git a/spec/features/officing/voters_spec.rb b/spec/features/officing/voters_spec.rb new file mode 100644 index 000000000..0a23a6a27 --- /dev/null +++ b/spec/features/officing/voters_spec.rb @@ -0,0 +1,64 @@ +require 'rails_helper' + +feature 'Voters' do + + let(:officer) { create(:poll_officer) } + + background do + login_as(officer.user) + create(:geozone, :in_census) + end + + scenario "Can vote", :js do + poll = create(:poll_officer_assignment, officer: officer).booth_assignment.poll + + visit new_officing_residence_path + officing_verify_residence + + expect(page).to have_content "Polls" + expect(page).to have_content poll.name + + click_button "Confirm vote" + + expect(page).to have_content "Vote introduced!" + expect(page).to_not have_button "Confirm vote" + + page.evaluate_script("window.location.reload()") + expect(page).to have_content "Has already participated in this poll" + expect(page).to_not have_button "Confirm vote" + end + + scenario "Already voted", :js do + poll1 = create(:poll) + poll2 = create(:poll) + + user = create(:user, :level_two) + voter = create(:poll_voter, poll: poll1, user: user) + + visit new_officing_voter_path(id: voter.user.id) + + within("#poll_#{poll1.id}") do + expect(page).to have_content "Has already participated in this poll" + expect(page).to_not have_button "Confirm vote" + end + + within("#poll_#{poll2.id}") do + expect(page).to have_button "Confirm vote" + end + end + + scenario "Had already verified his residence, but is not level 2 yet", :js do + user = create(:user, residence_verified_at: Time.current, document_type: "1", document_number: "12345678Z") + expect(user).to_not be_level_two_verified + poll = create(:poll_officer_assignment, officer: officer).booth_assignment.poll + + visit new_officing_residence_path + officing_verify_residence + + expect(page).to have_content "Polls" + expect(page).to have_content poll.name + end + + #Fix and use answerable_by(user) + xscenario "Display only answerable polls" +end diff --git a/spec/features/officing_spec.rb b/spec/features/officing_spec.rb new file mode 100644 index 000000000..d4cb416a1 --- /dev/null +++ b/spec/features/officing_spec.rb @@ -0,0 +1,109 @@ +require 'rails_helper' + +feature 'Poll Officing' do + let(:user) { create(:user) } + + scenario 'Access as regular user is not authorized' do + login_as(user) + visit root_path + + expect(page).to_not have_link("Polling officers") + visit officing_root_path + + expect(current_path).not_to eq(officing_root_path) + expect(current_path).to eq(proposals_path) + expect(page).to have_content "You do not have permission to access this page" + end + + scenario 'Access as moderator is not authorized' do + create(:moderator, user: user) + login_as(user) + visit root_path + + expect(page).to_not have_link("Polling officers") + visit officing_root_path + + expect(current_path).not_to eq(officing_root_path) + expect(current_path).to eq(proposals_path) + expect(page).to have_content "You do not have permission to access this page" + end + + scenario 'Access as manager is not authorized' do + create(:manager, user: user) + login_as(user) + visit root_path + + expect(page).to_not have_link("Polling officers") + visit officing_root_path + + expect(current_path).not_to eq(officing_root_path) + expect(current_path).to eq(proposals_path) + expect(page).to have_content "You do not have permission to access this page" + end + + scenario 'Access as a valuator is not authorized' do + create(:valuator, user: user) + login_as(user) + visit root_path + + expect(page).to_not have_link("Polling officers") + visit officing_root_path + + expect(current_path).not_to eq(officing_root_path) + expect(current_path).to eq(proposals_path) + expect(page).to have_content "You do not have permission to access this page" + end + + scenario 'Access as an poll officer is authorized' do + create(:poll_officer, user: user) + create(:poll) + login_as(user) + visit root_path + + expect(page).to have_link("Polling officers") + click_on "Polling officers" + + expect(current_path).to eq(officing_root_path) + expect(page).to_not have_content "You do not have permission to access this page" + end + + scenario 'Access as an administrator is authorized' do + create(:administrator, user: user) + create(:poll) + login_as(user) + visit root_path + + expect(page).to have_link("Polling officers") + click_on "Polling officers" + + expect(current_path).to eq(officing_root_path) + expect(page).to_not have_content "You do not have permission to access this page" + end + + scenario "Poll officer access links" do + create(:poll_officer, user: user) + login_as(user) + visit root_path + + expect(page).to have_link("Polling officers") + expect(page).to_not have_link('Valuation') + expect(page).to_not have_link('Administration') + expect(page).to_not have_link('Moderation') + end + + scenario 'Officing dashboard' do + create(:poll_officer, user: user) + create(:poll) + login_as(user) + visit root_path + + click_link 'Polling officers' + + expect(current_path).to eq(officing_root_path) + expect(page).to have_css('#officing_menu') + expect(page).to_not have_css('#valuation_menu') + expect(page).to_not have_css('#admin_menu') + expect(page).to_not have_css('#moderation_menu') + end + +end \ No newline at end of file diff --git a/spec/features/polls/polls_spec.rb b/spec/features/polls/polls_spec.rb new file mode 100644 index 000000000..047c71146 --- /dev/null +++ b/spec/features/polls/polls_spec.rb @@ -0,0 +1,197 @@ +require 'rails_helper' + +feature 'Polls' do + + context '#index' do + + scenario 'Polls can be listed' do + polls = create_list(:poll, 3) + + visit polls_path + + polls.each do |poll| + expect(page).to have_content(poll.name) + expect(page).to have_link("Participate in this poll") + end + end + + scenario 'Filtering polls' do + create(:poll, name: "Current poll") + create(:poll, :incoming, name: "Incoming poll") + create(:poll, :expired, name: "Expired poll") + + visit polls_path + expect(page).to have_content('Current poll') + expect(page).to have_link('Participate in this poll') + expect(page).to_not have_content('Incoming poll') + expect(page).to_not have_content('Expired poll') + + visit polls_path(filter: 'incoming') + expect(page).to_not have_content('Current poll') + expect(page).to have_content('Incoming poll') + expect(page).to have_link('More information') + expect(page).to_not have_content('Expired poll') + + visit polls_path(filter: 'expired') + expect(page).to_not have_content('Current poll') + expect(page).to_not have_content('Incoming poll') + expect(page).to have_content('Expired poll') + expect(page).to have_link('Poll ended') + end + + scenario "Current filter is properly highlighted" do + visit polls_path + expect(page).to_not have_link('Open') + expect(page).to have_link('Incoming') + expect(page).to have_link('Expired') + + visit polls_path(filter: 'incoming') + expect(page).to have_link('Open') + expect(page).to_not have_link('Incoming') + expect(page).to have_link('Expired') + + visit polls_path(filter: 'expired') + expect(page).to have_link('Open') + expect(page).to have_link('Incoming') + expect(page).to_not have_link('Expired') + end + end + + context 'Show' do + let(:geozone) { create(:geozone) } + let(:poll) { create(:poll) } + + scenario 'Lists questions from proposals as well as regular ones' do + normal_question = create(:poll_question, poll: poll) + proposal_question = create(:poll_question, poll: poll, proposal: create(:proposal)) + + visit poll_path(poll) + expect(page).to have_content(poll.name) + + expect(page).to have_content(normal_question.title) + expect(page).to have_content(proposal_question.title) + end + + scenario 'Non-logged in users' do + create(:poll_question, poll: poll, valid_answers: 'Han Solo, Chewbacca') + visit poll_path(poll) + + expect(page).to have_content('Han Solo') + expect(page).to have_content('Chewbacca') + expect(page).to have_content('You must Sign in or Sign up to participate') + + expect(page).to_not have_link('Han Solo') + expect(page).to_not have_link('Chewbacca') + end + + scenario 'Level 1 users' do + poll.update(geozone_restricted: true) + poll.geozones << geozone + create(:poll_question, poll: poll, valid_answers: 'Han Solo, Chewbacca') + login_as(create(:user, geozone: geozone)) + visit poll_path(poll) + + expect(page).to have_content('You must verify your account in order to answer') + + expect(page).to have_content('Han Solo') + expect(page).to have_content('Chewbacca') + + expect(page).to_not have_link('Han Solo') + expect(page).to_not have_link('Chewbacca') + end + + scenario 'Level 2 users in an incoming poll' do + incoming_poll = create(:poll, :incoming, geozone_restricted: true) + incoming_poll.geozones << geozone + create(:poll_question, poll: incoming_poll, valid_answers: 'Rey, Finn') + login_as(create(:user, :level_two, geozone: geozone)) + + visit poll_path(incoming_poll) + + expect(page).to have_content('Rey') + expect(page).to have_content('Finn') + expect(page).to_not have_link('Rey') + expect(page).to_not have_link('Finn') + + expect(page).to have_content('This poll has not yet started') + end + + scenario 'Level 2 users in an expired poll' do + expired_poll = create(:poll, :expired, geozone_restricted: true) + expired_poll.geozones << geozone + create(:poll_question, poll: expired_poll, valid_answers: 'Luke, Leia') + login_as(create(:user, :level_two, geozone: geozone)) + + visit poll_path(expired_poll) + + expect(page).to have_content('Luke') + expect(page).to have_content('Leia') + expect(page).to_not have_link('Luke') + expect(page).to_not have_link('Leia') + + expect(page).to have_content('This poll has finished') + end + + scenario 'Level 2 users in a poll with questions for a geozone which is not theirs' do + poll.update(geozone_restricted: true) + poll.geozones << create(:geozone) + create(:poll_question, poll: poll, valid_answers: 'Vader, Palpatine') + login_as(create(:user, :level_two)) + + visit poll_path(poll) + + expect(page).to have_content('Vader') + expect(page).to have_content('Palpatine') + expect(page).to_not have_link('Vader') + expect(page).to_not have_link('Palpatine') + end + + scenario 'Level 2 users reading a same-geozone poll' do + poll.update(geozone_restricted: true) + poll.geozones << geozone + create(:poll_question, poll: poll, valid_answers: 'Han Solo, Chewbacca') + login_as(create(:user, :level_two, geozone: geozone)) + visit poll_path(poll) + + expect(page).to have_link('Han Solo') + expect(page).to have_link('Chewbacca') + end + + scenario 'Level 2 users reading a all-geozones poll' do + create(:poll_question, poll: poll, valid_answers: 'Han Solo, Chewbacca') + login_as(create(:user, :level_two)) + visit poll_path(poll) + + expect(page).to have_link('Han Solo') + expect(page).to have_link('Chewbacca') + end + + scenario 'Level 2 users who have already answered' do + question = create(:poll_question, poll: poll, valid_answers: 'Han Solo, Chewbacca') + user = create(:user, :level_two) + create(:poll_answer, question: question, author: user, answer: 'Chewbacca') + + login_as user + visit poll_path(poll) + + expect(page).to have_link('Han Solo') + expect(page).to_not have_link('Chewbacca') + expect(page).to have_content('Chewbacca') + end + + scenario 'Level 2 users answering', :js do + poll.update(geozone_restricted: true) + poll.geozones << geozone + create(:poll_question, poll: poll, valid_answers: 'Han Solo, Chewbacca') + user = create(:user, :level_two, geozone: geozone) + login_as user + visit poll_path(poll) + + click_link 'Han Solo' + + expect(page).to_not have_link('Han Solo') + expect(page).to have_link('Chewbacca') + end + + end +end diff --git a/spec/features/polls/questions_spec.rb b/spec/features/polls/questions_spec.rb new file mode 100644 index 000000000..6f2225175 --- /dev/null +++ b/spec/features/polls/questions_spec.rb @@ -0,0 +1,112 @@ +require 'rails_helper' + +feature 'Poll Questions' do + + scenario 'Lists questions from proposals before regular questions' do + poll = create(:poll) + normal_question = create(:poll_question, poll: poll) + proposal_question = create(:poll_question, proposal: create(:proposal), poll: poll) + + visit poll_path(poll) + + expect(proposal_question.title).to appear_before(normal_question.title) + end + + scenario 'shows the author visible name instead of a link to the author' do + poll = create(:poll) + question_with_author = create(:poll_question, poll: poll) + question_with_author_visible_name = create(:poll_question, poll: poll, author_visible_name: 'potato') + + visit question_path(question_with_author) + expect(page).to have_link(question_with_author.author.name) + + visit question_path(question_with_author_visible_name) + expect(page).to_not have_link(question_with_author_visible_name.author.name) + expect(page).to have_content(question_with_author_visible_name.author_visible_name) + end + + context 'Answering' do + let(:geozone) { create(:geozone) } + let(:poll) { create(:poll, geozone_restricted: true, geozone_ids: [geozone.id]) } + + scenario 'Non-logged in users' do + question = create(:poll_question, valid_answers: 'Han Solo, Chewbacca') + + visit question_path(question) + + expect(page).to have_content('You must Sign in or Sign up to participate') + end + + scenario 'Level 1 users' do + question = create(:poll_question, poll: poll, valid_answers: 'Han Solo, Chewbacca') + + login_as(create(:user, geozone: geozone)) + visit question_path(question) + + expect(page).to have_content('You must verify your account in order to answer') + end + + scenario 'Level 2 users in an poll question for a geozone which is not theirs' do + + other_poll = create(:poll, geozone_restricted: true, geozone_ids: [create(:geozone).id]) + question = create(:poll_question, poll: other_poll, valid_answers: 'Vader, Palpatine') + + login_as(create(:user, :level_two, geozone: geozone)) + visit question_path(question) + + expect(page).to have_content('This question is not available on your geozone') + end + + scenario 'Level 2 users who can answer' do + question = create(:poll_question, poll: poll, valid_answers: 'Han Solo, Chewbacca') + + login_as(create(:user, :level_two, geozone: geozone)) + visit question_path(question) + + expect(page).to have_link('Answer this question') + end + + scenario 'Level 2 users who have already answered' do + question = create(:poll_question, poll: poll, valid_answers: 'Han Solo, Chewbacca') + + user = create(:user, :level_two, geozone: geozone) + create(:poll_answer, question: question, author: user, answer: 'Chewbacca') + + login_as user + visit question_path(question) + + expect(page).to have_link('Answer this question') + end + + scenario 'Level 2 users answering', :js do + question = create(:poll_question, poll: poll, valid_answers: 'Han Solo, Chewbacca') + user = create(:user, :level_two, geozone: geozone) + + login_as user + visit question_path(question) + + expect(page).to have_link('Answer this question') + end + + scenario 'Records participation', :js do + question = create(:poll_question, poll: poll, valid_answers: 'Han Solo, Chewbacca') + user = create(:user, :level_two, geozone: geozone, gender: 'female', date_of_birth: 33.years.ago) + + login_as user + visit question_path(question) + + click_link 'Answer this question' + click_link 'Han Solo' + + expect(page).to_not have_link('Han Solo') + + voter = poll.voters.first + expect(voter.document_number).to eq(user.document_number) + expect(voter.geozone_id).to eq(user.geozone_id) + expect(voter.gender).to eq(user.gender) + expect(voter.age).to eq(33) + expect(voter.poll_id).to eq(poll.id) + end + + end +end diff --git a/spec/features/proposal_ballots_spec.rb b/spec/features/proposal_ballots_spec.rb index 01ba71bf7..8f5e99eb8 100644 --- a/spec/features/proposal_ballots_spec.rb +++ b/spec/features/proposal_ballots_spec.rb @@ -10,7 +10,7 @@ feature 'Proposal ballots' do expect(page).to_not have_css("#next-voting") expect(page).to have_css("#featured-proposals") - create_successfull_proposals + create_successful_proposals visit proposals_path @@ -18,22 +18,22 @@ feature 'Proposal ballots' do expect(page).to_not have_css("#featured-proposals") end - scenario 'Successfull proposals do not show support buttons in index' do - successfull_proposals = create_successfull_proposals + scenario 'Successful proposals do not show support buttons in index' do + successful_proposals = create_successful_proposals visit proposals_path - successfull_proposals.each do |proposal| + successful_proposals.each do |proposal| within("#proposal_#{proposal.id}_votes") do expect(page).to have_content "This proposal has reached the required supports" end end end - scenario 'Successfull proposals do not show support buttons in show' do - successfull_proposals = create_successfull_proposals + scenario 'Successful proposals do not show support buttons in show' do + successful_proposals = create_successful_proposals - successfull_proposals.each do |proposal| + successful_proposals.each do |proposal| visit proposal_path(proposal) within("#proposal_#{proposal.id}_votes") do expect(page).to have_content "This proposal has reached the required supports" @@ -41,15 +41,5 @@ feature 'Proposal ballots' do end end - scenario 'Successfull proposals are listed in the proposal ballots index' do - successfull_proposals = create_successfull_proposals - - visit proposal_ballots_path - - successfull_proposals.each do |proposal| - expect(page).to have_content(proposal.title) - end - end - end diff --git a/spec/features/proposals_spec.rb b/spec/features/proposals_spec.rb index 18864dc9b..843b74bf5 100644 --- a/spec/features/proposals_spec.rb +++ b/spec/features/proposals_spec.rb @@ -63,7 +63,7 @@ feature 'Proposals' do expect(page.html).to include "#{proposal.title}" within('.social-share-button') do - expect(page.all('a').count).to be(3) # Twitter, Facebook, Google+ + expect(page.all('a').count).to be(4) # Twitter, Facebook, Google+, Telegram end end @@ -980,6 +980,28 @@ feature 'Proposals' do end end + scenario "Search by custom invalid date range", :js do + proposal1 = create(:proposal, created_at: 2.days.ago) + proposal2 = create(:proposal, created_at: 3.days.ago) + proposal3 = create(:proposal, created_at: 9.days.ago) + + visit proposals_path + + click_link "Advanced search" + select "Customized", from: "js-advanced-search-date-min" + fill_in "advanced_search_date_min", with: 4000.years.ago + fill_in "advanced_search_date_max", with: "wrong date" + click_button "Filter" + + expect(page).to have_content("There are 3 citizen proposals") + + within("#proposals") do + expect(page).to have_content(proposal1.title) + expect(page).to have_content(proposal2.title) + expect(page).to have_content(proposal3.title) + end + end + scenario "Search by multiple filters", :js do ana = create :user, official_level: 1 john = create :user, official_level: 1 @@ -1350,3 +1372,69 @@ feature 'Proposals' do end end + +feature 'Successful proposals' do + + scenario 'Banner shows in proposal index' do + create_featured_proposals + + visit proposals_path + expect(page).to_not have_css("#next-voting") + expect(page).to have_css("#featured-proposals") + + create_successful_proposals + + visit proposals_path + + expect(page).to have_css("#next-voting") + expect(page).to_not have_css("#featured-proposals") + end + + scenario 'Successful proposals do not show support buttons in index' do + successful_proposals = create_successful_proposals + + visit proposals_path + + successful_proposals.each do |proposal| + within("#proposal_#{proposal.id}_votes") do + expect(page).to_not have_css(".supports") + expect(page).to have_content "This proposal has reached the required supports" + end + end + end + + scenario 'Successful proposals do not show support buttons in show' do + successful_proposals = create_successful_proposals + + successful_proposals.each do |proposal| + visit proposal_path(proposal) + within("#proposal_#{proposal.id}_votes") do + expect(page).to_not have_css(".supports") + expect(page).to have_content "This proposal has reached the required supports" + end + end + end + + scenario 'Successful proposals show create question button to admin users' do + successful_proposals = create_successful_proposals + + visit proposals_path + + successful_proposals.each do |proposal| + within("#proposal_#{proposal.id}_votes") do + expect(page).to_not have_link "Create question" + end + end + + login_as(create(:administrator).user) + + visit proposals_path + + successful_proposals.each do |proposal| + within("#proposal_#{proposal.id}_votes") do + expect(page).to have_link "Create question" + end + end + + end +end \ No newline at end of file diff --git a/spec/features/site_customization/content_blocks_spec.rb b/spec/features/site_customization/content_blocks_spec.rb new file mode 100644 index 000000000..995982063 --- /dev/null +++ b/spec/features/site_customization/content_blocks_spec.rb @@ -0,0 +1,33 @@ +require 'rails_helper' + +feature "Custom content blocks" do + scenario "top links" do + create(:site_customization_content_block, name: "top_links", locale: "en", body: "content for top links") + create(:site_customization_content_block, name: "top_links", locale: "es", body: "contenido para top links") + + visit "/?locale=en" + + expect(page).to have_content("content for top links") + expect(page).to_not have_content("contenido para top links") + + visit "/?locale=es" + + expect(page).to have_content("contenido para top links") + expect(page).to_not have_content("content for top links") + end + + scenario "footer" do + create(:site_customization_content_block, name: "footer", locale: "en", body: "content for footer") + create(:site_customization_content_block, name: "footer", locale: "es", body: "contenido para footer") + + visit "/?locale=en" + + expect(page).to have_content("content for footer") + expect(page).to_not have_content("contenido para footer") + + visit "/?locale=es" + + expect(page).to have_content("contenido para footer") + expect(page).to_not have_content("content for footer") + end +end diff --git a/spec/features/site_customization/custom_pages_spec.rb b/spec/features/site_customization/custom_pages_spec.rb new file mode 100644 index 000000000..2e2fac84a --- /dev/null +++ b/spec/features/site_customization/custom_pages_spec.rb @@ -0,0 +1,102 @@ +require 'rails_helper' + +feature "Custom Pages" do + context "Override existing page" do + scenario "See default content when custom page is not published" do + custom_page = create(:site_customization_page, + slug: "conditions", + title: "Custom conditions", + content: "New text for conditions page", + print_content_flag: true + ) + + visit custom_page.url + + expect(page).to have_title("Terms of use") + expect(page).to have_selector("h1", text: "Terms and conditions of use") + expect(page).to have_content("Página de información sobre las condiciones de uso, privacidad y protección de datos personales.") + expect(page).to have_content("Print this info") + end + + scenario "See custom content when custom page is published" do + custom_page = create(:site_customization_page, :published, + slug: "conditions", + title: "Custom conditions", + content: "New text for conditions page", + print_content_flag: true + ) + + visit custom_page.url + + expect(page).to have_title("Custom conditions") + expect(page).to have_selector("h1", text: "Custom conditions") + expect(page).to have_content("New text for conditions page") + expect(page).to have_content("Print this info") + end + end + + context "New custom page" do + context "Draft" do + scenario "See page" do + custom_page = create(:site_customization_page, + slug: "other-slug", + title: "Custom page", + content: "Text for new custom page", + print_content_flag: false + ) + + visit custom_page.url + + expect(page.status_code).to eq(404) + end + end + + context "Published" do + scenario "See page" do + custom_page = create(:site_customization_page, :published, + slug: "other-slug", + title: "Custom page", + content: "Text for new custom page", + print_content_flag: false + ) + + visit custom_page.url + + expect(page).to have_title("Custom page") + expect(page).to have_selector("h1", text: "Custom page") + expect(page).to have_content("Text for new custom page") + expect(page).to_not have_content("Print this info") + end + + scenario "Listed in more information page" do + custom_page = create(:site_customization_page, :published, + slug: "another-slug", title: "Another custom page", + subtitle: "Subtitle for custom page", + more_info_flag: true + ) + + visit more_info_path + + expect(page).to have_content("Another custom page") + end + + scenario "Not listed in more information page" do + custom_page = create(:site_customization_page, :published, + slug: "another-slug", title: "Another custom page", + subtitle: "Subtitle for custom page", + more_info_flag: false + ) + + visit more_info_path + + expect(page).to_not have_content("Another custom page") + + visit custom_page.url + + expect(page).to have_title("Another custom page") + expect(page).to have_selector("h1", text: "Another custom page") + expect(page).to have_content("Subtitle for custom page") + end + end + end +end diff --git a/spec/features/tags/budget_investments_spec.rb b/spec/features/tags/budget_investments_spec.rb index 28d19333f..4c978c740 100644 --- a/spec/features/tags/budget_investments_spec.rb +++ b/spec/features/tags/budget_investments_spec.rb @@ -199,11 +199,12 @@ feature 'Tags' do if budget.balloting? [investment1, investment2, investment3].each do |investment| - investment.update(selected: true) + investment.update(selected: true, feasibility: "feasible") end end - visit budget_investments_path(budget, heading_id: heading.id) + visit budget_path(budget) + click_link group.name within "#tag-cloud" do click_link "Medio Ambiente" @@ -246,11 +247,12 @@ feature 'Tags' do if budget.balloting? [investment1, investment2, investment3].each do |investment| - investment.update(selected: true) + investment.update(selected: true, feasibility: "feasible") end end - visit budget_investments_path(budget, heading_id: heading.id, search: 'Economía') + visit budget_path(budget) + click_link group.name within "#categories" do click_link "Medio Ambiente" diff --git a/spec/features/valuation/budget_investments_spec.rb b/spec/features/valuation/budget_investments_spec.rb index bac7d1fe2..f6e930cc6 100644 --- a/spec/features/valuation/budget_investments_spec.rb +++ b/spec/features/valuation/budget_investments_spec.rb @@ -13,6 +13,12 @@ feature 'Valuation budget investments' do expect{ visit valuation_budget_budget_investments_path(create(:budget)) }.to raise_exception(FeatureFlags::FeatureDisabled) end + scenario 'Display link to valuation section' do + Setting['feature.budgets'] = true + visit root_path + expect(page).to have_link "Valuation", href: valuation_root_path + end + scenario 'Index shows budget investments assigned to current valuator' do investment1 = create(:budget_investment, budget: @budget) investment2 = create(:budget_investment, budget: @budget) diff --git a/spec/features/valuation_spec.rb b/spec/features/valuation_spec.rb index 2dcdc91c1..a0a4c9ba5 100644 --- a/spec/features/valuation_spec.rb +++ b/spec/features/valuation_spec.rb @@ -46,6 +46,19 @@ feature 'Valuation' do expect(page).to have_content "You do not have permission to access this page" end + scenario 'Access as poll officer is not authorized' do + create(:poll_officer, user: user) + login_as(user) + visit root_path + + expect(page).to_not have_link("Valuation") + visit valuation_root_path + + expect(current_path).not_to eq(valuation_root_path) + expect(current_path).to eq(proposals_path) + expect(page).to have_content "You do not have permission to access this page" + end + scenario 'Access as a valuator is authorized' do create(:valuator, user: user) login_as(user) diff --git a/spec/features/verification/residence_spec.rb b/spec/features/verification/residence_spec.rb index cdf33dfab..ed2ffd083 100644 --- a/spec/features/verification/residence_spec.rb +++ b/spec/features/verification/residence_spec.rb @@ -22,6 +22,31 @@ feature 'Residence' do expect(page).to have_content 'Residence verified' end + scenario 'When trying to verify a deregistered account old votes are reassigned' do + erased_user = create(:user, document_number: '12345678Z', document_type: '1', erased_at: Time.current) + vote = create(:vote, voter: erased_user) + new_user = create(:user) + + login_as(new_user) + + visit account_path + click_link 'Verify my account' + + fill_in 'residence_document_number', with: '12345678Z' + select 'DNI', from: 'residence_document_type' + select_date '31-December-1980', from: 'residence_date_of_birth' + fill_in 'residence_postal_code', with: '28013' + check 'residence_terms_of_service' + + click_button 'Verify residence' + + expect(page).to have_content 'Residence verified' + + expect(vote.reload.voter).to eq(new_user) + expect(erased_user.reload.document_number).to be_blank + expect(new_user.reload.document_number).to eq('12345678Z') + end + scenario 'Error on verify' do user = create(:user) login_as(user) @@ -102,24 +127,4 @@ feature 'Residence' do expect(page).to have_content "You have reached the maximum number of attempts. Please try again later." expect(current_path).to eq(account_path) end - - scenario 'Error when trying to verify a deregistered account' do - create(:user, document_number: '12345678Z', document_type: '1', erased_at: Time.now) - - login_as(create(:user)) - - visit account_path - click_link 'Verify my account' - - fill_in 'residence_document_number', with: "12345678Z" - select 'DNI', from: 'residence_document_type' - select_date '31-December-1980', from: 'residence_date_of_birth' - fill_in 'residence_postal_code', with: '28013' - check 'residence_terms_of_service' - - click_button 'Verify residence' - - expect(page).to_not have_content 'Residence verified' - expect(page).to have_content 'has already been taken' - end end diff --git a/spec/fixtures/files/logo_header.png b/spec/fixtures/files/logo_header.png new file mode 100644 index 000000000..5f5355574 Binary files /dev/null and b/spec/fixtures/files/logo_header.png differ diff --git a/spec/fixtures/files/social-media-icon.png b/spec/fixtures/files/social-media-icon.png new file mode 100644 index 000000000..fcd00e212 Binary files /dev/null and b/spec/fixtures/files/social-media-icon.png differ diff --git a/spec/helpers/admin_helper_spec.rb b/spec/helpers/admin_helper_spec.rb new file mode 100644 index 000000000..0239f1926 --- /dev/null +++ b/spec/helpers/admin_helper_spec.rb @@ -0,0 +1,19 @@ +require 'rails_helper' + +describe AdminHelper do + + describe "#admin_submit_action" do + + it "returns new when the the resource has not been persisted" do + poll = build(:poll) + expect(admin_submit_action(poll)).to eq("new") + end + + it "returns edit when the the resource has been persisted" do + poll = create(:poll) + expect(admin_submit_action(poll)).to eq("edit") + end + + end + +end \ No newline at end of file diff --git a/spec/lib/age_spec.rb b/spec/lib/age_spec.rb new file mode 100644 index 000000000..6e1012fd4 --- /dev/null +++ b/spec/lib/age_spec.rb @@ -0,0 +1,58 @@ +require 'rails_helper' + +describe Age do + describe '.in_years' do + it "handles nils" do + expect(Age.in_years(nil)).to be_nil + end + + it "calculates age correctly for common dates" do + d = Date.new(1980, 3, 13) + expect(Age.in_years(d, Date.new(2000, 3, 12))).to eq(19) + expect(Age.in_years(d, Date.new(2000, 3, 13))).to eq(20) + expect(Age.in_years(d, Date.new(2000, 3, 14))).to eq(20) + end + + it "calculates age correctly for people born near a year's limit" do + d = Date.new(1980, 12, 31) + expect(Age.in_years(d, Date.new(2000, 12, 30))).to eq(19) + expect(Age.in_years(d, Date.new(2000, 12, 31))).to eq(20) + expect(Age.in_years(d, Date.new(2001, 1, 1))).to eq(20) + + d = Date.new(1980, 1, 1) + expect(Age.in_years(d, Date.new(2000, 12, 31))).to eq(20) + expect(Age.in_years(d, Date.new(2001, 1, 1))).to eq(21) + expect(Age.in_years(d, Date.new(2001, 1, 2))).to eq(21) + end + + it "calculates age correctly for people born around February the 29th" do + # 1980 and 2000 are leap years. 2001 is a regular year + d = Date.new(1980, 2, 29) + expect(Age.in_years(d, Date.new(2000, 2, 27))).to eq(19) + expect(Age.in_years(d, Date.new(2000, 2, 28))).to eq(19) + expect(Age.in_years(d, Date.new(2000, 2, 29))).to eq(20) + expect(Age.in_years(d, Date.new(2000, 3, 1))).to eq(20) + expect(Age.in_years(d, Date.new(2001, 2, 27))).to eq(20) + expect(Age.in_years(d, Date.new(2001, 2, 28))).to eq(20) + expect(Age.in_years(d, Date.new(2001, 3, 1))).to eq(21) + + d = Date.new(1980, 2, 28) + expect(Age.in_years(d, Date.new(2000, 2, 27))).to eq(19) + expect(Age.in_years(d, Date.new(2000, 2, 28))).to eq(20) + expect(Age.in_years(d, Date.new(2000, 2, 29))).to eq(20) + expect(Age.in_years(d, Date.new(2000, 3, 1))).to eq(20) + expect(Age.in_years(d, Date.new(2001, 2, 27))).to eq(20) + expect(Age.in_years(d, Date.new(2001, 2, 28))).to eq(21) + expect(Age.in_years(d, Date.new(2001, 3, 1))).to eq(21) + + d = Date.new(1980, 3, 1) + expect(Age.in_years(d, Date.new(2000, 2, 27))).to eq(19) + expect(Age.in_years(d, Date.new(2000, 2, 28))).to eq(19) + expect(Age.in_years(d, Date.new(2000, 2, 29))).to eq(19) + expect(Age.in_years(d, Date.new(2000, 3, 1))).to eq(20) + expect(Age.in_years(d, Date.new(2001, 2, 27))).to eq(20) + expect(Age.in_years(d, Date.new(2001, 2, 28))).to eq(20) + expect(Age.in_years(d, Date.new(2001, 3, 1))).to eq(21) + end + end +end diff --git a/spec/lib/email_digests_spec.rb b/spec/lib/email_digests_spec.rb index 76efffe2e..6f15a92d4 100644 --- a/spec/lib/email_digests_spec.rb +++ b/spec/lib/email_digests_spec.rb @@ -122,6 +122,19 @@ describe EmailDigest do expect(notification3.emailed_at).to_not be end + it "resets the failed_email_digests_count flag" do + user1 = create(:user, failed_email_digests_count: 0) + user2 = create(:user, failed_email_digests_count: 3) + + email_digest_1 = EmailDigest.new(user1) + email_digest_2 = EmailDigest.new(user2) + email_digest_1.mark_as_emailed + email_digest_2.mark_as_emailed + + expect(user1.failed_email_digests_count).to eq(0) + expect(user2.failed_email_digests_count).to eq(0) + end + end end diff --git a/spec/lib/spending_proposals_importer_spec.rb b/spec/lib/migrate_spending_proposals_to_investments_spec.rb similarity index 96% rename from spec/lib/spending_proposals_importer_spec.rb rename to spec/lib/migrate_spending_proposals_to_investments_spec.rb index d8cb50082..a87d8cacc 100644 --- a/spec/lib/spending_proposals_importer_spec.rb +++ b/spec/lib/migrate_spending_proposals_to_investments_spec.rb @@ -1,8 +1,8 @@ require 'rails_helper' -describe SpendingProposalsImporter do +describe MigrateSpendingProposalsToInvestments do - let(:importer) { SpendingProposalsImporter.new } + let(:importer) { MigrateSpendingProposalsToInvestments.new } describe '#import' do diff --git a/spec/models/abilities/administrator_spec.rb b/spec/models/abilities/administrator_spec.rb index 8c4e8a1ef..5312221b8 100644 --- a/spec/models/abilities/administrator_spec.rb +++ b/spec/models/abilities/administrator_spec.rb @@ -67,5 +67,4 @@ describe "Abilities::Administrator" do it { should be_able_to(:valuate, create(:budget_investment, budget: create(:budget, phase: 'valuating'))) } it { should be_able_to(:valuate, create(:budget_investment, budget: create(:budget, phase: 'finished'))) } - end diff --git a/spec/models/abilities/common_spec.rb b/spec/models/abilities/common_spec.rb index ce90ab967..7c02f50fd 100644 --- a/spec/models/abilities/common_spec.rb +++ b/spec/models/abilities/common_spec.rb @@ -3,128 +3,215 @@ require 'cancan/matchers' describe "Abilities::Common" do subject(:ability) { Ability.new(user) } + let(:geozone) { create(:geozone) } - let(:user) { create(:user) } + let(:user) { create(:user, geozone: geozone) } + + let(:debate) { create(:debate) } + let(:comment) { create(:comment) } + let(:proposal) { create(:proposal) } + let(:own_debate) { create(:debate, author: user) } + let(:own_comment) { create(:comment, author: user) } + let(:own_proposal) { create(:proposal, author: user) } - let(:debate) { create(:debate) } - let(:comment) { create(:comment) } - let(:proposal) { create(:proposal) } let(:accepting_budget) { create(:budget, phase: 'accepting') } + let(:reviewing_budget) { create(:budget, phase: 'reviewing') } let(:selecting_budget) { create(:budget, phase: 'selecting') } let(:balloting_budget) { create(:budget, phase: 'balloting') } let(:investment_in_accepting_budget) { create(:budget_investment, budget: accepting_budget) } + let(:investment_in_reviewing_budget) { create(:budget_investment, budget: reviewing_budget) } let(:investment_in_selecting_budget) { create(:budget_investment, budget: selecting_budget) } let(:investment_in_balloting_budget) { create(:budget_investment, budget: balloting_budget) } + let(:own_investment_in_accepting_budget) { create(:budget_investment, budget: accepting_budget, author: user) } + let(:own_investment_in_reviewing_budget) { create(:budget_investment, budget: reviewing_budget, author: user) } + let(:own_investment_in_selecting_budget) { create(:budget_investment, budget: selecting_budget, author: user) } + let(:own_investment_in_balloting_budget) { create(:budget_investment, budget: balloting_budget, author: user) } let(:ballot_in_accepting_budget) { create(:budget_ballot, budget: accepting_budget) } let(:ballot_in_selecting_budget) { create(:budget_ballot, budget: selecting_budget) } let(:ballot_in_balloting_budget) { create(:budget_ballot, budget: balloting_budget) } - let(:own_debate) { create(:debate, author: user) } - let(:own_comment) { create(:comment, author: user) } - let(:own_proposal) { create(:proposal, author: user) } + + let(:current_poll) { create(:poll) } + let(:incoming_poll) { create(:poll, :incoming) } + let(:incoming_poll_from_own_geozone) { create(:poll, :incoming, geozone_restricted: true, geozones: [geozone]) } + let(:incoming_poll_from_other_geozone) { create(:poll, :incoming, geozone_restricted: true, geozones: [create(:geozone)]) } + let(:expired_poll) { create(:poll, :expired) } + let(:expired_poll_from_own_geozone) { create(:poll, :expired, geozone_restricted: true, geozones: [geozone]) } + let(:expired_poll_from_other_geozone) { create(:poll, :expired, geozone_restricted: true, geozones: [create(:geozone)]) } + let(:poll) { create(:poll, geozone_restricted: false) } + let(:poll_from_own_geozone) { create(:poll, geozone_restricted: true, geozones: [geozone]) } + let(:poll_from_other_geozone) { create(:poll, geozone_restricted: true, geozones: [create(:geozone)]) } + + let(:poll_question_from_own_geozone) { create(:poll_question, poll: poll_from_own_geozone) } + let(:poll_question_from_other_geozone) { create(:poll_question, poll: poll_from_other_geozone) } + let(:poll_question_from_all_geozones) { create(:poll_question, poll: poll) } + + let(:expired_poll_question_from_own_geozone) { create(:poll_question, poll: expired_poll_from_own_geozone) } + let(:expired_poll_question_from_other_geozone) { create(:poll_question, poll: expired_poll_from_other_geozone) } + let(:expired_poll_question_from_all_geozones) { create(:poll_question, poll: expired_poll) } + + let(:incoming_poll_question_from_own_geozone) { create(:poll_question, poll: incoming_poll_from_own_geozone) } + let(:incoming_poll_question_from_other_geozone) { create(:poll_question, poll: incoming_poll_from_other_geozone) } + let(:incoming_poll_question_from_all_geozones) { create(:poll_question, poll: incoming_poll) } it { should be_able_to(:index, Debate) } - it { should be_able_to(:show, debate) } - it { should be_able_to(:vote, debate) } + it { should be_able_to(:show, debate) } + it { should be_able_to(:vote, debate) } it { should be_able_to(:show, user) } it { should be_able_to(:edit, user) } it { should be_able_to(:create, Comment) } - it { should be_able_to(:vote, Comment) } + it { should be_able_to(:vote, Comment) } - it { should be_able_to(:index, Proposal) } - it { should be_able_to(:show, proposal) } + it { should be_able_to(:index, Proposal) } + it { should be_able_to(:show, proposal) } it { should_not be_able_to(:vote, Proposal) } it { should_not be_able_to(:vote_featured, Proposal) } - it { should be_able_to(:index, SpendingProposal) } - it { should_not be_able_to(:create, SpendingProposal) } + it { should be_able_to(:index, SpendingProposal) } + it { should_not be_able_to(:create, SpendingProposal) } it { should_not be_able_to(:destroy, SpendingProposal) } - it { should_not be_able_to(:comment_as_administrator, debate) } - it { should_not be_able_to(:comment_as_moderator, debate) } + it { should_not be_able_to(:comment_as_administrator, debate) } + it { should_not be_able_to(:comment_as_moderator, debate) } it { should_not be_able_to(:comment_as_administrator, proposal) } - it { should_not be_able_to(:comment_as_moderator, proposal) } + it { should_not be_able_to(:comment_as_moderator, proposal) } - it { should be_able_to(:new, DirectMessage) } + it { should be_able_to(:new, DirectMessage) } it { should_not be_able_to(:create, DirectMessage) } - it { should_not be_able_to(:show, DirectMessage) } + it { should_not be_able_to(:show, DirectMessage) } describe 'flagging content' do - it { should be_able_to(:flag, debate) } + it { should be_able_to(:flag, debate) } it { should be_able_to(:unflag, debate) } - it { should be_able_to(:flag, comment) } + it { should be_able_to(:flag, comment) } it { should be_able_to(:unflag, comment) } - it { should be_able_to(:flag, proposal) } + it { should be_able_to(:flag, proposal) } it { should be_able_to(:unflag, proposal) } describe "own content" do - it { should_not be_able_to(:flag, own_comment) } + it { should_not be_able_to(:flag, own_comment) } it { should_not be_able_to(:unflag, own_comment) } - it { should_not be_able_to(:flag, own_debate) } + it { should_not be_able_to(:flag, own_debate) } it { should_not be_able_to(:unflag, own_debate) } - it { should_not be_able_to(:flag, own_proposal) } + it { should_not be_able_to(:flag, own_proposal) } it { should_not be_able_to(:unflag, own_proposal) } end end describe "other users" do let(:other_user) { create(:user) } - it { should be_able_to(:show, other_user) } + + it { should be_able_to(:show, other_user) } it { should_not be_able_to(:edit, other_user) } end describe "editing debates" do let(:own_debate_non_editable) { create(:debate, author: user) } + before { allow(own_debate_non_editable).to receive(:editable?).and_return(false) } - it { should be_able_to(:edit, own_debate) } - it { should_not be_able_to(:edit, debate) } # Not his + it { should be_able_to(:edit, own_debate) } + it { should_not be_able_to(:edit, debate) } # Not his it { should_not be_able_to(:edit, own_debate_non_editable) } end describe "editing proposals" do let(:own_proposal_non_editable) { create(:proposal, author: user) } + before { allow(own_proposal_non_editable).to receive(:editable?).and_return(false) } - it { should be_able_to(:edit, own_proposal) } - it { should_not be_able_to(:edit, proposal) } # Not his + it { should be_able_to(:edit, own_proposal) } + it { should_not be_able_to(:edit, proposal) } # Not his it { should_not be_able_to(:edit, own_proposal_non_editable) } end describe "when level 2 verified" do let(:own_spending_proposal) { create(:spending_proposal, author: user) } + let(:own_direct_message) { create(:direct_message, sender: user) } before{ user.update(residence_verified_at: Time.current, confirmed_phone: "1") } - it { should be_able_to(:vote, Proposal) } - it { should be_able_to(:vote_featured, Proposal) } + describe "Proposal" do + it { should be_able_to(:vote, Proposal) } + it { should be_able_to(:vote_featured, Proposal) } + end - it { should be_able_to(:create, SpendingProposal) } - it { should_not be_able_to(:destroy, create(:spending_proposal)) } - it { should_not be_able_to(:destroy, own_spending_proposal) } + describe "Spending Proposal" do + it { should be_able_to(:create, SpendingProposal) } + it { should_not be_able_to(:destroy, create(:spending_proposal)) } + it { should_not be_able_to(:destroy, own_spending_proposal) } + end - it { should be_able_to(:create, investment_in_accepting_budget) } - it { should_not be_able_to(:create, investment_in_selecting_budget) } - it { should_not be_able_to(:create, investment_in_balloting_budget) } + describe "Direct Message" do + it { should be_able_to(:new, DirectMessage) } + it { should be_able_to(:create, DirectMessage) } + it { should be_able_to(:show, own_direct_message) } + it { should_not be_able_to(:show, create(:direct_message)) } + end - it { should_not be_able_to(:vote, investment_in_accepting_budget) } - it { should be_able_to(:vote, investment_in_selecting_budget) } - it { should_not be_able_to(:vote, investment_in_balloting_budget) } + describe "Poll" do + it { should be_able_to(:answer, current_poll) } + it { should_not be_able_to(:answer, expired_poll) } + it { should_not be_able_to(:answer, incoming_poll) } - it { should_not be_able_to(:create, ballot_in_accepting_budget) } - it { should_not be_able_to(:create, ballot_in_selecting_budget) } - it { should be_able_to(:create, ballot_in_balloting_budget) } + it { should be_able_to(:answer, poll_question_from_own_geozone) } + it { should be_able_to(:answer, poll_question_from_all_geozones) } + it { should_not be_able_to(:answer, poll_question_from_other_geozone) } - it { should be_able_to(:new, DirectMessage) } - it { should be_able_to(:create, DirectMessage) } - it { should be_able_to(:show, own_direct_message) } - it { should_not be_able_to(:show, create(:direct_message)) } + it { should_not be_able_to(:answer, expired_poll_question_from_own_geozone) } + it { should_not be_able_to(:answer, expired_poll_question_from_all_geozones) } + it { should_not be_able_to(:answer, expired_poll_question_from_other_geozone) } + + it { should_not be_able_to(:answer, incoming_poll_question_from_own_geozone) } + it { should_not be_able_to(:answer, incoming_poll_question_from_all_geozones) } + it { should_not be_able_to(:answer, incoming_poll_question_from_other_geozone) } + + context "without geozone" do + before(:each) { user.geozone = nil } + + it { should_not be_able_to(:answer, poll_question_from_own_geozone) } + it { should be_able_to(:answer, poll_question_from_all_geozones) } + it { should_not be_able_to(:answer, poll_question_from_other_geozone) } + + it { should_not be_able_to(:answer, expired_poll_question_from_own_geozone) } + it { should_not be_able_to(:answer, expired_poll_question_from_all_geozones) } + it { should_not be_able_to(:answer, expired_poll_question_from_other_geozone) } + + it { should_not be_able_to(:answer, incoming_poll_question_from_own_geozone) } + it { should_not be_able_to(:answer, incoming_poll_question_from_all_geozones) } + it { should_not be_able_to(:answer, incoming_poll_question_from_other_geozone) } + end + end + + describe "Budgets" do + it { should be_able_to(:create, investment_in_accepting_budget) } + it { should_not be_able_to(:create, investment_in_selecting_budget) } + it { should_not be_able_to(:create, investment_in_balloting_budget) } + + it { should_not be_able_to(:vote, investment_in_accepting_budget) } + it { should be_able_to(:vote, investment_in_selecting_budget) } + it { should_not be_able_to(:vote, investment_in_balloting_budget) } + + it { should_not be_able_to(:destroy, investment_in_accepting_budget) } + it { should_not be_able_to(:destroy, investment_in_reviewing_budget) } + it { should_not be_able_to(:destroy, investment_in_selecting_budget) } + it { should_not be_able_to(:destroy, investment_in_balloting_budget) } + + it { should be_able_to(:destroy, own_investment_in_accepting_budget) } + it { should be_able_to(:destroy, own_investment_in_reviewing_budget) } + it { should_not be_able_to(:destroy, own_investment_in_selecting_budget) } + it { should_not be_able_to(:destroy, investment_in_balloting_budget) } + + it { should_not be_able_to(:create, ballot_in_accepting_budget) } + it { should_not be_able_to(:create, ballot_in_selecting_budget) } + it { should be_able_to(:create, ballot_in_balloting_budget) } + end end describe "when level 3 verified" do @@ -132,16 +219,47 @@ describe "Abilities::Common" do let(:own_direct_message) { create(:direct_message, sender: user) } before{ user.update(verified_at: Time.current) } - it { should be_able_to(:vote, Proposal) } + it { should be_able_to(:vote, Proposal) } it { should be_able_to(:vote_featured, Proposal) } - it { should be_able_to(:create, SpendingProposal) } + it { should be_able_to(:create, SpendingProposal) } it { should_not be_able_to(:destroy, create(:spending_proposal)) } - it { should_not be_able_to(:destroy, own_spending_proposal) } + it { should_not be_able_to(:destroy, own_spending_proposal) } - it { should be_able_to(:new, DirectMessage) } - it { should be_able_to(:create, DirectMessage) } - it { should be_able_to(:show, own_direct_message) } + it { should be_able_to(:new, DirectMessage) } + it { should be_able_to(:create, DirectMessage) } + it { should be_able_to(:show, own_direct_message) } it { should_not be_able_to(:show, create(:direct_message)) } + + it { should be_able_to(:answer, current_poll) } + it { should_not be_able_to(:answer, expired_poll) } + it { should_not be_able_to(:answer, incoming_poll) } + + it { should be_able_to(:answer, poll_question_from_own_geozone) } + it { should be_able_to(:answer, poll_question_from_all_geozones) } + it { should_not be_able_to(:answer, poll_question_from_other_geozone) } + + it { should_not be_able_to(:answer, expired_poll_question_from_own_geozone) } + it { should_not be_able_to(:answer, expired_poll_question_from_all_geozones) } + it { should_not be_able_to(:answer, expired_poll_question_from_other_geozone) } + + it { should_not be_able_to(:answer, incoming_poll_question_from_own_geozone) } + it { should_not be_able_to(:answer, incoming_poll_question_from_all_geozones) } + it { should_not be_able_to(:answer, incoming_poll_question_from_other_geozone) } + + context "without geozone" do + before(:each) { user.geozone = nil } + it { should_not be_able_to(:answer, poll_question_from_own_geozone) } + it { should be_able_to(:answer, poll_question_from_all_geozones) } + it { should_not be_able_to(:answer, poll_question_from_other_geozone) } + + it { should_not be_able_to(:answer, expired_poll_question_from_own_geozone) } + it { should_not be_able_to(:answer, expired_poll_question_from_all_geozones) } + it { should_not be_able_to(:answer, expired_poll_question_from_other_geozone) } + + it { should_not be_able_to(:answer, incoming_poll_question_from_own_geozone) } + it { should_not be_able_to(:answer, incoming_poll_question_from_all_geozones) } + it { should_not be_able_to(:answer, incoming_poll_question_from_other_geozone) } + end end end diff --git a/spec/models/activity_spec.rb b/spec/models/activity_spec.rb index 0c7b99027..44ecc6ffd 100644 --- a/spec/models/activity_spec.rb +++ b/spec/models/activity_spec.rb @@ -56,11 +56,12 @@ describe Activity do activity3 = create(:activity, user: user1, action: "hide", actionable: create(:proposal)) activity4 = create(:activity, user: user1, action: "hide", actionable: create(:comment)) activity5 = create(:activity, user: user1, action: "block", actionable: create(:user)) + activity6 = create(:activity, user: user1, action: "valuate", actionable: create(:budget_investment)) create_list(:activity, 3) - expect(Activity.by(user1).size).to eq 5 + expect(Activity.by(user1).size).to eq 6 - [activity1, activity2, activity3, activity4, activity5].each do |a| + [activity1, activity2, activity3, activity4, activity5, activity6].each do |a| expect(Activity.by(user1)).to include(a) end end @@ -68,20 +69,23 @@ describe Activity do describe "scopes by actionable" do it "should filter by actionable type" do - on_proposal = create(:activity, actionable: create(:proposal)) - on_debate = create(:activity, actionable: create(:debate)) - on_comment = create(:activity, actionable: create(:comment)) - on_user = create(:activity, actionable: create(:user)) + on_proposal = create(:activity, actionable: create(:proposal)) + on_debate = create(:activity, actionable: create(:debate)) + on_comment = create(:activity, actionable: create(:comment)) + on_user = create(:activity, actionable: create(:user)) + on_investment = create(:activity, actionable: create(:budget_investment)) expect(Activity.on_proposals.size).to eq 1 expect(Activity.on_debates.size).to eq 1 expect(Activity.on_comments.size).to eq 1 expect(Activity.on_users.size).to eq 1 + expect(Activity.on_budget_investments.size).to eq 1 expect(Activity.on_proposals.first).to eq on_proposal expect(Activity.on_debates.first).to eq on_debate expect(Activity.on_comments.first).to eq on_comment expect(Activity.on_users.first).to eq on_user + expect(Activity.on_budget_investments.first).to eq on_investment end end diff --git a/spec/models/budget/ballot_spec.rb b/spec/models/budget/ballot_spec.rb index 259a4a4f4..e0eca4c38 100644 --- a/spec/models/budget/ballot_spec.rb +++ b/spec/models/budget/ballot_spec.rb @@ -2,13 +2,39 @@ require 'rails_helper' describe Budget::Ballot do + describe "validations" do + + it "should be valid" do + budget = create(:budget) + ballot = create(:budget_ballot, budget: budget) + + expect(ballot).to be_valid + end + + it "should not be valid with the same investment twice" do + budget = create(:budget) + group = create(:budget_group, budget: budget) + heading = create(:budget_heading, group: group) + investment = create(:budget_investment, :selected, heading: heading) + + ballot = create(:budget_ballot, budget: budget) + ballot.investments << investment + + expect { ballot.investments << investment }.to raise_error(ActiveRecord::RecordNotUnique) + end + + end + describe "#amount_spent" do it "returns the total amount spent in investments" do budget = create(:budget) + group1 = create(:budget_group, budget: budget) group2 = create(:budget_group, budget: budget) + heading1 = create(:budget_heading, group: group1, price: 100000) heading2 = create(:budget_heading, group: group2, price: 200000) + inv1 = create(:budget_investment, :selected, price: 10000, heading: heading1) inv2 = create(:budget_investment, :selected, price: 20000, heading: heading2) @@ -24,10 +50,13 @@ describe Budget::Ballot do it "returns the amount spent on all investments assigned to a specific heading" do budget = create(:budget) + group1 = create(:budget_group, budget: budget) group2 = create(:budget_group, budget: budget) + heading1 = create(:budget_heading, group: group1, price: 100000) heading2 = create(:budget_heading, group: group2, price: 200000) + inv1 = create(:budget_investment, :selected, price: 10000, heading: heading1) inv2 = create(:budget_investment, :selected, price: 20000, heading: heading2) inv3 = create(:budget_investment, :selected, price: 40000, heading: heading1) @@ -48,10 +77,13 @@ describe Budget::Ballot do describe "#amount_available" do it "returns how much is left after taking some investments" do budget = create(:budget) + group1 = create(:budget_group, budget: budget) group2 = create(:budget_group, budget: budget) + heading1 = create(:budget_heading, group: group1, price: 1000) heading2 = create(:budget_heading, group: group2, price: 300) + inv1 = create(:budget_investment, :selected, price: 100, heading: heading1) inv2 = create(:budget_investment, :selected, price: 200, heading: heading2) inv3 = create(:budget_investment, :selected, price: 400, heading: heading1) @@ -68,4 +100,36 @@ describe Budget::Ballot do end end + describe "#heading_for_group" do + + it "returns the heading with balloted investments for a group" do + budget = create(:budget) + group = create(:budget_group, budget: budget) + + heading1 = create(:budget_heading, group: group) + heading2 = create(:budget_heading, group: group) + + inv1 = create(:budget_investment, :selected, heading: heading1) + inv2 = create(:budget_investment, :selected, heading: heading2) + + ballot = create(:budget_ballot, budget: budget) + ballot.investments << inv2 + + expect(ballot.heading_for_group(group)).to eq heading2 + end + + it "returns nil if there are no headings with balloted investments in a group" do + budget = create(:budget) + group = create(:budget_group, budget: budget) + + heading1 = create(:budget_heading, group: group) + heading2 = create(:budget_heading, group: group) + + ballot = create(:budget_ballot, budget: budget) + + expect(ballot.heading_for_group(group)).to eq nil + end + + end + end diff --git a/spec/models/budget/investment_spec.rb b/spec/models/budget/investment_spec.rb index abbc8ad26..2c5f03738 100644 --- a/spec/models/budget/investment_spec.rb +++ b/spec/models/budget/investment_spec.rb @@ -35,7 +35,26 @@ describe Budget::Investment do expect(investment.description).to eq("alert('danger');") end - describe "#unfeasibility_explanation" do + it "set correct group and budget ids" do + budget = create(:budget) + group_1 = create(:budget_group, budget: budget) + group_2 = create(:budget_group, budget: budget) + + heading_1 = create(:budget_heading, group: group_1) + heading_2 = create(:budget_heading, group: group_2) + + investment = create(:budget_investment, heading: heading_1) + + expect(investment.budget_id).to eq budget.id + expect(investment.group_id).to eq group_1.id + + investment.update(heading: heading_2) + + expect(investment.budget_id).to eq budget.id + expect(investment.group_id).to eq group_2.id + end + + describe "#unfeasibility_explanation blank" do it "should be valid if valuation not finished" do investment.unfeasibility_explanation = "" investment.valuation_finished = false @@ -57,6 +76,29 @@ describe Budget::Investment do end end + describe "#price blank" do + it "should be valid if valuation not finished" do + investment.price = "" + investment.valuation_finished = false + expect(investment).to be_valid + end + + it "should be valid if valuation finished and unfeasible" do + investment.price = "" + investment.unfeasibility_explanation = "reason" + investment.feasibility = "unfeasible" + investment.valuation_finished = true + expect(investment).to be_valid + end + + it "should not be valid if valuation finished and feasible" do + investment.price = "" + investment.feasibility = "feasible" + investment.valuation_finished = true + expect(investment).to_not be_valid + end + end + describe "#code" do let(:investment) { create(:budget_investment) } @@ -102,6 +144,76 @@ describe Budget::Investment do end end + describe "#should_show_vote_count?" do + it "returns true in valuating phase" do + budget = create(:budget, phase: "valuating") + investment = create(:budget_investment, budget: budget) + + expect(investment.should_show_vote_count?).to eq(true) + end + + it "returns false in any other phase" do + Budget::PHASES.reject {|phase| phase == "valuating"}.each do |phase| + budget = create(:budget, phase: phase) + investment = create(:budget_investment, budget: budget) + + expect(investment.should_show_vote_count?).to eq(false) + end + end + end + + describe "#should_show_ballots?" do + it "returns true in balloting phase" do + budget = create(:budget, phase: "balloting") + investment = create(:budget_investment, budget: budget) + + expect(investment.should_show_ballots?).to eq(true) + end + + it "returns false in any other phase" do + Budget::PHASES.reject {|phase| phase == "balloting"}.each do |phase| + budget = create(:budget, phase: phase) + investment = create(:budget_investment, budget: budget) + + expect(investment.should_show_ballots?).to eq(false) + end + end + end + + describe "#should_show_price_info?" do + it "returns true for feasibles if phase is balloting or later and price_explanation is present" do + ["balloting", "reviewing_ballots", "finished"].each do |phase| + budget = create(:budget, phase: phase) + investment = create(:budget_investment, :feasible, budget: budget, price_explanation: "price explanation") + + expect(investment.should_show_price_info?).to eq(true) + end + end + + it "returns false in any other phase" do + (Budget::PHASES - ["balloting", "reviewing_ballots", "finished"]).each do |phase| + budget = create(:budget, phase: phase) + investment = create(:budget_investment, :feasible, budget: budget, price_explanation: "price explanation") + + expect(investment.should_show_price_info?).to eq(false) + end + end + + it "returns false if investment is unfeasible" do + budget = create(:budget, phase: "balloting") + investment = create(:budget_investment, :unfeasible, budget: budget, price_explanation: "price explanation") + + expect(investment.should_show_price_info?).to eq(false) + end + + it "returns false if price_explanation is blank" do + budget = create(:budget, phase: "balloting") + investment = create(:budget_investment, :unfeasible, budget: budget, price_explanation: "") + + expect(investment.should_show_price_info?).to eq(false) + end + end + describe "by_admin" do it "should return investments assigned to specific administrator" do investment1 = create(:budget_investment, administrator_id: 33) @@ -225,6 +337,92 @@ describe Budget::Investment do end end + describe "apply_filters_and_search" do + + let(:budget) { create(:budget) } + + it "returns feasible investments" do + investment1 = create(:budget_investment, :feasible, budget: budget) + investment2 = create(:budget_investment, :feasible, budget: budget) + investment3 = create(:budget_investment, :unfeasible, budget: budget) + + results = Budget::Investment::apply_filters_and_search(budget, {}, :feasible) + + expect(results).to include investment1 + expect(results).to include investment2 + expect(results).to_not include investment3 + end + + it "returns unfeasible investments" do + investment1 = create(:budget_investment, :unfeasible, budget: budget) + investment2 = create(:budget_investment, :unfeasible, budget: budget) + investment3 = create(:budget_investment, :feasible, budget: budget) + + results = Budget::Investment::apply_filters_and_search(budget, {}, :unfeasible) + + expect(results).to include investment1 + expect(results).to include investment2 + expect(results).to_not include investment3 + end + + it "returns selected investments" do + budget.update(phase: "balloting") + + investment1 = create(:budget_investment, :feasible, :selected, budget: budget) + investment2 = create(:budget_investment, :feasible, :selected, budget: budget) + investment3 = create(:budget_investment, :feasible, :unselected, budget: budget) + + results = Budget::Investment::apply_filters_and_search(budget, {}, :selected) + + expect(results).to include investment1 + expect(results).to include investment2 + expect(results).to_not include investment3 + end + + it "returns unselected investments" do + budget.update(phase: "balloting") + + investment1 = create(:budget_investment, :feasible, :unselected, budget: budget) + investment2 = create(:budget_investment, :feasible, :unselected, budget: budget) + investment3 = create(:budget_investment, :feasible, :selected, budget: budget) + + results = Budget::Investment::apply_filters_and_search(budget, {}, :unselected) + + expect(results).to include investment1 + expect(results).to include investment2 + expect(results).to_not include investment3 + end + + it "returns investmens by heading" do + group = create(:budget_group, budget: budget) + + heading1 = create(:budget_heading, group: group) + heading2 = create(:budget_heading, group: group) + + investment1 = create(:budget_investment, heading: heading1, budget: budget) + investment2 = create(:budget_investment, heading: heading1, budget: budget) + investment3 = create(:budget_investment, heading: heading2, budget: budget) + + results = Budget::Investment::apply_filters_and_search(budget, heading_id: heading1.id) + + expect(results).to include investment1 + expect(results).to include investment2 + expect(results).to_not include investment3 + end + + it "returns investments by search string" do + investment1 = create(:budget_investment, title: "health for all", budget: budget) + investment2 = create(:budget_investment, title: "improved health", budget: budget) + investment3 = create(:budget_investment, title: "finance", budget: budget) + + results = Budget::Investment::apply_filters_and_search(budget, search: "health") + + expect(results).to include investment1 + expect(results).to include investment2 + expect(results).to_not include investment3 + end + end + describe "search" do context "tags" do @@ -285,6 +483,45 @@ describe Budget::Investment do expect(salamanca_investment.valid_heading?(user)).to eq(false) end + + it "allows votes in a group with a single heading" do + all_city_investment = create(:budget_investment, heading: heading) + expect(all_city_investment.valid_heading?(user)).to eq(true) + end + + it "allows votes in a group with a single heading after voting in that heading" do + all_city_investment1 = create(:budget_investment, heading: heading) + all_city_investment2 = create(:budget_investment, heading: heading) + + create(:vote, votable: all_city_investment1, voter: user) + + expect(all_city_investment2.valid_heading?(user)).to eq(true) + end + + it "allows votes in a group with a single heading after voting in another group" do + districts = create(:budget_group, budget: budget) + carabanchel = create(:budget_heading, group: districts) + + all_city_investment = create(:budget_investment, heading: heading) + carabanchel_investment = create(:budget_investment, heading: carabanchel) + + create(:vote, votable: carabanchel_investment, voter: user) + + expect(all_city_investment.valid_heading?(user)).to eq(true) + end + + it "allows votes in a group with multiple headings after voting in group with a single heading" do + districts = create(:budget_group, budget: budget) + carabanchel = create(:budget_heading, group: districts) + salamanca = create(:budget_heading, group: districts) + + all_city_investment = create(:budget_investment, heading: heading) + carabanchel_investment = create(:budget_investment, heading: carabanchel) + + create(:vote, votable: all_city_investment, voter: user) + + expect(carabanchel_investment.valid_heading?(user)).to eq(true) + end end end diff --git a/spec/models/budget_spec.rb b/spec/models/budget_spec.rb index 1157a152b..a1d1a1f42 100644 --- a/spec/models/budget_spec.rb +++ b/spec/models/budget_spec.rb @@ -49,6 +49,52 @@ describe Budget do budget.phase = "finished" expect(budget).to be_finished end + + it "on_hold?" do + budget.phase = "accepting" + expect(budget).to_not be_on_hold + + budget.phase = "reviewing" + expect(budget).to be_on_hold + + budget.phase = "selecting" + expect(budget).to_not be_on_hold + + budget.phase = "valuating" + expect(budget).to be_on_hold + + budget.phase = "balloting" + expect(budget).to_not be_on_hold + + budget.phase = "reviewing_ballots" + expect(budget).to be_on_hold + + budget.phase = "finished" + expect(budget).to_not be_on_hold + end + + it "balloting_or_later?" do + budget.phase = "accepting" + expect(budget).to_not be_balloting_or_later + + budget.phase = "reviewing" + expect(budget).to_not be_balloting_or_later + + budget.phase = "selecting" + expect(budget).to_not be_balloting_or_later + + budget.phase = "valuating" + expect(budget).to_not be_balloting_or_later + + budget.phase = "balloting" + expect(budget).to be_balloting_or_later + + budget.phase = "reviewing_ballots" + expect(budget).to be_balloting_or_later + + budget.phase = "finished" + expect(budget).to be_balloting_or_later + end end describe "heading_price" do diff --git a/spec/models/officing/residence_spec.rb b/spec/models/officing/residence_spec.rb new file mode 100644 index 000000000..dee63b85a --- /dev/null +++ b/spec/models/officing/residence_spec.rb @@ -0,0 +1,121 @@ +require 'rails_helper' + +describe Officing::Residence do + + let!(:geozone) { create(:geozone, census_code: "01") } + let(:residence) { build(:officing_residence, document_number: "12345678Z") } + + describe "validations" do + + it "should be valid" do + expect(residence).to be_valid + end + + it "should not be valid without a document number" do + residence.document_number = nil + expect(residence).to_not be_valid + end + + it "should not be valid without a document type" do + residence.document_type = nil + expect(residence).to_not be_valid + end + + it "should not be valid without a year of birth" do + residence.year_of_birth = nil + expect(residence).to_not be_valid + end + + describe "allowed age" do + it "should not be valid if user is under allowed age" do + allow_any_instance_of(Officing::Residence).to receive(:date_of_birth).and_return(15.years.ago) + expect(residence).to_not be_valid + expect(residence.errors[:year_of_birth]).to include("You don't have the required age to participate") + end + + it "should be valid if user is above allowed age" do + allow_any_instance_of(Officing::Residence).to receive(:date_of_birth).and_return(16.years.ago) + expect(residence).to be_valid + expect(residence.errors[:year_of_birth]).to be_empty + end + end + + end + + describe "new" do + it "should upcase document number" do + residence = Officing::Residence.new({document_number: "x1234567z"}) + expect(residence.document_number).to eq("X1234567Z") + end + + it "should remove all characters except numbers and letters" do + residence = Officing::Residence.new({document_number: " 12.345.678 - B"}) + expect(residence.document_number).to eq("12345678B") + end + end + + describe "save" do + + it "should store document number, document type, geozone, date of birth and gender" do + residence.save + user = residence.user + + expect(user.document_number).to eq('12345678Z') + expect(user.document_type).to eq("1") + expect(user.date_of_birth.year).to eq(1980) + expect(user.date_of_birth.month).to eq(12) + expect(user.date_of_birth.day).to eq(31) + expect(user.gender).to eq('male') + expect(user.geozone).to eq(geozone) + end + + it "should find existing user and use demographic information" do + geozone = create(:geozone) + create(:user, document_number: "12345678Z", + document_type: "1", + date_of_birth: Date.new(1981, 11, 30), + gender: 'female', + geozone: geozone) + + residence = build(:officing_residence, + document_number: "12345678Z", + document_type: "1") + + residence.save + user = residence.user + + expect(user.document_number).to eq('12345678Z') + expect(user.document_type).to eq("1") + expect(user.date_of_birth.year).to eq(1981) + expect(user.date_of_birth.month).to eq(11) + expect(user.date_of_birth.day).to eq(30) + expect(user.gender).to eq('female') + expect(user.geozone).to eq(geozone) + end + + it "makes half-verified users fully verified" do + user = create(:user, residence_verified_at: Time.current, document_type: "1", document_number: "12345678Z") + expect(user).to be_unverified + residence = build(:officing_residence, document_number: "12345678Z", year_of_birth: 1980) + expect(residence).to be_valid + expect(user.reload).to be_unverified + residence.save + expect(user.reload).to be_level_three_verified + end + + it "stores failed census calls" do + residence = build(:officing_residence, :invalid, document_number: "12345678Z") + residence.save + + expect(FailedCensusCall.count).to eq(1) + expect(FailedCensusCall.first).to have_attributes({ + user_id: residence.user.id, + poll_officer_id: residence.officer.id, + document_number: "12345678Z", + document_type: "1", + year_of_birth: Time.current.year + }) + end + + end +end diff --git a/spec/models/poll/answer_spec.rb b/spec/models/poll/answer_spec.rb new file mode 100644 index 000000000..10ccafbb6 --- /dev/null +++ b/spec/models/poll/answer_spec.rb @@ -0,0 +1,30 @@ +require 'rails_helper' + +describe Poll::Answer do + + describe "validations" do + it "validates that the answers are included in the Poll::Question's list" do + q = create(:poll_question, valid_answers: 'One, Two, Three') + expect(build(:poll_answer, question: q, answer: 'One')).to be_valid + expect(build(:poll_answer, question: q, answer: 'Two')).to be_valid + expect(build(:poll_answer, question: q, answer: 'Three')).to be_valid + + expect(build(:poll_answer, question: q, answer: 'Four')).to_not be_valid + end + end + + describe "#record_voter_participation" do + it "creates a poll_voter with user and poll data" do + answer = create(:poll_answer) + expect(answer.poll.voters).to be_blank + + answer.record_voter_participation + expect(answer.poll.reload.voters.size).to eq(1) + voter = answer.poll.voters.first + + expect(voter.document_number).to eq(answer.author.document_number) + expect(voter.poll_id).to eq(answer.poll.id) + end + end + +end diff --git a/spec/models/poll/booth_spec.rb b/spec/models/poll/booth_spec.rb new file mode 100644 index 000000000..c095c62cd --- /dev/null +++ b/spec/models/poll/booth_spec.rb @@ -0,0 +1,27 @@ +require 'rails_helper' + +describe :booth do + + let(:booth) { build(:poll_booth) } + + it "should be valid" do + expect(booth).to be_valid + end + + it "should not be valid without a name" do + booth.name = nil + expect(booth).to_not be_valid + end + + describe "#search" do + it "should find booths searching by name or location" do + booth1 = create(:poll_booth, name: "Booth number 1", location: "City center") + booth2 = create(:poll_booth, name: "Central", location: "Town hall") + + expect(Poll::Booth.search("number")).to eq([booth1]) + expect(Poll::Booth.search("hall")).to eq([booth2]) + expect(Poll::Booth.search("cen").size).to eq 2 + end + end + +end \ No newline at end of file diff --git a/spec/models/poll/final_recount_spec.rb b/spec/models/poll/final_recount_spec.rb new file mode 100644 index 000000000..bccd772ca --- /dev/null +++ b/spec/models/poll/final_recount_spec.rb @@ -0,0 +1,40 @@ +require 'rails_helper' + +describe :final_recount do + + it "should update count_log if count changes" do + final_recount = create(:poll_final_recount, count: 33) + + expect(final_recount.count_log).to eq("") + + final_recount.count = 33 + final_recount.save + final_recount.count = 32 + final_recount.save + final_recount.count = 34 + final_recount.save + + expect(final_recount.count_log).to eq(":33:32") + end + + it "should update officer_assignment_id_log if count changes" do + final_recount = create(:poll_final_recount, count: 33) + + expect(final_recount.count_log).to eq("") + + final_recount.count = 33 + final_recount.officer_assignment = create(:poll_officer_assignment, id: 111) + final_recount.save + + final_recount.count = 32 + final_recount.officer_assignment = create(:poll_officer_assignment, id: 112) + final_recount.save + + final_recount.count = 34 + final_recount.officer_assignment = create(:poll_officer_assignment, id: 113) + final_recount.save + + expect(final_recount.officer_assignment_id_log).to eq(":111:112") + end + +end \ No newline at end of file diff --git a/spec/models/poll/null_result_spec.rb b/spec/models/poll/null_result_spec.rb new file mode 100644 index 000000000..746b48377 --- /dev/null +++ b/spec/models/poll/null_result_spec.rb @@ -0,0 +1,70 @@ +require 'rails_helper' + +describe Poll::NullResult do + + describe "logging changes" do + it "should update amount_log if amount changes" do + null_result = create(:poll_null_result, amount: 33) + + expect(null_result.amount_log).to eq("") + + null_result.amount = 33 + null_result.save + null_result.amount = 32 + null_result.save + null_result.amount = 34 + null_result.save + + expect(null_result.amount_log).to eq(":33:32") + end + + it "should update officer_assignment_id_log if amount changes" do + null_result = create(:poll_null_result, amount: 33) + + expect(null_result.amount_log).to eq("") + expect(null_result.officer_assignment_id_log).to eq("") + + null_result.amount = 33 + null_result.officer_assignment = create(:poll_officer_assignment, id: 101) + null_result.save + + null_result.amount = 32 + null_result.officer_assignment = create(:poll_officer_assignment, id: 102) + null_result.save + + null_result.amount = 34 + null_result.officer_assignment = create(:poll_officer_assignment, id: 103) + null_result.save + + expect(null_result.amount_log).to eq(":33:32") + expect(null_result.officer_assignment_id_log).to eq(":101:102") + end + + it "should update author_id if amount changes" do + null_result = create(:poll_null_result, amount: 33) + + expect(null_result.amount_log).to eq("") + expect(null_result.author_id_log).to eq("") + + author_A = create(:poll_officer).user + author_B = create(:poll_officer).user + author_C = create(:poll_officer).user + + null_result.amount = 33 + null_result.author_id = author_A.id + null_result.save! + + null_result.amount = 32 + null_result.author_id = author_B.id + null_result.save! + + null_result.amount = 34 + null_result.author_id = author_C.id + null_result.save! + + expect(null_result.amount_log).to eq(":33:32") + expect(null_result.author_id_log).to eq(":#{author_A.id}:#{author_B.id}") + end + end + +end \ No newline at end of file diff --git a/spec/models/poll/officer_assignment_spec.rb b/spec/models/poll/officer_assignment_spec.rb new file mode 100644 index 000000000..b6bce5481 --- /dev/null +++ b/spec/models/poll/officer_assignment_spec.rb @@ -0,0 +1,12 @@ +require 'rails_helper' + +describe :officer_assignment do + it "should log user data on creation" do + user = create(:user, username: "Larry Bird", email: "larry@lege.nd") + officer = create(:poll_officer, user: user) + + oa = create(:poll_officer_assignment, officer: officer) + + expect(oa.reload.user_data_log).to eq "#{user.id} - Larry Bird (larry@lege.nd)" + end +end \ No newline at end of file diff --git a/spec/models/poll/officer_spec.rb b/spec/models/poll/officer_spec.rb new file mode 100644 index 000000000..83bdbecc2 --- /dev/null +++ b/spec/models/poll/officer_spec.rb @@ -0,0 +1,124 @@ +require 'rails_helper' + +describe :officer do + + describe "#voting_days_assigned_polls" do + it "should return all polls with this officer assigned during voting days" do + officer = create(:poll_officer) + + poll_1 = create(:poll) + poll_2 = create(:poll) + poll_3 = create(:poll) + + booth_assignment_1a = create(:poll_booth_assignment, poll: poll_1) + booth_assignment_1b = create(:poll_booth_assignment, poll: poll_1) + booth_assignment_2 = create(:poll_booth_assignment, poll: poll_2) + + create(:poll_officer_assignment, booth_assignment: booth_assignment_1a, officer: officer, date: poll_1.starts_at) + create(:poll_officer_assignment, booth_assignment: booth_assignment_1b, officer: officer, date: poll_1.ends_at) + create(:poll_officer_assignment, booth_assignment: booth_assignment_2, officer: officer) + + assigned_polls = officer.voting_days_assigned_polls + expect(assigned_polls.size).to eq 2 + expect(assigned_polls.include?(poll_1)).to eq(true) + expect(assigned_polls.include?(poll_2)).to eq(true) + expect(assigned_polls.include?(poll_3)).to eq(false) + end + + it "should not return polls with this officer assigned for final recount/results" do + officer = create(:poll_officer) + + poll_1 = create(:poll) + poll_2 = create(:poll) + + booth_assignment_1 = create(:poll_booth_assignment, poll: poll_1) + booth_assignment_2 = create(:poll_booth_assignment, poll: poll_2) + + create(:poll_officer_assignment, booth_assignment: booth_assignment_1, officer: officer, date: poll_1.starts_at) + create(:poll_officer_assignment, booth_assignment: booth_assignment_2, officer: officer, final: true) + + assigned_polls = officer.voting_days_assigned_polls + expect(assigned_polls.size).to eq 1 + expect(assigned_polls.include?(poll_1)).to eq(true) + expect(assigned_polls.include?(poll_2)).to eq(false) + end + + it "should return polls ordered by end date (desc)" do + officer = create(:poll_officer) + + poll_1 = create(:poll, ends_at: 1.day.ago) + poll_2 = create(:poll, ends_at: 10.days.from_now) + poll_3 = create(:poll, ends_at: 10.day.ago) + + [poll_1, poll_2, poll_3].each do |p| + create(:poll_officer_assignment, officer: officer, booth_assignment: create(:poll_booth_assignment, poll: p)) + end + + assigned_polls = officer.voting_days_assigned_polls + + expect(assigned_polls.first).to eq(poll_2) + expect(assigned_polls.second).to eq(poll_1) + expect(assigned_polls.last).to eq(poll_3) + end + end + + describe "#final_days_assigned_polls" do + it "should return all polls with this officer assigned for final recount/results" do + officer = create(:poll_officer) + + poll_1 = create(:poll) + poll_2 = create(:poll) + poll_3 = create(:poll) + + booth_assignment_1a = create(:poll_booth_assignment, poll: poll_1) + booth_assignment_1b = create(:poll_booth_assignment, poll: poll_1) + booth_assignment_2 = create(:poll_booth_assignment, poll: poll_2) + + create(:poll_officer_assignment, booth_assignment: booth_assignment_1a, officer: officer, date: poll_1.starts_at, final: true) + create(:poll_officer_assignment, booth_assignment: booth_assignment_1b, officer: officer, date: poll_1.ends_at, final: true) + create(:poll_officer_assignment, booth_assignment: booth_assignment_2, officer: officer, final: true) + + assigned_polls = officer.final_days_assigned_polls + expect(assigned_polls.size).to eq 2 + expect(assigned_polls.include?(poll_1)).to eq(true) + expect(assigned_polls.include?(poll_2)).to eq(true) + expect(assigned_polls.include?(poll_3)).to eq(false) + end + + it "should not return polls with this officer assigned for voting days" do + officer = create(:poll_officer) + + poll_1 = create(:poll) + poll_2 = create(:poll) + + booth_assignment_1 = create(:poll_booth_assignment, poll: poll_1) + booth_assignment_2 = create(:poll_booth_assignment, poll: poll_2) + + create(:poll_officer_assignment, booth_assignment: booth_assignment_1, officer: officer, date: poll_1.starts_at) + create(:poll_officer_assignment, booth_assignment: booth_assignment_2, officer: officer, final: true) + + assigned_polls = officer.final_days_assigned_polls + expect(assigned_polls.size).to eq 1 + expect(assigned_polls.include?(poll_1)).to eq(false) + expect(assigned_polls.include?(poll_2)).to eq(true) + end + + it "should return polls ordered by end date (desc)" do + officer = create(:poll_officer) + + poll_1 = create(:poll, ends_at: 1.day.ago) + poll_2 = create(:poll, ends_at: 10.days.from_now) + poll_3 = create(:poll, ends_at: 10.day.ago) + + [poll_1, poll_2, poll_3].each do |p| + create(:poll_officer_assignment, officer: officer, booth_assignment: create(:poll_booth_assignment, poll: p), final: true) + end + + assigned_polls = officer.final_days_assigned_polls + + expect(assigned_polls.first).to eq(poll_2) + expect(assigned_polls.second).to eq(poll_1) + expect(assigned_polls.last).to eq(poll_3) + end + end +end \ No newline at end of file diff --git a/spec/models/poll/partial_result_spec.rb b/spec/models/poll/partial_result_spec.rb new file mode 100644 index 000000000..dd6176a06 --- /dev/null +++ b/spec/models/poll/partial_result_spec.rb @@ -0,0 +1,81 @@ +require 'rails_helper' + +describe Poll::PartialResult do + + describe "validations" do + it "validates that the answers are included in the Poll::Question's list" do + q = create(:poll_question, valid_answers: 'One, Two, Three') + expect(build(:poll_partial_result, question: q, answer: 'One')).to be_valid + expect(build(:poll_partial_result, question: q, answer: 'Two')).to be_valid + expect(build(:poll_partial_result, question: q, answer: 'Three')).to be_valid + + expect(build(:poll_partial_result, question: q, answer: 'Four')).to_not be_valid + end + end + + describe "logging changes" do + it "should update amount_log if amount changes" do + partial_result = create(:poll_partial_result, amount: 33) + + expect(partial_result.amount_log).to eq("") + + partial_result.amount = 33 + partial_result.save + partial_result.amount = 32 + partial_result.save + partial_result.amount = 34 + partial_result.save + + expect(partial_result.amount_log).to eq(":33:32") + end + + it "should update officer_assignment_id_log if amount changes" do + partial_result = create(:poll_partial_result, amount: 33) + + expect(partial_result.amount_log).to eq("") + expect(partial_result.officer_assignment_id_log).to eq("") + + partial_result.amount = 33 + partial_result.officer_assignment = create(:poll_officer_assignment, id: 10) + partial_result.save + + partial_result.amount = 32 + partial_result.officer_assignment = create(:poll_officer_assignment, id: 20) + partial_result.save + + partial_result.amount = 34 + partial_result.officer_assignment = create(:poll_officer_assignment, id: 30) + partial_result.save + + expect(partial_result.amount_log).to eq(":33:32") + expect(partial_result.officer_assignment_id_log).to eq(":10:20") + end + + it "should update author_id if amount changes" do + partial_result = create(:poll_partial_result, amount: 33) + + expect(partial_result.amount_log).to eq("") + expect(partial_result.author_id_log).to eq("") + + author_A = create(:poll_officer).user + author_B = create(:poll_officer).user + author_C = create(:poll_officer).user + + partial_result.amount = 33 + partial_result.author_id = author_A.id + partial_result.save! + + partial_result.amount = 32 + partial_result.author_id = author_B.id + partial_result.save! + + partial_result.amount = 34 + partial_result.author_id = author_C.id + partial_result.save! + + expect(partial_result.amount_log).to eq(":33:32") + expect(partial_result.author_id_log).to eq(":#{author_A.id}:#{author_B.id}") + end + end + +end diff --git a/spec/models/poll/poll_spec.rb b/spec/models/poll/poll_spec.rb new file mode 100644 index 000000000..80a57e916 --- /dev/null +++ b/spec/models/poll/poll_spec.rb @@ -0,0 +1,143 @@ +require 'rails_helper' + +describe :poll do + + let(:poll) { build(:poll) } + + describe "validations" do + it "should be valid" do + expect(poll).to be_valid + end + + it "should not be valid without a name" do + poll.name = nil + expect(poll).to_not be_valid + end + + it "should not be valid without a start date" do + poll.starts_at = nil + expect(poll).to_not be_valid + end + + it "should not be valid without an end date" do + poll.ends_at = nil + expect(poll).to_not be_valid + end + + it "should not be valid without a proper start/end date range" do + poll.starts_at = 1.week.ago + poll.ends_at = 2.months.ago + expect(poll).to_not be_valid + end + end + + describe "#opened?" do + it "returns true only when it isn't too early or too late" do + expect(create(:poll, :incoming)).to_not be_current + expect(create(:poll, :expired)).to_not be_current + expect(create(:poll)).to be_current + end + end + + describe "#incoming?" do + it "returns true only when it is too early" do + expect(create(:poll, :incoming)).to be_incoming + expect(create(:poll, :expired)).to_not be_incoming + expect(create(:poll)).to_not be_incoming + end + end + + describe "#expired?" do + it "returns true only when it is too late" do + expect(create(:poll, :incoming)).to_not be_expired + expect(create(:poll, :expired)).to be_expired + expect(create(:poll)).to_not be_expired + end + end + + describe "#published?" do + it "returns true only when published is true" do + expect(create(:poll)).to_not be_published + expect(create(:poll, :published)).to be_published + end + end + + describe "#document_has_voted?" do + it "returns true if Poll::Voter with document exists" do + poll = create(:poll) + voter = create(:poll_voter, :valid_document, poll: poll) + + expect(poll.document_has_voted?(voter.document_number, voter.document_type)).to eq(true) + end + + it "returns false if Poll::Voter with document does not exists" do + poll_2 = create(:poll) + voter = create(:poll_voter, :valid_document, poll: poll_2) + + expect(poll.document_has_voted?(voter.document_number, voter.document_type)).to eq(false) + end + end + + describe "answerable_by" do + let(:geozone) {create(:geozone) } + + let!(:current_poll) { create(:poll) } + let!(:expired_poll) { create(:poll, :expired) } + let!(:incoming_poll) { create(:poll, :incoming) } + let!(:current_restricted_poll) { create(:poll, geozone_restricted: true, geozones: [geozone]) } + let!(:expired_restricted_poll) { create(:poll, :expired, geozone_restricted: true, geozones: [geozone]) } + let!(:incoming_restricted_poll) { create(:poll, :incoming, geozone_restricted: true, geozones: [geozone]) } + let!(:all_polls) { [current_poll, expired_poll, incoming_poll, current_poll, expired_restricted_poll, incoming_restricted_poll] } + let(:non_current_polls) { [expired_poll, incoming_poll, expired_restricted_poll, incoming_restricted_poll] } + + let(:non_user) { nil } + let(:level1) { create(:user) } + let(:level2) { create(:user, :level_two) } + let(:level2_from_geozone) { create(:user, :level_two, geozone: geozone) } + let(:all_users) { [non_user, level1, level2, level2_from_geozone] } + + describe 'instance method' do + it "rejects non-users and level 1 users" do + all_polls.each do |poll| + expect(poll).to_not be_answerable_by(non_user) + expect(poll).to_not be_answerable_by(level1) + end + end + + it "rejects everyone when not current" do + non_current_polls.each do |poll| + all_users.each do |user| + expect(poll).to_not be_answerable_by(user) + end + end + end + + it "accepts level 2 users when unrestricted and current" do + expect(current_poll).to be_answerable_by(level2) + expect(current_poll).to be_answerable_by(level2_from_geozone) + end + + it "accepts level 2 users only from the same geozone when restricted by geozone" do + expect(current_restricted_poll).to_not be_answerable_by(level2) + expect(current_restricted_poll).to be_answerable_by(level2_from_geozone) + end + end + + describe 'class method' do + it "returns no polls for non-users and level 1 users" do + expect(Poll.answerable_by(nil)).to be_empty + expect(Poll.answerable_by(level1)).to be_empty + end + + it "returns unrestricted polls for level 2 users" do + expect(Poll.answerable_by(level2).to_a).to eq([current_poll]) + end + + it "returns restricted & unrestricted polls for level 2 users of the correct geozone" do + list = Poll.answerable_by(level2_from_geozone) + .order(:geozone_restricted) + expect(list.to_a).to eq([current_poll, current_restricted_poll]) + end + end + end +end diff --git a/spec/models/poll/question_spec.rb b/spec/models/poll/question_spec.rb new file mode 100644 index 000000000..057dfebca --- /dev/null +++ b/spec/models/poll/question_spec.rb @@ -0,0 +1,26 @@ +require 'rails_helper' + +RSpec.describe Poll::Question, type: :model do + + describe "#valid_answers" do + it "gets a comma-separated string, but returns an array" do + q = create(:poll_question, valid_answers: "Yes, No") + expect(q.valid_answers).to eq(["Yes", "No"]) + end + end + + describe "#copy_attributes_from_proposal" do + it "copies the attributes from the proposal" do + create_list(:geozone, 3) + p = create(:proposal) + q = create(:poll_question) + q.copy_attributes_from_proposal(p) + expect(q.valid_answers).to eq(['Yes', 'No']) + expect(q.author).to eq(p.author) + expect(q.author_visible_name).to eq(p.author.name) + expect(q.proposal_id).to eq(p.id) + expect(q.title).to eq(p.title) + end + end + +end diff --git a/spec/models/poll/recount_spec.rb b/spec/models/poll/recount_spec.rb new file mode 100644 index 000000000..fb2c80704 --- /dev/null +++ b/spec/models/poll/recount_spec.rb @@ -0,0 +1,40 @@ +require 'rails_helper' + +describe :recount do + + it "should update count_log if count changes" do + recount = create(:poll_recount, count: 33) + + expect(recount.count_log).to eq("") + + recount.count = 33 + recount.save + recount.count = 32 + recount.save + recount.count = 34 + recount.save + + expect(recount.count_log).to eq(":33:32") + end + + it "should update officer_assignment_id_log if count changes" do + recount = create(:poll_recount, count: 33) + + expect(recount.count_log).to eq("") + + recount.count = 33 + recount.officer_assignment = create(:poll_officer_assignment, id: 11) + recount.save + + recount.count = 32 + recount.officer_assignment = create(:poll_officer_assignment, id: 12) + recount.save + + recount.count = 34 + recount.officer_assignment = create(:poll_officer_assignment, id: 13) + recount.save + + expect(recount.officer_assignment_id_log).to eq(":11:12") + end + +end \ No newline at end of file diff --git a/spec/models/poll/voter_spec.rb b/spec/models/poll/voter_spec.rb new file mode 100644 index 000000000..752ac5bbc --- /dev/null +++ b/spec/models/poll/voter_spec.rb @@ -0,0 +1,114 @@ +require 'rails_helper' + +describe :voter do + + let(:poll) { create(:poll) } + let(:booth) { create(:poll_booth) } + let(:booth_assignment) { create(:poll_booth_assignment, poll: poll, booth: booth) } + let(:voter) { create(:poll_voter) } + + describe "validations" do + + it "should be valid" do + expect(voter).to be_valid + end + + it "should not be valid without a user" do + voter.user = nil + expect(voter).to_not be_valid + end + + it "should not be valid without a poll" do + voter.poll = nil + expect(voter).to_not be_valid + end + + it "should be valid if has not voted" do + voter = build(:poll_voter, :valid_document) + + expect(voter).to be_valid + end + + it "should not be valid if the user has already voted in the same poll or booth_assignment" do + user = create(:user, :level_two) + + voter1 = create(:poll_voter, user: user, poll: poll) + voter2 = build(:poll_voter, user: user, poll: poll) + + expect(voter2).to_not be_valid + expect(voter2.errors.messages[:document_number]).to eq(["User has already voted"]) + end + + it "should not be valid if the user has already voted in the same poll/booth" do + user = create(:user, :level_two) + + voter1 = create(:poll_voter, user: user, poll: poll, booth_assignment: booth_assignment) + voter2 = build(:poll_voter, user: user, poll: poll, booth_assignment: booth_assignment) + + expect(voter2).to_not be_valid + expect(voter2.errors.messages[:document_number]).to eq(["User has already voted"]) + end + + it "should not be valid if the user has already voted in different booth in the same poll" do + booth_assignment1 = create(:poll_booth_assignment, poll: poll) + booth_assignment2 = create(:poll_booth_assignment, poll: poll) + + user = create(:user, :level_two) + + voter1 = create(:poll_voter, user: user, poll: poll, booth_assignment: booth_assignment1) + voter2 = build(:poll_voter, user: user, poll: poll, booth_assignment: booth_assignment2) + + expect(voter2).to_not be_valid + expect(voter2.errors.messages[:document_number]).to eq(["User has already voted"]) + end + + it "should be valid if the user has already voted in the same booth in different poll" do + booth_assignment1 = create(:poll_booth_assignment, booth: booth) + booth_assignment2 = create(:poll_booth_assignment, booth: booth, poll: poll) + + user = create(:user, :level_two) + + voter1 = create(:poll_voter, user: user, booth_assignment: booth_assignment1) + voter2 = build(:poll_voter, user: user, booth_assignment: booth_assignment2) + + expect(voter2).to be_valid + end + + it "should not be valid if the user has voted via web" do + answer = create(:poll_answer) + answer.record_voter_participation + + voter = build(:poll_voter, poll: answer.question.poll, user: answer.author) + expect(voter).to_not be_valid + expect(voter.errors.messages[:document_number]).to eq(["User has already voted"]) + end + + end + + describe "save" do + + it "sets demographic info" do + geozone = create(:geozone) + user = create(:user, + geozone: geozone, + date_of_birth: 30.years.ago, + gender: "female") + + voter = build(:poll_voter, user: user) + voter.save + + expect(voter.geozone).to eq(geozone) + expect(voter.age).to eq(30) + expect(voter.gender).to eq("female") + end + + it "sets user info" do + user = create(:user, document_number: "1234A", document_type: "1") + voter = build(:poll_voter, user: user) + voter.save + + expect(voter.document_number).to eq("1234A") + expect(voter.document_type).to eq("1") + end + end +end \ No newline at end of file diff --git a/spec/models/poll/white_result_spec.rb b/spec/models/poll/white_result_spec.rb new file mode 100644 index 000000000..687c4ebe1 --- /dev/null +++ b/spec/models/poll/white_result_spec.rb @@ -0,0 +1,70 @@ +require 'rails_helper' + +describe Poll::WhiteResult do + + describe "logging changes" do + it "should update amount_log if amount changes" do + white_result = create(:poll_white_result, amount: 33) + + expect(white_result.amount_log).to eq("") + + white_result.amount = 33 + white_result.save + white_result.amount = 32 + white_result.save + white_result.amount = 34 + white_result.save + + expect(white_result.amount_log).to eq(":33:32") + end + + it "should update officer_assignment_id_log if amount changes" do + white_result = create(:poll_white_result, amount: 33) + + expect(white_result.amount_log).to eq("") + expect(white_result.officer_assignment_id_log).to eq("") + + white_result.amount = 33 + white_result.officer_assignment = create(:poll_officer_assignment, id: 21) + white_result.save + + white_result.amount = 32 + white_result.officer_assignment = create(:poll_officer_assignment, id: 22) + white_result.save + + white_result.amount = 34 + white_result.officer_assignment = create(:poll_officer_assignment, id: 23) + white_result.save + + expect(white_result.amount_log).to eq(":33:32") + expect(white_result.officer_assignment_id_log).to eq(":21:22") + end + + it "should update author_id if amount changes" do + white_result = create(:poll_white_result, amount: 33) + + expect(white_result.amount_log).to eq("") + expect(white_result.author_id_log).to eq("") + + author_A = create(:poll_officer).user + author_B = create(:poll_officer).user + author_C = create(:poll_officer).user + + white_result.amount = 33 + white_result.author_id = author_A.id + white_result.save! + + white_result.amount = 32 + white_result.author_id = author_B.id + white_result.save! + + white_result.amount = 34 + white_result.author_id = author_C.id + white_result.save! + + expect(white_result.amount_log).to eq(":33:32") + expect(white_result.author_id_log).to eq(":#{author_A.id}:#{author_B.id}") + end + end + +end \ No newline at end of file diff --git a/spec/models/signature_sheet_spec.rb b/spec/models/signature_sheet_spec.rb index 473a7b96d..aeed78996 100644 --- a/spec/models/signature_sheet_spec.rb +++ b/spec/models/signature_sheet_spec.rb @@ -46,12 +46,20 @@ describe SignatureSheet do expect(signature_sheet.name).to eq("Citizen proposal #{proposal.id}") end + it "returns name for spending proposal signature sheets" do spending_proposal = create(:spending_proposal) signature_sheet.signable = spending_proposal expect(signature_sheet.name).to eq("Spending proposal #{spending_proposal.id}") end + + it "returns name for budget investment signature sheets" do + budget_investment = create(:budget_investment) + signature_sheet.signable = budget_investment + + expect(signature_sheet.name).to eq("Investment #{budget_investment.id}") + end end describe "#verify_signatures" do diff --git a/spec/models/signature_spec.rb b/spec/models/signature_spec.rb index 8bb090614..207208bbf 100644 --- a/spec/models/signature_spec.rb +++ b/spec/models/signature_spec.rb @@ -46,49 +46,61 @@ describe Signature do end end - describe "#verified?" do - - it "returns true if user exists" do - user = create(:user, :level_two, document_number: "123A") - signature = create(:signature, document_number: user.document_number) - - expect(signature.verified?).to eq(true) - end - - it "returns true if document number in census" do - signature = create(:signature, document_number: "12345678Z") - - expect(signature.verified?).to eq(true) - end - - it "returns false if user does not exist and not in census" do - signature = create(:signature, document_number: "123A") - - expect(signature.verified?).to eq(false) - end - - end - - describe "#assign_vote" do + describe "#verify" do describe "existing user" do - it "assigns vote to user" do + it "assigns vote to user on proposal" do user = create(:user, :level_two, document_number: "123A") signature = create(:signature, document_number: user.document_number) proposal = signature.signable - signature.assign_vote + signature.verify expect(user.voted_for?(proposal)).to be end + it "assigns vote to user on budget investment" do + investment = create(:budget_investment) + signature_sheet = create(:signature_sheet, signable: investment) + user = create(:user, :level_two, document_number: "123A") + signature = create(:signature, document_number: user.document_number, signature_sheet: signature_sheet) + + signature.verify + + expect(user.voted_for?(investment)).to be + end + it "does not assign vote to user multiple times" do user = create(:user, :level_two, document_number: "123A") signature = create(:signature, document_number: user.document_number) - signature.assign_vote - signature.assign_vote + signature.verify + signature.verify + + expect(Vote.count).to eq(1) + end + + it "does not assigns vote to invalid user on budget investment" do + investment = create(:budget_investment) + signature_sheet = create(:signature_sheet, signable: investment) + user = create(:user, document_number: "123A") + signature = create(:signature, document_number: user.document_number, signature_sheet: signature_sheet) + + signature.verify + + expect(user.voted_for?(investment)).to_not be + expect(Vote.count).to eq(0) + end + + it "does not assign vote to user multiple times on budget investment" do + investment = create(:budget_investment) + signature_sheet = create(:signature_sheet, signable: investment) + user = create(:user, :level_two, document_number: "123A") + signature = create(:signature, document_number: user.document_number, signature_sheet: signature_sheet) + + signature.verify + signature.verify expect(Vote.count).to eq(1) end @@ -100,7 +112,22 @@ describe Signature do signature_sheet = create(:signature_sheet, signable: proposal) signature = create(:signature, signature_sheet: signature_sheet, document_number: user.document_number) - signature.assign_vote + signature.verify + + expect(Vote.count).to eq(1) + end + + it "does not assign vote to user if already voted on budget investment" do + investment = create(:budget_investment) + user = create(:user, :level_two, document_number: "123A") + vote = create(:vote, votable: investment, voter: user) + + signature_sheet = create(:signature_sheet, signable: investment) + signature = create(:signature, document_number: user.document_number, signature_sheet: signature_sheet) + + expect(Vote.count).to eq(1) + + signature.verify expect(Vote.count).to eq(1) end @@ -108,7 +135,7 @@ describe Signature do it "marks the vote as coming from a signature" do signature = create(:signature, document_number: "12345678Z") - signature.assign_vote + signature.verify expect(Vote.last.signature).to eq(signature) end @@ -118,23 +145,27 @@ describe Signature do describe "inexistent user" do it "creates a user with that document number" do + create(:geozone, census_code: "01") signature = create(:signature, document_number: "12345678Z") proposal = signature.signable - signature.assign_vote + signature.verify user = User.last expect(user.document_number).to eq("12345678Z") expect(user.created_from_signature).to eq(true) expect(user.verified_at).to be expect(user.erased_at).to be + expect(user.geozone).to be + expect(user.gender).to be + expect(user.date_of_birth).to be end it "assign the vote to newly created user" do signature = create(:signature, document_number: "12345678Z") proposal = signature.signable - signature.assign_vote + signature.verify user = signature.user expect(user.voted_for?(proposal)).to be @@ -143,22 +174,18 @@ describe Signature do it "assigns signature to vote" do signature = create(:signature, document_number: "12345678Z") - signature.assign_vote + signature.verify expect(Vote.last.signature).to eq(signature) end end - end - - describe "#verify" do - describe "document in census" do - it "calls assign_vote" do + it "calls assign_vote_to_user" do signature = create(:signature, document_number: "12345678Z") - expect(signature).to receive(:assign_vote) + expect(signature).to receive(:assign_vote_to_user) signature.verify end @@ -175,10 +202,10 @@ describe Signature do describe "document not in census" do - it "does not call assign_vote" do + it "does not call assign_vote_to_user" do signature = create(:signature, document_number: "123A") - expect(signature).to_not receive(:assign_vote) + expect(signature).to_not receive(:assign_vote_to_user) signature.verify end diff --git a/spec/models/site_customization/content_block_spec.rb b/spec/models/site_customization/content_block_spec.rb new file mode 100644 index 000000000..2eb6279f6 --- /dev/null +++ b/spec/models/site_customization/content_block_spec.rb @@ -0,0 +1,20 @@ +require 'rails_helper' + +RSpec.describe SiteCustomization::ContentBlock, type: :model do + let(:block) { build(:site_customization_content_block) } + + it "should be valid" do + expect(block).to be_valid + end + + it "name is unique per locale" do + create(:site_customization_content_block, name: "top_links", locale: "en") + invalid_block = build(:site_customization_content_block, name: "top_links", locale: "en") + + expect(invalid_block).to be_invalid + expect(invalid_block.errors.full_messages).to include("Name has already been taken") + + valid_block = build(:site_customization_content_block, name: "top_links", locale: "es") + expect(valid_block).to be_valid + end +end diff --git a/spec/models/site_customization/page_spec.rb b/spec/models/site_customization/page_spec.rb new file mode 100644 index 000000000..2868f62d1 --- /dev/null +++ b/spec/models/site_customization/page_spec.rb @@ -0,0 +1,14 @@ +require 'rails_helper' + +RSpec.describe SiteCustomization::Page, type: :model do + let(:custom_page) { build(:site_customization_page) } + + it "should be valid" do + expect(custom_page).to be_valid + end + + it "is invalid if slug has symbols" do + custom_page = build(:site_customization_page, slug: "as/as*la") + expect(custom_page).to be_invalid + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index ed0bac164..283a01c96 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -65,6 +65,13 @@ describe User do end end + describe "#age" do + it "is the rounded integer age based on the date_of_birth" do + user = create(:user, date_of_birth: 33.years.ago) + expect(user.age).to eq(33) + end + end + describe 'preferences' do describe 'email_on_comment' do it 'should be false by default' do @@ -151,6 +158,18 @@ describe User do end end + describe "poll_officer?" do + it "is false when the user is not a poll officer" do + expect(subject.poll_officer?).to be false + end + + it "is true when the user is a poll officer" do + subject.save + create(:poll_officer, user: subject) + expect(subject.poll_officer?).to be true + end + end + describe "organization?" do it "is false when the user is not an organization" do expect(subject.organization?).to be false @@ -351,6 +370,20 @@ describe User do end end + + describe "erased" do + + it "returns users that have been erased" do + user1 = create(:user, erased_at: Time.current) + user2 = create(:user, erased_at: Time.current) + user3 = create(:user, erased_at: nil) + + expect(User.erased).to include(user1) + expect(User.erased).to include(user2) + expect(User.erased).to_not include(user3) + end + + end end describe "self.search" do @@ -469,4 +502,134 @@ describe User do end + describe "#take_votes_from" do + it "logs info of previous users" do + user = create(:user, :level_three) + other_user = create(:user, :level_three) + another_user = create(:user) + + expect(user.former_users_data_log).to be_blank + + user.take_votes_from other_user + + expect(user.former_users_data_log).to include("id: #{other_user.id}") + + user.take_votes_from another_user + + expect(user.former_users_data_log).to include("id: #{other_user.id}") + expect(user.former_users_data_log).to include("| id: #{another_user.id}") + end + + it "reassigns votes from other user" do + other_user = create(:user, :level_three) + user = create(:user, :level_three) + + v1 = create(:vote, voter: other_user, votable: create(:debate)) + v2 = create(:vote, voter: other_user, votable: create(:proposal)) + v3 = create(:vote, voter: other_user, votable: create(:comment)) + + create(:vote) + + expect(other_user.votes.count).to eq(3) + expect(user.votes.count).to eq(0) + + user.take_votes_from other_user + + expect(other_user.votes.count).to eq(0) + expect(user.vote_ids.sort).to eq([v1.id, v2.id, v3.id].sort) + end + + it "reassigns budget ballots from other user" do + other_user = create(:user, :level_three) + user = create(:user, :level_three) + + b1 = create(:budget_ballot, user: other_user) + b2 = create(:budget_ballot, user: other_user) + + create(:budget_ballot) + + expect(Budget::Ballot.where(user: other_user).count).to eq(2) + expect(Budget::Ballot.where(user: user).count).to eq(0) + + user.take_votes_from other_user + + expect(Budget::Ballot.where(user: other_user).count).to eq(0) + expect(Budget::Ballot.where(user: user).sort).to eq([b1, b2].sort) + end + + it "reassigns poll voters from other user" do + other_user = create(:user, :level_three) + user = create(:user, :level_three) + + v1 = create(:poll_voter, user: other_user) + v2 = create(:poll_voter, user: other_user) + + create(:poll_voter) + + expect(Poll::Voter.where(user: other_user).count).to eq(2) + expect(Poll::Voter.where(user: user).count).to eq(0) + + user.take_votes_from other_user + + expect(Poll::Voter.where(user: other_user).count).to eq(0) + expect(Poll::Voter.where(user: user).sort).to eq([v1, v2].sort) + end + end + + describe "#take_votes_if_erased_document" do + it "does nothing if no erased user with received document" do + user_1 = create(:user, :level_three) + user_2 = create(:user, :level_three) + + create(:vote, voter: user_1) + create(:budget_ballot, user: user_1) + create(:poll_voter, user: user_1) + + user_2.take_votes_if_erased_document(111, 1) + + expect(user_1.votes.count).to eq(1) + expect(user_2.votes.count).to eq(0) + + expect(Budget::Ballot.where(user: user_1).count).to eq(1) + expect(Budget::Ballot.where(user: user_2).count).to eq(0) + + expect(Poll::Voter.where(user: user_1).count).to eq(1) + expect(Poll::Voter.where(user: user_2).count).to eq(0) + end + + it "takes votes from erased user with received document" do + user_1 = create(:user, :level_two, document_number: "12345777", document_type: "1") + user_2 = create(:user) + + create(:vote, voter: user_1) + create(:budget_ballot, user: user_1) + create(:poll_voter, user: user_1) + + user_1.erase + + user_2.take_votes_if_erased_document("12345777", "1") + + expect(user_1.votes.count).to eq(0) + expect(user_2.votes.count).to eq(1) + + expect(Budget::Ballot.where(user: user_1).count).to eq(0) + expect(Budget::Ballot.where(user: user_2).count).to eq(1) + + expect(Poll::Voter.where(user: user_1).count).to eq(0) + expect(Poll::Voter.where(user: user_2).count).to eq(1) + end + + it "removes document from erased user and logs info" do + user_1 = create(:user, document_number: "12345777", document_type: "1") + user_2 = create(:user) + user_1.erase + + user_2.take_votes_if_erased_document("12345777", "1") + + expect(user_2.reload.former_users_data_log).to include("id: #{user_1.id}") + expect(user_1.reload.document_number).to be_blank + end + + end + end diff --git a/spec/support/common_actions.rb b/spec/support/common_actions.rb index 5cd4e748a..e0fdfe425 100644 --- a/spec/support/common_actions.rb +++ b/spec/support/common_actions.rb @@ -141,6 +141,16 @@ module CommonActions expect(page).to have_content 'Residence verified' end + def officing_verify_residence + select 'DNI', from: 'residence_document_type' + fill_in 'residence_document_number', with: "12345678Z" + fill_in 'residence_year_of_birth', with: "1980" + + click_button 'Validate document' + + expect(page).to have_content 'Document verified with Census' + end + def confirm_phone fill_in 'sms_phone', with: "611111111" click_button 'Send' @@ -203,7 +213,7 @@ module CommonActions create(:debate, :with_confidence_score, cached_votes_up: 80)] end - def create_successfull_proposals + def create_successful_proposals [create(:proposal, title: "Winter is coming", question: "Do you speak it?", cached_votes_up: Proposal.votes_needed_for_success + 100), create(:proposal, title: "Fire and blood", question: "You talking to me?", cached_votes_up: Proposal.votes_needed_for_success + 1)] end