Create followable concern, follow model. Add followable to proposal model.

This commit is contained in:
Senén Rodero Rodríguez
2017-06-26 20:36:09 +02:00
parent e8e6eff679
commit 84dbef16a4
18 changed files with 372 additions and 4 deletions

View File

@@ -0,0 +1,28 @@
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.each do |name, value|
if name =~ /(.+)_id$/
return $1.classify.constantize.find(value)
end
end
nil
end
end

View File

@@ -0,0 +1,28 @@
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(entity)
t('shared.follow_entity', entity: t("activerecord.models.#{entity}.one").downcase)
end
def unfollow_entity_text(entity)
t('shared.unfollow_entity', entity: t("activerecord.models.#{entity}.one").downcase)
end
def entity_name(followable)
followable.class.name.downcase
end
private
def followed?(followable)
Follow.followed?(current_user, followable)
end
end

View File

@@ -34,6 +34,8 @@ module Abilities
can [:flag, :unflag], Proposal can [:flag, :unflag], Proposal
cannot [:flag, :unflag], Proposal, author_id: user.id cannot [:flag, :unflag], Proposal, author_id: user.id
can [:create, :destroy], Follow
unless user.organization? unless user.organization?
can :vote, Debate can :vote, Debate
can :vote, Comment can :vote, Comment

View File

@@ -0,0 +1,8 @@
module Followable
extend ActiveSupport::Concern
included do
has_many :follows, as: :followable
end
end

32
app/models/follow.rb Normal file
View File

@@ -0,0 +1,32 @@
class Follow < ActiveRecord::Base
belongs_to :user
#TODO Rock&RoR: Check touch usage on cache system
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.follow(user, followable)
# return false if interested?(user, followable)
# create(user: user, followable: followable)
# end
#
# def self.unfollow(user, followable)
# interests = by_user_and_followable(user, followable)
# return false if interests.empty?
# interests.destroy_all
# 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 Filterable
include HasPublicAuthor include HasPublicAuthor
include Graphqlable include Graphqlable
include Followable
acts_as_votable acts_as_votable
acts_as_paranoid column: :hidden_at acts_as_paranoid column: :hidden_at

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_sent, class_name: 'DirectMessage', foreign_key: :sender_id
has_many :direct_messages_received, class_name: 'DirectMessage', foreign_key: :receiver_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 :legislation_answers, class_name: 'Legislation::Answer', dependent: :destroy, inverse_of: :user
has_many :follows
belongs_to :geozone belongs_to :geozone
validates :username, presence: true, if: :username_required? validates :username, presence: true, if: :username_required?

View File

@@ -0,0 +1,20 @@
<span class="followable-content">
<% if show_follow_action? followable %>
<a id="follow-expand-<%= entity_name(followable) %>-<%= followable.id %>" data-toggle="follow-drop-<%= entity_name(followable) %>-<%= followable.id %>" title="<%= follow_entity_text(entity_name(followable)) %>">
<%= t('shared.follow') %>
</a>
<div class="dropdown-pane" id="follow-drop-<%= entity_name(followable) %>-<%= followable.id %>" data-dropdown data-auto-focus="true">
<%= link_to follow_entity_text(entity_name(followable)), follows_path("#{entity_name(followable)}_id": followable.id), method: :post, remote: true, id: "follow-#{entity_name(followable)}-#{ followable.id }" %>
</div>
<% end %>
<% if show_unfollow_action? followable %>
<% follow = followable.follows.where(user: current_user).first %>
<a id="unfollow-expand-<%= entity_name(followable) %>-<%= followable.id %>" data-toggle="unfollow-drop-<%= entity_name(followable) %>-<%= followable.id %>" title="<%= unfollow_entity_text(entity_name(followable)) %>">
<%= t('shared.unfollow') %>
</a>
<div class="dropdown-pane" id="unfollow-drop-<%= entity_name(followable) %>-<%= followable.id %>" data-dropdown data-auto-focus="true">
<%= link_to unfollow_entity_text(entity_name(followable)), follow_path(follow), method: :delete, remote: true, id: "unfollow-#{entity_name(followable)}-#{ followable.id }" %>
</div>
<% end %>
</span>

View File

@@ -0,0 +1 @@
$("#<%= dom_id(@followable) %> .js-follow").html('<%= j render("followable_button", followable: @followable) %>');

View File

@@ -53,6 +53,10 @@
<span class="js-flag-actions"> <span class="js-flag-actions">
<%= render 'proposals/flag_actions', proposal: @proposal %> <%= render 'proposals/flag_actions', proposal: @proposal %>
</span> </span>
<span class="bullet">&nbsp;&bull;&nbsp;</span>
<span class="js-follow">
<%= render 'follows/followable_button', followable: @proposal %>
</span>
</div> </div>
<br> <br>

View File

