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 13a0b3567..39dee65f8 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.1.9'
# 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,10 +28,10 @@ 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'
@@ -40,30 +40,32 @@ gem 'foundation_rails_helper', '~> 2.0.0'
gem 'acts_as_votable'
gem 'ckeditor', '~> 4.2.2'
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 'paranoia', '~> 2.2.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.0.0.332'
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.5.5'
+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'
group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
@@ -72,28 +74,28 @@ group :development, :test do
gem 'spring'
gem 'spring-commands-rspec'
gem 'rspec-rails', '~> 3.5'
- gem 'capybara'
- gem 'factory_girl_rails'
+ gem 'capybara', '~> 2.13.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.12'
+ gem 'capistrano', '~> 3.8.0', 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.47.1', require: false
gem 'knapsack'
end
group :test do
gem 'database_cleaner'
- gem 'poltergeist'
- gem 'coveralls', require: false
+ gem 'poltergeist', '~> 1.14.0'
+ gem 'coveralls', '~> 0.8.19', require: false
gem 'email_spec'
end
diff --git a/Gemfile.lock b/Gemfile.lock
index e2c21605c..67ab5a78b 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.0)
+ public_suffix (~> 2.0, >= 2.0.2)
+ ahoy_matey (1.5.5)
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.1.2)
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.0)
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.13.0)
addressable
mime-types (>= 1.16)
nokogiri (>= 1.3.3)
@@ -109,9 +108,9 @@ 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.19)
json (>= 1.8, < 3)
simplecov (~> 0.12.0)
term-ansicolor (~> 1.3)
@@ -151,14 +150,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,20 +172,20 @@ 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)
- groupdate (3.1.1)
+ groupdate (3.2.0)
activesupport (>= 3)
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.12)
activesupport (>= 4.0.2)
ast (>= 2.1.0)
easy_translate (>= 0.5.0)
@@ -200,26 +199,35 @@ GEM
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.0.3)
+ 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.2)
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)
@@ -230,29 +238,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.0.0.332)
+ 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)
@@ -265,24 +274,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)
+ paperclip (5.1.0)
+ activemodel (>= 4.2.0)
+ activesupport (>= 4.2.0)
+ cocaine (~> 0.5.5)
+ mime-types
+ mimemagic (~> 0.3.0)
+ paranoia (2.2.1)
activerecord (>= 4.0, < 5.1)
- parser (2.3.3.1)
+ 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.14.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)
@@ -292,40 +308,40 @@ 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)
+ rainbow (2.2.1)
raindrops (0.17.0)
rake (12.0.0)
- redcarpet (3.3.4)
+ redcarpet (3.4.0)
referer-parser (0.3.0)
- request_store (1.3.1)
+ request_store (1.3.2)
responders (2.3.0)
railties (>= 4.2.0, < 5.1)
rinku (2.0.2)
- rollbar (2.14.0)
+ rollbar (2.14.1)
multi_json
rspec-core (3.5.4)
rspec-support (~> 3.5.0)
@@ -344,8 +360,8 @@ GEM
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)
+ rubocop (0.47.1)
+ parser (>= 2.3.3.1, < 3.0)
powerpack (~> 0.1)
rainbow (>= 1.99.1, < 3.0)
ruby-progressbar (~> 1.7)
@@ -355,9 +371,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)
@@ -377,12 +393,12 @@ GEM
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)
@@ -396,7 +412,7 @@ GEM
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
- sshkit (1.11.4)
+ sshkit (1.12.0)
net-scp (>= 1.1.2)
net-ssh (>= 2.8.0)
term-ansicolor (1.4.0)
@@ -405,10 +421,10 @@ GEM
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)
@@ -421,9 +437,9 @@ GEM
tilt (>= 1.4, < 3)
tzinfo (1.2.2)
thread_safe (~> 0.1)
- uglifier (3.0.4)
+ uglifier (3.1.9)
execjs (>= 0.3.0, < 3)
- unicode-display_width (1.1.1)
+ unicode-display_width (1.1.3)
unicorn (5.2.0)
kgio (~> 2.6)
raindrops (~> 0.7)
@@ -439,7 +455,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)
@@ -453,20 +469,20 @@ PLATFORMS
DEPENDENCIES
acts-as-taggable-on
acts_as_votable
- ahoy_matey (~> 1.5.3)
+ ahoy_matey (~> 1.5.5)
ancestry (~> 2.2.2)
browser
- bullet
+ bullet (~> 5.5.1)
byebug
- cancancan
- capistrano (= 3.5.0)
+ cancancan (~> 1.16.0)
+ capistrano (~> 3.8.0)
capistrano-bundler (~> 1.2)
- capistrano-rails (= 1.1.8)
- capistrano3-delayed-job (~> 1.0)
- capybara
+ capistrano-rails (~> 1.2.3)
+ capistrano3-delayed-job (~> 1.7.3)
+ capybara (~> 2.13.0)
ckeditor (~> 4.2.2)
coffee-rails (~> 4.2.1)
- coveralls
+ coveralls (~> 0.8.19)
daemons
dalli
database_cleaner
@@ -475,50 +491,54 @@ 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
- groupdate (~> 3.1.0)
- i18n-tasks
+ groupdate (~> 3.2.0)
+ i18n-tasks (~> 0.9.12)
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.0.0.332)
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.2.1)
+ pg (~> 0.20.0)
pg_search
- poltergeist
+ poltergeist (~> 1.14.0)
quiet_assets
- rails (= 4.2.7.1)
- redcarpet
+ rails (= 4.2.8)
+ redcarpet (~> 3.4.0)
responders (~> 2.3.0)
rinku (~> 2.0.2)
- rollbar (~> 2.14.0)
+ rollbar (~> 2.14.1)
rspec-rails (~> 3.5)
- rubocop (~> 0.45.0)
+ rubocop (~> 0.47.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)
+ uglifier (~> 3.1.9)
unicorn (~> 5.2.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)
+
# Consul
Citizen Participation and Open Government Application
-[](https://gitter.im/consul/consul?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[](https://travis-ci.org/consul/consul)
[](https://codeclimate.com/github/consul/consul)
[](https://gemnasium.com/consul/consul)
[](https://coveralls.io/github/consul/consul?branch=master)
+[](http://www.gnu.org/licenses/agpl-3.0)
+
+[](https://www.w3.org/WAI/eval/Overview)
+[](https://rocketvalidator.com/opensource)
+
+[](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)
+
# Consul
Aplicación de Participación Ciudadana y Gobierno Abierto
-[](https://gitter.im/consul/consul?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[](https://travis-ci.org/consul/consul)
[](https://codeclimate.com/github/consul/consul)
[](https://gemnasium.com/consul/consul)
[](https://coveralls.io/github/consul/consul?branch=master)
+[](http://www.gnu.org/licenses/agpl-3.0)
+
+[](https://www.w3.org/WAI/eval/Overview)
+[](https://rocketvalidator.com/opensource)
+
+[](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..fac6d35ed 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,46 @@ table.investment-projects-summary {
white-space: nowrap;
}
}
+
+.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 1511910b1..94d403528 100644
--- a/app/assets/stylesheets/layout.scss
+++ b/app/assets/stylesheets/layout.scss
@@ -144,6 +144,10 @@ a {
padding-top: $line-height;
}
+.light {
+ background: $light;
+}
+
.highlight {
background: $highlight;
}
@@ -204,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;
}
@@ -462,6 +490,7 @@ header {
.input-group-button {
line-height: $line-height*1.5;
+ padding-bottom: 0;
button {
background: $border;
@@ -483,7 +512,6 @@ header {
}
.submenu {
- background: white;
border-bottom: 1px solid $border;
clear: both;
margin-bottom: $line-height/2;
@@ -585,7 +613,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;
@@ -1104,11 +1133,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 {
@@ -1166,7 +1196,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;
@@ -1174,9 +1208,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);
@@ -1214,9 +1250,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);
@@ -1254,9 +1292,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);
@@ -1293,6 +1333,48 @@ 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;
+ }
+}
+
.social {
a {
@@ -1384,6 +1466,30 @@ table {
color: #CE3E26;
}
}
+
+ .ssb-telegram {
+ background: #0088cc;
+ color: white;
+ height: $line-height;
+ position: relative;
+ width: $line-height*2;
+
+ &:before {
+ content: "A";
+ 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..ad413925b 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;
+ }
}
}
@@ -1202,7 +1208,7 @@ ul.ballot-list {
}
}
-// 07. Proposals successfull
+// 07. Proposals successful
// -------------------------
.dark-heading {
@@ -1235,7 +1241,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 +1265,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 +1289,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 +1310,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/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/concerns/commentable_actions.rb b/app/controllers/concerns/commentable_actions.rb
index ef5e60dcb..ee69ac809 100644
--- a/app/controllers/concerns/commentable_actions.rb
+++ b/app/controllers/concerns/commentable_actions.rb
@@ -129,16 +129,16 @@ module CommentableActions
when '4'
1.year.ago
else
- Date.parse(params[:advanced_search][:date_min]) rescue nil
+ Date.parse(params[:advanced_search][:date_min]) rescue 100.years.ago
end
end
def search_finish_date
- params[:advanced_search][:date_max].try(:to_date) || Date.today
+ (params[:advanced_search][:date_max].to_date rescue Date.today) || Date.today
end
def search_date_range
- search_start_date.beginning_of_day..search_finish_date.end_of_day
+ [100.years.ago, search_start_date].max.beginning_of_day..[search_finish_date, Date.today].min.end_of_day
end
def set_search_order
diff --git a/app/controllers/debates_controller.rb b/app/controllers/debates_controller.rb
index 8262f014a..6d17eb749 100644
--- a/app/controllers/debates_controller.rb
+++ b/app/controllers/debates_controller.rb
@@ -22,7 +22,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..daec4a051 100644
--- a/app/controllers/proposals_controller.rb
+++ b/app/controllers/proposals_controller.rb
@@ -28,8 +28,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 +103,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/budgets_helper.rb b/app/helpers/budgets_helper.rb
index 4b9dfd311..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)
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/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 7ac6a12f4..6a8ef594c 100644
--- a/app/models/abilities/common.rb
+++ b/app/models/abilities/common.rb
@@ -53,6 +53,12 @@ module Abilities
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/investment.rb b/app/models/budget/investment.rb
index 100433707..4bfd038c3 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 }
@@ -136,6 +137,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 +230,7 @@ class Budget
def should_show_aside?
(budget.selecting? && !unfeasible?) ||
(budget.balloting? && feasible?) ||
- (budget.valuating? && feasible?)
+ (budget.valuating? && !unfeasible?)
end
def should_show_votes?
@@ -259,7 +264,7 @@ 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
diff --git a/app/models/comment.rb b/app/models/comment.rb
index d4b1cc8e7..cd84a3578 100644
--- a/app/models/comment.rb
+++ b/app/models/comment.rb
@@ -10,7 +10,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 7e38ce97d..824879ec6 100644
--- a/app/models/geozone.rb
+++ b/app/models/geozone.rb
@@ -9,6 +9,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 32175f9f7..0abde3584 100644
--- a/app/models/proposal.rb
+++ b/app/models/proposal.rb
@@ -45,12 +45,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
@@ -155,7 +155,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 f8991d676..c3038c88a 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -13,6 +13,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
@@ -55,6 +56,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
@@ -123,6 +125,10 @@ class User < ActiveRecord::Base
manager.present?
end
+ def poll_officer?
+ poll_officer.present?
+ end
+
def organization?
organization.present?
end
@@ -188,6 +194,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
@@ -241,6 +263,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..fecd29419 100644
--- a/app/views/admin/_menu.html.erb
+++ b/app/views/admin/_menu.html.erb
@@ -1,126 +1,166 @@
-