Create followable concern, follow model. Add followable to proposal model.
This commit is contained in:
28
app/controllers/follows_controller.rb
Normal file
28
app/controllers/follows_controller.rb
Normal 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
|
||||
28
app/helpers/follows_helper.rb
Normal file
28
app/helpers/follows_helper.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
8
app/models/concerns/followable.rb
Normal file
8
app/models/concerns/followable.rb
Normal 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
32
app/models/follow.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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?
|
||||
|
||||
20
app/views/follows/_followable_button.html.erb
Normal file
20
app/views/follows/_followable_button.html.erb
Normal 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>
|
||||
1
app/views/follows/refresh_follow_button.js.erb
Normal file
1
app/views/follows/refresh_follow_button.js.erb
Normal file
@@ -0,0 +1 @@
|
||||
$("#<%= dom_id(@followable) %> .js-follow").html('<%= j render("followable_button", followable: @followable) %>');
|
||||
@@ -53,6 +53,10 @@
|
||||
<span class="js-flag-actions">
|
||||
<%= render 'proposals/flag_actions', proposal: @proposal %>
|
||||
</span>
|
||||
<span class="bullet"> • </span>
|
||||
<span class="js-follow">
|
||||
<%= render 'follows/followable_button', followable: @proposal %>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
@@ -500,6 +500,8 @@ en:
|
||||
check_none: None
|
||||
collective: Collective
|
||||
flag: Flag as inappropriate
|
||||
follow: "Follow"
|
||||
follow_entity: "Follow %{entity}"
|
||||
hide: Hide
|
||||
print:
|
||||
print_button: Print this info
|
||||
@@ -532,6 +534,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
|
||||
@@ -708,4 +712,3 @@ en:
|
||||
invisible_captcha:
|
||||
sentence_for_humans: "If you are human, ignore this field"
|
||||
timestamp_error_message: "Sorry, that was too quick! Please resubmit."
|
||||
|
||||
|
||||
@@ -500,6 +500,8 @@ es:
|
||||
check_none: Ninguno
|
||||
collective: Colectivo
|
||||
flag: Denunciar como inapropiado
|
||||
follow: "Seguir"
|
||||
follow_entity: "Seguir %{entity}"
|
||||
hide: Ocultar
|
||||
print:
|
||||
print_button: Imprimir esta información
|
||||
@@ -532,6 +534,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
|
||||
@@ -707,4 +711,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
|
||||
13
db/schema.rb
13
db/schema.rb
@@ -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"], 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"
|
||||
@@ -1036,6 +1048,7 @@ ActiveRecord::Schema.define(version: 20170704105112) 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,14 @@ FactoryGirl.define do
|
||||
association :user, factory: :user
|
||||
end
|
||||
|
||||
factory :follow do
|
||||
association :user, factory: :user
|
||||
|
||||
trait :followed_proposal do
|
||||
association :followable, factory: :proposal
|
||||
end
|
||||
end
|
||||
|
||||
factory :comment do
|
||||
association :commentable, factory: :debate
|
||||
user
|
||||
|
||||
@@ -1223,6 +1223,73 @@ feature 'Proposals' do
|
||||
expect(Flag.flagged?(user, proposal)).to_not be
|
||||
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
|
||||
user = create(:user)
|
||||
proposal = create(:proposal, author: user)
|
||||
|
||||
134
spec/models/follow_spec.rb
Normal file
134
spec/models/follow_spec.rb
Normal 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
|
||||
Reference in New Issue
Block a user