@@ -500,6 +500,8 @@ en:
check_none: None check_none: None
collective: Collective collective: Collective
flag: Flag as inappropriate flag: Flag as inappropriate
follow: "Follow"
follow_entity: "Follow %{entity}"
hide: Hide hide: Hide
print: print:
print_button: Print this info print_button: Print this info
@@ -532,6 +534,8 @@ en:
target_blank_html: " (link opens in new window)" target_blank_html: " (link opens in new window)"
you_are_in: "You are in" you_are_in: "You are in"
unflag: Unflag unflag: Unflag
unfollow: "Unfollow"
unfollow_entity: "Unfollow %{entity}"
outline: outline:
debates: Debates debates: Debates
proposals: Proposals proposals: Proposals
@@ -708,4 +712,3 @@ en:
invisible_captcha: invisible_captcha:
sentence_for_humans: "If you are human, ignore this field" sentence_for_humans: "If you are human, ignore this field"
timestamp_error_message: "Sorry, that was too quick! Please resubmit." timestamp_error_message: "Sorry, that was too quick! Please resubmit."

View File

@@ -500,6 +500,8 @@ es:
check_none: Ninguno check_none: Ninguno
collective: Colectivo collective: Colectivo
flag: Denunciar como inapropiado flag: Denunciar como inapropiado
follow: "Seguir"
follow_entity: "Seguir %{entity}"
hide: Ocultar hide: Ocultar
print: print:
print_button: Imprimir esta información print_button: Imprimir esta información
@@ -532,6 +534,8 @@ es:
target_blank_html: " (se abre en ventana nueva)" target_blank_html: " (se abre en ventana nueva)"
you_are_in: "Estás en" you_are_in: "Estás en"
unflag: Deshacer denuncia unflag: Deshacer denuncia
unfollow: Dejar de seguir
unfollow_entity: "Dejar de seguir %{entity}"
outline: outline:
debates: Debates debates: Debates
proposals: Propuestas proposals: Propuestas
@@ -707,4 +711,4 @@ es:
user_permission_votes: Participar en las votaciones finales* user_permission_votes: Participar en las votaciones finales*
invisible_captcha: invisible_captcha:
sentence_for_humans: "Si eres humano, por favor ignora este campo" 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
end end
resources :follows, only: [:create, :destroy]
resources :stats, only: [:index] resources :stats, only: [:index]
resources :legacy_legislations, only: [:show], path: 'legislations' 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

@@ -326,6 +326,18 @@ ActiveRecord::Schema.define(version: 20170704105112) do
add_index "flags", ["user_id", "flaggable_type", "flaggable_id"], name: "access_inappropiate_flags", using: :btree 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 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| create_table "geozones", force: :cascade do |t|
t.string "name" t.string "name"
t.string "html_map_coordinates" t.string "html_map_coordinates"
@@ -1036,6 +1048,7 @@ ActiveRecord::Schema.define(version: 20170704105112) do
add_foreign_key "failed_census_calls", "poll_officers" add_foreign_key "failed_census_calls", "poll_officers"
add_foreign_key "failed_census_calls", "users" add_foreign_key "failed_census_calls", "users"
add_foreign_key "flags", "users" add_foreign_key "flags", "users"
add_foreign_key "follows", "users"
add_foreign_key "geozones_polls", "geozones" add_foreign_key "geozones_polls", "geozones"
add_foreign_key "geozones_polls", "polls" add_foreign_key "geozones_polls", "polls"
add_foreign_key "identities", "users" add_foreign_key "identities", "users"

View File

@@ -166,8 +166,8 @@ FactoryGirl.define do
end end
trait :flagged do trait :flagged do
after :create do |debate| after :create do |proposal|
Flag.flag(FactoryGirl.create(:user), debate) Flag.flag(FactoryGirl.create(:user), proposal)
end end
end end
@@ -349,6 +349,14 @@ FactoryGirl.define do
association :user, factory: :user association :user, factory: :user
end end
factory :follow do
association :user, factory: :user
trait :followed_proposal do
association :followable, factory: :proposal
end
end
factory :comment do factory :comment do
association :commentable, factory: :debate association :commentable, factory: :debate
user user

View File

