diff --git a/CUSTOMIZE_ES.md b/CUSTOMIZE_ES.md new file mode 100644 index 000000000..71c3701e4 --- /dev/null +++ b/CUSTOMIZE_ES.md @@ -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 +``` diff --git a/Gemfile b/Gemfile index 2ea2eb5c4..1819992d3 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,7 @@ source 'https://rubygems.org' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' -gem 'rails', '4.2.7' +gem 'rails', '4.2.7.1' # Use PostgreSQL gem 'pg' # 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 gem 'turbolinks' +# Fix sprockets on the +gem 'sprockets', '~> 3.6.3' + gem 'devise', '~> 3.5.7' # Use ActiveModel has_secure_password # 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 gem 'web-console', '3.3.0' end + +eval_gemfile './Gemfile_custom' diff --git a/Gemfile.lock b/Gemfile.lock index ad24ae9ec..121d7b86e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,36 +1,36 @@ GEM remote: https://rubygems.org/ specs: - actionmailer (4.2.7) - actionpack (= 4.2.7) - actionview (= 4.2.7) - activejob (= 4.2.7) + actionmailer (4.2.7.1) + actionpack (= 4.2.7.1) + actionview (= 4.2.7.1) + activejob (= 4.2.7.1) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 1.0, >= 1.0.5) - actionpack (4.2.7) - actionview (= 4.2.7) - activesupport (= 4.2.7) + actionpack (4.2.7.1) + actionview (= 4.2.7.1) + activesupport (= 4.2.7.1) 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) - activesupport (= 4.2.7) + actionview (4.2.7.1) + activesupport (= 4.2.7.1) 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) - activesupport (= 4.2.7) + activejob (4.2.7.1) + activesupport (= 4.2.7.1) globalid (>= 0.3.0) - activemodel (4.2.7) - activesupport (= 4.2.7) + activemodel (4.2.7.1) + activesupport (= 4.2.7.1) builder (~> 3.1) - activerecord (4.2.7) - activemodel (= 4.2.7) - activesupport (= 4.2.7) + activerecord (4.2.7.1) + activemodel (= 4.2.7.1) + activesupport (= 4.2.7.1) arel (~> 6.0) - activesupport (4.2.7) + activesupport (4.2.7.1) i18n (~> 0.7) json (~> 1.7, >= 1.7.7) minitest (~> 5.1) @@ -67,7 +67,7 @@ GEM bcrypt (3.1.11) browser (2.2.0) builder (3.2.2) - bullet (5.1.1) + bullet (5.2.0) activesupport (>= 3.0.0) uniform_notifier (~> 1.10.0) byebug (9.0.5) @@ -114,12 +114,12 @@ GEM execjs coffee-script-source (1.10.0) concurrent-ruby (1.0.2) - coveralls (0.8.14) + coveralls (0.8.15) json (>= 1.8, < 3) simplecov (~> 0.12.0) term-ansicolor (~> 1.3) thor (~> 0.19.1) - tins (~> 1.6.0) + tins (>= 1.6.0, < 2) daemons (1.2.3) dalli (2.7.6) database_cleaner (1.5.3) @@ -156,7 +156,7 @@ GEM factory_girl_rails (4.7.0) factory_girl (~> 4.7.0) railties (>= 3.0.0) - faker (1.6.5) + faker (1.6.6) i18n (~> 0.5) faraday (0.9.2) multipart-post (>= 1.2, < 3) @@ -174,7 +174,7 @@ GEM rspec (~> 3.0) ruby-progressbar (~> 1.4) geocoder (1.3.7) - globalid (0.3.6) + globalid (0.3.7) activesupport (>= 4.1.0) groupdate (3.0.1) activesupport (>= 3) @@ -290,16 +290,16 @@ GEM rack rack-test (0.6.3) rack (>= 1.0) - rails (4.2.7) - actionmailer (= 4.2.7) - actionpack (= 4.2.7) - actionview (= 4.2.7) - activejob (= 4.2.7) - activemodel (= 4.2.7) - activerecord (= 4.2.7) - activesupport (= 4.2.7) + 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) bundler (>= 1.3.0, < 2.0) - railties (= 4.2.7) + railties (= 4.2.7.1) sprockets-rails rails-deprecated_sanitizer (1.0.3) activesupport (>= 4.2.0.alpha) @@ -309,9 +309,9 @@ GEM rails-deprecated_sanitizer (>= 1.0.1) rails-html-sanitizer (1.0.3) loofah (~> 2.0) - railties (4.2.7) - actionpack (= 4.2.7) - activesupport (= 4.2.7) + railties (4.2.7.1) + actionpack (= 4.2.7.1) + activesupport (= 4.2.7.1) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) raindrops (0.16.0) @@ -350,7 +350,7 @@ GEM safely_block (0.1.1) errbase sass (3.4.22) - sass-rails (5.0.5) + sass-rails (5.0.6) railties (>= 4.0.0, < 6) sass (~> 3.1) sprockets (>= 2.8, < 4.0) @@ -396,7 +396,7 @@ GEM thread (0.2.2) thread_safe (0.3.5) tilt (2.0.5) - tins (1.6.0) + tins (1.11.0) tolk (1.9.3) rails (>= 4.0, < 4.3) safe_yaml (>= 0.8.6) @@ -408,7 +408,7 @@ GEM tilt (>= 1.4, < 3) tzinfo (1.2.2) thread_safe (~> 0.1) - uglifier (3.0.0) + uglifier (3.0.1) execjs (>= 0.3.0, < 3) unicorn (5.1.0) kgio (~> 2.6) @@ -485,7 +485,7 @@ DEPENDENCIES pg_search poltergeist quiet_assets - rails (= 4.2.7) + rails (= 4.2.7.1) redcarpet responders rinku @@ -496,6 +496,7 @@ DEPENDENCIES social-share-button spring spring-commands-rspec + sprockets (~> 3.6.3) tolk turbolinks turnout diff --git a/Gemfile_custom b/Gemfile_custom new file mode 100644 index 000000000..9d91e0680 --- /dev/null +++ b/Gemfile_custom @@ -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 +# diff --git a/README.md b/README.md index 7a92401f3..27162bfd7 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,10 @@ But for some actions like voting, you will need a verified user, the seeds file **user:** verified@consul.dev **pass:** 12345678 +### Customization + +See [CUSTOMIZE_ES.md](CUSTOMIZE_ES.md) + ### 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* diff --git a/README_ES.md b/README_ES.md index 489412299..d8c394680 100644 --- a/README_ES.md +++ b/README_ES.md @@ -61,6 +61,9 @@ Pero para ciertas acciones, como apoyar, necesitarás un usuario verificado, el **user:** verified@consul.dev **pass:** 12345678 +### Customización + +Ver fichero [CUSTOMIZE_ES.md](CUSTOMIZE_ES.md) ### OAuth diff --git a/app/assets/images/custom/.keep b/app/assets/images/custom/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index a1b8dd70a..7fb92b5e3 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -16,7 +16,7 @@ //= require jquery-ui/datepicker-es //= require foundation //= require turbolinks -//= require ckeditor/init +//= require ckeditor/loader //= require_directory ./ckeditor //= require social-share-button //= require initial @@ -45,6 +45,7 @@ //= require valuation_spending_proposal_form //= require embed_video //= require banners +//= require custom var initialize_modules = function() { App.Comments.initialize(); diff --git a/app/assets/javascripts/ckeditor/loader.js.erb b/app/assets/javascripts/ckeditor/loader.js.erb new file mode 100644 index 000000000..66e1d8347 --- /dev/null +++ b/app/assets/javascripts/ckeditor/loader.js.erb @@ -0,0 +1,3 @@ +//= require ckeditor/init + +CKEDITOR.config.customConfig = '<%= javascript_path 'ckeditor/config.js' %>'; diff --git a/app/assets/javascripts/custom.js b/app/assets/javascripts/custom.js new file mode 100644 index 000000000..6c880b3a9 --- /dev/null +++ b/app/assets/javascripts/custom.js @@ -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 +// +// + diff --git a/app/assets/stylesheets/admin.scss b/app/assets/stylesheets/admin.scss index cd8241fca..196d0be17 100644 --- a/app/assets/stylesheets/admin.scss +++ b/app/assets/stylesheets/admin.scss @@ -36,12 +36,21 @@ body.admin { input[type="text"], textarea { width: 100%; } + + .input-group input[type="text"] { + border-radius: 0; + margin-bottom: 0 !important; + } } table { th { text-align: left; + + &.with-button { + line-height: $line-height*2; + } } tr { diff --git a/app/assets/stylesheets/custom.scss b/app/assets/stylesheets/custom.scss index c764f4ad2..090eb0342 100644 --- a/app/assets/stylesheets/custom.scss +++ b/app/assets/stylesheets/custom.scss @@ -1,2 +1,5 @@ // Overrides and adds customized styles in this file -// \ No newline at end of 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 +// diff --git a/app/controllers/admin/budget_groups_controller.rb b/app/controllers/admin/budget_groups_controller.rb new file mode 100644 index 000000000..18f5a6b12 --- /dev/null +++ b/app/controllers/admin/budget_groups_controller.rb @@ -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 \ No newline at end of file diff --git a/app/controllers/admin/budget_headings_controller.rb b/app/controllers/admin/budget_headings_controller.rb new file mode 100644 index 000000000..3c8ccafa0 --- /dev/null +++ b/app/controllers/admin/budget_headings_controller.rb @@ -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 \ No newline at end of file diff --git a/app/controllers/admin/budgets_controller.rb b/app/controllers/admin/budgets_controller.rb new file mode 100644 index 000000000..144b43a7f --- /dev/null +++ b/app/controllers/admin/budgets_controller.rb @@ -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 diff --git a/app/helpers/budgets_helper.rb b/app/helpers/budgets_helper.rb new file mode 100644 index 000000000..d281ef182 --- /dev/null +++ b/app/helpers/budgets_helper.rb @@ -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 \ No newline at end of file diff --git a/app/helpers/geozones_helper.rb b/app/helpers/geozones_helper.rb index bfc5f9105..ce03e0579 100644 --- a/app/helpers/geozones_helper.rb +++ b/app/helpers/geozones_helper.rb @@ -8,4 +8,9 @@ module GeozonesHelper Geozone.all.order(name: :asc).collect { |g| [ g.name, g.id ] } 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 diff --git a/app/mailers/mailer.rb b/app/mailers/mailer.rb index ad87359af..25c019857 100644 --- a/app/mailers/mailer.rb +++ b/app/mailers/mailer.rb @@ -60,8 +60,8 @@ class Mailer < ApplicationMailer end end - def proposal_notification_digest(user) - @notifications = user.notifications.where(notifiable_type: "ProposalNotification") + def proposal_notification_digest(user, notifications) + @notifications = notifications with_user(user) do mail(to: user.email, subject: t('mailers.proposal_notification_digest.title', org_name: Setting['org_name'])) diff --git a/app/models/abilities/administrator.rb b/app/models/abilities/administrator.rb index 42e4ecbd3..f3f0b8f9b 100644 --- a/app/models/abilities/administrator.rb +++ b/app/models/abilities/administrator.rb @@ -42,7 +42,9 @@ module Abilities 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 :valuate, Budget::Investment, budget: { valuating: true } can :create, Budget::ValuatorAssignment diff --git a/app/models/budget.rb b/app/models/budget.rb index c79210b18..b8fd34da9 100644 --- a/app/models/budget.rb +++ b/app/models/budget.rb @@ -3,7 +3,9 @@ class Budget < ActiveRecord::Base include Sanitizable VALID_PHASES = %W{on_hold accepting selecting balloting finished} + CURRENCY_SYMBOLS = %W{€ $ £ ¥} + validates :name, presence: true validates :phase, inclusion: { in: VALID_PHASES } validates :currency_symbol, presence: true @@ -13,6 +15,9 @@ class Budget < ActiveRecord::Base has_many :headings, through: :groups has_many :investments, through: :headings + scope :open, -> { where.not(phase: "finished") } + scope :finished, -> { where(phase: "finished") } + def on_hold? phase == "on_hold" end diff --git a/app/models/custom/.keep b/app/models/custom/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/app/models/custom/verification/residence.rb b/app/models/custom/verification/residence.rb new file mode 100644 index 000000000..1cbc6f7ab --- /dev/null +++ b/app/models/custom/verification/residence.rb @@ -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 diff --git a/app/models/notification.rb b/app/models/notification.rb index 9695c1b01..c6c32eb8d 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -2,9 +2,11 @@ class Notification < ActiveRecord::Base belongs_to :user, counter_cache: true belongs_to :notifiable, polymorphic: true - scope :unread, -> { all } - scope :recent, -> { order(id: :desc) } - scope :for_render, -> { includes(:notifiable) } + scope :unread, -> { all } + scope :recent, -> { order(id: :desc) } + scope :not_emailed, -> { where(emailed_at: nil) } + scope :for_render, -> { includes(:notifiable) } + def timestamp notifiable.created_at diff --git a/app/models/proposal.rb b/app/models/proposal.rb index b6a8ddb26..7ccf8995f 100644 --- a/app/models/proposal.rb +++ b/app/models/proposal.rb @@ -95,7 +95,7 @@ class Proposal < ActiveRecord::Base end def voters - votes_for.voters + User.active.where(id: votes_for.voters) end def editable? diff --git a/app/models/user.rb b/app/models/user.rb index bf252a6fc..195328273 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -53,6 +53,7 @@ class User < ActiveRecord::Base scope :for_render, -> { includes(:organization) } 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) } before_validation :clean_document_number diff --git a/app/models/verification/residence.rb b/app/models/verification/residence.rb index 5756af2b3..cc24bb7c8 100644 --- a/app/models/verification/residence.rb +++ b/app/models/verification/residence.rb @@ -16,8 +16,6 @@ class Verification::Residence validate :allowed_age validate :document_number_uniqueness - validate :postal_code_in_madrid - validate :residence_in_madrid def initialize(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? 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 FailedCensusCall.create({ 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? end - def valid_postal_code? - postal_code =~ /^280/ - end - end diff --git a/app/views/admin/_menu.html.erb b/app/views/admin/_menu.html.erb index 7ea48b2c6..d1feaeb1e 100644 --- a/app/views/admin/_menu.html.erb +++ b/app/views/admin/_menu.html.erb @@ -35,6 +35,14 @@ <% end %> + <%# if feature?(:budgets) %> +
  • > + <%= link_to admin_budgets_path do %> + <%= t("admin.menu.budgets") %> + <% end %> +
  • + <%# end %> +
  • > <%= link_to admin_banners_path do %> <%= t("admin.menu.banner") %> diff --git a/app/views/admin/budget_groups/create.js.erb b/app/views/admin/budget_groups/create.js.erb new file mode 100644 index 000000000..cb926a7c6 --- /dev/null +++ b/app/views/admin/budget_groups/create.js.erb @@ -0,0 +1,2 @@ +$("#<%= dom_id(@budget) %>_groups").html('<%= j render("admin/budgets/groups", groups: @groups) %>'); +App.Forms.toggleLink(); \ No newline at end of file diff --git a/app/views/admin/budget_headings/create.js.erb b/app/views/admin/budget_headings/create.js.erb new file mode 100644 index 000000000..5d8eefb2d --- /dev/null +++ b/app/views/admin/budget_headings/create.js.erb @@ -0,0 +1,2 @@ +$("#<%= dom_id(@budget_group) %>").html('<%= j render("admin/budgets/group", group: @budget_group, headings: @headings) %>'); +App.Forms.toggleLink(); \ No newline at end of file diff --git a/app/views/admin/budgets/_group.html.erb b/app/views/admin/budgets/_group.html.erb new file mode 100644 index 000000000..3660fa0c1 --- /dev/null +++ b/app/views/admin/budgets/_group.html.erb @@ -0,0 +1,76 @@ +
    + + + + + + + <% if headings.blank? %> + + + + + <% else %> + + + + + + + + <% end %> + + + + + + + + <% headings.each do |heading| %> + + + + + + <% end %> + + +
    + <%= 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" } %> +
    +
    + <%= t("admin.budgets.form.no_heading") %> +
    +
    <%= t("admin.budgets.form.table_heading") %><%= t("admin.budgets.form.table_amount") %><%= t("admin.budgets.form.table_geozone") %>
    + <%= heading.name %> + + <%= heading.price %> + + <%= geozone_name_from_id heading.geozone_id %> +
    +
    \ No newline at end of file diff --git a/app/views/admin/budgets/_groups.html.erb b/app/views/admin/budgets/_groups.html.erb new file mode 100644 index 000000000..ba785ee0c --- /dev/null +++ b/app/views/admin/budgets/_groups.html.erb @@ -0,0 +1,34 @@ +
    +

    <%= t('admin.budgets.show.groups') %>

    + <% if groups.blank? %> +
    + <%= t("admin.budgets.form.no_groups") %> + <%= link_to t("admin.budgets.form.add_group"), "#", + class: "js-toggle-link", + data: { "toggle-selector" => "#new-group-form" } %> +
    + <% 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| %> +
    + + + + <%= f.text_field :name, + label: false, + maxlength: 50, + placeholder: t("admin.budgets.form.group") %> +
    + <%= f.submit t("admin.budgets.form.create_group"), class: "button success" %> +
    +
    + <% end %> + + <% groups.each do |group| %> +
    + <%= render "admin/budgets/group", group: group, headings: group.headings %> +
    + <% end %> +
    \ No newline at end of file diff --git a/app/views/admin/budgets/index.html.erb b/app/views/admin/budgets/index.html.erb new file mode 100644 index 000000000..193b6a7ef --- /dev/null +++ b/app/views/admin/budgets/index.html.erb @@ -0,0 +1,25 @@ +

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

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

    <%= page_entries_info @budgets %>

    + + + <% @budgets.each do |budget| %> + + + + + <% end %> +
    + <%= link_to budget.name, admin_budget_path(budget) %> + + <%= t("budget.phase.#{budget.phase}") %> +
    + +<%= paginate @budgets %> \ No newline at end of file diff --git a/app/views/admin/budgets/new.html.erb b/app/views/admin/budgets/new.html.erb new file mode 100644 index 000000000..9ca0f34a5 --- /dev/null +++ b/app/views/admin/budgets/new.html.erb @@ -0,0 +1,29 @@ +
    +
    +

    <%= t("admin.budgets.new.title") %>

    + + <%= 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") %> + +
    +
    + <%= f.label :description, t("admin.budgets.new.phase") %> + <%= f.select :phase, budget_phases_select_options, {label: false} %> +
    +
    + <%= f.label :description, t("admin.budgets.new.currency") %> + <%= f.select :currency_symbol, budget_currency_symbol_select_options, {label: false} %> +
    +
    + <%= f.submit t("admin.budgets.new.create"), class: "button success" %> + <% end %> +
    +
    \ No newline at end of file diff --git a/app/views/admin/budgets/show.html.erb b/app/views/admin/budgets/show.html.erb new file mode 100644 index 000000000..847aa2f60 --- /dev/null +++ b/app/views/admin/budgets/show.html.erb @@ -0,0 +1,16 @@ +
    +
    +

    <%= @budget.name %>

    + + <%= simple_format(text_with_links(@budget.description), {}, sanitize: false) %> + +

    + <%= t('admin.budgets.show.phase') %>: <%= t("budget.phase.#{@budget.phase}") %> | + <%= t('admin.budgets.show.currency') %>: <%= @budget.currency_symbol %> +

    +
    +
    + +
    + <%= render "groups", groups: @budget.groups %> +
    \ No newline at end of file diff --git a/app/views/comments/_comment.html.erb b/app/views/comments/_comment.html.erb index 7b5ff4b7d..bf8bfb50d 100644 --- a/app/views/comments/_comment.html.erb +++ b/app/views/comments/_comment.html.erb @@ -74,6 +74,7 @@ <% if comment.children.size > 0 %> <%= link_to "", class: "js-toggle-children relative", data: {'id': "#{dom_id(comment)}"} do %> + <%= t("shared.show") %> <%= t("comments.comment.responses", count: comment.children.size) %> <% end %> <% else %> diff --git a/app/views/comments/_votes.html.erb b/app/views/comments/_votes.html.erb index 20fc2d1cf..8ee315e35 100644 --- a/app/views/comments/_votes.html.erb +++ b/app/views/comments/_votes.html.erb @@ -7,7 +7,9 @@ <% if can?(:vote, comment) %> <%= link_to vote_comment_path(comment, value: 'yes'), method: "post", remote: true do %> - + + <%= t('votes.agree') %> + <% end %> <% else %> @@ -19,7 +21,9 @@ <% if can?(:vote, comment) %> <%= link_to vote_comment_path(comment, value: 'no'), method: "post", remote: true do %> - + + <%= t('votes.disagree') %> + <% end %> <% else %> diff --git a/app/views/custom/.keep b/app/views/custom/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/app/views/debates/_votes.html.erb b/app/views/debates/_votes.html.erb index 9ac6638d6..4c5f5a9b7 100644 --- a/app/views/debates/_votes.html.erb +++ b/app/views/debates/_votes.html.erb @@ -3,7 +3,9 @@
    <%= link_to vote_debate_path(debate, value: 'yes'), class: "like #{voted_classes[:in_favor]}", title: t('votes.agree'), method: "post", remote: true do %> - + + <%= t('votes.agree') %> + <%= votes_percentage('likes', debate) %> <% end %>
    @@ -12,7 +14,9 @@
    <%= link_to vote_debate_path(debate, value: 'no'), class: "unlike #{voted_classes[:against]}", title: t('votes.disagree'), method: "post", remote: true do %> - + + <%= t('votes.disagree') %> + <%= votes_percentage('dislikes', debate) %> <% end %>
    diff --git a/app/views/devise/menu/_login_items.html.erb b/app/views/devise/menu/_login_items.html.erb index 55665708a..0ab28e2ee 100644 --- a/app/views/devise/menu/_login_items.html.erb +++ b/app/views/devise/menu/_login_items.html.erb @@ -1,6 +1,7 @@ <% if user_signed_in? %>
  • <%= link_to notifications_path, class: "notifications", accesskey: "n" do %> + <%= t("layouts.header.notifications") %> <% if current_user.notifications_count > 0 %>
  • <% end %> - <% if current_user.moderator? || current_user.administrator? %> + <% if current_user.administrator? || current_user.moderator? %>
  • <%= link_to t("layouts.header.moderation"), moderation_root_path %>
  • <% end %> - <% if feature?(:spending_proposals) && (current_user.valuator? || current_user.administrator?) %> + <% if feature?(:spending_proposals) && (current_user.administrator? || current_user.valuator?) %>
  • <%= link_to t("layouts.header.valuation"), valuation_root_path %>
  • diff --git a/app/views/welcome/index.html.erb b/app/views/welcome/index.html.erb index c7ed38d8c..978ff489c 100644 --- a/app/views/welcome/index.html.erb +++ b/app/views/welcome/index.html.erb @@ -21,24 +21,24 @@
    - <%= 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")) %>

    <%= t("welcome.debates.title") %>

    <%= t("welcome.debates.description") %>

    - <%= 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")) %>

    <%= t("welcome.proposal.title") %>

    <%= t("welcome.proposal.description") %>

    - <%= 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")) %>

    <%= t("welcome.decide.title") %>

    <%= t("welcome.decide.description") %>

    - <%= 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")) %>

    <%= t("welcome.do.title") %>

    <%= t("welcome.do.description") %>

    diff --git a/config/application.rb b/config/application.rb index 395251a52..3f2a0861a 100644 --- a/config/application.rb +++ b/config/application.rb @@ -1,3 +1,4 @@ + require File.expand_path('../boot', __FILE__) require 'rails/all' @@ -34,5 +35,17 @@ module Consul config.autoload_paths << Rails.root.join('lib') config.time_zone = 'Madrid' 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 + +require "./config/application_custom.rb" diff --git a/config/application_custom.rb b/config/application_custom.rb new file mode 100644 index 000000000..b99944bc7 --- /dev/null +++ b/config/application_custom.rb @@ -0,0 +1,4 @@ +module Consul + class Application < Rails::Application + end +end diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index 250ed18f9..e24d3af0c 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -112,6 +112,7 @@ ignore_unused: - 'admin.banners.index.filters.*' - 'admin.debates.index.filter*' - 'admin.proposals.index.filter*' + - 'admin.budgets.index.filter*' - 'admin.spending_proposals.index.filter*' - 'admin.organizations.index.filter*' - 'admin.users.index.filter*' diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index 7764ae0ec..e5fc916f0 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -9,8 +9,12 @@ Rails.application.config.assets.version = '1.0' # Precompile additional assets. # 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( 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( stat_graphs.js ) Rails.application.config.assets.precompile += %w( print.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) diff --git a/config/initializers/ckeditor.rb b/config/initializers/ckeditor.rb new file mode 100644 index 000000000..58e119048 --- /dev/null +++ b/config/initializers/ckeditor.rb @@ -0,0 +1,4 @@ +Ckeditor.setup do |config| + config.assets_languages = I18n.available_locales.map(&:to_s) + config.assets_plugins = [] +end diff --git a/config/locales/activerecord.en.yml b/config/locales/activerecord.en.yml index 07791e53c..5a7a591bd 100644 --- a/config/locales/activerecord.en.yml +++ b/config/locales/activerecord.en.yml @@ -4,6 +4,9 @@ en: activity: one: "activity" other: "activities" + budget: + one: "Participatory budget" + other: "Participatory budgets" comment: one: "Comment" other: "Comments" diff --git a/config/locales/activerecord.es.yml b/config/locales/activerecord.es.yml index d5b7f0005..ccd0240e8 100644 --- a/config/locales/activerecord.es.yml +++ b/config/locales/activerecord.es.yml @@ -4,6 +4,9 @@ es: activity: one: "actividad" other: "actividades" + budget: + one: "Presupuesto participativo" + other: "Presupuestos participativos" comment: one: "Comentario" other: "Comentarios" diff --git a/config/locales/admin.en.yml b/config/locales/admin.en.yml index 2f73f64d8..78a3a6d6c 100755 --- a/config/locales/admin.en.yml +++ b/config/locales/admin.en.yml @@ -32,8 +32,6 @@ en: editing: Edit banner form: submit_button: Save changes - errors: - form: errors: form: error: @@ -60,6 +58,40 @@ en: on_users: Users title: Moderator activity 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: index: filter: Filter @@ -96,6 +128,7 @@ en: activity: Moderator activity admin: Admin menu banner: Manage banners + budgets: Participatory budgets debate_topics: Debate topics hidden_comments: Hidden comments hidden_debates: Hidden debates diff --git a/config/locales/admin.es.yml b/config/locales/admin.es.yml index 5aada1ce1..496a0208d 100644 --- a/config/locales/admin.es.yml +++ b/config/locales/admin.es.yml @@ -58,6 +58,40 @@ es: on_users: Usuarios title: Actividad de los Moderadores 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: index: filter: Filtro @@ -94,6 +128,7 @@ es: activity: Actividad de moderadores admin: Menú de administración banner: Gestionar banners + budgets: Presupuestos participativos debate_topics: Temas de debate hidden_comments: Comentarios ocultos hidden_debates: Debates ocultos diff --git a/config/locales/custom/.keep b/config/locales/custom/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/config/locales/en.yml b/config/locales/en.yml index 874d53ef8..d20c69885 100755 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -33,6 +33,13 @@ en: application: close: Close menu: Menu + budget: + phase: + on_hold: On hold + accepting: Accepting proposals + selecting: Selecting + balloting: Balloting + finished: Finished comments: comment: admin: Administrator @@ -198,6 +205,7 @@ en: more_information: More information my_account_link: My account my_activity_link: My activity + notifications: Notifications new_notifications: one: You have a new notification other: You have %{count} new notifications @@ -413,6 +421,7 @@ en: flag: Flag as inappropriate print: print_button: Print this info + show: Show suggest: debate: found: @@ -653,19 +662,15 @@ en: not_voting_allowed: Voting phase is closed welcome: debates: - alt: Icon debates description: For meeting, discussing and sharing the things that matter to us in our city. title: Debates decide: - alt: Icon decide description: The public decides if it accepts or rejects the most supported proposals. title: You decide 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. title: It gets done proposal: - alt: Icon propose description: Open space for citizen proposals about the kind of city we want to live in. title: You propose verification: diff --git a/config/locales/es.yml b/config/locales/es.yml index 6330262a3..62b1f8059 100755 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -33,6 +33,13 @@ es: application: close: Cerrar menu: Menú + budget: + phase: + on_hold: En pausa + accepting: Aceptando propuestas + selecting: Fase de selección + balloting: Fase de Votación + finished: Terminado comments: comment: admin: Administrador @@ -198,6 +205,7 @@ es: more_information: Más información my_account_link: Mi cuenta my_activity_link: Mi actividad + notifications: Notificaciones new_notifications: one: Tienes una nueva notificación other: Tienes %{count} notificaciones nuevas @@ -413,6 +421,7 @@ es: flag: Denunciar como inapropiado print: print_button: Imprimir esta información + show: Mostrar suggest: debate: found: @@ -653,19 +662,15 @@ es: not_voting_allowed: El periodo de votación está cerrado. welcome: debates: - alt: Icono debates description: Encontrarnos, debatir y compartir lo que nos parece importante en nuestra ciudad. title: Debates decide: - alt: Icono decides description: La ciudadanía decide si acepta o rechaza las propuestas más apoyadas. title: Decides do: - alt: Icono se hace description: Si la propuesta es aceptada mayoritariamente, el Ayuntamiento la asume como propia y se hace. title: Se hace proposal: - alt: Icono propones description: Espacio abierto para propuestas ciudadanas sobre el tipo de ciudad en el que queremos vivir. title: Propones verification: diff --git a/config/routes.rb b/config/routes.rb index 5027631f3..92f3d6c22 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -156,6 +156,13 @@ Rails.application.routes.draw do get :summary, on: :collection 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 collection { get :search} end diff --git a/db/migrate/20160803154011_add_emailed_at_to_notifications.rb b/db/migrate/20160803154011_add_emailed_at_to_notifications.rb new file mode 100644 index 000000000..83e38b00f --- /dev/null +++ b/db/migrate/20160803154011_add_emailed_at_to_notifications.rb @@ -0,0 +1,5 @@ +class AddEmailedAtToNotifications < ActiveRecord::Migration + def change + add_column :notifications, :emailed_at, :datetime + end +end diff --git a/db/schema.rb b/db/schema.rb index e555b2950..ff2a0f590 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160617172616) do +ActiveRecord::Schema.define(version: 20160803154011) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -210,10 +210,10 @@ ActiveRecord::Schema.define(version: 20160617172616) do t.string "visit_id" t.datetime "hidden_at" t.integer "flags_count", default: 0 - t.datetime "ignored_flag_at" t.integer "cached_votes_total", default: 0 t.integer "cached_votes_up", default: 0 t.integer "cached_votes_down", default: 0 + t.datetime "ignored_flag_at" t.integer "comments_count", default: 0 t.datetime "confirmed_hide_at" t.integer "cached_anonymous_votes_total", default: 0 @@ -337,10 +337,11 @@ ActiveRecord::Schema.define(version: 20160617172616) do add_index "moderators", ["user_id"], name: "index_moderators_on_user_id", using: :btree create_table "notifications", force: :cascade do |t| - t.integer "user_id" - t.integer "notifiable_id" - t.string "notifiable_type" - t.integer "counter", default: 1 + t.integer "user_id" + t.integer "notifiable_id" + t.string "notifiable_type" + t.integer "counter", default: 1 + t.datetime "emailed_at" end add_index "notifications", ["user_id"], name: "index_notifications_on_user_id", using: :btree diff --git a/lib/email_digest.rb b/lib/email_digest.rb index 90838f78f..209014dcc 100644 --- a/lib/email_digest.rb +++ b/lib/email_digest.rb @@ -1,14 +1,27 @@ class EmailDigest - def initialize + attr_accessor :user, :notifications + + def initialize(user) + @user = user end - def create - User.email_digest.each do |user| - if user.notifications.where(notifiable_type: "ProposalNotification").any? - Mailer.proposal_notification_digest(user).deliver_later - end + def notifications + user.notifications.not_emailed.where(notifiable_type: "ProposalNotification") + end + + def pending_notifications? + notifications.any? + end + + def deliver + if pending_notifications? + Mailer.proposal_notification_digest(user, notifications.to_a).deliver_later end end + def mark_as_emailed + notifications.update_all(emailed_at: Time.now) + end + end \ No newline at end of file diff --git a/lib/tasks/emails.rake b/lib/tasks/emails.rake index 6670264a5..ffadebf05 100644 --- a/lib/tasks/emails.rake +++ b/lib/tasks/emails.rake @@ -2,8 +2,11 @@ namespace :emails do desc "Sends email digest of proposal notifications to each user" task digest: :environment do - email_digest = EmailDigest.new - email_digest.create + User.email_digest.find_each do |user| + email_digest = EmailDigest.new(user) + email_digest.deliver + email_digest.mark_as_emailed + end end end diff --git a/spec/factories.rb b/spec/factories.rb index e38f47c6e..db694b4ce 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -193,6 +193,10 @@ FactoryGirl.define do currency_symbol "€" phase 'on_hold' + trait :accepting do + phase 'accepting' + end + trait :selecting do phase 'selecting' end diff --git a/spec/features/admin/budgets_spec.rb b/spec/features/admin/budgets_spec.rb new file mode 100644 index 000000000..123ca43e8 --- /dev/null +++ b/spec/features/admin/budgets_spec.rb @@ -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 \ No newline at end of file diff --git a/spec/features/emails_spec.rb b/spec/features/emails_spec.rb index 429d74738..a9569e9b2 100644 --- a/spec/features/emails_spec.rb +++ b/spec/features/emails_spec.rb @@ -201,8 +201,9 @@ feature 'Emails' do notification2 = create_proposal_notification(proposal2) notification3 = create_proposal_notification(proposal3) - email_digest = EmailDigest.new - email_digest.create + email_digest = EmailDigest.new(user) + email_digest.deliver + email_digest.mark_as_emailed email = open_last_email 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 have_body_text(/#{account_path}/) + + notification1.reload + notification2.reload + expect(notification1.emailed_at).to be + expect(notification2.emailed_at).to be end end diff --git a/spec/features/notifications_spec.rb b/spec/features/notifications_spec.rb index e16dedf37..5fbc51736 100644 --- a/spec/features/notifications_spec.rb +++ b/spec/features/notifications_spec.rb @@ -180,9 +180,10 @@ feature "Notifications" do find(".icon-notification").click + notification_for_user1 = Notification.where(user: user1).first 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_xpath "//a[@href='#{notification_path(Notification.last)}']" + expect(page).to have_xpath "//a[@href='#{notification_path(notification_for_user1)}']" logout login_as user2 @@ -190,9 +191,10 @@ feature "Notifications" do find(".icon-notification").click + notification_for_user2 = Notification.where(user: user2).first 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_xpath "//a[@href='#{notification_path(Notification.first)}']" + expect(page).to have_xpath "//a[@href='#{notification_path(notification_for_user2)}']" logout login_as user3 diff --git a/spec/features/proposal_notifications_spec.rb b/spec/features/proposal_notifications_spec.rb index 6092289f0..a10e41a7d 100644 --- a/spec/features/proposal_notifications_spec.rb +++ b/spec/features/proposal_notifications_spec.rb @@ -24,6 +24,44 @@ feature 'Proposal Notifications' do expect(page).to have_content "Please share it with others so we can make it happen!" 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 proposal = create(:proposal) notification1 = create(:proposal_notification, proposal: proposal, title: "Hey guys", body: "Just wanted to let you know that...") diff --git a/spec/helpers/geozones_helper_spec.rb b/spec/helpers/geozones_helper_spec.rb index 605a774a6..0c0c13d70 100644 --- a/spec/helpers/geozones_helper_spec.rb +++ b/spec/helpers/geozones_helper_spec.rb @@ -31,4 +31,19 @@ describe GeozonesHelper do 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 diff --git a/spec/lib/email_digests_spec.rb b/spec/lib/email_digests_spec.rb index 6fc6eef53..ae2122793 100644 --- a/spec/lib/email_digests_spec.rb +++ b/spec/lib/email_digests_spec.rb @@ -2,8 +2,126 @@ require 'rails_helper' describe EmailDigest do - describe "create" do - pending "only send unread notifications" + describe "notifications" do + + 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 \ No newline at end of file diff --git a/spec/models/custom/.keep b/spec/models/custom/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/spec/models/custom/residence_spec.rb b/spec/models/custom/residence_spec.rb new file mode 100644 index 000000000..81c0b2e9b --- /dev/null +++ b/spec/models/custom/residence_spec.rb @@ -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 diff --git a/spec/models/proposal_spec.rb b/spec/models/proposal_spec.rb index 4958204ec..247cd9e66 100644 --- a/spec/models/proposal_spec.rb +++ b/spec/models/proposal_spec.rb @@ -367,6 +367,50 @@ describe Proposal do 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 context "attributes" do diff --git a/spec/models/residence_spec.rb b/spec/models/residence_spec.rb index 5288a20b5..968a80e62 100644 --- a/spec/models/residence_spec.rb +++ b/spec/models/residence_spec.rb @@ -30,29 +30,6 @@ describe Verification::Residence do expect(residence.errors[:date_of_birth]).to include("You must be at least 16 years old") 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 user = create(:user) residence.user = user diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index d54f3a42e..b6084a971 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -325,6 +325,34 @@ describe User do 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 it "find users by email" do user1 = create(:user, email: "larry@consul.dev")