Revised public fields, wrote more exhaustive specs
This commit is contained in:
@@ -29,4 +29,8 @@ module Graphqlable
|
|||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def public_created_at
|
||||||
|
self.created_at.change(min: 0)
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -286,6 +286,18 @@ class User < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
delegate :can?, :cannot?, to: :ability
|
delegate :can?, :cannot?, to: :ability
|
||||||
|
|
||||||
|
def public_proposals
|
||||||
|
public_activity? ? proposals : []
|
||||||
|
end
|
||||||
|
|
||||||
|
def public_debates
|
||||||
|
public_activity? ? debates : []
|
||||||
|
end
|
||||||
|
|
||||||
|
def public_comments
|
||||||
|
public_activity? ? comments : []
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def clean_document_number
|
def clean_document_number
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
class Vote < ActsAsVotable::Vote
|
class Vote < ActsAsVotable::Vote
|
||||||
|
|
||||||
include Graphqlable
|
include Graphqlable
|
||||||
|
|
||||||
def self.public_for_api
|
def self.public_for_api
|
||||||
joins("FULL OUTER JOIN debates ON votable_type = 'Debate' AND votable_id = debates.id").
|
joins("FULL OUTER JOIN debates ON votable_type = 'Debate' AND votable_id = debates.id").
|
||||||
joins("FULL OUTER JOIN proposals ON votable_type = 'Proposal' AND votable_id = proposals.id").
|
joins("FULL OUTER JOIN proposals ON votable_type = 'Proposal' AND votable_id = proposals.id").
|
||||||
joins("FULL OUTER JOIN comments ON votable_type = 'Comment' AND votable_id = comments.id").
|
joins("FULL OUTER JOIN comments ON votable_type = 'Comment' AND votable_id = comments.id").
|
||||||
where("votable_type = 'Proposal' AND proposals.hidden_at IS NULL OR votable_type = 'Debate' AND debates.hidden_at IS NULL OR votable_type = 'Comment' AND comments.hidden_at IS NULL")
|
where("(votable_type = 'Proposal' AND proposals.hidden_at IS NULL) OR \
|
||||||
end
|
(votable_type = 'Debate' AND debates.hidden_at IS NULL) OR \
|
||||||
|
( \
|
||||||
def public_timestamp
|
(votable_type = 'Comment' AND comments.hidden_at IS NULL) AND \
|
||||||
self.created_at.change(min: 0)
|
( \
|
||||||
|
(comments.commentable_type = 'Proposal' AND (comments.commentable_id IN (SELECT id FROM proposals WHERE hidden_at IS NULL GROUP BY id))) OR \
|
||||||
|
(comments.commentable_type = 'Debate' AND (comments.commentable_id IN (SELECT id FROM debates WHERE hidden_at IS NULL GROUP BY id))) \
|
||||||
|
) \
|
||||||
|
)")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,24 +2,22 @@ User:
|
|||||||
fields:
|
fields:
|
||||||
id: integer
|
id: integer
|
||||||
username: string
|
username: string
|
||||||
debates: [Debate]
|
public_debates: [Debate]
|
||||||
proposals: [Proposal]
|
public_proposals: [Proposal]
|
||||||
comments: [Comment]
|
public_comments: [Comment]
|
||||||
organization: Organization
|
# organization: Organization
|
||||||
Debate:
|
Debate:
|
||||||
fields:
|
fields:
|
||||||
id: integer
|
id: integer
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
created_at: string
|
public_created_at: string
|
||||||
cached_votes_total: integer
|
cached_votes_total: integer
|
||||||
cached_votes_up: integer
|
cached_votes_up: integer
|
||||||
cached_votes_down: integer
|
cached_votes_down: integer
|
||||||
comments_count: integer
|
comments_count: integer
|
||||||
hot_score: integer
|
hot_score: integer
|
||||||
confidence_score: integer
|
confidence_score: integer
|
||||||
geozone_id: integer
|
|
||||||
geozone: Geozone
|
|
||||||
comments: [Comment]
|
comments: [Comment]
|
||||||
public_author: User
|
public_author: User
|
||||||
votes_for: [Vote]
|
votes_for: [Vote]
|
||||||
@@ -34,7 +32,7 @@ Proposal:
|
|||||||
comments_count: integer
|
comments_count: integer
|
||||||
hot_score: integer
|
hot_score: integer
|
||||||
confidence_score: integer
|
confidence_score: integer
|
||||||
created_at: string
|
public_created_at: string
|
||||||
summary: string
|
summary: string
|
||||||
video_url: string
|
video_url: string
|
||||||
geozone_id: integer
|
geozone_id: integer
|
||||||
@@ -53,7 +51,7 @@ Comment:
|
|||||||
commentable_id: integer
|
commentable_id: integer
|
||||||
commentable_type: string
|
commentable_type: string
|
||||||
body: string
|
body: string
|
||||||
created_at: string
|
public_created_at: string
|
||||||
cached_votes_total: integer
|
cached_votes_total: integer
|
||||||
cached_votes_up: integer
|
cached_votes_up: integer
|
||||||
cached_votes_down: integer
|
cached_votes_down: integer
|
||||||
@@ -67,11 +65,11 @@ Geozone:
|
|||||||
name: string
|
name: string
|
||||||
ProposalNotification:
|
ProposalNotification:
|
||||||
fields:
|
fields:
|
||||||
title: string
|
title: string
|
||||||
body: string
|
body: string
|
||||||
proposal_id: integer
|
proposal_id: integer
|
||||||
created_at: string
|
public_created_at: string
|
||||||
proposal: Proposal
|
proposal: Proposal
|
||||||
ActsAsTaggableOn::Tag:
|
ActsAsTaggableOn::Tag:
|
||||||
fields:
|
fields:
|
||||||
id: integer
|
id: integer
|
||||||
@@ -80,12 +78,12 @@ ActsAsTaggableOn::Tag:
|
|||||||
kind: string
|
kind: string
|
||||||
Vote:
|
Vote:
|
||||||
fields:
|
fields:
|
||||||
votable_id: integer
|
votable_id: integer
|
||||||
votable_type: string
|
votable_type: string
|
||||||
public_timestamp: string
|
public_created_at: string
|
||||||
vote_flag: boolean
|
vote_flag: boolean
|
||||||
Organization:
|
# Organization:
|
||||||
fields:
|
# fields:
|
||||||
id: integer
|
# id: integer
|
||||||
user_id: integer
|
# user_id: integer
|
||||||
name: string
|
# name: string
|
||||||
|
|||||||
@@ -44,11 +44,7 @@ module GraphQL
|
|||||||
field(field_name, -> { created_types[field_type] }) do
|
field(field_name, -> { created_types[field_type] }) do
|
||||||
resolve -> (object, arguments, context) do
|
resolve -> (object, arguments, context) do
|
||||||
association_target = object.send(field_name)
|
association_target = object.send(field_name)
|
||||||
if association_target.nil?
|
association_target.present? ? field_type.public_for_api.find_by(id: association_target.id) : nil
|
||||||
nil
|
|
||||||
else
|
|
||||||
field_type.public_for_api.find_by(id: association_target.id)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
when :multiple_association
|
when :multiple_association
|
||||||
|
|||||||
@@ -21,32 +21,46 @@ def hidden_field?(response, field_name)
|
|||||||
data_is_empty && error_is_present
|
data_is_empty && error_is_present
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def extract_fields(response, collection_name, field_chain)
|
||||||
|
fields = field_chain.split('.')
|
||||||
|
dig(response, "data.#{collection_name}.edges").collect do |node|
|
||||||
|
begin
|
||||||
|
if fields.size > 1
|
||||||
|
node['node'][fields.first][fields.second]
|
||||||
|
else
|
||||||
|
node['node'][fields.first]
|
||||||
|
end
|
||||||
|
rescue NoMethodError
|
||||||
|
end
|
||||||
|
end.compact
|
||||||
|
end
|
||||||
|
|
||||||
describe 'ConsulSchema' do
|
describe 'ConsulSchema' do
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
let(:proposal) { create(:proposal, author: user) }
|
let(:proposal) { create(:proposal, author: user) }
|
||||||
|
|
||||||
it "returns fields of Int type" do
|
it 'returns fields of Int type' do
|
||||||
response = execute("{ proposal(id: #{proposal.id}) { id } }")
|
response = execute("{ proposal(id: #{proposal.id}) { id } }")
|
||||||
expect(dig(response, 'data.proposal.id')).to eq(proposal.id)
|
expect(dig(response, 'data.proposal.id')).to eq(proposal.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns fields of String type" do
|
it 'returns fields of String type' do
|
||||||
response = execute("{ proposal(id: #{proposal.id}) { title } }")
|
response = execute("{ proposal(id: #{proposal.id}) { title } }")
|
||||||
expect(dig(response, 'data.proposal.title')).to eq(proposal.title)
|
expect(dig(response, 'data.proposal.title')).to eq(proposal.title)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns has_one associations" do
|
xit 'returns has_one associations' do
|
||||||
organization = create(:organization)
|
organization = create(:organization)
|
||||||
response = execute("{ user(id: #{organization.user_id}) { organization { name } } }")
|
response = execute("{ user(id: #{organization.user_id}) { organization { name } } }")
|
||||||
expect(dig(response, 'data.user.organization.name')).to eq(organization.name)
|
expect(dig(response, 'data.user.organization.name')).to eq(organization.name)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns belongs_to associations" do
|
it 'returns belongs_to associations' do
|
||||||
response = execute("{ proposal(id: #{proposal.id}) { public_author { username } } }")
|
response = execute("{ proposal(id: #{proposal.id}) { public_author { username } } }")
|
||||||
expect(dig(response, 'data.proposal.public_author.username')).to eq(proposal.public_author.username)
|
expect(dig(response, 'data.proposal.public_author.username')).to eq(proposal.public_author.username)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns has_many associations" do
|
it 'returns has_many associations' do
|
||||||
comments_author = create(:user)
|
comments_author = create(:user)
|
||||||
comment_1 = create(:comment, author: comments_author, commentable: proposal)
|
comment_1 = create(:comment, author: comments_author, commentable: proposal)
|
||||||
comment_2 = create(:comment, author: comments_author, commentable: proposal)
|
comment_2 = create(:comment, author: comments_author, commentable: proposal)
|
||||||
@@ -58,7 +72,7 @@ describe 'ConsulSchema' do
|
|||||||
expect(comment_bodies).to match_array([comment_1.body, comment_2.body])
|
expect(comment_bodies).to match_array([comment_1.body, comment_2.body])
|
||||||
end
|
end
|
||||||
|
|
||||||
it "executes deeply nested queries" do
|
xit 'executes deeply nested queries' do
|
||||||
org_user = create(:user)
|
org_user = create(:user)
|
||||||
organization = create(:organization, user: org_user)
|
organization = create(:organization, user: org_user)
|
||||||
org_proposal = create(:proposal, author: org_user)
|
org_proposal = create(:proposal, author: org_user)
|
||||||
@@ -67,36 +81,594 @@ describe 'ConsulSchema' do
|
|||||||
expect(dig(response, 'data.proposal.public_author.organization.name')).to eq(organization.name)
|
expect(dig(response, 'data.proposal.public_author.organization.name')).to eq(organization.name)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "hides confidential fields of Int type" do
|
it 'hides confidential fields of Int type' do
|
||||||
response = execute("{ user(id: #{user.id}) { failed_census_calls_count } }")
|
response = execute("{ user(id: #{user.id}) { failed_census_calls_count } }")
|
||||||
expect(hidden_field?(response, 'failed_census_calls_count')).to be_truthy
|
expect(hidden_field?(response, 'failed_census_calls_count')).to be_truthy
|
||||||
end
|
end
|
||||||
|
|
||||||
it "hides confidential fields of String type" do
|
it 'hides confidential fields of String type' do
|
||||||
response = execute("{ user(id: #{user.id}) { encrypted_password } }")
|
response = execute("{ user(id: #{user.id}) { encrypted_password } }")
|
||||||
expect(hidden_field?(response, 'encrypted_password')).to be_truthy
|
expect(hidden_field?(response, 'encrypted_password')).to be_truthy
|
||||||
end
|
end
|
||||||
|
|
||||||
it "hides confidential has_one associations" do
|
xit 'hides confidential has_one associations' do
|
||||||
user.administrator = create(:administrator)
|
user.administrator = create(:administrator)
|
||||||
response = execute("{ user(id: #{user.id}) { administrator { id } } }")
|
response = execute("{ user(id: #{user.id}) { administrator { id } } }")
|
||||||
expect(hidden_field?(response, 'administrator')).to be_truthy
|
expect(hidden_field?(response, 'administrator')).to be_truthy
|
||||||
end
|
end
|
||||||
|
|
||||||
it "hides confidential belongs_to associations" do
|
it 'hides confidential belongs_to associations' do
|
||||||
create(:failed_census_call, user: user)
|
create(:failed_census_call, user: user)
|
||||||
response = execute("{ user(id: #{user.id}) { failed_census_calls { id } } }")
|
response = execute("{ user(id: #{user.id}) { failed_census_calls { id } } }")
|
||||||
expect(hidden_field?(response, 'failed_census_calls')).to be_truthy
|
expect(hidden_field?(response, 'failed_census_calls')).to be_truthy
|
||||||
end
|
end
|
||||||
|
|
||||||
it "hides confidential has_many associations" do
|
it 'hides confidential has_many associations' do
|
||||||
create(:direct_message, sender: user)
|
create(:direct_message, sender: user)
|
||||||
response = execute("{ user(id: #{user.id}) { direct_messages_sent { id } } }")
|
response = execute("{ user(id: #{user.id}) { direct_messages_sent { id } } }")
|
||||||
expect(hidden_field?(response, 'direct_messages_sent')).to be_truthy
|
expect(hidden_field?(response, 'direct_messages_sent')).to be_truthy
|
||||||
end
|
end
|
||||||
|
|
||||||
it "hides confidential fields inside deeply nested queries" do
|
it 'hides confidential fields inside deeply nested queries' do
|
||||||
response = execute("{ proposals(first: 1) { edges { node { public_author { encrypted_password } } } } }")
|
response = execute("{ proposals(first: 1) { edges { node { public_author { encrypted_password } } } } }")
|
||||||
expect(hidden_field?(response, 'encrypted_password')).to be_truthy
|
expect(hidden_field?(response, 'encrypted_password')).to be_truthy
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'Users' do
|
||||||
|
let(:user) { create(:user, public_activity: false) }
|
||||||
|
|
||||||
|
it 'does not link debates if activity is not public' do
|
||||||
|
create(:debate, author: user)
|
||||||
|
|
||||||
|
response = execute("{ user(id: #{user.id}) { public_debates { edges { node { title } } } } }")
|
||||||
|
received_debates = dig(response, 'data.user.public_debates.edges')
|
||||||
|
|
||||||
|
expect(received_debates).to eq []
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not link proposals if activity is not public' do
|
||||||
|
create(:proposal, author: user)
|
||||||
|
|
||||||
|
response = execute("{ user(id: #{user.id}) { public_proposals { edges { node { title } } } } }")
|
||||||
|
received_proposals = dig(response, 'data.user.public_proposals.edges')
|
||||||
|
|
||||||
|
expect(received_proposals).to eq []
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not link comments if activity is not public' do
|
||||||
|
create(:comment, author: user)
|
||||||
|
|
||||||
|
response = execute("{ user(id: #{user.id}) { public_comments { edges { node { body } } } } }")
|
||||||
|
received_comments = dig(response, 'data.user.public_comments.edges')
|
||||||
|
|
||||||
|
expect(received_comments).to eq []
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'Proposals' do
|
||||||
|
it 'does not include hidden proposals' do
|
||||||
|
visible_proposal = create(:proposal)
|
||||||
|
hidden_proposal = create(:proposal, :hidden)
|
||||||
|
|
||||||
|
response = execute('{ proposals { edges { node { title } } } }')
|
||||||
|
received_titles = extract_fields(response, 'proposals', 'title')
|
||||||
|
|
||||||
|
expect(received_titles).to match_array [visible_proposal.title]
|
||||||
|
end
|
||||||
|
|
||||||
|
xit 'only returns proposals of the Human Rights proceeding' do
|
||||||
|
proposal = create(:proposal)
|
||||||
|
human_rights_proposal = create(:proposal, proceeding: 'Derechos Humanos', sub_proceeding: 'Right to have a job')
|
||||||
|
other_proceeding_proposal = create(:proposal)
|
||||||
|
other_proceeding_proposal.update_attribute(:proceeding, 'Another proceeding')
|
||||||
|
|
||||||
|
response = execute('{ proposals { edges { node { title } } } }')
|
||||||
|
received_titles = extract_fields(response, 'proposals', 'title')
|
||||||
|
|
||||||
|
expect(received_titles).to match_array [proposal.title, human_rights_proposal.title]
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes proposals of authors even if public activity is set to false' do
|
||||||
|
visible_author = create(:user, public_activity: true)
|
||||||
|
hidden_author = create(:user, public_activity: false)
|
||||||
|
|
||||||
|
visible_proposal = create(:proposal, author: visible_author)
|
||||||
|
hidden_proposal = create(:proposal, author: hidden_author)
|
||||||
|
|
||||||
|
response = execute('{ proposals { edges { node { title } } } }')
|
||||||
|
received_titles = extract_fields(response, 'proposals', 'title')
|
||||||
|
|
||||||
|
expect(received_titles).to match_array [visible_proposal.title, hidden_proposal.title]
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not link author if public activity is set to false' do
|
||||||
|
visible_author = create(:user, public_activity: true)
|
||||||
|
hidden_author = create(:user, public_activity: false)
|
||||||
|
|
||||||
|
visible_proposal = create(:proposal, author: visible_author)
|
||||||
|
hidden_proposal = create(:proposal, author: hidden_author)
|
||||||
|
|
||||||
|
response = execute('{ proposals { edges { node { public_author { username } } } } }')
|
||||||
|
received_authors = extract_fields(response, 'proposals', 'public_author.username')
|
||||||
|
|
||||||
|
expect(received_authors).to match_array [visible_author.username]
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'only returns date and hour for created_at' do
|
||||||
|
created_at = Time.new(2017, 12, 31, 9, 30, 15).in_time_zone(Time.zone)
|
||||||
|
create(:proposal, created_at: created_at)
|
||||||
|
|
||||||
|
response = execute('{ proposals { edges { node { public_created_at } } } }')
|
||||||
|
received_timestamps = extract_fields(response, 'proposals', 'public_created_at')
|
||||||
|
|
||||||
|
expect(received_timestamps.first).to include('09:00:00')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'only retruns tags with kind nil or category' do
|
||||||
|
tag = create(:tag, name: 'Parks')
|
||||||
|
category_tag = create(:tag, name: 'Health', kind: 'category')
|
||||||
|
admin_tag = create(:tag, name: 'Admin tag', kind: 'admin')
|
||||||
|
|
||||||
|
proposal = create(:proposal, tag_list: 'Parks, Health, Admin tag')
|
||||||
|
|
||||||
|
response = execute("{ proposal(id: #{proposal.id}) { tags { edges { node { name } } } } }")
|
||||||
|
received_tags = dig(response, 'data.proposal.tags.edges').map { |node| node['node']['name'] }
|
||||||
|
|
||||||
|
expect(received_tags).to match_array ['Parks', 'Health']
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'Debates' do
|
||||||
|
it 'does not include hidden debates' do
|
||||||
|
visible_debate = create(:debate)
|
||||||
|
hidden_debate = create(:debate, :hidden)
|
||||||
|
|
||||||
|
response = execute('{ debates { edges { node { title } } } }')
|
||||||
|
received_titles = extract_fields(response, 'debates', 'title')
|
||||||
|
|
||||||
|
expect(received_titles).to match_array [visible_debate.title]
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes debates of authors even if public activity is set to false' do
|
||||||
|
visible_author = create(:user, public_activity: true)
|
||||||
|
hidden_author = create(:user, public_activity: false)
|
||||||
|
|
||||||
|
visible_debate = create(:debate, author: visible_author)
|
||||||
|
hidden_debate = create(:debate, author: hidden_author)
|
||||||
|
|
||||||
|
response = execute('{ debates { edges { node { title } } } }')
|
||||||
|
received_titles = extract_fields(response, 'debates', 'title')
|
||||||
|
|
||||||
|
expect(received_titles).to match_array [visible_debate.title, hidden_debate.title]
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not link author if public activity is set to false' do
|
||||||
|
visible_author = create(:user, public_activity: true)
|
||||||
|
hidden_author = create(:user, public_activity: false)
|
||||||
|
|
||||||
|
visible_debate = create(:debate, author: visible_author)
|
||||||
|
hidden_debate = create(:debate, author: hidden_author)
|
||||||
|
|
||||||
|
response = execute('{ debates { edges { node { public_author { username } } } } }')
|
||||||
|
received_authors = extract_fields(response, 'debates', 'public_author.username')
|
||||||
|
|
||||||
|
expect(received_authors).to match_array [visible_author.username]
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'only returns date and hour for created_at' do
|
||||||
|
created_at = Time.new(2017, 12, 31, 9, 30, 15).in_time_zone(Time.zone)
|
||||||
|
create(:debate, created_at: created_at)
|
||||||
|
|
||||||
|
response = execute('{ debates { edges { node { public_created_at } } } }')
|
||||||
|
received_timestamps = extract_fields(response, 'debates', 'public_created_at')
|
||||||
|
|
||||||
|
expect(received_timestamps.first).to include('09:00:00')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'only retruns tags with kind nil or category' do
|
||||||
|
tag = create(:tag, name: 'Parks')
|
||||||
|
category_tag = create(:tag, name: 'Health', kind: 'category')
|
||||||
|
admin_tag = create(:tag, name: 'Admin tag', kind: 'admin')
|
||||||
|
|
||||||
|
debate = create(:debate, tag_list: 'Parks, Health, Admin tag')
|
||||||
|
|
||||||
|
response = execute("{ debate(id: #{debate.id}) { tags { edges { node { name } } } } }")
|
||||||
|
received_tags = dig(response, 'data.debate.tags.edges').map { |node| node['node']['name'] }
|
||||||
|
|
||||||
|
expect(received_tags).to match_array ['Parks', 'Health']
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'Comments' do
|
||||||
|
it 'only returns comments from proposals and debates' do
|
||||||
|
proposal_comment = create(:comment, commentable: create(:proposal))
|
||||||
|
debate_comment = create(:comment, commentable: create(:debate))
|
||||||
|
spending_proposal_comment = build(:comment, commentable: create(:spending_proposal)).save(skip_validation: true)
|
||||||
|
|
||||||
|
response = execute('{ comments { edges { node { commentable_type } } } }')
|
||||||
|
received_commentables = extract_fields(response, 'comments', 'commentable_type')
|
||||||
|
|
||||||
|
expect(received_commentables).to match_array ['Proposal', 'Debate']
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'displays comments of authors even if public activity is set to false' do
|
||||||
|
visible_author = create(:user, public_activity: true)
|
||||||
|
hidden_author = create(:user, public_activity: false)
|
||||||
|
|
||||||
|
visible_comment = create(:comment, user: visible_author)
|
||||||
|
hidden_comment = create(:comment, user: hidden_author)
|
||||||
|
|
||||||
|
response = execute('{ comments { edges { node { body } } } }')
|
||||||
|
received_comments = extract_fields(response, 'comments', 'body')
|
||||||
|
|
||||||
|
expect(received_comments).to match_array [visible_comment.body, hidden_comment.body]
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not link author if public activity is set to false' do
|
||||||
|
visible_author = create(:user, public_activity: true)
|
||||||
|
hidden_author = create(:user, public_activity: false)
|
||||||
|
|
||||||
|
visible_comment = create(:comment, author: visible_author)
|
||||||
|
hidden_comment = create(:comment, author: hidden_author)
|
||||||
|
|
||||||
|
response = execute('{ comments { edges { node { public_author { username } } } } }')
|
||||||
|
received_authors = extract_fields(response, 'comments', 'public_author.username')
|
||||||
|
|
||||||
|
expect(received_authors).to match_array [visible_author.username]
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not include hidden comments' do
|
||||||
|
visible_comment = create(:comment)
|
||||||
|
hidden_comment = create(:comment, hidden_at: Time.now)
|
||||||
|
|
||||||
|
response = execute('{ comments { edges { node { body } } } }')
|
||||||
|
received_comments = extract_fields(response, 'comments', 'body')
|
||||||
|
|
||||||
|
expect(received_comments).to match_array [visible_comment.body]
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not include comments from hidden proposals' do
|
||||||
|
visible_proposal = create(:proposal)
|
||||||
|
hidden_proposal = create(:proposal, hidden_at: Time.now)
|
||||||
|
|
||||||
|
visible_proposal_comment = create(:comment, commentable: visible_proposal)
|
||||||
|
hidden_proposal_comment = create(:comment, commentable: hidden_proposal)
|
||||||
|
|
||||||
|
response = execute('{ comments { edges { node { body } } } }')
|
||||||
|
received_comments = extract_fields(response, 'comments', 'body')
|
||||||
|
|
||||||
|
expect(received_comments).to match_array [visible_proposal_comment.body]
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not include comments from hidden debates' do
|
||||||
|
visible_debate = create(:debate)
|
||||||
|
hidden_debate = create(:debate, hidden_at: Time.now)
|
||||||
|
|
||||||
|
visible_debate_comment = create(:comment, commentable: visible_debate)
|
||||||
|
hidden_debate_comment = create(:comment, commentable: hidden_debate)
|
||||||
|
|
||||||
|
response = execute('{ comments { edges { node { body } } } }')
|
||||||
|
received_comments = extract_fields(response, 'comments', 'body')
|
||||||
|
|
||||||
|
expect(received_comments).to match_array [visible_debate_comment.body]
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not include comments of debates that are not public' do
|
||||||
|
not_public_debate = create(:debate, :hidden)
|
||||||
|
not_public_debate_comment = create(:comment, commentable: not_public_debate)
|
||||||
|
allow(Comment).to receive(:public_for_api).and_return([])
|
||||||
|
|
||||||
|
response = execute('{ comments { edges { node { body } } } }')
|
||||||
|
received_comments = extract_fields(response, 'comments', 'body')
|
||||||
|
|
||||||
|
expect(received_comments).to_not include(not_public_debate_comment.body)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not include comments of proposals that are not public' do
|
||||||
|
not_public_proposal = create(:proposal)
|
||||||
|
not_public_proposal_comment = create(:comment, commentable: not_public_proposal)
|
||||||
|
allow(Comment).to receive(:public_for_api).and_return([])
|
||||||
|
|
||||||
|
response = execute('{ comments { edges { node { body } } } }')
|
||||||
|
received_comments = extract_fields(response, 'comments', 'body')
|
||||||
|
|
||||||
|
expect(received_comments).to_not include(not_public_proposal_comment.body)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'only returns date and hour for created_at' do
|
||||||
|
created_at = Time.new(2017, 12, 31, 9, 30, 15).in_time_zone(Time.zone)
|
||||||
|
create(:comment, created_at: created_at)
|
||||||
|
|
||||||
|
response = execute('{ comments { edges { node { public_created_at } } } }')
|
||||||
|
received_timestamps = extract_fields(response, 'comments', 'public_created_at')
|
||||||
|
|
||||||
|
expect(received_timestamps.first).to include('09:00:00')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'Geozones' do
|
||||||
|
it 'returns geozones' do
|
||||||
|
geozone_names = [ create(:geozone), create(:geozone) ].map { |geozone| geozone.name }
|
||||||
|
|
||||||
|
response = execute('{ geozones { edges { node { name } } } }')
|
||||||
|
received_names = extract_fields(response, 'geozones', 'name')
|
||||||
|
|
||||||
|
expect(received_names).to match_array geozone_names
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'Proposal notifications' do
|
||||||
|
|
||||||
|
it 'does not include proposal notifications for hidden proposals' do
|
||||||
|
visible_proposal = create(:proposal)
|
||||||
|
hidden_proposal = create(:proposal, :hidden)
|
||||||
|
|
||||||
|
visible_proposal_notification = create(:proposal_notification, proposal: visible_proposal)
|
||||||
|
hidden_proposal_notification = create(:proposal_notification, proposal: hidden_proposal)
|
||||||
|
|
||||||
|
response = execute('{ proposal_notifications { edges { node { title } } } }')
|
||||||
|
received_notifications = extract_fields(response, 'proposal_notifications', 'title')
|
||||||
|
|
||||||
|
expect(received_notifications).to match_array [visible_proposal_notification.title]
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not include proposal notifications for proposals that are not public' do
|
||||||
|
not_public_proposal = create(:proposal)
|
||||||
|
not_public_proposal_notification = create(:proposal_notification, proposal: not_public_proposal)
|
||||||
|
allow(ProposalNotification).to receive(:public_for_api).and_return([])
|
||||||
|
|
||||||
|
response = execute('{ proposal_notifications { edges { node { title } } } }')
|
||||||
|
received_notifications = extract_fields(response, 'proposal_notifications', 'title')
|
||||||
|
|
||||||
|
expect(received_notifications).to_not include(not_public_proposal_notification.title)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'only returns date and hour for created_at' do
|
||||||
|
created_at = Time.new(2017, 12, 31, 9, 30, 15).in_time_zone(Time.zone)
|
||||||
|
create(:proposal_notification, created_at: created_at)
|
||||||
|
|
||||||
|
response = execute('{ proposal_notifications { edges { node { public_created_at } } } }')
|
||||||
|
received_timestamps = extract_fields(response, 'proposal_notifications', 'public_created_at')
|
||||||
|
|
||||||
|
expect(received_timestamps.first).to include('09:00:00')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'only links proposal if public' do
|
||||||
|
visible_proposal = create(:proposal)
|
||||||
|
hidden_proposal = create(:proposal, :hidden)
|
||||||
|
|
||||||
|
visible_proposal_notification = create(:proposal_notification, proposal: visible_proposal)
|
||||||
|
hidden_proposal_notification = create(:proposal_notification, proposal: hidden_proposal)
|
||||||
|
|
||||||
|
response = execute('{ proposal_notifications { edges { node { proposal { title } } } } }')
|
||||||
|
received_proposals = extract_fields(response, 'proposal_notifications', 'proposal.title')
|
||||||
|
|
||||||
|
expect(received_proposals).to match_array [visible_proposal.title]
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'Tags' do
|
||||||
|
it 'only display tags with kind nil or category' do
|
||||||
|
tag = create(:tag, name: 'Parks')
|
||||||
|
category_tag = create(:tag, name: 'Health', kind: 'category')
|
||||||
|
admin_tag = create(:tag, name: 'Admin tag', kind: 'admin')
|
||||||
|
|
||||||
|
proposal = create(:proposal, tag_list: 'Parks')
|
||||||
|
proposal = create(:proposal, tag_list: 'Health')
|
||||||
|
proposal = create(:proposal, tag_list: 'Admin tag')
|
||||||
|
|
||||||
|
response = execute('{ tags { edges { node { name } } } }')
|
||||||
|
received_tags = extract_fields(response, 'tags', 'name')
|
||||||
|
|
||||||
|
expect(received_tags).to match_array ['Parks', 'Health']
|
||||||
|
end
|
||||||
|
|
||||||
|
xit 'uppercase and lowercase tags work ok together for proposals' do
|
||||||
|
create(:tag, name: 'Health')
|
||||||
|
create(:tag, name: 'health')
|
||||||
|
create(:proposal, tag_list: 'health')
|
||||||
|
create(:proposal, tag_list: 'Health')
|
||||||
|
|
||||||
|
response = execute('{ tags { edges { node { name } } } }')
|
||||||
|
received_tags = extract_fields(response, 'tags', 'name')
|
||||||
|
|
||||||
|
expect(received_tags).to match_array ['Health', 'health']
|
||||||
|
end
|
||||||
|
|
||||||
|
xit 'uppercase and lowercase tags work ok together for debates' do
|
||||||
|
create(:tag, name: 'Health')
|
||||||
|
create(:tag, name: 'health')
|
||||||
|
create(:debate, tag_list: 'Health')
|
||||||
|
create(:debate, tag_list: 'health')
|
||||||
|
|
||||||
|
response = execute('{ tags { edges { node { name } } } }')
|
||||||
|
received_tags = extract_fields(response, 'tags', 'name')
|
||||||
|
|
||||||
|
expect(received_tags).to match_array ['Health', 'health']
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not display tags for hidden proposals' do
|
||||||
|
proposal = create(:proposal, tag_list: 'Health')
|
||||||
|
hidden_proposal = create(:proposal, :hidden, tag_list: 'SPAM')
|
||||||
|
|
||||||
|
response = execute('{ tags { edges { node { name } } } }')
|
||||||
|
received_tags = extract_fields(response, 'tags', 'name')
|
||||||
|
|
||||||
|
expect(received_tags).to match_array ['Health']
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not display tags for hidden debates' do
|
||||||
|
debate = create(:debate, tag_list: 'Health, Transportation')
|
||||||
|
hidden_debate = create(:debate, :hidden, tag_list: 'SPAM')
|
||||||
|
|
||||||
|
response = execute('{ tags { edges { node { name } } } }')
|
||||||
|
received_tags = extract_fields(response, 'tags', 'name')
|
||||||
|
|
||||||
|
expect(received_tags).to match_array ['Health', 'Transportation']
|
||||||
|
end
|
||||||
|
|
||||||
|
xit "does not display tags for proceeding's proposals" do
|
||||||
|
valid_proceeding_proposal = create(:proposal, proceeding: 'Derechos Humanos', sub_proceeding: 'Right to a Home', tag_list: 'Health')
|
||||||
|
invalid_proceeding_proposal = create(:proposal, tag_list: 'Animals')
|
||||||
|
invalid_proceeding_proposal.update_attribute('proceeding', 'Random')
|
||||||
|
|
||||||
|
response = execute('{ tags { edges { node { name } } } }')
|
||||||
|
received_tags = extract_fields(response, 'tags', 'name')
|
||||||
|
|
||||||
|
expect(received_tags).to match_array ['Health']
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not display tags for taggings that are not public' do
|
||||||
|
proposal = create(:proposal, tag_list: 'Health')
|
||||||
|
allow(ActsAsTaggableOn::Tag).to receive(:public_for_api).and_return([])
|
||||||
|
|
||||||
|
response = execute('{ tags { edges { node { name } } } }')
|
||||||
|
received_tags = extract_fields(response, 'tags', 'name')
|
||||||
|
|
||||||
|
expect(received_tags).to_not include('Health')
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'Votes' do
|
||||||
|
|
||||||
|
it 'only returns votes from proposals, debates and comments' do
|
||||||
|
proposal = create(:proposal)
|
||||||
|
debate = create(:debate)
|
||||||
|
comment = create(:comment)
|
||||||
|
spending_proposal = create(:spending_proposal)
|
||||||
|
|
||||||
|
proposal_vote = create(:vote, votable: proposal)
|
||||||
|
debate_vote = create(:vote, votable: debate)
|
||||||
|
comment_vote = create(:vote, votable: comment)
|
||||||
|
spending_proposal_vote = create(:vote, votable: spending_proposal)
|
||||||
|
|
||||||
|
response = execute('{ votes { edges { node { votable_type } } } }')
|
||||||
|
received_votables = extract_fields(response, 'votes', 'votable_type')
|
||||||
|
|
||||||
|
expect(received_votables).to match_array ['Proposal', 'Debate', 'Comment']
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not include votes from hidden debates' do
|
||||||
|
visible_debate = create(:debate)
|
||||||
|
hidden_debate = create(:debate, :hidden)
|
||||||
|
|
||||||
|
visible_debate_vote = create(:vote, votable: visible_debate)
|
||||||
|
hidden_debate_vote = create(:vote, votable: hidden_debate)
|
||||||
|
|
||||||
|
response = execute('{ votes { edges { node { votable_id } } } }')
|
||||||
|
received_debates = extract_fields(response, 'votes', 'votable_id')
|
||||||
|
|
||||||
|
expect(received_debates).to match_array [visible_debate.id]
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not include votes of hidden proposals' do
|
||||||
|
visible_proposal = create(:proposal)
|
||||||
|
hidden_proposal = create(:proposal, hidden_at: Time.now)
|
||||||
|
|
||||||
|
visible_proposal_vote = create(:vote, votable: visible_proposal)
|
||||||
|
hidden_proposal_vote = create(:vote, votable: hidden_proposal)
|
||||||
|
|
||||||
|
response = execute('{ votes { edges { node { votable_id } } } }')
|
||||||
|
received_proposals = extract_fields(response, 'votes', 'votable_id')
|
||||||
|
|
||||||
|
expect(received_proposals).to match_array [visible_proposal.id]
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not include votes of hidden comments' do
|
||||||
|
visible_comment = create(:comment)
|
||||||
|
hidden_comment = create(:comment, hidden_at: Time.now)
|
||||||
|
|
||||||
|
visible_comment_vote = create(:vote, votable: visible_comment)
|
||||||
|
hidden_comment_vote = create(:vote, votable: hidden_comment)
|
||||||
|
|
||||||
|
response = execute('{ votes { edges { node { votable_id } } } }')
|
||||||
|
received_comments = extract_fields(response, 'votes', 'votable_id')
|
||||||
|
|
||||||
|
expect(received_comments).to match_array [visible_comment.id]
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not include votes of comments from a hidden proposal' do
|
||||||
|
visible_proposal = create(:proposal)
|
||||||
|
hidden_proposal = create(:proposal, :hidden)
|
||||||
|
|
||||||
|
visible_proposal_comment = create(:comment, commentable: visible_proposal)
|
||||||
|
hidden_proposal_comment = create(:comment, commentable: hidden_proposal)
|
||||||
|
|
||||||
|
visible_proposal_comment_vote = create(:vote, votable: visible_proposal_comment)
|
||||||
|
hidden_proposal_comment_vote = create(:vote, votable: hidden_proposal_comment)
|
||||||
|
|
||||||
|
response = execute('{ votes { edges { node { votable_id } } } }')
|
||||||
|
received_votables = extract_fields(response, 'votes', 'votable_id')
|
||||||
|
|
||||||
|
expect(received_votables).to match_array [visible_proposal_comment.id]
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not include votes of comments from a hidden debate' do
|
||||||
|
visible_debate = create(:debate)
|
||||||
|
hidden_debate = create(:debate, :hidden)
|
||||||
|
|
||||||
|
visible_debate_comment = create(:comment, commentable: visible_debate)
|
||||||
|
hidden_debate_comment = create(:comment, commentable: hidden_debate)
|
||||||
|
|
||||||
|
visible_debate_comment_vote = create(:vote, votable: visible_debate_comment)
|
||||||
|
hidden_debate_comment_vote = create(:vote, votable: hidden_debate_comment)
|
||||||
|
|
||||||
|
response = execute('{ votes { edges { node { votable_id } } } }')
|
||||||
|
received_votables = extract_fields(response, 'votes', 'votable_id')
|
||||||
|
|
||||||
|
expect(received_votables).to match_array [visible_debate_comment.id]
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not include votes of debates that are not public' do
|
||||||
|
not_public_debate = create(:debate)
|
||||||
|
allow(Vote).to receive(:public_for_api).and_return([])
|
||||||
|
|
||||||
|
not_public_debate_vote = create(:vote, votable: not_public_debate)
|
||||||
|
|
||||||
|
response = execute('{ votes { edges { node { votable_id } } } }')
|
||||||
|
received_votables = extract_fields(response, 'votes', 'votable_id')
|
||||||
|
|
||||||
|
expect(received_votables).to_not include(not_public_debate.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not include votes of a hidden proposals' do
|
||||||
|
not_public_proposal = create(:proposal)
|
||||||
|
allow(Vote).to receive(:public_for_api).and_return([])
|
||||||
|
|
||||||
|
not_public_proposal_vote = create(:vote, votable: not_public_proposal)
|
||||||
|
|
||||||
|
response = execute('{ votes { edges { node { votable_id } } } }')
|
||||||
|
received_votables = extract_fields(response, 'votes', 'votable_id')
|
||||||
|
|
||||||
|
expect(received_votables).to_not include(not_public_proposal.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not include votes of a hidden comments' do
|
||||||
|
not_public_comment = create(:comment)
|
||||||
|
allow(Vote).to receive(:public_for_api).and_return([])
|
||||||
|
|
||||||
|
not_public_comment_vote = create(:vote, votable: not_public_comment)
|
||||||
|
|
||||||
|
response = execute('{ votes { edges { node { votable_id } } } }')
|
||||||
|
received_votables = extract_fields(response, 'votes', 'votable_id')
|
||||||
|
|
||||||
|
expect(received_votables).to_not include(not_public_comment.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'only returns date and hour for created_at' do
|
||||||
|
created_at = Time.new(2017, 12, 31, 9, 30, 15).in_time_zone(Time.zone)
|
||||||
|
create(:vote, created_at: created_at)
|
||||||
|
|
||||||
|
response = execute('{ votes { edges { node { public_created_at } } } }')
|
||||||
|
received_timestamps = extract_fields(response, 'votes', 'public_created_at')
|
||||||
|
|
||||||
|
expect(received_timestamps.first).to include('09:00:00')
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -181,5 +181,11 @@ describe Comment do
|
|||||||
|
|
||||||
expect(Comment.public_for_api).not_to include(comment)
|
expect(Comment.public_for_api).not_to include(comment)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'does not return comments with no commentable' do
|
||||||
|
comment = build(:comment, commentable: nil).save!(validate: false)
|
||||||
|
|
||||||
|
expect(Comment.public_for_api).to_not include(comment)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -30,12 +30,18 @@ describe ProposalNotification do
|
|||||||
expect(ProposalNotification.public_for_api).to include(notification)
|
expect(ProposalNotification.public_for_api).to include(notification)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "blocks notifications whose proposal is hidden" do
|
it "blocks proposal notifications whose proposal is hidden" do
|
||||||
proposal = create(:proposal, :hidden)
|
proposal = create(:proposal, :hidden)
|
||||||
notification = create(:proposal_notification, proposal: proposal)
|
notification = create(:proposal_notification, proposal: proposal)
|
||||||
|
|
||||||
expect(ProposalNotification.public_for_api).not_to include(notification)
|
expect(ProposalNotification.public_for_api).not_to include(notification)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "blocks proposal notifications without proposal" do
|
||||||
|
proposal = build(:proposal_notification, proposal: nil).save!(validate: false)
|
||||||
|
|
||||||
|
expect(ProposalNotification.public_for_api).not_to include(notification)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "minimum interval between notifications" do
|
describe "minimum interval between notifications" do
|
||||||
|
|||||||
@@ -84,18 +84,33 @@ describe 'Vote' do
|
|||||||
expect(Vote.public_for_api).not_to include(vote)
|
expect(Vote.public_for_api).not_to include(vote)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'blocks votes on comments on hidden proposals' do
|
||||||
|
hidden_proposal = create(:proposal, :hidden)
|
||||||
|
comment_on_hidden_proposal = create(:comment, commentable: hidden_proposal)
|
||||||
|
vote = create(:vote, votable: comment_on_hidden_proposal)
|
||||||
|
|
||||||
|
expect(Vote.public_for_api).to_not include(vote)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'blocks votes on comments on hidden debates' do
|
||||||
|
hidden_debate = create(:debate, :hidden)
|
||||||
|
comment_on_hidden_debate = create(:comment, commentable: hidden_debate)
|
||||||
|
vote = create(:vote, votable: comment_on_hidden_debate)
|
||||||
|
|
||||||
|
expect(Vote.public_for_api).to_not include(vote)
|
||||||
|
end
|
||||||
|
|
||||||
it 'blocks any other kind of votes' do
|
it 'blocks any other kind of votes' do
|
||||||
spending_proposal = create(:spending_proposal)
|
spending_proposal = create(:spending_proposal)
|
||||||
vote = create(:vote, votable: spending_proposal)
|
vote = create(:vote, votable: spending_proposal)
|
||||||
|
|
||||||
expect(Vote.public_for_api).not_to include(vote)
|
expect(Vote.public_for_api).not_to include(vote)
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
describe '#public_timestamp' do
|
it 'blocks votes without votable' do
|
||||||
it "truncates created_at timestamp up to minutes" do
|
vote = build(:vote, votable: nil).save!(validate: false)
|
||||||
vote = create(:vote, created_at: Time.zone.parse('2016-02-10 15:30:45'))
|
|
||||||
expect(vote.public_timestamp).to eq(Time.zone.parse('2016-02-10 15:00:00'))
|
expect(Vote.public_for_api).not_to include(vote)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user