@@ -1223,6 +1223,73 @@ feature 'Proposals' do
expect(Flag.flagged?(user, proposal)).to_not be expect(Flag.flagged?(user, proposal)).to_not be
end end
feature "Follows" do
scenario "Should not show follow button when there is no logged user" do
proposal = create(:proposal)
visit proposal_path(proposal)
within "#proposal_#{proposal.id}" do
expect(page).not_to have_link("Follow citizen proposal")
end
end
scenario "Following", :js do
user = create(:user)
proposal = create(:proposal)
login_as(user)
visit proposal_path(proposal)
within "#proposal_#{proposal.id}" do
page.find("#follow-expand-proposal-#{proposal.id}").click
page.find("#follow-proposal-#{proposal.id}").click
expect(page).to have_css("#unfollow-expand-proposal-#{proposal.id}")
end
expect(Follow.followed?(user, proposal)).to be
end
scenario "Show unfollow button when user already follow this proposal" do
user = create(:user)
follow = create(:follow, :followed_proposal, user: user)
login_as(user)
visit proposal_path(follow.followable)
expect(page).to have_link("Unfollow citizen proposal")
end
scenario "Unfollowing", :js do
user = create(:user)
proposal = create(:proposal)
follow = create(:follow, :followed_proposal, user: user, followable: proposal)
login_as(user)
visit proposal_path(proposal)
within "#proposal_#{proposal.id}" do
page.find("#unfollow-expand-proposal-#{proposal.id}").click
page.find("#unfollow-proposal-#{proposal.id}").click
expect(page).to have_css("#follow-expand-proposal-#{proposal.id}")
end
expect(Follow.followed?(user, proposal)).not_to be
end
scenario "Show follow button when user is not following this proposal" do
user = create(:user)
proposal = create(:proposal)
login_as(user)
visit proposal_path(proposal)
expect(page).to have_link("Follow citizen proposal")
end
end
scenario 'Erased author' do scenario 'Erased author' do
user = create(:user) user = create(:user)
proposal = create(:proposal, author: user) proposal = create(:proposal, author: user)

134
spec/models/follow_spec.rb Normal file
View File

@@ -0,0 +1,134 @@
require 'rails_helper'
describe Follow do
let(:follow) { build(:follow, :followed_proposal) }
# it_behaves_like "has_public_author"
it "should be valid" do
expect(follow).to be_valid
end
it "should not be valid without an user_id" do
follow.user_id = nil
expect(follow).to_not be_valid
end
it "should not be valid without an followable_id" do
follow.followable_id = nil
expect(follow).to_not be_valid
end
it "should not be valid without an followable_type" do
follow.followable_type = nil
expect(follow).to_not be_valid
end
# describe "proposal" do
#
# let(:proposal) { create(:proposal) }
#
# describe 'create' do
#
# it 'creates a interest when there is none' do
# expect { described_class.follow(user, proposal) }.to change{ Interest.count }.by(1)
# expect(Interest.last.user).to eq(user)
# expect(Interest.last.interestable).to eq(proposal)
# end
#
# it 'does nothing if the interest already exists' do
# described_class.follow(user, proposal)
# expect(described_class.follow(user, proposal)).to eq(false)
# expect(Interest.by_user_and_interestable(user, proposal).count).to eq(1)
# end
#
# it 'increases the interest count' do
# expect { described_class.follow(user, proposal) }.to change{ proposal.reload.interests_count }.by(1)
# end
# end
#
# describe 'destroy' do
# it 'raises an error if the interest does not exist' do
# expect(described_class.unfollow(user, proposal)).to eq(false)
# end
#
# describe 'when the interest already exists' do
# before(:each) { described_class.follow(user, proposal) }
#
# it 'removes an existing interest' do
# expect { described_class.unfollow(user, proposal) }.to change{ Interest.count }.by(-1)
# end
#
# it 'decreases the interest count' do
# expect { described_class.unfollow(user, proposal) }.to change{ proposal.reload.interests_count }.by(-1)
# end
# end
# end
#
# describe '.interested?' do
# it 'returns false when the user has not flagged the proposal' do
# expect(described_class.interested?(user, proposal)).to_not be
# end
#
# it 'returns true when the user has interested the proposal' do
# described_class.follow(user, proposal)
# expect(described_class.interested?(user, proposal)).to be
# end
# end
# end
#
# describe "debate" do
#
# let(:debate) { create(:debate) }
#
# describe 'create' do
#
# it 'creates a interest when there is none' do
# expect { described_class.follow(user, debate) }.to change{ Interest.count }.by(1)
# expect(Interest.last.user).to eq(user)
# expect(Interest.last.interestable).to eq(debate)
# end
#
# it 'does nothing if the interest already exists' do
# described_class.follow(user, debate)
# expect(described_class.follow(user, debate)).to eq(false)
# expect(Interest.by_user_and_interestable(user, debate).count).to eq(1)
# end
#
# it 'increases the interest count' do
# expect { described_class.follow(user, debate) }.to change{ debate.reload.interests_count }.by(1)
# end
# end
#
# describe 'destroy' do
# it 'raises an error if the interest does not exist' do
# expect(described_class.unfollow(user, debate)).to eq(false)
# end
#
# describe 'when the interest already exists' do
# before(:each) { described_class.follow(user, debate) }
#
# it 'removes an existing interest' do
# expect { described_class.unfollow(user, debate) }.to change{ Interest.count }.by(-1)
# end
#
# it 'decreases the interest count' do
# expect { described_class.unfollow(user, debate) }.to change{ debate.reload.interests_count }.by(-1)
# end
# end
# end
#
# describe '.interested?' do
# it 'returns false when the user has not flagged the debate' do
# expect(described_class.interested?(user, debate)).to_not be
# end
#
# it 'returns true when the user has interested the debate' do
# described_class.follow(user, debate)
# expect(described_class.interested?(user, debate)).to be
# end
# end
# end
end