@@ -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(){
|
||||
|
||||
10
app/assets/javascripts/followable.js.coffee
Normal file
10
app/assets/javascripts/followable.js.coffee
Normal file
@@ -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()
|
||||
@@ -1982,6 +1982,14 @@ table {
|
||||
}
|
||||
}
|
||||
|
||||
.public-interests {
|
||||
margin-top: $line-height;
|
||||
|
||||
.column {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 18. Banners
|
||||
// -----------
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
23
app/controllers/follows_controller.rb
Normal file
23
app/controllers/follows_controller.rb
Normal file
@@ -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
|
||||
@@ -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
|
||||
end
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -26,4 +26,4 @@ module FlagsHelper
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
61
app/helpers/follows_helper.rb
Normal file
61
app/helpers/follows_helper.rb
Normal file
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -6,6 +6,7 @@ class Budget
|
||||
include Taggable
|
||||
include Searchable
|
||||
include Reclassification
|
||||
include Followable
|
||||
|
||||
acts_as_votable
|
||||
acts_as_paranoid column: :hidden_at
|
||||
|
||||
9
app/models/concerns/followable.rb
Normal file
9
app/models/concerns/followable.rb
Normal file
@@ -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
|
||||
20
app/models/follow.rb
Normal file
20
app/models/follow.rb
Normal file
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -40,6 +40,16 @@
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= f.label :public_interests do %>
|
||||
<%= f.check_box :public_interests, title: t('account.show.public_interests_label'), label: false %>
|
||||
<span class="checkbox">
|
||||
<%= t("account.show.public_interests_label") %>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
|
||||
<% if @account.email.present? %>
|
||||
<h2><%= t("account.show.notifications")%></h2>
|
||||
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
45
app/views/follows/_followable_button.html.erb
Normal file
45
app/views/follows/_followable_button.html.erb
Normal file
@@ -0,0 +1,45 @@
|
||||
<span class="js-follow">
|
||||
<span class="followable-content">
|
||||
|
||||
<% 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 %>
|
||||
<div class="dropdown-pane" id="<%= follow_drop_id(followable) %>"
|
||||
data-dropdown data-auto-focus="true">
|
||||
<%= 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) %>
|
||||
</div>
|
||||
|
||||
<% 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 %>
|
||||
<div class="dropdown-pane" id="<%= unfollow_drop_id(followable) %>"
|
||||
data-dropdown data-auto-focus="true">
|
||||
<%= link_to unfollow_entity_text(followable),
|
||||
follow_path(follow),
|
||||
method: :delete, remote: true,
|
||||
id: unfollow_link_id(followable) %>
|
||||
</div>
|
||||
|
||||
<% end %>
|
||||
|
||||
</span>
|
||||
</span>
|
||||
2
app/views/follows/refresh_follow_button.js.erb
Normal file
2
app/views/follows/refresh_follow_button.js.erb
Normal file
@@ -0,0 +1,2 @@
|
||||
App.Followable.update("<%= dom_id(@followable) %>",
|
||||
"<%= j render('followable_button', followable: @followable) %>")
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="callout primary">
|
||||
<p>
|
||||
<%= 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 %>
|
||||
</p>
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
<span class="flag-content">
|
||||
<% if show_flag_action? proposal %>
|
||||
<a id="flag-expand-proposal-<%= proposal.id %>" data-toggle="flag-drop-proposal-<%= proposal.id %>" title="<%= t('shared.flag') %>">
|
||||
<span class="icon-flag flag-disable"></span>
|
||||
</a>
|
||||
<div class="dropdown-pane" id="flag-drop-proposal-<%= proposal.id %>" data-dropdown data-auto-focus="true">
|
||||
<%= link_to t('shared.flag'), flag_proposal_path(proposal), method: :put, remote: true, id: "flag-proposal-#{ proposal.id }" %>
|
||||
</div>
|
||||
<% end %>
|
||||
<span class="js-flag-actions">
|
||||
<span class="flag-content">
|
||||
<% if show_flag_action? proposal %>
|
||||
<a id="flag-expand-proposal-<%= proposal.id %>" data-toggle="flag-drop-proposal-<%= proposal.id %>" title="<%= t('shared.flag') %>">
|
||||
<span class="icon-flag flag-disable"></span>
|
||||
</a>
|
||||
<div class="dropdown-pane" id="flag-drop-proposal-<%= proposal.id %>" data-dropdown data-auto-focus="true">
|
||||
<%= link_to t('shared.flag'), flag_proposal_path(proposal), method: :put, remote: true, id: "flag-proposal-#{ proposal.id }" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if show_unflag_action? proposal %>
|
||||
<a id="unflag-expand-proposal-<%= proposal.id %>" data-toggle="unflag-drop-proposal-<%= proposal.id %>" title="<%= t('shared.unflag') %>">
|
||||
<span class="icon-flag flag-active"></span>
|
||||
</a>
|
||||
<div class="dropdown-pane" id="unflag-drop-proposal-<%= proposal.id %>" data-dropdown data-auto-focus="true">
|
||||
<%= link_to t('shared.unflag'), unflag_proposal_path(proposal), method: :put, remote: true, id: "unflag-proposal-#{ proposal.id }" %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if show_unflag_action? proposal %>
|
||||
<a id="unflag-expand-proposal-<%= proposal.id %>" data-toggle="unflag-drop-proposal-<%= proposal.id %>" title="<%= t('shared.unflag') %>">
|
||||
<span class="icon-flag flag-active"></span>
|
||||
</a>
|
||||
<div class="dropdown-pane" id="unflag-drop-proposal-<%= proposal.id %>" data-dropdown data-auto-focus="true">
|
||||
<%= link_to t('shared.unflag'), unflag_proposal_path(proposal), method: :put, remote: true, id: "unflag-proposal-#{ proposal.id }" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@@ -49,10 +49,12 @@
|
||||
<span class="bullet"> • </span>
|
||||
<span class="icon-comments"></span>
|
||||
<%= link_to t("proposals.show.comments", count: @proposal.comments_count), "#comments" %>
|
||||
<span class="bullet"> • </span>
|
||||
<span class="js-flag-actions">
|
||||
|
||||
<% if current_user %>
|
||||
<span class="bullet"> • </span>
|
||||
<%= render 'proposals/flag_actions', proposal: @proposal %>
|
||||
</span>
|
||||
<% end %>
|
||||
|
||||
</div>
|
||||
|
||||
<br>
|
||||
@@ -137,6 +139,9 @@
|
||||
title: @proposal.title,
|
||||
url: proposal_url(@proposal)
|
||||
} %>
|
||||
|
||||
<%= render 'follows/followable_button', followable: @proposal if current_user %>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -46,6 +46,21 @@
|
||||
<p><%= t('users.show.private_activity') %></p>
|
||||
<% end %>
|
||||
|
||||
<% if @user.public_interests || @authorized_current_user %>
|
||||
<div id="public_interests" class="public-interests">
|
||||
<h4><%= t('account.show.public_interests_title_list') %></h4>
|
||||
<% @user.interests.in_groups_of(10, false) do |interests_group| %>
|
||||
<div class="small-4 column end">
|
||||
<ul class="no-bullet">
|
||||
<% interests_group.each do |interest| %>
|
||||
<li> <small><%= interest %></small> </li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -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."
|
||||
|
||||
|
||||
@@ -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."
|
||||
timestamp_error_message: "Eso ha sido demasiado rápido. Por favor, reenvía el formulario."
|
||||
|
||||
@@ -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'
|
||||
|
||||
12
db/migrate/20170626180127_create_follows.rb
Normal file
12
db/migrate/20170626180127_create_follows.rb
Normal file
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
class AddPublicInterestsToUser < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :users, :public_interests, :boolean, default: false
|
||||
end
|
||||
end
|
||||
19
db/schema.rb
19
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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 "<title>#{proposal.title}</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)
|
||||
|
||||
@@ -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
|
||||
|
||||
26
spec/models/follow_spec.rb
Normal file
26
spec/models/follow_spec.rb
Normal file
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
83
spec/shared/features/followable.rb
Normal file
83
spec/shared/features/followable.rb
Normal file
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user