Merge pull request #1727 from rockandror/followable

Followable
This commit is contained in:
BertoCQ
2017-07-11 13:19:42 +02:00
committed by GitHub
40 changed files with 725 additions and 34 deletions

View File

@@ -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(){

View 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()

View File

@@ -1982,6 +1982,14 @@ table {
}
}
.public-interests {
margin-top: $line-height;
.column {
padding-left: 0;
}
}
// 18. Banners
// -----------

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -26,4 +26,4 @@ module FlagsHelper
end
end
end
end

View 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

View File

@@ -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

View File

@@ -6,6 +6,7 @@ class Budget
include Taggable
include Searchable
include Reclassification
include Followable
acts_as_votable
acts_as_paranoid column: :hidden_at

View 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
View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View 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>

View File

@@ -0,0 +1,2 @@
App.Followable.update("<%= dom_id(@followable) %>",
"<%= j render('followable_button', followable: @followable) %>")

View File

@@ -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>

View File

@@ -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>

View File

@@ -49,10 +49,12 @@
<span class="bullet">&nbsp;&bull;&nbsp;</span>
<span class="icon-comments"></span>&nbsp;
<%= link_to t("proposals.show.comments", count: @proposal.comments_count), "#comments" %>
<span class="bullet">&nbsp;&bull;&nbsp;</span>
<span class="js-flag-actions">
<% if current_user %>
<span class="bullet">&nbsp;&bull;&nbsp;</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>

View File

@@ -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>

View File

@@ -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."

View File

@@ -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."

View File

@@ -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'

View 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

View File

@@ -0,0 +1,5 @@
class AddPublicInterestsToUser < ActiveRecord::Migration
def change
add_column :users, :public_interests, :boolean, default: false
end
end

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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