Skip to content

Commit

Permalink
Merge pull request consuldemocracy#1653 from consul/amiedes-api-dev-P…
Browse files Browse the repository at this point in the history
…Rs-2

Graphql API
  • Loading branch information
kikito authored Jun 15, 2017
2 parents a6bc109 + 9ab0f9c commit 2a095a3
Show file tree
Hide file tree
Showing 35 changed files with 1,559 additions and 16 deletions.
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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'
6 changes: 5 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
52 changes: 52 additions & 0 deletions app/controllers/graphql_controller.rb
Original file line number Diff line number Diff line change
@@ -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
8 changes: 8 additions & 0 deletions app/models/comment.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
class Comment < ActiveRecord::Base
include Flaggable
include HasPublicAuthor
include Graphqlable

acts_as_paranoid column: :hidden_at
include ActsAsParanoidAliases
Expand All @@ -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) }
Expand Down
36 changes: 36 additions & 0 deletions app/models/concerns/graphqlable.rb
Original file line number Diff line number Diff line change
@@ -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 changes: 5 additions & 0 deletions app/models/concerns/has_public_author.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module HasPublicAuthor
def public_author
self.author.public_activity? ? self.author : nil
end
end
6 changes: 3 additions & 3 deletions app/models/concerns/measurable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -21,4 +21,4 @@ def description_max_length

end

end
end
3 changes: 3 additions & 0 deletions app/models/debate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
5 changes: 5 additions & 0 deletions app/models/geozone.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down
3 changes: 3 additions & 0 deletions app/models/organization.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
class Organization < ActiveRecord::Base

include Graphqlable

belongs_to :user, touch: true

validates :name, presence: true
Expand Down
3 changes: 3 additions & 0 deletions app/models/proposal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions app/models/proposal_notification.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
class ProposalNotification < ActiveRecord::Base

include Graphqlable

belongs_to :author, class_name: 'User', foreign_key: 'author_id'
belongs_to :proposal

Expand All @@ -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
Expand Down
15 changes: 15 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -288,6 +291,18 @@ def ability
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
Expand Down
14 changes: 13 additions & 1 deletion app/models/vote.rb
Original file line number Diff line number Diff line change
@@ -1,2 +1,14 @@
class Vote < ActsAsVotable::Vote
end

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 changes: 89 additions & 0 deletions config/api.yml
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 2a095a3

Please sign in to comment.