3
Gemfile
3
Gemfile
@@ -71,6 +71,8 @@ gem 'rails-assets-markdown-it', source: 'https://rails-assets.org'
|
||||
|
||||
gem 'cocoon'
|
||||
|
||||
gem 'graphql', '~> 1.6.3'
|
||||
|
||||
group :development, :test do
|
||||
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
|
||||
gem 'byebug'
|
||||
@@ -106,6 +108,7 @@ end
|
||||
group :development do
|
||||
# Access an IRB console on exception pages or by using <%= console %> in views
|
||||
gem 'web-console', '3.3.0'
|
||||
gem 'graphiql-rails', '~> 1.4.1'
|
||||
end
|
||||
|
||||
eval_gemfile './Gemfile_custom'
|
||||
|
||||
@@ -176,8 +176,10 @@ GEM
|
||||
geocoder (1.4.3)
|
||||
globalid (0.3.7)
|
||||
activesupport (>= 4.1.0)
|
||||
graphiql-rails (1.4.1)
|
||||
rails
|
||||
graphql (1.6.3)
|
||||
groupdate (3.2.0)
|
||||
activesupport (>= 3)
|
||||
gyoku (1.3.1)
|
||||
builder (>= 2.1.2)
|
||||
hashie (3.5.5)
|
||||
@@ -504,6 +506,8 @@ DEPENDENCIES
|
||||
foundation-rails (~> 6.2.4.0)
|
||||
foundation_rails_helper (~> 2.0.0)
|
||||
fuubar
|
||||
graphiql-rails (~> 1.4.1)
|
||||
graphql (~> 1.6.3)
|
||||
groupdate (~> 3.2.0)
|
||||
i18n-tasks (~> 0.9.15)
|
||||
initialjs-rails (= 0.2.0.4)
|
||||
|
||||
52
app/controllers/graphql_controller.rb
Normal file
52
app/controllers/graphql_controller.rb
Normal file
@@ -0,0 +1,52 @@
|
||||
class GraphqlController < ApplicationController
|
||||
|
||||
skip_before_action :verify_authenticity_token
|
||||
skip_authorization_check
|
||||
|
||||
class QueryStringError < StandardError; end
|
||||
|
||||
def query
|
||||
begin
|
||||
if query_string.nil? then raise GraphqlController::QueryStringError end
|
||||
response = consul_schema.execute query_string, variables: query_variables
|
||||
render json: response, status: :ok
|
||||
rescue GraphqlController::QueryStringError
|
||||
render json: { message: 'Query string not present' }, status: :bad_request
|
||||
rescue JSON::ParserError
|
||||
render json: { message: 'Error parsing JSON' }, status: :bad_request
|
||||
rescue GraphQL::ParseError
|
||||
render json: { message: 'Query string is not valid JSON' }, status: :bad_request
|
||||
rescue
|
||||
unless Rails.env.production? then raise end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def consul_schema
|
||||
api_types = GraphQL::ApiTypesCreator.create(API_TYPE_DEFINITIONS)
|
||||
query_type = GraphQL::QueryTypeCreator.create(api_types)
|
||||
|
||||
GraphQL::Schema.define do
|
||||
query query_type
|
||||
max_depth 8
|
||||
max_complexity 2500
|
||||
end
|
||||
end
|
||||
|
||||
def query_string
|
||||
if request.headers["CONTENT_TYPE"] == 'application/graphql'
|
||||
request.body.string # request.body.class => StringIO
|
||||
else
|
||||
params[:query]
|
||||
end
|
||||
end
|
||||
|
||||
def query_variables
|
||||
if params[:variables].blank? || params[:variables] == 'null'
|
||||
{}
|
||||
else
|
||||
JSON.parse(params[:variables])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,5 +1,7 @@
|
||||
class Comment < ActiveRecord::Base
|
||||
include Flaggable
|
||||
include HasPublicAuthor
|
||||
include Graphqlable
|
||||
|
||||
acts_as_paranoid column: :hidden_at
|
||||
include ActsAsParanoidAliases
|
||||
@@ -24,6 +26,12 @@ class Comment < ActiveRecord::Base
|
||||
scope :with_visible_author, -> { joins(:user).where("users.hidden_at IS NULL") }
|
||||
scope :not_as_admin_or_moderator, -> { where("administrator_id IS NULL").where("moderator_id IS NULL")}
|
||||
scope :sort_by_flags, -> { order(flags_count: :desc, updated_at: :desc) }
|
||||
scope :public_for_api, -> do
|
||||
where(%{(comments.commentable_type = 'Debate' and comments.commentable_id in (?)) or
|
||||
(comments.commentable_type = 'Proposal' and comments.commentable_id in (?))},
|
||||
Debate.public_for_api.pluck(:id),
|
||||
Proposal.public_for_api.pluck(:id))
|
||||
end
|
||||
|
||||
scope :sort_by_most_voted, -> { order(confidence_score: :desc, created_at: :desc) }
|
||||
scope :sort_descendants_by_most_voted, -> { order(confidence_score: :desc, created_at: :asc) }
|
||||
|
||||
36
app/models/concerns/graphqlable.rb
Normal file
36
app/models/concerns/graphqlable.rb
Normal file
@@ -0,0 +1,36 @@
|
||||
module Graphqlable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
class_methods do
|
||||
|
||||
def graphql_field_name
|
||||
self.name.gsub('::', '_').underscore.to_sym
|
||||
end
|
||||
|
||||
def graphql_field_description
|
||||
"Find one #{self.model_name.human} by ID"
|
||||
end
|
||||
|
||||
def graphql_pluralized_field_name
|
||||
self.name.gsub('::', '_').underscore.pluralize.to_sym
|
||||
end
|
||||
|
||||
def graphql_pluralized_field_description
|
||||
"Find all #{self.model_name.human.pluralize}"
|
||||
end
|
||||
|
||||
def graphql_type_name
|
||||
self.name.gsub('::', '_')
|
||||
end
|
||||
|
||||
def graphql_type_description
|
||||
"#{self.model_name.human}"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def public_created_at
|
||||
self.created_at.change(min: 0)
|
||||
end
|
||||
|
||||
end
|
||||
5
app/models/concerns/has_public_author.rb
Normal file
5
app/models/concerns/has_public_author.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
module HasPublicAuthor
|
||||
def public_author
|
||||
self.author.public_activity? ? self.author : nil
|
||||
end
|
||||
end
|
||||
@@ -4,11 +4,11 @@ module Measurable
|
||||
class_methods do
|
||||
|
||||
def title_max_length
|
||||
@@title_max_length ||= self.columns.find { |c| c.name == 'title' }.limit || 80
|
||||
@@title_max_length ||= (self.columns.find { |c| c.name == 'title' }.limit rescue nil) || 80
|
||||
end
|
||||
|
||||
def responsible_name_max_length
|
||||
@@responsible_name_max_length ||= self.columns.find { |c| c.name == 'responsible_name' }.limit || 60
|
||||
@@responsible_name_max_length ||= (self.columns.find { |c| c.name == 'responsible_name' }.limit rescue nil) || 60
|
||||
end
|
||||
|
||||
def question_max_length
|
||||
|
||||
@@ -7,6 +7,8 @@ class Debate < ActiveRecord::Base
|
||||
include Sanitizable
|
||||
include Searchable
|
||||
include Filterable
|
||||
include HasPublicAuthor
|
||||
include Graphqlable
|
||||
|
||||
acts_as_votable
|
||||
acts_as_paranoid column: :hidden_at
|
||||
@@ -37,6 +39,7 @@ class Debate < ActiveRecord::Base
|
||||
scope :sort_by_flags, -> { order(flags_count: :desc, updated_at: :desc) }
|
||||
scope :last_week, -> { where("created_at >= ?", 7.days.ago)}
|
||||
scope :featured, -> { where("featured_at is not null")}
|
||||
scope :public_for_api, -> { all }
|
||||
# Ahoy setup
|
||||
visitable # Ahoy will automatically assign visit_id on create
|
||||
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
class Geozone < ActiveRecord::Base
|
||||
|
||||
include Graphqlable
|
||||
|
||||
has_many :proposals
|
||||
has_many :spending_proposals
|
||||
has_many :debates
|
||||
has_many :users
|
||||
validates :name, presence: true
|
||||
|
||||
scope :public_for_api, -> { all }
|
||||
|
||||
def self.names
|
||||
Geozone.pluck(:name)
|
||||
end
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
class Organization < ActiveRecord::Base
|
||||
|
||||
include Graphqlable
|
||||
|
||||
belongs_to :user, touch: true
|
||||
|
||||
validates :name, presence: true
|
||||
|
||||
@@ -6,6 +6,8 @@ class Proposal < ActiveRecord::Base
|
||||
include Sanitizable
|
||||
include Searchable
|
||||
include Filterable
|
||||
include HasPublicAuthor
|
||||
include Graphqlable
|
||||
|
||||
acts_as_votable
|
||||
acts_as_paranoid column: :hidden_at
|
||||
@@ -51,6 +53,7 @@ class Proposal < ActiveRecord::Base
|
||||
scope :retired, -> { where.not(retired_at: nil) }
|
||||
scope :not_retired, -> { where(retired_at: nil) }
|
||||
scope :successful, -> { where("cached_votes_up >= ?", Proposal.votes_needed_for_success) }
|
||||
scope :public_for_api, -> { all }
|
||||
|
||||
def to_param
|
||||
"#{id}-#{title}".parameterize
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
class ProposalNotification < ActiveRecord::Base
|
||||
|
||||
include Graphqlable
|
||||
|
||||
belongs_to :author, class_name: 'User', foreign_key: 'author_id'
|
||||
belongs_to :proposal
|
||||
|
||||
@@ -7,6 +10,8 @@ class ProposalNotification < ActiveRecord::Base
|
||||
validates :proposal, presence: true
|
||||
validate :minimum_interval
|
||||
|
||||
scope :public_for_api, -> { where(proposal_id: Proposal.public_for_api.pluck(:id)) }
|
||||
|
||||
def minimum_interval
|
||||
return true if proposal.try(:notifications).blank?
|
||||
if proposal.notifications.last.created_at > (Time.current - Setting[:proposal_notification_minimum_interval_in_days].to_i.days).to_datetime
|
||||
|
||||
@@ -10,6 +10,8 @@ class User < ActiveRecord::Base
|
||||
acts_as_paranoid column: :hidden_at
|
||||
include ActsAsParanoidAliases
|
||||
|
||||
include Graphqlable
|
||||
|
||||
has_one :administrator
|
||||
has_one :moderator
|
||||
has_one :valuator
|
||||
@@ -61,6 +63,7 @@ class User < ActiveRecord::Base
|
||||
scope :email_digest, -> { where(email_digest: true) }
|
||||
scope :active, -> { where(erased_at: nil) }
|
||||
scope :erased, -> { where.not(erased_at: nil) }
|
||||
scope :public_for_api, -> { all }
|
||||
|
||||
before_validation :clean_document_number
|
||||
|
||||
@@ -288,6 +291,18 @@ class User < ActiveRecord::Base
|
||||
end
|
||||
delegate :can?, :cannot?, to: :ability
|
||||
|
||||
def public_proposals
|
||||
public_activity? ? proposals : User.none
|
||||
end
|
||||
|
||||
def public_debates
|
||||
public_activity? ? debates : User.none
|
||||
end
|
||||
|
||||
def public_comments
|
||||
public_activity? ? comments : User.none
|
||||
end
|
||||
|
||||
# overwritting of Devise method to allow login using email OR username
|
||||
def self.find_for_database_authentication(warden_conditions)
|
||||
conditions = warden_conditions.dup
|
||||
|
||||
@@ -1,2 +1,14 @@
|
||||
class Vote < ActsAsVotable::Vote
|
||||
|
||||
include Graphqlable
|
||||
|
||||
scope :public_for_api, -> do
|
||||
where(%{(votes.votable_type = 'Debate' and votes.votable_id in (?)) or
|
||||
(votes.votable_type = 'Proposal' and votes.votable_id in (?)) or
|
||||
(votes.votable_type = 'Comment' and votes.votable_id in (?))},
|
||||
Debate.public_for_api.pluck(:id),
|
||||
Proposal.public_for_api.pluck(:id),
|
||||
Comment.public_for_api.pluck(:id))
|
||||
end
|
||||
|
||||
end
|
||||
89
config/api.yml
Normal file
89
config/api.yml
Normal file
@@ -0,0 +1,89 @@
|
||||
User:
|
||||
fields:
|
||||
id: integer
|
||||
username: string
|
||||
public_debates: [Debate]
|
||||
public_proposals: [Proposal]
|
||||
public_comments: [Comment]
|
||||
# organization: Organization
|
||||
Debate:
|
||||
fields:
|
||||
id: integer
|
||||
title: string
|
||||
description: string
|
||||
public_created_at: string
|
||||
cached_votes_total: integer
|
||||
cached_votes_up: integer
|
||||
cached_votes_down: integer
|
||||
comments_count: integer
|
||||
hot_score: integer
|
||||
confidence_score: integer
|
||||
comments: [Comment]
|
||||
public_author: User
|
||||
votes_for: [Vote]
|
||||
tags: ["ActsAsTaggableOn::Tag"]
|
||||
Proposal:
|
||||
fields:
|
||||
id: integer
|
||||
title: string
|
||||
description: string
|
||||
external_url: string
|
||||
cached_votes_up: integer
|
||||
comments_count: integer
|
||||
hot_score: integer
|
||||
confidence_score: integer
|
||||
public_created_at: string
|
||||
summary: string
|
||||
video_url: string
|
||||
geozone_id: integer
|
||||
retired_at: string
|
||||
retired_reason: string
|
||||
retired_explanation: string
|
||||
geozone: Geozone
|
||||
comments: [Comment]
|
||||
proposal_notifications: [ProposalNotification]
|
||||
public_author: User
|
||||
votes_for: [Vote]
|
||||
tags: ["ActsAsTaggableOn::Tag"]
|
||||
Comment:
|
||||
fields:
|
||||
id: integer
|
||||
commentable_id: integer
|
||||
commentable_type: string
|
||||
body: string
|
||||
public_created_at: string
|
||||
cached_votes_total: integer
|
||||
cached_votes_up: integer
|
||||
cached_votes_down: integer
|
||||
ancestry: string
|
||||
confidence_score: integer
|
||||
public_author: User
|
||||
votes_for: [Vote]
|
||||
Geozone:
|
||||
fields:
|
||||
id: integer
|
||||
name: string
|
||||
ProposalNotification:
|
||||
fields:
|
||||
title: string
|
||||
body: string
|
||||
proposal_id: integer
|
||||
public_created_at: string
|
||||
proposal: Proposal
|
||||
ActsAsTaggableOn::Tag:
|
||||
fields:
|
||||
id: integer
|
||||
name: string
|
||||
taggings_count: integer
|
||||
kind: string
|
||||
Vote:
|
||||
fields:
|
||||
votable_id: integer
|
||||
votable_type: string
|
||||
public_created_at: string
|
||||
vote_flag: boolean
|
||||
# Organization:
|
||||
# fields:
|
||||
# id: integer
|
||||
# user_id: integer
|
||||
# name: string
|
||||
@@ -5,6 +5,15 @@ module ActsAsTaggableOn
|
||||
after_create :increment_tag_custom_counter
|
||||
after_destroy :touch_taggable, :decrement_tag_custom_counter
|
||||
|
||||
scope :public_for_api, -> do
|
||||
where(%{taggings.tag_id in (?) and
|
||||
(taggings.taggable_type = 'Debate' and taggings.taggable_id in (?)) or
|
||||
(taggings.taggable_type = 'Proposal' and taggings.taggable_id in (?))},
|
||||
Tag.where('kind IS NULL or kind = ?', 'category').pluck(:id),
|
||||
Debate.public_for_api.pluck(:id),
|
||||
Proposal.public_for_api.pluck(:id))
|
||||
end
|
||||
|
||||
def touch_taggable
|
||||
taggable.touch if taggable.present?
|
||||
end
|
||||
@@ -20,6 +29,14 @@ module ActsAsTaggableOn
|
||||
|
||||
Tag.class_eval do
|
||||
|
||||
include Graphqlable
|
||||
|
||||
scope :public_for_api, -> do
|
||||
where('(tags.kind IS NULL or tags.kind = ?) and tags.id in (?)',
|
||||
'category',
|
||||
Tagging.public_for_api.pluck('DISTINCT taggings.tag_id'))
|
||||
end
|
||||
|
||||
def increment_custom_counter_for(taggable_type)
|
||||
Tag.increment_counter(custom_counter_field_name_for(taggable_type), id)
|
||||
end
|
||||
@@ -42,6 +59,18 @@ module ActsAsTaggableOn
|
||||
ActsAsTaggableOn::Tag.where('taggings.taggable_type' => 'SpendingProposal').includes(:taggings).order(:name).uniq
|
||||
end
|
||||
|
||||
def self.graphql_field_name
|
||||
:tag
|
||||
end
|
||||
|
||||
def self.graphql_pluralized_field_name
|
||||
:tags
|
||||
end
|
||||
|
||||
def self.graphql_type_name
|
||||
'Tag'
|
||||
end
|
||||
|
||||
private
|
||||
def custom_counter_field_name_for(taggable_type)
|
||||
"#{taggable_type.underscore.pluralize}_count"
|
||||
|
||||
4
config/initializers/graphql.rb
Normal file
4
config/initializers/graphql.rb
Normal file
@@ -0,0 +1,4 @@
|
||||
if ActiveRecord::Base.connection.tables.any?
|
||||
api_config = YAML.load_file('./config/api.yml')
|
||||
API_TYPE_DEFINITIONS = GraphQL::ApiTypesCreator::parse_api_config_file(api_config)
|
||||
end
|
||||
@@ -400,8 +400,13 @@ Rails.application.routes.draw do
|
||||
root to: "dashboard#index"
|
||||
end
|
||||
|
||||
# GraphQL
|
||||
get '/graphql', to: 'graphql#query'
|
||||
post '/graphql', to: 'graphql#query'
|
||||
|
||||
if Rails.env.development?
|
||||
mount LetterOpenerWeb::Engine, at: "/letter_opener"
|
||||
mount GraphiQL::Rails::Engine, at: '/graphiql', graphql_path: '/graphql'
|
||||
end
|
||||
|
||||
mount Tolk::Engine => '/translate', :as => 'tolk'
|
||||
|
||||
@@ -59,7 +59,17 @@ print "Creating Users"
|
||||
|
||||
def create_user(email, username = Faker::Name.name)
|
||||
pwd = '12345678'
|
||||
User.create!(username: username, email: email, password: pwd, password_confirmation: pwd, confirmed_at: Time.current, terms_of_service: "1")
|
||||
User.create!(
|
||||
username: username,
|
||||
email: email,
|
||||
password: pwd,
|
||||
password_confirmation: pwd,
|
||||
confirmed_at: Time.current,
|
||||
terms_of_service: "1",
|
||||
gender: ['Male', 'Female'].sample,
|
||||
date_of_birth: rand((Time.current - 80.years) .. (Time.current - 16.years)),
|
||||
public_activity: (rand(1..100) > 30)
|
||||
)
|
||||
end
|
||||
|
||||
admin = create_user('admin@consul.dev', 'admin')
|
||||
@@ -103,11 +113,11 @@ end
|
||||
official.update(official_level: i, official_position: "Official position #{i}")
|
||||
end
|
||||
|
||||
(1..40).each do |i|
|
||||
(1..100).each do |i|
|
||||
user = create_user("user#{i}@consul.dev")
|
||||
level = [1, 2, 3].sample
|
||||
if level >= 2
|
||||
user.update(residence_verified_at: Time.current, confirmed_phone: Faker::PhoneNumber.phone_number, document_number: Faker::Number.number(10), document_type: "1")
|
||||
user.update(residence_verified_at: Time.current, confirmed_phone: Faker::PhoneNumber.phone_number, document_number: Faker::Number.number(10), document_type: "1", geozone: Geozone.reorder("RANDOM()").first)
|
||||
end
|
||||
if level == 3
|
||||
user.update(verified_at: Time.current, document_number: Faker::Number.number(10))
|
||||
@@ -285,7 +295,7 @@ puts " ✅"
|
||||
print "Voting Debates, Proposals & Comments"
|
||||
|
||||
100.times do
|
||||
voter = not_org_users.reorder("RANDOM()").first
|
||||
voter = not_org_users.level_two_or_three_verified.reorder("RANDOM()").first
|
||||
vote = [true, false].sample
|
||||
debate = Debate.reorder("RANDOM()").first
|
||||
debate.vote_by(voter: voter, vote: vote)
|
||||
@@ -299,7 +309,7 @@ end
|
||||
end
|
||||
|
||||
100.times do
|
||||
voter = User.level_two_or_three_verified.reorder("RANDOM()").first
|
||||
voter = not_org_users.level_two_or_three_verified.reorder("RANDOM()").first
|
||||
proposal = Proposal.reorder("RANDOM()").first
|
||||
proposal.vote_by(voter: voter, vote: true)
|
||||
end
|
||||
@@ -487,6 +497,16 @@ Proposal.last(3).each do |proposal|
|
||||
created_at: rand((Time.current - 1.week)..Time.current))
|
||||
end
|
||||
|
||||
puts " ✅"
|
||||
puts "Creating proposal notifications"
|
||||
|
||||
100.times do |i|
|
||||
ProposalNotification.create!(title: "Proposal notification title #{i}",
|
||||
body: "Proposal notification body #{i}",
|
||||
author: User.reorder("RANDOM()").first,
|
||||
proposal: Proposal.reorder("RANDOM()").first)
|
||||
end
|
||||
|
||||
puts " ✅"
|
||||
print "Creating polls"
|
||||
|
||||
|
||||
@@ -232,10 +232,10 @@ ActiveRecord::Schema.define(version: 20170613203256) do
|
||||
t.string "visit_id"
|
||||
t.datetime "hidden_at"
|
||||
t.integer "flags_count", default: 0
|
||||
t.datetime "ignored_flag_at"
|
||||
t.integer "cached_votes_total", default: 0
|
||||
t.integer "cached_votes_up", default: 0
|
||||
t.integer "cached_votes_down", default: 0
|
||||
t.datetime "ignored_flag_at"
|
||||
t.integer "comments_count", default: 0
|
||||
t.datetime "confirmed_hide_at"
|
||||
t.integer "cached_anonymous_votes_total", default: 0
|
||||
@@ -254,7 +254,6 @@ ActiveRecord::Schema.define(version: 20170613203256) do
|
||||
add_index "debates", ["cached_votes_total"], name: "index_debates_on_cached_votes_total", using: :btree
|
||||
add_index "debates", ["cached_votes_up"], name: "index_debates_on_cached_votes_up", using: :btree
|
||||
add_index "debates", ["confidence_score"], name: "index_debates_on_confidence_score", using: :btree
|
||||
add_index "debates", ["description"], name: "index_debates_on_description", using: :btree
|
||||
add_index "debates", ["geozone_id"], name: "index_debates_on_geozone_id", using: :btree
|
||||
add_index "debates", ["hidden_at"], name: "index_debates_on_hidden_at", using: :btree
|
||||
add_index "debates", ["hot_score"], name: "index_debates_on_hot_score", using: :btree
|
||||
@@ -718,7 +717,6 @@ ActiveRecord::Schema.define(version: 20170613203256) do
|
||||
add_index "proposals", ["author_id"], name: "index_proposals_on_author_id", using: :btree
|
||||
add_index "proposals", ["cached_votes_up"], name: "index_proposals_on_cached_votes_up", using: :btree
|
||||
add_index "proposals", ["confidence_score"], name: "index_proposals_on_confidence_score", using: :btree
|
||||
add_index "proposals", ["description"], name: "index_proposals_on_description", using: :btree
|
||||
add_index "proposals", ["geozone_id"], name: "index_proposals_on_geozone_id", using: :btree
|
||||
add_index "proposals", ["hidden_at"], name: "index_proposals_on_hidden_at", using: :btree
|
||||
add_index "proposals", ["hot_score"], name: "index_proposals_on_hot_score", using: :btree
|
||||
@@ -926,7 +924,7 @@ ActiveRecord::Schema.define(version: 20170613203256) do
|
||||
t.boolean "email_digest", default: true
|
||||
t.boolean "email_on_direct_message", default: true
|
||||
t.boolean "official_position_badge", default: false
|
||||
t.datetime "password_changed_at", default: '2016-11-23 10:59:20', null: false
|
||||
t.datetime "password_changed_at", default: '2016-12-21 17:55:08', null: false
|
||||
t.boolean "created_from_signature", default: false
|
||||
t.integer "failed_email_digests_count", default: 0
|
||||
t.text "former_users_data_log", default: ""
|
||||
|
||||
84
lib/graph_ql/api_types_creator.rb
Normal file
84
lib/graph_ql/api_types_creator.rb
Normal file
@@ -0,0 +1,84 @@
|
||||
require 'graphql'
|
||||
|
||||
module GraphQL
|
||||
class ApiTypesCreator
|
||||
SCALAR_TYPES = {
|
||||
integer: GraphQL::INT_TYPE,
|
||||
boolean: GraphQL::BOOLEAN_TYPE,
|
||||
float: GraphQL::FLOAT_TYPE,
|
||||
double: GraphQL::FLOAT_TYPE,
|
||||
string: GraphQL::STRING_TYPE
|
||||
}
|
||||
|
||||
def self.create(api_types_definitions)
|
||||
created_types = {}
|
||||
api_types_definitions.each do |model, info|
|
||||
create_type(model, info[:fields], created_types)
|
||||
end
|
||||
created_types
|
||||
end
|
||||
|
||||
def self.type_kind(type)
|
||||
if SCALAR_TYPES[type]
|
||||
:scalar
|
||||
elsif type.class == Class
|
||||
:singular_association
|
||||
elsif type.class == Array
|
||||
:multiple_association
|
||||
end
|
||||
end
|
||||
|
||||
def self.create_type(model, fields, created_types)
|
||||
|
||||
created_types[model] = GraphQL::ObjectType.define do
|
||||
|
||||
name model.graphql_type_name
|
||||
description model.graphql_type_description
|
||||
|
||||
# Make a field for each column, association or method
|
||||
fields.each do |field_name, field_type|
|
||||
case ApiTypesCreator.type_kind(field_type)
|
||||
when :scalar
|
||||
field(field_name, SCALAR_TYPES[field_type], model.human_attribute_name(field_name))
|
||||
when :singular_association
|
||||
field(field_name, -> { created_types[field_type] }) do
|
||||
resolve -> (object, arguments, context) do
|
||||
association_target = object.send(field_name)
|
||||
association_target.present? ? field_type.public_for_api.find_by(id: association_target.id) : nil
|
||||
end
|
||||
end
|
||||
when :multiple_association
|
||||
field_type = field_type.first
|
||||
connection(field_name, -> { created_types[field_type].connection_type }, max_page_size: 50, complexity: 1000) do
|
||||
resolve -> (object, arguments, context) { object.send(field_name).public_for_api }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
def self.parse_api_config_file(file)
|
||||
api_type_definitions = {}
|
||||
|
||||
file.each do |api_type_model, api_type_info|
|
||||
model = api_type_model.constantize
|
||||
fields = {}
|
||||
|
||||
api_type_info['fields'].each do |field_name, field_type|
|
||||
if field_type.is_a?(Array) # paginated association
|
||||
fields[field_name.to_sym] = [field_type.first.constantize]
|
||||
elsif SCALAR_TYPES[field_type.to_sym]
|
||||
fields[field_name.to_sym] = field_type.to_sym
|
||||
else # simple association
|
||||
fields[field_name.to_sym] = field_type.constantize
|
||||
end
|
||||
end
|
||||
|
||||
api_type_definitions[model] = { fields: fields }
|
||||
end
|
||||
|
||||
api_type_definitions
|
||||
end
|
||||
end
|
||||
end
|
||||
31
lib/graph_ql/query_type_creator.rb
Normal file
31
lib/graph_ql/query_type_creator.rb
Normal file
@@ -0,0 +1,31 @@
|
||||
require 'graphql'
|
||||
|
||||
module GraphQL
|
||||
class QueryTypeCreator
|
||||
|
||||
def self.create(api_types)
|
||||
GraphQL::ObjectType.define do
|
||||
name 'QueryType'
|
||||
description 'The root query for the schema'
|
||||
|
||||
api_types.each do |model, created_type|
|
||||
if created_type.fields['id']
|
||||
field model.graphql_field_name do
|
||||
type created_type
|
||||
description model.graphql_field_description
|
||||
argument :id, !types.ID
|
||||
resolve -> (object, arguments, context) { model.public_for_api.find_by(id: arguments['id'])}
|
||||
end
|
||||
end
|
||||
|
||||
connection(model.graphql_pluralized_field_name, created_type.connection_type, max_page_size: 50, complexity: 1000) do
|
||||
description model.graphql_pluralized_field_description
|
||||
resolve -> (object, arguments, context) { model.public_for_api }
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
91
spec/controllers/graphql_controller_spec.rb
Normal file
91
spec/controllers/graphql_controller_spec.rb
Normal file
@@ -0,0 +1,91 @@
|
||||
require 'rails_helper'
|
||||
|
||||
# Useful resource: http://graphql.org/learn/serving-over-http/
|
||||
|
||||
def parser_error_raised?(response)
|
||||
data_is_empty = response['data'].nil?
|
||||
error_is_present = (JSON.parse(response.body)['errors'].first['message'] =~ /^Parse error on/)
|
||||
data_is_empty && error_is_present
|
||||
end
|
||||
|
||||
describe GraphqlController, type: :request do
|
||||
let(:proposal) { create(:proposal) }
|
||||
|
||||
describe "handles GET request" do
|
||||
specify "with query string inside query params" do
|
||||
get '/graphql', query: "{ proposal(id: #{proposal.id}) { title } }"
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(JSON.parse(response.body)['data']['proposal']['title']).to eq(proposal.title)
|
||||
end
|
||||
|
||||
specify "with malformed query string" do
|
||||
get '/graphql', query: 'Malformed query string'
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(parser_error_raised?(response)).to be_truthy
|
||||
end
|
||||
|
||||
specify "without query string" do
|
||||
get '/graphql'
|
||||
|
||||
expect(response).to have_http_status(:bad_request)
|
||||
expect(JSON.parse(response.body)['message']).to eq('Query string not present')
|
||||
end
|
||||
end
|
||||
|
||||
describe "handles POST request" do
|
||||
let(:json_headers) { { "CONTENT_TYPE" => "application/json" } }
|
||||
|
||||
specify "with json-encoded query string inside body" do
|
||||
post '/graphql', { query: "{ proposal(id: #{proposal.id}) { title } }" }.to_json, json_headers
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(JSON.parse(response.body)['data']['proposal']['title']).to eq(proposal.title)
|
||||
end
|
||||
|
||||
specify "with raw query string inside body" do
|
||||
graphql_headers = { "CONTENT_TYPE" => "application/graphql" }
|
||||
post '/graphql', "{ proposal(id: #{proposal.id}) { title } }", graphql_headers
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(JSON.parse(response.body)['data']['proposal']['title']).to eq(proposal.title)
|
||||
end
|
||||
|
||||
specify "with malformed query string" do
|
||||
post '/graphql', { query: "Malformed query string" }.to_json, json_headers
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(parser_error_raised?(response)).to be_truthy
|
||||
end
|
||||
|
||||
it "without query string" do
|
||||
post '/graphql', json_headers
|
||||
|
||||
expect(response).to have_http_status(:bad_request)
|
||||
expect(JSON.parse(response.body)['message']).to eq('Query string not present')
|
||||
end
|
||||
end
|
||||
|
||||
describe "correctly parses query variables" do
|
||||
let(:query_string) { "{ proposal(id: #{proposal.id}) { title } }" }
|
||||
|
||||
specify "when absent" do
|
||||
get '/graphql', { query: query_string }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
|
||||
specify "when specified as the 'null' string" do
|
||||
get '/graphql', { query: query_string, variables: 'null' }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
|
||||
specify "when specified as an empty string" do
|
||||
get '/graphql', { query: query_string, variables: '' }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -6,8 +6,9 @@ FactoryGirl.define do
|
||||
sequence(:email) { |n| "manuela#{n}@consul.dev" }
|
||||
|
||||
password 'judgmentday'
|
||||
terms_of_service '1'
|
||||
terms_of_service '1'
|
||||
confirmed_at { Time.current }
|
||||
public_activity true
|
||||
|
||||
trait :incomplete_verification do
|
||||
after :create do |user|
|
||||
|
||||
@@ -64,6 +64,75 @@ describe 'ActsAsTaggableOn' do
|
||||
expect(tag.proposals_count).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
describe "public_for_api scope" do
|
||||
|
||||
it "returns tags whose kind is NULL and have at least one tagging whose taggable is not hidden" do
|
||||
tag = create(:tag, kind: nil)
|
||||
proposal = create(:proposal)
|
||||
proposal.tag_list.add(tag)
|
||||
proposal.save
|
||||
|
||||
expect(ActsAsTaggableOn::Tag.public_for_api).to include(tag)
|
||||
end
|
||||
|
||||
it "returns tags whose kind is 'category' and have at least one tagging whose taggable is not hidden" do
|
||||
tag = create(:tag, kind: 'category')
|
||||
proposal = create(:proposal)
|
||||
proposal.tag_list.add(tag)
|
||||
proposal.save
|
||||
|
||||
expect(ActsAsTaggableOn::Tag.public_for_api).to include(tag)
|
||||
end
|
||||
|
||||
it "blocks other kinds of tags" do
|
||||
tag = create(:tag, kind: 'foo')
|
||||
proposal = create(:proposal)
|
||||
proposal.tag_list.add(tag)
|
||||
proposal.save
|
||||
|
||||
expect(ActsAsTaggableOn::Tag.public_for_api).not_to include(tag)
|
||||
end
|
||||
|
||||
it "blocks tags that don't have at least one tagged element" do
|
||||
tag = create(:tag)
|
||||
|
||||
expect(ActsAsTaggableOn::Tag.public_for_api).to_not include(tag)
|
||||
end
|
||||
|
||||
it 'only permits tags on proposals or debates' do
|
||||
tag_1 = create(:tag)
|
||||
tag_2 = create(:tag)
|
||||
tag_3 = create(:tag)
|
||||
|
||||
proposal = create(:proposal)
|
||||
spending_proposal = create(:spending_proposal)
|
||||
debate = create(:debate)
|
||||
|
||||
proposal.tag_list.add(tag_1)
|
||||
spending_proposal.tag_list.add(tag_2)
|
||||
debate.tag_list.add(tag_3)
|
||||
|
||||
proposal.save
|
||||
spending_proposal.save
|
||||
debate.save
|
||||
|
||||
expect(ActsAsTaggableOn::Tag.public_for_api).to match_array([tag_1, tag_3])
|
||||
end
|
||||
|
||||
it 'blocks tags after its taggings became hidden' do
|
||||
tag = create(:tag)
|
||||
proposal = create(:proposal)
|
||||
proposal.tag_list.add(tag)
|
||||
proposal.save
|
||||
|
||||
expect(ActsAsTaggableOn::Tag.public_for_api).to include(tag)
|
||||
|
||||
proposal.delete
|
||||
|
||||
expect(ActsAsTaggableOn::Tag.public_for_api).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
58
spec/lib/graph_ql/api_types_creator_spec.rb
Normal file
58
spec/lib/graph_ql/api_types_creator_spec.rb
Normal file
@@ -0,0 +1,58 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe GraphQL::ApiTypesCreator do
|
||||
let(:created_types) { {} }
|
||||
|
||||
describe "::create_type" do
|
||||
it "creates fields for Int attributes" do
|
||||
debate_type = GraphQL::ApiTypesCreator.create_type(Debate, { id: :integer }, created_types)
|
||||
created_field = debate_type.fields['id']
|
||||
|
||||
expect(created_field).to be_a(GraphQL::Field)
|
||||
expect(created_field.type).to be_a(GraphQL::ScalarType)
|
||||
expect(created_field.type.name).to eq('Int')
|
||||
end
|
||||
|
||||
it "creates fields for String attributes" do
|
||||
debate_type = GraphQL::ApiTypesCreator.create_type(Debate, { title: :string }, created_types)
|
||||
created_field = debate_type.fields['title']
|
||||
|
||||
expect(created_field).to be_a(GraphQL::Field)
|
||||
expect(created_field.type).to be_a(GraphQL::ScalarType)
|
||||
expect(created_field.type.name).to eq('String')
|
||||
end
|
||||
|
||||
it "creates connections for :belongs_to associations" do
|
||||
user_type = GraphQL::ApiTypesCreator.create_type(User, { id: :integer }, created_types)
|
||||
debate_type = GraphQL::ApiTypesCreator.create_type(Debate, { author: User }, created_types)
|
||||
|
||||
connection = debate_type.fields['author']
|
||||
|
||||
expect(connection).to be_a(GraphQL::Field)
|
||||
expect(connection.type).to eq(user_type)
|
||||
expect(connection.name).to eq('author')
|
||||
end
|
||||
|
||||
it "creates connections for :has_one associations" do
|
||||
user_type = GraphQL::ApiTypesCreator.create_type(User, { organization: Organization }, created_types)
|
||||
organization_type = GraphQL::ApiTypesCreator.create_type(Organization, { id: :integer }, created_types)
|
||||
|
||||
connection = user_type.fields['organization']
|
||||
|
||||
expect(connection).to be_a(GraphQL::Field)
|
||||
expect(connection.type).to eq(organization_type)
|
||||
expect(connection.name).to eq('organization')
|
||||
end
|
||||
|
||||
it "creates connections for :has_many associations" do
|
||||
comment_type = GraphQL::ApiTypesCreator.create_type(Comment, { id: :integer }, created_types)
|
||||
debate_type = GraphQL::ApiTypesCreator.create_type(Debate, { comments: [Comment] }, created_types)
|
||||
|
||||
connection = debate_type.fields['comments']
|
||||
|
||||
expect(connection).to be_a(GraphQL::Field)
|
||||
expect(connection.type).to eq(comment_type.connection_type)
|
||||
expect(connection.name).to eq('comments')
|
||||
end
|
||||
end
|
||||
end
|
||||
35
spec/lib/graph_ql/query_type_creator_spec.rb
Normal file
35
spec/lib/graph_ql/query_type_creator_spec.rb
Normal file
@@ -0,0 +1,35 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe GraphQL::QueryTypeCreator do
|
||||
let(:api_type_definitions) do
|
||||
{
|
||||
ProposalNotification => { fields: { title: :string } },
|
||||
Proposal => { fields: { id: :integer, title: :string } }
|
||||
}
|
||||
end
|
||||
let(:api_types) { GraphQL::ApiTypesCreator.create(api_type_definitions) }
|
||||
|
||||
describe "::create" do
|
||||
let(:query_type) { GraphQL::QueryTypeCreator.create(api_types) }
|
||||
|
||||
it 'creates a QueryType with fields to retrieve single objects whose model fields included an ID' do
|
||||
field = query_type.fields['proposal']
|
||||
|
||||
expect(field).to be_a(GraphQL::Field)
|
||||
expect(field.type).to eq(api_types[Proposal])
|
||||
expect(field.name).to eq('proposal')
|
||||
end
|
||||
|
||||
it 'creates a QueryType without fields to retrieve single objects whose model fields did not include an ID' do
|
||||
expect(query_type.fields['proposal_notification']).to be_nil
|
||||
end
|
||||
|
||||
it "creates a QueryType with connections to retrieve collections of objects" do
|
||||
connection = query_type.fields['proposals']
|
||||
|
||||
expect(connection).to be_a(GraphQL::Field)
|
||||
expect(connection.type).to eq(api_types[Proposal].connection_type)
|
||||
expect(connection.name).to eq('proposals')
|
||||
end
|
||||
end
|
||||
end
|
||||
674
spec/lib/graphql_spec.rb
Normal file
674
spec/lib/graphql_spec.rb
Normal file
@@ -0,0 +1,674 @@
|
||||
require 'rails_helper'
|
||||
|
||||
api_types = GraphQL::ApiTypesCreator.create(API_TYPE_DEFINITIONS)
|
||||
query_type = GraphQL::QueryTypeCreator.create(api_types)
|
||||
ConsulSchema = GraphQL::Schema.define do
|
||||
query query_type
|
||||
max_depth 12
|
||||
end
|
||||
|
||||
def execute(query_string, context = {}, variables = {})
|
||||
ConsulSchema.execute(query_string, context: context, variables: variables)
|
||||
end
|
||||
|
||||
def dig(response, path)
|
||||
response.dig(*path.split('.'))
|
||||
end
|
||||
|
||||
def hidden_field?(response, field_name)
|
||||
data_is_empty = response['data'].nil?
|
||||
error_is_present = ((response['errors'].first['message'] =~ /Field '#{field_name}' doesn't exist on type '[[:alnum:]]*'/) == 0)
|
||||
data_is_empty && error_is_present
|
||||
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
|
||||
let(:user) { create(:user) }
|
||||
let(:proposal) { create(:proposal, author: user) }
|
||||
|
||||
it 'returns fields of Int type' do
|
||||
response = execute("{ proposal(id: #{proposal.id}) { id } }")
|
||||
expect(dig(response, 'data.proposal.id')).to eq(proposal.id)
|
||||
end
|
||||
|
||||
it 'returns fields of String type' do
|
||||
response = execute("{ proposal(id: #{proposal.id}) { title } }")
|
||||
expect(dig(response, 'data.proposal.title')).to eq(proposal.title)
|
||||
end
|
||||
|
||||
xit 'returns has_one associations' do
|
||||
organization = create(:organization)
|
||||
response = execute("{ user(id: #{organization.user_id}) { organization { name } } }")
|
||||
expect(dig(response, 'data.user.organization.name')).to eq(organization.name)
|
||||
end
|
||||
|
||||
it 'returns belongs_to associations' do
|
||||
response = execute("{ proposal(id: #{proposal.id}) { public_author { username } } }")
|
||||
expect(dig(response, 'data.proposal.public_author.username')).to eq(proposal.public_author.username)
|
||||
end
|
||||
|
||||
it 'returns has_many associations' do
|
||||
comments_author = create(:user)
|
||||
comment_1 = create(:comment, author: comments_author, commentable: proposal)
|
||||
comment_2 = create(:comment, author: comments_author, commentable: proposal)
|
||||
|
||||
response = execute("{ proposal(id: #{proposal.id}) { comments { edges { node { body } } } } }")
|
||||
comments = dig(response, 'data.proposal.comments.edges').collect { |edge| edge['node'] }
|
||||
comment_bodies = comments.collect { |comment| comment['body'] }
|
||||
|
||||
expect(comment_bodies).to match_array([comment_1.body, comment_2.body])
|
||||
end
|
||||
|
||||
xit 'executes deeply nested queries' do
|
||||
org_user = create(:user)
|
||||
organization = create(:organization, user: org_user)
|
||||
org_proposal = create(:proposal, author: org_user)
|
||||
response = execute("{ proposal(id: #{org_proposal.id}) { public_author { organization { name } } } }")
|
||||
|
||||
expect(dig(response, 'data.proposal.public_author.organization.name')).to eq(organization.name)
|
||||
end
|
||||
|
||||
it 'hides confidential fields of Int type' do
|
||||
response = execute("{ user(id: #{user.id}) { failed_census_calls_count } }")
|
||||
expect(hidden_field?(response, 'failed_census_calls_count')).to be_truthy
|
||||
end
|
||||
|
||||
it 'hides confidential fields of String type' do
|
||||
response = execute("{ user(id: #{user.id}) { encrypted_password } }")
|
||||
expect(hidden_field?(response, 'encrypted_password')).to be_truthy
|
||||
end
|
||||
|
||||
xit 'hides confidential has_one associations' do
|
||||
user.administrator = create(:administrator)
|
||||
response = execute("{ user(id: #{user.id}) { administrator { id } } }")
|
||||
expect(hidden_field?(response, 'administrator')).to be_truthy
|
||||
end
|
||||
|
||||
it 'hides confidential belongs_to associations' do
|
||||
create(:failed_census_call, user: user)
|
||||
response = execute("{ user(id: #{user.id}) { failed_census_calls { id } } }")
|
||||
expect(hidden_field?(response, 'failed_census_calls')).to be_truthy
|
||||
end
|
||||
|
||||
it 'hides confidential has_many associations' do
|
||||
create(:direct_message, sender: user)
|
||||
response = execute("{ user(id: #{user.id}) { direct_messages_sent { id } } }")
|
||||
expect(hidden_field?(response, 'direct_messages_sent')).to be_truthy
|
||||
end
|
||||
|
||||
it 'hides confidential fields inside deeply nested queries' do
|
||||
response = execute("{ proposals(first: 1) { edges { node { public_author { encrypted_password } } } } }")
|
||||
expect(hidden_field?(response, 'encrypted_password')).to be_truthy
|
||||
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.zone.parse("2017-12-31 9:30:15")
|
||||
create(:proposal, created_at: created_at)
|
||||
|
||||
response = execute('{ proposals { edges { node { public_created_at } } } }')
|
||||
received_timestamps = extract_fields(response, 'proposals', 'public_created_at')
|
||||
|
||||
expect(Time.zone.parse(received_timestamps.first)).to eq Time.zone.parse("2017-12-31 9: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.zone.parse("2017-12-31 9:30:15")
|
||||
create(:debate, created_at: created_at)
|
||||
|
||||
response = execute('{ debates { edges { node { public_created_at } } } }')
|
||||
received_timestamps = extract_fields(response, 'debates', 'public_created_at')
|
||||
|
||||
expect(Time.zone.parse(received_timestamps.first)).to eq Time.zone.parse("2017-12-31 9: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.zone.parse("2017-12-31 9:30:15")
|
||||
create(:comment, created_at: created_at)
|
||||
|
||||
response = execute('{ comments { edges { node { public_created_at } } } }')
|
||||
received_timestamps = extract_fields(response, 'comments', 'public_created_at')
|
||||
|
||||
expect(Time.zone.parse(received_timestamps.first)).to eq Time.zone.parse("2017-12-31 9: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.zone.parse("2017-12-31 9:30:15")
|
||||
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(Time.zone.parse(received_timestamps.first)).to eq Time.zone.parse("2017-12-31 9: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.zone.parse("2017-12-31 9:30:15")
|
||||
create(:vote, created_at: created_at)
|
||||
|
||||
response = execute('{ votes { edges { node { public_created_at } } } }')
|
||||
received_timestamps = extract_fields(response, 'votes', 'public_created_at')
|
||||
|
||||
expect(Time.zone.parse(received_timestamps.first)).to eq Time.zone.parse("2017-12-31 9:00:00")
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
@@ -4,6 +4,8 @@ describe Comment do
|
||||
|
||||
let(:comment) { build(:comment) }
|
||||
|
||||
it_behaves_like "has_public_author"
|
||||
|
||||
it "is valid" do
|
||||
expect(comment).to be_valid
|
||||
end
|
||||
@@ -132,4 +134,58 @@ describe Comment do
|
||||
end
|
||||
end
|
||||
|
||||
describe "public_for_api scope" do
|
||||
it "returns comments" do
|
||||
comment = create(:comment)
|
||||
|
||||
expect(Comment.public_for_api).to include(comment)
|
||||
end
|
||||
|
||||
it "does not return hidden comments" do
|
||||
hidden_comment = create(:comment, :hidden)
|
||||
|
||||
expect(Comment.public_for_api).not_to include(hidden_comment)
|
||||
end
|
||||
|
||||
it "returns comments on debates" do
|
||||
debate = create(:debate)
|
||||
comment = create(:comment, commentable: debate)
|
||||
|
||||
expect(Comment.public_for_api).to include(comment)
|
||||
end
|
||||
|
||||
it "does not return comments on hidden debates" do
|
||||
hidden_debate = create(:debate, :hidden)
|
||||
comment = create(:comment, commentable: hidden_debate)
|
||||
|
||||
expect(Comment.public_for_api).not_to include(comment)
|
||||
end
|
||||
|
||||
it "returns comments on proposals" do
|
||||
proposal = create(:proposal)
|
||||
comment = create(:comment, commentable: proposal)
|
||||
|
||||
expect(Comment.public_for_api).to include(comment)
|
||||
end
|
||||
|
||||
it "does not return comments on hidden proposals" do
|
||||
hidden_proposal = create(:proposal, :hidden)
|
||||
comment = create(:comment, commentable: hidden_proposal)
|
||||
|
||||
expect(Comment.public_for_api).not_to include(comment)
|
||||
end
|
||||
|
||||
it 'does not return comments on elements which are not debates or proposals' do
|
||||
budget_investment = create(:budget_investment)
|
||||
comment = create(:comment, commentable: budget_investment)
|
||||
|
||||
expect(Comment.public_for_api).not_to include(comment)
|
||||
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
|
||||
|
||||
21
spec/models/concerns/has_public_author_spec.rb
Normal file
21
spec/models/concerns/has_public_author_spec.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
require 'spec_helper'
|
||||
|
||||
shared_examples_for 'has_public_author' do
|
||||
let(:model) { described_class }
|
||||
|
||||
describe 'public_author' do
|
||||
it "returns author if author's activity is public" do
|
||||
author = create(:user, public_activity: true)
|
||||
authored_element = create(model.to_s.underscore.to_sym, author: author)
|
||||
|
||||
expect(authored_element.public_author).to eq(author)
|
||||
end
|
||||
|
||||
it "returns nil if author's activity is private" do
|
||||
author = create(:user, public_activity: false)
|
||||
authored_element = create(model.to_s.underscore.to_sym, author: author)
|
||||
|
||||
expect(authored_element.public_author).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4,6 +4,8 @@ require 'rails_helper'
|
||||
describe Debate do
|
||||
let(:debate) { build(:debate) }
|
||||
|
||||
it_behaves_like "has_public_author"
|
||||
|
||||
it "should be valid" do
|
||||
expect(debate).to be_valid
|
||||
end
|
||||
@@ -700,4 +702,16 @@ describe Debate do
|
||||
end
|
||||
end
|
||||
|
||||
describe 'public_for_api scope' do
|
||||
it 'returns debates' do
|
||||
debate = create(:debate)
|
||||
expect(Debate.public_for_api).to include(debate)
|
||||
end
|
||||
|
||||
it 'does not return hidden debates' do
|
||||
debate = create(:debate, :hidden)
|
||||
expect(Debate.public_for_api).to_not include(debate)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -22,6 +22,28 @@ describe ProposalNotification do
|
||||
expect(notification).to_not be_valid
|
||||
end
|
||||
|
||||
describe "public_for_api scope" do
|
||||
it "returns proposal notifications" do
|
||||
proposal = create(:proposal)
|
||||
notification = create(:proposal_notification, proposal: proposal)
|
||||
|
||||
expect(ProposalNotification.public_for_api).to include(notification)
|
||||
end
|
||||
|
||||
it "blocks proposal notifications whose proposal is hidden" do
|
||||
proposal = create(:proposal, :hidden)
|
||||
notification = create(:proposal_notification, proposal: proposal)
|
||||
|
||||
expect(ProposalNotification.public_for_api).not_to include(notification)
|
||||
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
|
||||
|
||||
describe "minimum interval between notifications" do
|
||||
|
||||
before(:each) do
|
||||
|
||||
@@ -4,6 +4,8 @@ require 'rails_helper'
|
||||
describe Proposal do
|
||||
let(:proposal) { build(:proposal) }
|
||||
|
||||
it_behaves_like "has_public_author"
|
||||
|
||||
it "should be valid" do
|
||||
expect(proposal).to be_valid
|
||||
end
|
||||
@@ -843,4 +845,16 @@ describe Proposal do
|
||||
end
|
||||
end
|
||||
|
||||
describe 'public_for_api scope' do
|
||||
it 'returns proposals' do
|
||||
proposal = create(:proposal)
|
||||
expect(Proposal.public_for_api).to include(proposal)
|
||||
end
|
||||
|
||||
it 'does not return hidden proposals' do
|
||||
proposal = create(:proposal, :hidden)
|
||||
expect(Proposal.public_for_api).to_not include(proposal)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -40,4 +40,77 @@ describe 'Vote' do
|
||||
expect(vote.value).to eq(false)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'public_for_api scope' do
|
||||
it 'returns votes on debates' do
|
||||
debate = create(:debate)
|
||||
vote = create(:vote, votable: debate)
|
||||
|
||||
expect(Vote.public_for_api).to include(vote)
|
||||
end
|
||||
|
||||
it 'blocks votes on hidden debates' do
|
||||
debate = create(:debate, :hidden)
|
||||
vote = create(:vote, votable: debate)
|
||||
|
||||
expect(Vote.public_for_api).not_to include(vote)
|
||||
end
|
||||
|
||||
it 'returns votes on proposals' do
|
||||
proposal = create(:proposal)
|
||||
vote = create(:vote, votable: proposal)
|
||||
|
||||
expect(Vote.public_for_api).to include(vote)
|
||||
end
|
||||
|
||||
it 'blocks votes on hidden proposals' do
|
||||
proposal = create(:proposal, :hidden)
|
||||
vote = create(:vote, votable: proposal)
|
||||
|
||||
expect(Vote.public_for_api).not_to include(vote)
|
||||
end
|
||||
|
||||
it 'returns votes on comments' do
|
||||
comment = create(:comment)
|
||||
vote = create(:vote, votable: comment)
|
||||
|
||||
expect(Vote.public_for_api).to include(vote)
|
||||
end
|
||||
|
||||
it 'blocks votes on hidden comments' do
|
||||
comment = create(:comment, :hidden)
|
||||
vote = create(:vote, votable: comment)
|
||||
|
||||
expect(Vote.public_for_api).not_to include(vote)
|
||||
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
|
||||
spending_proposal = create(:spending_proposal)
|
||||
vote = create(:vote, votable: spending_proposal)
|
||||
|
||||
expect(Vote.public_for_api).not_to include(vote)
|
||||
end
|
||||
|
||||
it 'blocks votes without votable' do
|
||||
vote = build(:vote, votable: nil).save!(validate: false)
|
||||
|
||||
expect(Vote.public_for_api).not_to include(vote)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,6 +3,7 @@ require 'database_cleaner'
|
||||
require 'email_spec'
|
||||
require 'devise'
|
||||
require 'knapsack'
|
||||
Dir["./spec/models/concerns/*.rb"].each { |f| require f }
|
||||
Dir["./spec/support/**/*.rb"].sort.each { |f| require f }
|
||||
|
||||
RSpec.configure do |config|
|
||||
|
||||
Reference in New Issue
Block a user