Merge pull request #1750 from rockandror/followable-user-activity

User page: new following section
This commit is contained in:
BertoCQ
2017-07-24 13:10:09 +02:00
committed by GitHub
27 changed files with 417 additions and 183 deletions

View File

@@ -94,7 +94,6 @@ var initialize_modules = function() {
App.LegislationAnnotatable.initialize();
App.WatchFormChanges.initialize();
App.TreeNavigator.initialize();
App.Followable.initialize();
};
$(function(){

View File

@@ -1,10 +1,8 @@
App.Followable =
initialize: ->
$('.followable-content a[data-toggle]').on 'click', (event) ->
event.preventDefault()
update: (followable_id, button) ->
update: (followable_id, button, notice) ->
$("#" + followable_id + " .js-follow").html(button)
# Temporary line. Waiting for issue resolution: https://github.com/consul/consul/issues/1736
initialize_modules()
if ($('[data-alert]').length > 0)
$('[data-alert]').replaceWith(notice)
else
$("body").append(notice)

View File

@@ -1965,8 +1965,39 @@ table {
.activity {
margin-bottom: $line-height * 2;
.accordion li {
margin-bottom: $line-height / 2;
.accordion-title {
border-bottom: 1px solid $border;
background: #f8f9fb;
font-size: $small-font-size;
padding: $line-height / 2;
}
.accordion-content {
padding: 0;
}
}
.accordion .title {
display: block;
line-height: $line-height;
}
.accordion .icon {
font-size: rem-calc(20);
float: left;
margin-right: $line-height / 3;
&.icon-debates {
margin-top: rem-calc(3);
}
}
table {
border: 0;
margin-bottom: 0;
}
td {

View File

@@ -3,15 +3,15 @@ class FollowsController < ApplicationController
load_and_authorize_resource
def create
@followable = find_followable
@follow = Follow.create(user: current_user, followable: @followable)
@follow = Follow.create(user: current_user, followable: find_followable)
flash.now[:notice] = t("shared.followable.#{followable_translation_key(@follow.followable)}.create.notice_html")
render :refresh_follow_button
end
def destroy
@follow = Follow.find(params[:id])
@followable = @follow.followable
@follow.destroy
flash.now[:notice] = t("shared.followable.#{followable_translation_key(@follow.followable)}.destroy.notice_html")
render :refresh_follow_button
end
@@ -20,4 +20,9 @@ class FollowsController < ApplicationController
def find_followable
params[:followable_type].constantize.find(params[:followable_id])
end
def followable_translation_key(followable)
followable.class.name.parameterize("_")
end
end

View File

@@ -1,13 +1,12 @@
class UsersController < ApplicationController
has_filters %w{proposals debates budget_investments comments}, only: :show
has_filters %w{proposals debates budget_investments comments follows}, only: :show
load_and_authorize_resource
helper_method :author?
helper_method :author_or_admin?
helper_method :valid_interests_access?
def show
load_filtered_activity if valid_access?
load_interests if valid_interests_access?
end
private
@@ -17,7 +16,8 @@ class UsersController < ApplicationController
proposals: Proposal.where(author_id: @user.id).count,
debates: (Setting['feature.debates'] ? Debate.where(author_id: @user.id).count : 0),
budget_investments: (Setting['feature.budgets'] ? Budget::Investment.where(author_id: @user.id).count : 0),
comments: only_active_commentables.count)
comments: only_active_commentables.count,
follows: @user.follows.count)
end
def load_filtered_activity
@@ -26,7 +26,8 @@ class UsersController < ApplicationController
when "proposals" then load_proposals
when "debates" then load_debates
when "budget_investments" then load_budget_investments
when "comments" then load_comments
when "comments" then load_comments
when "follows" then load_follows
else load_available_activity
end
end
@@ -44,6 +45,9 @@ class UsersController < ApplicationController
elsif @activity_counts[:comments] > 0
load_comments
@current_filter = "comments"
elsif @activity_counts[:follows] > 0
load_follows
@current_filter = "follows"
end
end
@@ -63,8 +67,8 @@ 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
def load_follows
@follows = @user.follows.group_by(&:followable_type)
end
def valid_access?
@@ -75,12 +79,8 @@ class UsersController < ApplicationController
@user.public_interests || authorized_current_user?
end
def author?
@author ||= current_user && (current_user == @user)
end
def author_or_admin?
@author_or_admin ||= current_user && (author? || current_user.administrator?)
def author?(proposal)
proposal.author_id == current_user.id if current_user
end
def authorized_current_user?

View File

@@ -0,0 +1,30 @@
module FollowablesHelper
def followable_type_title(followable_type)
t("activerecord.models.#{followable_type.underscore}.other")
end
def followable_icon(followable)
{
proposals: 'Proposal',
budget: 'Budget::Investment'
}.invert[followable]
end
def render_follow(follow)
followable = follow.followable
partial = followable_class_name(followable)
locals = {followable_class_name(followable).to_sym => followable}
render partial, locals
end
def followable_class_name(followable)
followable.class.to_s.parameterize.gsub('-', '_')
end
def find_or_build_follow(user, followable)
Follow.find_or_initialize_by(user: user, followable: followable)
end
end

View File

@@ -1,61 +1,13 @@
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
def follow_text(followable)
entity = followable.class.name.underscore
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
def unfollow_text(followable)
entity = followable.class.name.underscore
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

@@ -41,4 +41,4 @@ module UsersHelper
current_user && current_user.administrator?
end
end
end

View File

@@ -50,7 +50,7 @@ module Abilities
can :create, Budget::Investment, budget: { phase: "accepting" }
can :suggest, Budget::Investment, budget: { phase: "accepting" }
can :destroy, Budget::Investment, budget: { phase: ["accepting", "reviewing"] }, author_id: user.id
can :vote, Budget::Investment, budget: { phase: "selecting" }
can :vote, Budget::Investment, budget: { phase: "selecting" }
can [:show, :create], Budget::Ballot, budget: { phase: "balloting" }
can [:create, :destroy], Budget::Ballot::Line, budget: { phase: "balloting" }

View File

@@ -6,4 +6,8 @@ module Followable
has_many :followers, through: :follows, source: :user
end
def followed_by?(user)
followers.include?(user)
end
end

View File

@@ -6,15 +6,4 @@ class Follow < ActiveRecord::Base
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

@@ -113,7 +113,9 @@
url: budget_investment_url(budget_id: investment.budget_id, id: investment.id)
} %>
<%= render 'follows/followable_button', followable: investment if current_user %>
<% if current_user %>
<%= render 'follows/follow_button', follow: find_or_build_follow(current_user, investment) %>
<% end %>
</aside>
</div>

View File

@@ -0,0 +1,24 @@
<span class="js-follow">
<span class="followable-content">
<% if follow.followable.followed_by?(current_user) %>
<%= link_to t('shared.unfollow'),
follow_path(follow),
method: :delete, remote: true,
title: unfollow_text(follow.followable),
class: 'button hollow' %>
<% else %>
<%= link_to t('shared.follow'),
follows_path(followable_id: follow.followable.id,
followable_type: follow.followable.class.name),
method: :post, remote: true,
title: follow_text(follow.followable),
class: 'button hollow' %>
<% end %>
</span>
</span>

View File

@@ -1,45 +0,0 @@
<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

@@ -1,2 +1,3 @@
App.Followable.update("<%= dom_id(@followable) %>",
"<%= j render('followable_button', followable: @followable) %>")
App.Followable.update("<%= dom_id(@follow.followable) %>",
"<%= j render('follow_button', follow: @follow) %>",
"<%= j render('layouts/flash') %>")

View File

@@ -140,7 +140,9 @@
url: proposal_url(@proposal)
} %>
<%= render 'follows/followable_button', followable: @proposal if current_user %>
<% if current_user %>
<%= render 'follows/follow_button', follow: find_or_build_follow(current_user, @proposal) %>
<% end %>
</aside>
</div>

View File

@@ -1,3 +1,4 @@
<%= render "following" if @follows.present? %>
<%= render "proposals" if @proposals.present? %>
<%= render "debates" if @debates.present? && feature?(:debates) %>
<%= render "budget_investments" if @budget_investments.present? && feature?(:budgets) %>

View File

@@ -0,0 +1,11 @@
<tr id="budget_investment_<%= budget_investment.id %>">
<td>
<%= link_to budget_investment.title, budget_investment_path(budget_investment.budget, budget_investment) %>
</td>
<td class="text-right">
<% if can? :destroy, budget_investment %>
<%= link_to t('shared.delete'), budget_investment_path(budget_investment.budget, budget_investment),
method: :delete, class: "button hollow alert" %>
<% end %>
</td>
</tr>

View File

@@ -1,16 +1,6 @@
<table id="budget_investments_list" class="clear activity-budget-investments">
<% @budget_investments.each do |budget_investment| %>
<tr id="budget_investment_<%= budget_investment.id %>">
<td>
<%= link_to budget_investment.title, budget_investment_path(budget_investment.budget, budget_investment) %>
</td>
<td class="text-right">
<% if can? :destroy, budget_investment %>
<%= link_to t('shared.delete'), budget_investment_path(budget_investment.budget, budget_investment),
method: :delete, class: "button hollow alert" %>
<% end %>
</td>
</tr>
<%= render "budget_investment", budget_investment: budget_investment %>
<% end %>
</table>

View File

@@ -0,0 +1,30 @@
<ul class="accordion" data-accordion data-allow-all-closed="true">
<% @follows.each do |followable_type, follows| %>
<li class="accordion-item" data-accordion-item>
<a href="#" class="accordion-title">
<span class="icon">
<i class="icon icon-<%= followable_icon(followable_type) %>"></i>
</span>
<span class="title">
<strong><%= followable_type_title(followable_type) %></strong>
</span>
</a>
<div class="accordion-content" data-tab-content>
<table>
<tbody>
<% follows.each do |follow| %>
<%= render_follow(follow) %>
<% end %>
</tbody>
</table>
</div>
</li>
<% end %>
</ul>

View File

@@ -0,0 +1,34 @@
<tr id="proposal_<%= proposal.id %>">
<td>
<%= link_to proposal.title, proposal, proposal.retired? ? { class: 'retired' } : {} %>
<br>
<%= proposal.summary %>
</td>
<% if proposal.retired? %>
<td class="text-center">
<span class="label alert"><%= t('users.proposals.retired') %></span>
</td>
<% elsif author?(proposal) %>
<td class="text-center">
<%= link_to t("users.proposals.send_notification"),
new_proposal_notification_path(proposal_id: proposal.id),
class: 'button hollow' %>
</td>
<td class="text-center">
<% if proposal.retired? %>
<span class="label alert"><%= t('users.proposals.retired') %></span>
<% else %>
<%= link_to t('users.proposals.retire'),
retire_form_proposal_path(proposal),
class: 'button hollow alert' %>
<% end %>
</td>
<% end %>
</tr>

View File

@@ -1,30 +1,6 @@
<table class="clear activity-proposals">
<% @proposals.each do |proposal| %>
<tr id="proposal_<%= proposal.id %>">
<td>
<%= link_to proposal.title, proposal, proposal.retired? ? {class: 'retired'} : {} %>
<br>
<%= proposal.summary %>
</td>
<% if author? %>
<td class="text-center">
<%= link_to t("users.proposals.send_notification"), new_proposal_notification_path(proposal_id: proposal.id),
class: 'button hollow' %>
</td>
<td class="text-center">
<% if proposal.retired? %>
<span class="label alert"><%= t('users.proposals.retired') %></span>
<% else %>
<%= link_to t('users.proposals.retire'),
retire_form_proposal_path(proposal),
class: 'button hollow alert' %>
<% end %>
</td>
<% end %>
</tr>
<%= render "proposal", proposal: proposal %>
<% end %>
</table>

View File

@@ -46,7 +46,7 @@
<p><%= t('users.show.private_activity') %></p>
<% end %>
<% if @user.public_interests || @authorized_current_user %>
<% if valid_interests_access? %>
<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| %>

View File

@@ -505,7 +505,17 @@ en:
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."
followable:
budget_investment:
create:
notice_html: "You are now following this investment project! </br> We will notify you of changes as they occur so that you are up-to-date."
destroy:
notice_html: "You have stopped following this investment project! </br> You will no longer receive notifications related to this project."
proposal:
create:
notice_html: "Now you are following this citizen proposal! </br> We will notify you of changes as they occur so that you are up-to-date."
destroy:
notice_html: "You have stopped following this citizen proposal! </br> You will no longer receive notifications related to this proposal."
hide: Hide
print:
print_button: Print this info

View File

@@ -505,7 +505,17 @@ es:
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."
followable:
budget_investment:
create:
notice_html: "¡Ahora estás siguiendo este proyecto de inversión! </br> Te notificaremos los cambios a medida que se produzcan para que estés al día."
destroy:
notice_html: "¡Has dejado de seguir este proyecto de inverisión! </br> Ya no recibirás más notificaciones relacionadas con este proyecto."
proposal:
create:
notice_html: "¡Ahora estás siguiendo esta propuesta ciudadana! </br> Te notificaremos los cambios a medida que se produzcan para que estés al día."
destroy:
notice_html: "¡Has dejado de seguir esta propuesta ciudadana! </br> Ya no recibirás más notificaciones relacionadas con esta propuesta."
hide: Ocultar
print:
print_button: Imprimir esta información

View File

@@ -357,4 +357,159 @@ feature 'Users' do
end
end
feature 'Following (public page)' do
before do
@user = create(:user)
end
scenario 'Not display following tab when user is not following any followable' do
visit user_path(@user)
expect(page).not_to have_content('0 Following')
end
scenario 'Active following tab by default when follows filters selected', :js do
proposal = create(:proposal, author: @user)
create(:follow, followable: proposal, user: @user)
visit user_path(@user, filter: "follows")
expect(page).to have_selector(".activity li.active", text: "1 Following" )
end
describe 'Proposals' do
scenario 'Display following tab when user is following one proposal at least' do
proposal = create(:proposal)
create(:follow, followable: proposal, user: @user)
visit user_path(@user)
expect(page).to have_content('1 Following')
end
scenario 'Display accordion proposal tab when user is following one proposal at least' do
proposal = create(:proposal)
create(:follow, followable: proposal, user: @user)
visit user_path(@user, filter: "follows")
expect(page).to have_link('Citizen proposals', href: "#")
end
scenario 'Not display accordion proposal tab when user is not following any proposal' do
visit user_path(@user, filter: "follows")
expect(page).not_to have_link('Citizen proposals', href: "#")
end
scenario 'Display proposal with action buttons inside accordion proposal tab when current user is proposal author', :js do
proposal = create(:proposal, author: @user)
create(:follow, followable: proposal, user: @user)
login_as @user
visit user_path(@user, filter: "follows")
click_link 'Citizen proposals'
expect(page).to have_content proposal.title
expect(page).to have_link "Send notification"
expect(page).to have_link "Retire"
end
scenario 'Display proposal with action buttons inside accordion proposal tab when there is no logged user', :js do
proposal = create(:proposal, author: @user)
create(:follow, followable: proposal, user: @user)
visit user_path(@user, filter: "follows")
click_link 'Citizen proposals'
expect(page).to have_content proposal.title
expect(page).not_to have_link "Send notification"
expect(page).not_to have_link "Retire"
end
scenario 'Display proposal without action buttons inside accordion proposal tab when current user is not proposal author', :js do
proposal = create(:proposal)
create(:follow, followable: proposal, user: @user)
login_as @user
visit user_path(@user, filter: "follows")
click_link 'Citizen proposals'
expect(page).to have_content proposal.title
expect(page).not_to have_link "Send notification"
expect(page).not_to have_link "Retire"
end
end
describe 'Budget Investments' do
scenario 'Display following tab when user is following one budget investment at least' do
budget_investment = create(:budget_investment)
create(:follow, followable: budget_investment, user: @user)
visit user_path(@user)
expect(page).to have_content('1 Following')
end
scenario 'Display accordion budget investment tab when user is following one budget investment at least' do
budget_investment = create(:budget_investment)
create(:follow, followable: budget_investment, user: @user)
visit user_path(@user, filter: "follow")
expect(page).to have_link('Investments', href: "#")
end
scenario 'Not display accordion budget investment tab when user is not following any budget investment' do
visit user_path(@user, filter: "follow")
expect(page).not_to have_link('Investments', href: "#")
end
scenario 'Display budget investment with action buttons inside accordion budget investment tab when current user is a verified user and author', :js do
user = create(:user, :level_two)
budget_investment = create(:budget_investment, author: user)
create(:follow, followable: budget_investment, user: user)
login_as user
visit user_path(user, filter: "follows")
click_link 'Investments'
expect(page).to have_link budget_investment.title
expect(page).to have_link "Delete"
end
scenario 'Display budget investment with action buttons inside accordion budget investment tab when there is no logged user', :js do
user = create(:user, :level_two)
budget_investment = create(:budget_investment, author: user)
create(:follow, followable: budget_investment, user: user)
visit user_path(user, filter: "follows")
click_link 'Investments'
expect(page).to have_link budget_investment.title
expect(page).not_to have_link "Delete"
end
scenario 'Display budget investment without action buttons inside accordion budget investment tab when current user is not budget investment author', :js do
user = create(:user, :level_two)
budget_investment = create(:budget_investment)
create(:follow, followable: budget_investment, user: user)
login_as user
visit user_path(user, filter: "follows")
click_link 'Investments'
expect(page).to have_link budget_investment.title
expect(page).not_to have_link "Delete"
end
end
end
end

View File

@@ -41,19 +41,31 @@ shared_examples "followable" do |followable_class_name, followable_path, followa
expect(page).to have_link("Follow")
end
scenario "Should display unfollow button when click on follow button", :js do
scenario "Should display unfollow after user clicks 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}")
expect(page).not_to have_link "Follow"
expect(page).to have_link "Unfollow"
end
end
scenario "Should display new follower notice after user clicks on follow button", :js do
user = create(:user)
login_as(user)
visit send(followable_path, arguments)
within "##{dom_id(followable)}" do
click_link "Follow"
end
expect(page).to have_content strip_tags(t("shared.followable.#{followable_class_name}.create.notice_html"))
end
scenario "Display unfollow button when user already following" do
user = create(:user)
follow = create(:follow, user: user, followable: followable)
@@ -64,7 +76,7 @@ shared_examples "followable" do |followable_class_name, followable_path, followa
expect(page).to have_link("Unfollow")
end
scenario "Should display follow button when click on unfollow button", :js do
scenario "Should update follow button and show destroy notice after user clicks on unfollow button", :js do
user = create(:user)
follow = create(:follow, user: user, followable: followable)
login_as(user)
@@ -72,12 +84,25 @@ shared_examples "followable" do |followable_class_name, followable_path, followa
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}")
expect(page).not_to have_link "Unfollow"
expect(page).to have_link "Follow"
end
end
scenario "Should display destroy follower notice after user clicks 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"
end
expect(page).to have_content strip_tags(t("shared.followable.#{followable_class_name}.destroy.notice_html"))
end
end
end