Merge branch 'budget' into budget-public-controllers

This commit is contained in:
rgarcia
2016-09-02 13:15:40 +02:00
70 changed files with 1252 additions and 132 deletions

214
CUSTOMIZE_ES.md Normal file
View File

@@ -0,0 +1,214 @@
# Personalización
Puedes modificar consul y ponerle tu propia imagen, para esto debes primero hacer un fork de https://github.com/consul/consul creando un repositorio nuevo en Github. Puedes usar otro servicio como Gitlab, pero no te olvides de poner el enlace en el footer a tu repositorio en cumplimiento con la licencia de este proyecto (GPL Affero 3).
Hemos creado una estructura específica donde puedes sobreescribir y personalizar la aplicación para que puedas actualizar sin que tengas problemas al hacer merge y se sobreescriban por error tus cambios. Intentamos que Consul sea una aplicación Ruby on Rails lo más plain vanilla posible para facilitar el acceso de nuevas desarrolladoras.
## Ficheros y directorios especiales
Para adaptarlo puedes hacerlo a través de los directorios que están en custom dentro de:
* config/locales/custom/
* app/assets/images/custom/
* app/views/custom/
* app/controllers/custom/
* app/models/custom/
Aparte de estos directorios también cuentas con ciertos ficheros para:
* app/assets/stylesheets/custom.css
* app/assets/javascripts/custom.js
* Gemfile_custom
* config/application.custom.rb
### Internacionalización
Si quieres modificar algún texto de la web deberías encontrarlos en los ficheros formato YML disponibles en *config/locales/*. Puedes leer la [guía de internacionalización](http://guides.rubyonrails.org/i18n.html) de Ruby on Rails sobre como funciona este sistema.
Las adaptaciones los debes poner en el directorio *config/locales/custom/*, recomendamos poner solo los textos que quieras personalizar. Por ejemplo si quieres personalizar el texto de "Ayuntamiento de Madrid, 2016" que se encuentra en el footer en todas las páginas, primero debemos ubicar en que plantilla se encuentra (app/views/layouts/_footer.html.erb), vemos que en el código pone lo siguiente:
```
<%= t("layouts.footer.copyright", year: Time.now.year) %>
```
Y que en el fichero config/locales/es.yml sigue esta estructura (solo ponemos lo relevante para este caso):
```
es:
layouts:
footer:
copyright: Ayuntamiento de Madrid, %{year}
```
Si creamos el fichero config/locales/custom/es.yml y modificamos "Ayuntamiento de Madrid" por el nombre de la organización que se este haciendo la modificación. Recomendamos directamente copiar los ficheros config/locales/ e ir revisando y corrigiendo las que querramos, borrando las líneas que no querramos traducir.
### Imágenes
Si quieres sobreescribir alguna imagen debes primero fijarte el nombre que tiene, por defecto se encuentran en *app/assets/images*. Por ejemplo si quieres modificar *app/assets/images/logo_header.png* debes poner otra con ese mismo nombre en el directorio app/assets/images/custom. Los iconos que seguramente quieras modificar son:
* apple-touch-icon-200.png
* icon_home.png
* logo_email.png
* logo_header.png
* map.jpg
* social-media-icon.png
### Vistas (HTML)
Si quieres modificar el HTML de alguna página puedes hacerlo copiando el HTML de *app/views* y poniendolo en *app/views/custom* respetando los subdirectorios que encuentres ahí. Por ejemplo si quieres modificar *app/views/pages/conditions.html* debes copiarlo y modificarla en app/views/custom/pages/conditions.html.erb
### CSS
Si quieres cambiar algun selector CSS (de las hojas de estilo) puedes hacerlo en el fichero *app/assets/stylesheets/custom.scss*. Por ejemplo si quieres cambiar el color del header (.top-links) puedes hacerlo agregando:
```
.top-links {
background: red;
}
```
Usamos un preprocesador de CSS, [SASS, con la sintaxis SCSS](http://sass-lang.com/guide).
### Javascript
Si quieres agregar código Javascript puedes hacerlo en el fichero *app/assets/javascripts/custom.js". Por ejemplo si quieres que salga una alerta puedes poner lo siguiente:
```
$(function(){
alert('foobar');
});
```
### Modelos
Si quieres agregar modelos nuevos, o modificar o agregar métodos a uno ya existente puedes hacerlo en *app/models/custom*. En el caso de los modelos antiguos debes primero hacer un require de la dependencia.
Por ejemplo en el caso del Ayuntamiento de Madrid se requiere comprobar que el código postal durante la verificación sigue un cierto formato (empieza con 280). Esto se realiza creando este fichero en *app/models/custom/verification/residence.rb*:
```
require_dependency Rails.root.join('app', 'models', 'verification', 'residence').to_s
class Verification::Residence
validate :postal_code_in_madrid
validate :residence_in_madrid
def postal_code_in_madrid
errors.add(:postal_code, I18n.t('verification.residence.new.error_not_allowed_postal_code')) unless valid_postal_code?
end
def residence_in_madrid
return if errors.any?
unless residency_valid?
errors.add(:residence_in_madrid, false)
store_failed_attempt
Lock.increase_tries(user)
end
end
private
def valid_postal_code?
postal_code =~ /^280/
end
end
```
No olvides poner los tests relevantes en *spec/models/custom*, siguiendo con el ejemplo pondriamos lo siguiente en *spec/models/custom/residence_spec.rb*:
```
require 'rails_helper'
describe Verification::Residence do
let(:residence) { build(:verification_residence, document_number: "12345678Z") }
describe "verification" do
describe "postal code" do
it "should be valid with postal codes starting with 280" do
residence.postal_code = "28012"
residence.valid?
expect(residence.errors[:postal_code].size).to eq(0)
residence.postal_code = "28023"
residence.valid?
expect(residence.errors[:postal_code].size).to eq(0)
end
it "should not be valid with postal codes not starting with 280" do
residence.postal_code = "12345"
residence.valid?
expect(residence.errors[:postal_code].size).to eq(1)
residence.postal_code = "13280"
residence.valid?
expect(residence.errors[:postal_code].size).to eq(1)
expect(residence.errors[:postal_code]).to include("In order to be verified, you must be registered in the municipality of Madrid.")
end
end
end
end
```
### Controladores
TODO
### Gemfile
Para agregar librerías (gems) nuevas puedes hacerlo en el fichero *Gemfile_custom*. Por ejemplo si quieres agregar la gema [rails-footnotes](https://github.com/josevalim/rails-footnotes) debes hacerlo agregandole
```
gem 'rails-footnotes', '~> 4.0'
```
Y siguiendo el flujo clásico en Ruby on Rails (bundle install y seguir con los pasos específicos de la gema en la documentación)
### application.rb
Cuando necesites extender o modificar el *config/application.rb* puedes hacerlo a través del fichero *config/application_custom.rb*. Por ejemplo si quieres modificar el idioma por defecto al inglés pondrías lo siguiente:
```
module Consul
class Application < Rails::Application
config.i18n.default_locale = :en
config.i18n.available_locales = [:en, :es]
end
end
```
Recuerda que para ver reflejado estos cambios debes reiniciar el servidor de desarrollo.
### lib/
TODO
### public/
TODO
### Seeds
TODO
## Actualizar
Te recomendamos que agregues el remote de consul para facilitar este proceso de merge:
```
$ git remote add consul https://github.com/consul/consul
```
Con esto puedes actualizarte con
```
git checkout -b consul_update
git pull consul master
```

View File

@@ -1,7 +1,7 @@
source 'https://rubygems.org' source 'https://rubygems.org'
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '4.2.7' gem 'rails', '4.2.7.1'
# Use PostgreSQL # Use PostgreSQL
gem 'pg' gem 'pg'
# Use SCSS for stylesheets # Use SCSS for stylesheets
@@ -19,6 +19,9 @@ gem 'jquery-ui-rails'
# Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks # Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks
gem 'turbolinks' gem 'turbolinks'
# Fix sprockets on the
gem 'sprockets', '~> 3.6.3'
gem 'devise', '~> 3.5.7' gem 'devise', '~> 3.5.7'
# Use ActiveModel has_secure_password # Use ActiveModel has_secure_password
# gem 'bcrypt', '~> 3.1.7' # gem 'bcrypt', '~> 3.1.7'
@@ -94,3 +97,5 @@ group :development do
# Access an IRB console on exception pages or by using <%= console %> in views # Access an IRB console on exception pages or by using <%= console %> in views
gem 'web-console', '3.3.0' gem 'web-console', '3.3.0'
end end
eval_gemfile './Gemfile_custom'

View File

@@ -1,36 +1,36 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actionmailer (4.2.7) actionmailer (4.2.7.1)
actionpack (= 4.2.7) actionpack (= 4.2.7.1)
actionview (= 4.2.7) actionview (= 4.2.7.1)
activejob (= 4.2.7) activejob (= 4.2.7.1)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 1.0, >= 1.0.5) rails-dom-testing (~> 1.0, >= 1.0.5)
actionpack (4.2.7) actionpack (4.2.7.1)
actionview (= 4.2.7) actionview (= 4.2.7.1)
activesupport (= 4.2.7) activesupport (= 4.2.7.1)
rack (~> 1.6) rack (~> 1.6)
rack-test (~> 0.6.2) rack-test (~> 0.6.2)
rails-dom-testing (~> 1.0, >= 1.0.5) rails-dom-testing (~> 1.0, >= 1.0.5)
rails-html-sanitizer (~> 1.0, >= 1.0.2) rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (4.2.7) actionview (4.2.7.1)
activesupport (= 4.2.7) activesupport (= 4.2.7.1)
builder (~> 3.1) builder (~> 3.1)
erubis (~> 2.7.0) erubis (~> 2.7.0)
rails-dom-testing (~> 1.0, >= 1.0.5) rails-dom-testing (~> 1.0, >= 1.0.5)
rails-html-sanitizer (~> 1.0, >= 1.0.2) rails-html-sanitizer (~> 1.0, >= 1.0.2)
activejob (4.2.7) activejob (4.2.7.1)
activesupport (= 4.2.7) activesupport (= 4.2.7.1)
globalid (>= 0.3.0) globalid (>= 0.3.0)
activemodel (4.2.7) activemodel (4.2.7.1)
activesupport (= 4.2.7) activesupport (= 4.2.7.1)
builder (~> 3.1) builder (~> 3.1)
activerecord (4.2.7) activerecord (4.2.7.1)
activemodel (= 4.2.7) activemodel (= 4.2.7.1)
activesupport (= 4.2.7) activesupport (= 4.2.7.1)
arel (~> 6.0) arel (~> 6.0)
activesupport (4.2.7) activesupport (4.2.7.1)
i18n (~> 0.7) i18n (~> 0.7)
json (~> 1.7, >= 1.7.7) json (~> 1.7, >= 1.7.7)
minitest (~> 5.1) minitest (~> 5.1)
@@ -67,7 +67,7 @@ GEM
bcrypt (3.1.11) bcrypt (3.1.11)
browser (2.2.0) browser (2.2.0)
builder (3.2.2) builder (3.2.2)
bullet (5.1.1) bullet (5.2.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
uniform_notifier (~> 1.10.0) uniform_notifier (~> 1.10.0)
byebug (9.0.5) byebug (9.0.5)
@@ -114,12 +114,12 @@ GEM
execjs execjs
coffee-script-source (1.10.0) coffee-script-source (1.10.0)
concurrent-ruby (1.0.2) concurrent-ruby (1.0.2)
coveralls (0.8.14) coveralls (0.8.15)
json (>= 1.8, < 3) json (>= 1.8, < 3)
simplecov (~> 0.12.0) simplecov (~> 0.12.0)
term-ansicolor (~> 1.3) term-ansicolor (~> 1.3)
thor (~> 0.19.1) thor (~> 0.19.1)
tins (~> 1.6.0) tins (>= 1.6.0, < 2)
daemons (1.2.3) daemons (1.2.3)
dalli (2.7.6) dalli (2.7.6)
database_cleaner (1.5.3) database_cleaner (1.5.3)
@@ -156,7 +156,7 @@ GEM
factory_girl_rails (4.7.0) factory_girl_rails (4.7.0)
factory_girl (~> 4.7.0) factory_girl (~> 4.7.0)
railties (>= 3.0.0) railties (>= 3.0.0)
faker (1.6.5) faker (1.6.6)
i18n (~> 0.5) i18n (~> 0.5)
faraday (0.9.2) faraday (0.9.2)
multipart-post (>= 1.2, < 3) multipart-post (>= 1.2, < 3)
@@ -174,7 +174,7 @@ GEM
rspec (~> 3.0) rspec (~> 3.0)
ruby-progressbar (~> 1.4) ruby-progressbar (~> 1.4)
geocoder (1.3.7) geocoder (1.3.7)
globalid (0.3.6) globalid (0.3.7)
activesupport (>= 4.1.0) activesupport (>= 4.1.0)
groupdate (3.0.1) groupdate (3.0.1)
activesupport (>= 3) activesupport (>= 3)
@@ -290,16 +290,16 @@ GEM
rack rack
rack-test (0.6.3) rack-test (0.6.3)
rack (>= 1.0) rack (>= 1.0)
rails (4.2.7) rails (4.2.7.1)
actionmailer (= 4.2.7) actionmailer (= 4.2.7.1)
actionpack (= 4.2.7) actionpack (= 4.2.7.1)
actionview (= 4.2.7) actionview (= 4.2.7.1)
activejob (= 4.2.7) activejob (= 4.2.7.1)
activemodel (= 4.2.7) activemodel (= 4.2.7.1)
activerecord (= 4.2.7) activerecord (= 4.2.7.1)
activesupport (= 4.2.7) activesupport (= 4.2.7.1)
bundler (>= 1.3.0, < 2.0) bundler (>= 1.3.0, < 2.0)
railties (= 4.2.7) railties (= 4.2.7.1)
sprockets-rails sprockets-rails
rails-deprecated_sanitizer (1.0.3) rails-deprecated_sanitizer (1.0.3)
activesupport (>= 4.2.0.alpha) activesupport (>= 4.2.0.alpha)
@@ -309,9 +309,9 @@ GEM
rails-deprecated_sanitizer (>= 1.0.1) rails-deprecated_sanitizer (>= 1.0.1)
rails-html-sanitizer (1.0.3) rails-html-sanitizer (1.0.3)
loofah (~> 2.0) loofah (~> 2.0)
railties (4.2.7) railties (4.2.7.1)
actionpack (= 4.2.7) actionpack (= 4.2.7.1)
activesupport (= 4.2.7) activesupport (= 4.2.7.1)
rake (>= 0.8.7) rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0) thor (>= 0.18.1, < 2.0)
raindrops (0.16.0) raindrops (0.16.0)
@@ -350,7 +350,7 @@ GEM
safely_block (0.1.1) safely_block (0.1.1)
errbase errbase
sass (3.4.22) sass (3.4.22)
sass-rails (5.0.5) sass-rails (5.0.6)
railties (>= 4.0.0, < 6) railties (>= 4.0.0, < 6)
sass (~> 3.1) sass (~> 3.1)
sprockets (>= 2.8, < 4.0) sprockets (>= 2.8, < 4.0)
@@ -396,7 +396,7 @@ GEM
thread (0.2.2) thread (0.2.2)
thread_safe (0.3.5) thread_safe (0.3.5)
tilt (2.0.5) tilt (2.0.5)
tins (1.6.0) tins (1.11.0)
tolk (1.9.3) tolk (1.9.3)
rails (>= 4.0, < 4.3) rails (>= 4.0, < 4.3)
safe_yaml (>= 0.8.6) safe_yaml (>= 0.8.6)
@@ -408,7 +408,7 @@ GEM
tilt (>= 1.4, < 3) tilt (>= 1.4, < 3)
tzinfo (1.2.2) tzinfo (1.2.2)
thread_safe (~> 0.1) thread_safe (~> 0.1)
uglifier (3.0.0) uglifier (3.0.1)
execjs (>= 0.3.0, < 3) execjs (>= 0.3.0, < 3)
unicorn (5.1.0) unicorn (5.1.0)
kgio (~> 2.6) kgio (~> 2.6)
@@ -485,7 +485,7 @@ DEPENDENCIES
pg_search pg_search
poltergeist poltergeist
quiet_assets quiet_assets
rails (= 4.2.7) rails (= 4.2.7.1)
redcarpet redcarpet
responders responders
rinku rinku
@@ -496,6 +496,7 @@ DEPENDENCIES
social-share-button social-share-button
spring spring
spring-commands-rspec spring-commands-rspec
sprockets (~> 3.6.3)
tolk tolk
turbolinks turbolinks
turnout turnout

5
Gemfile_custom Normal file
View File

@@ -0,0 +1,5 @@
# Overrides and adds customized gems in this file
# Read more on documentation:
# * English: https://github.com/consul/consul/blob/master/CUSTOMIZE_EN.md#gemfile
# * Spanish: https://github.com/consul/consul/blob/master/CUSTOMIZE_ES.md#gemfile
#

View File

@@ -62,6 +62,10 @@ But for some actions like voting, you will need a verified user, the seeds file
**user:** verified@consul.dev **user:** verified@consul.dev
**pass:** 12345678 **pass:** 12345678
### Customization
See [CUSTOMIZE_ES.md](CUSTOMIZE_ES.md)
### OAuth ### OAuth
To test authentication services with external OAuth suppliers - right now Twitter, Facebook and Google - you'll need to create an "application" in each of the supported platforms and set the *key* and *secret* provided in your *secrets.yml* To test authentication services with external OAuth suppliers - right now Twitter, Facebook and Google - you'll need to create an "application" in each of the supported platforms and set the *key* and *secret* provided in your *secrets.yml*

View File

@@ -61,6 +61,9 @@ Pero para ciertas acciones, como apoyar, necesitarás un usuario verificado, el
**user:** verified@consul.dev **user:** verified@consul.dev
**pass:** 12345678 **pass:** 12345678
### Customización
Ver fichero [CUSTOMIZE_ES.md](CUSTOMIZE_ES.md)
### OAuth ### OAuth

View File

View File

@@ -16,7 +16,7 @@
//= require jquery-ui/datepicker-es //= require jquery-ui/datepicker-es
//= require foundation //= require foundation
//= require turbolinks //= require turbolinks
//= require ckeditor/init //= require ckeditor/loader
//= require_directory ./ckeditor //= require_directory ./ckeditor
//= require social-share-button //= require social-share-button
//= require initial //= require initial
@@ -45,6 +45,7 @@
//= require valuation_spending_proposal_form //= require valuation_spending_proposal_form
//= require embed_video //= require embed_video
//= require banners //= require banners
//= require custom
var initialize_modules = function() { var initialize_modules = function() {
App.Comments.initialize(); App.Comments.initialize();

View File

@@ -0,0 +1,3 @@
//= require ckeditor/init
CKEDITOR.config.customConfig = '<%= javascript_path 'ckeditor/config.js' %>';

View File

@@ -0,0 +1,7 @@
// Overrides and adds customized javascripts in this file
// Read more on documentation:
// * English: https://github.com/consul/consul/blob/master/CUSTOMIZE_EN.md#javascript
// * Spanish: https://github.com/consul/consul/blob/master/CUSTOMIZE_ES.md#javascript
//
//

View File

@@ -36,12 +36,21 @@ body.admin {
input[type="text"], textarea { input[type="text"], textarea {
width: 100%; width: 100%;
} }
.input-group input[type="text"] {
border-radius: 0;
margin-bottom: 0 !important;
}
} }
table { table {
th { th {
text-align: left; text-align: left;
&.with-button {
line-height: $line-height*2;
}
} }
tr { tr {

View File

@@ -1,2 +1,5 @@
// Overrides and adds customized styles in this file // Overrides and adds customized styles in this file
// Read more on documentation:
// * English: https://github.com/consul/consul/blob/master/CUSTOMIZE_EN.md#css
// * Spanish: https://github.com/consul/consul/blob/master/CUSTOMIZE_ES.md#css
// //

View File

@@ -0,0 +1,15 @@
class Admin::BudgetGroupsController < Admin::BaseController
def create
@budget = Budget.find params[:budget_id]
@budget.groups.create(budget_group_params)
@groups = @budget.groups.includes(:headings)
end
private
def budget_group_params
params.require(:budget_group).permit(:name)
end
end

View File

@@ -0,0 +1,16 @@
class Admin::BudgetHeadingsController < Admin::BaseController
def create
@budget = Budget.find params[:budget_id]
@budget_group = @budget.groups.find params[:budget_group_id]
@budget_group.headings.create(budget_heading_params)
@headings = @budget_group.headings
end
private
def budget_heading_params
params.require(:budget_heading).permit(:name, :price, :geozone_id)
end
end

View File

@@ -0,0 +1,34 @@
class Admin::BudgetsController < Admin::BaseController
has_filters %w{open finished}, only: :index
load_and_authorize_resource
def index
@budgets = Budget.send(@current_filter).order(created_at: :desc).page(params[:page])
end
def show
@budget = Budget.includes(groups: :headings).find(params[:id])
end
def new
@budget = Budget.new
end
def create
@budget = Budget.new(budget_params)
if @budget.save
redirect_to admin_budget_path(@budget), notice: t('admin.budgets.create.notice')
else
render :new
end
end
private
def budget_params
params.require(:budget).permit(:name, :description, :phase, :currency_symbol)
end
end

View File

@@ -0,0 +1,11 @@
module BudgetsHelper
def budget_phases_select_options
Budget::VALID_PHASES.map { |ph| [ t("budget.phase.#{ph}"), ph ] }
end
def budget_currency_symbol_select_options
Budget::CURRENCY_SYMBOLS.map { |cs| [ cs, cs ] }
end
end

View File

@@ -8,4 +8,9 @@ module GeozonesHelper
Geozone.all.order(name: :asc).collect { |g| [ g.name, g.id ] } Geozone.all.order(name: :asc).collect { |g| [ g.name, g.id ] }
end end
def geozone_name_from_id(g_id)
@all_geozones ||= Geozone.all.collect{ |g| [ g.id, g.name ] }.to_h
@all_geozones[g_id] || t("geozones.none")
end
end end

View File

@@ -60,8 +60,8 @@ class Mailer < ApplicationMailer
end end
end end
def proposal_notification_digest(user) def proposal_notification_digest(user, notifications)
@notifications = user.notifications.where(notifiable_type: "ProposalNotification") @notifications = notifications
with_user(user) do with_user(user) do
mail(to: user.email, subject: t('mailers.proposal_notification_digest.title', org_name: Setting['org_name'])) mail(to: user.email, subject: t('mailers.proposal_notification_digest.title', org_name: Setting['org_name']))

View File

@@ -42,7 +42,9 @@ module Abilities
can [:read, :update, :valuate, :destroy, :summary], SpendingProposal can [:read, :update, :valuate, :destroy, :summary], SpendingProposal
can [:create, :update], Budget can [:index, :read, :new, :create, :update, :destroy], Budget
can [:read, :create, :update, :destroy], Budget::Group
can [:read, :create, :update, :destroy], Budget::Heading
can [:hide, :update], Budget::Investment can [:hide, :update], Budget::Investment
can :valuate, Budget::Investment, budget: { valuating: true } can :valuate, Budget::Investment, budget: { valuating: true }
can :create, Budget::ValuatorAssignment can :create, Budget::ValuatorAssignment

View File

@@ -3,7 +3,9 @@ class Budget < ActiveRecord::Base
include Sanitizable include Sanitizable
VALID_PHASES = %W{on_hold accepting selecting balloting finished} VALID_PHASES = %W{on_hold accepting selecting balloting finished}
CURRENCY_SYMBOLS = %W{€ $ £ ¥}
validates :name, presence: true
validates :phase, inclusion: { in: VALID_PHASES } validates :phase, inclusion: { in: VALID_PHASES }
validates :currency_symbol, presence: true validates :currency_symbol, presence: true
@@ -13,6 +15,9 @@ class Budget < ActiveRecord::Base
has_many :headings, through: :groups has_many :headings, through: :groups
has_many :investments, through: :headings has_many :investments, through: :headings
scope :open, -> { where.not(phase: "finished") }
scope :finished, -> { where(phase: "finished") }
def on_hold? def on_hold?
phase == "on_hold" phase == "on_hold"
end end

0
app/models/custom/.keep Normal file
View File

View File

@@ -0,0 +1,29 @@
require_dependency Rails.root.join('app', 'models', 'verification', 'residence').to_s
class Verification::Residence
validate :postal_code_in_madrid
validate :residence_in_madrid
def postal_code_in_madrid
errors.add(:postal_code, I18n.t('verification.residence.new.error_not_allowed_postal_code')) unless valid_postal_code?
end
def residence_in_madrid
return if errors.any?
unless residency_valid?
errors.add(:residence_in_madrid, false)
store_failed_attempt
Lock.increase_tries(user)
end
end
private
def valid_postal_code?
postal_code =~ /^280/
end
end

View File

@@ -4,8 +4,10 @@ class Notification < ActiveRecord::Base
scope :unread, -> { all } scope :unread, -> { all }
scope :recent, -> { order(id: :desc) } scope :recent, -> { order(id: :desc) }
scope :not_emailed, -> { where(emailed_at: nil) }
scope :for_render, -> { includes(:notifiable) } scope :for_render, -> { includes(:notifiable) }
def timestamp def timestamp
notifiable.created_at notifiable.created_at
end end

View File

@@ -95,7 +95,7 @@ class Proposal < ActiveRecord::Base
end end
def voters def voters
votes_for.voters User.active.where(id: votes_for.voters)
end end
def editable? def editable?

View File

@@ -53,6 +53,7 @@ class User < ActiveRecord::Base
scope :for_render, -> { includes(:organization) } scope :for_render, -> { includes(:organization) }
scope :by_document, -> (document_type, document_number) { where(document_type: document_type, document_number: document_number) } scope :by_document, -> (document_type, document_number) { where(document_type: document_type, document_number: document_number) }
scope :email_digest, -> { where(email_digest: true) } scope :email_digest, -> { where(email_digest: true) }
scope :active, -> { where(erased_at: nil) }
before_validation :clean_document_number before_validation :clean_document_number

View File

@@ -16,8 +16,6 @@ class Verification::Residence
validate :allowed_age validate :allowed_age
validate :document_number_uniqueness validate :document_number_uniqueness
validate :postal_code_in_madrid
validate :residence_in_madrid
def initialize(attrs={}) def initialize(attrs={})
self.date_of_birth = parse_date('date_of_birth', attrs) self.date_of_birth = parse_date('date_of_birth', attrs)
@@ -45,20 +43,6 @@ class Verification::Residence
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.where(document_number: document_number).any?
end end
def postal_code_in_madrid
errors.add(:postal_code, I18n.t('verification.residence.new.error_not_allowed_postal_code')) unless valid_postal_code?
end
def residence_in_madrid
return if errors.any?
unless residency_valid?
errors.add(:residence_in_madrid, false)
store_failed_attempt
Lock.increase_tries(user)
end
end
def store_failed_attempt def store_failed_attempt
FailedCensusCall.create({ FailedCensusCall.create({
user: user, user: user,
@@ -97,8 +81,4 @@ class Verification::Residence
self.document_number = self.document_number.gsub(/[^a-z0-9]+/i, "").upcase unless self.document_number.blank? self.document_number = self.document_number.gsub(/[^a-z0-9]+/i, "").upcase unless self.document_number.blank?
end end
def valid_postal_code?
postal_code =~ /^280/
end
end end

View File

@@ -35,6 +35,14 @@
</li> </li>
<% end %> <% end %>
<%# if feature?(:budgets) %>
<li <%= "class=active" if controller_name == "budgets" %>>
<%= link_to admin_budgets_path do %>
<span class="icon-budget"></span><%= t("admin.menu.budgets") %>
<% end %>
</li>
<%# end %>
<li <%= "class=active" if controller_name == "banners" %>> <li <%= "class=active" if controller_name == "banners" %>>
<%= link_to admin_banners_path do %> <%= link_to admin_banners_path do %>
<span class="icon-eye"></span><%= t("admin.menu.banner") %> <span class="icon-eye"></span><%= t("admin.menu.banner") %>

View File

@@ -0,0 +1,2 @@
$("#<%= dom_id(@budget) %>_groups").html('<%= j render("admin/budgets/groups", groups: @groups) %>');
App.Forms.toggleLink();

View File

@@ -0,0 +1,2 @@
$("#<%= dom_id(@budget_group) %>").html('<%= j render("admin/budgets/group", group: @budget_group, headings: @headings) %>');
App.Forms.toggleLink();

View File

@@ -0,0 +1,76 @@
<div class="small-12 column">
<table>
<thead>
<tr>
<th colspan="3" class="with-button">
<%= group.name %>
<%= link_to t("admin.budgets.form.add_heading"), "#", class: "button float-right js-toggle-link", data: { "toggle-selector" => "#group-#{group.id}-new-heading-form" } %>
</th>
</tr>
<% if headings.blank? %>
<tbody>
<tr>
<td colspan="3">
<div class="callout primary">
<%= t("admin.budgets.form.no_heading") %>
</div>
</td>
</tr>
<% else %>
<tr>
<th><%= t("admin.budgets.form.table_heading") %></th>
<th><%= t("admin.budgets.form.table_amount") %></th>
<th><%= t("admin.budgets.form.table_geozone") %></th>
</tr>
</thead>
<tbody>
<% end %>
<!-- new heading form -->
<tr id="group-<%= group.id %>-new-heading-form" style="display:none">
<td colspan="3">
<%= form_for [:admin, @budget, group, Budget::Heading.new], remote: true do |f| %>
<label><%= t("admin.budgets.form.heading") %></label>
<%= f.text_field :name,
label: false,
maxlength: 50,
placeholder: t("admin.budgets.form.heading") %>
<div class="row">
<div class="small-12 medium-6 column">
<label><%= t("admin.budgets.form.amount") %></label>
<%= f.text_field :price,
label: false,
maxlength: 8,
placeholder: t("admin.budgets.form.amount") %>
</div>
<div class="small-12 medium-6 column">
<label><%= t("admin.budgets.form.geozone") %></label>
<%= f.select :geozone_id, geozone_select_options, {include_blank: t("geozones.none"), label: false} %>
</div>
</div>
<%= f.submit t("admin.budgets.form.save_heading"), class: "button success" %>
<% end %>
</td>
</tr>
<!-- /. new heading form -->
<!-- headings list -->
<% headings.each do |heading| %>
<tr>
<td>
<%= heading.name %>
</td>
<td>
<%= heading.price %>
</td>
<td>
<%= geozone_name_from_id heading.geozone_id %>
</td>
</tr>
<% end %>
<!-- /. headings list -->
</tbody>
</table>
</div>

View File

@@ -0,0 +1,34 @@
<div class="small-12 column">
<h3 class="inline-block"><%= t('admin.budgets.show.groups') %></h3>
<% if groups.blank? %>
<div class="callout primary">
<%= t("admin.budgets.form.no_groups") %>
<strong><%= link_to t("admin.budgets.form.add_group"), "#",
class: "js-toggle-link",
data: { "toggle-selector" => "#new-group-form" } %></strong>
</div>
<% else %>
<%= link_to t("admin.budgets.form.add_group"), "#", class: "button float-right js-toggle-link", data: { "toggle-selector" => "#new-group-form" } %>
<% end %>
<%= form_for [:admin, @budget, Budget::Group.new], html: {id: "new-group-form", style: "display:none"}, remote: true do |f| %>
<div class="input-group">
<span class="input-group-label">
<label><%= f.label :name,t("admin.budgets.form.group") %></label>
</span>
<%= f.text_field :name,
label: false,
maxlength: 50,
placeholder: t("admin.budgets.form.group") %>
<div class="input-group-button">
<%= f.submit t("admin.budgets.form.create_group"), class: "button success" %>
</div>
</div>
<% end %>
<% groups.each do |group| %>
<div class="row" id="<%= dom_id(group) %>">
<%= render "admin/budgets/group", group: group, headings: group.headings %>
</div>
<% end %>
</div>

View File

@@ -0,0 +1,25 @@
<h2 class="inline-block"><%= t("admin.budgets.index.title") %></h2>
<%= link_to t("admin.budgets.index.new_link"),
new_admin_budget_path,
class: "button float-right margin-right" %>
<%= render 'shared/filter_subnav', i18n_namespace: "admin.budgets.index" %>
<h3><%= page_entries_info @budgets %></h3>
<table>
<% @budgets.each do |budget| %>
<tr id="<%= dom_id(budget) %>" class="budget">
<td>
<%= link_to budget.name, admin_budget_path(budget) %>
</td>
<td class="small">
<%= t("budget.phase.#{budget.phase}") %>
</td>
</tr>
<% end %>
</table>
<%= paginate @budgets %>

View File

@@ -0,0 +1,29 @@
<div class="row">
<div class="small-12 medium-9 column">
<h2><%= t("admin.budgets.new.title") %></h2>
<%= form_for [:admin, @budget] do |f| %>
<%= f.label :name, t("admin.budgets.new.name") %>
<%= f.text_field :name,
label: false,
maxlength: 30,
placeholder: t("admin.budgets.new.name") %>
<%= f.label :description, t("admin.budgets.new.description") %>
<%= f.text_area :description, rows: 3, maxlength: 6000, label: false, placeholder: t("admin.budgets.new.description") %>
<div class="row">
<div class="small-12 medium-9 column">
<%= f.label :description, t("admin.budgets.new.phase") %>
<%= f.select :phase, budget_phases_select_options, {label: false} %>
</div>
<div class="small-12 medium-3 column">
<%= f.label :description, t("admin.budgets.new.currency") %>
<%= f.select :currency_symbol, budget_currency_symbol_select_options, {label: false} %>
</div>
</div>
<%= f.submit t("admin.budgets.new.create"), class: "button success" %>
<% end %>
</div>
</div>

View File

@@ -0,0 +1,16 @@
<div class="row">
<div class="small-12 medium-9 column">
<h2><%= @budget.name %></h2>
<%= simple_format(text_with_links(@budget.description), {}, sanitize: false) %>
<p>
<strong><%= t('admin.budgets.show.phase') %>:</strong> <%= t("budget.phase.#{@budget.phase}") %> |
<strong><%= t('admin.budgets.show.currency') %>:</strong> <%= @budget.currency_symbol %>
</p>
</div>
</div>
<div id="<%= dom_id @budget %>_groups" class="row">
<%= render "groups", groups: @budget.groups %>
</div>

View File

@@ -74,6 +74,7 @@
<% if comment.children.size > 0 %> <% if comment.children.size > 0 %>
<%= link_to "", class: "js-toggle-children relative", data: {'id': "#{dom_id(comment)}"} do %> <%= link_to "", class: "js-toggle-children relative", data: {'id': "#{dom_id(comment)}"} do %>
<span class="sr-only"><%= t("shared.show") %></span>
<span id="<%= dom_id(comment) %>_children_arrow" class="icon-arrow-down"></span> <%= t("comments.comment.responses", count: comment.children.size) %> <span id="<%= dom_id(comment) %>_children_arrow" class="icon-arrow-down"></span> <%= t("comments.comment.responses", count: comment.children.size) %>
<% end %> <% end %>
<% else %> <% else %>

View File

@@ -7,7 +7,9 @@
<% if can?(:vote, comment) %> <% if can?(:vote, comment) %>
<%= link_to vote_comment_path(comment, value: 'yes'), <%= link_to vote_comment_path(comment, value: 'yes'),
method: "post", remote: true do %> method: "post", remote: true do %>
<span class="icon-angle-up"></span> <span class="icon-angle-up">
<span class="sr-only"><%= t('votes.agree') %></span>
</span>
<% end %> <% end %>
<% else %> <% else %>
<span class="icon-angle-up"></span> <span class="icon-angle-up"></span>
@@ -19,7 +21,9 @@
<% if can?(:vote, comment) %> <% if can?(:vote, comment) %>
<%= link_to vote_comment_path(comment, value: 'no'), <%= link_to vote_comment_path(comment, value: 'no'),
method: "post", remote: true do %> method: "post", remote: true do %>
<span class="icon-angle-down"></span> <span class="icon-angle-down">
<span class="sr-only"><%= t('votes.disagree') %></span>
</span>
<% end %> <% end %>
<% else %> <% else %>
<span class="icon-angle-down"></span> <span class="icon-angle-down"></span>

0
app/views/custom/.keep Normal file
View File

View File

@@ -3,7 +3,9 @@
<div class="in-favor inline-block"> <div class="in-favor inline-block">
<%= link_to vote_debate_path(debate, value: 'yes'), <%= link_to vote_debate_path(debate, value: 'yes'),
class: "like #{voted_classes[:in_favor]}", title: t('votes.agree'), method: "post", remote: true do %> class: "like #{voted_classes[:in_favor]}", title: t('votes.agree'), method: "post", remote: true do %>
<span class="icon-like"></span> <span class="icon-like">
<span class="sr-only"><%= t('votes.agree') %></span>
</span>
<span class="percentage"><%= votes_percentage('likes', debate) %></span> <span class="percentage"><%= votes_percentage('likes', debate) %></span>
<% end %> <% end %>
</div> </div>
@@ -12,7 +14,9 @@
<div class="against inline-block"> <div class="against inline-block">
<%= link_to vote_debate_path(debate, value: 'no'), class: "unlike #{voted_classes[:against]}", title: t('votes.disagree'), method: "post", remote: true do %> <%= link_to vote_debate_path(debate, value: 'no'), class: "unlike #{voted_classes[:against]}", title: t('votes.disagree'), method: "post", remote: true do %>
<span class="icon-unlike"></span> <span class="icon-unlike">
<span class="sr-only"><%= t('votes.disagree') %></span>
</span>
<span class="percentage"><%= votes_percentage('dislikes', debate) %></span> <span class="percentage"><%= votes_percentage('dislikes', debate) %></span>
<% end %> <% end %>
</div> </div>

View File

@@ -1,6 +1,7 @@
<% if user_signed_in? %> <% if user_signed_in? %>
<li> <li>
<%= link_to notifications_path, class: "notifications", accesskey: "n" do %> <%= link_to notifications_path, class: "notifications", accesskey: "n" do %>
<span class="sr-only"><%= t("layouts.header.notifications") %></span>
<% if current_user.notifications_count > 0 %> <% if current_user.notifications_count > 0 %>
<span class="icon-circle" aria-hidden="true"></span> <span class="icon-circle" aria-hidden="true"></span>
<span class="icon-notification" aria-hidden="true" title="<%= t('layouts.header.new_notifications', count: current_user.notifications_count).html_safe %>"> <span class="icon-notification" aria-hidden="true" title="<%= t('layouts.header.new_notifications', count: current_user.notifications_count).html_safe %>">

View File

@@ -5,13 +5,13 @@
</li> </li>
<% end %> <% end %>
<% if current_user.moderator? || current_user.administrator? %> <% if current_user.administrator? || current_user.moderator? %>
<li> <li>
<%= link_to t("layouts.header.moderation"), moderation_root_path %> <%= link_to t("layouts.header.moderation"), moderation_root_path %>
</li> </li>
<% end %> <% end %>
<% if feature?(:spending_proposals) && (current_user.valuator? || current_user.administrator?) %> <% if feature?(:spending_proposals) && (current_user.administrator? || current_user.valuator?) %>
<li> <li>
<%= link_to t("layouts.header.valuation"), valuation_root_path %> <%= link_to t("layouts.header.valuation"), valuation_root_path %>
</li> </li>

View File

@@ -21,24 +21,24 @@
<main> <main>
<div class="row text-center margin"> <div class="row text-center margin">
<div class="small-12 medium-3 column"> <div class="small-12 medium-3 column">
<%= image_tag("icon_home_debate.png", size: "168x168", alt: t("welcome.debates.alt"), title: t("welcome.debates.title")) %> <%= image_tag("icon_home_debate.png", size: "168x168", alt: "", title: t("welcome.debates.title")) %>
<h2><%= t("welcome.debates.title") %></h2> <h2><%= t("welcome.debates.title") %></h2>
<p><%= t("welcome.debates.description") %></p> <p><%= t("welcome.debates.description") %></p>
</div> </div>
<div class="small-12 medium-3 column"> <div class="small-12 medium-3 column">
<%= image_tag("icon_home_proposal.png", size: "168x168", alt: t("welcome.proposal.alt"), title: t("welcome.proposal.title")) %> <%= image_tag("icon_home_proposal.png", size: "168x168", alt: "", title: t("welcome.proposal.title")) %>
<h2><%= t("welcome.proposal.title") %></h2> <h2><%= t("welcome.proposal.title") %></h2>
<p><%= t("welcome.proposal.description") %></p> <p><%= t("welcome.proposal.description") %></p>
</div> </div>
<div class="small-12 medium-3 column"> <div class="small-12 medium-3 column">
<%= image_tag("icon_home_decide.png", size: "168x168", alt: t("welcome.decide.alt"), title: t("welcome.decide.title")) %> <%= image_tag("icon_home_decide.png", size: "168x168", alt: "", title: t("welcome.decide.title")) %>
<h2><%= t("welcome.decide.title") %></h2> <h2><%= t("welcome.decide.title") %></h2>
<p><%= t("welcome.decide.description") %></p> <p><%= t("welcome.decide.description") %></p>
</div> </div>
<div class="small-12 medium-3 column"> <div class="small-12 medium-3 column">
<%= image_tag("icon_home_do.png", size: "168x168", alt: t("welcome.do.alt"), title: t("welcome.do.title")) %> <%= image_tag("icon_home_do.png", size: "168x168", alt: "", title: t("welcome.do.title")) %>
<h2><%= t("welcome.do.title") %></h2> <h2><%= t("welcome.do.title") %></h2>
<p><%= t("welcome.do.description") %></p> <p><%= t("welcome.do.description") %></p>
</div> </div>

View File

@@ -1,3 +1,4 @@
require File.expand_path('../boot', __FILE__) require File.expand_path('../boot', __FILE__)
require 'rails/all' require 'rails/all'
@@ -34,5 +35,17 @@ module Consul
config.autoload_paths << Rails.root.join('lib') config.autoload_paths << Rails.root.join('lib')
config.time_zone = 'Madrid' config.time_zone = 'Madrid'
config.active_job.queue_adapter = :delayed_job config.active_job.queue_adapter = :delayed_job
# Consul specific custom overrides
# Read more on documentation:
# * English: https://github.com/consul/consul/blob/master/CUSTOMIZE_EN.md
# * Spanish: https://github.com/consul/consul/blob/master/CUSTOMIZE_ES.md
#
config.autoload_paths << "#{Rails.root}/app/controllers/custom"
config.autoload_paths << "#{Rails.root}/app/models/custom"
config.paths['app/views'].unshift(Rails.root.join('app', 'views', 'custom'))
end end
end end
require "./config/application_custom.rb"

View File

@@ -0,0 +1,4 @@
module Consul
class Application < Rails::Application
end
end

View File

@@ -112,6 +112,7 @@ ignore_unused:
- 'admin.banners.index.filters.*' - 'admin.banners.index.filters.*'
- 'admin.debates.index.filter*' - 'admin.debates.index.filter*'
- 'admin.proposals.index.filter*' - 'admin.proposals.index.filter*'
- 'admin.budgets.index.filter*'
- 'admin.spending_proposals.index.filter*' - 'admin.spending_proposals.index.filter*'
- 'admin.organizations.index.filter*' - 'admin.organizations.index.filter*'
- 'admin.users.index.filter*' - 'admin.users.index.filter*'

View File

@@ -9,8 +9,12 @@ Rails.application.config.assets.version = '1.0'
# Precompile additional assets. # Precompile additional assets.
# application.js, application.css, and all non-JS/CSS in app/assets folder are already added. # application.js, application.css, and all non-JS/CSS in app/assets folder are already added.
# Rails.application.config.assets.precompile += %w( search.js ) # Rails.application.config.assets.precompile += %w( search.js )
Rails.application.config.assets.precompile += %w( ckeditor/* ) Rails.application.config.assets.precompile += %w( ckeditor/config.js )
Rails.application.config.assets.precompile += %w( ie_lt9.js ) Rails.application.config.assets.precompile += %w( ie_lt9.js )
Rails.application.config.assets.precompile += %w( stat_graphs.js ) Rails.application.config.assets.precompile += %w( stat_graphs.js )
Rails.application.config.assets.precompile += %w( print.css ) Rails.application.config.assets.precompile += %w( print.css )
Rails.application.config.assets.precompile += %w( ie.css ) Rails.application.config.assets.precompile += %w( ie.css )
# Loads app/assets/images/custom before app/assets/images
images_path = Rails.application.config.assets.paths
images_path = images_path.insert(0, Rails.root.join("app", "assets", "images", "custom").to_s)

View File

@@ -0,0 +1,4 @@
Ckeditor.setup do |config|
config.assets_languages = I18n.available_locales.map(&:to_s)
config.assets_plugins = []
end

View File

@@ -4,6 +4,9 @@ en:
activity: activity:
one: "activity" one: "activity"
other: "activities" other: "activities"
budget:
one: "Participatory budget"
other: "Participatory budgets"
comment: comment:
one: "Comment" one: "Comment"
other: "Comments" other: "Comments"

View File

@@ -4,6 +4,9 @@ es:
activity: activity:
one: "actividad" one: "actividad"
other: "actividades" other: "actividades"
budget:
one: "Presupuesto participativo"
other: "Presupuestos participativos"
comment: comment:
one: "Comentario" one: "Comentario"
other: "Comentarios" other: "Comentarios"

View File

@@ -32,8 +32,6 @@ en:
editing: Edit banner editing: Edit banner
form: form:
submit_button: Save changes submit_button: Save changes
errors:
form:
errors: errors:
form: form:
error: error:
@@ -60,6 +58,40 @@ en:
on_users: Users on_users: Users
title: Moderator activity title: Moderator activity
type: Type type: Type
budgets:
index:
title: Participatory budgets
new_link: Create new
filters:
open: Open
finished: Finished
create:
notice: New participatory budget created successfully!
new:
title: New participatory budget
create: Create budget
name: Budget's name
description: Description
phase: Phase
currency: Currency
show:
phase: Current phase
currency: Currency
groups: Groups of budget headings
form:
group: Group's name
no_groups: No groups created yet. Each user will be able to vote in only one heading per group.
add_group: Add new group
create_group: Create group
heading: Heading's name
add_heading: Add heading
amount: Amount
save_heading: Save heading
no_heading: This group has no assigned heading.
geozone: Scope of operation
table_heading: Heading
table_amount: Amount
table_geozone: Scope of operation
comments: comments:
index: index:
filter: Filter filter: Filter
@@ -96,6 +128,7 @@ en:
activity: Moderator activity activity: Moderator activity
admin: Admin menu admin: Admin menu
banner: Manage banners banner: Manage banners
budgets: Participatory budgets
debate_topics: Debate topics debate_topics: Debate topics
hidden_comments: Hidden comments hidden_comments: Hidden comments
hidden_debates: Hidden debates hidden_debates: Hidden debates

View File

@@ -58,6 +58,40 @@ es:
on_users: Usuarios on_users: Usuarios
title: Actividad de los Moderadores title: Actividad de los Moderadores
type: Tipo type: Tipo
budgets:
index:
title: Presupuestos participativos
new_link: Crear nuevo
filters:
open: Abiertos
finished: Terminados
create:
notice: ¡Nueva campaña de presupuestos participativos creada con éxito!
new:
title: Nuevo presupuesto ciudadano
create: Crear presupuesto
name: Nombre del presupuesto
description: Descripción
phase: Fase
currency: Divisa
show:
phase: Fase actual
currency: Divisa
groups: Grupos de partidas presupuestarias
form:
group: Nombre del grupo
no_groups: No hay grupos creados todavía. Cada usuario podrá votar en una sola partida de cada grupo.
add_group: Añadir nuevo grupo
create_group: Crear grupo
heading: Nombre de la partida
add_heading: Añadir partida
amount: Cantidad
save_heading: Guardar partida
no_heading: Este grupo no tiene ninguna partida asignada.
geozone: Ámbito de actuación
table_heading: Partida
table_amount: Cantidad
table_geozone: Ámbito de actuación
comments: comments:
index: index:
filter: Filtro filter: Filtro
@@ -94,6 +128,7 @@ es:
activity: Actividad de moderadores activity: Actividad de moderadores
admin: Menú de administración admin: Menú de administración
banner: Gestionar banners banner: Gestionar banners
budgets: Presupuestos participativos
debate_topics: Temas de debate debate_topics: Temas de debate
hidden_comments: Comentarios ocultos hidden_comments: Comentarios ocultos
hidden_debates: Debates ocultos hidden_debates: Debates ocultos

View File

View File

@@ -33,6 +33,13 @@ en:
application: application:
close: Close close: Close
menu: Menu menu: Menu
budget:
phase:
on_hold: On hold
accepting: Accepting proposals
selecting: Selecting
balloting: Balloting
finished: Finished
comments: comments:
comment: comment:
admin: Administrator admin: Administrator
@@ -198,6 +205,7 @@ en:
more_information: More information more_information: More information
my_account_link: My account my_account_link: My account
my_activity_link: My activity my_activity_link: My activity
notifications: Notifications
new_notifications: new_notifications:
one: You have a new notification one: You have a new notification
other: You have %{count} new notifications other: You have %{count} new notifications
@@ -413,6 +421,7 @@ en:
flag: Flag as inappropriate flag: Flag as inappropriate
print: print:
print_button: Print this info print_button: Print this info
show: Show
suggest: suggest:
debate: debate:
found: found:
@@ -653,19 +662,15 @@ en:
not_voting_allowed: Voting phase is closed not_voting_allowed: Voting phase is closed
welcome: welcome:
debates: debates:
alt: Icon debates
description: For meeting, discussing and sharing the things that matter to us in our city. description: For meeting, discussing and sharing the things that matter to us in our city.
title: Debates title: Debates
decide: decide:
alt: Icon decide
description: The public decides if it accepts or rejects the most supported proposals. description: The public decides if it accepts or rejects the most supported proposals.
title: You decide title: You decide
do: do:
alt: Icon it gets done
description: If the proposal is accepted by the majority, the City Council accepts it as its own and it gets done. description: If the proposal is accepted by the majority, the City Council accepts it as its own and it gets done.
title: It gets done title: It gets done
proposal: proposal:
alt: Icon propose
description: Open space for citizen proposals about the kind of city we want to live in. description: Open space for citizen proposals about the kind of city we want to live in.
title: You propose title: You propose
verification: verification:

View File

@@ -33,6 +33,13 @@ es:
application: application:
close: Cerrar close: Cerrar
menu: Menú menu: Menú
budget:
phase:
on_hold: En pausa
accepting: Aceptando propuestas
selecting: Fase de selección
balloting: Fase de Votación
finished: Terminado
comments: comments:
comment: comment:
admin: Administrador admin: Administrador
@@ -198,6 +205,7 @@ es:
more_information: Más información more_information: Más información
my_account_link: Mi cuenta my_account_link: Mi cuenta
my_activity_link: Mi actividad my_activity_link: Mi actividad
notifications: Notificaciones
new_notifications: new_notifications:
one: Tienes una nueva notificación one: Tienes una nueva notificación
other: Tienes %{count} notificaciones nuevas other: Tienes %{count} notificaciones nuevas
@@ -413,6 +421,7 @@ es:
flag: Denunciar como inapropiado flag: Denunciar como inapropiado
print: print:
print_button: Imprimir esta información print_button: Imprimir esta información
show: Mostrar
suggest: suggest:
debate: debate:
found: found:
@@ -653,19 +662,15 @@ es:
not_voting_allowed: El periodo de votación está cerrado. not_voting_allowed: El periodo de votación está cerrado.
welcome: welcome:
debates: debates:
alt: Icono debates
description: Encontrarnos, debatir y compartir lo que nos parece importante en nuestra ciudad. description: Encontrarnos, debatir y compartir lo que nos parece importante en nuestra ciudad.
title: Debates title: Debates
decide: decide:
alt: Icono decides
description: La ciudadanía decide si acepta o rechaza las propuestas más apoyadas. description: La ciudadanía decide si acepta o rechaza las propuestas más apoyadas.
title: Decides title: Decides
do: do:
alt: Icono se hace
description: Si la propuesta es aceptada mayoritariamente, el Ayuntamiento la asume como propia y se hace. description: Si la propuesta es aceptada mayoritariamente, el Ayuntamiento la asume como propia y se hace.
title: Se hace title: Se hace
proposal: proposal:
alt: Icono propones
description: Espacio abierto para propuestas ciudadanas sobre el tipo de ciudad en el que queremos vivir. description: Espacio abierto para propuestas ciudadanas sobre el tipo de ciudad en el que queremos vivir.
title: Propones title: Propones
verification: verification:

View File

@@ -156,6 +156,13 @@ Rails.application.routes.draw do
get :summary, on: :collection get :summary, on: :collection
end end
resources :budgets do
resources :budget_groups do
resources :budget_headings do
end
end
end
resources :banners, only: [:index, :new, :create, :edit, :update, :destroy] do resources :banners, only: [:index, :new, :create, :edit, :update, :destroy] do
collection { get :search} collection { get :search}
end end

View File

@@ -0,0 +1,5 @@
class AddEmailedAtToNotifications < ActiveRecord::Migration
def change
add_column :notifications, :emailed_at, :datetime
end
end

View File

@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20160617172616) do ActiveRecord::Schema.define(version: 20160803154011) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@@ -210,10 +210,10 @@ ActiveRecord::Schema.define(version: 20160617172616) do
t.string "visit_id" t.string "visit_id"
t.datetime "hidden_at" t.datetime "hidden_at"
t.integer "flags_count", default: 0 t.integer "flags_count", default: 0
t.datetime "ignored_flag_at"
t.integer "cached_votes_total", default: 0 t.integer "cached_votes_total", default: 0
t.integer "cached_votes_up", default: 0 t.integer "cached_votes_up", default: 0
t.integer "cached_votes_down", default: 0 t.integer "cached_votes_down", default: 0
t.datetime "ignored_flag_at"
t.integer "comments_count", default: 0 t.integer "comments_count", default: 0
t.datetime "confirmed_hide_at" t.datetime "confirmed_hide_at"
t.integer "cached_anonymous_votes_total", default: 0 t.integer "cached_anonymous_votes_total", default: 0
@@ -341,6 +341,7 @@ ActiveRecord::Schema.define(version: 20160617172616) do
t.integer "notifiable_id" t.integer "notifiable_id"
t.string "notifiable_type" t.string "notifiable_type"
t.integer "counter", default: 1 t.integer "counter", default: 1
t.datetime "emailed_at"
end end
add_index "notifications", ["user_id"], name: "index_notifications_on_user_id", using: :btree add_index "notifications", ["user_id"], name: "index_notifications_on_user_id", using: :btree

View File

@@ -1,14 +1,27 @@
class EmailDigest class EmailDigest
def initialize attr_accessor :user, :notifications
def initialize(user)
@user = user
end end
def create def notifications
User.email_digest.each do |user| user.notifications.not_emailed.where(notifiable_type: "ProposalNotification")
if user.notifications.where(notifiable_type: "ProposalNotification").any? end
Mailer.proposal_notification_digest(user).deliver_later
def pending_notifications?
notifications.any?
end
def deliver
if pending_notifications?
Mailer.proposal_notification_digest(user, notifications.to_a).deliver_later
end end
end end
def mark_as_emailed
notifications.update_all(emailed_at: Time.now)
end end
end end

View File

@@ -2,8 +2,11 @@ namespace :emails do
desc "Sends email digest of proposal notifications to each user" desc "Sends email digest of proposal notifications to each user"
task digest: :environment do task digest: :environment do
email_digest = EmailDigest.new User.email_digest.find_each do |user|
email_digest.create email_digest = EmailDigest.new(user)
email_digest.deliver
email_digest.mark_as_emailed
end
end end
end end

View File

@@ -193,6 +193,10 @@ FactoryGirl.define do
currency_symbol "" currency_symbol ""
phase 'on_hold' phase 'on_hold'
trait :accepting do
phase 'accepting'
end
trait :selecting do trait :selecting do
phase 'selecting' phase 'selecting'
end end

View File

@@ -0,0 +1,158 @@
require 'rails_helper'
feature 'Admin budgets' do
background do
admin = create(:administrator)
login_as(admin.user)
end
context 'Feature flag' do
xscenario 'Disabled with a feature flag' do
Setting['feature.budgets'] = nil
expect{ visit admin_budgets_path }.to raise_exception(FeatureFlags::FeatureDisabled)
end
end
context 'Index' do
scenario 'Displaying budgets' do
budget = create(:budget)
visit admin_budgets_path
expect(page).to have_content(budget.name)
expect(page).to have_content(I18n.t("budget.phase.#{budget.phase}"))
end
scenario 'Filters by phase' do
budget1 = create(:budget)
budget2 = create(:budget, :accepting)
budget3 = create(:budget, :selecting)
budget4 = create(:budget, :balloting)
budget5 = create(:budget, :finished)
visit admin_budgets_path
expect(page).to have_content(budget1.name)
expect(page).to have_content(budget2.name)
expect(page).to have_content(budget3.name)
expect(page).to have_content(budget4.name)
expect(page).to_not have_content(budget5.name)
click_link 'Finished'
expect(page).to_not have_content(budget1.name)
expect(page).to_not have_content(budget2.name)
expect(page).to_not have_content(budget3.name)
expect(page).to_not have_content(budget4.name)
expect(page).to have_content(budget5.name)
click_link 'Open'
expect(page).to have_content(budget1.name)
expect(page).to have_content(budget2.name)
expect(page).to have_content(budget3.name)
expect(page).to have_content(budget4.name)
expect(page).to_not have_content(budget5.name)
end
scenario 'Current filter is properly highlighted' do
filters_links = {'open' => 'Open', 'finished' => 'Finished'}
visit admin_budgets_path
expect(page).to_not have_link(filters_links.values.first)
filters_links.keys.drop(1).each { |filter| expect(page).to have_link(filters_links[filter]) }
filters_links.each_pair do |current_filter, link|
visit admin_budgets_path(filter: current_filter)
expect(page).to_not have_link(link)
(filters_links.keys - [current_filter]).each do |filter|
expect(page).to have_link(filters_links[filter])
end
end
end
end
context 'New' do
scenario 'Create budget' do
visit admin_budgets_path
click_link 'Create new'
fill_in 'budget_name', with: 'M30 - Summer campaign'
fill_in 'budget_description', with: 'Budgeting for summer 2017 maintenance and improvements of the road M-30'
select 'Accepting proposals', from: 'budget[phase]'
click_button 'Create budget'
expect(page).to have_content 'New participatory budget created successfully!'
expect(page).to have_content 'M30 - Summer campaign'
end
scenario 'Name is mandatory' do
visit new_admin_budget_path
click_button 'Create budget'
expect(page).to_not have_content 'New participatory budget created successfully!'
expect(page).to have_css("label.error", text: "Budget's name")
end
end
context 'Manage groups and headings' do
scenario 'Create group', :js do
create(:budget, name: 'Yearly participatory budget')
visit admin_budgets_path
click_link 'Yearly participatory budget'
expect(page).to have_content 'No groups created yet.'
click_link 'Add new group'
fill_in 'budget_group_name', with: 'General improvments'
click_button 'Create group'
expect(page).to have_content 'Yearly participatory budget'
expect(page).to_not have_content 'No groups created yet.'
visit admin_budgets_path
click_link 'Yearly participatory budget'
expect(page).to have_content 'Yearly participatory budget'
expect(page).to_not have_content 'No groups created yet.'
end
scenario 'Create heading', :js do
budget = create(:budget, name: 'Yearly participatory budget')
group = create(:budget_group, budget: budget, name: 'Districts improvments')
visit admin_budget_path(budget)
within("#budget_group_#{group.id}") do
expect(page).to have_content 'This group has no assigned heading.'
click_link 'Add heading'
fill_in 'budget_heading_name', with: 'District 9 reconstruction'
fill_in 'budget_heading_price', with: '6785'
click_button 'Save heading'
end
expect(page).to_not have_content 'This group has no assigned heading.'
visit admin_budget_path(budget)
within("#budget_group_#{group.id}") do
expect(page).to_not have_content 'This group has no assigned heading.'
expect(page).to have_content 'District 9 reconstruction'
expect(page).to have_content '6785'
expect(page).to have_content 'All city'
end
end
end
end

View File

@@ -201,8 +201,9 @@ feature 'Emails' do
notification2 = create_proposal_notification(proposal2) notification2 = create_proposal_notification(proposal2)
notification3 = create_proposal_notification(proposal3) notification3 = create_proposal_notification(proposal3)
email_digest = EmailDigest.new email_digest = EmailDigest.new(user)
email_digest.create email_digest.deliver
email_digest.mark_as_emailed
email = open_last_email email = open_last_email
expect(email).to have_subject("Proposal notifications in Consul") expect(email).to have_subject("Proposal notifications in Consul")
@@ -227,6 +228,11 @@ feature 'Emails' do
expect(email).to_not have_body_text(proposal3.title) expect(email).to_not have_body_text(proposal3.title)
expect(email).to have_body_text(/#{account_path}/) expect(email).to have_body_text(/#{account_path}/)
notification1.reload
notification2.reload
expect(notification1.emailed_at).to be
expect(notification2.emailed_at).to be
end end
end end

View File

@@ -180,9 +180,10 @@ feature "Notifications" do
find(".icon-notification").click find(".icon-notification").click
notification_for_user1 = Notification.where(user: user1).first
expect(page).to have_css ".notification", count: 1 expect(page).to have_css ".notification", count: 1
expect(page).to have_content "There is one new notification on #{proposal.title}" expect(page).to have_content "There is one new notification on #{proposal.title}"
expect(page).to have_xpath "//a[@href='#{notification_path(Notification.last)}']" expect(page).to have_xpath "//a[@href='#{notification_path(notification_for_user1)}']"
logout logout
login_as user2 login_as user2
@@ -190,9 +191,10 @@ feature "Notifications" do
find(".icon-notification").click find(".icon-notification").click
notification_for_user2 = Notification.where(user: user2).first
expect(page).to have_css ".notification", count: 1 expect(page).to have_css ".notification", count: 1
expect(page).to have_content "There is one new notification on #{proposal.title}" expect(page).to have_content "There is one new notification on #{proposal.title}"
expect(page).to have_xpath "//a[@href='#{notification_path(Notification.first)}']" expect(page).to have_xpath "//a[@href='#{notification_path(notification_for_user2)}']"
logout logout
login_as user3 login_as user3

View File

@@ -24,6 +24,44 @@ feature 'Proposal Notifications' do
expect(page).to have_content "Please share it with others so we can make it happen!" expect(page).to have_content "Please share it with others so we can make it happen!"
end end
scenario "Send a notification (Active voter)" do
author = create(:user)
proposal = create(:proposal, author: author)
voter = create(:user, :level_two)
create(:vote, voter: voter, votable: proposal)
create_proposal_notification(proposal)
expect(Notification.count).to eq(1)
end
scenario "Send a notification (Blocked voter)" do
author = create(:user)
proposal = create(:proposal, author: author)
voter = create(:user, :level_two)
create(:vote, voter: voter, votable: proposal)
voter.block
create_proposal_notification(proposal)
expect(Notification.count).to eq(0)
end
scenario "Send a notification (Erased voter)" do
author = create(:user)
proposal = create(:proposal, author: author)
voter = create(:user, :level_two)
create(:vote, voter: voter, votable: proposal)
voter.erase
create_proposal_notification(proposal)
expect(Notification.count).to eq(0)
end
scenario "Show notifications" do scenario "Show notifications" do
proposal = create(:proposal) proposal = create(:proposal)
notification1 = create(:proposal_notification, proposal: proposal, title: "Hey guys", body: "Just wanted to let you know that...") notification1 = create(:proposal_notification, proposal: proposal, title: "Hey guys", body: "Just wanted to let you know that...")

View File

@@ -31,4 +31,19 @@ describe GeozonesHelper do
end end
end end
describe "#geozone_name_from_id" do
it "returns geozone name if present" do
g1 = create(:geozone, name: "AAA")
g2 = create(:geozone, name: "BBB")
expect(geozone_name_from_id(g1.id)).to eq "AAA"
expect(geozone_name_from_id(g2.id)).to eq "BBB"
end
it "returns default string for no geozone if geozone is blank" do
expect(geozone_name_from_id(nil)).to eq "All city"
end
end
end end

View File

@@ -2,8 +2,126 @@ require 'rails_helper'
describe EmailDigest do describe EmailDigest do
describe "create" do describe "notifications" do
pending "only send unread notifications"
it "returns notifications for a user" do
user1 = create(:user)
user2 = create(:user)
proposal_notification = create(:proposal_notification)
notification1 = create(:notification, notifiable: proposal_notification, user: user1)
notification2 = create(:notification, notifiable: proposal_notification, user: user2)
email_digest = EmailDigest.new(user1)
expect(email_digest.notifications).to include(notification1)
expect(email_digest.notifications).to_not include(notification2)
end
it "returns only proposal notifications" do
user = create(:user)
proposal_notification = create(:proposal_notification)
comment = create(:comment)
notification1 = create(:notification, notifiable: proposal_notification, user: user)
notification2 = create(:notification, notifiable: comment, user: user)
email_digest = EmailDigest.new(user)
expect(email_digest.notifications).to include(notification1)
expect(email_digest.notifications).to_not include(notification2)
end
end
describe "pending_notifications?" do
it "returns true when notifications have not been emailed" do
user = create(:user)
proposal_notification = create(:proposal_notification)
notification = create(:notification, notifiable: proposal_notification, user: user)
email_digest = EmailDigest.new(user)
expect(email_digest.pending_notifications?).to be
end
it "returns false when notifications have been emailed" do
user = create(:user)
proposal_notification = create(:proposal_notification)
notification = create(:notification, notifiable: proposal_notification, user: user, emailed_at: Time.now)
email_digest = EmailDigest.new(user)
expect(email_digest.pending_notifications?).to_not be
end
it "returns false when there are no notifications for a user" do
user = create(:user)
email_digest = EmailDigest.new(user)
expect(email_digest.pending_notifications?).to_not be
end
end
describe "deliver" do
it "delivers email if notifications pending" do
user = create(:user)
proposal_notification = create(:proposal_notification)
notification = create(:notification, notifiable: proposal_notification, user: user)
reset_mailer
email_digest = EmailDigest.new(user)
email_digest.deliver
email = open_last_email
expect(email).to have_subject("Proposal notifications in Consul")
end
it "does not deliver email if no notifications pending" do
user = create(:user)
proposal_notification = create(:proposal_notification)
notification = create(:notification, notifiable: proposal_notification, user: user, emailed_at: Time.now)
reset_mailer
email_digest = EmailDigest.new(user)
email_digest.deliver
expect(all_emails.count).to eq(0)
end
end
describe "mark_as_emailed" do
it "marks notifications as emailed" do
user1 = create(:user)
user2 = create(:user)
proposal_notification = create(:proposal_notification)
notification1 = create(:notification, notifiable: proposal_notification, user: user1)
notification2 = create(:notification, notifiable: proposal_notification, user: user1)
notification3 = create(:notification, notifiable: proposal_notification, user: user2)
expect(notification1.emailed_at).to_not be
expect(notification2.emailed_at).to_not be
expect(notification3.emailed_at).to_not be
email_digest = EmailDigest.new(user1)
email_digest.mark_as_emailed
notification1.reload
notification2.reload
notification3.reload
expect(notification1.emailed_at).to be
expect(notification2.emailed_at).to be
expect(notification3.emailed_at).to_not be
end
end end
end end

0
spec/models/custom/.keep Normal file
View File

View File

@@ -0,0 +1,34 @@
require 'rails_helper'
describe Verification::Residence do
let(:residence) { build(:verification_residence, document_number: "12345678Z") }
describe "verification" do
describe "postal code" do
it "should be valid with postal codes starting with 280" do
residence.postal_code = "28012"
residence.valid?
expect(residence.errors[:postal_code].size).to eq(0)
residence.postal_code = "28023"
residence.valid?
expect(residence.errors[:postal_code].size).to eq(0)
end
it "should not be valid with postal codes not starting with 280" do
residence.postal_code = "12345"
residence.valid?
expect(residence.errors[:postal_code].size).to eq(1)
residence.postal_code = "13280"
residence.valid?
expect(residence.errors[:postal_code].size).to eq(1)
expect(residence.errors[:postal_code]).to include("In order to be verified, you must be registered.")
end
end
end
end

View File

@@ -367,6 +367,50 @@ describe Proposal do
end end
end end
describe "voters" do
it "returns users that have voted for the proposal" do
proposal = create(:proposal)
voter1 = create(:user, :level_two)
voter2 = create(:user, :level_two)
voter3 = create(:user, :level_two)
create(:vote, voter: voter1, votable: proposal)
create(:vote, voter: voter2, votable: proposal)
expect(proposal.voters).to include(voter1)
expect(proposal.voters).to include(voter2)
expect(proposal.voters).to_not include(voter3)
end
it "does not return users that have been erased" do
proposal = create(:proposal)
voter1 = create(:user, :level_two)
voter2 = create(:user, :level_two)
create(:vote, voter: voter1, votable: proposal)
create(:vote, voter: voter2, votable: proposal)
voter2.erase
expect(proposal.voters).to include(voter1)
expect(proposal.voters).to_not include(voter2)
end
it "does not return users that have been blocked" do
proposal = create(:proposal)
voter1 = create(:user, :level_two)
voter2 = create(:user, :level_two)
create(:vote, voter: voter1, votable: proposal)
create(:vote, voter: voter2, votable: proposal)
voter2.block
expect(proposal.voters).to include(voter1)
expect(proposal.voters).to_not include(voter2)
end
end
describe "search" do describe "search" do
context "attributes" do context "attributes" do

View File

@@ -30,29 +30,6 @@ describe Verification::Residence do
expect(residence.errors[:date_of_birth]).to include("You must be at least 16 years old") expect(residence.errors[:date_of_birth]).to include("You must be at least 16 years old")
end end
describe "postal code" do
it "should be valid with postal codes starting with 280" do
residence.postal_code = "28012"
residence.valid?
expect(residence.errors[:postal_code].size).to eq(0)
residence.postal_code = "28023"
residence.valid?
expect(residence.errors[:postal_code].size).to eq(0)
end
it "should not be valid with postal codes not starting with 280" do
residence.postal_code = "12345"
residence.valid?
expect(residence.errors[:postal_code].size).to eq(1)
residence.postal_code = "13280"
residence.valid?
expect(residence.errors[:postal_code].size).to eq(1)
expect(residence.errors[:postal_code]).to include("In order to be verified, you must be registered.")
end
end
it "should validate uniquness of document_number" do it "should validate uniquness of document_number" do
user = create(:user) user = create(:user)
residence.user = user residence.user = user

View File

@@ -325,6 +325,34 @@ describe User do
end end
describe "scopes" do
describe "active" do
it "returns users that have not been erased" do
user1 = create(:user, erased_at: nil)
user2 = create(:user, erased_at: nil)
user3 = create(:user, erased_at: Time.now)
expect(User.active).to include(user1)
expect(User.active).to include(user2)
expect(User.active).to_not include(user3)
end
it "returns users that have not been blocked" do
user1 = create(:user)
user2 = create(:user)
user3 = create(:user)
user3.block
expect(User.active).to include(user1)
expect(User.active).to include(user2)
expect(User.active).to_not include(user3)
end
end
end
describe "self.search" do describe "self.search" do
it "find users by email" do it "find users by email" do
user1 = create(:user, email: "larry@consul.dev") user1 = create(:user, email: "larry@consul.dev")