diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index d736be793..e45a792f3 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -58,6 +58,7 @@
//= require legislation_allegations
//= require legislation_annotatable
//= require watch_form_changes
+//= require followable
//= require tree_navigator
//= require custom
@@ -93,6 +94,7 @@ var initialize_modules = function() {
App.LegislationAnnotatable.initialize();
App.WatchFormChanges.initialize();
App.TreeNavigator.initialize();
+ App.Followable.initialize();
};
$(function(){
diff --git a/app/assets/javascripts/followable.js.coffee b/app/assets/javascripts/followable.js.coffee
new file mode 100644
index 000000000..69ea61bac
--- /dev/null
+++ b/app/assets/javascripts/followable.js.coffee
@@ -0,0 +1,10 @@
+App.Followable =
+
+ initialize: ->
+ $('.followable-content a[data-toggle]').on 'click', (event) ->
+ event.preventDefault()
+
+ update: (followable_id, button) ->
+ $("#" + followable_id + " .js-follow").html(button)
+ # Temporary line. Waiting for issue resolution: https://github.com/consul/consul/issues/1736
+ initialize_modules()
diff --git a/app/assets/stylesheets/layout.scss b/app/assets/stylesheets/layout.scss
index e223ffa56..b4ea0cca3 100644
--- a/app/assets/stylesheets/layout.scss
+++ b/app/assets/stylesheets/layout.scss
@@ -1982,6 +1982,14 @@ table {
}
}
+.public-interests {
+ margin-top: $line-height;
+
+ .column {
+ padding-left: 0;
+ }
+}
+
// 18. Banners
// -----------
diff --git a/app/controllers/account_controller.rb b/app/controllers/account_controller.rb
index 3b6808de9..4b64fdc2e 100644
--- a/app/controllers/account_controller.rb
+++ b/app/controllers/account_controller.rb
@@ -26,7 +26,7 @@ class AccountController < ApplicationController
params.require(:account).permit(:phone_number, :email_on_comment, :email_on_comment_reply, :newsletter,
organization_attributes: [:name, :responsible_name])
else
- params.require(:account).permit(:username, :public_activity, :email_on_comment, :email_on_comment_reply,
+ params.require(:account).permit(:username, :public_activity, :public_interests, :email_on_comment, :email_on_comment_reply,
:email_on_direct_message, :email_digest, :newsletter, :official_position_badge)
end
end
diff --git a/app/controllers/follows_controller.rb b/app/controllers/follows_controller.rb
new file mode 100644
index 000000000..9cec999fa
--- /dev/null
+++ b/app/controllers/follows_controller.rb
@@ -0,0 +1,23 @@
+class FollowsController < ApplicationController
+ before_action :authenticate_user!
+ load_and_authorize_resource
+
+ def create
+ @followable = find_followable
+ @follow = Follow.create(user: current_user, followable: @followable)
+ render :refresh_follow_button
+ end
+
+ def destroy
+ @follow = Follow.find(params[:id])
+ @followable = @follow.followable
+ @follow.destroy
+ render :refresh_follow_button
+ end
+
+ private
+
+ def find_followable
+ params[:followable_type].constantize.find(params[:followable_id])
+ end
+end
diff --git a/app/controllers/proposal_notifications_controller.rb b/app/controllers/proposal_notifications_controller.rb
index 36e265e38..dfad56945 100644
--- a/app/controllers/proposal_notifications_controller.rb
+++ b/app/controllers/proposal_notifications_controller.rb
@@ -11,8 +11,8 @@ class ProposalNotificationsController < ApplicationController
@notification = ProposalNotification.new(proposal_notification_params)
@proposal = Proposal.find(proposal_notification_params[:proposal_id])
if @notification.save
- @proposal.voters.each do |voter|
- Notification.add(voter.id, @notification)
+ @proposal.users_to_notify.each do |user|
+ Notification.add(user.id, @notification)
end
redirect_to @notification, notice: I18n.t("flash.actions.create.proposal_notification")
else
@@ -30,4 +30,4 @@ class ProposalNotificationsController < ApplicationController
params.require(:proposal_notification).permit(:title, :body, :proposal_id)
end
-end
\ No newline at end of file
+end
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index e99102396..6ef59fb60 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -7,6 +7,7 @@ class UsersController < ApplicationController
def show
load_filtered_activity if valid_access?
+ load_interests if valid_interests_access?
end
private
@@ -62,10 +63,18 @@ class UsersController < ApplicationController
@budget_investments = Budget::Investment.where(author_id: @user.id).order(created_at: :desc).page(params[:page])
end
+ def load_interests
+ @user.interests
+ end
+
def valid_access?
@user.public_activity || authorized_current_user?
end
+ def valid_interests_access?
+ @user.public_interests || authorized_current_user?
+ end
+
def author?
@author ||= current_user && (current_user == @user)
end
diff --git a/app/helpers/flags_helper.rb b/app/helpers/flags_helper.rb
index 715937c0f..b5ba67f41 100644
--- a/app/helpers/flags_helper.rb
+++ b/app/helpers/flags_helper.rb
@@ -26,4 +26,4 @@ module FlagsHelper
end
end
-end
\ No newline at end of file
+end
diff --git a/app/helpers/follows_helper.rb b/app/helpers/follows_helper.rb
new file mode 100644
index 000000000..63f50f7be
--- /dev/null
+++ b/app/helpers/follows_helper.rb
@@ -0,0 +1,61 @@
+module FollowsHelper
+
+ def show_follow_action?(followable)
+ current_user && !followed?(followable)
+ end
+
+ def show_unfollow_action?(followable)
+ current_user && followed?(followable)
+ end
+
+ def follow_entity_text(followable)
+ entity = followable.class.name.gsub('::', '/').downcase
+ t('shared.follow_entity', entity: t("activerecord.models.#{entity}.one").downcase)
+ end
+
+ def follow_entity_title(followable)
+ entity = followable.class.name.gsub('::', '/').downcase
+ t('shared.follow_entity_title', entity: t("activerecord.models.#{entity}.one").downcase)
+ end
+
+ def unfollow_entity_text(followable)
+ entity = followable.class.name.gsub('::', '/').downcase
+ t('shared.unfollow_entity', entity: t("activerecord.models.#{entity}.one").downcase)
+ end
+
+ def entity_full_name(followable)
+ name = followable.class.name
+ name.downcase.gsub("::", "-")
+ end
+
+ def follow_link_wrapper_id(followable)
+ "follow-expand-#{entity_full_name(followable)}-#{followable.id}"
+ end
+
+ def unfollow_link_wrapper_id(followable)
+ "unfollow-expand-#{entity_full_name(followable)}-#{followable.id}"
+ end
+
+ def follow_link_id(followable)
+ "follow-#{entity_full_name(followable)}-#{followable.id}"
+ end
+
+ def unfollow_link_id(followable)
+ "unfollow-#{entity_full_name(followable)}-#{followable.id}"
+ end
+
+ def follow_drop_id(followable)
+ "follow-drop-#{entity_full_name(followable)}-#{followable.id}"
+ end
+
+ def unfollow_drop_id(followable)
+ "unfollow-drop-#{entity_full_name(followable)}-#{followable.id}"
+ end
+
+ private
+
+ def followed?(followable)
+ Follow.followed?(current_user, followable)
+ end
+
+end
diff --git a/app/models/abilities/common.rb b/app/models/abilities/common.rb
index 2b32abd07..cf183ec53 100644
--- a/app/models/abilities/common.rb
+++ b/app/models/abilities/common.rb
@@ -34,6 +34,8 @@ module Abilities
can [:flag, :unflag], Proposal
cannot [:flag, :unflag], Proposal, author_id: user.id
+ can [:create, :destroy], Follow
+
unless user.organization?
can :vote, Debate
can :vote, Comment
diff --git a/app/models/budget/investment.rb b/app/models/budget/investment.rb
index 6a83b28a2..a5bea4508 100644
--- a/app/models/budget/investment.rb
+++ b/app/models/budget/investment.rb
@@ -6,6 +6,7 @@ class Budget
include Taggable
include Searchable
include Reclassification
+ include Followable
acts_as_votable
acts_as_paranoid column: :hidden_at
diff --git a/app/models/concerns/followable.rb b/app/models/concerns/followable.rb
new file mode 100644
index 000000000..51469698e
--- /dev/null
+++ b/app/models/concerns/followable.rb
@@ -0,0 +1,9 @@
+module Followable
+ extend ActiveSupport::Concern
+
+ included do
+ has_many :follows, as: :followable, dependent: :destroy
+ has_many :followers, through: :follows, source: :user
+ end
+
+end
diff --git a/app/models/follow.rb b/app/models/follow.rb
new file mode 100644
index 000000000..6c2c62e99
--- /dev/null
+++ b/app/models/follow.rb
@@ -0,0 +1,20 @@
+class Follow < ActiveRecord::Base
+ belongs_to :user
+ belongs_to :followable, polymorphic: true
+
+ validates :user_id, presence: true
+ validates :followable_id, presence: true
+ validates :followable_type, presence: true
+
+ scope(:by_user_and_followable, lambda do |user, followable|
+ where(user_id: user.id,
+ followable_type: followable.class.to_s,
+ followable_id: followable.id)
+ end)
+
+ def self.followed?(user, followable)
+ return false unless user
+ !! by_user_and_followable(user, followable).try(:first)
+ end
+
+end
diff --git a/app/models/proposal.rb b/app/models/proposal.rb
index 9e02c197e..21335c5a5 100644
--- a/app/models/proposal.rb
+++ b/app/models/proposal.rb
@@ -8,6 +8,7 @@ class Proposal < ActiveRecord::Base
include Filterable
include HasPublicAuthor
include Graphqlable
+ include Followable
acts_as_votable
acts_as_paranoid column: :hidden_at
@@ -170,6 +171,10 @@ class Proposal < ActiveRecord::Base
proposal_notifications
end
+ def users_to_notify
+ (voters + followers).uniq
+ end
+
protected
def set_responsible_name
diff --git a/app/models/user.rb b/app/models/user.rb
index b41057157..60536195b 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -31,6 +31,7 @@ class User < ActiveRecord::Base
has_many :direct_messages_sent, class_name: 'DirectMessage', foreign_key: :sender_id
has_many :direct_messages_received, class_name: 'DirectMessage', foreign_key: :receiver_id
has_many :legislation_answers, class_name: 'Legislation::Answer', dependent: :destroy, inverse_of: :user
+ has_many :follows
belongs_to :geozone
validates :username, presence: true, if: :username_required?
@@ -308,6 +309,10 @@ class User < ActiveRecord::Base
where(conditions.to_hash).where(["username = ?", login]).first
end
+ def interests
+ follows.map{|follow| follow.followable.tags.map(&:name)}.flatten.compact.uniq
+ end
+
private
def clean_document_number
diff --git a/app/views/account/show.html.erb b/app/views/account/show.html.erb
index a0d9f1693..a3407f5a7 100644
--- a/app/views/account/show.html.erb
+++ b/app/views/account/show.html.erb
@@ -40,6 +40,16 @@
<% end %>
+
+ <%= f.label :public_interests do %>
+ <%= f.check_box :public_interests, title: t('account.show.public_interests_label'), label: false %>
+
+ <%= t("account.show.public_interests_label") %>
+
+ <% end %>
+
+
+
<% if @account.email.present? %>
<%= t("account.show.notifications")%>
diff --git a/app/views/budgets/investments/_investment_show.html.erb b/app/views/budgets/investments/_investment_show.html.erb
index 42a8b9f26..950b93b6a 100644
--- a/app/views/budgets/investments/_investment_show.html.erb
+++ b/app/views/budgets/investments/_investment_show.html.erb
@@ -112,6 +112,9 @@
title: investment.title,
url: budget_investment_url(budget_id: investment.budget_id, id: investment.id)
} %>
+
+ <%= render 'follows/followable_button', followable: investment if current_user %>
+
diff --git a/app/views/follows/_followable_button.html.erb b/app/views/follows/_followable_button.html.erb
new file mode 100644
index 000000000..527a7e029
--- /dev/null
+++ b/app/views/follows/_followable_button.html.erb
@@ -0,0 +1,45 @@
+
+
+
+ <% if show_follow_action? followable %>
+
+ <%= link_to "##{follow_link_wrapper_id(followable)}",
+ id: follow_link_wrapper_id(followable),
+ title: follow_entity_text(followable),
+ data: { toggle: follow_drop_id(followable) },
+ class: 'button hollow' do %>
+ <%= t('shared.follow') %>
+ <% end %>
+
+ <%= link_to follow_entity_text(followable),
+ follows_path(followable_id: followable.id,
+ followable_type: followable.class.name),
+ method: :post, remote: true,
+ id: follow_link_id(followable) %>
+
+
+ <% end %>
+
+ <% if show_unfollow_action? followable %>
+
+ <% follow = followable.follows.where(user: current_user).first %>
+ <%= link_to "##{unfollow_link_wrapper_id(followable)}",
+ id: unfollow_link_wrapper_id(followable),
+ title: unfollow_entity_text(followable),
+ data: { toggle: unfollow_drop_id(followable) },
+ class: 'button hollow' do %>
+ <%= t('shared.unfollow') %>
+ <% end %>
+
+ <%= link_to unfollow_entity_text(followable),
+ follow_path(follow),
+ method: :delete, remote: true,
+ id: unfollow_link_id(followable) %>
+
+
+ <% end %>
+
+
+
\ No newline at end of file
diff --git a/app/views/follows/refresh_follow_button.js.erb b/app/views/follows/refresh_follow_button.js.erb
new file mode 100644
index 000000000..31e2627a3
--- /dev/null
+++ b/app/views/follows/refresh_follow_button.js.erb
@@ -0,0 +1,2 @@
+App.Followable.update("<%= dom_id(@followable) %>",
+ "<%= j render('followable_button', followable: @followable) %>")
diff --git a/app/views/proposal_notifications/new.html.erb b/app/views/proposal_notifications/new.html.erb
index 678e00088..55341eb58 100644
--- a/app/views/proposal_notifications/new.html.erb
+++ b/app/views/proposal_notifications/new.html.erb
@@ -7,7 +7,7 @@
<%= t("proposal_notifications.new.info_about_receivers_html",
- count: @proposal.voters.count,
+ count: @proposal.users_to_notify.count,
proposal_page: link_to(t("proposal_notifications.new.proposal_page"),
proposal_path(@proposal, anchor: "comments"))).html_safe %>
diff --git a/app/views/proposals/_flag_actions.html.erb b/app/views/proposals/_flag_actions.html.erb
index aba7a2423..875a23f07 100644
--- a/app/views/proposals/_flag_actions.html.erb
+++ b/app/views/proposals/_flag_actions.html.erb
@@ -1,19 +1,21 @@
-
- <% if show_flag_action? proposal %>
-
-
-
-
- <%= link_to t('shared.flag'), flag_proposal_path(proposal), method: :put, remote: true, id: "flag-proposal-#{ proposal.id }" %>
-
- <% end %>
+
+
+ <% if show_flag_action? proposal %>
+
+
+
+
+ <%= link_to t('shared.flag'), flag_proposal_path(proposal), method: :put, remote: true, id: "flag-proposal-#{ proposal.id }" %>
+
+ <% end %>
- <% if show_unflag_action? proposal %>
-
-
-
-
- <%= link_to t('shared.unflag'), unflag_proposal_path(proposal), method: :put, remote: true, id: "unflag-proposal-#{ proposal.id }" %>
-
- <% end %>
+ <% if show_unflag_action? proposal %>
+
+
+
+
+ <%= link_to t('shared.unflag'), unflag_proposal_path(proposal), method: :put, remote: true, id: "unflag-proposal-#{ proposal.id }" %>
+
+ <% end %>
+
diff --git a/app/views/proposals/show.html.erb b/app/views/proposals/show.html.erb
index d678214d2..d5c83d9f5 100644
--- a/app/views/proposals/show.html.erb
+++ b/app/views/proposals/show.html.erb
@@ -49,10 +49,12 @@
•
<%= link_to t("proposals.show.comments", count: @proposal.comments_count), "#comments" %>
- •
-
+
+ <% if current_user %>
+ •
<%= render 'proposals/flag_actions', proposal: @proposal %>
-
+ <% end %>
+
@@ -137,6 +139,9 @@
title: @proposal.title,
url: proposal_url(@proposal)
} %>
+
+ <%= render 'follows/followable_button', followable: @proposal if current_user %>
+
diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb
index c00769216..fb4a3d1f9 100644
--- a/app/views/users/show.html.erb
+++ b/app/views/users/show.html.erb
@@ -46,6 +46,21 @@
<%= t('users.show.private_activity') %>
<% end %>
+ <% if @user.public_interests || @authorized_current_user %>
+
+
<%= t('account.show.public_interests_title_list') %>
+ <% @user.interests.in_groups_of(10, false) do |interests_group| %>
+
+
+ <% interests_group.each do |interest| %>
+ - <%= interest %>
+ <% end %>
+
+
+ <% end %>
+
+ <% end %>
+
diff --git a/config/locales/en/general.yml b/config/locales/en/general.yml
index 8c0763b32..fd5a128b8 100644
--- a/config/locales/en/general.yml
+++ b/config/locales/en/general.yml
@@ -13,6 +13,8 @@ en:
personal: Personal details
phone_number_label: Phone number
public_activity_label: Keep my list of activities public
+ public_interests_label: Keep my interests public
+ public_interests_title_list: List of interests
save_changes_submit: Save changes
subscription_to_website_newsletter_label: Receive by email website relevant information
email_on_direct_message_label: Receive emails about direct messages
@@ -500,6 +502,9 @@ en:
check_none: None
collective: Collective
flag: Flag as inappropriate
+ follow: "Follow"
+ follow_entity: "Follow %{entity}"
+ follow_entity_title: "Follow %{entity}: You can participate and receive notifications of any related events."
hide: Hide
print:
print_button: Print this info
@@ -532,6 +537,8 @@ en:
target_blank_html: " (link opens in new window)"
you_are_in: "You are in"
unflag: Unflag
+ unfollow: "Unfollow"
+ unfollow_entity: "Unfollow %{entity}"
outline:
debates: Debates
proposals: Proposals
@@ -643,6 +650,9 @@ en:
budget_investments:
one: 1 Investment
other: "%{count} Investments"
+ follows:
+ one: 1 Following
+ other: "%{count} Following"
no_activity: User has no public activity
no_private_messages: "This user doesn't accept private messages."
private_activity: This user decided to keep the activity list private
@@ -708,4 +718,3 @@ en:
invisible_captcha:
sentence_for_humans: "If you are human, ignore this field"
timestamp_error_message: "Sorry, that was too quick! Please resubmit."
-
diff --git a/config/locales/es/general.yml b/config/locales/es/general.yml
index d38184647..941a03813 100644
--- a/config/locales/es/general.yml
+++ b/config/locales/es/general.yml
@@ -13,6 +13,8 @@ es:
personal: Datos personales
phone_number_label: Teléfono
public_activity_label: Mostrar públicamente mi lista de actividades
+ public_interests_label: Mostrar públicamente mis intereses
+ public_interests_title_list: Lista de intereses
save_changes_submit: Guardar cambios
subscription_to_website_newsletter_label: Recibir emails con información interesante sobre la web
email_on_direct_message_label: Recibir emails con mensajes privados
@@ -500,6 +502,9 @@ es:
check_none: Ninguno
collective: Colectivo
flag: Denunciar como inapropiado
+ follow: "Seguir"
+ follow_entity: "Seguir %{entity}"
+ follow_entity_title: "Seguir %{entity}: Podrás participar y recibir notificaciones de cualquier suceso relacionado."
hide: Ocultar
print:
print_button: Imprimir esta información
@@ -532,6 +537,8 @@ es:
target_blank_html: " (se abre en ventana nueva)"
you_are_in: "Estás en"
unflag: Deshacer denuncia
+ unfollow: Dejar de seguir
+ unfollow_entity: "Dejar de seguir %{entity}"
outline:
debates: Debates
proposals: Propuestas
@@ -643,6 +650,9 @@ es:
budget_investments:
one: 1 Proyecto de presupuestos participativos
other: "%{count} Proyectos de presupuestos participativos"
+ follows:
+ one: 1 Siguiendo
+ other: "%{count} Siguiendo"
no_activity: Usuario sin actividad pública
no_private_messages: "Este usuario no acepta mensajes privados."
private_activity: Este usuario ha decidido mantener en privado su lista de actividades
@@ -707,4 +717,4 @@ es:
user_permission_votes: Participar en las votaciones finales*
invisible_captcha:
sentence_for_humans: "Si eres humano, por favor ignora este campo"
- timestamp_error_message: "Eso ha sido demasiado rápido. Por favor, reenvía el formulario."
\ No newline at end of file
+ timestamp_error_message: "Eso ha sido demasiado rápido. Por favor, reenvía el formulario."
diff --git a/config/routes.rb b/config/routes.rb
index ea12cf1ab..aaa9b5db7 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -93,6 +93,8 @@ Rails.application.routes.draw do
end
end
+ resources :follows, only: [:create, :destroy]
+
resources :stats, only: [:index]
resources :legacy_legislations, only: [:show], path: 'legislations'
diff --git a/db/migrate/20170626180127_create_follows.rb b/db/migrate/20170626180127_create_follows.rb
new file mode 100644
index 000000000..d4225cab8
--- /dev/null
+++ b/db/migrate/20170626180127_create_follows.rb
@@ -0,0 +1,12 @@
+class CreateFollows < ActiveRecord::Migration
+ def change
+ create_table :follows do |t|
+ t.references :user, index: true, foreign_key: true
+ t.references :followable, polymorphic: true, index: true
+
+ t.timestamps null: false
+ end
+
+ add_index :follows, [:user_id, :followable_type, :followable_id], name: "access_follows"
+ end
+end
diff --git a/db/migrate/20170630105250_add_public_interests_to_user.rb b/db/migrate/20170630105250_add_public_interests_to_user.rb
new file mode 100644
index 000000000..8fec4a748
--- /dev/null
+++ b/db/migrate/20170630105250_add_public_interests_to_user.rb
@@ -0,0 +1,5 @@
+class AddPublicInterestsToUser < ActiveRecord::Migration
+ def change
+ add_column :users, :public_interests, :boolean, default: false
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 440fb2a9a..f4c717bd8 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -95,9 +95,8 @@ ActiveRecord::Schema.define(version: 20170708225159) do
create_table "budget_ballots", force: :cascade do |t|
t.integer "user_id"
t.integer "budget_id"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.integer "ballot_lines_count", default: 0
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
end
create_table "budget_groups", force: :cascade do |t|
@@ -326,6 +325,18 @@ ActiveRecord::Schema.define(version: 20170708225159) do
add_index "flags", ["user_id", "flaggable_type", "flaggable_id"], name: "access_inappropiate_flags", using: :btree
add_index "flags", ["user_id"], name: "index_flags_on_user_id", using: :btree
+ create_table "follows", force: :cascade do |t|
+ t.integer "user_id"
+ t.integer "followable_id"
+ t.string "followable_type"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ add_index "follows", ["followable_type", "followable_id"], name: "index_follows_on_followable_type_and_followable_id", using: :btree
+ add_index "follows", ["user_id", "followable_type", "followable_id"], name: "access_follows", using: :btree
+ add_index "follows", ["user_id"], name: "index_follows_on_user_id", using: :btree
+
create_table "geozones", force: :cascade do |t|
t.string "name"
t.string "html_map_coordinates"
@@ -917,6 +928,7 @@ ActiveRecord::Schema.define(version: 20170708225159) do
t.boolean "created_from_signature", default: false
t.integer "failed_email_digests_count", default: 0
t.text "former_users_data_log", default: ""
+ t.boolean "public_interests", default: false
end
add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree
@@ -1010,6 +1022,7 @@ ActiveRecord::Schema.define(version: 20170708225159) do
add_foreign_key "failed_census_calls", "poll_officers"
add_foreign_key "failed_census_calls", "users"
add_foreign_key "flags", "users"
+ add_foreign_key "follows", "users"
add_foreign_key "geozones_polls", "geozones"
add_foreign_key "geozones_polls", "polls"
add_foreign_key "identities", "users"
diff --git a/spec/factories.rb b/spec/factories.rb
index 9a3c292b3..d9521847d 100644
--- a/spec/factories.rb
+++ b/spec/factories.rb
@@ -166,8 +166,8 @@ FactoryGirl.define do
end
trait :flagged do
- after :create do |debate|
- Flag.flag(FactoryGirl.create(:user), debate)
+ after :create do |proposal|
+ Flag.flag(FactoryGirl.create(:user), proposal)
end
end
@@ -349,6 +349,18 @@ FactoryGirl.define do
association :user, factory: :user
end
+ factory :follow do
+ association :user, factory: :user
+
+ trait :followed_proposal do
+ association :followable, factory: :proposal
+ end
+
+ trait :followed_investment do
+ association :followable, factory: :budget_investment
+ end
+ end
+
factory :comment do
association :commentable, factory: :debate
user
diff --git a/spec/features/budgets/investments_spec.rb b/spec/features/budgets/investments_spec.rb
index ca86587f0..96a26ad82 100644
--- a/spec/features/budgets/investments_spec.rb
+++ b/spec/features/budgets/investments_spec.rb
@@ -329,6 +329,14 @@ feature 'Budget Investments' do
end
end
+ scenario "Don't display flaggable buttons" do
+ investment = create(:budget_investment, heading: heading)
+
+ visit budget_investment_path(budget_id: budget.id, id: investment.id)
+
+ expect(page).not_to have_selector ".js-follow"
+ end
+
scenario "Show back link contains heading id" do
investment = create(:budget_investment, heading: heading)
visit budget_investment_path(budget, investment)
@@ -421,6 +429,8 @@ feature 'Budget Investments' do
end
end
+ it_behaves_like "followable", "budget_investment", "budget_investment_path", {"budget_id": "budget_id", "id": "id"}
+
context "Destroy" do
scenario "Admin cannot destroy budget investments" do
diff --git a/spec/features/notifications_spec.rb b/spec/features/notifications_spec.rb
index b5a78c38c..a53024109 100644
--- a/spec/features/notifications_spec.rb
+++ b/spec/features/notifications_spec.rb
@@ -236,6 +236,60 @@ feature "Notifications" do
expect(page).to have_css ".notification", count: 0
end
+ scenario "Followers should receive a notification", :js do
+ author = create(:user)
+
+ user1 = create(:user)
+ user2 = create(:user)
+ user3 = create(:user)
+
+ proposal = create(:proposal, author: author)
+
+ create(:follow, :followed_proposal, user: user1, followable: proposal)
+ create(:follow, :followed_proposal, user: user2, followable: proposal)
+
+ login_as(author)
+ visit root_path
+
+ visit new_proposal_notification_path(proposal_id: proposal.id)
+
+ fill_in 'proposal_notification_title', with: "Thank you for supporting my proposal"
+ fill_in 'proposal_notification_body', with: "Please share it with others so we can make it happen!"
+ click_button "Send message"
+
+ expect(page).to have_content "Your message has been sent correctly."
+
+ logout
+ login_as user1
+ visit root_path
+
+ 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_for_user1)}']"
+
+ logout
+ login_as user2
+ visit root_path
+
+ 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_for_user2)}']"
+
+ logout
+ login_as user3
+ visit root_path
+
+ find(".icon-no-notification").click
+
+ expect(page).to have_css ".notification", count: 0
+ end
+
pending "group notifications for the same proposal"
end
diff --git a/spec/features/proposal_notifications_spec.rb b/spec/features/proposal_notifications_spec.rb
index 2c12c85c8..4756a1903 100644
--- a/spec/features/proposal_notifications_spec.rb
+++ b/spec/features/proposal_notifications_spec.rb
@@ -36,6 +36,33 @@ feature 'Proposal Notifications' do
expect(Notification.count).to eq(1)
end
+ scenario "Send a notification (Follower)" do
+ author = create(:user)
+ proposal = create(:proposal, author: author)
+ user_follower = create(:user)
+ create(:follow, :followed_proposal, user: user_follower, followable: proposal)
+
+ create_proposal_notification(proposal)
+
+ expect(Notification.count).to eq(1)
+ end
+
+ scenario "Send a notification (Follower and Voter)" do
+ author = create(:user)
+ proposal = create(:proposal, author: author)
+
+ user_voter_follower = create(:user)
+ create(:follow, :followed_proposal, user: user_voter_follower, followable: proposal)
+ create(:vote, voter: user_voter_follower, votable: proposal)
+
+ user_follower = create(:user)
+ create(:follow, :followed_proposal, user: user_follower, followable: proposal)
+
+ create_proposal_notification(proposal)
+
+ expect(Notification.count).to eq(2)
+ end
+
scenario "Send a notification (Blocked voter)" do
author = create(:user)
proposal = create(:proposal, author: author)
@@ -77,7 +104,7 @@ feature 'Proposal Notifications' do
expect(page).to have_content "We are almost there please share with your peoples!"
end
- scenario "Message about receivers" do
+ scenario "Message about receivers (Voters)" do
author = create(:user)
proposal = create(:proposal, author: author)
@@ -90,6 +117,48 @@ feature 'Proposal Notifications' do
expect(page).to have_link("the proposal's page", href: proposal_path(proposal, anchor: 'comments'))
end
+ scenario "Message about receivers (Followers)" do
+ author = create(:user)
+ proposal = create(:proposal, author: author)
+
+ 7.times { create(:follow, :followed_proposal, followable: proposal) }
+
+ login_as(author)
+ visit new_proposal_notification_path(proposal_id: proposal.id)
+
+ expect(page).to have_content "This message will be send to 7 people and it will be visible in the proposal's page"
+ expect(page).to have_link("the proposal's page", href: proposal_path(proposal, anchor: 'comments'))
+ end
+
+ scenario "Message about receivers (Disctinct Followers and Voters)" do
+ author = create(:user)
+ proposal = create(:proposal, author: author)
+
+ 7.times { create(:follow, :followed_proposal, followable: proposal) }
+ 7.times { create(:vote, votable: proposal, vote_flag: true) }
+
+ login_as(author)
+ visit new_proposal_notification_path(proposal_id: proposal.id)
+
+ expect(page).to have_content "This message will be send to 14 people and it will be visible in the proposal's page"
+ expect(page).to have_link("the proposal's page", href: proposal_path(proposal, anchor: 'comments'))
+ end
+
+ scenario "Message about receivers (Same Followers and Voters)" do
+ author = create(:user)
+ proposal = create(:proposal, author: author)
+
+ user_voter_follower = create(:user)
+ create(:follow, :followed_proposal, user: user_voter_follower, followable: proposal)
+ create(:vote, voter: user_voter_follower, votable: proposal)
+
+ login_as(author)
+ visit new_proposal_notification_path(proposal_id: proposal.id)
+
+ expect(page).to have_content "This message will be send to 1 people and it will be visible in the proposal's page"
+ expect(page).to have_link("the proposal's page", href: proposal_path(proposal, anchor: 'comments'))
+ end
+
context "Permissions" do
scenario "Link to send the message" do
diff --git a/spec/features/proposals_spec.rb b/spec/features/proposals_spec.rb
index 5ec0d36fd..1cb39a579 100644
--- a/spec/features/proposals_spec.rb
+++ b/spec/features/proposals_spec.rb
@@ -61,6 +61,8 @@ feature 'Proposals' do
expect(page).to have_content I18n.l(proposal.created_at.to_date)
expect(page).to have_selector(avatar(proposal.author.name))
expect(page.html).to include "#{proposal.title}"
+ expect(page).not_to have_selector ".js-flag-actions"
+ expect(page).not_to have_selector ".js-follow"
within('.social-share-button') do
expect(page.all('a').count).to be(4) # Twitter, Facebook, Google+, Telegram
@@ -1224,6 +1226,8 @@ feature 'Proposals' do
expect(Flag.flagged?(user, proposal)).to_not be
end
+ it_behaves_like "followable", "proposal", "proposal_path", { "id": "id" }
+
scenario 'Erased author' do
user = create(:user)
proposal = create(:proposal, author: user)
diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb
index 90ee6b94c..a6e9e2941 100644
--- a/spec/features/users_spec.rb
+++ b/spec/features/users_spec.rb
@@ -213,6 +213,105 @@ feature 'Users' do
end
+ feature 'Public interest' do
+ background do
+ @user = create(:user)
+ end
+
+ scenario 'Display interests' do
+ proposal = create(:proposal, tag_list: "Sport")
+ create(:follow, :followed_proposal, followable: proposal, user: @user)
+
+ login_as(@user)
+ visit account_path
+
+ check 'account_public_interests'
+ click_button 'Save changes'
+
+ logout
+
+ visit user_path(@user)
+ expect(page).to have_content("Sport")
+ end
+
+ scenario 'Not display interests when proposal has been destroyed' do
+ proposal = create(:proposal, tag_list: "Sport")
+ create(:follow, :followed_proposal, followable: proposal, user: @user)
+ proposal.destroy
+
+ login_as(@user)
+ visit account_path
+
+ check 'account_public_interests'
+ click_button 'Save changes'
+
+ logout
+
+ visit user_path(@user)
+ expect(page).not_to have_content("Sport")
+ end
+
+ scenario 'No visible by default' do
+ visit user_path(@user)
+
+ expect(page).to have_content(@user.username)
+ expect(page).not_to have_css('#public_interests')
+ end
+
+ scenario 'User can display public page' do
+ login_as(@user)
+ visit account_path
+
+ check 'account_public_interests'
+ click_button 'Save changes'
+
+ logout
+
+ visit user_path(@user)
+ expect(page).to have_css('#public_interests')
+ end
+
+ scenario 'Is always visible for the owner' do
+ login_as(@user)
+ visit account_path
+
+ uncheck 'account_public_interests'
+ click_button 'Save changes'
+
+ visit user_path(@user)
+ expect(page).to have_css('#public_interests')
+ end
+
+ scenario 'Is always visible for admins' do
+ login_as(@user)
+ visit account_path
+
+ uncheck 'account_public_interests'
+ click_button 'Save changes'
+
+ logout
+
+ login_as(create(:administrator).user)
+ visit user_path(@user)
+ expect(page).to have_css('#public_interests')
+ end
+
+ scenario 'Is always visible for moderators' do
+ login_as(@user)
+ visit account_path
+
+ uncheck 'account_public_interests'
+ click_button 'Save changes'
+
+ logout
+
+ login_as(create(:moderator).user)
+ visit user_path(@user)
+ expect(page).to have_css('#public_interests')
+ end
+
+ end
+
feature 'Special comments' do
scenario 'comments posted as moderator are not visible in user activity' do
diff --git a/spec/models/follow_spec.rb b/spec/models/follow_spec.rb
new file mode 100644
index 000000000..fc17924f5
--- /dev/null
+++ b/spec/models/follow_spec.rb
@@ -0,0 +1,26 @@
+require 'rails_helper'
+
+describe Follow do
+
+ let(:follow) { build(:follow, :followed_proposal) }
+
+ it "should be valid" do
+ expect(follow).to be_valid
+ end
+
+ it "should not be valid without a user_id" do
+ follow.user_id = nil
+ expect(follow).to_not be_valid
+ end
+
+ it "should not be valid without a followable_id" do
+ follow.followable_id = nil
+ expect(follow).to_not be_valid
+ end
+
+ it "should not be valid without a followable_type" do
+ follow.followable_type = nil
+ expect(follow).to_not be_valid
+ end
+
+end
diff --git a/spec/models/proposal_spec.rb b/spec/models/proposal_spec.rb
index d8c467974..5bf6d90e1 100644
--- a/spec/models/proposal_spec.rb
+++ b/spec/models/proposal_spec.rb
@@ -857,4 +857,26 @@ describe Proposal do
end
end
+ describe "#user_to_notify" do
+
+ it "should return voters and followers" do
+ proposal = create(:proposal)
+ voter = create(:user, :level_two)
+ follower = create(:user, :level_two)
+ follow = create(:follow, user: follower, followable: proposal)
+ create(:vote, voter: voter, votable: proposal)
+
+ expect(proposal.users_to_notify).to eq([voter, follower])
+ end
+
+ it "should return voters and followers discarding duplicates" do
+ proposal = create(:proposal)
+ voter_and_follower = create(:user, :level_two)
+ follow = create(:follow, user: voter_and_follower, followable: proposal)
+ create(:vote, voter: voter_and_follower, votable: proposal)
+
+ expect(proposal.users_to_notify).to eq([voter_and_follower])
+ end
+
+ end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 2e92e5c96..2ca1dfe05 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -657,4 +657,27 @@ describe User do
end
end
+ describe "#interests" do
+ let(:user) { create(:user) }
+
+ it "should return followed object tags" do
+ proposal = create(:proposal, tag_list: "Sport")
+ create(:follow, followable: proposal, user: user)
+
+ expect(user.interests).to eq ["Sport"]
+ end
+
+ it "should discard followed objects duplicated tags" do
+ proposal1 = create(:proposal, tag_list: "Sport")
+ proposal2 = create(:proposal, tag_list: "Sport")
+ budget_investment = create(:budget_investment, tag_list: "Sport")
+
+ create(:follow, followable: proposal1, user: user)
+ create(:follow, followable: proposal2, user: user)
+ create(:follow, followable: budget_investment, user: user)
+
+ expect(user.interests).to eq ["Sport"]
+ end
+
+ end
end
diff --git a/spec/shared/features/followable.rb b/spec/shared/features/followable.rb
new file mode 100644
index 000000000..7285ee15f
--- /dev/null
+++ b/spec/shared/features/followable.rb
@@ -0,0 +1,83 @@
+shared_examples "followable" do |followable_class_name, followable_path, followable_path_arguments|
+ include ActionView::Helpers
+
+ let!(:arguments) { {} }
+ let!(:followable) { create(followable_class_name) }
+ let!(:followable_dom_name) { followable_class_name.gsub('_', '-') }
+
+ before do
+ followable_path_arguments.each do |argument_name, path_to_value|
+ arguments.merge!("#{argument_name}": followable.send(path_to_value))
+ end
+ end
+
+ context "Show" do
+
+ scenario "Should not display follow button when there is no logged user" do
+ visit send(followable_path, arguments)
+
+ within "##{dom_id(followable)}" do
+ expect(page).not_to have_link("Follow")
+ end
+ end
+
+ scenario "Should display follow button when user is logged in" do
+ user = create(:user)
+ login_as(user)
+
+ visit send(followable_path, arguments)
+
+ within "##{dom_id(followable)}" do
+ expect(page).to have_link("Follow")
+ end
+ end
+
+ scenario "Should display follow button when user is logged and is not following" do
+ user = create(:user)
+ login_as(user)
+
+ visit send(followable_path, arguments)
+
+ expect(page).to have_link("Follow")
+ end
+
+ scenario "Should display unfollow button when click on follow button", :js do
+ user = create(:user)
+ login_as(user)
+
+ visit send(followable_path, arguments)
+ within "##{dom_id(followable)}" do
+ click_link "Follow"
+ page.find("#follow-#{followable_dom_name}-#{followable.id}").click
+
+ expect(page).to have_css("#unfollow-expand-#{followable_dom_name}-#{followable.id}")
+ end
+ end
+
+ scenario "Display unfollow button when user already following" do
+ user = create(:user)
+ follow = create(:follow, user: user, followable: followable)
+ login_as(user)
+
+ visit send(followable_path, arguments)
+
+ expect(page).to have_link("Unfollow")
+ end
+
+ scenario "Should display follow button when click on unfollow button", :js do
+ user = create(:user)
+ follow = create(:follow, user: user, followable: followable)
+ login_as(user)
+
+ visit send(followable_path, arguments)
+ within "##{dom_id(followable)}" do
+ click_link "Unfollow"
+ page.find("#unfollow-#{followable_dom_name}-#{followable.id}").click
+
+ expect(page).to have_css("#follow-expand-#{followable_dom_name}-#{followable.id}")
+ end
+ end
+
+ end
+
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 7ef336a4d..1548eada8 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -5,6 +5,7 @@ require 'devise'
require 'knapsack'
Dir["./spec/models/concerns/*.rb"].each { |f| require f }
Dir["./spec/support/**/*.rb"].sort.each { |f| require f }
+Dir["./spec/shared/**/*.rb"].sort.each { |f| require f }
RSpec.configure do |config|
config.use_transactional_fixtures = false