From df5caea56f18da6e6f7b698933b562aadfff74c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Wed, 28 Sep 2016 12:45:34 +0200 Subject: [PATCH 001/155] Installed needed gems for GraphQL --- Gemfile | 3 +++ Gemfile.lock | 7 ++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 83a3bddbd..cf753556a 100644 --- a/Gemfile +++ b/Gemfile @@ -63,6 +63,9 @@ gem 'browser' gem 'turnout' gem 'redcarpet' +gem 'graphql' +gem 'graphiql-rails' + group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug' diff --git a/Gemfile.lock b/Gemfile.lock index bf37a7091..71a7ce2eb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -176,6 +176,9 @@ GEM geocoder (1.3.7) globalid (0.3.7) activesupport (>= 4.1.0) + graphiql-rails (1.3.0) + rails + graphql (0.18.11) groupdate (3.0.1) activesupport (>= 3) gyoku (1.3.1) @@ -466,6 +469,8 @@ DEPENDENCIES foundation-rails foundation_rails_helper fuubar + graphiql-rails + graphql groupdate i18n-tasks initialjs-rails (= 0.2.0.4) @@ -506,4 +511,4 @@ DEPENDENCIES whenever BUNDLED WITH - 1.12.5 + 1.13.1 From ab333d6627ae780990ceb750bb99136994487e8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Wed, 28 Sep 2016 12:47:07 +0200 Subject: [PATCH 002/155] Created controller and route to answer GraphQL queries --- app/controllers/graphql_controller.rb | 11 +++++++++++ config/routes.rb | 5 +++++ 2 files changed, 16 insertions(+) create mode 100644 app/controllers/graphql_controller.rb diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb new file mode 100644 index 000000000..11accf52a --- /dev/null +++ b/app/controllers/graphql_controller.rb @@ -0,0 +1,11 @@ +class GraphqlController < ApplicationController + + def query + #puts "I'm the GraphqlController inside the #query action!!" + + render json: ConsulSchema.execute( + params[:query], + variables: params[:variables] || {} + ) + end +end diff --git a/config/routes.rb b/config/routes.rb index df1a7c6fb..b86cb0c30 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -262,6 +262,11 @@ Rails.application.routes.draw do end end + # GraphQL + mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/queries" + post '/queries', to: 'graphql#query' + + if Rails.env.development? mount LetterOpenerWeb::Engine, at: "/letter_opener" end From 022340b1cf076dd969c4969d6fcd463dd3b72bb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Wed, 28 Sep 2016 12:48:24 +0200 Subject: [PATCH 003/155] Created GraphQL Schema and query root --- app/graph/consul_schema.rb | 3 +++ app/graph/types/query_root.rb | 5 +++++ config/application.rb | 2 ++ 3 files changed, 10 insertions(+) create mode 100644 app/graph/consul_schema.rb create mode 100644 app/graph/types/query_root.rb diff --git a/app/graph/consul_schema.rb b/app/graph/consul_schema.rb new file mode 100644 index 000000000..dc7db1354 --- /dev/null +++ b/app/graph/consul_schema.rb @@ -0,0 +1,3 @@ +ConsulSchema = GraphQL::Schema.define do + query QueryRoot +end diff --git a/app/graph/types/query_root.rb b/app/graph/types/query_root.rb new file mode 100644 index 000000000..121b5f389 --- /dev/null +++ b/app/graph/types/query_root.rb @@ -0,0 +1,5 @@ +QueryRoot = GraphQL::ObjectType.define do + name "Query" + description "The query root for this schema" + +end diff --git a/config/application.rb b/config/application.rb index 3f2a0861a..3182e319c 100644 --- a/config/application.rb +++ b/config/application.rb @@ -45,6 +45,8 @@ module Consul config.autoload_paths << "#{Rails.root}/app/models/custom" config.paths['app/views'].unshift(Rails.root.join('app', 'views', 'custom')) + # Add GraphQL directories to the autoload path + config.autoload_paths << Rails.root.join('app', 'graph', 'types') end end From 530da6b893021cc64d891163dfe4982430519384 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Wed, 28 Sep 2016 12:49:38 +0200 Subject: [PATCH 004/155] Implemented simple test query to proposals --- app/controllers/graphql_controller.rb | 1 + app/graph/queries/getProposal.graphql | 7 +++++++ app/graph/types/proposal_type.rb | 8 ++++++++ app/graph/types/query_root.rb | 8 ++++++++ 4 files changed, 24 insertions(+) create mode 100644 app/graph/queries/getProposal.graphql create mode 100644 app/graph/types/proposal_type.rb diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index 11accf52a..47deceac0 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -1,4 +1,5 @@ class GraphqlController < ApplicationController + authorize_resource :proposal def query #puts "I'm the GraphqlController inside the #query action!!" diff --git a/app/graph/queries/getProposal.graphql b/app/graph/queries/getProposal.graphql new file mode 100644 index 000000000..875dc0831 --- /dev/null +++ b/app/graph/queries/getProposal.graphql @@ -0,0 +1,7 @@ +query getProposal { + proposal(id: 22) { + id, + title, + description + } +} diff --git a/app/graph/types/proposal_type.rb b/app/graph/types/proposal_type.rb new file mode 100644 index 000000000..223906f90 --- /dev/null +++ b/app/graph/types/proposal_type.rb @@ -0,0 +1,8 @@ +ProposalType = GraphQL::ObjectType.define do + name "Proposal" + description "A single proposal entry returns a proposal with author, total votes and comments" + # Expose fields associated with Proposal model + field :id, types.ID, "The id of this proposal" + field :title, types.String, "The title of this proposal" + field :description, types.String, "The description of this proposal" +end diff --git a/app/graph/types/query_root.rb b/app/graph/types/query_root.rb index 121b5f389..28d1685be 100644 --- a/app/graph/types/query_root.rb +++ b/app/graph/types/query_root.rb @@ -2,4 +2,12 @@ QueryRoot = GraphQL::ObjectType.define do name "Query" description "The query root for this schema" + field :proposal do + type ProposalType + description "Find a Proposal by id" + argument :id, !types.ID + resolve -> (object, arguments, context) { + Proposal.find(arguments["id"]) + } + end end From b90a6664b0733f1c124a79e8cab0f8280c003408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 29 Sep 2016 11:21:25 +0200 Subject: [PATCH 005/155] Deleted useless file --- app/graph/queries/getProposal.graphql | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 app/graph/queries/getProposal.graphql diff --git a/app/graph/queries/getProposal.graphql b/app/graph/queries/getProposal.graphql deleted file mode 100644 index 875dc0831..000000000 --- a/app/graph/queries/getProposal.graphql +++ /dev/null @@ -1,7 +0,0 @@ -query getProposal { - proposal(id: 22) { - id, - title, - description - } -} From 5c7f03bdf10b9b9ea9ec085e1120a09f79af9190 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 29 Sep 2016 11:22:18 +0200 Subject: [PATCH 006/155] Added more fields to graphql proposal_type --- app/graph/types/proposal_type.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/graph/types/proposal_type.rb b/app/graph/types/proposal_type.rb index 223906f90..292c0aa15 100644 --- a/app/graph/types/proposal_type.rb +++ b/app/graph/types/proposal_type.rb @@ -5,4 +5,11 @@ ProposalType = GraphQL::ObjectType.define do field :id, types.ID, "The id of this proposal" field :title, types.String, "The title of this proposal" field :description, types.String, "The description of this proposal" + field :question, types.String, "The question of this proposal" + field :external_url, types.String, "External url related to this proposal" + field :flags_count, types.Int, "Number of flags of this proposal" + field :cached_votes_up, types.Int, "Number of upvotes of this proposal" + field :comments_count, types.Int, "Number of comments on this proposal" + field :summary, types.String, "The summary of this proposal" + field :video_url, types.String, "External video url related to this proposal" end From 96d688e2869bb13df6c9f9d522982aca28cc16d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 29 Sep 2016 11:31:08 +0200 Subject: [PATCH 007/155] Created graphql comment_type --- app/graph/types/comment_type.rb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 app/graph/types/comment_type.rb diff --git a/app/graph/types/comment_type.rb b/app/graph/types/comment_type.rb new file mode 100644 index 000000000..2c74bee40 --- /dev/null +++ b/app/graph/types/comment_type.rb @@ -0,0 +1,17 @@ +CommentType = GraphQL::ObjectType.define do + name "Comment" + description "A reply to a proposal, spending proposal, debate or comment" + + field :id, !types.ID, "The unique ID of this comment" + field :commentable_id, !types.Int, "ID of the resource where this comment was placed on" + field :commentable_type, !types.String, "Type of resource where this comment was placed on" + field :body, !types.String, "The body of this comment" + field :subject, !types.String, "The subject of this comment" + field :user_id, !types.Int, "The ID of the user who made this comment" + field :created_at, !types.String, "The date this comment was posted" + field :updated_at, !types.String, "The date this comment was edited" + field :flags_count, !types.Int, "The number of flags of this comment" + field :cached_votes_total, !types.Int, "The total number of votes of this comment" + field :cached_votes_up, !types.Int, "The total number of upvotes of this comment" + field :cached_votes_down, !types.Int, "The total number of downvotes of this comment" +end From dc0c36d11718a4d5544a6188f4b685f8d5c2bc31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 29 Sep 2016 11:41:02 +0200 Subject: [PATCH 008/155] Added more fields to graphql proposal_type --- app/graph/types/proposal_type.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/graph/types/proposal_type.rb b/app/graph/types/proposal_type.rb index 292c0aa15..67e06d70d 100644 --- a/app/graph/types/proposal_type.rb +++ b/app/graph/types/proposal_type.rb @@ -7,9 +7,14 @@ ProposalType = GraphQL::ObjectType.define do field :description, types.String, "The description of this proposal" field :question, types.String, "The question of this proposal" field :external_url, types.String, "External url related to this proposal" + field :author_id, types.Int, "ID of the author of this proposal" field :flags_count, types.Int, "Number of flags of this proposal" field :cached_votes_up, types.Int, "Number of upvotes of this proposal" field :comments_count, types.Int, "Number of comments on this proposal" field :summary, types.String, "The summary of this proposal" field :video_url, types.String, "External video url related to this proposal" + field :geozone_id, types.Int, "ID of the geozone affected by this proposal" + field :retired_at, types.String, "Date when this proposal was retired" + field :retired_reason, types.String, "Reason why this proposal was retired" + field :retired_explanation, types.String, "Explanation why this proposal was retired" end From d201f6a48ad9034a7bbeb006aad68a792596a2de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 29 Sep 2016 11:42:13 +0200 Subject: [PATCH 009/155] Ability to retrieve proposal comments --- app/graph/types/proposal_type.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/graph/types/proposal_type.rb b/app/graph/types/proposal_type.rb index 67e06d70d..2bc1e37b0 100644 --- a/app/graph/types/proposal_type.rb +++ b/app/graph/types/proposal_type.rb @@ -17,4 +17,7 @@ ProposalType = GraphQL::ObjectType.define do field :retired_at, types.String, "Date when this proposal was retired" field :retired_reason, types.String, "Reason why this proposal was retired" field :retired_explanation, types.String, "Explanation why this proposal was retired" + + # Linked resources + field :comments, !types[!CommentType], "Comments in this proposal" end From de913c5536125d956cf28598ed57d4a4d38cabbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 29 Sep 2016 13:10:05 +0200 Subject: [PATCH 010/155] Fixed grapqhl comment_type fields --- app/graph/types/comment_type.rb | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/graph/types/comment_type.rb b/app/graph/types/comment_type.rb index 2c74bee40..8701ee04f 100644 --- a/app/graph/types/comment_type.rb +++ b/app/graph/types/comment_type.rb @@ -3,15 +3,15 @@ CommentType = GraphQL::ObjectType.define do description "A reply to a proposal, spending proposal, debate or comment" field :id, !types.ID, "The unique ID of this comment" - field :commentable_id, !types.Int, "ID of the resource where this comment was placed on" - field :commentable_type, !types.String, "Type of resource where this comment was placed on" - field :body, !types.String, "The body of this comment" - field :subject, !types.String, "The subject of this comment" + field :commentable_id, types.Int, "ID of the resource where this comment was placed on" + field :commentable_type, types.String, "Type of resource where this comment was placed on" + field :body, types.String, "The body of this comment" + field :subject, types.String, "The subject of this comment" field :user_id, !types.Int, "The ID of the user who made this comment" - field :created_at, !types.String, "The date this comment was posted" - field :updated_at, !types.String, "The date this comment was edited" - field :flags_count, !types.Int, "The number of flags of this comment" - field :cached_votes_total, !types.Int, "The total number of votes of this comment" - field :cached_votes_up, !types.Int, "The total number of upvotes of this comment" - field :cached_votes_down, !types.Int, "The total number of downvotes of this comment" + field :created_at, types.String, "The date this comment was posted" + field :updated_at, types.String, "The date this comment was edited" + field :flags_count, types.Int, "The number of flags of this comment" + field :cached_votes_total, types.Int, "The total number of votes of this comment" + field :cached_votes_up, types.Int, "The total number of upvotes of this comment" + field :cached_votes_down, types.Int, "The total number of downvotes of this comment" end From 6a3f7d725cecb8ed649839b25571abfbe39f5a32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 29 Sep 2016 13:11:34 +0200 Subject: [PATCH 011/155] QueryRoot now returns proposals, comment(:id) and comments --- app/graph/types/query_root.rb | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/app/graph/types/query_root.rb b/app/graph/types/query_root.rb index 28d1685be..5b8664dc6 100644 --- a/app/graph/types/query_root.rb +++ b/app/graph/types/query_root.rb @@ -10,4 +10,29 @@ QueryRoot = GraphQL::ObjectType.define do Proposal.find(arguments["id"]) } end + + field :proposals do + type types[ProposalType] + description "Find all Proposals" + resolve -> (object, arguments, context) { + Proposal.all + } + end + + field :comment do + type CommentType + description "Find a Comment by id" + argument :id, !types.ID + resolve -> (object, arguments, context) { + Comment.find(arguments["id"]) + } + end + + field :comments do + type types[CommentType] + description "Find all Comments" + resolve -> (object, arguments, context) { + Comment.all + } + end end From a6149018d9fb754f7d781feb235ac7878f80a3d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 29 Sep 2016 13:26:18 +0200 Subject: [PATCH 012/155] Created graphql user_type --- app/graph/types/user_type.rb | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 app/graph/types/user_type.rb diff --git a/app/graph/types/user_type.rb b/app/graph/types/user_type.rb new file mode 100644 index 000000000..c88e5acde --- /dev/null +++ b/app/graph/types/user_type.rb @@ -0,0 +1,11 @@ +UserType = GraphQL::ObjectType.define do + name "User" + description "An user entry, returns basic user information" + # Expose fields associated with User model + field :id, types.ID, "The id of this user" + field :created_at, types.String, "Date when this user was created" + field :username, types.String, "The username of this user" + field :geozone_id, types.Int, "The ID of the geozone where this user is active" + field :gender, types.String, "The gender of this user" + field :date_of_birth, types.String, "The birthdate of this user" +end From 657f57f9a2161b0baef6be78474e99e49d73173e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 29 Sep 2016 13:26:54 +0200 Subject: [PATCH 013/155] Ability to link authors/users from comments and proposals --- app/graph/types/comment_type.rb | 3 +++ app/graph/types/proposal_type.rb | 1 + 2 files changed, 4 insertions(+) diff --git a/app/graph/types/comment_type.rb b/app/graph/types/comment_type.rb index 8701ee04f..9078257af 100644 --- a/app/graph/types/comment_type.rb +++ b/app/graph/types/comment_type.rb @@ -14,4 +14,7 @@ CommentType = GraphQL::ObjectType.define do field :cached_votes_total, types.Int, "The total number of votes of this comment" field :cached_votes_up, types.Int, "The total number of upvotes of this comment" field :cached_votes_down, types.Int, "The total number of downvotes of this comment" + + # Linked resources + field :user, !UserType, "User who made this comment" end diff --git a/app/graph/types/proposal_type.rb b/app/graph/types/proposal_type.rb index 2bc1e37b0..1e3514997 100644 --- a/app/graph/types/proposal_type.rb +++ b/app/graph/types/proposal_type.rb @@ -19,5 +19,6 @@ ProposalType = GraphQL::ObjectType.define do field :retired_explanation, types.String, "Explanation why this proposal was retired" # Linked resources + field :author, UserType, "Author of this proposal" field :comments, !types[!CommentType], "Comments in this proposal" end From 31f0b9b98d6f4b5e4d25a209d5fb03ae5804cc77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 30 Sep 2016 11:28:16 +0200 Subject: [PATCH 014/155] Created graphql geozone_type --- app/graph/types/geozone_type.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 app/graph/types/geozone_type.rb diff --git a/app/graph/types/geozone_type.rb b/app/graph/types/geozone_type.rb new file mode 100644 index 000000000..fc4bb2343 --- /dev/null +++ b/app/graph/types/geozone_type.rb @@ -0,0 +1,10 @@ +GeozoneType = GraphQL::ObjectType.define do + name "Geozone" + description "A geozone entry, returns basic geozone information" + # Expose fields associated with Geozone model + field :id, types.ID, "The id of this geozone" + field :name, types.String, "The name of this geozone" + field :html_map_coordinates, types.String, "HTML map coordinates of this geozone" + field :external_code, types.String, "The external code of this geozone" + field :census_code, types.String, "The census code of this geozone" +end From 57b01e09216a42221c64008dfe21755131f5386c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 30 Sep 2016 11:33:52 +0200 Subject: [PATCH 015/155] Ability to retrieve geozone from proposals and users --- app/graph/types/proposal_type.rb | 1 + app/graph/types/user_type.rb | 3 +++ 2 files changed, 4 insertions(+) diff --git a/app/graph/types/proposal_type.rb b/app/graph/types/proposal_type.rb index 1e3514997..58ece72a6 100644 --- a/app/graph/types/proposal_type.rb +++ b/app/graph/types/proposal_type.rb @@ -21,4 +21,5 @@ ProposalType = GraphQL::ObjectType.define do # Linked resources field :author, UserType, "Author of this proposal" field :comments, !types[!CommentType], "Comments in this proposal" + field :geozone, GeozoneType, "Geozone affected by this proposal" end diff --git a/app/graph/types/user_type.rb b/app/graph/types/user_type.rb index c88e5acde..224ae2702 100644 --- a/app/graph/types/user_type.rb +++ b/app/graph/types/user_type.rb @@ -8,4 +8,7 @@ UserType = GraphQL::ObjectType.define do field :geozone_id, types.Int, "The ID of the geozone where this user is active" field :gender, types.String, "The gender of this user" field :date_of_birth, types.String, "The birthdate of this user" + + # Linked resources + field :geozone, GeozoneType, "Geozone where this user is registered" end From eae18d9e376ad71d3e2f5f6ac6cf2a8d6d94b2db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 30 Sep 2016 11:34:12 +0200 Subject: [PATCH 016/155] Deleted useless debugging comment --- app/controllers/graphql_controller.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index 47deceac0..bf9858a7d 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -2,8 +2,6 @@ class GraphqlController < ApplicationController authorize_resource :proposal def query - #puts "I'm the GraphqlController inside the #query action!!" - render json: ConsulSchema.execute( params[:query], variables: params[:variables] || {} From 78423edb2a85196dec7d20347f0614a1c8aa7b64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 30 Sep 2016 11:49:05 +0200 Subject: [PATCH 017/155] Created graphql debate_type --- app/graph/types/debate_type.rb | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 app/graph/types/debate_type.rb diff --git a/app/graph/types/debate_type.rb b/app/graph/types/debate_type.rb new file mode 100644 index 000000000..095d50688 --- /dev/null +++ b/app/graph/types/debate_type.rb @@ -0,0 +1,22 @@ +DebateType = GraphQL::ObjectType.define do + name "Debate" + description "A single debate entry with associated info" + # Expose fields associated with Debate model + field :id, !types.ID, "The id of this debate" + field :title, types.String, "The title of this debate" + field :description, types.String, "The description of this debate" + field :author_id, types.Int, "ID of the author of this proposal" + field :created_at, types.String, "Date when this debate was created" + field :updated_at, types.String, "Date when this debate was edited" + field :flags_count, types.Int, "Number of flags of this debate" + field :cached_votes_total, types.Int, "The total number of votes of this debate" + field :cached_votes_up, types.Int, "The total number of upvotes of this debate" + field :cached_votes_down, types.Int, "The total number of downvotes of this debate" + field :comments_count, types.Int, "Number of comments on this debate" + field :geozone_id, types.Int, "ID of the geozone affected by this debate" + + # Linked resources + field :author, UserType, "Author of this debate" + field :comments, !types[!CommentType], "Comments in this debate" + field :geozone, GeozoneType, "Geozone affected by this debate" +end From afb2d19ab32051a5888d055d525edea347cf757c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 30 Sep 2016 11:49:34 +0200 Subject: [PATCH 018/155] Revised which fields are allowed to be null --- app/graph/types/geozone_type.rb | 2 +- app/graph/types/proposal_type.rb | 2 +- app/graph/types/query_root.rb | 4 ++-- app/graph/types/user_type.rb | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/graph/types/geozone_type.rb b/app/graph/types/geozone_type.rb index fc4bb2343..bcea6dc93 100644 --- a/app/graph/types/geozone_type.rb +++ b/app/graph/types/geozone_type.rb @@ -2,7 +2,7 @@ GeozoneType = GraphQL::ObjectType.define do name "Geozone" description "A geozone entry, returns basic geozone information" # Expose fields associated with Geozone model - field :id, types.ID, "The id of this geozone" + field :id, !types.ID, "The id of this geozone" field :name, types.String, "The name of this geozone" field :html_map_coordinates, types.String, "HTML map coordinates of this geozone" field :external_code, types.String, "The external code of this geozone" diff --git a/app/graph/types/proposal_type.rb b/app/graph/types/proposal_type.rb index 58ece72a6..8456f0a4f 100644 --- a/app/graph/types/proposal_type.rb +++ b/app/graph/types/proposal_type.rb @@ -2,7 +2,7 @@ ProposalType = GraphQL::ObjectType.define do name "Proposal" description "A single proposal entry returns a proposal with author, total votes and comments" # Expose fields associated with Proposal model - field :id, types.ID, "The id of this proposal" + field :id, !types.ID, "The id of this proposal" field :title, types.String, "The title of this proposal" field :description, types.String, "The description of this proposal" field :question, types.String, "The question of this proposal" diff --git a/app/graph/types/query_root.rb b/app/graph/types/query_root.rb index 5b8664dc6..1193f8b52 100644 --- a/app/graph/types/query_root.rb +++ b/app/graph/types/query_root.rb @@ -12,7 +12,7 @@ QueryRoot = GraphQL::ObjectType.define do end field :proposals do - type types[ProposalType] + type !types[!ProposalType] description "Find all Proposals" resolve -> (object, arguments, context) { Proposal.all @@ -29,7 +29,7 @@ QueryRoot = GraphQL::ObjectType.define do end field :comments do - type types[CommentType] + type !types[!CommentType] description "Find all Comments" resolve -> (object, arguments, context) { Comment.all diff --git a/app/graph/types/user_type.rb b/app/graph/types/user_type.rb index 224ae2702..bb64b818f 100644 --- a/app/graph/types/user_type.rb +++ b/app/graph/types/user_type.rb @@ -2,7 +2,7 @@ UserType = GraphQL::ObjectType.define do name "User" description "An user entry, returns basic user information" # Expose fields associated with User model - field :id, types.ID, "The id of this user" + field :id, !types.ID, "The id of this user" field :created_at, types.String, "Date when this user was created" field :username, types.String, "The username of this user" field :geozone_id, types.Int, "The ID of the geozone where this user is active" From c4c6b1b9f7a35e1339936e06566e4f67491d8ee1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 30 Sep 2016 11:49:59 +0200 Subject: [PATCH 019/155] Ability to query for debate(:id) and debates at the QueryRoot --- app/graph/types/query_root.rb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/app/graph/types/query_root.rb b/app/graph/types/query_root.rb index 1193f8b52..38f292bb9 100644 --- a/app/graph/types/query_root.rb +++ b/app/graph/types/query_root.rb @@ -19,6 +19,23 @@ QueryRoot = GraphQL::ObjectType.define do } end + field :debate do + type DebateType + description "Find a Debate by id" + argument :id, !types.ID + resolve -> (object, arguments, context) { + Debate.find(arguments["id"]) + } + end + + field :debates do + type !types[!DebateType] + description "Find all Debates" + resolve -> (object, arguments, context) { + Debate.all + } + end + field :comment do type CommentType description "Find a Comment by id" From afd0e98ceabb3eaccff7ce70de7d45e9f1a268c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 30 Sep 2016 12:03:37 +0200 Subject: [PATCH 020/155] Created CommentableInterface --- app/graph/types/comment_type.rb | 1 + app/graph/types/commentable_interface.rb | 21 +++++++++++++++++++++ app/graph/types/debate_type.rb | 3 +++ app/graph/types/proposal_type.rb | 3 +++ 4 files changed, 28 insertions(+) create mode 100644 app/graph/types/commentable_interface.rb diff --git a/app/graph/types/comment_type.rb b/app/graph/types/comment_type.rb index 9078257af..36a9f709e 100644 --- a/app/graph/types/comment_type.rb +++ b/app/graph/types/comment_type.rb @@ -17,4 +17,5 @@ CommentType = GraphQL::ObjectType.define do # Linked resources field :user, !UserType, "User who made this comment" + field :commentable, CommentableInterface, "Element which was commented" end diff --git a/app/graph/types/commentable_interface.rb b/app/graph/types/commentable_interface.rb new file mode 100644 index 000000000..3bbe96c4f --- /dev/null +++ b/app/graph/types/commentable_interface.rb @@ -0,0 +1,21 @@ +CommentableInterface = GraphQL::InterfaceType.define do + name "Commentable" + + field :id, !types.ID, "ID of the commentable" + field :title, types.String, "The title of this commentable" + field :description, types.String, "The description of this commentable" + field :author_id, types.Int, "ID of the author of this commentable" + field :comments_count, types.Int, "Number of comments on this commentable" + + # Linked resources + field :author, UserType, "Author of this commentable" + field :comments, !types[!CommentType], "Comments in this commentable" + field :geozone, GeozoneType, "Geozone affected by this commentable" +end + + +# Then, object types may include it: +CoffeeType = GraphQL::ObjectType.define do + # ... + interfaces([BeverageInterface]) +end diff --git a/app/graph/types/debate_type.rb b/app/graph/types/debate_type.rb index 095d50688..1cae3a2a4 100644 --- a/app/graph/types/debate_type.rb +++ b/app/graph/types/debate_type.rb @@ -1,6 +1,9 @@ DebateType = GraphQL::ObjectType.define do name "Debate" description "A single debate entry with associated info" + + interfaces([CommentableInterface]) + # Expose fields associated with Debate model field :id, !types.ID, "The id of this debate" field :title, types.String, "The title of this debate" diff --git a/app/graph/types/proposal_type.rb b/app/graph/types/proposal_type.rb index 8456f0a4f..007b5824c 100644 --- a/app/graph/types/proposal_type.rb +++ b/app/graph/types/proposal_type.rb @@ -1,6 +1,9 @@ ProposalType = GraphQL::ObjectType.define do name "Proposal" description "A single proposal entry returns a proposal with author, total votes and comments" + + interfaces([CommentableInterface]) + # Expose fields associated with Proposal model field :id, !types.ID, "The id of this proposal" field :title, types.String, "The title of this proposal" From 5f407d505955a5d0f9be189cc1f99ca8c5c18308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 30 Sep 2016 12:04:03 +0200 Subject: [PATCH 021/155] Added schema interface resolver --- app/graph/consul_schema.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/graph/consul_schema.rb b/app/graph/consul_schema.rb index dc7db1354..bf05f48b9 100644 --- a/app/graph/consul_schema.rb +++ b/app/graph/consul_schema.rb @@ -1,3 +1,9 @@ ConsulSchema = GraphQL::Schema.define do query QueryRoot + + resolve_type -> (object, ctx) { + # look up types by class name + type_name = object.class.name + ConsulSchema.types[type_name] + } end From fab190b16cc09a513a1609234aafe0cdd3474b83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 30 Sep 2016 12:06:10 +0200 Subject: [PATCH 022/155] Deleted accidentally commited code --- app/graph/types/commentable_interface.rb | 7 ------- 1 file changed, 7 deletions(-) diff --git a/app/graph/types/commentable_interface.rb b/app/graph/types/commentable_interface.rb index 3bbe96c4f..1f62b0d09 100644 --- a/app/graph/types/commentable_interface.rb +++ b/app/graph/types/commentable_interface.rb @@ -12,10 +12,3 @@ CommentableInterface = GraphQL::InterfaceType.define do field :comments, !types[!CommentType], "Comments in this commentable" field :geozone, GeozoneType, "Geozone affected by this commentable" end - - -# Then, object types may include it: -CoffeeType = GraphQL::ObjectType.define do - # ... - interfaces([BeverageInterface]) -end From fe2546dbbbb7c8c5cfcf1a6f0457240788a73b47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Sat, 1 Oct 2016 08:50:16 +0200 Subject: [PATCH 023/155] Added clarifying comments related to security --- app/controllers/graphql_controller.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index bf9858a7d..86ea423da 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -1,4 +1,7 @@ class GraphqlController < ApplicationController + + # (!!) Está autorizando todos los resources, no sólo Proposal ¿por qué? + # (!!) Nos da acceso a recursos a los que se supone que no tenemos acceso, cómo 'Geozones', ¿por qué? authorize_resource :proposal def query From 4c121cb9f5d8480bb94fab298e577a82fcc7903f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 6 Oct 2016 11:05:47 +0200 Subject: [PATCH 024/155] Reject deeply-nested queries --- app/graph/consul_schema.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/graph/consul_schema.rb b/app/graph/consul_schema.rb index bf05f48b9..47ef8a112 100644 --- a/app/graph/consul_schema.rb +++ b/app/graph/consul_schema.rb @@ -1,6 +1,9 @@ ConsulSchema = GraphQL::Schema.define do query QueryRoot + # Reject deeply-nested queries + max_depth 7 + resolve_type -> (object, ctx) { # look up types by class name type_name = object.class.name From 56c4dbc2f47446addc1cd1748d8c995b17b07fb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Wed, 12 Oct 2016 17:27:04 +0200 Subject: [PATCH 025/155] Add support for GraphQL pagination --- app/graph/types/commentable_interface.rb | 9 ++++++++- app/graph/types/debate_type.rb | 8 +++++++- app/graph/types/proposal_type.rb | 10 ++++++++-- app/graph/types/query_root.rb | 9 +++------ 4 files changed, 26 insertions(+), 10 deletions(-) diff --git a/app/graph/types/commentable_interface.rb b/app/graph/types/commentable_interface.rb index 1f62b0d09..ad51a55eb 100644 --- a/app/graph/types/commentable_interface.rb +++ b/app/graph/types/commentable_interface.rb @@ -1,6 +1,8 @@ CommentableInterface = GraphQL::InterfaceType.define do name "Commentable" + # Expose fields associated with Commentable models + field :id, !types.ID, "ID of the commentable" field :title, types.String, "The title of this commentable" field :description, types.String, "The description of this commentable" @@ -8,7 +10,12 @@ CommentableInterface = GraphQL::InterfaceType.define do field :comments_count, types.Int, "Number of comments on this commentable" # Linked resources + field :author, UserType, "Author of this commentable" - field :comments, !types[!CommentType], "Comments in this commentable" + + connection :comments, CommentType.connection_type do + description "Comments in this commentable" + end + field :geozone, GeozoneType, "Geozone affected by this commentable" end diff --git a/app/graph/types/debate_type.rb b/app/graph/types/debate_type.rb index 1cae3a2a4..1c36f8b40 100644 --- a/app/graph/types/debate_type.rb +++ b/app/graph/types/debate_type.rb @@ -5,6 +5,7 @@ DebateType = GraphQL::ObjectType.define do interfaces([CommentableInterface]) # Expose fields associated with Debate model + field :id, !types.ID, "The id of this debate" field :title, types.String, "The title of this debate" field :description, types.String, "The description of this debate" @@ -19,7 +20,12 @@ DebateType = GraphQL::ObjectType.define do field :geozone_id, types.Int, "ID of the geozone affected by this debate" # Linked resources + field :author, UserType, "Author of this debate" - field :comments, !types[!CommentType], "Comments in this debate" + + connection :comments, CommentType.connection_type do + description "Comments in this debate" + end + field :geozone, GeozoneType, "Geozone affected by this debate" end diff --git a/app/graph/types/proposal_type.rb b/app/graph/types/proposal_type.rb index 007b5824c..c9d599b8d 100644 --- a/app/graph/types/proposal_type.rb +++ b/app/graph/types/proposal_type.rb @@ -3,8 +3,9 @@ ProposalType = GraphQL::ObjectType.define do description "A single proposal entry returns a proposal with author, total votes and comments" interfaces([CommentableInterface]) - + # Expose fields associated with Proposal model + field :id, !types.ID, "The id of this proposal" field :title, types.String, "The title of this proposal" field :description, types.String, "The description of this proposal" @@ -22,7 +23,12 @@ ProposalType = GraphQL::ObjectType.define do field :retired_explanation, types.String, "Explanation why this proposal was retired" # Linked resources + field :author, UserType, "Author of this proposal" - field :comments, !types[!CommentType], "Comments in this proposal" + + connection :comments, CommentType.connection_type do + description "Comments in this proposal" + end + field :geozone, GeozoneType, "Geozone affected by this proposal" end diff --git a/app/graph/types/query_root.rb b/app/graph/types/query_root.rb index 38f292bb9..007271932 100644 --- a/app/graph/types/query_root.rb +++ b/app/graph/types/query_root.rb @@ -11,8 +11,7 @@ QueryRoot = GraphQL::ObjectType.define do } end - field :proposals do - type !types[!ProposalType] + connection :proposals, ProposalType.connection_type do description "Find all Proposals" resolve -> (object, arguments, context) { Proposal.all @@ -28,8 +27,7 @@ QueryRoot = GraphQL::ObjectType.define do } end - field :debates do - type !types[!DebateType] + connection :debates, DebateType.connection_type do description "Find all Debates" resolve -> (object, arguments, context) { Debate.all @@ -45,8 +43,7 @@ QueryRoot = GraphQL::ObjectType.define do } end - field :comments do - type !types[!CommentType] + connection :comments, CommentType.connection_type do description "Find all Comments" resolve -> (object, arguments, context) { Comment.all From dc62f6c914971263ef72bfd442bbcb2152b36564 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Wed, 12 Oct 2016 17:28:01 +0200 Subject: [PATCH 026/155] Skip authorization check in GraphQL controller --- app/controllers/graphql_controller.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index 86ea423da..665e6b93d 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -1,8 +1,6 @@ class GraphqlController < ApplicationController - # (!!) Está autorizando todos los resources, no sólo Proposal ¿por qué? - # (!!) Nos da acceso a recursos a los que se supone que no tenemos acceso, cómo 'Geozones', ¿por qué? - authorize_resource :proposal + skip_authorization_check def query render json: ConsulSchema.execute( From c96d1c460a2b2e9f2da555dfe4c9bb05389d6350 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Wed, 12 Oct 2016 17:56:24 +0200 Subject: [PATCH 027/155] Ability to query for a specific User --- app/graph/types/query_root.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/graph/types/query_root.rb b/app/graph/types/query_root.rb index 007271932..0c3ed66b9 100644 --- a/app/graph/types/query_root.rb +++ b/app/graph/types/query_root.rb @@ -49,4 +49,14 @@ QueryRoot = GraphQL::ObjectType.define do Comment.all } end + + field :user do + type UserType + description "Find a User by id" + argument :id, !types.ID + resolve -> (object, arguments, context) { + User.find(arguments["id"]) + } + end + end From 757e68969df8fd5443b5c6800ffa263e7efa2f2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Wed, 12 Oct 2016 17:57:56 +0200 Subject: [PATCH 028/155] Ability to access proposals, debates and comments authored by a specific user --- app/graph/types/user_type.rb | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/app/graph/types/user_type.rb b/app/graph/types/user_type.rb index bb64b818f..922f51258 100644 --- a/app/graph/types/user_type.rb +++ b/app/graph/types/user_type.rb @@ -1,7 +1,9 @@ UserType = GraphQL::ObjectType.define do name "User" description "An user entry, returns basic user information" + # Expose fields associated with User model + field :id, !types.ID, "The id of this user" field :created_at, types.String, "Date when this user was created" field :username, types.String, "The username of this user" @@ -10,5 +12,18 @@ UserType = GraphQL::ObjectType.define do field :date_of_birth, types.String, "The birthdate of this user" # Linked resources + field :geozone, GeozoneType, "Geozone where this user is registered" + + connection :proposals, ProposalType.connection_type do + description "Proposals authored by this user" + end + + connection :debates, DebateType.connection_type do + description "Debates authored by this user" + end + + connection :comments, CommentType.connection_type do + description "Comments authored by this user" + end end From e7de5c3a7f0c6e7bcc7b4caea1ada23e2bef02cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Tue, 8 Nov 2016 16:38:51 +0100 Subject: [PATCH 029/155] Deleted old GraphQL type files --- app/graph/types/comment_type.rb | 21 --------------- app/graph/types/commentable_interface.rb | 21 --------------- app/graph/types/debate_type.rb | 31 --------------------- app/graph/types/geozone_type.rb | 10 ------- app/graph/types/proposal_type.rb | 34 ------------------------ app/graph/types/user_type.rb | 29 -------------------- 6 files changed, 146 deletions(-) delete mode 100644 app/graph/types/comment_type.rb delete mode 100644 app/graph/types/commentable_interface.rb delete mode 100644 app/graph/types/debate_type.rb delete mode 100644 app/graph/types/geozone_type.rb delete mode 100644 app/graph/types/proposal_type.rb delete mode 100644 app/graph/types/user_type.rb diff --git a/app/graph/types/comment_type.rb b/app/graph/types/comment_type.rb deleted file mode 100644 index 36a9f709e..000000000 --- a/app/graph/types/comment_type.rb +++ /dev/null @@ -1,21 +0,0 @@ -CommentType = GraphQL::ObjectType.define do - name "Comment" - description "A reply to a proposal, spending proposal, debate or comment" - - field :id, !types.ID, "The unique ID of this comment" - field :commentable_id, types.Int, "ID of the resource where this comment was placed on" - field :commentable_type, types.String, "Type of resource where this comment was placed on" - field :body, types.String, "The body of this comment" - field :subject, types.String, "The subject of this comment" - field :user_id, !types.Int, "The ID of the user who made this comment" - field :created_at, types.String, "The date this comment was posted" - field :updated_at, types.String, "The date this comment was edited" - field :flags_count, types.Int, "The number of flags of this comment" - field :cached_votes_total, types.Int, "The total number of votes of this comment" - field :cached_votes_up, types.Int, "The total number of upvotes of this comment" - field :cached_votes_down, types.Int, "The total number of downvotes of this comment" - - # Linked resources - field :user, !UserType, "User who made this comment" - field :commentable, CommentableInterface, "Element which was commented" -end diff --git a/app/graph/types/commentable_interface.rb b/app/graph/types/commentable_interface.rb deleted file mode 100644 index ad51a55eb..000000000 --- a/app/graph/types/commentable_interface.rb +++ /dev/null @@ -1,21 +0,0 @@ -CommentableInterface = GraphQL::InterfaceType.define do - name "Commentable" - - # Expose fields associated with Commentable models - - field :id, !types.ID, "ID of the commentable" - field :title, types.String, "The title of this commentable" - field :description, types.String, "The description of this commentable" - field :author_id, types.Int, "ID of the author of this commentable" - field :comments_count, types.Int, "Number of comments on this commentable" - - # Linked resources - - field :author, UserType, "Author of this commentable" - - connection :comments, CommentType.connection_type do - description "Comments in this commentable" - end - - field :geozone, GeozoneType, "Geozone affected by this commentable" -end diff --git a/app/graph/types/debate_type.rb b/app/graph/types/debate_type.rb deleted file mode 100644 index 1c36f8b40..000000000 --- a/app/graph/types/debate_type.rb +++ /dev/null @@ -1,31 +0,0 @@ -DebateType = GraphQL::ObjectType.define do - name "Debate" - description "A single debate entry with associated info" - - interfaces([CommentableInterface]) - - # Expose fields associated with Debate model - - field :id, !types.ID, "The id of this debate" - field :title, types.String, "The title of this debate" - field :description, types.String, "The description of this debate" - field :author_id, types.Int, "ID of the author of this proposal" - field :created_at, types.String, "Date when this debate was created" - field :updated_at, types.String, "Date when this debate was edited" - field :flags_count, types.Int, "Number of flags of this debate" - field :cached_votes_total, types.Int, "The total number of votes of this debate" - field :cached_votes_up, types.Int, "The total number of upvotes of this debate" - field :cached_votes_down, types.Int, "The total number of downvotes of this debate" - field :comments_count, types.Int, "Number of comments on this debate" - field :geozone_id, types.Int, "ID of the geozone affected by this debate" - - # Linked resources - - field :author, UserType, "Author of this debate" - - connection :comments, CommentType.connection_type do - description "Comments in this debate" - end - - field :geozone, GeozoneType, "Geozone affected by this debate" -end diff --git a/app/graph/types/geozone_type.rb b/app/graph/types/geozone_type.rb deleted file mode 100644 index bcea6dc93..000000000 --- a/app/graph/types/geozone_type.rb +++ /dev/null @@ -1,10 +0,0 @@ -GeozoneType = GraphQL::ObjectType.define do - name "Geozone" - description "A geozone entry, returns basic geozone information" - # Expose fields associated with Geozone model - field :id, !types.ID, "The id of this geozone" - field :name, types.String, "The name of this geozone" - field :html_map_coordinates, types.String, "HTML map coordinates of this geozone" - field :external_code, types.String, "The external code of this geozone" - field :census_code, types.String, "The census code of this geozone" -end diff --git a/app/graph/types/proposal_type.rb b/app/graph/types/proposal_type.rb deleted file mode 100644 index c9d599b8d..000000000 --- a/app/graph/types/proposal_type.rb +++ /dev/null @@ -1,34 +0,0 @@ -ProposalType = GraphQL::ObjectType.define do - name "Proposal" - description "A single proposal entry returns a proposal with author, total votes and comments" - - interfaces([CommentableInterface]) - - # Expose fields associated with Proposal model - - field :id, !types.ID, "The id of this proposal" - field :title, types.String, "The title of this proposal" - field :description, types.String, "The description of this proposal" - field :question, types.String, "The question of this proposal" - field :external_url, types.String, "External url related to this proposal" - field :author_id, types.Int, "ID of the author of this proposal" - field :flags_count, types.Int, "Number of flags of this proposal" - field :cached_votes_up, types.Int, "Number of upvotes of this proposal" - field :comments_count, types.Int, "Number of comments on this proposal" - field :summary, types.String, "The summary of this proposal" - field :video_url, types.String, "External video url related to this proposal" - field :geozone_id, types.Int, "ID of the geozone affected by this proposal" - field :retired_at, types.String, "Date when this proposal was retired" - field :retired_reason, types.String, "Reason why this proposal was retired" - field :retired_explanation, types.String, "Explanation why this proposal was retired" - - # Linked resources - - field :author, UserType, "Author of this proposal" - - connection :comments, CommentType.connection_type do - description "Comments in this proposal" - end - - field :geozone, GeozoneType, "Geozone affected by this proposal" -end diff --git a/app/graph/types/user_type.rb b/app/graph/types/user_type.rb deleted file mode 100644 index 922f51258..000000000 --- a/app/graph/types/user_type.rb +++ /dev/null @@ -1,29 +0,0 @@ -UserType = GraphQL::ObjectType.define do - name "User" - description "An user entry, returns basic user information" - - # Expose fields associated with User model - - field :id, !types.ID, "The id of this user" - field :created_at, types.String, "Date when this user was created" - field :username, types.String, "The username of this user" - field :geozone_id, types.Int, "The ID of the geozone where this user is active" - field :gender, types.String, "The gender of this user" - field :date_of_birth, types.String, "The birthdate of this user" - - # Linked resources - - field :geozone, GeozoneType, "Geozone where this user is registered" - - connection :proposals, ProposalType.connection_type do - description "Proposals authored by this user" - end - - connection :debates, DebateType.connection_type do - description "Debates authored by this user" - end - - connection :comments, CommentType.connection_type do - description "Comments authored by this user" - end -end From 277c784234ba16da286f4dc87970c392237a3f01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Tue, 8 Nov 2016 17:40:13 +0100 Subject: [PATCH 030/155] First simple API version with autogenerated GraphQL types --- app/graph/exposable_field.rb | 26 +++++++++++++ app/graph/exposable_model.rb | 31 ++++++++++++++++ app/graph/{types => }/query_root.rb | 17 +++++---- app/graph/type_builder.rb | 57 +++++++++++++++++++++++++++++ config/application.rb | 2 +- 5 files changed, 125 insertions(+), 8 deletions(-) create mode 100644 app/graph/exposable_field.rb create mode 100644 app/graph/exposable_model.rb rename app/graph/{types => }/query_root.rb (77%) create mode 100644 app/graph/type_builder.rb diff --git a/app/graph/exposable_field.rb b/app/graph/exposable_field.rb new file mode 100644 index 000000000..2b54083a7 --- /dev/null +++ b/app/graph/exposable_field.rb @@ -0,0 +1,26 @@ +class ExposableField + attr_reader :name, :graphql_type + + def initialize(column, options = {}) + @name = column.name + @graphql_type = ExposableField.convert_type(column.type) + end + + private + + # Return a GraphQL type for 'database_type' + def self.convert_type(database_type) + case database_type + when :integer + GraphQL::INT_TYPE + when :boolean + GraphQL::BOOLEAN_TYPE + when :float + GraphQL::FLOAT_TYPE + when :double + GraphQL::FLOAT_TYPE + else + GraphQL::STRING_TYPE + end + end +end diff --git a/app/graph/exposable_model.rb b/app/graph/exposable_model.rb new file mode 100644 index 000000000..9e4b228ad --- /dev/null +++ b/app/graph/exposable_model.rb @@ -0,0 +1,31 @@ +class ExposableModel + attr_reader :exposed_fields, :name, :description + + def initialize(model_class, options = {}) + @model_class = model_class + @filter_list = options[:filter_list] || [] + @name = model_class.name + @description = model_class.model_name.human + @filter_strategy = options[:filter_strategy] + set_exposed_fields + end + + private + + # determine which model fields are exposed to the API + def set_exposed_fields + case @filter_strategy + when :whitelist + @exposed_fields = @model_class.columns.select do |column| + @filter_list.include? column.name + end + when :blacklist + @exposed_fields = @model_class.columns.select do |column| + !(@filter_list.include? column.name) + end + else + @exposed_fields = [] + end + end + +end diff --git a/app/graph/types/query_root.rb b/app/graph/query_root.rb similarity index 77% rename from app/graph/types/query_root.rb rename to app/graph/query_root.rb index 0c3ed66b9..928dceb6a 100644 --- a/app/graph/types/query_root.rb +++ b/app/graph/query_root.rb @@ -3,7 +3,7 @@ QueryRoot = GraphQL::ObjectType.define do description "The query root for this schema" field :proposal do - type ProposalType + type TYPE_BUILDER.types[Proposal] description "Find a Proposal by id" argument :id, !types.ID resolve -> (object, arguments, context) { @@ -11,7 +11,8 @@ QueryRoot = GraphQL::ObjectType.define do } end - connection :proposals, ProposalType.connection_type do + field :proposals do + type types[TYPE_BUILDER.types[Proposal]] description "Find all Proposals" resolve -> (object, arguments, context) { Proposal.all @@ -19,7 +20,7 @@ QueryRoot = GraphQL::ObjectType.define do end field :debate do - type DebateType + type TYPE_BUILDER.types[Debate] description "Find a Debate by id" argument :id, !types.ID resolve -> (object, arguments, context) { @@ -27,7 +28,8 @@ QueryRoot = GraphQL::ObjectType.define do } end - connection :debates, DebateType.connection_type do + field :debates do + type types[TYPE_BUILDER.types[Debate]] description "Find all Debates" resolve -> (object, arguments, context) { Debate.all @@ -35,7 +37,7 @@ QueryRoot = GraphQL::ObjectType.define do end field :comment do - type CommentType + type TYPE_BUILDER.types[Comment] description "Find a Comment by id" argument :id, !types.ID resolve -> (object, arguments, context) { @@ -43,7 +45,8 @@ QueryRoot = GraphQL::ObjectType.define do } end - connection :comments, CommentType.connection_type do + field :comments do + type types[TYPE_BUILDER.types[Comment]] description "Find all Comments" resolve -> (object, arguments, context) { Comment.all @@ -51,7 +54,7 @@ QueryRoot = GraphQL::ObjectType.define do end field :user do - type UserType + type TYPE_BUILDER.types[User] description "Find a User by id" argument :id, !types.ID resolve -> (object, arguments, context) { diff --git a/app/graph/type_builder.rb b/app/graph/type_builder.rb new file mode 100644 index 000000000..7de5bfd23 --- /dev/null +++ b/app/graph/type_builder.rb @@ -0,0 +1,57 @@ +class TypeBuilder + attr_reader :filter_strategy, :graphql_models + + def initialize(graphql_models, options = {}) + @graphql_models = graphql_models + @graphql_types = {} + + # determine filter strategy for this field + if (options[:filter_strategy] == :blacklist) + @filter_strategy = :blacklist + else + @filter_strategy = :whitelist + end + + create_all_types + end + + def types + @graphql_types + end + +private + + def create_all_types + @graphql_models.keys.each do |model_class| + @graphql_types[model_class] = create_type(model_class) + end + end + + def create_type(model_class) + type_builder = self + + graphql_type = GraphQL::ObjectType.define do + em = ExposableModel.new(model_class, filter_strategy: type_builder.filter_strategy, filter_list: type_builder.graphql_models[model_class]) + + name(em.name) + description(em.description) + + em.exposed_fields.each do |column| + ef = ExposableField.new(column) + field(ef.name, ef.graphql_type) + end + end + + return graphql_type + end +end + +graphql_models = {} + +graphql_models.store(User, ['id', 'username']) +graphql_models.store(Proposal, ['id', 'title', 'description', 'author_id', 'created_at']) +graphql_models.store(Debate, ['id', 'title', 'description', 'author_id', 'created_at']) +graphql_models.store(Comment, ['id', 'commentable_id', 'commentable_type', 'body']) +graphql_models.store(Geozone, ['id', 'name', 'html_map_coordinates']) + +TYPE_BUILDER = TypeBuilder.new(graphql_models, filter_strategy: :whitelist) diff --git a/config/application.rb b/config/application.rb index 3182e319c..623afc4f1 100644 --- a/config/application.rb +++ b/config/application.rb @@ -46,7 +46,7 @@ module Consul config.paths['app/views'].unshift(Rails.root.join('app', 'views', 'custom')) # Add GraphQL directories to the autoload path - config.autoload_paths << Rails.root.join('app', 'graph', 'types') + config.autoload_paths << Rails.root.join('app', 'graph') end end From 49317e2c110736db802d5a5c513c3438d723f789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Tue, 8 Nov 2016 22:19:16 +0100 Subject: [PATCH 031/155] Added support for associations (WITHOUT PAGINATION) --- app/graph/exposable_association.rb | 8 ++++++ app/graph/exposable_model.rb | 27 +++++++++++------- app/graph/type_builder.rb | 46 +++++++++++++++++++++++++----- 3 files changed, 63 insertions(+), 18 deletions(-) create mode 100644 app/graph/exposable_association.rb diff --git a/app/graph/exposable_association.rb b/app/graph/exposable_association.rb new file mode 100644 index 000000000..78e650b0a --- /dev/null +++ b/app/graph/exposable_association.rb @@ -0,0 +1,8 @@ +class ExposableAssociation + attr_reader :name, :type + + def initialize(association) + @name = association.name + @type = association.macro # :has_one, :belongs_to or :has_many + end +end diff --git a/app/graph/exposable_model.rb b/app/graph/exposable_model.rb index 9e4b228ad..22976c63d 100644 --- a/app/graph/exposable_model.rb +++ b/app/graph/exposable_model.rb @@ -1,30 +1,35 @@ class ExposableModel - attr_reader :exposed_fields, :name, :description + attr_reader :name, :description, :exposed_fields, :exposed_associations def initialize(model_class, options = {}) - @model_class = model_class - @filter_list = options[:filter_list] || [] @name = model_class.name @description = model_class.model_name.human + @field_filter_list = options[:field_filter_list] || [] + @assoc_filter_list = options[:assoc_filter_list] || [] @filter_strategy = options[:filter_strategy] - set_exposed_fields + set_exposed_items(model_class) end private - # determine which model fields are exposed to the API - def set_exposed_fields + # determine which model fields and associations are exposed to the API + def set_exposed_items(model_class) + @exposed_fields = check_against_safety_list(model_class.columns, @field_filter_list) + @exposed_associations = check_against_safety_list(model_class.reflect_on_all_associations, @assoc_filter_list) + end + + def check_against_safety_list(all_items, safety_list) case @filter_strategy when :whitelist - @exposed_fields = @model_class.columns.select do |column| - @filter_list.include? column.name + exposed_items = all_items.select do |column| + safety_list.include? column.name.to_s # works for both symbols and strings end when :blacklist - @exposed_fields = @model_class.columns.select do |column| - !(@filter_list.include? column.name) + exposed_items = all_items.select do |column| + !(safety_list.include? column.name.to_s) end else - @exposed_fields = [] + exposed_items = [] end end diff --git a/app/graph/type_builder.rb b/app/graph/type_builder.rb index 7de5bfd23..1277f8746 100644 --- a/app/graph/type_builder.rb +++ b/app/graph/type_builder.rb @@ -1,5 +1,6 @@ class TypeBuilder attr_reader :filter_strategy, :graphql_models + attr_accessor :graphql_types # contains all generated GraphQL types def initialize(graphql_models, options = {}) @graphql_models = graphql_models @@ -31,14 +32,30 @@ private type_builder = self graphql_type = GraphQL::ObjectType.define do - em = ExposableModel.new(model_class, filter_strategy: type_builder.filter_strategy, filter_list: type_builder.graphql_models[model_class]) + em = ExposableModel.new( + model_class, + filter_strategy: type_builder.filter_strategy, + field_filter_list: type_builder.graphql_models[model_class][:fields], + assoc_filter_list: type_builder.graphql_models[model_class][:associations] + ) name(em.name) description(em.description) em.exposed_fields.each do |column| ef = ExposableField.new(column) - field(ef.name, ef.graphql_type) + field(ef.name, ef.graphql_type) # returns a GraphQL::Field + end + + em.exposed_associations.each do |association| + ea = ExposableAssociation.new(association) + if ea.type.in? [:has_one, :belongs_to] + field(ea.name, -> { type_builder.graphql_types[association.klass] }) + elsif ea.type.in? [:has_many] + field(ea.name, -> { + types[type_builder.graphql_types[association.klass]] + }) + end end end @@ -48,10 +65,25 @@ end graphql_models = {} -graphql_models.store(User, ['id', 'username']) -graphql_models.store(Proposal, ['id', 'title', 'description', 'author_id', 'created_at']) -graphql_models.store(Debate, ['id', 'title', 'description', 'author_id', 'created_at']) -graphql_models.store(Comment, ['id', 'commentable_id', 'commentable_type', 'body']) -graphql_models.store(Geozone, ['id', 'name', 'html_map_coordinates']) +graphql_models.store(User, { + fields: ['id', 'username'], + associations: ['proposals', 'debates'] +}) +graphql_models.store(Proposal, { + fields: ['id', 'title', 'description', 'author_id', 'created_at'], + associations: ['author'] +}) +graphql_models.store(Debate, { + fields: ['id', 'title', 'description', 'author_id', 'created_at'], + associations: ['author'] +}) +graphql_models.store(Comment, { + fields: ['id', 'commentable_id', 'commentable_type', 'body'], + associations: ['author'] +}) +graphql_models.store(Geozone, { + fields: ['id', 'name', 'html_map_coordinates'], + associations: [] +}) TYPE_BUILDER = TypeBuilder.new(graphql_models, filter_strategy: :whitelist) From 59a355df1b17df6a8ca05776a961b2bd14514884 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 10 Nov 2016 20:53:15 +0100 Subject: [PATCH 032/155] First working version of new GraphQL::TypeCreator after major refactoring --- app/graph/consul_schema.rb | 12 ---- app/graph/exposable_association.rb | 8 --- app/graph/exposable_field.rb | 26 --------- app/graph/exposable_model.rb | 36 ------------ app/graph/query_root.rb | 65 ---------------------- app/graph/type_builder.rb | 89 ------------------------------ config/application.rb | 3 - config/initializers/graphql.rb | 46 +++++++++++++++ lib/graph_ql/type_creator.rb | 42 ++++++++++++++ 9 files changed, 88 insertions(+), 239 deletions(-) delete mode 100644 app/graph/consul_schema.rb delete mode 100644 app/graph/exposable_association.rb delete mode 100644 app/graph/exposable_field.rb delete mode 100644 app/graph/exposable_model.rb delete mode 100644 app/graph/query_root.rb delete mode 100644 app/graph/type_builder.rb create mode 100644 config/initializers/graphql.rb create mode 100644 lib/graph_ql/type_creator.rb diff --git a/app/graph/consul_schema.rb b/app/graph/consul_schema.rb deleted file mode 100644 index 47ef8a112..000000000 --- a/app/graph/consul_schema.rb +++ /dev/null @@ -1,12 +0,0 @@ -ConsulSchema = GraphQL::Schema.define do - query QueryRoot - - # Reject deeply-nested queries - max_depth 7 - - resolve_type -> (object, ctx) { - # look up types by class name - type_name = object.class.name - ConsulSchema.types[type_name] - } -end diff --git a/app/graph/exposable_association.rb b/app/graph/exposable_association.rb deleted file mode 100644 index 78e650b0a..000000000 --- a/app/graph/exposable_association.rb +++ /dev/null @@ -1,8 +0,0 @@ -class ExposableAssociation - attr_reader :name, :type - - def initialize(association) - @name = association.name - @type = association.macro # :has_one, :belongs_to or :has_many - end -end diff --git a/app/graph/exposable_field.rb b/app/graph/exposable_field.rb deleted file mode 100644 index 2b54083a7..000000000 --- a/app/graph/exposable_field.rb +++ /dev/null @@ -1,26 +0,0 @@ -class ExposableField - attr_reader :name, :graphql_type - - def initialize(column, options = {}) - @name = column.name - @graphql_type = ExposableField.convert_type(column.type) - end - - private - - # Return a GraphQL type for 'database_type' - def self.convert_type(database_type) - case database_type - when :integer - GraphQL::INT_TYPE - when :boolean - GraphQL::BOOLEAN_TYPE - when :float - GraphQL::FLOAT_TYPE - when :double - GraphQL::FLOAT_TYPE - else - GraphQL::STRING_TYPE - end - end -end diff --git a/app/graph/exposable_model.rb b/app/graph/exposable_model.rb deleted file mode 100644 index 22976c63d..000000000 --- a/app/graph/exposable_model.rb +++ /dev/null @@ -1,36 +0,0 @@ -class ExposableModel - attr_reader :name, :description, :exposed_fields, :exposed_associations - - def initialize(model_class, options = {}) - @name = model_class.name - @description = model_class.model_name.human - @field_filter_list = options[:field_filter_list] || [] - @assoc_filter_list = options[:assoc_filter_list] || [] - @filter_strategy = options[:filter_strategy] - set_exposed_items(model_class) - end - - private - - # determine which model fields and associations are exposed to the API - def set_exposed_items(model_class) - @exposed_fields = check_against_safety_list(model_class.columns, @field_filter_list) - @exposed_associations = check_against_safety_list(model_class.reflect_on_all_associations, @assoc_filter_list) - end - - def check_against_safety_list(all_items, safety_list) - case @filter_strategy - when :whitelist - exposed_items = all_items.select do |column| - safety_list.include? column.name.to_s # works for both symbols and strings - end - when :blacklist - exposed_items = all_items.select do |column| - !(safety_list.include? column.name.to_s) - end - else - exposed_items = [] - end - end - -end diff --git a/app/graph/query_root.rb b/app/graph/query_root.rb deleted file mode 100644 index 928dceb6a..000000000 --- a/app/graph/query_root.rb +++ /dev/null @@ -1,65 +0,0 @@ -QueryRoot = GraphQL::ObjectType.define do - name "Query" - description "The query root for this schema" - - field :proposal do - type TYPE_BUILDER.types[Proposal] - description "Find a Proposal by id" - argument :id, !types.ID - resolve -> (object, arguments, context) { - Proposal.find(arguments["id"]) - } - end - - field :proposals do - type types[TYPE_BUILDER.types[Proposal]] - description "Find all Proposals" - resolve -> (object, arguments, context) { - Proposal.all - } - end - - field :debate do - type TYPE_BUILDER.types[Debate] - description "Find a Debate by id" - argument :id, !types.ID - resolve -> (object, arguments, context) { - Debate.find(arguments["id"]) - } - end - - field :debates do - type types[TYPE_BUILDER.types[Debate]] - description "Find all Debates" - resolve -> (object, arguments, context) { - Debate.all - } - end - - field :comment do - type TYPE_BUILDER.types[Comment] - description "Find a Comment by id" - argument :id, !types.ID - resolve -> (object, arguments, context) { - Comment.find(arguments["id"]) - } - end - - field :comments do - type types[TYPE_BUILDER.types[Comment]] - description "Find all Comments" - resolve -> (object, arguments, context) { - Comment.all - } - end - - field :user do - type TYPE_BUILDER.types[User] - description "Find a User by id" - argument :id, !types.ID - resolve -> (object, arguments, context) { - User.find(arguments["id"]) - } - end - -end diff --git a/app/graph/type_builder.rb b/app/graph/type_builder.rb deleted file mode 100644 index 1277f8746..000000000 --- a/app/graph/type_builder.rb +++ /dev/null @@ -1,89 +0,0 @@ -class TypeBuilder - attr_reader :filter_strategy, :graphql_models - attr_accessor :graphql_types # contains all generated GraphQL types - - def initialize(graphql_models, options = {}) - @graphql_models = graphql_models - @graphql_types = {} - - # determine filter strategy for this field - if (options[:filter_strategy] == :blacklist) - @filter_strategy = :blacklist - else - @filter_strategy = :whitelist - end - - create_all_types - end - - def types - @graphql_types - end - -private - - def create_all_types - @graphql_models.keys.each do |model_class| - @graphql_types[model_class] = create_type(model_class) - end - end - - def create_type(model_class) - type_builder = self - - graphql_type = GraphQL::ObjectType.define do - em = ExposableModel.new( - model_class, - filter_strategy: type_builder.filter_strategy, - field_filter_list: type_builder.graphql_models[model_class][:fields], - assoc_filter_list: type_builder.graphql_models[model_class][:associations] - ) - - name(em.name) - description(em.description) - - em.exposed_fields.each do |column| - ef = ExposableField.new(column) - field(ef.name, ef.graphql_type) # returns a GraphQL::Field - end - - em.exposed_associations.each do |association| - ea = ExposableAssociation.new(association) - if ea.type.in? [:has_one, :belongs_to] - field(ea.name, -> { type_builder.graphql_types[association.klass] }) - elsif ea.type.in? [:has_many] - field(ea.name, -> { - types[type_builder.graphql_types[association.klass]] - }) - end - end - end - - return graphql_type - end -end - -graphql_models = {} - -graphql_models.store(User, { - fields: ['id', 'username'], - associations: ['proposals', 'debates'] -}) -graphql_models.store(Proposal, { - fields: ['id', 'title', 'description', 'author_id', 'created_at'], - associations: ['author'] -}) -graphql_models.store(Debate, { - fields: ['id', 'title', 'description', 'author_id', 'created_at'], - associations: ['author'] -}) -graphql_models.store(Comment, { - fields: ['id', 'commentable_id', 'commentable_type', 'body'], - associations: ['author'] -}) -graphql_models.store(Geozone, { - fields: ['id', 'name', 'html_map_coordinates'], - associations: [] -}) - -TYPE_BUILDER = TypeBuilder.new(graphql_models, filter_strategy: :whitelist) diff --git a/config/application.rb b/config/application.rb index 623afc4f1..a3d048fef 100644 --- a/config/application.rb +++ b/config/application.rb @@ -44,9 +44,6 @@ module Consul config.autoload_paths << "#{Rails.root}/app/controllers/custom" config.autoload_paths << "#{Rails.root}/app/models/custom" config.paths['app/views'].unshift(Rails.root.join('app', 'views', 'custom')) - - # Add GraphQL directories to the autoload path - config.autoload_paths << Rails.root.join('app', 'graph') end end diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb new file mode 100644 index 000000000..7dec74d12 --- /dev/null +++ b/config/initializers/graphql.rb @@ -0,0 +1,46 @@ +API_TYPE_DEFINITIONS = { + User => %I[ id username ], + Proposal => %I[ id title description author_id author created_at ] +} + +api_types = {} + +API_TYPE_DEFINITIONS.each do |model, fields| + api_types[model] = GraphQL::TypeCreator.create(model, fields, api_types) +end + +ConsulSchema = GraphQL::Schema.define do + query QueryRoot + + # Reject deeply-nested queries + max_depth 7 + + resolve_type -> (object, ctx) { + # look up types by class name + type_name = object.class.name + ConsulSchema.types[type_name] + } +end + +QueryRoot = GraphQL::ObjectType.define do + name "Query" + description "The query root for this schema" + + field :proposal do + type api_types[Proposal] + description "Find a Proposal by id" + argument :id, !types.ID + resolve -> (object, arguments, context) { + Proposal.find(arguments["id"]) + } + end + + field :proposals do + type types[api_types[Proposal]] + description "Find all Proposals" + resolve -> (object, arguments, context) { + Proposal.all + } + end + +end diff --git a/lib/graph_ql/type_creator.rb b/lib/graph_ql/type_creator.rb new file mode 100644 index 000000000..44a542ed2 --- /dev/null +++ b/lib/graph_ql/type_creator.rb @@ -0,0 +1,42 @@ +module GraphQL + class TypeCreator + + # Return a GraphQL type for a 'database_type' + TYPES_CONVERSION = Hash.new(GraphQL::STRING_TYPE).merge( + integer: GraphQL::INT_TYPE, + boolean: GraphQL::BOOLEAN_TYPE, + float: GraphQL::FLOAT_TYPE, + double: GraphQL::FLOAT_TYPE + ) + + def self.create(model, field_names, api_types) + + new_graphql_type = GraphQL::ObjectType.define do + + name(model.name) + description("Generated programmatically from model: #{model.name}") + + # Make a field for each column + field_names.each do |field_name| + if model.column_names.include?(field_name.to_s) + field(field_name.to_s, TYPES_CONVERSION[field_name]) + else + association = model.reflect_on_all_associations.find { |a| a.name == field_name } + case association.macro + when :has_one + field(association.name, -> { api_types[association.klass] }) + when :belongs_to + field(association.name, -> { api_types[association.klass] }) + when :has_many + connection(association.name, api_types[association.klass].connection_type { + description "Description for a field with pagination" + }) + end + end + end + end + + return new_graphql_type + end + end +end From dd4b29898cb9e557fb568701833f95343be9524f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 10 Nov 2016 22:38:22 +0100 Subject: [PATCH 033/155] Ran forgotten migration added by my last pull from consul/master --- db/schema.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/schema.rb b/db/schema.rb index a93942873..48e591bf7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -464,7 +464,7 @@ ActiveRecord::Schema.define(version: 20161102133838) 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-02 13:51:14', null: false + t.datetime "password_changed_at", default: '2016-11-10 21:34:46', null: false end add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree From 0a23533a02186078aa88923c0c274e941c0f74f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 11 Nov 2016 12:51:45 +0100 Subject: [PATCH 034/155] Refactor creation of the QueryRoot Also incremented the max_depth for nested queries since the use of pagination increases the needed depth to retrieve useful data (edges, nodes, etc.) --- config/initializers/graphql.rb | 39 +++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index 7dec74d12..188d515dd 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -1,5 +1,6 @@ API_TYPE_DEFINITIONS = { - User => %I[ id username ], + User => %I[ id username proposals ], + Debate => %I[ id title description author_id author created_at ], Proposal => %I[ id title description author_id author created_at ] } @@ -13,7 +14,7 @@ ConsulSchema = GraphQL::Schema.define do query QueryRoot # Reject deeply-nested queries - max_depth 7 + max_depth 10 resolve_type -> (object, ctx) { # look up types by class name @@ -26,21 +27,25 @@ QueryRoot = GraphQL::ObjectType.define do name "Query" description "The query root for this schema" - field :proposal do - type api_types[Proposal] - description "Find a Proposal by id" - argument :id, !types.ID - resolve -> (object, arguments, context) { - Proposal.find(arguments["id"]) - } - end + API_TYPE_DEFINITIONS.each_key do |model| - field :proposals do - type types[api_types[Proposal]] - description "Find all Proposals" - resolve -> (object, arguments, context) { - Proposal.all - } - end + # create an entry field to retrive a single object + field model.name.underscore.to_sym do + type api_types[model] + description "Find one #{model.model_name.human} by ID" + argument :id, !types.ID + resolve -> (object, arguments, context) { + model.find(arguments["id"]) + } + end + # create an entry filed to retrive a paginated collection + connection model.name.underscore.pluralize.to_sym, api_types[model].connection_type do + description "Find all #{model.model_name.human.pluralize}" + resolve -> (object, arguments, context) { + model.all + } + end + + end end From fc2d4fda5b682bc58019d78f8bb927702a06aba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 11 Nov 2016 12:52:54 +0100 Subject: [PATCH 035/155] Fixed bug in TypeCreator and improved field descriptions --- lib/graph_ql/type_creator.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/graph_ql/type_creator.rb b/lib/graph_ql/type_creator.rb index 44a542ed2..ae5c65aee 100644 --- a/lib/graph_ql/type_creator.rb +++ b/lib/graph_ql/type_creator.rb @@ -14,7 +14,7 @@ module GraphQL new_graphql_type = GraphQL::ObjectType.define do name(model.name) - description("Generated programmatically from model: #{model.name}") + description("#{model.model_name.human}") # Make a field for each column field_names.each do |field_name| @@ -29,7 +29,10 @@ module GraphQL field(association.name, -> { api_types[association.klass] }) when :has_many connection(association.name, api_types[association.klass].connection_type { - description "Description for a field with pagination" + description "#{association.klass.model_name.human.pluralize}" + resolve -> (object, arguments, context) { + association.klass.all + } }) end end From f6cd7a345651388b54912e993d067a36301f507f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Sun, 13 Nov 2016 20:07:09 +0100 Subject: [PATCH 036/155] Disable CSRF protection for GraphQL controller Related documentation at: http://api.rubyonrails.org/classes/ActionController/RequestForgeryProtec tion/ClassMethods.html --- app/controllers/graphql_controller.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index 665e6b93d..f1f4795ae 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -1,5 +1,6 @@ class GraphqlController < ApplicationController + skip_before_action :verify_authenticity_token skip_authorization_check def query From 195d1c5f690525b59c77a27ce10eb222ed788fe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 14 Nov 2016 15:11:41 +0100 Subject: [PATCH 037/155] Fixed bug in GraphQL::TypeCreator --- lib/graph_ql/type_creator.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/graph_ql/type_creator.rb b/lib/graph_ql/type_creator.rb index ae5c65aee..c2c03046e 100644 --- a/lib/graph_ql/type_creator.rb +++ b/lib/graph_ql/type_creator.rb @@ -19,7 +19,7 @@ module GraphQL # Make a field for each column field_names.each do |field_name| if model.column_names.include?(field_name.to_s) - field(field_name.to_s, TYPES_CONVERSION[field_name]) + field(field_name.to_s, TYPES_CONVERSION[model.columns_hash[field_name.to_s].type]) else association = model.reflect_on_all_associations.find { |a| a.name == field_name } case association.macro From b10b7013193ed409e0d60632dcee63b938d8c5fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 14 Nov 2016 16:08:56 +0100 Subject: [PATCH 038/155] Started GraphQL::TypeCreator testing --- config/initializers/graphql.rb | 3 +- spec/lib/graphql_spec.rb | 68 ++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 spec/lib/graphql_spec.rb diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index 188d515dd..843fe2c00 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -1,7 +1,8 @@ API_TYPE_DEFINITIONS = { User => %I[ id username proposals ], Debate => %I[ id title description author_id author created_at ], - Proposal => %I[ id title description author_id author created_at ] + Proposal => %I[ id title description author_id author created_at ], + Comment => %I[ id body author_id author commentable_id commentable] } api_types = {} diff --git a/spec/lib/graphql_spec.rb b/spec/lib/graphql_spec.rb new file mode 100644 index 000000000..2369df348 --- /dev/null +++ b/spec/lib/graphql_spec.rb @@ -0,0 +1,68 @@ +require 'rails_helper' + +describe GraphQL::TypeCreator do + let!(:api_types) { {} } + let!(:user_type) { GraphQL::TypeCreator.create(User, %I[ id ], api_types) } + let!(:comment_type) { GraphQL::TypeCreator.create(Comment, %I[ id ], api_types) } + let!(:debate_type) { GraphQL::TypeCreator.create(Debate, %I[ id title author ], api_types) } + # TODO: no puedo añadir los comentarios a la field_list de Debate porque como + # las conexiones se crean de forma lazy creo que provoca que falle la creación + # del resto de tipos y provoca que fallen todos los tests. + # let!(:debate_type) { GraphQL::TypeCreator.create(Debate, %I[ id title author comments ], api_types) } + + describe "::create" do + describe "creates fields" do + it "for int attributes" do + 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 "for string attributes" do + 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 + end + + describe "creates connections for" do + it ":belongs_to associations" do + connection = debate_type.fields['author'] + + # TODO: because connection types are created and added lazily to the + # api_types hash (with that proc thing ->) I don't really know how to + # test this. + # connection.class shows GraphQL::Field + # connection.inspect shows some weird info + + expect(connection).to be_a(GraphQL::Field) + # expect(connection.type).to be_a(api_types[User]) + expect(connection.name).to eq('author') + end + + it ":has_one associations" do + skip "need to find association example that uses :has_one" + end + + it ":has_many associations" do + skip "still don't know how to handle relay connections inside RSpec" + + connection = debate_type.fields['comments'] + + # TODO: because connection types are created and added lazily to the + # api_types hash (with that proc thing ->) I don't really know how to + # test this. + # connection.class shows GraphQL::Field + # connection.inspect shows some weird info + + expect(connection).to be_a(GraphQL::Field) + # expect(created_connection.type).to be_a(api_types[Comment]) + expect(connection.name).to eq('comments') + end + end + end +end From 36bdad8851b38e8c0770939b3a599aa9814ce68a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 17 Nov 2016 17:23:27 +0100 Subject: [PATCH 039/155] Fixed bug in GraphQL::TypeCreator --- lib/graph_ql/type_creator.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/graph_ql/type_creator.rb b/lib/graph_ql/type_creator.rb index c2c03046e..848d92704 100644 --- a/lib/graph_ql/type_creator.rb +++ b/lib/graph_ql/type_creator.rb @@ -28,12 +28,12 @@ module GraphQL when :belongs_to field(association.name, -> { api_types[association.klass] }) when :has_many - connection(association.name, api_types[association.klass].connection_type { + connection(association.name, -> { api_types[association.klass].connection_type { description "#{association.klass.model_name.human.pluralize}" resolve -> (object, arguments, context) { association.klass.all } - }) + }}) end end end From 6fca1f02feb48f148e3ba3156ad43a90b085d5c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 17 Nov 2016 17:23:57 +0100 Subject: [PATCH 040/155] Added clarifying comments related to GraphQL return types --- app/controllers/graphql_controller.rb | 2 ++ lib/graph_ql/type_creator.rb | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index f1f4795ae..e3bedceea 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -4,6 +4,8 @@ class GraphqlController < ApplicationController skip_authorization_check def query + # ConsulSchema.execute returns the query result in the shape of a Hash, which + # is sent back to the client rendered in JSON render json: ConsulSchema.execute( params[:query], variables: params[:variables] || {} diff --git a/lib/graph_ql/type_creator.rb b/lib/graph_ql/type_creator.rb index 848d92704..5a831fd7d 100644 --- a/lib/graph_ql/type_creator.rb +++ b/lib/graph_ql/type_creator.rb @@ -38,8 +38,7 @@ module GraphQL end end end - - return new_graphql_type + return new_graphql_type # GraphQL::ObjectType end end end From fc61d63affe3dc4a7095fc84976347899eed500e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 17 Nov 2016 17:25:27 +0100 Subject: [PATCH 041/155] Fixed bug for the Comment element inside the API_TYPE_DEFINITIONS --- config/initializers/graphql.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index 843fe2c00..06b39aaab 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -2,7 +2,7 @@ API_TYPE_DEFINITIONS = { User => %I[ id username proposals ], Debate => %I[ id title description author_id author created_at ], Proposal => %I[ id title description author_id author created_at ], - Comment => %I[ id body author_id author commentable_id commentable] + Comment => %I[ id body user_id user commentable_id ] } api_types = {} From 90d5034174f320343db986ab11a0e991d25c5f19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 17 Nov 2016 17:47:09 +0100 Subject: [PATCH 042/155] Half-made tests for GraphqlController --- spec/controllers/graphql_controller_spec.rb | 62 +++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 spec/controllers/graphql_controller_spec.rb diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb new file mode 100644 index 000000000..8afc8e60b --- /dev/null +++ b/spec/controllers/graphql_controller_spec.rb @@ -0,0 +1,62 @@ +require 'rails_helper' + +describe GraphqlController do + let!(:uri) { URI::HTTP.build(host: 'localhost', path: '/queries', port: 3000) } + + describe "GET request" do + it "is accepted when valid" do + # Like POST requests but the query string goes in the URL + # More info at: http://graphql.org/learn/serving-over-http/#get-request + skip + end + + it "is rejected when not valid" do + skip + end + end + + describe "POST request" do + let(:post_request) { + req = Net::HTTP::Post.new(uri) + req['Content-Type'] = 'application/json' + req + } + + it "succeeds when valid" do + body = { query: "{ proposals(first: 2) { edges { node { id } } } }" }.to_json + response = Net::HTTP.start(uri.host, uri.port) do |http| + post_request.body = body + http.request(post_request) + end + + # Is it enough to check the status code or should I also check the body? + expect(response.code.to_i).to eq(200) + end + + it "succeeds and returns an error when disclosed attributes are requested" do + body = { query: "{ user(id: 1) { encrypted_password } }" }.to_json + response = Net::HTTP.start(uri.host, uri.port) do |http| + post_request.body = body + http.request(post_request) + end + + body_hash = JSON.parse(response.body) + expect(body_hash['errors']).to be_present + end + + it "fails when no query string is provided" do + body = {}.to_json + response = Net::HTTP.start(uri.host, uri.port) do |http| + post_request.body = body + http.request(post_request) + end + + # TODO: I must find a way to handle this better. Right now it shows a 500 + # Internal Server Error, I think I should always return a valid (but empty) + # JSON document like '{}' + expect(response.code.to_i).not_to eq(200) + end + + end + +end From 96bfbbf9af55bca7ed909c5be7ade11dc137cad3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 17 Nov 2016 17:48:03 +0100 Subject: [PATCH 043/155] Half-made tests for GraphQL::TypeCreator --- spec/lib/{graphql_spec.rb => type_creator_spec.rb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename spec/lib/{graphql_spec.rb => type_creator_spec.rb} (100%) diff --git a/spec/lib/graphql_spec.rb b/spec/lib/type_creator_spec.rb similarity index 100% rename from spec/lib/graphql_spec.rb rename to spec/lib/type_creator_spec.rb From 4e2a003931a48bdbae907115f0d76e851bd2dc44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 17 Nov 2016 17:54:02 +0100 Subject: [PATCH 044/155] Half-made tests for ConsulSchema --- spec/lib/graphql_spec.rb | 56 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 spec/lib/graphql_spec.rb diff --git a/spec/lib/graphql_spec.rb b/spec/lib/graphql_spec.rb new file mode 100644 index 000000000..5f7ab34bb --- /dev/null +++ b/spec/lib/graphql_spec.rb @@ -0,0 +1,56 @@ +require 'rails_helper' + +describe ConsulSchema do + let(:context) { {} } # should be overriden for specific queries + let(:variables) { {} } + let(:result) { ConsulSchema.execute(query_string, context: context, variables: variables) } + + describe "answers simple queries" do + let(:query_string) { "{ proposals { edges { node { title } } } }" } + let(:context) { + { proposal: create(:proposal, title: "A new proposal") } + } + + it "can return proposal titles" do + proposals = result["data"]["proposals"]["edges"].collect { |edge| edge['node'] } + expect(proposals.size).to eq(1) + expect(proposals.first['title']).to eq("A new proposal") + end + end + + describe "queries with nested associations" do + let(:query_string) { "{ proposals { edges { node { id, title, author { username } } } } }" } + + let(:mrajoy) { create(:user, username: 'mrajoy') } + let(:dtrump) { create(:user, username: 'dtrump') } + let(:context) { + { proposal_1: create(:proposal, id: 1, title: "Bajar el IVA", author: mrajoy) } + { proposal_2: create(:proposal, id: 2, title: "Censurar los memes", author: mrajoy) } + { proposal_3: create(:proposal, id: 3, title: "Construir un muro", author: dtrump) } + } + + it "returns the appropiate fields" do + proposals = result["data"]["proposals"]["edges"].collect { |edge| edge['node'] } + + expected_result = [ + { + 'id' => 1, + 'title' => 'Bajar el IVA', + 'author' => { 'username' => 'mrajoy' } + }, + { + 'id' => 2, + 'title' => 'Censurar los memes', + 'author' => { 'username' => 'mrajoy' } + }, + { + 'id' => 3, + 'title' => 'Construir un muro', + 'author' => { 'username' => 'dtrump' } + } + ] + + expect(proposals).to match_array(expected_result) + end + end +end From 700fcaab168255853277936fa2d96f0c70610bd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 18 Nov 2016 16:24:41 +0100 Subject: [PATCH 045/155] Expose proposal/debate comments in the API so I can test :has_many associations --- config/initializers/graphql.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index 06b39aaab..1f0c8968d 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -1,7 +1,7 @@ API_TYPE_DEFINITIONS = { User => %I[ id username proposals ], - Debate => %I[ id title description author_id author created_at ], - Proposal => %I[ id title description author_id author created_at ], + Debate => %I[ id title description author_id author created_at comments ], + Proposal => %I[ id title description author_id author created_at comments ], Comment => %I[ id body user_id user commentable_id ] } From 860704d908c54cf711581b4c33ec2386a78822d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 18 Nov 2016 16:27:15 +0100 Subject: [PATCH 046/155] Rewrote tests to tackle more specific stuff --- spec/lib/graphql_spec.rb | 64 ++++++++++++++++++++++++++++++++++------ 1 file changed, 55 insertions(+), 9 deletions(-) diff --git a/spec/lib/graphql_spec.rb b/spec/lib/graphql_spec.rb index 5f7ab34bb..f14242dd6 100644 --- a/spec/lib/graphql_spec.rb +++ b/spec/lib/graphql_spec.rb @@ -5,16 +5,62 @@ describe ConsulSchema do let(:variables) { {} } let(:result) { ConsulSchema.execute(query_string, context: context, variables: variables) } - describe "answers simple queries" do - let(:query_string) { "{ proposals { edges { node { title } } } }" } - let(:context) { - { proposal: create(:proposal, title: "A new proposal") } - } + describe "queries to single elements" do + let(:proposal) { create(:proposal, title: 'Proposal Title') } + let(:query_string) { "{ proposal(id: #{proposal.id}) { id, title } }" } - it "can return proposal titles" do - proposals = result["data"]["proposals"]["edges"].collect { |edge| edge['node'] } - expect(proposals.size).to eq(1) - expect(proposals.first['title']).to eq("A new proposal") + it "returns fields of numeric type" do + returned_proposal = result['data']['proposal'] + expect(returned_proposal['id']).to eq(proposal.id) + end + + it "returns fields of String type" do + returned_proposal = result['data']['proposal'] + expect(returned_proposal['title']).to eq('Proposal Title') + end + + describe "supports nested queries" do + it "with :has_one associations" do + skip "I think this test isn't needed" + # TODO: the only has_one associations inside the project are in the User + # model (administrator, valuator, etc.). But since I think this data + # shouldn't be exposed to the API, there's no point in testing this. + # + # user = create(:user) + # admin = create(:administrator) + # user.administrator = admin + # query_string = "{ user(id: #{user.id}) { administrator { id } } }" + # + # result = ConsulSchema.execute(query_string, context: {}, variables: {}) + # returned_admin = result['data']['user']['administrator'] + # + # expect(returned_admin.id).to eq(admin.id) + end + + it "with :belongs_to associations" do + user = create(:user) + proposal = create(:proposal, author: user) + query_string = "{ proposal(id: #{proposal.id}) { author { username } } }" + + result = ConsulSchema.execute(query_string, context: {}, variables: {}) + returned_proposal = result['data']['proposal'] + + expect(returned_proposal['author']['username']).to eq(user.username) + end + + it "with :has_many associations" do + user = create(:user) + proposal = create(:proposal) + create(:comment, body: "Blah Blah", author: user, commentable: proposal) + create(:comment, body: "I told ya", author: user, commentable: proposal) + query_string = "{ proposal(id: #{proposal.id}) { comments { edges { node { body } } } } }" + + result = ConsulSchema.execute(query_string, context: {}, variables: {}) + comments = result['data']['proposal']['comments']['edges'].collect { |edge| edge['node'] } + comment_bodies = comments.collect { |comment| comment['body'] } + + expect(comment_bodies).to match_array(["Blah Blah", "I told ya"]) + end end end From 7bb17781248f848e455b4b75f7eefa4869b961db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 18 Nov 2016 19:57:07 +0100 Subject: [PATCH 047/155] Finished tests for GraphQL queries for single resources --- spec/lib/graphql_spec.rb | 71 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 4 deletions(-) diff --git a/spec/lib/graphql_spec.rb b/spec/lib/graphql_spec.rb index f14242dd6..c0d2fbdfc 100644 --- a/spec/lib/graphql_spec.rb +++ b/spec/lib/graphql_spec.rb @@ -20,6 +20,8 @@ describe ConsulSchema do end describe "supports nested queries" do + let(:user) { create(:user) } + it "with :has_one associations" do skip "I think this test isn't needed" # TODO: the only has_one associations inside the project are in the User @@ -38,7 +40,6 @@ describe ConsulSchema do end it "with :belongs_to associations" do - user = create(:user) proposal = create(:proposal, author: user) query_string = "{ proposal(id: #{proposal.id}) { author { username } } }" @@ -49,7 +50,6 @@ describe ConsulSchema do end it "with :has_many associations" do - user = create(:user) proposal = create(:proposal) create(:comment, body: "Blah Blah", author: user, commentable: proposal) create(:comment, body: "I told ya", author: user, commentable: proposal) @@ -62,9 +62,72 @@ describe ConsulSchema do expect(comment_bodies).to match_array(["Blah Blah", "I told ya"]) end end + + describe "does not expose confidential" do + let(:user) { create(:user) } + + it "Int fields" do + query_string = "{ user(id: #{user.id}) { failed_census_calls_count } }" + + result = ConsulSchema.execute(query_string, context: {}, variables: {}) + + expect(result['data']).to be_nil + expect(result['errors'].first['message']).to eq("Field 'failed_census_calls_count' doesn't exist on type 'User'") + end + + it "String fields" do + query_string = "{ user(id: #{user.id}) { encrypted_password } }" + + result = ConsulSchema.execute(query_string, context: {}, variables: {}) + + expect(result['data']).to be_nil + expect(result['errors'].first['message']).to eq("Field 'encrypted_password' doesn't exist on type 'User'") + end + + it ":has_one associations" do + user.administrator = create(:administrator) + query_string = "{ user(id: #{user.id}) { administrator { id } } }" + + result = ConsulSchema.execute(query_string, context: {}, variables: {}) + + expect(result['data']).to be_nil + expect(result['errors'].first['message']).to eq("Field 'administrator' doesn't exist on type 'User'") + end + + it ":belongs_to associations" do + create(:failed_census_call, user: user) + + query_string = "{ user(id: #{user.id}) { failed_census_calls { id } } }" + + result = ConsulSchema.execute(query_string, context: {}, variables: {}) + + expect(result['data']).to be_nil + expect(result['errors'].first['message']).to eq("Field 'failed_census_calls' doesn't exist on type 'User'") + end + + it ":has_many associations" do + create(:direct_message, sender: user) + query_string = "{ user(id: #{user.id}) { direct_messages_sent { id } } }" + + result = ConsulSchema.execute(query_string, context: {}, variables: {}) + + expect(result['data']).to be_nil + expect(result['errors'].first['message']).to eq("Field 'direct_messages_sent' doesn't exist on type 'User'") + end + + it "fields inside nested queries" do + proposal = create(:proposal, author: user) + query_string = "{ proposal(id: #{proposal.id}) { author { reset_password_sent_at } } }" + + result = ConsulSchema.execute(query_string, context: {}, variables: {}) + + expect(result['data']).to be_nil + expect(result['errors'].first['message']).to eq("Field 'reset_password_sent_at' doesn't exist on type 'User'") + end + end end - describe "queries with nested associations" do + describe "queries to collections" do let(:query_string) { "{ proposals { edges { node { id, title, author { username } } } } }" } let(:mrajoy) { create(:user, username: 'mrajoy') } @@ -75,7 +138,7 @@ describe ConsulSchema do { proposal_3: create(:proposal, id: 3, title: "Construir un muro", author: dtrump) } } - it "returns the appropiate fields" do + it "return the appropiate fields" do proposals = result["data"]["proposals"]["edges"].collect { |edge| edge['node'] } expected_result = [ From 2271f6d634050ef9c5e9e8767cf0f40272bb7751 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Tue, 22 Nov 2016 15:22:27 +0100 Subject: [PATCH 048/155] Renamed GraphQL route, added support for GET requests --- config/routes.rb | 6 +++--- spec/controllers/graphql_controller_spec.rb | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index b86cb0c30..63be98384 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -263,9 +263,9 @@ Rails.application.routes.draw do end # GraphQL - mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/queries" - post '/queries', to: 'graphql#query' - + mount GraphiQL::Rails::Engine, at: '/graphiql', graphql_path: '/graphql' + get '/graphql', to: 'graphql#query' + post '/graphql', to: 'graphql#query' if Rails.env.development? mount LetterOpenerWeb::Engine, at: "/letter_opener" diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb index 8afc8e60b..bf4bb935e 100644 --- a/spec/controllers/graphql_controller_spec.rb +++ b/spec/controllers/graphql_controller_spec.rb @@ -1,7 +1,7 @@ require 'rails_helper' describe GraphqlController do - let!(:uri) { URI::HTTP.build(host: 'localhost', path: '/queries', port: 3000) } + let!(:uri) { URI::HTTP.build(host: 'localhost', path: '/graphql', port: 3000) } describe "GET request" do it "is accepted when valid" do From 13273f4bc2dfe992ead940e33f979b37c26b835e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Tue, 22 Nov 2016 23:49:05 +0100 Subject: [PATCH 049/155] DRYed specs --- spec/lib/graphql_spec.rb | 172 +++++++++++++++++---------------------- 1 file changed, 74 insertions(+), 98 deletions(-) diff --git a/spec/lib/graphql_spec.rb b/spec/lib/graphql_spec.rb index c0d2fbdfc..00505ac1c 100644 --- a/spec/lib/graphql_spec.rb +++ b/spec/lib/graphql_spec.rb @@ -7,122 +7,98 @@ describe ConsulSchema do describe "queries to single elements" do let(:proposal) { create(:proposal, title: 'Proposal Title') } - let(:query_string) { "{ proposal(id: #{proposal.id}) { id, title } }" } - it "returns fields of numeric type" do - returned_proposal = result['data']['proposal'] - expect(returned_proposal['id']).to eq(proposal.id) + describe "can returns fields of" do + let(:query_string) { "{ proposal(id: #{proposal.id}) { id, title } }" } + subject(:returned_proposal) { result['data']['proposal'] } + + it "numeric type" do + expect(returned_proposal['id']).to eq(proposal.id) + end + + it "String type" do + expect(returned_proposal['title']).to eq(proposal.title) + end end - it "returns fields of String type" do - returned_proposal = result['data']['proposal'] - expect(returned_proposal['title']).to eq('Proposal Title') - end + describe "support nested" do + let(:proposal_author) { create(:user) } + let(:comments_author) { create(:user) } + let(:proposal) { create(:proposal, author: proposal_author) } + let!(:comment_1) { create(:comment, author: comments_author, commentable: proposal) } + let!(:comment_2) { create(:comment, author: comments_author, commentable: proposal) } + let(:query_string) { "{ proposal(id: #{proposal.id}) { author { username }, comments { edges { node { body } } } } }" } + subject(:returned_proposal) { result['data']['proposal'] } - describe "supports nested queries" do - let(:user) { create(:user) } - - it "with :has_one associations" do + it ":has_one associations" do skip "I think this test isn't needed" # TODO: the only has_one associations inside the project are in the User # model (administrator, valuator, etc.). But since I think this data # shouldn't be exposed to the API, there's no point in testing this. - # - # user = create(:user) - # admin = create(:administrator) - # user.administrator = admin - # query_string = "{ user(id: #{user.id}) { administrator { id } } }" - # - # result = ConsulSchema.execute(query_string, context: {}, variables: {}) - # returned_admin = result['data']['user']['administrator'] - # - # expect(returned_admin.id).to eq(admin.id) - end - - it "with :belongs_to associations" do - proposal = create(:proposal, author: user) - query_string = "{ proposal(id: #{proposal.id}) { author { username } } }" - - result = ConsulSchema.execute(query_string, context: {}, variables: {}) - returned_proposal = result['data']['proposal'] - - expect(returned_proposal['author']['username']).to eq(user.username) - end - - it "with :has_many associations" do - proposal = create(:proposal) - create(:comment, body: "Blah Blah", author: user, commentable: proposal) - create(:comment, body: "I told ya", author: user, commentable: proposal) - query_string = "{ proposal(id: #{proposal.id}) { comments { edges { node { body } } } } }" - - result = ConsulSchema.execute(query_string, context: {}, variables: {}) - comments = result['data']['proposal']['comments']['edges'].collect { |edge| edge['node'] } - comment_bodies = comments.collect { |comment| comment['body'] } - - expect(comment_bodies).to match_array(["Blah Blah", "I told ya"]) - end - end - - describe "does not expose confidential" do - let(:user) { create(:user) } - - it "Int fields" do - query_string = "{ user(id: #{user.id}) { failed_census_calls_count } }" - - result = ConsulSchema.execute(query_string, context: {}, variables: {}) - - expect(result['data']).to be_nil - expect(result['errors'].first['message']).to eq("Field 'failed_census_calls_count' doesn't exist on type 'User'") - end - - it "String fields" do - query_string = "{ user(id: #{user.id}) { encrypted_password } }" - - result = ConsulSchema.execute(query_string, context: {}, variables: {}) - - expect(result['data']).to be_nil - expect(result['errors'].first['message']).to eq("Field 'encrypted_password' doesn't exist on type 'User'") - end - - it ":has_one associations" do - user.administrator = create(:administrator) - query_string = "{ user(id: #{user.id}) { administrator { id } } }" - - result = ConsulSchema.execute(query_string, context: {}, variables: {}) - - expect(result['data']).to be_nil - expect(result['errors'].first['message']).to eq("Field 'administrator' doesn't exist on type 'User'") end it ":belongs_to associations" do - create(:failed_census_call, user: user) - - query_string = "{ user(id: #{user.id}) { failed_census_calls { id } } }" - - result = ConsulSchema.execute(query_string, context: {}, variables: {}) - - expect(result['data']).to be_nil - expect(result['errors'].first['message']).to eq("Field 'failed_census_calls' doesn't exist on type 'User'") + expect(returned_proposal['author']['username']).to eq(proposal_author.username) end it ":has_many associations" do - create(:direct_message, sender: user) - query_string = "{ user(id: #{user.id}) { direct_messages_sent { id } } }" + comments = returned_proposal['comments']['edges'].collect { |edge| edge['node'] } + comment_bodies = comments.collect { |comment| comment['body'] } - result = ConsulSchema.execute(query_string, context: {}, variables: {}) + expect(comment_bodies).to match_array([comment_1.body, comment_2.body]) + end + end - expect(result['data']).to be_nil - expect(result['errors'].first['message']).to eq("Field 'direct_messages_sent' doesn't exist on type 'User'") + describe "do not expose confidential" do + let(:user) { create(:user) } + subject(:data) { result['data'] } + subject(:errors) { result['errors'] } + subject(:error_msg) { errors.first['message'] } + + describe "fields of Int type" do + let(:query_string) { "{ user(id: #{user.id}) { failed_census_calls_count } }" } + + specify { expect(data).to be_nil } + specify { expect(error_msg).to eq("Field 'failed_census_calls_count' doesn't exist on type 'User'") } end - it "fields inside nested queries" do - proposal = create(:proposal, author: user) - query_string = "{ proposal(id: #{proposal.id}) { author { reset_password_sent_at } } }" + describe "fields of String type" do + let(:query_string) { "{ user(id: #{user.id}) { encrypted_password } }" } - result = ConsulSchema.execute(query_string, context: {}, variables: {}) + specify { expect(data).to be_nil } + specify { expect(error_msg).to eq("Field 'encrypted_password' doesn't exist on type 'User'") } + end - expect(result['data']).to be_nil - expect(result['errors'].first['message']).to eq("Field 'reset_password_sent_at' doesn't exist on type 'User'") + describe "fields inside nested queries" do + let(:proposal) { create(:proposal, author: user) } + let(:query_string) { "{ proposal(id: #{proposal.id}) { author { reset_password_sent_at } } }" } + + specify { expect(data).to be_nil } + specify { expect(error_msg).to eq("Field 'reset_password_sent_at' doesn't exist on type 'User'") } + end + + describe ":has_one associations" do + let(:administrator) { create(:administrator) } + let(:query_string) { "{ user(id: #{user.id}) { administrator { id } } }" } + + specify { expect(data).to be_nil } + specify { expect(error_msg).to eq("Field 'administrator' doesn't exist on type 'User'") } + end + + describe ":belongs_to associations" do + let(:query_string) { "{ user(id: #{user.id}) { failed_census_calls { id } } }" } + let(:census_call) { create(:failed_census_call, user: user) } + + specify { expect(data).to be_nil } + specify { expect(error_msg).to eq("Field 'failed_census_calls' doesn't exist on type 'User'") } + end + + describe ":has_many associations" do + let(:message) { create(:direct_message, sender: user) } + let(:query_string) { "{ user(id: #{user.id}) { direct_messages_sent { id } } }" } + + specify { expect(data).to be_nil } + specify { expect(error_msg).to eq("Field 'direct_messages_sent' doesn't exist on type 'User'") } end end end @@ -132,11 +108,11 @@ describe ConsulSchema do let(:mrajoy) { create(:user, username: 'mrajoy') } let(:dtrump) { create(:user, username: 'dtrump') } - let(:context) { + let(:context) do { proposal_1: create(:proposal, id: 1, title: "Bajar el IVA", author: mrajoy) } { proposal_2: create(:proposal, id: 2, title: "Censurar los memes", author: mrajoy) } { proposal_3: create(:proposal, id: 3, title: "Construir un muro", author: dtrump) } - } + end it "return the appropiate fields" do proposals = result["data"]["proposals"]["edges"].collect { |edge| edge['node'] } From 027bdf3f1601fb3b531a9043d34b545bf0621ec6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Wed, 23 Nov 2016 00:16:01 +0100 Subject: [PATCH 050/155] DRYed specs --- spec/lib/graphql_spec.rb | 67 +++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 38 deletions(-) diff --git a/spec/lib/graphql_spec.rb b/spec/lib/graphql_spec.rb index 00505ac1c..8e2679f8f 100644 --- a/spec/lib/graphql_spec.rb +++ b/spec/lib/graphql_spec.rb @@ -6,19 +6,17 @@ describe ConsulSchema do let(:result) { ConsulSchema.execute(query_string, context: context, variables: variables) } describe "queries to single elements" do - let(:proposal) { create(:proposal, title: 'Proposal Title') } + let(:proposal) { create(:proposal) } + subject(:returned_proposal) { result['data']['proposal'] } - describe "can returns fields of" do - let(:query_string) { "{ proposal(id: #{proposal.id}) { id, title } }" } - subject(:returned_proposal) { result['data']['proposal'] } + describe "return fields of Int type" do + let(:query_string) { "{ proposal(id: #{proposal.id}) { id } }" } + specify { expect(returned_proposal['id']).to eq(proposal.id) } + end - it "numeric type" do - expect(returned_proposal['id']).to eq(proposal.id) - end - - it "String type" do - expect(returned_proposal['title']).to eq(proposal.title) - end + describe "return fields of String type" do + let(:query_string) { "{ proposal(id: #{proposal.id}) { title } }" } + specify { expect(returned_proposal['title']).to eq(proposal.title) } end describe "support nested" do @@ -28,7 +26,6 @@ describe ConsulSchema do let!(:comment_1) { create(:comment, author: comments_author, commentable: proposal) } let!(:comment_2) { create(:comment, author: comments_author, commentable: proposal) } let(:query_string) { "{ proposal(id: #{proposal.id}) { author { username }, comments { edges { node { body } } } } }" } - subject(:returned_proposal) { result['data']['proposal'] } it ":has_one associations" do skip "I think this test isn't needed" @@ -104,38 +101,32 @@ describe ConsulSchema do end describe "queries to collections" do - let(:query_string) { "{ proposals { edges { node { id, title, author { username } } } } }" } - let(:mrajoy) { create(:user, username: 'mrajoy') } let(:dtrump) { create(:user, username: 'dtrump') } - let(:context) do - { proposal_1: create(:proposal, id: 1, title: "Bajar el IVA", author: mrajoy) } - { proposal_2: create(:proposal, id: 2, title: "Censurar los memes", author: mrajoy) } - { proposal_3: create(:proposal, id: 3, title: "Construir un muro", author: dtrump) } + let!(:proposal_1) { create(:proposal, id: 1, title: "Bajar el IVA", author: mrajoy) } + let!(:proposal_2) { create(:proposal, id: 2, title: "Censurar los memes", author: mrajoy) } + let!(:proposal_3) { create(:proposal, id: 3, title: "Construir un muro", author: dtrump) } + subject(:returned_proposals) { result['data']['proposals']["edges"].collect { |edge| edge['node'] } } + + describe "return fields of Int type" do + let(:query_string) { "{ proposals { edges { node { id } } } }" } + let(:ids) { returned_proposals.collect { |proposal| proposal['id'] } } + + specify { expect(ids).to match_array([3, 1, 2]) } end - it "return the appropiate fields" do - proposals = result["data"]["proposals"]["edges"].collect { |edge| edge['node'] } + describe "return fields of String type" do + let(:query_string) { "{ proposals { edges { node { title } } } }" } + let(:titles) { returned_proposals.collect { |proposal| proposal['title'] } } - expected_result = [ - { - 'id' => 1, - 'title' => 'Bajar el IVA', - 'author' => { 'username' => 'mrajoy' } - }, - { - 'id' => 2, - 'title' => 'Censurar los memes', - 'author' => { 'username' => 'mrajoy' } - }, - { - 'id' => 3, - 'title' => 'Construir un muro', - 'author' => { 'username' => 'dtrump' } - } - ] + specify { expect(titles).to match_array(['Construir un muro', 'Censurar los memes', 'Bajar el IVA']) } + end - expect(proposals).to match_array(expected_result) + describe "return nested fields" do + let(:query_string) { "{ proposals { edges { node { author { username } } } } }" } + let(:authors) { returned_proposals.collect { |proposal| proposal['author']['username'] } } + + specify { expect(authors).to match_array(['mrajoy', 'dtrump', 'mrajoy']) } end end end From 6230712b7433092cc33696e8c5dc78ea85e5b087 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 25 Nov 2016 00:56:26 +0100 Subject: [PATCH 051/155] Fixed bug in my schema.rb --- db/schema.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/schema.rb b/db/schema.rb index 48e591bf7..a93942873 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -464,7 +464,7 @@ ActiveRecord::Schema.define(version: 20161102133838) 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-10 21:34:46', null: false + t.datetime "password_changed_at", default: '2016-11-02 13:51:14', null: false end add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree From 9ed51cf477514d9641cf447c2e48b5cfee188a76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 25 Nov 2016 01:02:05 +0100 Subject: [PATCH 052/155] Useless commit --- spec/controllers/graphql_controller_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb index bf4bb935e..3504de501 100644 --- a/spec/controllers/graphql_controller_spec.rb +++ b/spec/controllers/graphql_controller_spec.rb @@ -12,6 +12,7 @@ describe GraphqlController do it "is rejected when not valid" do skip + # Just doing this to trigger Travis CI build end end From 0eae2fada628f37399468d3aa91e1cd39230f5b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Sun, 27 Nov 2016 13:50:01 +0100 Subject: [PATCH 053/155] Refactor GraphQL controller specs --- Gemfile | 1 + Gemfile.lock | 15 +++ config/routes.rb | 1 - spec/controllers/graphql_controller_spec.rb | 100 +++++++++++--------- 4 files changed, 71 insertions(+), 46 deletions(-) diff --git a/Gemfile b/Gemfile index 6d7f660f3..9180e62fb 100644 --- a/Gemfile +++ b/Gemfile @@ -96,6 +96,7 @@ group :test do gem 'poltergeist' gem 'coveralls', require: false gem 'email_spec' + gem 'http' end group :development do diff --git a/Gemfile.lock b/Gemfile.lock index 9638fae74..eaef01916 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -143,6 +143,8 @@ GEM railties (>= 3.2.6, < 5.0) diff-lcs (1.2.5) docile (1.1.5) + domain_name (0.5.20161021) + unf (>= 0.0.5, < 1.0.0) easy_translate (0.5.0) json thread @@ -189,6 +191,15 @@ GEM hashie (3.4.3) highline (1.7.8) htmlentities (4.3.4) + http (2.1.0) + addressable (~> 2.3) + http-cookie (~> 1.0) + http-form_data (~> 1.0.1) + http_parser.rb (~> 0.6.0) + http-cookie (1.0.3) + domain_name (~> 0.5) + http-form_data (1.0.1) + http_parser.rb (0.6.0) httpi (2.4.1) rack i18n (0.7.0) @@ -423,6 +434,9 @@ GEM thread_safe (~> 0.1) uglifier (3.0.3) execjs (>= 0.3.0, < 3) + unf (0.1.4) + unf_ext + unf_ext (0.0.7.2) unicode-display_width (1.1.1) unicorn (5.1.0) kgio (~> 2.6) @@ -484,6 +498,7 @@ DEPENDENCIES graphiql-rails graphql groupdate (~> 3.1.0) + http i18n-tasks initialjs-rails (= 0.2.0.4) invisible_captcha (~> 0.9.1) diff --git a/config/routes.rb b/config/routes.rb index 63be98384..b530df52c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -264,7 +264,6 @@ Rails.application.routes.draw do # GraphQL mount GraphiQL::Rails::Engine, at: '/graphiql', graphql_path: '/graphql' - get '/graphql', to: 'graphql#query' post '/graphql', to: 'graphql#query' if Rails.env.development? diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb index 3504de501..2b53cd0fb 100644 --- a/spec/controllers/graphql_controller_spec.rb +++ b/spec/controllers/graphql_controller_spec.rb @@ -1,63 +1,73 @@ require 'rails_helper' +require 'http' describe GraphqlController do - let!(:uri) { URI::HTTP.build(host: 'localhost', path: '/graphql', port: 3000) } + let(:uri) { URI::HTTP.build(host: 'localhost', path: '/graphql', port: 3000) } + let(:query_string) { "" } + let(:body) { {query: query_string}.to_json } - describe "GET request" do - it "is accepted when valid" do - # Like POST requests but the query string goes in the URL - # More info at: http://graphql.org/learn/serving-over-http/#get-request - skip - end + describe "POST requests" do + let(:author) { create(:user) } + let(:proposal) { create(:proposal, author: author) } + let(:response) { HTTP.headers('Content-Type' => 'application/json').post(uri, body: body) } + let(:response_body) { JSON.parse(response.body) } - it "is rejected when not valid" do - skip - # Just doing this to trigger Travis CI build - end - end + context "when query string is valid" do + let(:query_string) { "{ proposal(id: #{proposal.id}) { title, author { username } } }" } + let(:returned_proposal) { response_body['data']['proposal'] } - describe "POST request" do - let(:post_request) { - req = Net::HTTP::Post.new(uri) - req['Content-Type'] = 'application/json' - req - } - - it "succeeds when valid" do - body = { query: "{ proposals(first: 2) { edges { node { id } } } }" }.to_json - response = Net::HTTP.start(uri.host, uri.port) do |http| - post_request.body = body - http.request(post_request) + it "returns HTTP 200 OK" do + expect(response.code).to eq(200) end - # Is it enough to check the status code or should I also check the body? - expect(response.code.to_i).to eq(200) - end - - it "succeeds and returns an error when disclosed attributes are requested" do - body = { query: "{ user(id: 1) { encrypted_password } }" }.to_json - response = Net::HTTP.start(uri.host, uri.port) do |http| - post_request.body = body - http.request(post_request) + it "returns first-level fields" do + expect(returned_proposal['title']).to eq(proposal.title) end - body_hash = JSON.parse(response.body) - expect(body_hash['errors']).to be_present + it "returns nested fields" do + expect(returned_proposal['author']['username']).to eq(author.username) + end end - it "fails when no query string is provided" do - body = {}.to_json - response = Net::HTTP.start(uri.host, uri.port) do |http| - post_request.body = body - http.request(post_request) + context "when query string asks for invalid fields" do + let(:query_string) { "{ proposal(id: #{proposal.id}) { missing_field } }" } + + it "returns HTTP 200 OK" do + expect(response.code).to eq(200) end - # TODO: I must find a way to handle this better. Right now it shows a 500 - # Internal Server Error, I think I should always return a valid (but empty) - # JSON document like '{}' - expect(response.code.to_i).not_to eq(200) + it "doesn't return any data" do + expect(response_body['data']).to be_nil + end + + it "returns error inside body" do + expect(response_body['errors']).to be_present + end + end + + context "when query string is not valid" do + let(:query_string) { "invalid" } + + it "returns HTTP 400 Bad Request" do + expect(response.code).to eq(400) + end + end + + context "when query string is missing" do + let(:query_string) { nil } + + it "returns HTTP 400 Bad Request" do + expect(response.code).to eq(400) + end + end + + context "when body is missing" do + let(:body) { nil } + + it "returns HTTP 400 Bad Request" do + expect(response.code).to eq(400) + end end end - end From 76dd11631aab8f477c46c151db05861f04a3c32b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Wed, 30 Nov 2016 13:38:13 +0100 Subject: [PATCH 054/155] Fixes error initializing database --- app/models/concerns/measurable.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/concerns/measurable.rb b/app/models/concerns/measurable.rb index 44b7ddabb..5ac2f2a14 100644 --- a/app/models/concerns/measurable.rb +++ b/app/models/concerns/measurable.rb @@ -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 @@ -21,4 +21,4 @@ module Measurable end -end \ No newline at end of file +end From d1e1ee9d4159d43c00cd8a4e5a5383d95427230a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Wed, 30 Nov 2016 13:43:10 +0100 Subject: [PATCH 055/155] Moved api_types into TypeCreator --- config/initializers/graphql.rb | 10 +++++----- lib/graph_ql/type_creator.rb | 31 +++++++++++++++++++------------ 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index 1f0c8968d..3918d794a 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -5,10 +5,10 @@ API_TYPE_DEFINITIONS = { Comment => %I[ id body user_id user commentable_id ] } -api_types = {} +type_creator = GraphQL::TypeCreator.new API_TYPE_DEFINITIONS.each do |model, fields| - api_types[model] = GraphQL::TypeCreator.create(model, fields, api_types) + type_creator.create(model, fields) end ConsulSchema = GraphQL::Schema.define do @@ -28,11 +28,11 @@ QueryRoot = GraphQL::ObjectType.define do name "Query" description "The query root for this schema" - API_TYPE_DEFINITIONS.each_key do |model| + type_creator.created_types.each do |model, created_type| # create an entry field to retrive a single object field model.name.underscore.to_sym do - type api_types[model] + type created_type description "Find one #{model.model_name.human} by ID" argument :id, !types.ID resolve -> (object, arguments, context) { @@ -41,7 +41,7 @@ QueryRoot = GraphQL::ObjectType.define do end # create an entry filed to retrive a paginated collection - connection model.name.underscore.pluralize.to_sym, api_types[model].connection_type do + connection model.name.underscore.pluralize.to_sym, created_type.connection_type do description "Find all #{model.model_name.human.pluralize}" resolve -> (object, arguments, context) { model.all diff --git a/lib/graph_ql/type_creator.rb b/lib/graph_ql/type_creator.rb index 5a831fd7d..b47624df9 100644 --- a/lib/graph_ql/type_creator.rb +++ b/lib/graph_ql/type_creator.rb @@ -1,6 +1,5 @@ module GraphQL class TypeCreator - # Return a GraphQL type for a 'database_type' TYPES_CONVERSION = Hash.new(GraphQL::STRING_TYPE).merge( integer: GraphQL::INT_TYPE, @@ -9,9 +8,16 @@ module GraphQL double: GraphQL::FLOAT_TYPE ) - def self.create(model, field_names, api_types) + attr_accessor :created_types - new_graphql_type = GraphQL::ObjectType.define do + def initialize + @created_types = {} + end + + def create(model, field_names) + type_creator = self + + created_type = GraphQL::ObjectType.define do name(model.name) description("#{model.model_name.human}") @@ -24,21 +30,22 @@ module GraphQL association = model.reflect_on_all_associations.find { |a| a.name == field_name } case association.macro when :has_one - field(association.name, -> { api_types[association.klass] }) + field(association.name, -> { type_creator.created_types[association.klass] }) when :belongs_to - field(association.name, -> { api_types[association.klass] }) + field(association.name, -> { type_creator.created_types[association.klass] }) when :has_many - connection(association.name, -> { api_types[association.klass].connection_type { - description "#{association.klass.model_name.human.pluralize}" - resolve -> (object, arguments, context) { - association.klass.all - } - }}) + connection association.name, -> do + type_creator.created_types[association.klass].connection_type do + description "#{association.klass.model_name.human.pluralize}" + resolve -> (object, arguments, context) { association.klass.all } + end + end end end end end - return new_graphql_type # GraphQL::ObjectType + created_types[model] = created_type + return created_type # GraphQL::ObjectType end end end From a51626272415e396023c29142f8e71a72a0389d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Wed, 30 Nov 2016 13:43:50 +0100 Subject: [PATCH 056/155] Example graphql_controller test --- spec/controllers/graphql_controller_spec.rb | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb index 2b53cd0fb..e0244ee68 100644 --- a/spec/controllers/graphql_controller_spec.rb +++ b/spec/controllers/graphql_controller_spec.rb @@ -1,6 +1,22 @@ require 'rails_helper' -require 'http' +# Hacerlo como los test de controlador de rails + +describe GraphqlController, type: :request do + let(:proposal) { create(:proposal) } + + it "answers simple json queries" do + headers = { "CONTENT_TYPE" => "application/json" } + #post "/widgets", '{ "widget": { "name":"My Widget" } }', headers + post '/graphql', { query: "{ proposal(id: #{proposal.id}) { title } }" }.to_json, headers + expect(response).to have_http_status(200) + expect(JSON.parse(response.body)['data']['proposal']['title']).to eq(proposal.title) + end + + +end + +=begin describe GraphqlController do let(:uri) { URI::HTTP.build(host: 'localhost', path: '/graphql', port: 3000) } let(:query_string) { "" } @@ -71,3 +87,4 @@ describe GraphqlController do end end +=end From aef20aadf73ef438d93d7c8446af2d364be3adfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Wed, 30 Nov 2016 13:44:31 +0100 Subject: [PATCH 057/155] Refactor specs for TypeCreator and ConsulSchema --- config/initializers/graphql.rb | 5 +- spec/lib/graphql_spec.rb | 283 ++++++++++++++++++--------------- spec/lib/type_creator_spec.rb | 24 ++- 3 files changed, 174 insertions(+), 138 deletions(-) diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index 3918d794a..306c01890 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -1,8 +1,9 @@ API_TYPE_DEFINITIONS = { - User => %I[ id username proposals ], + User => %I[ id username proposals organization ], Debate => %I[ id title description author_id author created_at comments ], Proposal => %I[ id title description author_id author created_at comments ], - Comment => %I[ id body user_id user commentable_id ] + Comment => %I[ id body user_id user commentable_id ], + Organization => %I[ id name ] } type_creator = GraphQL::TypeCreator.new diff --git a/spec/lib/graphql_spec.rb b/spec/lib/graphql_spec.rb index 8e2679f8f..5fac92e28 100644 --- a/spec/lib/graphql_spec.rb +++ b/spec/lib/graphql_spec.rb @@ -1,132 +1,157 @@ require 'rails_helper' -describe ConsulSchema do - let(:context) { {} } # should be overriden for specific queries - let(:variables) { {} } - let(:result) { ConsulSchema.execute(query_string, context: context, variables: variables) } - - describe "queries to single elements" do - let(:proposal) { create(:proposal) } - subject(:returned_proposal) { result['data']['proposal'] } - - describe "return fields of Int type" do - let(:query_string) { "{ proposal(id: #{proposal.id}) { id } }" } - specify { expect(returned_proposal['id']).to eq(proposal.id) } - end - - describe "return fields of String type" do - let(:query_string) { "{ proposal(id: #{proposal.id}) { title } }" } - specify { expect(returned_proposal['title']).to eq(proposal.title) } - end - - describe "support nested" do - let(:proposal_author) { create(:user) } - let(:comments_author) { create(:user) } - let(:proposal) { create(:proposal, author: proposal_author) } - let!(:comment_1) { create(:comment, author: comments_author, commentable: proposal) } - let!(:comment_2) { create(:comment, author: comments_author, commentable: proposal) } - let(:query_string) { "{ proposal(id: #{proposal.id}) { author { username }, comments { edges { node { body } } } } }" } - - it ":has_one associations" do - skip "I think this test isn't needed" - # TODO: the only has_one associations inside the project are in the User - # model (administrator, valuator, etc.). But since I think this data - # shouldn't be exposed to the API, there's no point in testing this. - end - - it ":belongs_to associations" do - expect(returned_proposal['author']['username']).to eq(proposal_author.username) - end - - it ":has_many associations" do - comments = returned_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 - end - - describe "do not expose confidential" do - let(:user) { create(:user) } - subject(:data) { result['data'] } - subject(:errors) { result['errors'] } - subject(:error_msg) { errors.first['message'] } - - describe "fields of Int type" do - let(:query_string) { "{ user(id: #{user.id}) { failed_census_calls_count } }" } - - specify { expect(data).to be_nil } - specify { expect(error_msg).to eq("Field 'failed_census_calls_count' doesn't exist on type 'User'") } - end - - describe "fields of String type" do - let(:query_string) { "{ user(id: #{user.id}) { encrypted_password } }" } - - specify { expect(data).to be_nil } - specify { expect(error_msg).to eq("Field 'encrypted_password' doesn't exist on type 'User'") } - end - - describe "fields inside nested queries" do - let(:proposal) { create(:proposal, author: user) } - let(:query_string) { "{ proposal(id: #{proposal.id}) { author { reset_password_sent_at } } }" } - - specify { expect(data).to be_nil } - specify { expect(error_msg).to eq("Field 'reset_password_sent_at' doesn't exist on type 'User'") } - end - - describe ":has_one associations" do - let(:administrator) { create(:administrator) } - let(:query_string) { "{ user(id: #{user.id}) { administrator { id } } }" } - - specify { expect(data).to be_nil } - specify { expect(error_msg).to eq("Field 'administrator' doesn't exist on type 'User'") } - end - - describe ":belongs_to associations" do - let(:query_string) { "{ user(id: #{user.id}) { failed_census_calls { id } } }" } - let(:census_call) { create(:failed_census_call, user: user) } - - specify { expect(data).to be_nil } - specify { expect(error_msg).to eq("Field 'failed_census_calls' doesn't exist on type 'User'") } - end - - describe ":has_many associations" do - let(:message) { create(:direct_message, sender: user) } - let(:query_string) { "{ user(id: #{user.id}) { direct_messages_sent { id } } }" } - - specify { expect(data).to be_nil } - specify { expect(error_msg).to eq("Field 'direct_messages_sent' doesn't exist on type 'User'") } - end - end - end - - describe "queries to collections" do - let(:mrajoy) { create(:user, username: 'mrajoy') } - let(:dtrump) { create(:user, username: 'dtrump') } - let!(:proposal_1) { create(:proposal, id: 1, title: "Bajar el IVA", author: mrajoy) } - let!(:proposal_2) { create(:proposal, id: 2, title: "Censurar los memes", author: mrajoy) } - let!(:proposal_3) { create(:proposal, id: 3, title: "Construir un muro", author: dtrump) } - subject(:returned_proposals) { result['data']['proposals']["edges"].collect { |edge| edge['node'] } } - - describe "return fields of Int type" do - let(:query_string) { "{ proposals { edges { node { id } } } }" } - let(:ids) { returned_proposals.collect { |proposal| proposal['id'] } } - - specify { expect(ids).to match_array([3, 1, 2]) } - end - - describe "return fields of String type" do - let(:query_string) { "{ proposals { edges { node { title } } } }" } - let(:titles) { returned_proposals.collect { |proposal| proposal['title'] } } - - specify { expect(titles).to match_array(['Construir un muro', 'Censurar los memes', 'Bajar el IVA']) } - end - - describe "return nested fields" do - let(:query_string) { "{ proposals { edges { node { author { username } } } } }" } - let(:authors) { returned_proposals.collect { |proposal| proposal['author']['username'] } } - - specify { expect(authors).to match_array(['mrajoy', 'dtrump', 'mrajoy']) } - end - end +# TODO: uno por cada tipo escalar, uno por cada asociacion, uno con query anidada * 2 (uno para asegurarse de que se muestra y otro para cuando se oculta) +def execute(query_string, context = {}, variables = {}) + ConsulSchema.execute(query_string, context: context, variables: variables) +end + +def dig(response, path) + response.dig(*path.split('.')) +end + +describe ConsulSchema do + let(:proposal_author) { create(:user) } + let(:proposal) { create(:proposal, author: proposal_author) } + + 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 + + it "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}) { author { username } } }") + expect(dig(response, 'data.proposal.author.username')).to eq(proposal.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 + + # it "hides confidential fields of Int type" do + # response = execute("{ user(id: #{user.id}) { failed_census_calls_count } }") + # expect(response['data']).to be_nil + # expect(response[]) + # end + # + # it "hides confidential fields of String type" do + # skip + # response = execute("{ user(id: #{user.id}) { encrypted_password } }") + # end + # + # it "hides confidential has_one associations" do + # skip + # response = execute("{ user(id: #{user.id}) { administrator { id } } }") + # end + # + # it "hides confidential belongs_to associations" do + # skip + # response = execute("{ user(id: #{user.id}) { failed_census_calls { id } } }") + # end + # + # it "hides confidential has_many associations" do + # skip + # response = execute("{ user(id: #{user.id}) { direct_messages_sent { id } } }") + # end + # + # describe "do not expose confidential" do + # let(:user) { create(:user) } + # subject(:data) { response['data'] } + # subject(:errors) { response['errors'] } + # subject(:error_msg) { errors.first['message'] } + # + # describe "fields of Int type" do + # let(:query_string) { "{ user(id: #{user.id}) { failed_census_calls_count } }" } + # + # specify { expect(data).to be_nil } + # specify { expect(error_msg).to eq("Field 'failed_census_calls_count' doesn't exist on type 'User'") } + # end + # + # describe "fields of String type" do + # let(:query_string) { "{ user(id: #{user.id}) { encrypted_password } }" } + # + # specify { expect(data).to be_nil } + # specify { expect(error_msg).to eq("Field 'encrypted_password' doesn't exist on type 'User'") } + # end + # + # describe "fields inside nested queries" do + # let(:proposal) { create(:proposal, author: user) } + # let(:query_string) { "{ proposal(id: #{proposal.id}) { author { reset_password_sent_at } } }" } + # + # specify { expect(data).to be_nil } + # specify { expect(error_msg).to eq("Field 'reset_password_sent_at' doesn't exist on type 'User'") } + # end + # + # describe ":has_one associations" do + # let(:administrator) { create(:administrator) } + # let(:query_string) { "{ user(id: #{user.id}) { administrator { id } } }" } + # + # specify { expect(data).to be_nil } + # specify { expect(error_msg).to eq("Field 'administrator' doesn't exist on type 'User'") } + # end + # + # describe ":belongs_to associations" do + # let(:query_string) { "{ user(id: #{user.id}) { failed_census_calls { id } } }" } + # let(:census_call) { create(:failed_census_call, user: user) } + # + # specify { expect(data).to be_nil } + # specify { expect(error_msg).to eq("Field 'failed_census_calls' doesn't exist on type 'User'") } + # end + # + # describe ":has_many associations" do + # let(:message) { create(:direct_message, sender: user) } + # let(:query_string) { "{ user(id: #{user.id}) { direct_messages_sent { id } } }" } + # + # specify { expect(data).to be_nil } + # specify { expect(error_msg).to eq("Field 'direct_messages_sent' doesn't exist on type 'User'") } + # end + # end + # + # describe "queries to collections" do + # let(:mrajoy) { create(:user, username: 'mrajoy') } + # let(:dtrump) { create(:user, username: 'dtrump') } + # let!(:proposal_1) { create(:proposal, id: 1, title: "Bajar el IVA", author: mrajoy) } + # let!(:proposal_2) { create(:proposal, id: 2, title: "Censurar los memes", author: mrajoy) } + # let!(:proposal_3) { create(:proposal, id: 3, title: "Construir un muro", author: dtrump) } + # subject(:returned_proposals) { response['data']['proposals']["edges"].collect { |edge| edge['node'] } } + # + # describe "return fields of Int type" do + # let(:query_string) { "{ proposals { edges { node { id } } } }" } + # let(:ids) { returned_proposals.collect { |proposal| proposal['id'] } } + # + # specify { expect(ids).to match_array([3, 1, 2]) } + # end + # + # describe "return fields of String type" do + # let(:query_string) { "{ proposals { edges { node { title } } } }" } + # let(:titles) { returned_proposals.collect { |proposal| proposal['title'] } } + # + # specify { expect(titles).to match_array(['Construir un muro', 'Censurar los memes', 'Bajar el IVA']) } + # end + # + # describe "return nested fields" do + # let(:query_string) { "{ proposals { edges { node { author { username } } } } }" } + # let(:authors) { returned_proposals.collect { |proposal| proposal['author']['username'] } } + # + # specify { expect(authors).to match_array(['mrajoy', 'dtrump', 'mrajoy']) } + # end + # end end diff --git a/spec/lib/type_creator_spec.rb b/spec/lib/type_creator_spec.rb index 2369df348..4ad667812 100644 --- a/spec/lib/type_creator_spec.rb +++ b/spec/lib/type_creator_spec.rb @@ -1,10 +1,12 @@ require 'rails_helper' describe GraphQL::TypeCreator do - let!(:api_types) { {} } - let!(:user_type) { GraphQL::TypeCreator.create(User, %I[ id ], api_types) } - let!(:comment_type) { GraphQL::TypeCreator.create(Comment, %I[ id ], api_types) } - let!(:debate_type) { GraphQL::TypeCreator.create(Debate, %I[ id title author ], api_types) } + let(:type_creator) { GraphQL::TypeCreator.new } + + #let(:api_types) { {} } + #let!(:user_type) { GraphQL::TypeCreator.create(User, %I[ id ], api_types) } + #let!(:comment_type) { GraphQL::TypeCreator.create(Comment, %I[ id ], api_types) } + #let!(:debate_type) { GraphQL::TypeCreator.create(Debate, %I[ id title author ], api_types) } # TODO: no puedo añadir los comentarios a la field_list de Debate porque como # las conexiones se crean de forma lazy creo que provoca que falle la creación # del resto de tipos y provoca que fallen todos los tests. @@ -13,6 +15,7 @@ describe GraphQL::TypeCreator do describe "::create" do describe "creates fields" do it "for int attributes" do + debate_type = type_creator.create(Debate, %I[ id ]) created_field = debate_type.fields['id'] expect(created_field).to be_a(GraphQL::Field) @@ -21,6 +24,7 @@ describe GraphQL::TypeCreator do end it "for string attributes" do + skip created_field = debate_type.fields['title'] expect(created_field).to be_a(GraphQL::Field) @@ -31,6 +35,9 @@ describe GraphQL::TypeCreator do describe "creates connections for" do it ":belongs_to associations" do + user_type = type_creator.create(User, %I[ id ]) + debate_type = type_creator.create(Debate, %I[ author ]) + connection = debate_type.fields['author'] # TODO: because connection types are created and added lazily to the @@ -40,7 +47,8 @@ describe GraphQL::TypeCreator do # connection.inspect shows some weird info expect(connection).to be_a(GraphQL::Field) - # expect(connection.type).to be_a(api_types[User]) + #debugger + expect(connection.type).to eq(user_type) expect(connection.name).to eq('author') end @@ -49,7 +57,9 @@ describe GraphQL::TypeCreator do end it ":has_many associations" do - skip "still don't know how to handle relay connections inside RSpec" + #skip "still don't know how to handle relay connections inside RSpec" + comment_type = type_creator.create(Comment, %I[ id ]) + debate_type = type_creator.create(Debate, %I[ author ]) connection = debate_type.fields['comments'] @@ -60,7 +70,7 @@ describe GraphQL::TypeCreator do # connection.inspect shows some weird info expect(connection).to be_a(GraphQL::Field) - # expect(created_connection.type).to be_a(api_types[Comment]) + expect(connection.type).to be_a(api_types[Comment]) expect(connection.name).to eq('comments') end end From c973195267593e5cd447117a9f9f9981d74c3d4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 2 Dec 2016 21:30:13 +0100 Subject: [PATCH 058/155] Refactor ConsulSchema specs --- spec/lib/graphql_spec.rb | 160 ++++++++++++--------------------------- 1 file changed, 49 insertions(+), 111 deletions(-) diff --git a/spec/lib/graphql_spec.rb b/spec/lib/graphql_spec.rb index 5fac92e28..50e115a7f 100644 --- a/spec/lib/graphql_spec.rb +++ b/spec/lib/graphql_spec.rb @@ -1,6 +1,5 @@ require 'rails_helper' -# TODO: uno por cada tipo escalar, uno por cada asociacion, uno con query anidada * 2 (uno para asegurarse de que se muestra y otro para cuando se oculta) def execute(query_string, context = {}, variables = {}) ConsulSchema.execute(query_string, context: context, variables: variables) end @@ -9,9 +8,15 @@ 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 + describe ConsulSchema do - let(:proposal_author) { create(:user) } - let(:proposal) { create(:proposal, author: proposal_author) } + let(:user) { create(:user) } + let(:proposal) { create(:proposal, author: user) } it "returns fields of Int type" do response = execute("{ proposal(id: #{proposal.id}) { id } }") @@ -46,112 +51,45 @@ describe ConsulSchema do expect(comment_bodies).to match_array([comment_1.body, comment_2.body]) end - # it "hides confidential fields of Int type" do - # response = execute("{ user(id: #{user.id}) { failed_census_calls_count } }") - # expect(response['data']).to be_nil - # expect(response[]) - # end - # - # it "hides confidential fields of String type" do - # skip - # response = execute("{ user(id: #{user.id}) { encrypted_password } }") - # end - # - # it "hides confidential has_one associations" do - # skip - # response = execute("{ user(id: #{user.id}) { administrator { id } } }") - # end - # - # it "hides confidential belongs_to associations" do - # skip - # response = execute("{ user(id: #{user.id}) { failed_census_calls { id } } }") - # end - # - # it "hides confidential has_many associations" do - # skip - # response = execute("{ user(id: #{user.id}) { direct_messages_sent { id } } }") - # end - # - # describe "do not expose confidential" do - # let(:user) { create(:user) } - # subject(:data) { response['data'] } - # subject(:errors) { response['errors'] } - # subject(:error_msg) { errors.first['message'] } - # - # describe "fields of Int type" do - # let(:query_string) { "{ user(id: #{user.id}) { failed_census_calls_count } }" } - # - # specify { expect(data).to be_nil } - # specify { expect(error_msg).to eq("Field 'failed_census_calls_count' doesn't exist on type 'User'") } - # end - # - # describe "fields of String type" do - # let(:query_string) { "{ user(id: #{user.id}) { encrypted_password } }" } - # - # specify { expect(data).to be_nil } - # specify { expect(error_msg).to eq("Field 'encrypted_password' doesn't exist on type 'User'") } - # end - # - # describe "fields inside nested queries" do - # let(:proposal) { create(:proposal, author: user) } - # let(:query_string) { "{ proposal(id: #{proposal.id}) { author { reset_password_sent_at } } }" } - # - # specify { expect(data).to be_nil } - # specify { expect(error_msg).to eq("Field 'reset_password_sent_at' doesn't exist on type 'User'") } - # end - # - # describe ":has_one associations" do - # let(:administrator) { create(:administrator) } - # let(:query_string) { "{ user(id: #{user.id}) { administrator { id } } }" } - # - # specify { expect(data).to be_nil } - # specify { expect(error_msg).to eq("Field 'administrator' doesn't exist on type 'User'") } - # end - # - # describe ":belongs_to associations" do - # let(:query_string) { "{ user(id: #{user.id}) { failed_census_calls { id } } }" } - # let(:census_call) { create(:failed_census_call, user: user) } - # - # specify { expect(data).to be_nil } - # specify { expect(error_msg).to eq("Field 'failed_census_calls' doesn't exist on type 'User'") } - # end - # - # describe ":has_many associations" do - # let(:message) { create(:direct_message, sender: user) } - # let(:query_string) { "{ user(id: #{user.id}) { direct_messages_sent { id } } }" } - # - # specify { expect(data).to be_nil } - # specify { expect(error_msg).to eq("Field 'direct_messages_sent' doesn't exist on type 'User'") } - # end - # end - # - # describe "queries to collections" do - # let(:mrajoy) { create(:user, username: 'mrajoy') } - # let(:dtrump) { create(:user, username: 'dtrump') } - # let!(:proposal_1) { create(:proposal, id: 1, title: "Bajar el IVA", author: mrajoy) } - # let!(:proposal_2) { create(:proposal, id: 2, title: "Censurar los memes", author: mrajoy) } - # let!(:proposal_3) { create(:proposal, id: 3, title: "Construir un muro", author: dtrump) } - # subject(:returned_proposals) { response['data']['proposals']["edges"].collect { |edge| edge['node'] } } - # - # describe "return fields of Int type" do - # let(:query_string) { "{ proposals { edges { node { id } } } }" } - # let(:ids) { returned_proposals.collect { |proposal| proposal['id'] } } - # - # specify { expect(ids).to match_array([3, 1, 2]) } - # end - # - # describe "return fields of String type" do - # let(:query_string) { "{ proposals { edges { node { title } } } }" } - # let(:titles) { returned_proposals.collect { |proposal| proposal['title'] } } - # - # specify { expect(titles).to match_array(['Construir un muro', 'Censurar los memes', 'Bajar el IVA']) } - # end - # - # describe "return nested fields" do - # let(:query_string) { "{ proposals { edges { node { author { username } } } } }" } - # let(:authors) { returned_proposals.collect { |proposal| proposal['author']['username'] } } - # - # specify { expect(authors).to match_array(['mrajoy', 'dtrump', 'mrajoy']) } - # end - # end + it "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}) { author { organization { name } } } }") + + expect(dig(response, 'data.proposal.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 + + it "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 { author { encrypted_password } } } } }") + expect(hidden_field?(response, 'encrypted_password')).to be_truthy + end end From a0f1976c1adba3de67c6204dec2df1324d5fa34a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 2 Dec 2016 21:53:36 +0100 Subject: [PATCH 059/155] Refactor GraphQL::TypeCreator specs --- spec/lib/type_creator_spec.rb | 96 ++++++++++++++--------------------- 1 file changed, 38 insertions(+), 58 deletions(-) diff --git a/spec/lib/type_creator_spec.rb b/spec/lib/type_creator_spec.rb index 4ad667812..2fc1bf78d 100644 --- a/spec/lib/type_creator_spec.rb +++ b/spec/lib/type_creator_spec.rb @@ -3,76 +3,56 @@ require 'rails_helper' describe GraphQL::TypeCreator do let(:type_creator) { GraphQL::TypeCreator.new } - #let(:api_types) { {} } - #let!(:user_type) { GraphQL::TypeCreator.create(User, %I[ id ], api_types) } - #let!(:comment_type) { GraphQL::TypeCreator.create(Comment, %I[ id ], api_types) } - #let!(:debate_type) { GraphQL::TypeCreator.create(Debate, %I[ id title author ], api_types) } - # TODO: no puedo añadir los comentarios a la field_list de Debate porque como - # las conexiones se crean de forma lazy creo que provoca que falle la creación - # del resto de tipos y provoca que fallen todos los tests. - # let!(:debate_type) { GraphQL::TypeCreator.create(Debate, %I[ id title author comments ], api_types) } - describe "::create" do - describe "creates fields" do - it "for int attributes" do - debate_type = type_creator.create(Debate, %I[ id ]) - created_field = debate_type.fields['id'] + it "creates fields for Int attributes" do + debate_type = type_creator.create(Debate, %I[ id ]) + 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 "for string attributes" do - skip - 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 + 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 - describe "creates connections for" do - it ":belongs_to associations" do - user_type = type_creator.create(User, %I[ id ]) - debate_type = type_creator.create(Debate, %I[ author ]) + it "creates fields for String attributes" do + debate_type = type_creator.create(Debate, %I[ title ]) + created_field = debate_type.fields['title'] - connection = debate_type.fields['author'] + 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 - # TODO: because connection types are created and added lazily to the - # api_types hash (with that proc thing ->) I don't really know how to - # test this. - # connection.class shows GraphQL::Field - # connection.inspect shows some weird info + it "creates connections for :belongs_to associations" do + user_type = type_creator.create(User, %I[ id ]) + debate_type = type_creator.create(Debate, %I[ author ]) - expect(connection).to be_a(GraphQL::Field) - #debugger - expect(connection.type).to eq(user_type) - expect(connection.name).to eq('author') - end + connection = debate_type.fields['author'] - it ":has_one associations" do - skip "need to find association example that uses :has_one" - end + expect(connection).to be_a(GraphQL::Field) + expect(connection.type).to eq(user_type) + expect(connection.name).to eq('author') + end - it ":has_many associations" do - #skip "still don't know how to handle relay connections inside RSpec" - comment_type = type_creator.create(Comment, %I[ id ]) - debate_type = type_creator.create(Debate, %I[ author ]) + it "creates connections for :has_one associations" do + user_type = type_creator.create(User, %I[ organization ]) + organization_type = type_creator.create(Organization, %I[ id ]) - connection = debate_type.fields['comments'] + connection = user_type.fields['organization'] - # TODO: because connection types are created and added lazily to the - # api_types hash (with that proc thing ->) I don't really know how to - # test this. - # connection.class shows GraphQL::Field - # connection.inspect shows some weird info + expect(connection).to be_a(GraphQL::Field) + expect(connection.type).to eq(organization_type) + expect(connection.name).to eq('organization') + end - expect(connection).to be_a(GraphQL::Field) - expect(connection.type).to be_a(api_types[Comment]) - expect(connection.name).to eq('comments') - end + it "creates connections for :has_many associations" do + comment_type = type_creator.create(Comment, %I[ id ]) + debate_type = type_creator.create(Debate, %I[ comments ]) + + 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 From 6013f8494577491576a104183330f63f73221e78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Sat, 3 Dec 2016 13:36:14 +0100 Subject: [PATCH 060/155] Handle multiple kind of requests in GraphQL controller * Added support for GET requests * Added support for application/graphql content-type for POST requests * Handling query string parsing errors in controller * Wrote specs for all this --- app/controllers/graphql_controller.rb | 26 +++-- config/routes.rb | 1 + spec/controllers/graphql_controller_spec.rb | 110 +++++++------------- 3 files changed, 59 insertions(+), 78 deletions(-) diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index e3bedceea..be4f51f03 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -4,11 +4,25 @@ class GraphqlController < ApplicationController skip_authorization_check def query - # ConsulSchema.execute returns the query result in the shape of a Hash, which - # is sent back to the client rendered in JSON - render json: ConsulSchema.execute( - params[:query], - variables: params[:variables] || {} - ) + + if request.headers["CONTENT_TYPE"] == 'application/graphql' + query_string = request.body.string # request.body.class => StringIO + else + query_string = params[:query] + end + + if query_string.nil? + render json: { message: 'Query string not present' }, status: :bad_request + else + begin + response = ConsulSchema.execute( + query_string, + variables: params[:variables] || {} + ) + render json: response, status: :ok + rescue GraphQL::ParseError + render json: { message: 'Query string is not valid JSON' }, status: :bad_request + end + end end end diff --git a/config/routes.rb b/config/routes.rb index b530df52c..63be98384 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -264,6 +264,7 @@ Rails.application.routes.draw do # GraphQL mount GraphiQL::Rails::Engine, at: '/graphiql', graphql_path: '/graphql' + get '/graphql', to: 'graphql#query' post '/graphql', to: 'graphql#query' if Rails.env.development? diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb index e0244ee68..e95bdd3c9 100644 --- a/spec/controllers/graphql_controller_spec.rb +++ b/spec/controllers/graphql_controller_spec.rb @@ -1,90 +1,56 @@ require 'rails_helper' -# Hacerlo como los test de controlador de rails +# Useful resource: http://graphql.org/learn/serving-over-http/ describe GraphqlController, type: :request do let(:proposal) { create(:proposal) } - it "answers simple json queries" do - headers = { "CONTENT_TYPE" => "application/json" } - #post "/widgets", '{ "widget": { "name":"My Widget" } }', headers - post '/graphql', { query: "{ proposal(id: #{proposal.id}) { title } }" }.to_json, headers - expect(response).to have_http_status(200) - expect(JSON.parse(response.body)['data']['proposal']['title']).to eq(proposal.title) + 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(:bad_request) + expect(JSON.parse(response.body)['message']).to eq('Query string is not valid JSON') + 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 - -end + describe "handles POST request" do + let(:json_headers) { { "CONTENT_TYPE" => "application/json" } } -=begin -describe GraphqlController do - let(:uri) { URI::HTTP.build(host: 'localhost', path: '/graphql', port: 3000) } - let(:query_string) { "" } - let(:body) { {query: query_string}.to_json } - - describe "POST requests" do - let(:author) { create(:user) } - let(:proposal) { create(:proposal, author: author) } - let(:response) { HTTP.headers('Content-Type' => 'application/json').post(uri, body: body) } - let(:response_body) { JSON.parse(response.body) } - - context "when query string is valid" do - let(:query_string) { "{ proposal(id: #{proposal.id}) { title, author { username } } }" } - let(:returned_proposal) { response_body['data']['proposal'] } - - it "returns HTTP 200 OK" do - expect(response.code).to eq(200) - end - - it "returns first-level fields" do - expect(returned_proposal['title']).to eq(proposal.title) - end - - it "returns nested fields" do - expect(returned_proposal['author']['username']).to eq(author.username) - end + 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 - context "when query string asks for invalid fields" do - let(:query_string) { "{ proposal(id: #{proposal.id}) { missing_field } }" } - - it "returns HTTP 200 OK" do - expect(response.code).to eq(200) - end - - it "doesn't return any data" do - expect(response_body['data']).to be_nil - end - - it "returns error inside body" do - expect(response_body['errors']).to be_present - 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 - context "when query string is not valid" do - let(:query_string) { "invalid" } - - it "returns HTTP 400 Bad Request" do - expect(response.code).to eq(400) - end + specify "with malformed query string" do + post '/graphql', { query: "Malformed query string" }.to_json, json_headers + expect(response).to have_http_status(:bad_request) + expect(JSON.parse(response.body)['message']).to eq('Query string is not valid JSON') end - context "when query string is missing" do - let(:query_string) { nil } - - it "returns HTTP 400 Bad Request" do - expect(response.code).to eq(400) - 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 - - context "when body is missing" do - let(:body) { nil } - - it "returns HTTP 400 Bad Request" do - expect(response.code).to eq(400) - end - end - end end -=end From 42355e770c77c58e52043b912243cb0d1441d256 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 5 Dec 2016 18:42:17 +0100 Subject: [PATCH 061/155] Fixed bug related to wrong use of GraphQL::Relay connection helper --- lib/graph_ql/type_creator.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/graph_ql/type_creator.rb b/lib/graph_ql/type_creator.rb index b47624df9..a508c88d6 100644 --- a/lib/graph_ql/type_creator.rb +++ b/lib/graph_ql/type_creator.rb @@ -34,11 +34,11 @@ module GraphQL when :belongs_to field(association.name, -> { type_creator.created_types[association.klass] }) when :has_many - connection association.name, -> do - type_creator.created_types[association.klass].connection_type do - description "#{association.klass.model_name.human.pluralize}" - resolve -> (object, arguments, context) { association.klass.all } - end + connection( + association.name, + Proc.new { type_creator.created_types[association.klass].connection_type } + ) do + resolve -> (object, arguments, context) { association.klass.all } end end end From b2730b6239de3b8a088acc202858702da8ffb40a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Sun, 11 Dec 2016 15:20:17 +0100 Subject: [PATCH 062/155] DRY GraphQL::TypeCreator --- lib/graph_ql/type_creator.rb | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/graph_ql/type_creator.rb b/lib/graph_ql/type_creator.rb index a508c88d6..d0dfde580 100644 --- a/lib/graph_ql/type_creator.rb +++ b/lib/graph_ql/type_creator.rb @@ -27,19 +27,11 @@ module GraphQL if model.column_names.include?(field_name.to_s) field(field_name.to_s, TYPES_CONVERSION[model.columns_hash[field_name.to_s].type]) else - association = model.reflect_on_all_associations.find { |a| a.name == field_name } - case association.macro - when :has_one - field(association.name, -> { type_creator.created_types[association.klass] }) - when :belongs_to - field(association.name, -> { type_creator.created_types[association.klass] }) - when :has_many - connection( - association.name, - Proc.new { type_creator.created_types[association.klass].connection_type } - ) do - resolve -> (object, arguments, context) { association.klass.all } - end + association = type_creator.class.association?(model, field_name) + if type_creator.class.needs_pagination?(association) + connection association.name, -> { type_creator.created_types[association.klass].connection_type } + else + field association.name, -> { type_creator.created_types[association.klass] } end end end @@ -47,5 +39,13 @@ module GraphQL created_types[model] = created_type return created_type # GraphQL::ObjectType end + + def self.association?(model, field_name) + model.reflect_on_all_associations.find { |a| a.name == field_name } + end + + def self.needs_pagination?(association) + association.macro == :has_many + end end end From 82954c922a33519ac8da7c37647fb97609fdf97b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Sun, 11 Dec 2016 15:42:17 +0100 Subject: [PATCH 063/155] Expose public_author for proposals --- app/models/concerns/has_public_author.rb | 9 +++++++++ app/models/concerns/has_public_author_types.rb | 9 +++++++++ app/models/proposal.rb | 2 ++ config/initializers/graphql.rb | 8 ++++---- lib/graph_ql/type_creator.rb | 7 ++++--- 5 files changed, 28 insertions(+), 7 deletions(-) create mode 100644 app/models/concerns/has_public_author.rb create mode 100644 app/models/concerns/has_public_author_types.rb diff --git a/app/models/concerns/has_public_author.rb b/app/models/concerns/has_public_author.rb new file mode 100644 index 000000000..17cd5edd4 --- /dev/null +++ b/app/models/concerns/has_public_author.rb @@ -0,0 +1,9 @@ +module HasPublicAuthor + def public_author_id + public_author.try(:id) + end + + def public_author + self.author.public_activity? ? self.author : nil + end +end diff --git a/app/models/concerns/has_public_author_types.rb b/app/models/concerns/has_public_author_types.rb new file mode 100644 index 000000000..6555b8b4e --- /dev/null +++ b/app/models/concerns/has_public_author_types.rb @@ -0,0 +1,9 @@ +module HasPublicAuthorTypes + def public_author_id_type + GraphQL::INT_TYPE + end + + def public_author_type + TypeCreator.created_types[User] + end +end diff --git a/app/models/proposal.rb b/app/models/proposal.rb index 7bd4b7a92..8e67f23dd 100644 --- a/app/models/proposal.rb +++ b/app/models/proposal.rb @@ -6,6 +6,8 @@ class Proposal < ActiveRecord::Base include Sanitizable include Searchable include Filterable + include HasPublicAuthor + extend HasPublicAuthorTypes acts_as_votable acts_as_paranoid column: :hidden_at diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index 306c01890..7213b64d9 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -1,15 +1,15 @@ API_TYPE_DEFINITIONS = { User => %I[ id username proposals organization ], Debate => %I[ id title description author_id author created_at comments ], - Proposal => %I[ id title description author_id author created_at comments ], + Proposal => %I[ id title description author_id author public_author_id public_author created_at comments ], Comment => %I[ id body user_id user commentable_id ], Organization => %I[ id name ] } -type_creator = GraphQL::TypeCreator.new +TypeCreator = GraphQL::TypeCreator.new API_TYPE_DEFINITIONS.each do |model, fields| - type_creator.create(model, fields) + TypeCreator.create(model, fields) end ConsulSchema = GraphQL::Schema.define do @@ -29,7 +29,7 @@ QueryRoot = GraphQL::ObjectType.define do name "Query" description "The query root for this schema" - type_creator.created_types.each do |model, created_type| + TypeCreator.created_types.each do |model, created_type| # create an entry field to retrive a single object field model.name.underscore.to_sym do diff --git a/lib/graph_ql/type_creator.rb b/lib/graph_ql/type_creator.rb index d0dfde580..127599f95 100644 --- a/lib/graph_ql/type_creator.rb +++ b/lib/graph_ql/type_creator.rb @@ -22,17 +22,18 @@ module GraphQL name(model.name) description("#{model.model_name.human}") - # Make a field for each column + # Make a field for each column, association or method field_names.each do |field_name| if model.column_names.include?(field_name.to_s) field(field_name.to_s, TYPES_CONVERSION[model.columns_hash[field_name.to_s].type]) - else - association = type_creator.class.association?(model, field_name) + elsif association = type_creator.class.association?(model, field_name) if type_creator.class.needs_pagination?(association) connection association.name, -> { type_creator.created_types[association.klass].connection_type } else field association.name, -> { type_creator.created_types[association.klass] } end + else + field field_name.to_s, model.send("#{field_name}_type".to_sym) end end end From 402b07d959538895c724c60d61140fb6246b9557 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Tue, 13 Dec 2016 14:04:53 +0100 Subject: [PATCH 064/155] Expose Proposal public_author using scopes instead of modules --- app/models/concerns/has_public_author.rb | 9 --------- app/models/concerns/has_public_author_types.rb | 9 --------- app/models/proposal.rb | 3 +-- app/models/user.rb | 17 +++++++++-------- config/initializers/graphql.rb | 8 ++++---- lib/graph_ql/type_creator.rb | 5 ++--- 6 files changed, 16 insertions(+), 35 deletions(-) delete mode 100644 app/models/concerns/has_public_author.rb delete mode 100644 app/models/concerns/has_public_author_types.rb diff --git a/app/models/concerns/has_public_author.rb b/app/models/concerns/has_public_author.rb deleted file mode 100644 index 17cd5edd4..000000000 --- a/app/models/concerns/has_public_author.rb +++ /dev/null @@ -1,9 +0,0 @@ -module HasPublicAuthor - def public_author_id - public_author.try(:id) - end - - def public_author - self.author.public_activity? ? self.author : nil - end -end diff --git a/app/models/concerns/has_public_author_types.rb b/app/models/concerns/has_public_author_types.rb deleted file mode 100644 index 6555b8b4e..000000000 --- a/app/models/concerns/has_public_author_types.rb +++ /dev/null @@ -1,9 +0,0 @@ -module HasPublicAuthorTypes - def public_author_id_type - GraphQL::INT_TYPE - end - - def public_author_type - TypeCreator.created_types[User] - end -end diff --git a/app/models/proposal.rb b/app/models/proposal.rb index 8e67f23dd..1f9ede642 100644 --- a/app/models/proposal.rb +++ b/app/models/proposal.rb @@ -6,8 +6,6 @@ class Proposal < ActiveRecord::Base include Sanitizable include Searchable include Filterable - include HasPublicAuthor - extend HasPublicAuthorTypes acts_as_votable acts_as_paranoid column: :hidden_at @@ -16,6 +14,7 @@ class Proposal < ActiveRecord::Base RETIRE_OPTIONS = %w(duplicated started unfeasible done other) belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' + belongs_to :public_author, -> { public_activity }, class_name: 'User', foreign_key: 'author_id' belongs_to :geozone has_many :comments, as: :commentable has_many :proposal_notifications diff --git a/app/models/user.rb b/app/models/user.rb index 3ca79cedf..a54be0a16 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -46,14 +46,15 @@ class User < ActiveRecord::Base attr_accessor :skip_password_validation attr_accessor :use_redeemable_code - scope :administrators, -> { joins(:administrators) } - scope :moderators, -> { joins(:moderator) } - scope :organizations, -> { joins(:organization) } - scope :officials, -> { where("official_level > 0") } - scope :for_render, -> { includes(:organization) } - scope :by_document, -> (document_type, document_number) { where(document_type: document_type, document_number: document_number) } - scope :email_digest, -> { where(email_digest: true) } - scope :active, -> { where(erased_at: nil) } + scope :administrators, -> { joins(:administrators) } + scope :moderators, -> { joins(:moderator) } + scope :organizations, -> { joins(:organization) } + scope :officials, -> { where("official_level > 0") } + scope :for_render, -> { includes(:organization) } + scope :by_document, -> (document_type, document_number) { where(document_type: document_type, document_number: document_number) } + scope :email_digest, -> { where(email_digest: true) } + scope :active, -> { where(erased_at: nil) } + scope :public_activity, -> { where(public_activity: true) } before_validation :clean_document_number diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index 7213b64d9..1cfd238aa 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -1,15 +1,15 @@ API_TYPE_DEFINITIONS = { User => %I[ id username proposals organization ], Debate => %I[ id title description author_id author created_at comments ], - Proposal => %I[ id title description author_id author public_author_id public_author created_at comments ], + Proposal => %I[ id title description public_author created_at comments ], Comment => %I[ id body user_id user commentable_id ], Organization => %I[ id name ] } -TypeCreator = GraphQL::TypeCreator.new +type_creator = GraphQL::TypeCreator.new API_TYPE_DEFINITIONS.each do |model, fields| - TypeCreator.create(model, fields) + type_creator.create(model, fields) end ConsulSchema = GraphQL::Schema.define do @@ -29,7 +29,7 @@ QueryRoot = GraphQL::ObjectType.define do name "Query" description "The query root for this schema" - TypeCreator.created_types.each do |model, created_type| + type_creator.created_types.each do |model, created_type| # create an entry field to retrive a single object field model.name.underscore.to_sym do diff --git a/lib/graph_ql/type_creator.rb b/lib/graph_ql/type_creator.rb index 127599f95..3f35c2c1e 100644 --- a/lib/graph_ql/type_creator.rb +++ b/lib/graph_ql/type_creator.rb @@ -26,14 +26,13 @@ module GraphQL field_names.each do |field_name| if model.column_names.include?(field_name.to_s) field(field_name.to_s, TYPES_CONVERSION[model.columns_hash[field_name.to_s].type]) - elsif association = type_creator.class.association?(model, field_name) + else + association = type_creator.class.association?(model, field_name) if type_creator.class.needs_pagination?(association) connection association.name, -> { type_creator.created_types[association.klass].connection_type } else field association.name, -> { type_creator.created_types[association.klass] } end - else - field field_name.to_s, model.send("#{field_name}_type".to_sym) end end end From c155da5e10b3ec00eb792b2052900e1775ea78a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Tue, 13 Dec 2016 14:20:43 +0100 Subject: [PATCH 065/155] Change seeds to provide meaningful data for querying public_author from API --- db/dev_seeds.rb | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/db/dev_seeds.rb b/db/dev_seeds.rb index 87dc94a2e..b0a2347ee 100644 --- a/db/dev_seeds.rb +++ b/db/dev_seeds.rb @@ -41,7 +41,15 @@ puts "Creating Users" def create_user(email, username = Faker::Name.name) pwd = '12345678' puts " #{username}" - 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", + public_activity: (rand(1..100) > 30) + ) end admin = create_user('admin@consul.dev', 'admin') From 10eedebcb2154a6f2743bce32135f6ade94d424c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Tue, 13 Dec 2016 14:26:00 +0100 Subject: [PATCH 066/155] Refactor GraphQL specs to use public_author instead of author --- spec/lib/graphql_spec.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/lib/graphql_spec.rb b/spec/lib/graphql_spec.rb index 50e115a7f..eed7c6d34 100644 --- a/spec/lib/graphql_spec.rb +++ b/spec/lib/graphql_spec.rb @@ -35,8 +35,8 @@ describe ConsulSchema do end it "returns belongs_to associations" do - response = execute("{ proposal(id: #{proposal.id}) { author { username } } }") - expect(dig(response, 'data.proposal.author.username')).to eq(proposal.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) end it "returns has_many associations" do @@ -55,9 +55,9 @@ describe ConsulSchema 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}) { author { organization { name } } } }") + response = execute("{ proposal(id: #{org_proposal.id}) { public_author { organization { name } } } }") - expect(dig(response, 'data.proposal.author.organization.name')).to eq(organization.name) + expect(dig(response, 'data.proposal.public_author.organization.name')).to eq(organization.name) end it "hides confidential fields of Int type" do @@ -89,7 +89,7 @@ describe ConsulSchema do end it "hides confidential fields inside deeply nested queries" do - response = execute("{ proposals(first: 1) { edges { node { author { encrypted_password } } } } }") + response = execute("{ proposals(first: 1) { edges { node { public_author { encrypted_password } } } } }") expect(hidden_field?(response, 'encrypted_password')).to be_truthy end end From efade39412e77f5691144796d99b508bbdba5b57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Tue, 13 Dec 2016 14:40:22 +0100 Subject: [PATCH 067/155] Added specs for User 'public_activity' scope --- spec/factories.rb | 3 ++- spec/models/user_spec.rb | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/spec/factories.rb b/spec/factories.rb index 7b446f3a0..63d870fd9 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -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| diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index d7235dc86..a76765cc5 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -351,6 +351,18 @@ describe User do end end + + describe "public activity" do + it "returns users whose activity feed is public" do + user1 = create(:user) + user2 = create(:user, public_activity: false) + user3 = create(:user) + + expect(User.public_activity).to include(user1) + expect(User.public_activity).not_to include(user2) + expect(User.public_activity).to include(user3) + end + end end describe "self.search" do From 64cf3cde34fc971efd41d781effaec669229d7d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 26 Dec 2016 12:52:23 +0100 Subject: [PATCH 068/155] Modified seeds to provide more meaningful data --- db/dev_seeds.rb | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/db/dev_seeds.rb b/db/dev_seeds.rb index b0a2347ee..5d1d19588 100644 --- a/db/dev_seeds.rb +++ b/db/dev_seeds.rb @@ -39,16 +39,17 @@ puts "Creating Geozones" puts "Creating Users" def create_user(email, username = Faker::Name.name) - pwd = '12345678' puts " #{username}" User.create!( - username: username, - email: email, - password: pwd, - password_confirmation: pwd, - confirmed_at: Time.current, - terms_of_service: "1", - public_activity: (rand(1..100) > 30) + username: username, + email: email, + password: '12345678', + password_confirmation: '12345678', + 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 @@ -86,11 +87,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) ) @@ -252,7 +253,7 @@ end puts "Voting Debates, Proposals & Comments" (1..100).each 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) @@ -266,7 +267,7 @@ end end (1..100).each 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 From 5daf59e81396decd1cedc98736c748d225cc67a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 26 Dec 2016 13:42:45 +0100 Subject: [PATCH 069/155] Update Proposal fields exposed in API --- config/initializers/graphql.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index 1cfd238aa..f0b27dac6 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -1,7 +1,7 @@ API_TYPE_DEFINITIONS = { User => %I[ id username proposals organization ], Debate => %I[ id title description author_id author created_at comments ], - Proposal => %I[ id title description public_author created_at comments ], + Proposal => %I[ id title description external_url cached_votes_up comments_count hot_score confidence_score created_at summary video_url geozone_id retired_at retired_reason retired_explanation geozone comments public_author ], Comment => %I[ id body user_id user commentable_id ], Organization => %I[ id name ] } From 2311cd4b1d3c8719cbafbba3d28b9f39e0b2edcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 26 Dec 2016 18:42:12 +0100 Subject: [PATCH 070/155] Renamed User::public_activity scope --- app/models/debate.rb | 1 + app/models/proposal.rb | 2 +- app/models/user.rb | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/models/debate.rb b/app/models/debate.rb index 020259d80..13cff5b3f 100644 --- a/app/models/debate.rb +++ b/app/models/debate.rb @@ -13,6 +13,7 @@ class Debate < ActiveRecord::Base include ActsAsParanoidAliases belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' + belongs_to :public_author, -> { with_public_activity }, class_name: 'User', foreign_key: 'author_id' belongs_to :geozone has_many :comments, as: :commentable diff --git a/app/models/proposal.rb b/app/models/proposal.rb index 1f9ede642..64cadbb24 100644 --- a/app/models/proposal.rb +++ b/app/models/proposal.rb @@ -14,7 +14,7 @@ class Proposal < ActiveRecord::Base RETIRE_OPTIONS = %w(duplicated started unfeasible done other) belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' - belongs_to :public_author, -> { public_activity }, class_name: 'User', foreign_key: 'author_id' + belongs_to :public_author, -> { with_public_activity }, class_name: 'User', foreign_key: 'author_id' belongs_to :geozone has_many :comments, as: :commentable has_many :proposal_notifications diff --git a/app/models/user.rb b/app/models/user.rb index c82ae3435..4952c286c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -54,7 +54,7 @@ class User < ActiveRecord::Base scope :by_document, -> (document_type, document_number) { where(document_type: document_type, document_number: document_number) } scope :email_digest, -> { where(email_digest: true) } scope :active, -> { where(erased_at: nil) } - scope :public_activity, -> { where(public_activity: true) } + scope :with_public_activity, -> { where(public_activity: true) } before_validation :clean_document_number From 2741dcf03ca384ce91f9c41a09bdd10bb5b9b17f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 26 Dec 2016 20:45:46 +0100 Subject: [PATCH 071/155] Temporary remove organizations from API --- config/initializers/graphql.rb | 2 -- spec/lib/graphql_spec.rb | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index f0b27dac6..420d53dcf 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -1,9 +1,7 @@ API_TYPE_DEFINITIONS = { - User => %I[ id username proposals organization ], Debate => %I[ id title description author_id author created_at comments ], Proposal => %I[ id title description external_url cached_votes_up comments_count hot_score confidence_score created_at summary video_url geozone_id retired_at retired_reason retired_explanation geozone comments public_author ], Comment => %I[ id body user_id user commentable_id ], - Organization => %I[ id name ] } type_creator = GraphQL::TypeCreator.new diff --git a/spec/lib/graphql_spec.rb b/spec/lib/graphql_spec.rb index eed7c6d34..3dbf393b7 100644 --- a/spec/lib/graphql_spec.rb +++ b/spec/lib/graphql_spec.rb @@ -29,6 +29,7 @@ describe ConsulSchema do end it "returns has_one associations" do + pending "Organizations are not being exposed yet" organization = create(:organization) response = execute("{ user(id: #{organization.user_id}) { organization { name } } }") expect(dig(response, 'data.user.organization.name')).to eq(organization.name) @@ -52,6 +53,7 @@ describe ConsulSchema do end it "executes deeply nested queries" do + pending "Organizations are not being exposed yet" org_user = create(:user) organization = create(:organization, user: org_user) org_proposal = create(:proposal, author: org_user) From 506696df0a31ee1602af432f5c0448ff3cb354fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Tue, 27 Dec 2016 18:17:02 +0100 Subject: [PATCH 072/155] Call ::public_for_api if available --- config/initializers/graphql.rb | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index 420d53dcf..02c8c750f 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -34,17 +34,25 @@ QueryRoot = GraphQL::ObjectType.define do type created_type description "Find one #{model.model_name.human} by ID" argument :id, !types.ID - resolve -> (object, arguments, context) { - model.find(arguments["id"]) - } + resolve -> (object, arguments, context) do + if model.respond_to?(:public_for_api) + model.public_for_api.find(arguments["id"]) + else + model.find(arguments["id"]) + end + end end # create an entry filed to retrive a paginated collection connection model.name.underscore.pluralize.to_sym, created_type.connection_type do description "Find all #{model.model_name.human.pluralize}" - resolve -> (object, arguments, context) { - model.all - } + resolve -> (object, arguments, context) do + if model.respond_to?(:public_for_api) + model.public_for_api + else + model.all + end + end end end From 0e501e2edd6f984ac522876a7158d263edf80f08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Tue, 27 Dec 2016 18:18:00 +0100 Subject: [PATCH 073/155] Update API fields for Debate, User and Geozone --- config/initializers/graphql.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index 02c8c750f..bf5d8d8f9 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -1,7 +1,9 @@ API_TYPE_DEFINITIONS = { - Debate => %I[ id title description author_id author created_at comments ], + User => %I[ id username proposals ], + Debate => %I[ id title description created_at cached_votes_total cached_votes_up cached_votes_down comments_count hot_score confidence_score geozone_id geozone comments public_author ], Proposal => %I[ id title description external_url cached_votes_up comments_count hot_score confidence_score created_at summary video_url geozone_id retired_at retired_reason retired_explanation geozone comments public_author ], Comment => %I[ id body user_id user commentable_id ], + Geozone => %I[ id name ] } type_creator = GraphQL::TypeCreator.new From 90ff84b6efb4db78d18b74b10f84fbd5c6971089 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Tue, 27 Dec 2016 18:18:22 +0100 Subject: [PATCH 074/155] Update API fields for Comment --- app/models/comment.rb | 7 ++++++ config/initializers/graphql.rb | 2 +- spec/models/comment_spec.rb | 41 ++++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/app/models/comment.rb b/app/models/comment.rb index 0bfb6a320..a1d89a33f 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -16,6 +16,7 @@ class Comment < ActiveRecord::Base belongs_to :commentable, -> { with_hidden }, polymorphic: true, counter_cache: true belongs_to :user, -> { with_hidden } + belongs_to :public_author, -> { with_public_activity }, class_name: 'User', foreign_key: 'user_id' before_save :calculate_confidence_score @@ -24,6 +25,12 @@ class Comment < ActiveRecord::Base 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 + joins("FULL OUTER JOIN debates ON commentable_type = 'Debate' AND commentable_id = debates.id"). + joins("FULL OUTER JOIN proposals ON commentable_type = 'Proposal' AND commentable_id = proposals.id"). + where("commentable_type = 'Proposal' AND proposals.hidden_at IS NULL OR commentable_type = 'Debate' AND debates.hidden_at IS NULL") + 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) } diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index bf5d8d8f9..b6d98562b 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -2,7 +2,7 @@ API_TYPE_DEFINITIONS = { User => %I[ id username proposals ], Debate => %I[ id title description created_at cached_votes_total cached_votes_up cached_votes_down comments_count hot_score confidence_score geozone_id geozone comments public_author ], Proposal => %I[ id title description external_url cached_votes_up comments_count hot_score confidence_score created_at summary video_url geozone_id retired_at retired_reason retired_explanation geozone comments public_author ], - Comment => %I[ id body user_id user commentable_id ], + Comment => %I[ id commentable_id commentable_type body created_at cached_votes_total cached_votes_up cached_votes_down ancestry confidence_score public_author ], Geozone => %I[ id name ] } diff --git a/spec/models/comment_spec.rb b/spec/models/comment_spec.rb index b899b4e89..ecd167237 100644 --- a/spec/models/comment_spec.rb +++ b/spec/models/comment_spec.rb @@ -132,4 +132,45 @@ describe Comment do end end + describe "public_for_api" 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 + end end From 989680407085a5ea44b8d9f451ed97f120cb1d5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Tue, 27 Dec 2016 18:30:35 +0100 Subject: [PATCH 075/155] Fixes typo in spec --- spec/models/user_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index e04ea9291..239815c10 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -358,9 +358,9 @@ describe User do user2 = create(:user, public_activity: false) user3 = create(:user) - expect(User.public_activity).to include(user1) - expect(User.public_activity).not_to include(user2) - expect(User.public_activity).to include(user3) + expect(User.with_public_activity).to include(user1) + expect(User.with_public_activity).not_to include(user2) + expect(User.with_public_activity).to include(user3) end end end From 074a8182f61126d6ce477e04126d41f26bd65e7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 29 Dec 2016 10:58:40 +0100 Subject: [PATCH 076/155] Add ProposalNotification to api --- app/models/proposal_notification.rb | 2 ++ config/initializers/graphql.rb | 3 ++- spec/models/proposal_notification_spec.rb | 16 ++++++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/app/models/proposal_notification.rb b/app/models/proposal_notification.rb index 60912d887..686f3ce90 100644 --- a/app/models/proposal_notification.rb +++ b/app/models/proposal_notification.rb @@ -7,6 +7,8 @@ class ProposalNotification < ActiveRecord::Base validates :proposal, presence: true validate :minimum_interval + scope :public_for_api, -> { joins(:proposal).where("proposals.hidden_at IS NULL") } + 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 diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index b6d98562b..b899476f7 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -1,9 +1,10 @@ API_TYPE_DEFINITIONS = { User => %I[ id username proposals ], Debate => %I[ id title description created_at cached_votes_total cached_votes_up cached_votes_down comments_count hot_score confidence_score geozone_id geozone comments public_author ], - Proposal => %I[ id title description external_url cached_votes_up comments_count hot_score confidence_score created_at summary video_url geozone_id retired_at retired_reason retired_explanation geozone comments public_author ], + Proposal => %I[ id title description external_url cached_votes_up comments_count hot_score confidence_score created_at summary video_url geozone_id retired_at retired_reason retired_explanation geozone comments proposal_notifications public_author ], Comment => %I[ id commentable_id commentable_type body created_at cached_votes_total cached_votes_up cached_votes_down ancestry confidence_score public_author ], Geozone => %I[ id name ] + ProposalNotification => %I[ title body proposal_id created_at proposal ], } type_creator = GraphQL::TypeCreator.new diff --git a/spec/models/proposal_notification_spec.rb b/spec/models/proposal_notification_spec.rb index f66b254b6..cd8bf346a 100644 --- a/spec/models/proposal_notification_spec.rb +++ b/spec/models/proposal_notification_spec.rb @@ -22,6 +22,22 @@ 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 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 + end + describe "minimum interval between notifications" do before(:each) do From 80c3108a18eee72e4da5c2f932ccbeda162f6d26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 29 Dec 2016 11:13:57 +0100 Subject: [PATCH 077/155] Add Tag to api --- app/models/tag.rb | 3 +++ config/initializers/graphql.rb | 1 + spec/models/tag_spec.rb | 24 ++++++++++++++++++++++++ 3 files changed, 28 insertions(+) create mode 100644 app/models/tag.rb create mode 100644 spec/models/tag_spec.rb diff --git a/app/models/tag.rb b/app/models/tag.rb new file mode 100644 index 000000000..8f34a8696 --- /dev/null +++ b/app/models/tag.rb @@ -0,0 +1,3 @@ +class Tag < ActsAsTaggableOn::Tag + scope :public_for_api, -> { where("kind IS NULL OR kind = 'category'") } +end diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index b899476f7..a7e1bfa85 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -5,6 +5,7 @@ API_TYPE_DEFINITIONS = { Comment => %I[ id commentable_id commentable_type body created_at cached_votes_total cached_votes_up cached_votes_down ancestry confidence_score public_author ], Geozone => %I[ id name ] ProposalNotification => %I[ title body proposal_id created_at proposal ], + Tag => %I[ id name taggings_count kind ], } type_creator = GraphQL::TypeCreator.new diff --git a/spec/models/tag_spec.rb b/spec/models/tag_spec.rb new file mode 100644 index 000000000..fa26b7622 --- /dev/null +++ b/spec/models/tag_spec.rb @@ -0,0 +1,24 @@ +require 'rails_helper' + +describe 'Tag' do + + describe "public_for_api scope" do + it "returns tags whose kind is NULL" do + tag = create(:tag, kind: nil) + + expect(Tag.public_for_api).to include(tag) + end + + it "returns tags whose kind is 'category'" do + tag = create(:tag, kind: 'category') + + expect(Tag.public_for_api).to include(tag) + end + + it "blocks other kinds of tags" do + tag = create(:tag, kind: 'foo') + + expect(Tag.public_for_api).not_to include(tag) + end + end +end From 90130f20e796f42d0a54dd3d72accbc4ff9a21b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 29 Dec 2016 12:03:00 +0100 Subject: [PATCH 078/155] Add Votes info to api --- app/models/user.rb | 18 ++++++------ app/models/vote.rb | 10 ++++++- config/initializers/graphql.rb | 5 ++-- spec/models/vote_spec.rb | 51 ++++++++++++++++++++++++++++++++++ 4 files changed, 73 insertions(+), 11 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 4952c286c..f1e48c572 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -46,15 +46,17 @@ class User < ActiveRecord::Base attr_accessor :skip_password_validation attr_accessor :use_redeemable_code - scope :administrators, -> { joins(:administrators) } - scope :moderators, -> { joins(:moderator) } - scope :organizations, -> { joins(:organization) } - scope :officials, -> { where("official_level > 0") } - scope :for_render, -> { includes(:organization) } - scope :by_document, -> (document_type, document_number) { where(document_type: document_type, document_number: document_number) } - scope :email_digest, -> { where(email_digest: true) } - scope :active, -> { where(erased_at: nil) } + scope :administrators, -> { joins(:administrators) } + scope :moderators, -> { joins(:moderator) } + scope :organizations, -> { joins(:organization) } + scope :officials, -> { where("official_level > 0") } + scope :for_render, -> { includes(:organization) } + scope :by_document, -> (document_type, document_number) { where(document_type: document_type, document_number: document_number) } + scope :email_digest, -> { where(email_digest: true) } + scope :active, -> { where(erased_at: nil) } scope :with_public_activity, -> { where(public_activity: true) } + scope :public_for_api, -> { select("id, username") } + scope :for_votes, -> { select("users.gender, users.geozone_id") } before_validation :clean_document_number diff --git a/app/models/vote.rb b/app/models/vote.rb index 47dd3f007..a6b0691c6 100644 --- a/app/models/vote.rb +++ b/app/models/vote.rb @@ -1,2 +1,10 @@ class Vote < ActsAsVotable::Vote -end \ No newline at end of file + belongs_to :public_voter, -> { for_votes }, class_name: 'User', foreign_key: :voter_id + + scope :public_for_api, -> do + 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 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") + end +end diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index a7e1bfa85..e837f294b 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -1,11 +1,12 @@ API_TYPE_DEFINITIONS = { - User => %I[ id username proposals ], + User => %I[ id username gender geozone_id geozone ], Debate => %I[ id title description created_at cached_votes_total cached_votes_up cached_votes_down comments_count hot_score confidence_score geozone_id geozone comments public_author ], Proposal => %I[ id title description external_url cached_votes_up comments_count hot_score confidence_score created_at summary video_url geozone_id retired_at retired_reason retired_explanation geozone comments proposal_notifications public_author ], Comment => %I[ id commentable_id commentable_type body created_at cached_votes_total cached_votes_up cached_votes_down ancestry confidence_score public_author ], - Geozone => %I[ id name ] + Geozone => %I[ id name ], ProposalNotification => %I[ title body proposal_id created_at proposal ], Tag => %I[ id name taggings_count kind ], + Vote => %I[ votable_id votable_type created_at public_voter ] } type_creator = GraphQL::TypeCreator.new diff --git a/spec/models/vote_spec.rb b/spec/models/vote_spec.rb index 1a44e7606..14929ef2c 100644 --- a/spec/models/vote_spec.rb +++ b/spec/models/vote_spec.rb @@ -40,4 +40,55 @@ 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 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 + end end From 0aed5338f64d6dc9fb2825d85e818c8d35ad143d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 29 Dec 2016 14:17:44 +0100 Subject: [PATCH 079/155] Customize resolvers to only permit allowed records --- lib/graph_ql/type_creator.rb | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/graph_ql/type_creator.rb b/lib/graph_ql/type_creator.rb index 3f35c2c1e..74c9b6fbe 100644 --- a/lib/graph_ql/type_creator.rb +++ b/lib/graph_ql/type_creator.rb @@ -28,10 +28,22 @@ module GraphQL field(field_name.to_s, TYPES_CONVERSION[model.columns_hash[field_name.to_s].type]) else association = type_creator.class.association?(model, field_name) + target_model = association.klass + public_elements = target_model.respond_to?(:public_for_api) ? target_model.public_for_api : target_model.all + if type_creator.class.needs_pagination?(association) - connection association.name, -> { type_creator.created_types[association.klass].connection_type } + connection(association.name, -> { type_creator.created_types[target_model].connection_type }) do + resolve -> (object, arguments, context) do + object.send(association.name).all & public_elements.all + end + end else - field association.name, -> { type_creator.created_types[association.klass] } + field(association.name, -> { type_creator.created_types[target_model] }) do + resolve -> (object, arguments, context) do + linked_element = object.send(field_name) + public_elements.include?(linked_element) ? linked_element : nil + end + end end end end From 0f663603d0af0876e481d5c5b8a071335b346009 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 29 Dec 2016 14:19:11 +0100 Subject: [PATCH 080/155] Only create field to retrieve single objects in QueryRoot if ID was exposed --- config/initializers/graphql.rb | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index e837f294b..d2353e308 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -35,15 +35,17 @@ QueryRoot = GraphQL::ObjectType.define do type_creator.created_types.each do |model, created_type| # create an entry field to retrive a single object - field model.name.underscore.to_sym do - type created_type - description "Find one #{model.model_name.human} by ID" - argument :id, !types.ID - resolve -> (object, arguments, context) do - if model.respond_to?(:public_for_api) - model.public_for_api.find(arguments["id"]) - else - model.find(arguments["id"]) + if API_TYPE_DEFINITIONS[model].include?(:id) + field model.name.underscore.to_sym do + type created_type + description "Find one #{model.model_name.human} by ID" + argument :id, !types.ID + resolve -> (object, arguments, context) do + if model.respond_to?(:public_for_api) + model.public_for_api.find(arguments["id"]) + else + model.find(arguments["id"]) + end end end end From 7dc4d80e7bbab49a45d818ccc321a2abfe291be5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Sun, 1 Jan 2017 19:36:07 +0100 Subject: [PATCH 081/155] Read GraphQL field types from file instead of from database --- config/initializers/graphql.rb | 90 ++++++++++++++++++++++++++++++---- lib/graph_ql/type_creator.rb | 59 +++++++++++----------- spec/lib/type_creator_spec.rb | 16 +++--- 3 files changed, 120 insertions(+), 45 deletions(-) diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index d2353e308..e9e64bac4 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -1,12 +1,84 @@ API_TYPE_DEFINITIONS = { - User => %I[ id username gender geozone_id geozone ], - Debate => %I[ id title description created_at cached_votes_total cached_votes_up cached_votes_down comments_count hot_score confidence_score geozone_id geozone comments public_author ], - Proposal => %I[ id title description external_url cached_votes_up comments_count hot_score confidence_score created_at summary video_url geozone_id retired_at retired_reason retired_explanation geozone comments proposal_notifications public_author ], - Comment => %I[ id commentable_id commentable_type body created_at cached_votes_total cached_votes_up cached_votes_down ancestry confidence_score public_author ], - Geozone => %I[ id name ], - ProposalNotification => %I[ title body proposal_id created_at proposal ], - Tag => %I[ id name taggings_count kind ], - Vote => %I[ votable_id votable_type created_at public_voter ] + User => { + id: :integer, + username: :string, + gender: :string, + geozone_id: :integer, + geozone: Geozone + }, + Debate => { + id: :integer, + title: :string, + description: :string, + created_at: :string, + cached_votes_total: :integer, + cached_votes_up: :integer, + cached_votes_down: :integer, + comments_count: :integer, + hot_score: :integer, + confidence_score: :integer, + geozone_id: :integer, + geozone: Geozone, + comments: [Comment], + public_author: User + }, + Proposal => { + id: :integer, + title: :string, + description: :sting, + external_url: :string, + cached_votes_up: :integer, + comments_count: :integer, + hot_score: :integer, + confidence_score: :integer, + 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 + }, + Comment => { + id: :integer, + commentable_id: :integer, + commentable_type: :string, + body: :string, + created_at: :string, + cached_votes_total: :integer, + cached_votes_up: :integer, + cached_votes_down: :integer, + ancestry: :string, + confidence_score: :integer, + public_author: User + }, + Geozone => { + id: :integer, + name: :string + }, + ProposalNotification => { + title: :string, + body: :string, + proposal_id: :integer, + created_at: :string, + proposal: Proposal + }, + Tag => { + id: :integer, + name: :string, + taggings_count: :integer, + kind: :string + }, + Vote => { + votable_id: :integer, + votable_type: :string, + created_at: :string, + public_voter: User + } } type_creator = GraphQL::TypeCreator.new @@ -35,7 +107,7 @@ QueryRoot = GraphQL::ObjectType.define do type_creator.created_types.each do |model, created_type| # create an entry field to retrive a single object - if API_TYPE_DEFINITIONS[model].include?(:id) + if API_TYPE_DEFINITIONS[model][:id] field model.name.underscore.to_sym do type created_type description "Find one #{model.model_name.human} by ID" diff --git a/lib/graph_ql/type_creator.rb b/lib/graph_ql/type_creator.rb index 74c9b6fbe..468577cff 100644 --- a/lib/graph_ql/type_creator.rb +++ b/lib/graph_ql/type_creator.rb @@ -1,12 +1,13 @@ module GraphQL class TypeCreator # Return a GraphQL type for a 'database_type' - TYPES_CONVERSION = Hash.new(GraphQL::STRING_TYPE).merge( + SCALAR_TYPES = { integer: GraphQL::INT_TYPE, boolean: GraphQL::BOOLEAN_TYPE, float: GraphQL::FLOAT_TYPE, - double: GraphQL::FLOAT_TYPE - ) + double: GraphQL::FLOAT_TYPE, + string: GraphQL::STRING_TYPE + } attr_accessor :created_types @@ -14,7 +15,7 @@ module GraphQL @created_types = {} end - def create(model, field_names) + def create(model, fields) type_creator = self created_type = GraphQL::ObjectType.define do @@ -23,41 +24,43 @@ module GraphQL description("#{model.model_name.human}") # Make a field for each column, association or method - field_names.each do |field_name| - if model.column_names.include?(field_name.to_s) - field(field_name.to_s, TYPES_CONVERSION[model.columns_hash[field_name.to_s].type]) - else - association = type_creator.class.association?(model, field_name) - target_model = association.klass - public_elements = target_model.respond_to?(:public_for_api) ? target_model.public_for_api : target_model.all - - if type_creator.class.needs_pagination?(association) - connection(association.name, -> { type_creator.created_types[target_model].connection_type }) do - resolve -> (object, arguments, context) do - object.send(association.name).all & public_elements.all - end + fields.each do |name, type| + case GraphQL::TypeCreator.type_kind(type) + when :scalar + field name, SCALAR_TYPES[type] + when :simple_association + field(name, -> { type_creator.created_types[type] }) do + resolve -> (object, arguments, context) do + target_public_elements = type.respond_to?(:public_for_api) ? type.public_for_api : type.all + wanted_element = object.send(name) + target_public_elements.include?(wanted_element) ? wanted_element : nil end - else - field(association.name, -> { type_creator.created_types[target_model] }) do - resolve -> (object, arguments, context) do - linked_element = object.send(field_name) - public_elements.include?(linked_element) ? linked_element : nil - end + end + when :paginated_association + type = type.first + connection(name, -> { type_creator.created_types[type].connection_type }) do + resolve -> (object, arguments, context) do + target_public_elements = type.respond_to?(:public_for_api) ? type.public_for_api : type.all + object.send(name).all & target_public_elements.all end end end end + end created_types[model] = created_type return created_type # GraphQL::ObjectType end - def self.association?(model, field_name) - model.reflect_on_all_associations.find { |a| a.name == field_name } + def self.type_kind(type) + if SCALAR_TYPES[type] + :scalar + elsif type.class == Class + :simple_association + elsif type.class == Array + :paginated_association + end end - def self.needs_pagination?(association) - association.macro == :has_many - end end end diff --git a/spec/lib/type_creator_spec.rb b/spec/lib/type_creator_spec.rb index 2fc1bf78d..1e25b91fd 100644 --- a/spec/lib/type_creator_spec.rb +++ b/spec/lib/type_creator_spec.rb @@ -5,7 +5,7 @@ describe GraphQL::TypeCreator do describe "::create" do it "creates fields for Int attributes" do - debate_type = type_creator.create(Debate, %I[ id ]) + debate_type = type_creator.create(Debate, { id: :integer }) created_field = debate_type.fields['id'] expect(created_field).to be_a(GraphQL::Field) @@ -14,7 +14,7 @@ describe GraphQL::TypeCreator do end it "creates fields for String attributes" do - debate_type = type_creator.create(Debate, %I[ title ]) + debate_type = type_creator.create(Debate, { title: :string }) created_field = debate_type.fields['title'] expect(created_field).to be_a(GraphQL::Field) @@ -23,8 +23,8 @@ describe GraphQL::TypeCreator do end it "creates connections for :belongs_to associations" do - user_type = type_creator.create(User, %I[ id ]) - debate_type = type_creator.create(Debate, %I[ author ]) + user_type = type_creator.create(User, { id: :integer }) + debate_type = type_creator.create(Debate, { author: User }) connection = debate_type.fields['author'] @@ -34,8 +34,8 @@ describe GraphQL::TypeCreator do end it "creates connections for :has_one associations" do - user_type = type_creator.create(User, %I[ organization ]) - organization_type = type_creator.create(Organization, %I[ id ]) + user_type = type_creator.create(User, { organization: Organization }) + organization_type = type_creator.create(Organization, { id: :integer }) connection = user_type.fields['organization'] @@ -45,8 +45,8 @@ describe GraphQL::TypeCreator do end it "creates connections for :has_many associations" do - comment_type = type_creator.create(Comment, %I[ id ]) - debate_type = type_creator.create(Debate, %I[ comments ]) + comment_type = type_creator.create(Comment, { id: :integer }) + debate_type = type_creator.create(Debate, { comments: [Comment] }) connection = debate_type.fields['comments'] From 9c6001e9874e8587dfd71e6a498aca0fb74ce5c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Sun, 1 Jan 2017 20:46:08 +0100 Subject: [PATCH 082/155] Move public_author into module --- app/models/comment.rb | 2 +- app/models/concerns/has_public_author.rb | 5 +++++ app/models/debate.rb | 2 +- app/models/proposal.rb | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 app/models/concerns/has_public_author.rb diff --git a/app/models/comment.rb b/app/models/comment.rb index a1d89a33f..1d5c9aa30 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -1,5 +1,6 @@ class Comment < ActiveRecord::Base include Flaggable + include HasPublicAuthor acts_as_paranoid column: :hidden_at include ActsAsParanoidAliases @@ -16,7 +17,6 @@ class Comment < ActiveRecord::Base belongs_to :commentable, -> { with_hidden }, polymorphic: true, counter_cache: true belongs_to :user, -> { with_hidden } - belongs_to :public_author, -> { with_public_activity }, class_name: 'User', foreign_key: 'user_id' before_save :calculate_confidence_score diff --git a/app/models/concerns/has_public_author.rb b/app/models/concerns/has_public_author.rb new file mode 100644 index 000000000..a1af255c5 --- /dev/null +++ b/app/models/concerns/has_public_author.rb @@ -0,0 +1,5 @@ +module HasPublicAuthor + def public_author + self.author.public_activity? ? self.author : nil + end +end diff --git a/app/models/debate.rb b/app/models/debate.rb index 13cff5b3f..5e7491b83 100644 --- a/app/models/debate.rb +++ b/app/models/debate.rb @@ -7,13 +7,13 @@ class Debate < ActiveRecord::Base include Sanitizable include Searchable include Filterable + include HasPublicAuthor acts_as_votable acts_as_paranoid column: :hidden_at include ActsAsParanoidAliases belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' - belongs_to :public_author, -> { with_public_activity }, class_name: 'User', foreign_key: 'author_id' belongs_to :geozone has_many :comments, as: :commentable diff --git a/app/models/proposal.rb b/app/models/proposal.rb index 64cadbb24..ad51436b0 100644 --- a/app/models/proposal.rb +++ b/app/models/proposal.rb @@ -6,6 +6,7 @@ class Proposal < ActiveRecord::Base include Sanitizable include Searchable include Filterable + include HasPublicAuthor acts_as_votable acts_as_paranoid column: :hidden_at @@ -14,7 +15,6 @@ class Proposal < ActiveRecord::Base RETIRE_OPTIONS = %w(duplicated started unfeasible done other) belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' - belongs_to :public_author, -> { with_public_activity }, class_name: 'User', foreign_key: 'author_id' belongs_to :geozone has_many :comments, as: :commentable has_many :proposal_notifications From 4783f14eb409573119a961464901ea20c9a630f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 2 Jan 2017 00:13:16 +0100 Subject: [PATCH 083/155] Show voter info in api --- app/models/user.rb | 2 -- app/models/vote.rb | 5 ++++- app/models/voter.rb | 1 + config/initializers/graphql.rb | 21 ++++++++++++--------- lib/graph_ql/type_creator.rb | 11 ++++++++++- 5 files changed, 27 insertions(+), 13 deletions(-) create mode 100644 app/models/voter.rb diff --git a/app/models/user.rb b/app/models/user.rb index f1e48c572..893ddd56d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -55,8 +55,6 @@ class User < ActiveRecord::Base scope :email_digest, -> { where(email_digest: true) } scope :active, -> { where(erased_at: nil) } scope :with_public_activity, -> { where(public_activity: true) } - scope :public_for_api, -> { select("id, username") } - scope :for_votes, -> { select("users.gender, users.geozone_id") } before_validation :clean_document_number diff --git a/app/models/vote.rb b/app/models/vote.rb index a6b0691c6..f51e67ef2 100644 --- a/app/models/vote.rb +++ b/app/models/vote.rb @@ -1,5 +1,4 @@ class Vote < ActsAsVotable::Vote - belongs_to :public_voter, -> { for_votes }, class_name: 'User', foreign_key: :voter_id scope :public_for_api, -> do joins("FULL OUTER JOIN debates ON votable_type = 'Debate' AND votable_id = debates.id"). @@ -7,4 +6,8 @@ class Vote < ActsAsVotable::Vote 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") end + + def public_voter + User.select("gender, geozone_id, date_of_birth").where(id: self.voter_id).first + end end diff --git a/app/models/voter.rb b/app/models/voter.rb new file mode 100644 index 000000000..021d1c768 --- /dev/null +++ b/app/models/voter.rb @@ -0,0 +1 @@ +Voter = User diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index e9e64bac4..e6e8be2f4 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -1,12 +1,15 @@ API_TYPE_DEFINITIONS = { - User => { + User => { id: :integer, - username: :string, - gender: :string, - geozone_id: :integer, - geozone: Geozone + username: :string }, - Debate => { + Voter => { + gender: :string, + date_of_birth: :string, + geozone_id: :integer, + geozone: Geozone + }, + Debate => { id: :integer, title: :string, description: :string, @@ -43,7 +46,7 @@ API_TYPE_DEFINITIONS = { proposal_notifications: [ProposalNotification], public_author: User }, - Comment => { + Comment => { id: :integer, commentable_id: :integer, commentable_type: :string, @@ -56,7 +59,7 @@ API_TYPE_DEFINITIONS = { confidence_score: :integer, public_author: User }, - Geozone => { + Geozone => { id: :integer, name: :string }, @@ -77,7 +80,7 @@ API_TYPE_DEFINITIONS = { votable_id: :integer, votable_type: :string, created_at: :string, - public_voter: User + public_voter: Voter } } diff --git a/lib/graph_ql/type_creator.rb b/lib/graph_ql/type_creator.rb index 468577cff..aa303ec58 100644 --- a/lib/graph_ql/type_creator.rb +++ b/lib/graph_ql/type_creator.rb @@ -33,7 +33,11 @@ module GraphQL resolve -> (object, arguments, context) do target_public_elements = type.respond_to?(:public_for_api) ? type.public_for_api : type.all wanted_element = object.send(name) - target_public_elements.include?(wanted_element) ? wanted_element : nil + if target_public_elements.include?(wanted_element) || GraphQL::TypeCreator.matching_exceptions.include?(name) + wanted_element + else + nil + end end end when :paginated_association @@ -62,5 +66,10 @@ module GraphQL end end + # TODO: esto es una ñapa, hay que buscar una solución mejor + def self.matching_exceptions + [:public_voter] + end + end end From 8fe38f889bd9c639011e718f62582231ceeac1dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 2 Jan 2017 10:52:27 +0100 Subject: [PATCH 084/155] Fix typo --- config/initializers/graphql.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index e6e8be2f4..cc242e722 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -28,7 +28,7 @@ API_TYPE_DEFINITIONS = { Proposal => { id: :integer, title: :string, - description: :sting, + description: :string, external_url: :string, cached_votes_up: :integer, comments_count: :integer, From ac6802572fe76b4fac8ed367a0ec7086ef6977e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 2 Jan 2017 11:06:24 +0100 Subject: [PATCH 085/155] Fix collision problems related to User and Voter classes --- app/models/voter.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/voter.rb b/app/models/voter.rb index 021d1c768..7d4f52676 100644 --- a/app/models/voter.rb +++ b/app/models/voter.rb @@ -1 +1,2 @@ -Voter = User +class Voter < User +end From eadceb106c98fc7caf341b4c0c910fa954a10756 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 2 Jan 2017 11:27:56 +0100 Subject: [PATCH 086/155] Only mount graphiql in development environment --- Gemfile | 2 +- config/routes.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile b/Gemfile index 16da51749..b9799f3d6 100644 --- a/Gemfile +++ b/Gemfile @@ -66,7 +66,6 @@ gem 'turnout', '~> 2.4.0' gem 'redcarpet' gem 'graphql' -gem 'graphiql-rails' group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console @@ -104,6 +103,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' end eval_gemfile './Gemfile_custom' diff --git a/config/routes.rb b/config/routes.rb index d8f0d4476..5a8d8c2b2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -265,12 +265,12 @@ Rails.application.routes.draw do end # GraphQL - mount GraphiQL::Rails::Engine, at: '/graphiql', graphql_path: '/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' From a2befe6bc1ceb3cb71c7a310e96a129e989f9f4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 2 Jan 2017 11:28:05 +0100 Subject: [PATCH 087/155] Remove unused gem --- Gemfile | 1 - Gemfile.lock | 15 --------------- 2 files changed, 16 deletions(-) diff --git a/Gemfile b/Gemfile index b9799f3d6..94d66a281 100644 --- a/Gemfile +++ b/Gemfile @@ -97,7 +97,6 @@ group :test do gem 'poltergeist' gem 'coveralls', require: false gem 'email_spec' - gem 'http' end group :development do diff --git a/Gemfile.lock b/Gemfile.lock index 1832910e5..ed7d74082 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -140,8 +140,6 @@ GEM railties (>= 3.2.6, < 5.0) diff-lcs (1.2.5) docile (1.1.5) - domain_name (0.5.20161021) - unf (>= 0.0.5, < 1.0.0) easy_translate (0.5.0) json thread @@ -188,15 +186,6 @@ GEM hashie (3.4.6) highline (1.7.8) htmlentities (4.3.4) - http (2.1.0) - addressable (~> 2.3) - http-cookie (~> 1.0) - http-form_data (~> 1.0.1) - http_parser.rb (~> 0.6.0) - http-cookie (1.0.3) - domain_name (~> 0.5) - http-form_data (1.0.1) - http_parser.rb (0.6.0) httpi (2.4.1) rack i18n (0.7.0) @@ -437,9 +426,6 @@ GEM thread_safe (~> 0.1) uglifier (3.0.4) execjs (>= 0.3.0, < 3) - unf (0.1.4) - unf_ext - unf_ext (0.0.7.2) unicode-display_width (1.1.1) unicorn (5.1.0) kgio (~> 2.6) @@ -500,7 +486,6 @@ DEPENDENCIES graphiql-rails graphql groupdate (~> 3.1.0) - http i18n-tasks initialjs-rails (= 0.2.0.4) invisible_captcha (~> 0.9.1) From 508360cfa5f3e6644b25ccda229b4abbf83c9ce9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 2 Jan 2017 11:35:19 +0100 Subject: [PATCH 088/155] Remove unused User scope --- app/models/user.rb | 1 - spec/models/user_spec.rb | 12 ------------ 2 files changed, 13 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 893ddd56d..03f2db27b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -54,7 +54,6 @@ class User < ActiveRecord::Base scope :by_document, -> (document_type, document_number) { where(document_type: document_type, document_number: document_number) } scope :email_digest, -> { where(email_digest: true) } scope :active, -> { where(erased_at: nil) } - scope :with_public_activity, -> { where(public_activity: true) } before_validation :clean_document_number diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 239815c10..ed0bac164 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -351,18 +351,6 @@ describe User do end end - - describe "public activity" do - it "returns users whose activity feed is public" do - user1 = create(:user) - user2 = create(:user, public_activity: false) - user3 = create(:user) - - expect(User.with_public_activity).to include(user1) - expect(User.with_public_activity).not_to include(user2) - expect(User.with_public_activity).to include(user3) - end - end end describe "self.search" do From f4e0ef7eea81577487bc864b3ee57de61dee44ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 2 Jan 2017 12:00:58 +0100 Subject: [PATCH 089/155] Truncate votes api timestamp to hour --- app/models/vote.rb | 4 ++++ config/initializers/graphql.rb | 8 ++++---- spec/models/vote_spec.rb | 7 +++++++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/app/models/vote.rb b/app/models/vote.rb index f51e67ef2..3f60688d7 100644 --- a/app/models/vote.rb +++ b/app/models/vote.rb @@ -10,4 +10,8 @@ class Vote < ActsAsVotable::Vote def public_voter User.select("gender, geozone_id, date_of_birth").where(id: self.voter_id).first end + + def public_timestamp + self.created_at.change(min: 0) + end end diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index cc242e722..fdb5975e3 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -77,10 +77,10 @@ API_TYPE_DEFINITIONS = { kind: :string }, Vote => { - votable_id: :integer, - votable_type: :string, - created_at: :string, - public_voter: Voter + votable_id: :integer, + votable_type: :string, + public_timestamp: :string, + public_voter: Voter } } diff --git a/spec/models/vote_spec.rb b/spec/models/vote_spec.rb index 14929ef2c..8e5db0ce7 100644 --- a/spec/models/vote_spec.rb +++ b/spec/models/vote_spec.rb @@ -91,4 +91,11 @@ describe 'Vote' do expect(Vote.public_for_api).not_to include(vote) end end + + describe '#public_timestamp' do + it "truncates created_at timestamp up to minutes" do + 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')) + end + end end From 0b4004042f95e28e8695286abd5e5aaca346db91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 2 Jan 2017 14:12:37 +0100 Subject: [PATCH 090/155] Expose age_range instead of date_of_birth for Voter/User class --- app/models/user.rb | 4 ++++ config/initializers/graphql.rb | 8 ++++---- lib/age_range_calculator.rb | 15 +++++++++++++++ spec/lib/age_range_calculator_spec.rb | 14 ++++++++++++++ spec/models/user_spec.rb | 6 ++++++ 5 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 lib/age_range_calculator.rb create mode 100644 spec/lib/age_range_calculator_spec.rb diff --git a/app/models/user.rb b/app/models/user.rb index 03f2db27b..04ca708a9 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -244,6 +244,10 @@ class User < ActiveRecord::Base end delegate :can?, :cannot?, to: :ability + def age_range + AgeRangeCalculator::range_from_birthday(self.date_of_birth).to_s + end + private def clean_document_number diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index fdb5975e3..6fb30ca45 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -4,10 +4,10 @@ API_TYPE_DEFINITIONS = { username: :string }, Voter => { - gender: :string, - date_of_birth: :string, - geozone_id: :integer, - geozone: Geozone + gender: :string, + age_range: :string, + geozone_id: :integer, + geozone: Geozone }, Debate => { id: :integer, diff --git a/lib/age_range_calculator.rb b/lib/age_range_calculator.rb new file mode 100644 index 000000000..acb12e19e --- /dev/null +++ b/lib/age_range_calculator.rb @@ -0,0 +1,15 @@ +class AgeRangeCalculator + + MIN_AGE = 16 + MAX_AGE = 1.0/0.0 # Infinity + RANGES = [ (MIN_AGE..25), (26..40), (41..60), (61..MAX_AGE) ] + + def self.range_from_birthday(dob) + # Inspired by: http://stackoverflow.com/questions/819263/get-persons-age-in-ruby/2357790#2357790 + now = Time.current.to_date + age = now.year - dob.year - ((now.month > dob.month || (now.month == dob.month && now.day >= dob.day)) ? 0 : 1) + + index = RANGES.find_index { |range| range.include?(age) } + index ? RANGES[index] : nil + end +end diff --git a/spec/lib/age_range_calculator_spec.rb b/spec/lib/age_range_calculator_spec.rb new file mode 100644 index 000000000..a6fd183ed --- /dev/null +++ b/spec/lib/age_range_calculator_spec.rb @@ -0,0 +1,14 @@ +require 'rails_helper' + +describe AgeRangeCalculator do + subject { AgeRangeCalculator } + + describe '::range_from_birthday' do + it 'returns the age range' do + expect(subject::range_from_birthday(Time.current - 1.year)).to eq(nil) + expect(subject::range_from_birthday(Time.current - 26.year)).to eq(26..40) + expect(subject::range_from_birthday(Time.current - 60.year)).to eq(41..60) + expect(subject::range_from_birthday(Time.current - 200.year)).to eq(61..subject::MAX_AGE) + end + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index ed0bac164..ca59cb288 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -469,4 +469,10 @@ describe User do end + describe "#age_range" do + it 'returns string representation of age range' do + user = create(:user, date_of_birth: Time.current - 41.years) + expect(user.age_range).to eq('41..60') + end + end end From 2c5925f9474a7e515d1d97a332dfd0b105c042a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Tue, 3 Jan 2017 13:24:00 +0100 Subject: [PATCH 091/155] Refactor GraphqlController --- app/controllers/graphql_controller.rb | 52 +++++++++++++++++---------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index be4f51f03..de7ae7de7 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -1,28 +1,44 @@ class GraphqlController < ApplicationController + attr_accessor :query_variables, :query_string skip_before_action :verify_authenticity_token skip_authorization_check + class QueryStringError < StandardError; end + def query - - if request.headers["CONTENT_TYPE"] == 'application/graphql' - query_string = request.body.string # request.body.class => StringIO - else - query_string = params[:query] - end - - if query_string.nil? + begin + set_query_environment + response = ConsulSchema.execute query_string, variables: query_variables + render json: response, status: :ok + rescue GraphqlController::QueryStringError render json: { message: 'Query string not present' }, status: :bad_request - else - begin - response = ConsulSchema.execute( - query_string, - variables: params[:variables] || {} - ) - render json: response, status: :ok - rescue GraphQL::ParseError - render json: { message: 'Query string is not valid JSON' }, status: :bad_request - end + rescue GraphQL::ParseError + render json: { message: 'Query string is not valid JSON' }, status: :bad_request end end + + private + + def set_query_environment + set_query_string + set_query_variables + end + + def set_query_string + if request.headers["CONTENT_TYPE"] == 'application/graphql' + @query_string = request.body.string # request.body.class => StringIO + else + @query_string = params[:query] + end + if query_string.nil? then raise GraphqlController::QueryStringError end + end + + def set_query_variables + if params[:variables].nil? + @query_variables = {} + else + @query_variables = params[:variables] + end + end end From 4e12e9055cf42574a577248542866caaf2a399d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Tue, 3 Jan 2017 16:27:11 +0100 Subject: [PATCH 092/155] Refactor GraphQL::TypeCreator --- lib/graph_ql/type_creator.rb | 49 ++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/lib/graph_ql/type_creator.rb b/lib/graph_ql/type_creator.rb index aa303ec58..2a4a4ad45 100644 --- a/lib/graph_ql/type_creator.rb +++ b/lib/graph_ql/type_creator.rb @@ -24,29 +24,18 @@ module GraphQL description("#{model.model_name.human}") # Make a field for each column, association or method - fields.each do |name, type| - case GraphQL::TypeCreator.type_kind(type) + fields.each do |field_name, field_type| + case TypeCreator.type_kind(field_type) when :scalar - field name, SCALAR_TYPES[type] + field(field_name, SCALAR_TYPES[field_type]) when :simple_association - field(name, -> { type_creator.created_types[type] }) do - resolve -> (object, arguments, context) do - target_public_elements = type.respond_to?(:public_for_api) ? type.public_for_api : type.all - wanted_element = object.send(name) - if target_public_elements.include?(wanted_element) || GraphQL::TypeCreator.matching_exceptions.include?(name) - wanted_element - else - nil - end - end + field(field_name, -> { type_creator.created_types[field_type] }) do + resolve TypeCreator.create_association_resolver(field_name, field_type) end when :paginated_association - type = type.first - connection(name, -> { type_creator.created_types[type].connection_type }) do - resolve -> (object, arguments, context) do - target_public_elements = type.respond_to?(:public_for_api) ? type.public_for_api : type.all - object.send(name).all & target_public_elements.all - end + field_type = field_type.first + connection(field_name, -> { type_creator.created_types[field_type].connection_type }) do + resolve TypeCreator.create_association_resolver(field_name, field_type) end end end @@ -56,6 +45,28 @@ module GraphQL return created_type # GraphQL::ObjectType end + def self.create_association_resolver(field_name, field_type) + -> (object, arguments, context) do + allowed_elements = target_public_elements(field_type) + requested_elements = object.send(field_name) + filter_forbidden_elements(requested_elements, allowed_elements, field_name) + end + end + + def self.target_public_elements(field_type) + field_type.respond_to?(:public_for_api) ? field_type.public_for_api : field_type.all + end + + def self.filter_forbidden_elements(requested, allowed_elements, field_name=nil) + if field_name && matching_exceptions.include?(field_name) + requested + elsif requested.respond_to?(:each) + requested.all & allowed_elements.all + else + allowed_elements.include?(requested) ? requested : nil + end + end + def self.type_kind(type) if SCALAR_TYPES[type] :scalar From cdc3b4637036dc1b7502147521a3b1719affa4ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Tue, 3 Jan 2017 16:52:13 +0100 Subject: [PATCH 093/155] Extract TypeCreator methods into AssociationResolver class --- lib/graph_ql/association_resolver.rb | 35 ++++++++++++++++++++++++++++ lib/graph_ql/type_creator.rb | 32 ++----------------------- 2 files changed, 37 insertions(+), 30 deletions(-) create mode 100644 lib/graph_ql/association_resolver.rb diff --git a/lib/graph_ql/association_resolver.rb b/lib/graph_ql/association_resolver.rb new file mode 100644 index 000000000..a896a0644 --- /dev/null +++ b/lib/graph_ql/association_resolver.rb @@ -0,0 +1,35 @@ +module GraphQL + class AssociationResolver + attr_reader :field_name, :target_model, :allowed_elements + + def initialize(field_name, target_model) + @field_name = field_name + @target_model = target_model + @allowed_elements = target_public_elements + end + + def call(object, arguments, context) + requested_elements = object.send(field_name) + filter_forbidden_elements(requested_elements) + end + + def target_public_elements + target_model.respond_to?(:public_for_api) ? target_model.public_for_api : target_model.all + end + + def filter_forbidden_elements(requested_elements) + if AssociationResolver.matching_exceptions.include?(field_name) + requested_elements + elsif requested_elements.respond_to?(:each) + requested_elements.all & allowed_elements.all + else + allowed_elements.include?(requested_elements) ? requested_elements : nil + end + end + + def self.matching_exceptions + [:public_voter] + end + + end +end diff --git a/lib/graph_ql/type_creator.rb b/lib/graph_ql/type_creator.rb index 2a4a4ad45..b77360c72 100644 --- a/lib/graph_ql/type_creator.rb +++ b/lib/graph_ql/type_creator.rb @@ -1,6 +1,5 @@ module GraphQL class TypeCreator - # Return a GraphQL type for a 'database_type' SCALAR_TYPES = { integer: GraphQL::INT_TYPE, boolean: GraphQL::BOOLEAN_TYPE, @@ -30,12 +29,12 @@ module GraphQL field(field_name, SCALAR_TYPES[field_type]) when :simple_association field(field_name, -> { type_creator.created_types[field_type] }) do - resolve TypeCreator.create_association_resolver(field_name, field_type) + resolve GraphQL::AssociationResolver.new(field_name, field_type) end when :paginated_association field_type = field_type.first connection(field_name, -> { type_creator.created_types[field_type].connection_type }) do - resolve TypeCreator.create_association_resolver(field_name, field_type) + resolve GraphQL::AssociationResolver.new(field_name, field_type) end end end @@ -45,28 +44,6 @@ module GraphQL return created_type # GraphQL::ObjectType end - def self.create_association_resolver(field_name, field_type) - -> (object, arguments, context) do - allowed_elements = target_public_elements(field_type) - requested_elements = object.send(field_name) - filter_forbidden_elements(requested_elements, allowed_elements, field_name) - end - end - - def self.target_public_elements(field_type) - field_type.respond_to?(:public_for_api) ? field_type.public_for_api : field_type.all - end - - def self.filter_forbidden_elements(requested, allowed_elements, field_name=nil) - if field_name && matching_exceptions.include?(field_name) - requested - elsif requested.respond_to?(:each) - requested.all & allowed_elements.all - else - allowed_elements.include?(requested) ? requested : nil - end - end - def self.type_kind(type) if SCALAR_TYPES[type] :scalar @@ -77,10 +54,5 @@ module GraphQL end end - # TODO: esto es una ñapa, hay que buscar una solución mejor - def self.matching_exceptions - [:public_voter] - end - end end From 9cfb3eb52e246f0b62cb147ab89d2d78cea400b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Tue, 3 Jan 2017 18:30:53 +0100 Subject: [PATCH 094/155] Write specs for GraphQL::AssociationResolver --- lib/graph_ql/association_resolver.rb | 29 +++++++------ spec/lib/association_resolver_spec.rb | 62 +++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 14 deletions(-) create mode 100644 spec/lib/association_resolver_spec.rb diff --git a/lib/graph_ql/association_resolver.rb b/lib/graph_ql/association_resolver.rb index a896a0644..142b24313 100644 --- a/lib/graph_ql/association_resolver.rb +++ b/lib/graph_ql/association_resolver.rb @@ -13,23 +13,24 @@ module GraphQL filter_forbidden_elements(requested_elements) end - def target_public_elements - target_model.respond_to?(:public_for_api) ? target_model.public_for_api : target_model.all - end - - def filter_forbidden_elements(requested_elements) - if AssociationResolver.matching_exceptions.include?(field_name) - requested_elements - elsif requested_elements.respond_to?(:each) - requested_elements.all & allowed_elements.all - else - allowed_elements.include?(requested_elements) ? requested_elements : nil - end - end - def self.matching_exceptions [:public_voter] end + private + + def target_public_elements + target_model.respond_to?(:public_for_api) ? target_model.public_for_api : target_model.all + end + + def filter_forbidden_elements(requested_elements) + if AssociationResolver.matching_exceptions.include?(field_name) + requested_elements + elsif requested_elements.respond_to?(:each) + requested_elements.all & allowed_elements.all + else + allowed_elements.include?(requested_elements) ? requested_elements : nil + end + end end end diff --git a/spec/lib/association_resolver_spec.rb b/spec/lib/association_resolver_spec.rb new file mode 100644 index 000000000..adb8a8b34 --- /dev/null +++ b/spec/lib/association_resolver_spec.rb @@ -0,0 +1,62 @@ +require 'rails_helper' + +describe GraphQL::AssociationResolver do + let(:comments_resolver) { GraphQL::AssociationResolver.new(:comments, Comment) } + let(:geozone_resolver) { GraphQL::AssociationResolver.new(:geozone, Geozone) } + let(:geozones_resolver) { GraphQL::AssociationResolver.new(:geozones, Geozone) } + + describe '#initialize' do + it 'sets allowed elements for unscoped models' do + geozone_1 = create(:geozone) + geozone_2 = create(:geozone) + + expect(geozones_resolver.allowed_elements).to match_array([geozone_1, geozone_2]) + end + + it 'sets allowed elements for scoped models' do + public_comment = create(:comment, commentable: create(:proposal)) + restricted_comment = create(:comment, commentable: create(:proposal, :hidden)) + + expect(comments_resolver.allowed_elements).to match_array([public_comment]) + end + end + + describe '#call' do + it 'resolves simple associations' do + geozone = create(:geozone) + proposal = create(:proposal, geozone: geozone) + + result = geozone_resolver.call(proposal, nil, nil) + + expect(result).to eq(geozone) + end + + it 'blocks forbidden elements when resolving simple associations' do + skip 'None of the current models allows this spec to be executed' + end + + it 'resolves paginated associations' do + proposal = create(:proposal) + comment_1 = create(:comment, commentable: proposal) + comment_2 = create(:comment, commentable: proposal) + comment_3 = create(:comment, commentable: create(:proposal)) + + result = comments_resolver.call(proposal, nil, nil) + + expect(result).to match_array([comment_1, comment_2]) + end + + it 'blocks forbidden elements when resolving paginated associations' do + proposal = create(:proposal, :hidden) + comment = create(:comment, commentable: proposal) + + result = comments_resolver.call(proposal, nil, nil) + + expect(result).to be_empty + end + + it 'permits all elements for exceptions' do + skip 'Current implementation is temporary' + end + end +end From b36deb03829d8e42932c821a05c8e41a3daf6311 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Wed, 4 Jan 2017 14:26:30 +0100 Subject: [PATCH 095/155] Read API types and fields from config/api.yml --- config/api.yml | 84 ++++++++++++++++++++++++ config/initializers/graphql.rb | 116 ++++++++------------------------- 2 files changed, 112 insertions(+), 88 deletions(-) create mode 100644 config/api.yml diff --git a/config/api.yml b/config/api.yml new file mode 100644 index 000000000..bdf1b8175 --- /dev/null +++ b/config/api.yml @@ -0,0 +1,84 @@ +User: + fields: + id: integer + username: string +Voter: + options: [disable_filtering] + fields: + gender: string + age_range: string + geozone_id: integer + geozone: Geozone +Debate: + fields: + id: integer + title: string + description: string + created_at: string + cached_votes_total: integer + cached_votes_up: integer + cached_votes_down: integer + comments_count: integer + hot_score: integer + confidence_score: integer + geozone_id: integer + geozone: Geozone + comments: [Comment] + public_author: User +Proposal: + fields: + id: integer + title: string + description: string + external_url: string + cached_votes_up: integer + comments_count: integer + hot_score: integer + confidence_score: integer + 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 +Comment: + fields: + id: integer + commentable_id: integer + commentable_type: string + body: string + created_at: string + cached_votes_total: integer + cached_votes_up: integer + cached_votes_down: integer + ancestry: string + confidence_score: integer + public_author: User +Geozone: + fields: + id: integer + name: string +ProposalNotification: + fields: + title: string + body: string + proposal_id: integer + created_at: string + proposal: Proposal +Tag: + fields: + id: integer + name: string + taggings_count: integer + kind: string +Vote: + fields: + votable_id: integer + votable_type: string + public_timestamp: string + public_voter: Voter diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index 6fb30ca45..2d257285d 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -1,93 +1,33 @@ -API_TYPE_DEFINITIONS = { - User => { - id: :integer, - username: :string - }, - Voter => { - gender: :string, - age_range: :string, - geozone_id: :integer, - geozone: Geozone - }, - Debate => { - id: :integer, - title: :string, - description: :string, - created_at: :string, - cached_votes_total: :integer, - cached_votes_up: :integer, - cached_votes_down: :integer, - comments_count: :integer, - hot_score: :integer, - confidence_score: :integer, - geozone_id: :integer, - geozone: Geozone, - comments: [Comment], - public_author: User - }, - Proposal => { - id: :integer, - title: :string, - description: :string, - external_url: :string, - cached_votes_up: :integer, - comments_count: :integer, - hot_score: :integer, - confidence_score: :integer, - 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 - }, - Comment => { - id: :integer, - commentable_id: :integer, - commentable_type: :string, - body: :string, - created_at: :string, - cached_votes_total: :integer, - cached_votes_up: :integer, - cached_votes_down: :integer, - ancestry: :string, - confidence_score: :integer, - public_author: User - }, - Geozone => { - id: :integer, - name: :string - }, - ProposalNotification => { - title: :string, - body: :string, - proposal_id: :integer, - created_at: :string, - proposal: Proposal - }, - Tag => { - id: :integer, - name: :string, - taggings_count: :integer, - kind: :string - }, - Vote => { - votable_id: :integer, - votable_type: :string, - public_timestamp: :string, - public_voter: Voter - } -} +api_config = YAML.load_file('./config/api.yml') + +API_TYPE_DEFINITIONS = {} + +# Parse API configuration file + +api_config.each do |api_type_model, api_type_info| + model = api_type_model.constantize + options = api_type_info['options'] + 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 GraphQL::TypeCreator::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] = { options: options, fields: fields } +end + +# Create all GraphQL types type_creator = GraphQL::TypeCreator.new -API_TYPE_DEFINITIONS.each do |model, fields| - type_creator.create(model, fields) +API_TYPE_DEFINITIONS.each do |model, info| + type_creator.create(model, info[:fields]) end ConsulSchema = GraphQL::Schema.define do @@ -110,7 +50,7 @@ QueryRoot = GraphQL::ObjectType.define do type_creator.created_types.each do |model, created_type| # create an entry field to retrive a single object - if API_TYPE_DEFINITIONS[model][:id] + if API_TYPE_DEFINITIONS[model][:fields][:id] field model.name.underscore.to_sym do type created_type description "Find one #{model.model_name.human} by ID" From 861a723724ca037fe23c946388a04d0550e6c89e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Sat, 7 Jan 2017 17:22:18 +0100 Subject: [PATCH 096/155] Only show public_voter if votable.total_votes are above threshold --- app/models/comment.rb | 1 + app/models/concerns/public_voters_stats.rb | 12 ++++++ app/models/debate.rb | 1 + app/models/proposal.rb | 1 + app/models/vote.rb | 2 +- db/dev_seeds.rb | 3 ++ spec/models/comment_spec.rb | 2 + .../concerns/public_voters_stats_spec.rb | 39 +++++++++++++++++++ spec/models/debate_spec.rb | 2 + spec/models/proposal_spec.rb | 2 + spec/models/vote_spec.rb | 19 +++++++++ spec/spec_helper.rb | 3 +- 12 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 app/models/concerns/public_voters_stats.rb create mode 100644 spec/models/concerns/public_voters_stats_spec.rb diff --git a/app/models/comment.rb b/app/models/comment.rb index 1d5c9aa30..c1e99fcef 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -1,6 +1,7 @@ class Comment < ActiveRecord::Base include Flaggable include HasPublicAuthor + include PublicVotersStats acts_as_paranoid column: :hidden_at include ActsAsParanoidAliases diff --git a/app/models/concerns/public_voters_stats.rb b/app/models/concerns/public_voters_stats.rb new file mode 100644 index 000000000..88d695f20 --- /dev/null +++ b/app/models/concerns/public_voters_stats.rb @@ -0,0 +1,12 @@ +module PublicVotersStats + + def votes_above_threshold? + threshold = Setting["#{self.class.name.downcase}_api_votes_threshold"] + threshold = (threshold ? threshold.to_i : default_threshold) + (total_votes >= threshold) + end + + def default_threshold + 200 + end +end diff --git a/app/models/debate.rb b/app/models/debate.rb index 5e7491b83..c2a4eb7f6 100644 --- a/app/models/debate.rb +++ b/app/models/debate.rb @@ -8,6 +8,7 @@ class Debate < ActiveRecord::Base include Searchable include Filterable include HasPublicAuthor + include PublicVotersStats acts_as_votable acts_as_paranoid column: :hidden_at diff --git a/app/models/proposal.rb b/app/models/proposal.rb index 6ddcf7085..f915f173a 100644 --- a/app/models/proposal.rb +++ b/app/models/proposal.rb @@ -7,6 +7,7 @@ class Proposal < ActiveRecord::Base include Searchable include Filterable include HasPublicAuthor + include PublicVotersStats acts_as_votable acts_as_paranoid column: :hidden_at diff --git a/app/models/vote.rb b/app/models/vote.rb index 3f60688d7..af607ffa8 100644 --- a/app/models/vote.rb +++ b/app/models/vote.rb @@ -8,7 +8,7 @@ class Vote < ActsAsVotable::Vote end def public_voter - User.select("gender, geozone_id, date_of_birth").where(id: self.voter_id).first + votable.votes_above_threshold? ? voter : nil end def public_timestamp diff --git a/db/dev_seeds.rb b/db/dev_seeds.rb index 0af5564e1..c73552d18 100644 --- a/db/dev_seeds.rb +++ b/db/dev_seeds.rb @@ -15,6 +15,9 @@ Setting.create(key: 'proposal_code_prefix', value: 'MAD') Setting.create(key: 'votes_for_proposal_success', value: '100') Setting.create(key: 'months_to_archive_proposals', value: '12') Setting.create(key: 'comments_body_max_length', value: '1000') +Settings.create(key: 'debate_api_votes_threshold', value: '150') +Settings.create(key: 'proposal_api_votes_threshold', value: '150') +Settings.create(key: 'comment_api_votes_threshold', value: '30') Setting.create(key: 'twitter_handle', value: '@consul_dev') Setting.create(key: 'twitter_hashtag', value: '#consul_dev') diff --git a/spec/models/comment_spec.rb b/spec/models/comment_spec.rb index ecd167237..e1f39ea0d 100644 --- a/spec/models/comment_spec.rb +++ b/spec/models/comment_spec.rb @@ -4,6 +4,8 @@ describe Comment do let(:comment) { build(:comment) } + it_behaves_like "public_voters_stats" + it "is valid" do expect(comment).to be_valid end diff --git a/spec/models/concerns/public_voters_stats_spec.rb b/spec/models/concerns/public_voters_stats_spec.rb new file mode 100644 index 000000000..eaa923599 --- /dev/null +++ b/spec/models/concerns/public_voters_stats_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +shared_examples_for 'public_voters_stats' do + let(:model) { described_class } # the class that includes the concern + + describe 'votes_above_threshold?' do + let(:votable) { create(model.to_s.underscore.to_sym) } + + context 'with default threshold value' do + it 'is true when votes are above threshold' do + 200.times { create(:vote, votable: votable) } + + expect(votable.votes_above_threshold?).to be_truthy + end + + it 'is false when votes are under threshold' do + 199.times { create(:vote, votable: votable) } + + expect(votable.votes_above_threshold?).to be_falsey + end + end + + context 'with custom threshold value' do + it 'is true when votes are above threshold' do + create(:setting, key: "#{model.to_s.underscore}_api_votes_threshold", value: '2') + 2.times { create(:vote, votable: votable) } + + expect(votable.votes_above_threshold?).to be_truthy + end + + it 'is false when votes are under threshold' do + create(:setting, key: "#{model.to_s.underscore}_api_votes_threshold", value: '2') + create(:vote, votable: votable) + + expect(votable.votes_above_threshold?).to be_falsey + end + end + end +end diff --git a/spec/models/debate_spec.rb b/spec/models/debate_spec.rb index 1b94393d1..7c40e63fe 100644 --- a/spec/models/debate_spec.rb +++ b/spec/models/debate_spec.rb @@ -4,6 +4,8 @@ require 'rails_helper' describe Debate do let(:debate) { build(:debate) } + it_behaves_like "public_voters_stats" + it "should be valid" do expect(debate).to be_valid end diff --git a/spec/models/proposal_spec.rb b/spec/models/proposal_spec.rb index 9a9e9e9bc..77eb3e358 100644 --- a/spec/models/proposal_spec.rb +++ b/spec/models/proposal_spec.rb @@ -4,6 +4,8 @@ require 'rails_helper' describe Proposal do let(:proposal) { build(:proposal) } + it_behaves_like "public_voters_stats" + it "should be valid" do expect(proposal).to be_valid end diff --git a/spec/models/vote_spec.rb b/spec/models/vote_spec.rb index 8e5db0ce7..9dc30d95a 100644 --- a/spec/models/vote_spec.rb +++ b/spec/models/vote_spec.rb @@ -92,6 +92,25 @@ describe 'Vote' do end end + describe 'public_voter' do + it 'only returns voter if votable has enough votes' do + create(:setting, key: 'proposal_api_votes_threshold', value: '2') + + proposal_1 = create(:proposal) + proposal_2 = create(:proposal) + + voter_1 = create(:user) + voter_2 = create(:user) + + vote_1 = create(:vote, votable: proposal_1, voter: voter_1) + vote_2 = create(:vote, votable: proposal_2, voter: voter_1) + vote_3 = create(:vote, votable: proposal_2, voter: voter_2) + + expect(vote_1.public_voter).to be_nil + expect(vote_2.public_voter).to eq(voter_1) + end + end + describe '#public_timestamp' do it "truncates created_at timestamp up to minutes" do vote = create(:vote, created_at: Time.zone.parse('2016-02-10 15:30:45')) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a40934d92..e9e055bca 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -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| @@ -105,4 +106,4 @@ RSpec.configure do |config| end # Parallel build helper configuration for travis -Knapsack::Adapters::RSpecAdapter.bind \ No newline at end of file +Knapsack::Adapters::RSpecAdapter.bind From f467bb64c76bdb3c12925425320b051bd2036011 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Sat, 7 Jan 2017 18:00:14 +0100 Subject: [PATCH 097/155] Wrote specs for HasPublicAuthor module --- spec/models/comment_spec.rb | 3 ++- .../models/concerns/has_public_author_spec.rb | 21 +++++++++++++++++++ spec/models/debate_spec.rb | 3 ++- spec/models/proposal_spec.rb | 1 + 4 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 spec/models/concerns/has_public_author_spec.rb diff --git a/spec/models/comment_spec.rb b/spec/models/comment_spec.rb index e1f39ea0d..09e6a9900 100644 --- a/spec/models/comment_spec.rb +++ b/spec/models/comment_spec.rb @@ -5,7 +5,8 @@ describe Comment do let(:comment) { build(:comment) } it_behaves_like "public_voters_stats" - + it_behaves_like "has_public_author" + it "is valid" do expect(comment).to be_valid end diff --git a/spec/models/concerns/has_public_author_spec.rb b/spec/models/concerns/has_public_author_spec.rb new file mode 100644 index 000000000..a5a9c2811 --- /dev/null +++ b/spec/models/concerns/has_public_author_spec.rb @@ -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 diff --git a/spec/models/debate_spec.rb b/spec/models/debate_spec.rb index 7c40e63fe..4db0e5f0a 100644 --- a/spec/models/debate_spec.rb +++ b/spec/models/debate_spec.rb @@ -5,7 +5,8 @@ describe Debate do let(:debate) { build(:debate) } it_behaves_like "public_voters_stats" - + it_behaves_like "has_public_author" + it "should be valid" do expect(debate).to be_valid end diff --git a/spec/models/proposal_spec.rb b/spec/models/proposal_spec.rb index 77eb3e358..08ed6b384 100644 --- a/spec/models/proposal_spec.rb +++ b/spec/models/proposal_spec.rb @@ -5,6 +5,7 @@ describe Proposal do let(:proposal) { build(:proposal) } it_behaves_like "public_voters_stats" + it_behaves_like "has_public_author" it "should be valid" do expect(proposal).to be_valid From 1e95cdfce37db6f200e0487e1bbfe0696e10abe7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Sat, 7 Jan 2017 21:58:29 +0100 Subject: [PATCH 098/155] Fix dev_seeds --- db/dev_seeds.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/db/dev_seeds.rb b/db/dev_seeds.rb index c73552d18..0a4738987 100644 --- a/db/dev_seeds.rb +++ b/db/dev_seeds.rb @@ -15,9 +15,9 @@ Setting.create(key: 'proposal_code_prefix', value: 'MAD') Setting.create(key: 'votes_for_proposal_success', value: '100') Setting.create(key: 'months_to_archive_proposals', value: '12') Setting.create(key: 'comments_body_max_length', value: '1000') -Settings.create(key: 'debate_api_votes_threshold', value: '150') -Settings.create(key: 'proposal_api_votes_threshold', value: '150') -Settings.create(key: 'comment_api_votes_threshold', value: '30') +Setting.create(key: 'debate_api_votes_threshold', value: '2') +Setting.create(key: 'proposal_api_votes_threshold', value: '2') +Setting.create(key: 'comment_api_votes_threshold', value: '2') Setting.create(key: 'twitter_handle', value: '@consul_dev') Setting.create(key: 'twitter_hashtag', value: '#consul_dev') From a31f6be3bde8d53c25610306288a5b36baa5fda1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Sat, 7 Jan 2017 22:16:17 +0100 Subject: [PATCH 099/155] Add proposal notifications to seeds --- db/dev_seeds.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/db/dev_seeds.rb b/db/dev_seeds.rb index 0a4738987..347994a00 100644 --- a/db/dev_seeds.rb +++ b/db/dev_seeds.rb @@ -376,3 +376,12 @@ Proposal.last(3).each do |proposal| created_at: rand((Time.current - 1.week) .. Time.current)) puts " #{banner.title}" end + +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 From 3c73f354167c92d8030b2c285957569ec3aa9d12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Sat, 7 Jan 2017 22:20:28 +0100 Subject: [PATCH 100/155] Removed useless code --- config/api.yml | 1 - lib/graph_ql/association_resolver.rb | 8 +------- spec/lib/association_resolver_spec.rb | 7 ++----- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/config/api.yml b/config/api.yml index bdf1b8175..452fa22f7 100644 --- a/config/api.yml +++ b/config/api.yml @@ -3,7 +3,6 @@ User: id: integer username: string Voter: - options: [disable_filtering] fields: gender: string age_range: string diff --git a/lib/graph_ql/association_resolver.rb b/lib/graph_ql/association_resolver.rb index 142b24313..22dd210f3 100644 --- a/lib/graph_ql/association_resolver.rb +++ b/lib/graph_ql/association_resolver.rb @@ -13,10 +13,6 @@ module GraphQL filter_forbidden_elements(requested_elements) end - def self.matching_exceptions - [:public_voter] - end - private def target_public_elements @@ -24,9 +20,7 @@ module GraphQL end def filter_forbidden_elements(requested_elements) - if AssociationResolver.matching_exceptions.include?(field_name) - requested_elements - elsif requested_elements.respond_to?(:each) + if requested_elements.respond_to?(:each) requested_elements.all & allowed_elements.all else allowed_elements.include?(requested_elements) ? requested_elements : nil diff --git a/spec/lib/association_resolver_spec.rb b/spec/lib/association_resolver_spec.rb index adb8a8b34..3e8bdf897 100644 --- a/spec/lib/association_resolver_spec.rb +++ b/spec/lib/association_resolver_spec.rb @@ -25,7 +25,7 @@ describe GraphQL::AssociationResolver do it 'resolves simple associations' do geozone = create(:geozone) proposal = create(:proposal, geozone: geozone) - + result = geozone_resolver.call(proposal, nil, nil) expect(result).to eq(geozone) @@ -54,9 +54,6 @@ describe GraphQL::AssociationResolver do expect(result).to be_empty end - - it 'permits all elements for exceptions' do - skip 'Current implementation is temporary' - end end + end From d76e4518e666816eb3264433553493f1513bc68f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Sat, 7 Jan 2017 22:43:42 +0100 Subject: [PATCH 101/155] Extract QueryRoot resolve functions into its own classes --- config/initializers/graphql.rb | 16 ++-------------- lib/graph_ql/root_collection_resolver.rb | 17 +++++++++++++++++ lib/graph_ql/root_element_resolver.rb | 17 +++++++++++++++++ 3 files changed, 36 insertions(+), 14 deletions(-) create mode 100644 lib/graph_ql/root_collection_resolver.rb create mode 100644 lib/graph_ql/root_element_resolver.rb diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index 2d257285d..b385f1483 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -55,26 +55,14 @@ QueryRoot = GraphQL::ObjectType.define do type created_type description "Find one #{model.model_name.human} by ID" argument :id, !types.ID - resolve -> (object, arguments, context) do - if model.respond_to?(:public_for_api) - model.public_for_api.find(arguments["id"]) - else - model.find(arguments["id"]) - end - end + resolve GraphQL::RootElementResolver.new(model) end end # create an entry filed to retrive a paginated collection connection model.name.underscore.pluralize.to_sym, created_type.connection_type do description "Find all #{model.model_name.human.pluralize}" - resolve -> (object, arguments, context) do - if model.respond_to?(:public_for_api) - model.public_for_api - else - model.all - end - end + resolve GraphQL::RootCollectionResolver.new(model) end end diff --git a/lib/graph_ql/root_collection_resolver.rb b/lib/graph_ql/root_collection_resolver.rb new file mode 100644 index 000000000..fa0e3c9ec --- /dev/null +++ b/lib/graph_ql/root_collection_resolver.rb @@ -0,0 +1,17 @@ +module GraphQL + class RootCollectionResolver + attr_reader :target_model + + def initialize(target_model) + @target_model = target_model + end + + def call(object, arguments, context) + if target_model.respond_to?(:public_for_api) + target_model.public_for_api + else + target_model.all + end + end + end +end diff --git a/lib/graph_ql/root_element_resolver.rb b/lib/graph_ql/root_element_resolver.rb new file mode 100644 index 000000000..05d0b9bfb --- /dev/null +++ b/lib/graph_ql/root_element_resolver.rb @@ -0,0 +1,17 @@ +module GraphQL + class RootElementResolver + attr_reader :target_model + + def initialize(target_model) + @target_model = target_model + end + + def call(object, arguments, context) + if target_model.respond_to?(:public_for_api) + target_model.public_for_api.find(arguments["id"]) + else + target_model.find(arguments["id"]) + end + end + end +end From 0b302c2afc06490c46db913ff8c84ad9394cb0e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Sat, 7 Jan 2017 23:04:30 +0100 Subject: [PATCH 102/155] Move code from GraphQL initializer into GraphQL::TypeCreator --- config/initializers/graphql.rb | 38 ++++-------------------------- lib/graph_ql/type_creator.rb | 42 +++++++++++++++++++++++++++++++--- spec/lib/type_creator_spec.rb | 20 ++++++++-------- 3 files changed, 54 insertions(+), 46 deletions(-) diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index b385f1483..478cf10e3 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -1,6 +1,6 @@ api_config = YAML.load_file('./config/api.yml') -API_TYPE_DEFINITIONS = {} +api_type_definitions = {} # Parse API configuration file @@ -19,16 +19,13 @@ api_config.each do |api_type_model, api_type_info| end end - API_TYPE_DEFINITIONS[model] = { options: options, fields: fields } + api_type_definitions[model] = { options: options, fields: fields } end # Create all GraphQL types - -type_creator = GraphQL::TypeCreator.new - -API_TYPE_DEFINITIONS.each do |model, info| - type_creator.create(model, info[:fields]) -end +type_creator = GraphQL::TypeCreator.new(api_type_definitions) +type_creator.create_api_types +QueryRoot = type_creator.create_query_root ConsulSchema = GraphQL::Schema.define do query QueryRoot @@ -42,28 +39,3 @@ ConsulSchema = GraphQL::Schema.define do ConsulSchema.types[type_name] } end - -QueryRoot = GraphQL::ObjectType.define do - name "Query" - description "The query root for this schema" - - type_creator.created_types.each do |model, created_type| - - # create an entry field to retrive a single object - if API_TYPE_DEFINITIONS[model][:fields][:id] - field model.name.underscore.to_sym do - type created_type - description "Find one #{model.model_name.human} by ID" - argument :id, !types.ID - resolve GraphQL::RootElementResolver.new(model) - end - end - - # create an entry filed to retrive a paginated collection - connection model.name.underscore.pluralize.to_sym, created_type.connection_type do - description "Find all #{model.model_name.human.pluralize}" - resolve GraphQL::RootCollectionResolver.new(model) - end - - end -end diff --git a/lib/graph_ql/type_creator.rb b/lib/graph_ql/type_creator.rb index b77360c72..15803285e 100644 --- a/lib/graph_ql/type_creator.rb +++ b/lib/graph_ql/type_creator.rb @@ -8,13 +8,20 @@ module GraphQL string: GraphQL::STRING_TYPE } - attr_accessor :created_types + attr_accessor :created_types, :api_type_definitions - def initialize + def initialize(api_type_definitions) + @api_type_definitions = api_type_definitions @created_types = {} end - def create(model, fields) + def create_api_types + api_type_definitions.each do |model, info| + self.create_type(model, info[:fields]) + end + end + + def create_type(model, fields) type_creator = self created_type = GraphQL::ObjectType.define do @@ -44,6 +51,35 @@ module GraphQL return created_type # GraphQL::ObjectType end + def create_query_root + type_creator = self + + GraphQL::ObjectType.define do + name 'QueryRoot' + description 'The query root for this schema' + + type_creator.created_types.each do |model, created_type| + + # create an entry field to retrive a single object + if type_creator.api_type_definitions[model][:fields][:id] + field model.name.underscore.to_sym do + type created_type + description "Find one #{model.model_name.human} by ID" + argument :id, !types.ID + resolve GraphQL::RootElementResolver.new(model) + end + end + + # create an entry filed to retrive a paginated collection + connection model.name.underscore.pluralize.to_sym, created_type.connection_type do + description "Find all #{model.model_name.human.pluralize}" + resolve GraphQL::RootCollectionResolver.new(model) + end + + end + end + end + def self.type_kind(type) if SCALAR_TYPES[type] :scalar diff --git a/spec/lib/type_creator_spec.rb b/spec/lib/type_creator_spec.rb index 1e25b91fd..9948d77d0 100644 --- a/spec/lib/type_creator_spec.rb +++ b/spec/lib/type_creator_spec.rb @@ -1,11 +1,11 @@ require 'rails_helper' describe GraphQL::TypeCreator do - let(:type_creator) { GraphQL::TypeCreator.new } + let(:type_creator) { GraphQL::TypeCreator.new({}) } - describe "::create" do + describe "::create_type" do it "creates fields for Int attributes" do - debate_type = type_creator.create(Debate, { id: :integer }) + debate_type = type_creator.create_type(Debate, { id: :integer }) created_field = debate_type.fields['id'] expect(created_field).to be_a(GraphQL::Field) @@ -14,7 +14,7 @@ describe GraphQL::TypeCreator do end it "creates fields for String attributes" do - debate_type = type_creator.create(Debate, { title: :string }) + debate_type = type_creator.create_type(Debate, { title: :string }) created_field = debate_type.fields['title'] expect(created_field).to be_a(GraphQL::Field) @@ -23,8 +23,8 @@ describe GraphQL::TypeCreator do end it "creates connections for :belongs_to associations" do - user_type = type_creator.create(User, { id: :integer }) - debate_type = type_creator.create(Debate, { author: User }) + user_type = type_creator.create_type(User, { id: :integer }) + debate_type = type_creator.create_type(Debate, { author: User }) connection = debate_type.fields['author'] @@ -34,8 +34,8 @@ describe GraphQL::TypeCreator do end it "creates connections for :has_one associations" do - user_type = type_creator.create(User, { organization: Organization }) - organization_type = type_creator.create(Organization, { id: :integer }) + user_type = type_creator.create_type(User, { organization: Organization }) + organization_type = type_creator.create_type(Organization, { id: :integer }) connection = user_type.fields['organization'] @@ -45,8 +45,8 @@ describe GraphQL::TypeCreator do end it "creates connections for :has_many associations" do - comment_type = type_creator.create(Comment, { id: :integer }) - debate_type = type_creator.create(Debate, { comments: [Comment] }) + comment_type = type_creator.create_type(Comment, { id: :integer }) + debate_type = type_creator.create_type(Debate, { comments: [Comment] }) connection = debate_type.fields['comments'] From ed6ba384dd2350b0b3dde7ebfaf81195e46bdf21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Sun, 8 Jan 2017 11:37:15 +0100 Subject: [PATCH 103/155] Wrote specs for GraphQL root resolvers --- lib/graph_ql/root_element_resolver.rb | 4 +-- .../graph_ql/root_collection_resolver_spec.rb | 27 ++++++++++++++ .../graph_ql/root_element_resolver_spec.rb | 35 +++++++++++++++++++ 3 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 spec/lib/graph_ql/root_collection_resolver_spec.rb create mode 100644 spec/lib/graph_ql/root_element_resolver_spec.rb diff --git a/lib/graph_ql/root_element_resolver.rb b/lib/graph_ql/root_element_resolver.rb index 05d0b9bfb..4191f8eee 100644 --- a/lib/graph_ql/root_element_resolver.rb +++ b/lib/graph_ql/root_element_resolver.rb @@ -8,9 +8,9 @@ module GraphQL def call(object, arguments, context) if target_model.respond_to?(:public_for_api) - target_model.public_for_api.find(arguments["id"]) + target_model.public_for_api.find_by(id: arguments["id"]) else - target_model.find(arguments["id"]) + target_model.find_by(id: arguments["id"]) end end end diff --git a/spec/lib/graph_ql/root_collection_resolver_spec.rb b/spec/lib/graph_ql/root_collection_resolver_spec.rb new file mode 100644 index 000000000..87f0319ff --- /dev/null +++ b/spec/lib/graph_ql/root_collection_resolver_spec.rb @@ -0,0 +1,27 @@ +require 'rails_helper' + +describe GraphQL::RootCollectionResolver do + let(:comments_resolver) { GraphQL::RootCollectionResolver.new(Comment) } + + describe '#call' do + it 'resolves collections' do + comment_1 = create(:comment) + comment_2 = create(:comment) + + result = comments_resolver.call(nil, nil, nil) + + expect(result).to match_array([comment_1, comment_2]) + end + + it 'blocks collection forbidden elements' do + proposal = create(:proposal, :hidden) + comment_1 = create(:comment) + comment_2 = create(:comment, commentable: proposal) + + result = comments_resolver.call(nil, nil, nil) + + expect(result).to match_array([comment_1]) + end + end + +end diff --git a/spec/lib/graph_ql/root_element_resolver_spec.rb b/spec/lib/graph_ql/root_element_resolver_spec.rb new file mode 100644 index 000000000..7ca64a7e8 --- /dev/null +++ b/spec/lib/graph_ql/root_element_resolver_spec.rb @@ -0,0 +1,35 @@ +require 'rails_helper' + +describe GraphQL::RootElementResolver do + let(:comment_resolver) { GraphQL::RootElementResolver.new(Comment) } + + describe '#call' do + it 'resolves simple elements' do + comment = create(:comment) + arguments = { 'id' => comment.id } + + result = comment_resolver.call(nil, arguments, nil) + + expect(result).to eq(comment) + end + + it 'returns nil when requested element is forbidden' do + proposal = create(:proposal, :hidden) + comment = create(:comment, commentable: proposal) + arguments = { 'id' => comment.id } + + result = comment_resolver.call(nil, arguments, nil) + + expect(result).to be_nil + end + + it 'returns nil when requested element does not exist' do + arguments = { 'id' => 1 } + + result = comment_resolver.call(nil, arguments, nil) + + expect(result).to be_nil + end + end + +end From 7027164867f2f9831c8e0459eee016fe26ef7102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Sun, 8 Jan 2017 11:37:56 +0100 Subject: [PATCH 104/155] Clean code --- config/initializers/graphql.rb | 13 ++++--------- lib/graph_ql/type_creator.rb | 6 +++++- .../lib/{ => graph_ql}/association_resolver_spec.rb | 0 spec/lib/{ => graph_ql}/type_creator_spec.rb | 0 4 files changed, 9 insertions(+), 10 deletions(-) rename spec/lib/{ => graph_ql}/association_resolver_spec.rb (100%) rename spec/lib/{ => graph_ql}/type_creator_spec.rb (100%) diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index 478cf10e3..01d54ab19 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -22,20 +22,15 @@ api_config.each do |api_type_model, api_type_info| api_type_definitions[model] = { options: options, fields: fields } end -# Create all GraphQL types type_creator = GraphQL::TypeCreator.new(api_type_definitions) -type_creator.create_api_types -QueryRoot = type_creator.create_query_root +QueryRoot = type_creator.query_root ConsulSchema = GraphQL::Schema.define do query QueryRoot - - # Reject deeply-nested queries max_depth 10 - resolve_type -> (object, ctx) { - # look up types by class name - type_name = object.class.name + resolve_type -> (object, ctx) do + type_name = object.class.name # look up types by class name ConsulSchema.types[type_name] - } + end end diff --git a/lib/graph_ql/type_creator.rb b/lib/graph_ql/type_creator.rb index 15803285e..9042d7f3f 100644 --- a/lib/graph_ql/type_creator.rb +++ b/lib/graph_ql/type_creator.rb @@ -1,3 +1,5 @@ +require 'graphql' + module GraphQL class TypeCreator SCALAR_TYPES = { @@ -8,11 +10,13 @@ module GraphQL string: GraphQL::STRING_TYPE } - attr_accessor :created_types, :api_type_definitions + attr_accessor :created_types, :api_type_definitions, :query_root def initialize(api_type_definitions) @api_type_definitions = api_type_definitions @created_types = {} + create_api_types + @query_root = create_query_root end def create_api_types diff --git a/spec/lib/association_resolver_spec.rb b/spec/lib/graph_ql/association_resolver_spec.rb similarity index 100% rename from spec/lib/association_resolver_spec.rb rename to spec/lib/graph_ql/association_resolver_spec.rb diff --git a/spec/lib/type_creator_spec.rb b/spec/lib/graph_ql/type_creator_spec.rb similarity index 100% rename from spec/lib/type_creator_spec.rb rename to spec/lib/graph_ql/type_creator_spec.rb From b9d2bc280140bfe79037020a3353c8aa1de2dfc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Sun, 8 Jan 2017 12:45:48 +0100 Subject: [PATCH 105/155] Wrote specs for QueryType creation --- config/initializers/graphql.rb | 4 +- lib/graph_ql/type_creator.rb | 70 +++++++++++++------------- spec/lib/graph_ql/type_creator_spec.rb | 35 ++++++++++++- 3 files changed, 72 insertions(+), 37 deletions(-) diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index 01d54ab19..971a565e4 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -23,10 +23,10 @@ api_config.each do |api_type_model, api_type_info| end type_creator = GraphQL::TypeCreator.new(api_type_definitions) -QueryRoot = type_creator.query_root +QueryType = type_creator.query_type ConsulSchema = GraphQL::Schema.define do - query QueryRoot + query QueryType max_depth 10 resolve_type -> (object, ctx) do diff --git a/lib/graph_ql/type_creator.rb b/lib/graph_ql/type_creator.rb index 9042d7f3f..b705839c3 100644 --- a/lib/graph_ql/type_creator.rb +++ b/lib/graph_ql/type_creator.rb @@ -10,21 +10,26 @@ module GraphQL string: GraphQL::STRING_TYPE } - attr_accessor :created_types, :api_type_definitions, :query_root + attr_accessor :created_types, :api_type_definitions, :query_type def initialize(api_type_definitions) @api_type_definitions = api_type_definitions @created_types = {} create_api_types - @query_root = create_query_root + create_query_type end - def create_api_types - api_type_definitions.each do |model, info| - self.create_type(model, info[:fields]) + def self.type_kind(type) + if SCALAR_TYPES[type] + :scalar + elsif type.class == Class + :simple_association + elsif type.class == Array + :paginated_association end end + # TODO: this method shouldn't be public just for testing purposes, ¿smell? def create_type(model, fields) type_creator = self @@ -55,44 +60,41 @@ module GraphQL return created_type # GraphQL::ObjectType end - def create_query_root - type_creator = self + private - GraphQL::ObjectType.define do - name 'QueryRoot' - description 'The query root for this schema' + def create_api_types + api_type_definitions.each do |model, info| + self.create_type(model, info[:fields]) + end + end - type_creator.created_types.each do |model, created_type| + def create_query_type + type_creator = self - # create an entry field to retrive a single object - if type_creator.api_type_definitions[model][:fields][:id] - field model.name.underscore.to_sym do - type created_type - description "Find one #{model.model_name.human} by ID" - argument :id, !types.ID - resolve GraphQL::RootElementResolver.new(model) + @query_type = GraphQL::ObjectType.define do + name 'QueryType' + description 'The root query for this schema' + + type_creator.created_types.each do |model, created_type| + # create field to retrive a single object + if type_creator.api_type_definitions[model][:fields][:id] + field model.name.underscore.to_sym do + type created_type + description "Find one #{model.model_name.human} by ID" + argument :id, !types.ID + resolve GraphQL::RootElementResolver.new(model) + end end - end - # create an entry filed to retrive a paginated collection - connection model.name.underscore.pluralize.to_sym, created_type.connection_type do - description "Find all #{model.model_name.human.pluralize}" - resolve GraphQL::RootCollectionResolver.new(model) + # create connection to retrive a collection + connection model.name.underscore.pluralize.to_sym, created_type.connection_type do + description "Find all #{model.model_name.human.pluralize}" + resolve GraphQL::RootCollectionResolver.new(model) + end end end end - end - - def self.type_kind(type) - if SCALAR_TYPES[type] - :scalar - elsif type.class == Class - :simple_association - elsif type.class == Array - :paginated_association - end - end end end diff --git a/spec/lib/graph_ql/type_creator_spec.rb b/spec/lib/graph_ql/type_creator_spec.rb index 9948d77d0..d5291c961 100644 --- a/spec/lib/graph_ql/type_creator_spec.rb +++ b/spec/lib/graph_ql/type_creator_spec.rb @@ -1,7 +1,40 @@ require 'rails_helper' describe GraphQL::TypeCreator do - let(:type_creator) { GraphQL::TypeCreator.new({}) } + let(:api_type_definitions) { {} } + let(:type_creator) { GraphQL::TypeCreator.new(api_type_definitions) } + + describe "::query_type" do + let(:api_type_definitions) do + { + ProposalNotification => { fields: { title: 'string' } }, + Proposal => { fields: { id: 'integer', title: 'string' } } + } + end + let(:query_type) { type_creator.query_type } + + it 'has fields to retrieve single objects whose model fields included an ID' do + field = query_type.fields['proposal'] + proposal_type = type_creator.created_types[Proposal] + + expect(field).to be_a(GraphQL::Field) + expect(field.type).to eq(proposal_type) + expect(field.name).to eq('proposal') + end + + it 'does not have 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 "has connections to retrieve collections of objects" do + connection = query_type.fields['proposals'] + proposal_type = type_creator.created_types[Proposal] + + expect(connection).to be_a(GraphQL::Field) + expect(connection.type).to eq(proposal_type.connection_type) + expect(connection.name).to eq('proposals') + end + end describe "::create_type" do it "creates fields for Int attributes" do From 398bc8c211dfee451da74ac985cc5fbab76e7958 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Sun, 8 Jan 2017 13:39:50 +0100 Subject: [PATCH 106/155] Split api types creation and query type creation between two different classes --- config/initializers/graphql.rb | 9 +- lib/graph_ql/api_types_creator.rb | 68 +++++++++++++ lib/graph_ql/query_type_creator.rb | 40 ++++++++ lib/graph_ql/type_creator.rb | 100 ------------------- spec/lib/graph_ql/api_types_creator_spec.rb | 58 +++++++++++ spec/lib/graph_ql/query_type_creator_spec.rb | 39 ++++++++ spec/lib/graph_ql/type_creator_spec.rb | 91 ----------------- 7 files changed, 211 insertions(+), 194 deletions(-) create mode 100644 lib/graph_ql/api_types_creator.rb create mode 100644 lib/graph_ql/query_type_creator.rb delete mode 100644 lib/graph_ql/type_creator.rb create mode 100644 spec/lib/graph_ql/api_types_creator_spec.rb create mode 100644 spec/lib/graph_ql/query_type_creator_spec.rb delete mode 100644 spec/lib/graph_ql/type_creator_spec.rb diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index 971a565e4..55c34ec1f 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -12,7 +12,7 @@ api_config.each do |api_type_model, api_type_info| 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 GraphQL::TypeCreator::SCALAR_TYPES[field_type.to_sym] + elsif GraphQL::ApiTypesCreator::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 @@ -22,8 +22,11 @@ api_config.each do |api_type_model, api_type_info| api_type_definitions[model] = { options: options, fields: fields } end -type_creator = GraphQL::TypeCreator.new(api_type_definitions) -QueryType = type_creator.query_type +api_types_creator = GraphQL::ApiTypesCreator.new(api_type_definitions) +created_api_types = api_types_creator.create + +query_type_creator = GraphQL::QueryTypeCreator.new(created_api_types) +QueryType = query_type_creator.create ConsulSchema = GraphQL::Schema.define do query QueryType diff --git a/lib/graph_ql/api_types_creator.rb b/lib/graph_ql/api_types_creator.rb new file mode 100644 index 000000000..51a7af0e8 --- /dev/null +++ b/lib/graph_ql/api_types_creator.rb @@ -0,0 +1,68 @@ +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 + } + + attr_accessor :created_types + + def initialize(api_type_definitions) + @api_type_definitions = api_type_definitions + @created_types = {} + end + + def create + @api_type_definitions.each do |model, info| + self.create_type(model, info[:fields]) + end + created_types + end + + def self.type_kind(type) + if SCALAR_TYPES[type] + :scalar + elsif type.class == Class + :simple_association + elsif type.class == Array + :paginated_association + end + end + + def create_type(model, fields) + api_types_creator = self + + created_type = GraphQL::ObjectType.define do + + name(model.name) + description("#{model.model_name.human}") + + # 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]) + when :simple_association + field(field_name, -> { api_types_creator.created_types[field_type] }) do + resolve GraphQL::AssociationResolver.new(field_name, field_type) + end + when :paginated_association + field_type = field_type.first + connection(field_name, -> { api_types_creator.created_types[field_type].connection_type }) do + resolve GraphQL::AssociationResolver.new(field_name, field_type) + end + end + end + + end + created_types[model] = created_type + return created_type # GraphQL::ObjectType + end + + end +end diff --git a/lib/graph_ql/query_type_creator.rb b/lib/graph_ql/query_type_creator.rb new file mode 100644 index 000000000..b7bde9f4e --- /dev/null +++ b/lib/graph_ql/query_type_creator.rb @@ -0,0 +1,40 @@ +require 'graphql' + +module GraphQL + class QueryTypeCreator + + attr_accessor :created_api_types + + def initialize(created_api_types) + @created_api_types = created_api_types + end + + def create + query_type_creator = self + + GraphQL::ObjectType.define do + name 'QueryType' + description 'The root query for the schema' + + query_type_creator.created_api_types.each do |model, created_type| + # debugger + if created_type.fields['id'] + field model.name.underscore.to_sym do + type created_type + description "Find one #{model.model_name.human} by ID" + argument :id, !types.ID + resolve GraphQL::RootElementResolver.new(model) + end + end + + connection model.name.underscore.pluralize.to_sym, created_type.connection_type do + description "Find all #{model.model_name.human.pluralize}" + resolve GraphQL::RootCollectionResolver.new(model) + end + + end + end + end + + end +end diff --git a/lib/graph_ql/type_creator.rb b/lib/graph_ql/type_creator.rb deleted file mode 100644 index b705839c3..000000000 --- a/lib/graph_ql/type_creator.rb +++ /dev/null @@ -1,100 +0,0 @@ -require 'graphql' - -module GraphQL - class TypeCreator - SCALAR_TYPES = { - integer: GraphQL::INT_TYPE, - boolean: GraphQL::BOOLEAN_TYPE, - float: GraphQL::FLOAT_TYPE, - double: GraphQL::FLOAT_TYPE, - string: GraphQL::STRING_TYPE - } - - attr_accessor :created_types, :api_type_definitions, :query_type - - def initialize(api_type_definitions) - @api_type_definitions = api_type_definitions - @created_types = {} - create_api_types - create_query_type - end - - def self.type_kind(type) - if SCALAR_TYPES[type] - :scalar - elsif type.class == Class - :simple_association - elsif type.class == Array - :paginated_association - end - end - - # TODO: this method shouldn't be public just for testing purposes, ¿smell? - def create_type(model, fields) - type_creator = self - - created_type = GraphQL::ObjectType.define do - - name(model.name) - description("#{model.model_name.human}") - - # Make a field for each column, association or method - fields.each do |field_name, field_type| - case TypeCreator.type_kind(field_type) - when :scalar - field(field_name, SCALAR_TYPES[field_type]) - when :simple_association - field(field_name, -> { type_creator.created_types[field_type] }) do - resolve GraphQL::AssociationResolver.new(field_name, field_type) - end - when :paginated_association - field_type = field_type.first - connection(field_name, -> { type_creator.created_types[field_type].connection_type }) do - resolve GraphQL::AssociationResolver.new(field_name, field_type) - end - end - end - - end - created_types[model] = created_type - return created_type # GraphQL::ObjectType - end - - private - - def create_api_types - api_type_definitions.each do |model, info| - self.create_type(model, info[:fields]) - end - end - - def create_query_type - type_creator = self - - @query_type = GraphQL::ObjectType.define do - name 'QueryType' - description 'The root query for this schema' - - type_creator.created_types.each do |model, created_type| - # create field to retrive a single object - if type_creator.api_type_definitions[model][:fields][:id] - field model.name.underscore.to_sym do - type created_type - description "Find one #{model.model_name.human} by ID" - argument :id, !types.ID - resolve GraphQL::RootElementResolver.new(model) - end - end - - # create connection to retrive a collection - connection model.name.underscore.pluralize.to_sym, created_type.connection_type do - description "Find all #{model.model_name.human.pluralize}" - resolve GraphQL::RootCollectionResolver.new(model) - end - end - - end - end - - end -end diff --git a/spec/lib/graph_ql/api_types_creator_spec.rb b/spec/lib/graph_ql/api_types_creator_spec.rb new file mode 100644 index 000000000..10b43ba28 --- /dev/null +++ b/spec/lib/graph_ql/api_types_creator_spec.rb @@ -0,0 +1,58 @@ +require 'rails_helper' + +describe GraphQL::ApiTypesCreator do + let(:api_types_creator) { GraphQL::ApiTypesCreator.new( {} ) } + + describe "::create_type" do + it "creates fields for Int attributes" do + debate_type = api_types_creator.create_type(Debate, { id: :integer }) + 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 = api_types_creator.create_type(Debate, { title: :string }) + 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 = api_types_creator.create_type(User, { id: :integer }) + debate_type = api_types_creator.create_type(Debate, { author: User }) + + 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 = api_types_creator.create_type(User, { organization: Organization }) + organization_type = api_types_creator.create_type(Organization, { id: :integer }) + + 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 = api_types_creator.create_type(Comment, { id: :integer }) + debate_type = api_types_creator.create_type(Debate, { comments: [Comment] }) + + 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 diff --git a/spec/lib/graph_ql/query_type_creator_spec.rb b/spec/lib/graph_ql/query_type_creator_spec.rb new file mode 100644 index 000000000..980c4f1a6 --- /dev/null +++ b/spec/lib/graph_ql/query_type_creator_spec.rb @@ -0,0 +1,39 @@ +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_creator) { GraphQL::ApiTypesCreator.new(api_type_definitions) } + let(:created_api_types) { api_types_creator.create } + let(:query_type_creator) { GraphQL::QueryTypeCreator.new(created_api_types) } + + describe "::create" do + let(:query_type) { query_type_creator.create } + + it 'creates a QueryType with fields to retrieve single objects whose model fields included an ID' do + field = query_type.fields['proposal'] + proposal_type = query_type_creator.created_api_types[Proposal] + + expect(field).to be_a(GraphQL::Field) + expect(field.type).to eq(proposal_type) + 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'] + proposal_type = query_type_creator.created_api_types[Proposal] + + expect(connection).to be_a(GraphQL::Field) + expect(connection.type).to eq(proposal_type.connection_type) + expect(connection.name).to eq('proposals') + end + end +end diff --git a/spec/lib/graph_ql/type_creator_spec.rb b/spec/lib/graph_ql/type_creator_spec.rb deleted file mode 100644 index d5291c961..000000000 --- a/spec/lib/graph_ql/type_creator_spec.rb +++ /dev/null @@ -1,91 +0,0 @@ -require 'rails_helper' - -describe GraphQL::TypeCreator do - let(:api_type_definitions) { {} } - let(:type_creator) { GraphQL::TypeCreator.new(api_type_definitions) } - - describe "::query_type" do - let(:api_type_definitions) do - { - ProposalNotification => { fields: { title: 'string' } }, - Proposal => { fields: { id: 'integer', title: 'string' } } - } - end - let(:query_type) { type_creator.query_type } - - it 'has fields to retrieve single objects whose model fields included an ID' do - field = query_type.fields['proposal'] - proposal_type = type_creator.created_types[Proposal] - - expect(field).to be_a(GraphQL::Field) - expect(field.type).to eq(proposal_type) - expect(field.name).to eq('proposal') - end - - it 'does not have 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 "has connections to retrieve collections of objects" do - connection = query_type.fields['proposals'] - proposal_type = type_creator.created_types[Proposal] - - expect(connection).to be_a(GraphQL::Field) - expect(connection.type).to eq(proposal_type.connection_type) - expect(connection.name).to eq('proposals') - end - end - - describe "::create_type" do - it "creates fields for Int attributes" do - debate_type = type_creator.create_type(Debate, { id: :integer }) - 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 = type_creator.create_type(Debate, { title: :string }) - 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 = type_creator.create_type(User, { id: :integer }) - debate_type = type_creator.create_type(Debate, { author: User }) - - 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 = type_creator.create_type(User, { organization: Organization }) - organization_type = type_creator.create_type(Organization, { id: :integer }) - - 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 = type_creator.create_type(Comment, { id: :integer }) - debate_type = type_creator.create_type(Debate, { comments: [Comment] }) - - 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 From e1b3d468e50923e8c9abcafb2928d99c7e50477b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Sun, 8 Jan 2017 14:27:20 +0100 Subject: [PATCH 107/155] Fix failing spec --- lib/graph_ql/query_type_creator.rb | 1 - spec/lib/graph_ql/query_type_creator_spec.rb | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/graph_ql/query_type_creator.rb b/lib/graph_ql/query_type_creator.rb index b7bde9f4e..9918b0638 100644 --- a/lib/graph_ql/query_type_creator.rb +++ b/lib/graph_ql/query_type_creator.rb @@ -17,7 +17,6 @@ module GraphQL description 'The root query for the schema' query_type_creator.created_api_types.each do |model, created_type| - # debugger if created_type.fields['id'] field model.name.underscore.to_sym do type created_type diff --git a/spec/lib/graph_ql/query_type_creator_spec.rb b/spec/lib/graph_ql/query_type_creator_spec.rb index 980c4f1a6..9669de0c1 100644 --- a/spec/lib/graph_ql/query_type_creator_spec.rb +++ b/spec/lib/graph_ql/query_type_creator_spec.rb @@ -3,8 +3,8 @@ require 'rails_helper' describe GraphQL::QueryTypeCreator do let(:api_type_definitions) do { - ProposalNotification => { fields: { title: 'string' } }, - Proposal => { fields: { id: 'integer', title: 'string' } } + ProposalNotification => { fields: { title: :string } }, + Proposal => { fields: { id: :integer, title: :string } } } end let(:api_types_creator) { GraphQL::ApiTypesCreator.new(api_type_definitions) } From 60786f17c2a11d520304a0403e61d016496aa050 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 9 Jan 2017 10:14:31 +0100 Subject: [PATCH 108/155] Improved code coverage for GraphQL resolvers specs --- lib/graph_ql/root_element_resolver.rb | 17 ++++++++++---- .../graph_ql/root_collection_resolver_spec.rb | 13 ++++++----- .../graph_ql/root_element_resolver_spec.rb | 23 +++++++++++++------ 3 files changed, 35 insertions(+), 18 deletions(-) diff --git a/lib/graph_ql/root_element_resolver.rb b/lib/graph_ql/root_element_resolver.rb index 4191f8eee..cb963ac1f 100644 --- a/lib/graph_ql/root_element_resolver.rb +++ b/lib/graph_ql/root_element_resolver.rb @@ -7,11 +7,18 @@ module GraphQL end def call(object, arguments, context) - if target_model.respond_to?(:public_for_api) - target_model.public_for_api.find_by(id: arguments["id"]) - else - target_model.find_by(id: arguments["id"]) - end + public_elements.find_by(id: arguments['id']) end + + private + + def public_elements + if target_model.respond_to?(:public_for_api) + target_model.public_for_api + else + target_model + end + end + end end diff --git a/spec/lib/graph_ql/root_collection_resolver_spec.rb b/spec/lib/graph_ql/root_collection_resolver_spec.rb index 87f0319ff..edfc6d393 100644 --- a/spec/lib/graph_ql/root_collection_resolver_spec.rb +++ b/spec/lib/graph_ql/root_collection_resolver_spec.rb @@ -1,19 +1,20 @@ require 'rails_helper' describe GraphQL::RootCollectionResolver do + let(:geozones_resolver) { GraphQL::RootCollectionResolver.new(Geozone) } let(:comments_resolver) { GraphQL::RootCollectionResolver.new(Comment) } describe '#call' do - it 'resolves collections' do - comment_1 = create(:comment) - comment_2 = create(:comment) + it 'returns the whole colleciton for unscoped models' do + geozone_1 = create(:geozone) + geozone_2 = create(:geozone) - result = comments_resolver.call(nil, nil, nil) + result = geozones_resolver.call(nil, nil, nil) - expect(result).to match_array([comment_1, comment_2]) + expect(result).to match_array([geozone_1, geozone_2]) end - it 'blocks collection forbidden elements' do + it 'blocks forbidden elements for scoped models' do proposal = create(:proposal, :hidden) comment_1 = create(:comment) comment_2 = create(:comment, commentable: proposal) diff --git a/spec/lib/graph_ql/root_element_resolver_spec.rb b/spec/lib/graph_ql/root_element_resolver_spec.rb index 7ca64a7e8..b86fab36e 100644 --- a/spec/lib/graph_ql/root_element_resolver_spec.rb +++ b/spec/lib/graph_ql/root_element_resolver_spec.rb @@ -2,13 +2,14 @@ require 'rails_helper' describe GraphQL::RootElementResolver do let(:comment_resolver) { GraphQL::RootElementResolver.new(Comment) } + let(:geozone_resolver) { GraphQL::RootElementResolver.new(Geozone) } describe '#call' do + it 'resolves simple elements' do comment = create(:comment) - arguments = { 'id' => comment.id } - result = comment_resolver.call(nil, arguments, nil) + result = comment_resolver.call(nil, {'id' => comment.id}, nil) expect(result).to eq(comment) end @@ -16,20 +17,28 @@ describe GraphQL::RootElementResolver do it 'returns nil when requested element is forbidden' do proposal = create(:proposal, :hidden) comment = create(:comment, commentable: proposal) - arguments = { 'id' => comment.id } - result = comment_resolver.call(nil, arguments, nil) + result = comment_resolver.call(nil, {'id' => comment.id}, nil) expect(result).to be_nil end it 'returns nil when requested element does not exist' do - arguments = { 'id' => 1 } - - result = comment_resolver.call(nil, arguments, nil) + result = comment_resolver.call(nil, {'id' => 1}, nil) expect(result).to be_nil end + + it 'uses the public_for_api scope when available' do + geozone = create(:geozone) + comment = create(:comment, commentable: create(:proposal, :hidden)) + + geozone_result = geozone_resolver.call(nil, {'id' => geozone.id}, nil) + comment_result = comment_resolver.call(nil, {'id' => comment.id}, nil) + + expect(geozone_result).to eq(geozone) + expect(comment_result).to be_nil + end end end From b0bd30e3d1d928d4817e095a4459df710071700c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 9 Jan 2017 13:36:53 +0100 Subject: [PATCH 109/155] Move api config file parsing method into the ApiTypesCreator class --- config/initializers/graphql.rb | 23 +---------------------- lib/graph_ql/api_types_creator.rb | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index 55c34ec1f..f10b42b3b 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -1,26 +1,5 @@ api_config = YAML.load_file('./config/api.yml') - -api_type_definitions = {} - -# Parse API configuration file - -api_config.each do |api_type_model, api_type_info| - model = api_type_model.constantize - options = api_type_info['options'] - 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 GraphQL::ApiTypesCreator::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] = { options: options, fields: fields } -end +api_type_definitions = GraphQL::ApiTypesCreator::parse_api_config_file(api_config) api_types_creator = GraphQL::ApiTypesCreator.new(api_type_definitions) created_api_types = api_types_creator.create diff --git a/lib/graph_ql/api_types_creator.rb b/lib/graph_ql/api_types_creator.rb index 51a7af0e8..374af269d 100644 --- a/lib/graph_ql/api_types_creator.rb +++ b/lib/graph_ql/api_types_creator.rb @@ -64,5 +64,28 @@ module GraphQL return created_type # GraphQL::ObjectType 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 + options = api_type_info['options'] + 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] = { options: options, fields: fields } + end + + api_type_definitions + end end end From 6940b90260a95c836dc7cb2858132df54c1db153 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 9 Jan 2017 14:11:51 +0100 Subject: [PATCH 110/155] Add more fields to api.yml --- app/models/comment.rb | 2 ++ app/models/debate.rb | 1 + app/models/proposal.rb | 1 + app/models/vote.rb | 4 ++++ config/api.yml | 10 ++++++++-- config/initializers/graphql.rb | 2 +- 6 files changed, 17 insertions(+), 3 deletions(-) diff --git a/app/models/comment.rb b/app/models/comment.rb index c1e99fcef..eed5fb4ff 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -19,6 +19,8 @@ class Comment < ActiveRecord::Base belongs_to :commentable, -> { with_hidden }, polymorphic: true, counter_cache: true belongs_to :user, -> { with_hidden } + has_many :votes, -> { for_comments }, foreign_key: 'votable_id' + before_save :calculate_confidence_score scope :for_render, -> { with_hidden.includes(user: :organization) } diff --git a/app/models/debate.rb b/app/models/debate.rb index c2a4eb7f6..34fcf548e 100644 --- a/app/models/debate.rb +++ b/app/models/debate.rb @@ -17,6 +17,7 @@ class Debate < ActiveRecord::Base belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' belongs_to :geozone has_many :comments, as: :commentable + has_many :votes, -> { for_debates }, foreign_key: 'votable_id' validates :title, presence: true validates :description, presence: true diff --git a/app/models/proposal.rb b/app/models/proposal.rb index f915f173a..527ed7968 100644 --- a/app/models/proposal.rb +++ b/app/models/proposal.rb @@ -19,6 +19,7 @@ class Proposal < ActiveRecord::Base belongs_to :geozone has_many :comments, as: :commentable has_many :proposal_notifications + has_many :votes, -> { for_proposals }, foreign_key: 'votable_id' validates :title, presence: true validates :question, presence: true diff --git a/app/models/vote.rb b/app/models/vote.rb index af607ffa8..841a5784e 100644 --- a/app/models/vote.rb +++ b/app/models/vote.rb @@ -1,5 +1,9 @@ class Vote < ActsAsVotable::Vote + scope :for_debates, -> { where(votable_type: 'Debate') } + scope :for_proposals, -> { where(votable_type: 'Proposal') } + scope :for_comments, -> { where(votable_type: 'Comment') } + scope :public_for_api, -> do 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"). diff --git a/config/api.yml b/config/api.yml index 452fa22f7..a8eb1efbe 100644 --- a/config/api.yml +++ b/config/api.yml @@ -1,7 +1,10 @@ User: fields: - id: integer - username: string + id: integer + username: string + debates: [Debate] + proposals: [Proposal] + comments: [Comment] Voter: fields: gender: string @@ -24,6 +27,7 @@ Debate: geozone: Geozone comments: [Comment] public_author: User + votes: [Vote] Proposal: fields: id: integer @@ -45,6 +49,7 @@ Proposal: comments: [Comment] proposal_notifications: [ProposalNotification] public_author: User + votes: [Vote] Comment: fields: id: integer @@ -58,6 +63,7 @@ Comment: ancestry: string confidence_score: integer public_author: User + votes: [Vote] Geozone: fields: id: integer diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index f10b42b3b..d477361fc 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -9,7 +9,7 @@ QueryType = query_type_creator.create ConsulSchema = GraphQL::Schema.define do query QueryType - max_depth 10 + max_depth 12 resolve_type -> (object, ctx) do type_name = object.class.name # look up types by class name From d1f14ffb93ce5c5463d5038db70988cb0d293f8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 12 Jan 2017 19:51:58 +0100 Subject: [PATCH 111/155] Simplify the way GraphQL 'resolve' is used --- app/models/comment.rb | 2 +- app/models/proposal_notification.rb | 4 +- app/models/tag.rb | 6 +- app/models/vote.rb | 2 +- .../initializers/active_record_extensions.rb | 1 + lib/active_record_extensions.rb | 12 ++++ lib/graph_ql/api_types_creator.rb | 4 +- lib/graph_ql/association_resolver.rb | 30 ---------- lib/graph_ql/query_type_creator.rb | 4 +- lib/graph_ql/root_collection_resolver.rb | 17 ------ lib/graph_ql/root_element_resolver.rb | 24 -------- .../lib/graph_ql/association_resolver_spec.rb | 59 ------------------- .../graph_ql/root_collection_resolver_spec.rb | 28 --------- .../graph_ql/root_element_resolver_spec.rb | 44 -------------- 14 files changed, 27 insertions(+), 210 deletions(-) create mode 100644 config/initializers/active_record_extensions.rb create mode 100644 lib/active_record_extensions.rb delete mode 100644 lib/graph_ql/association_resolver.rb delete mode 100644 lib/graph_ql/root_collection_resolver.rb delete mode 100644 lib/graph_ql/root_element_resolver.rb delete mode 100644 spec/lib/graph_ql/association_resolver_spec.rb delete mode 100644 spec/lib/graph_ql/root_collection_resolver_spec.rb delete mode 100644 spec/lib/graph_ql/root_element_resolver_spec.rb diff --git a/app/models/comment.rb b/app/models/comment.rb index eed5fb4ff..5671fa1dc 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -28,7 +28,7 @@ class Comment < ActiveRecord::Base 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 + def self.public_for_api joins("FULL OUTER JOIN debates ON commentable_type = 'Debate' AND commentable_id = debates.id"). joins("FULL OUTER JOIN proposals ON commentable_type = 'Proposal' AND commentable_id = proposals.id"). where("commentable_type = 'Proposal' AND proposals.hidden_at IS NULL OR commentable_type = 'Debate' AND debates.hidden_at IS NULL") diff --git a/app/models/proposal_notification.rb b/app/models/proposal_notification.rb index 686f3ce90..3483806c5 100644 --- a/app/models/proposal_notification.rb +++ b/app/models/proposal_notification.rb @@ -7,7 +7,9 @@ class ProposalNotification < ActiveRecord::Base validates :proposal, presence: true validate :minimum_interval - scope :public_for_api, -> { joins(:proposal).where("proposals.hidden_at IS NULL") } + def self.public_for_api + joins(:proposal).where("proposals.hidden_at IS NULL") + end def minimum_interval return true if proposal.try(:notifications).blank? diff --git a/app/models/tag.rb b/app/models/tag.rb index 8f34a8696..2bfc89942 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -1,3 +1,7 @@ class Tag < ActsAsTaggableOn::Tag - scope :public_for_api, -> { where("kind IS NULL OR kind = 'category'") } + + def self.public_for_api + where("kind IS NULL OR kind = 'category'") + end + end diff --git a/app/models/vote.rb b/app/models/vote.rb index 841a5784e..019191dca 100644 --- a/app/models/vote.rb +++ b/app/models/vote.rb @@ -4,7 +4,7 @@ class Vote < ActsAsVotable::Vote scope :for_proposals, -> { where(votable_type: 'Proposal') } scope :for_comments, -> { where(votable_type: 'Comment') } - scope :public_for_api, -> do + def self.public_for_api 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 comments ON votable_type = 'Comment' AND votable_id = comments.id"). diff --git a/config/initializers/active_record_extensions.rb b/config/initializers/active_record_extensions.rb new file mode 100644 index 000000000..57659f192 --- /dev/null +++ b/config/initializers/active_record_extensions.rb @@ -0,0 +1 @@ +require 'active_record_extensions' diff --git a/lib/active_record_extensions.rb b/lib/active_record_extensions.rb new file mode 100644 index 000000000..1dd9add87 --- /dev/null +++ b/lib/active_record_extensions.rb @@ -0,0 +1,12 @@ +module PublicForApi + + extend ActiveSupport::Concern + + class_methods do + def public_for_api + all + end + end +end + +ActiveRecord::Base.send(:include, PublicForApi) diff --git a/lib/graph_ql/api_types_creator.rb b/lib/graph_ql/api_types_creator.rb index 374af269d..604683de1 100644 --- a/lib/graph_ql/api_types_creator.rb +++ b/lib/graph_ql/api_types_creator.rb @@ -49,12 +49,12 @@ module GraphQL field(field_name, SCALAR_TYPES[field_type]) when :simple_association field(field_name, -> { api_types_creator.created_types[field_type] }) do - resolve GraphQL::AssociationResolver.new(field_name, field_type) + resolve -> (object, arguments, context) { field_type.public_for_api.find(object) } end when :paginated_association field_type = field_type.first connection(field_name, -> { api_types_creator.created_types[field_type].connection_type }) do - resolve GraphQL::AssociationResolver.new(field_name, field_type) + resolve -> (object, arguments, context) { field_type.public_for_api } end end end diff --git a/lib/graph_ql/association_resolver.rb b/lib/graph_ql/association_resolver.rb deleted file mode 100644 index 22dd210f3..000000000 --- a/lib/graph_ql/association_resolver.rb +++ /dev/null @@ -1,30 +0,0 @@ -module GraphQL - class AssociationResolver - attr_reader :field_name, :target_model, :allowed_elements - - def initialize(field_name, target_model) - @field_name = field_name - @target_model = target_model - @allowed_elements = target_public_elements - end - - def call(object, arguments, context) - requested_elements = object.send(field_name) - filter_forbidden_elements(requested_elements) - end - - private - - def target_public_elements - target_model.respond_to?(:public_for_api) ? target_model.public_for_api : target_model.all - end - - def filter_forbidden_elements(requested_elements) - if requested_elements.respond_to?(:each) - requested_elements.all & allowed_elements.all - else - allowed_elements.include?(requested_elements) ? requested_elements : nil - end - end - end -end diff --git a/lib/graph_ql/query_type_creator.rb b/lib/graph_ql/query_type_creator.rb index 9918b0638..6dfde67c0 100644 --- a/lib/graph_ql/query_type_creator.rb +++ b/lib/graph_ql/query_type_creator.rb @@ -22,13 +22,13 @@ module GraphQL type created_type description "Find one #{model.model_name.human} by ID" argument :id, !types.ID - resolve GraphQL::RootElementResolver.new(model) + resolve -> (object, arguments, context) { model.public_for_api.find_by(id: arguments['id'])} end end connection model.name.underscore.pluralize.to_sym, created_type.connection_type do description "Find all #{model.model_name.human.pluralize}" - resolve GraphQL::RootCollectionResolver.new(model) + resolve -> (object, arguments, context) { model.public_for_api } end end diff --git a/lib/graph_ql/root_collection_resolver.rb b/lib/graph_ql/root_collection_resolver.rb deleted file mode 100644 index fa0e3c9ec..000000000 --- a/lib/graph_ql/root_collection_resolver.rb +++ /dev/null @@ -1,17 +0,0 @@ -module GraphQL - class RootCollectionResolver - attr_reader :target_model - - def initialize(target_model) - @target_model = target_model - end - - def call(object, arguments, context) - if target_model.respond_to?(:public_for_api) - target_model.public_for_api - else - target_model.all - end - end - end -end diff --git a/lib/graph_ql/root_element_resolver.rb b/lib/graph_ql/root_element_resolver.rb deleted file mode 100644 index cb963ac1f..000000000 --- a/lib/graph_ql/root_element_resolver.rb +++ /dev/null @@ -1,24 +0,0 @@ -module GraphQL - class RootElementResolver - attr_reader :target_model - - def initialize(target_model) - @target_model = target_model - end - - def call(object, arguments, context) - public_elements.find_by(id: arguments['id']) - end - - private - - def public_elements - if target_model.respond_to?(:public_for_api) - target_model.public_for_api - else - target_model - end - end - - end -end diff --git a/spec/lib/graph_ql/association_resolver_spec.rb b/spec/lib/graph_ql/association_resolver_spec.rb deleted file mode 100644 index 3e8bdf897..000000000 --- a/spec/lib/graph_ql/association_resolver_spec.rb +++ /dev/null @@ -1,59 +0,0 @@ -require 'rails_helper' - -describe GraphQL::AssociationResolver do - let(:comments_resolver) { GraphQL::AssociationResolver.new(:comments, Comment) } - let(:geozone_resolver) { GraphQL::AssociationResolver.new(:geozone, Geozone) } - let(:geozones_resolver) { GraphQL::AssociationResolver.new(:geozones, Geozone) } - - describe '#initialize' do - it 'sets allowed elements for unscoped models' do - geozone_1 = create(:geozone) - geozone_2 = create(:geozone) - - expect(geozones_resolver.allowed_elements).to match_array([geozone_1, geozone_2]) - end - - it 'sets allowed elements for scoped models' do - public_comment = create(:comment, commentable: create(:proposal)) - restricted_comment = create(:comment, commentable: create(:proposal, :hidden)) - - expect(comments_resolver.allowed_elements).to match_array([public_comment]) - end - end - - describe '#call' do - it 'resolves simple associations' do - geozone = create(:geozone) - proposal = create(:proposal, geozone: geozone) - - result = geozone_resolver.call(proposal, nil, nil) - - expect(result).to eq(geozone) - end - - it 'blocks forbidden elements when resolving simple associations' do - skip 'None of the current models allows this spec to be executed' - end - - it 'resolves paginated associations' do - proposal = create(:proposal) - comment_1 = create(:comment, commentable: proposal) - comment_2 = create(:comment, commentable: proposal) - comment_3 = create(:comment, commentable: create(:proposal)) - - result = comments_resolver.call(proposal, nil, nil) - - expect(result).to match_array([comment_1, comment_2]) - end - - it 'blocks forbidden elements when resolving paginated associations' do - proposal = create(:proposal, :hidden) - comment = create(:comment, commentable: proposal) - - result = comments_resolver.call(proposal, nil, nil) - - expect(result).to be_empty - end - end - -end diff --git a/spec/lib/graph_ql/root_collection_resolver_spec.rb b/spec/lib/graph_ql/root_collection_resolver_spec.rb deleted file mode 100644 index edfc6d393..000000000 --- a/spec/lib/graph_ql/root_collection_resolver_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -require 'rails_helper' - -describe GraphQL::RootCollectionResolver do - let(:geozones_resolver) { GraphQL::RootCollectionResolver.new(Geozone) } - let(:comments_resolver) { GraphQL::RootCollectionResolver.new(Comment) } - - describe '#call' do - it 'returns the whole colleciton for unscoped models' do - geozone_1 = create(:geozone) - geozone_2 = create(:geozone) - - result = geozones_resolver.call(nil, nil, nil) - - expect(result).to match_array([geozone_1, geozone_2]) - end - - it 'blocks forbidden elements for scoped models' do - proposal = create(:proposal, :hidden) - comment_1 = create(:comment) - comment_2 = create(:comment, commentable: proposal) - - result = comments_resolver.call(nil, nil, nil) - - expect(result).to match_array([comment_1]) - end - end - -end diff --git a/spec/lib/graph_ql/root_element_resolver_spec.rb b/spec/lib/graph_ql/root_element_resolver_spec.rb deleted file mode 100644 index b86fab36e..000000000 --- a/spec/lib/graph_ql/root_element_resolver_spec.rb +++ /dev/null @@ -1,44 +0,0 @@ -require 'rails_helper' - -describe GraphQL::RootElementResolver do - let(:comment_resolver) { GraphQL::RootElementResolver.new(Comment) } - let(:geozone_resolver) { GraphQL::RootElementResolver.new(Geozone) } - - describe '#call' do - - it 'resolves simple elements' do - comment = create(:comment) - - result = comment_resolver.call(nil, {'id' => comment.id}, nil) - - expect(result).to eq(comment) - end - - it 'returns nil when requested element is forbidden' do - proposal = create(:proposal, :hidden) - comment = create(:comment, commentable: proposal) - - result = comment_resolver.call(nil, {'id' => comment.id}, nil) - - expect(result).to be_nil - end - - it 'returns nil when requested element does not exist' do - result = comment_resolver.call(nil, {'id' => 1}, nil) - - expect(result).to be_nil - end - - it 'uses the public_for_api scope when available' do - geozone = create(:geozone) - comment = create(:comment, commentable: create(:proposal, :hidden)) - - geozone_result = geozone_resolver.call(nil, {'id' => geozone.id}, nil) - comment_result = comment_resolver.call(nil, {'id' => comment.id}, nil) - - expect(geozone_result).to eq(geozone) - expect(comment_result).to be_nil - end - end - -end From 7a9373942a24d88bdd147dde0387e4ad0cc23980 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 12 Jan 2017 20:11:47 +0100 Subject: [PATCH 112/155] Testing GraphQL 1.3.0 in Travis --- Gemfile | 2 +- Gemfile.lock | 4 ++-- app/controllers/graphql_controller.rb | 19 ++++++++++++++++++- config/initializers/graphql.rb | 18 +----------------- spec/lib/graphql_spec.rb | 20 +++++++++++++++++++- 5 files changed, 41 insertions(+), 22 deletions(-) diff --git a/Gemfile b/Gemfile index 94d66a281..c195fc8e8 100644 --- a/Gemfile +++ b/Gemfile @@ -65,7 +65,7 @@ gem 'browser' gem 'turnout', '~> 2.4.0' gem 'redcarpet' -gem 'graphql' +gem 'graphql', '~> 1.3.0' group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console diff --git a/Gemfile.lock b/Gemfile.lock index ed7d74082..4ee71e106 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -178,7 +178,7 @@ GEM activesupport (>= 4.1.0) graphiql-rails (1.3.0) rails - graphql (0.18.11) + graphql (1.3.0) groupdate (3.1.1) activesupport (>= 3) gyoku (1.3.1) @@ -484,7 +484,7 @@ DEPENDENCIES foundation_rails_helper (~> 2.0.0) fuubar graphiql-rails - graphql + graphql (~> 1.3.0) groupdate (~> 3.1.0) i18n-tasks initialjs-rails (= 0.2.0.4) diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index de7ae7de7..f62e2b3dd 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -8,8 +8,25 @@ class GraphqlController < ApplicationController def query begin + # ------------------------------------------------------------------------ + api_types_creator = GraphQL::ApiTypesCreator.new(API_TYPE_DEFINITIONS) + created_api_types = api_types_creator.create + + query_type_creator = GraphQL::QueryTypeCreator.new(created_api_types) + query_type = query_type_creator.create + + consul_schema = GraphQL::Schema.define do + query query_type + max_depth 12 + + resolve_type -> (object, ctx) do + type_name = object.class.name # look up types by class name + ConsulSchema.types[type_name] + end + end + # ------------------------------------------------------------------------ set_query_environment - response = ConsulSchema.execute query_string, variables: query_variables + 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 diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index d477361fc..05cd2333c 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -1,18 +1,2 @@ api_config = YAML.load_file('./config/api.yml') -api_type_definitions = GraphQL::ApiTypesCreator::parse_api_config_file(api_config) - -api_types_creator = GraphQL::ApiTypesCreator.new(api_type_definitions) -created_api_types = api_types_creator.create - -query_type_creator = GraphQL::QueryTypeCreator.new(created_api_types) -QueryType = query_type_creator.create - -ConsulSchema = GraphQL::Schema.define do - query QueryType - max_depth 12 - - resolve_type -> (object, ctx) do - type_name = object.class.name # look up types by class name - ConsulSchema.types[type_name] - end -end +API_TYPE_DEFINITIONS = GraphQL::ApiTypesCreator::parse_api_config_file(api_config) diff --git a/spec/lib/graphql_spec.rb b/spec/lib/graphql_spec.rb index 3dbf393b7..caa2bd085 100644 --- a/spec/lib/graphql_spec.rb +++ b/spec/lib/graphql_spec.rb @@ -1,5 +1,23 @@ require 'rails_helper' +# ------------------------------------------------------------------------------ +api_types_creator = GraphQL::ApiTypesCreator.new(API_TYPE_DEFINITIONS) +created_api_types = api_types_creator.create + +query_type_creator = GraphQL::QueryTypeCreator.new(created_api_types) +QueryType = query_type_creator.create + +ConsulSchema = GraphQL::Schema.define do + query QueryType + max_depth 12 + + resolve_type -> (object, ctx) do + type_name = object.class.name # look up types by class name + ConsulSchema.types[type_name] + end +end +# ------------------------------------------------------------------------------ + def execute(query_string, context = {}, variables = {}) ConsulSchema.execute(query_string, context: context, variables: variables) end @@ -14,7 +32,7 @@ def hidden_field?(response, field_name) data_is_empty && error_is_present end -describe ConsulSchema do +describe 'ConsulSchema' do let(:user) { create(:user) } let(:proposal) { create(:proposal, author: user) } From f2eb8724d30f95c32b34b9f1ec48e33ebca73395 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 13 Jan 2017 00:54:52 +0100 Subject: [PATCH 113/155] Remove duplicated scopes --- app/models/vote.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/models/vote.rb b/app/models/vote.rb index 019191dca..15d486ea4 100644 --- a/app/models/vote.rb +++ b/app/models/vote.rb @@ -1,9 +1,5 @@ class Vote < ActsAsVotable::Vote - scope :for_debates, -> { where(votable_type: 'Debate') } - scope :for_proposals, -> { where(votable_type: 'Proposal') } - scope :for_comments, -> { where(votable_type: 'Comment') } - def self.public_for_api 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"). From d2a8d509b684b4cc3e8927365fbe883ad5cdf366 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 13 Jan 2017 01:21:59 +0100 Subject: [PATCH 114/155] Cleaning --- lib/graph_ql/api_types_creator.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/graph_ql/api_types_creator.rb b/lib/graph_ql/api_types_creator.rb index 604683de1..7e3b263fb 100644 --- a/lib/graph_ql/api_types_creator.rb +++ b/lib/graph_ql/api_types_creator.rb @@ -28,9 +28,9 @@ module GraphQL if SCALAR_TYPES[type] :scalar elsif type.class == Class - :simple_association + :singular_association elsif type.class == Array - :paginated_association + :multiple_association end end @@ -47,11 +47,11 @@ module GraphQL case ApiTypesCreator.type_kind(field_type) when :scalar field(field_name, SCALAR_TYPES[field_type]) - when :simple_association + when :singular_association field(field_name, -> { api_types_creator.created_types[field_type] }) do - resolve -> (object, arguments, context) { field_type.public_for_api.find(object) } + resolve -> (object, arguments, context) { field_type.public_for_api.find_by(id: object.id) } end - when :paginated_association + when :multiple_association field_type = field_type.first connection(field_name, -> { api_types_creator.created_types[field_type].connection_type }) do resolve -> (object, arguments, context) { field_type.public_for_api } From cb0a477d88ada2f9687c76b78f4b125e7634af27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 16 Jan 2017 11:23:48 +0100 Subject: [PATCH 115/155] Fix weird bug caused by pending specs --- spec/lib/graphql_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/lib/graphql_spec.rb b/spec/lib/graphql_spec.rb index caa2bd085..8c5e40910 100644 --- a/spec/lib/graphql_spec.rb +++ b/spec/lib/graphql_spec.rb @@ -47,7 +47,7 @@ describe 'ConsulSchema' do end it "returns has_one associations" do - pending "Organizations are not being exposed yet" + skip "Organizations are not being exposed yet" organization = create(:organization) response = execute("{ user(id: #{organization.user_id}) { organization { name } } }") expect(dig(response, 'data.user.organization.name')).to eq(organization.name) @@ -71,7 +71,7 @@ describe 'ConsulSchema' do end it "executes deeply nested queries" do - pending "Organizations are not being exposed yet" + skip "Organizations are not being exposed yet" org_user = create(:user) organization = create(:organization, user: org_user) org_proposal = create(:proposal, author: org_user) From 0107ddce110918a9585c10ba5f457382e6cdcf6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 16 Jan 2017 11:24:02 +0100 Subject: [PATCH 116/155] Fix bug when resolving multiple associations --- lib/graph_ql/api_types_creator.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/graph_ql/api_types_creator.rb b/lib/graph_ql/api_types_creator.rb index 7e3b263fb..cf266d13f 100644 --- a/lib/graph_ql/api_types_creator.rb +++ b/lib/graph_ql/api_types_creator.rb @@ -54,7 +54,7 @@ module GraphQL when :multiple_association field_type = field_type.first connection(field_name, -> { api_types_creator.created_types[field_type].connection_type }) do - resolve -> (object, arguments, context) { field_type.public_for_api } + resolve -> (object, arguments, context) { field_type.public_for_api & object.send(field_name) } end end end From 232f5aa621573646d1ebd07b338fedd7e29223b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 16 Jan 2017 11:25:12 +0100 Subject: [PATCH 117/155] Parse GraphQL query variables to JSON This is required after updating the gem to 1.3.0 --- app/controllers/graphql_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index f62e2b3dd..5c628fc58 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -52,10 +52,10 @@ class GraphqlController < ApplicationController end def set_query_variables - if params[:variables].nil? + if params[:variables].blank? @query_variables = {} else - @query_variables = params[:variables] + @query_variables = JSON.parse(params[:variables]) end end end From 18fb1485ebc97473111adc8f93f3a1c55a74ff32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 16 Jan 2017 11:28:28 +0100 Subject: [PATCH 118/155] No need to specify the resolve type for GraphQL::Schema This was needed back in the 0.18.11 version but not anymore after the update to 1.3.0 --- app/controllers/graphql_controller.rb | 5 ----- spec/lib/graphql_spec.rb | 5 ----- 2 files changed, 10 deletions(-) diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index 5c628fc58..eb694b981 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -18,11 +18,6 @@ class GraphqlController < ApplicationController consul_schema = GraphQL::Schema.define do query query_type max_depth 12 - - resolve_type -> (object, ctx) do - type_name = object.class.name # look up types by class name - ConsulSchema.types[type_name] - end end # ------------------------------------------------------------------------ set_query_environment diff --git a/spec/lib/graphql_spec.rb b/spec/lib/graphql_spec.rb index 8c5e40910..99597c690 100644 --- a/spec/lib/graphql_spec.rb +++ b/spec/lib/graphql_spec.rb @@ -10,11 +10,6 @@ QueryType = query_type_creator.create ConsulSchema = GraphQL::Schema.define do query QueryType max_depth 12 - - resolve_type -> (object, ctx) do - type_name = object.class.name # look up types by class name - ConsulSchema.types[type_name] - end end # ------------------------------------------------------------------------------ From ef6d089022d7f1b99d3e589d51fef1db0964f785 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Wed, 25 Jan 2017 11:17:08 +0100 Subject: [PATCH 119/155] Refactor GraphqlController --- app/controllers/graphql_controller.rb | 41 +++++++++------------------ 1 file changed, 14 insertions(+), 27 deletions(-) diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index eb694b981..98ceb3111 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -1,5 +1,4 @@ class GraphqlController < ApplicationController - attr_accessor :query_variables, :query_string skip_before_action :verify_authenticity_token skip_authorization_check @@ -8,19 +7,7 @@ class GraphqlController < ApplicationController def query begin - # ------------------------------------------------------------------------ - api_types_creator = GraphQL::ApiTypesCreator.new(API_TYPE_DEFINITIONS) - created_api_types = api_types_creator.create - - query_type_creator = GraphQL::QueryTypeCreator.new(created_api_types) - query_type = query_type_creator.create - - consul_schema = GraphQL::Schema.define do - query query_type - max_depth 12 - end - # ------------------------------------------------------------------------ - set_query_environment + 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 @@ -32,25 +19,25 @@ class GraphqlController < ApplicationController private - def set_query_environment - set_query_string - set_query_variables + def consul_schema + api_types = GraphQL::ApiTypesCreator.new(API_TYPE_DEFINITIONS).create + query_type = GraphQL::QueryTypeCreator.new(api_types).create + + GraphQL::Schema.define do + query query_type + max_depth 12 + end end - def set_query_string + def query_string if request.headers["CONTENT_TYPE"] == 'application/graphql' - @query_string = request.body.string # request.body.class => StringIO + request.body.string # request.body.class => StringIO else - @query_string = params[:query] + params[:query] end - if query_string.nil? then raise GraphqlController::QueryStringError end end - def set_query_variables - if params[:variables].blank? - @query_variables = {} - else - @query_variables = JSON.parse(params[:variables]) - end + def query_variables + params[:variables].blank? ? {} : JSON.parse(params[:variables]) end end From e3fca5c49f0f8008c88978f0e1f9f0548453ea38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Wed, 25 Jan 2017 13:37:29 +0100 Subject: [PATCH 120/155] Remove functionality related to votes demographic info Since this was giving me too much pain and nobody actually requested this functionality, I decided to remove it. --- app/models/comment.rb | 1 - app/models/concerns/public_voters_stats.rb | 12 ------------ app/models/debate.rb | 1 - app/models/proposal.rb | 1 - app/models/vote.rb | 4 ---- app/models/voter.rb | 2 -- config/api.yml | 7 ------- db/dev_seeds.rb | 3 --- lib/age_range_calculator.rb | 15 --------------- lib/graph_ql/api_types_creator.rb | 3 +-- spec/lib/age_range_calculator_spec.rb | 14 -------------- 11 files changed, 1 insertion(+), 62 deletions(-) delete mode 100644 app/models/concerns/public_voters_stats.rb delete mode 100644 app/models/voter.rb delete mode 100644 lib/age_range_calculator.rb delete mode 100644 spec/lib/age_range_calculator_spec.rb diff --git a/app/models/comment.rb b/app/models/comment.rb index 5671fa1dc..118e56b28 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -1,7 +1,6 @@ class Comment < ActiveRecord::Base include Flaggable include HasPublicAuthor - include PublicVotersStats acts_as_paranoid column: :hidden_at include ActsAsParanoidAliases diff --git a/app/models/concerns/public_voters_stats.rb b/app/models/concerns/public_voters_stats.rb deleted file mode 100644 index 88d695f20..000000000 --- a/app/models/concerns/public_voters_stats.rb +++ /dev/null @@ -1,12 +0,0 @@ -module PublicVotersStats - - def votes_above_threshold? - threshold = Setting["#{self.class.name.downcase}_api_votes_threshold"] - threshold = (threshold ? threshold.to_i : default_threshold) - (total_votes >= threshold) - end - - def default_threshold - 200 - end -end diff --git a/app/models/debate.rb b/app/models/debate.rb index 34fcf548e..ce5b63fd7 100644 --- a/app/models/debate.rb +++ b/app/models/debate.rb @@ -8,7 +8,6 @@ class Debate < ActiveRecord::Base include Searchable include Filterable include HasPublicAuthor - include PublicVotersStats acts_as_votable acts_as_paranoid column: :hidden_at diff --git a/app/models/proposal.rb b/app/models/proposal.rb index 527ed7968..0cfc9bcc9 100644 --- a/app/models/proposal.rb +++ b/app/models/proposal.rb @@ -7,7 +7,6 @@ class Proposal < ActiveRecord::Base include Searchable include Filterable include HasPublicAuthor - include PublicVotersStats acts_as_votable acts_as_paranoid column: :hidden_at diff --git a/app/models/vote.rb b/app/models/vote.rb index 15d486ea4..93bcc54be 100644 --- a/app/models/vote.rb +++ b/app/models/vote.rb @@ -7,10 +7,6 @@ class Vote < ActsAsVotable::Vote 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") end - def public_voter - votable.votes_above_threshold? ? voter : nil - end - def public_timestamp self.created_at.change(min: 0) end diff --git a/app/models/voter.rb b/app/models/voter.rb deleted file mode 100644 index 7d4f52676..000000000 --- a/app/models/voter.rb +++ /dev/null @@ -1,2 +0,0 @@ -class Voter < User -end diff --git a/config/api.yml b/config/api.yml index a8eb1efbe..7e8559ced 100644 --- a/config/api.yml +++ b/config/api.yml @@ -5,12 +5,6 @@ User: debates: [Debate] proposals: [Proposal] comments: [Comment] -Voter: - fields: - gender: string - age_range: string - geozone_id: integer - geozone: Geozone Debate: fields: id: integer @@ -86,4 +80,3 @@ Vote: votable_id: integer votable_type: string public_timestamp: string - public_voter: Voter diff --git a/db/dev_seeds.rb b/db/dev_seeds.rb index 347994a00..4a716a160 100644 --- a/db/dev_seeds.rb +++ b/db/dev_seeds.rb @@ -15,9 +15,6 @@ Setting.create(key: 'proposal_code_prefix', value: 'MAD') Setting.create(key: 'votes_for_proposal_success', value: '100') Setting.create(key: 'months_to_archive_proposals', value: '12') Setting.create(key: 'comments_body_max_length', value: '1000') -Setting.create(key: 'debate_api_votes_threshold', value: '2') -Setting.create(key: 'proposal_api_votes_threshold', value: '2') -Setting.create(key: 'comment_api_votes_threshold', value: '2') Setting.create(key: 'twitter_handle', value: '@consul_dev') Setting.create(key: 'twitter_hashtag', value: '#consul_dev') diff --git a/lib/age_range_calculator.rb b/lib/age_range_calculator.rb deleted file mode 100644 index acb12e19e..000000000 --- a/lib/age_range_calculator.rb +++ /dev/null @@ -1,15 +0,0 @@ -class AgeRangeCalculator - - MIN_AGE = 16 - MAX_AGE = 1.0/0.0 # Infinity - RANGES = [ (MIN_AGE..25), (26..40), (41..60), (61..MAX_AGE) ] - - def self.range_from_birthday(dob) - # Inspired by: http://stackoverflow.com/questions/819263/get-persons-age-in-ruby/2357790#2357790 - now = Time.current.to_date - age = now.year - dob.year - ((now.month > dob.month || (now.month == dob.month && now.day >= dob.day)) ? 0 : 1) - - index = RANGES.find_index { |range| range.include?(age) } - index ? RANGES[index] : nil - end -end diff --git a/lib/graph_ql/api_types_creator.rb b/lib/graph_ql/api_types_creator.rb index cf266d13f..03ddd5d3a 100644 --- a/lib/graph_ql/api_types_creator.rb +++ b/lib/graph_ql/api_types_creator.rb @@ -69,7 +69,6 @@ module GraphQL file.each do |api_type_model, api_type_info| model = api_type_model.constantize - options = api_type_info['options'] fields = {} api_type_info['fields'].each do |field_name, field_type| @@ -82,7 +81,7 @@ module GraphQL end end - api_type_definitions[model] = { options: options, fields: fields } + api_type_definitions[model] = { fields: fields } end api_type_definitions diff --git a/spec/lib/age_range_calculator_spec.rb b/spec/lib/age_range_calculator_spec.rb deleted file mode 100644 index a6fd183ed..000000000 --- a/spec/lib/age_range_calculator_spec.rb +++ /dev/null @@ -1,14 +0,0 @@ -require 'rails_helper' - -describe AgeRangeCalculator do - subject { AgeRangeCalculator } - - describe '::range_from_birthday' do - it 'returns the age range' do - expect(subject::range_from_birthday(Time.current - 1.year)).to eq(nil) - expect(subject::range_from_birthday(Time.current - 26.year)).to eq(26..40) - expect(subject::range_from_birthday(Time.current - 60.year)).to eq(41..60) - expect(subject::range_from_birthday(Time.current - 200.year)).to eq(61..subject::MAX_AGE) - end - end -end From e7f55b10e2d180c14c76a90705c8c37eaf3c4fe0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Wed, 25 Jan 2017 13:58:44 +0100 Subject: [PATCH 121/155] Remove obsolete specs --- app/models/user.rb | 4 -- spec/lib/graphql_spec.rb | 11 ++---- spec/models/comment_spec.rb | 1 - .../concerns/public_voters_stats_spec.rb | 39 ------------------- spec/models/debate_spec.rb | 1 - spec/models/proposal_spec.rb | 1 - spec/models/user_spec.rb | 6 --- spec/models/vote_spec.rb | 19 --------- 8 files changed, 3 insertions(+), 79 deletions(-) delete mode 100644 spec/models/concerns/public_voters_stats_spec.rb diff --git a/app/models/user.rb b/app/models/user.rb index 04ca708a9..03f2db27b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -244,10 +244,6 @@ class User < ActiveRecord::Base end delegate :can?, :cannot?, to: :ability - def age_range - AgeRangeCalculator::range_from_birthday(self.date_of_birth).to_s - end - private def clean_document_number diff --git a/spec/lib/graphql_spec.rb b/spec/lib/graphql_spec.rb index 99597c690..b2a9ed477 100644 --- a/spec/lib/graphql_spec.rb +++ b/spec/lib/graphql_spec.rb @@ -1,17 +1,12 @@ require 'rails_helper' -# ------------------------------------------------------------------------------ -api_types_creator = GraphQL::ApiTypesCreator.new(API_TYPE_DEFINITIONS) -created_api_types = api_types_creator.create - -query_type_creator = GraphQL::QueryTypeCreator.new(created_api_types) -QueryType = query_type_creator.create +api_types = GraphQL::ApiTypesCreator.new(API_TYPE_DEFINITIONS).create +query_type = GraphQL::QueryTypeCreator.new(api_types).create ConsulSchema = GraphQL::Schema.define do - query QueryType + query query_type max_depth 12 end -# ------------------------------------------------------------------------------ def execute(query_string, context = {}, variables = {}) ConsulSchema.execute(query_string, context: context, variables: variables) diff --git a/spec/models/comment_spec.rb b/spec/models/comment_spec.rb index 09e6a9900..db68a9e4e 100644 --- a/spec/models/comment_spec.rb +++ b/spec/models/comment_spec.rb @@ -4,7 +4,6 @@ describe Comment do let(:comment) { build(:comment) } - it_behaves_like "public_voters_stats" it_behaves_like "has_public_author" it "is valid" do diff --git a/spec/models/concerns/public_voters_stats_spec.rb b/spec/models/concerns/public_voters_stats_spec.rb deleted file mode 100644 index eaa923599..000000000 --- a/spec/models/concerns/public_voters_stats_spec.rb +++ /dev/null @@ -1,39 +0,0 @@ -require 'spec_helper' - -shared_examples_for 'public_voters_stats' do - let(:model) { described_class } # the class that includes the concern - - describe 'votes_above_threshold?' do - let(:votable) { create(model.to_s.underscore.to_sym) } - - context 'with default threshold value' do - it 'is true when votes are above threshold' do - 200.times { create(:vote, votable: votable) } - - expect(votable.votes_above_threshold?).to be_truthy - end - - it 'is false when votes are under threshold' do - 199.times { create(:vote, votable: votable) } - - expect(votable.votes_above_threshold?).to be_falsey - end - end - - context 'with custom threshold value' do - it 'is true when votes are above threshold' do - create(:setting, key: "#{model.to_s.underscore}_api_votes_threshold", value: '2') - 2.times { create(:vote, votable: votable) } - - expect(votable.votes_above_threshold?).to be_truthy - end - - it 'is false when votes are under threshold' do - create(:setting, key: "#{model.to_s.underscore}_api_votes_threshold", value: '2') - create(:vote, votable: votable) - - expect(votable.votes_above_threshold?).to be_falsey - end - end - end -end diff --git a/spec/models/debate_spec.rb b/spec/models/debate_spec.rb index 4db0e5f0a..5272b6e7a 100644 --- a/spec/models/debate_spec.rb +++ b/spec/models/debate_spec.rb @@ -4,7 +4,6 @@ require 'rails_helper' describe Debate do let(:debate) { build(:debate) } - it_behaves_like "public_voters_stats" it_behaves_like "has_public_author" it "should be valid" do diff --git a/spec/models/proposal_spec.rb b/spec/models/proposal_spec.rb index 08ed6b384..50df57963 100644 --- a/spec/models/proposal_spec.rb +++ b/spec/models/proposal_spec.rb @@ -4,7 +4,6 @@ require 'rails_helper' describe Proposal do let(:proposal) { build(:proposal) } - it_behaves_like "public_voters_stats" it_behaves_like "has_public_author" it "should be valid" do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index ca59cb288..ed0bac164 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -469,10 +469,4 @@ describe User do end - describe "#age_range" do - it 'returns string representation of age range' do - user = create(:user, date_of_birth: Time.current - 41.years) - expect(user.age_range).to eq('41..60') - end - end end diff --git a/spec/models/vote_spec.rb b/spec/models/vote_spec.rb index 9dc30d95a..8e5db0ce7 100644 --- a/spec/models/vote_spec.rb +++ b/spec/models/vote_spec.rb @@ -92,25 +92,6 @@ describe 'Vote' do end end - describe 'public_voter' do - it 'only returns voter if votable has enough votes' do - create(:setting, key: 'proposal_api_votes_threshold', value: '2') - - proposal_1 = create(:proposal) - proposal_2 = create(:proposal) - - voter_1 = create(:user) - voter_2 = create(:user) - - vote_1 = create(:vote, votable: proposal_1, voter: voter_1) - vote_2 = create(:vote, votable: proposal_2, voter: voter_1) - vote_3 = create(:vote, votable: proposal_2, voter: voter_2) - - expect(vote_1.public_voter).to be_nil - expect(vote_2.public_voter).to eq(voter_1) - end - end - describe '#public_timestamp' do it "truncates created_at timestamp up to minutes" do vote = create(:vote, created_at: Time.zone.parse('2016-02-10 15:30:45')) From 83267330a772c15bf9d9a89de7e6915b5447c67b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 26 Jan 2017 10:47:12 +0100 Subject: [PATCH 122/155] Group methods related to text generation for GraphQL documentation into a module --- app/models/comment.rb | 1 + app/models/concerns/graphqlable.rb | 32 +++++++++++++++++++++++++++++ app/models/debate.rb | 1 + app/models/geozone.rb | 3 +++ app/models/proposal.rb | 1 + app/models/proposal_notification.rb | 3 +++ app/models/user.rb | 2 ++ app/models/vote.rb | 2 ++ lib/graph_ql/api_types_creator.rb | 4 ++-- lib/graph_ql/query_type_creator.rb | 8 ++++---- 10 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 app/models/concerns/graphqlable.rb diff --git a/app/models/comment.rb b/app/models/comment.rb index 118e56b28..3c7e2c8db 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -1,6 +1,7 @@ class Comment < ActiveRecord::Base include Flaggable include HasPublicAuthor + include Graphqlable acts_as_paranoid column: :hidden_at include ActsAsParanoidAliases diff --git a/app/models/concerns/graphqlable.rb b/app/models/concerns/graphqlable.rb new file mode 100644 index 000000000..0e8fdb2de --- /dev/null +++ b/app/models/concerns/graphqlable.rb @@ -0,0 +1,32 @@ +module Graphqlable + extend ActiveSupport::Concern + + class_methods do + + def graphql_field_name + self.name.gsub('::', '_').underscore.to_sym + end + + def graphql_pluralized_field_name + self.name.gsub('::', '_').underscore.pluralize.to_sym + end + + def graphql_field_description + "Find one #{self.model_name.human} by ID" + 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 + +end diff --git a/app/models/debate.rb b/app/models/debate.rb index ce5b63fd7..cd4b147cb 100644 --- a/app/models/debate.rb +++ b/app/models/debate.rb @@ -8,6 +8,7 @@ class Debate < ActiveRecord::Base include Searchable include Filterable include HasPublicAuthor + include Graphqlable acts_as_votable acts_as_paranoid column: :hidden_at diff --git a/app/models/geozone.rb b/app/models/geozone.rb index 7e38ce97d..55121d0ee 100644 --- a/app/models/geozone.rb +++ b/app/models/geozone.rb @@ -1,4 +1,7 @@ class Geozone < ActiveRecord::Base + + include Graphqlable + has_many :proposals has_many :spending_proposals has_many :debates diff --git a/app/models/proposal.rb b/app/models/proposal.rb index 0cfc9bcc9..6e286104e 100644 --- a/app/models/proposal.rb +++ b/app/models/proposal.rb @@ -7,6 +7,7 @@ class Proposal < ActiveRecord::Base include Searchable include Filterable include HasPublicAuthor + include Graphqlable acts_as_votable acts_as_paranoid column: :hidden_at diff --git a/app/models/proposal_notification.rb b/app/models/proposal_notification.rb index 3483806c5..7faa0fec1 100644 --- a/app/models/proposal_notification.rb +++ b/app/models/proposal_notification.rb @@ -1,4 +1,7 @@ class ProposalNotification < ActiveRecord::Base + + include Graphqlable + belongs_to :author, class_name: 'User', foreign_key: 'author_id' belongs_to :proposal diff --git a/app/models/user.rb b/app/models/user.rb index 03f2db27b..e2916306c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -9,6 +9,8 @@ class User < ActiveRecord::Base acts_as_paranoid column: :hidden_at include ActsAsParanoidAliases + include Graphqlable + has_one :administrator has_one :moderator has_one :valuator diff --git a/app/models/vote.rb b/app/models/vote.rb index 93bcc54be..eae53e9c3 100644 --- a/app/models/vote.rb +++ b/app/models/vote.rb @@ -1,5 +1,7 @@ class Vote < ActsAsVotable::Vote + include Graphqlable + def self.public_for_api 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"). diff --git a/lib/graph_ql/api_types_creator.rb b/lib/graph_ql/api_types_creator.rb index 03ddd5d3a..4cb3b18c4 100644 --- a/lib/graph_ql/api_types_creator.rb +++ b/lib/graph_ql/api_types_creator.rb @@ -39,8 +39,8 @@ module GraphQL created_type = GraphQL::ObjectType.define do - name(model.name) - description("#{model.model_name.human}") + 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| diff --git a/lib/graph_ql/query_type_creator.rb b/lib/graph_ql/query_type_creator.rb index 6dfde67c0..79020950e 100644 --- a/lib/graph_ql/query_type_creator.rb +++ b/lib/graph_ql/query_type_creator.rb @@ -18,16 +18,16 @@ module GraphQL query_type_creator.created_api_types.each do |model, created_type| if created_type.fields['id'] - field model.name.underscore.to_sym do + field model.graphql_field_name do type created_type - description "Find one #{model.model_name.human} by ID" + 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.name.underscore.pluralize.to_sym, created_type.connection_type do - description "Find all #{model.model_name.human.pluralize}" + connection model.graphql_pluralized_field_name, created_type.connection_type do + description model.graphql_pluralized_field_description resolve -> (object, arguments, context) { model.public_for_api } end From 69fc161b8338fbec6f1b65703d3d2b00fd276f3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 26 Jan 2017 10:48:05 +0100 Subject: [PATCH 123/155] Show taggings in API --- app/models/tag.rb | 7 ------- config/api.yml | 4 +++- config/initializers/acts_as_taggable_on.rb | 8 +++++++- 3 files changed, 10 insertions(+), 9 deletions(-) delete mode 100644 app/models/tag.rb diff --git a/app/models/tag.rb b/app/models/tag.rb deleted file mode 100644 index 2bfc89942..000000000 --- a/app/models/tag.rb +++ /dev/null @@ -1,7 +0,0 @@ -class Tag < ActsAsTaggableOn::Tag - - def self.public_for_api - where("kind IS NULL OR kind = 'category'") - end - -end diff --git a/config/api.yml b/config/api.yml index 7e8559ced..4fda3393c 100644 --- a/config/api.yml +++ b/config/api.yml @@ -22,6 +22,7 @@ Debate: comments: [Comment] public_author: User votes: [Vote] + tags: ["ActsAsTaggableOn::Tag"] Proposal: fields: id: integer @@ -44,6 +45,7 @@ Proposal: proposal_notifications: [ProposalNotification] public_author: User votes: [Vote] + tags: ["ActsAsTaggableOn::Tag"] Comment: fields: id: integer @@ -69,7 +71,7 @@ ProposalNotification: proposal_id: integer created_at: string proposal: Proposal -Tag: +ActsAsTaggableOn::Tag: fields: id: integer name: string diff --git a/config/initializers/acts_as_taggable_on.rb b/config/initializers/acts_as_taggable_on.rb index 847a8c993..45b6e706c 100644 --- a/config/initializers/acts_as_taggable_on.rb +++ b/config/initializers/acts_as_taggable_on.rb @@ -20,6 +20,8 @@ module ActsAsTaggableOn Tag.class_eval do + include Graphqlable + def increment_custom_counter_for(taggable_type) Tag.increment_counter(custom_counter_field_name_for(taggable_type), id) end @@ -42,10 +44,14 @@ module ActsAsTaggableOn ActsAsTaggableOn::Tag.where('taggings.taggable_type' => 'SpendingProposal').includes(:taggings).order(:name).uniq end + def self.public_for_api + where("kind IS NULL OR kind = 'category'") + end + private def custom_counter_field_name_for(taggable_type) "#{taggable_type.underscore.pluralize}_count" end end -end \ No newline at end of file +end From e8fc387574b6f3c44bb74324f59ae14ada37e825 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 26 Jan 2017 10:57:02 +0100 Subject: [PATCH 124/155] Add Organization to API --- app/models/organization.rb | 3 +++ config/api.yml | 16 +++++++++++----- spec/lib/graphql_spec.rb | 2 -- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/app/models/organization.rb b/app/models/organization.rb index 74fd16111..94ef986a1 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -1,4 +1,7 @@ class Organization < ActiveRecord::Base + + include Graphqlable + belongs_to :user, touch: true validates :name, presence: true diff --git a/config/api.yml b/config/api.yml index 4fda3393c..d11a94bd3 100644 --- a/config/api.yml +++ b/config/api.yml @@ -1,10 +1,11 @@ User: fields: - id: integer - username: string - debates: [Debate] - proposals: [Proposal] - comments: [Comment] + id: integer + username: string + debates: [Debate] + proposals: [Proposal] + comments: [Comment] + organization: Organization Debate: fields: id: integer @@ -82,3 +83,8 @@ Vote: votable_id: integer votable_type: string public_timestamp: string +Organization: + fields: + id: integer + user_id: integer + name: string diff --git a/spec/lib/graphql_spec.rb b/spec/lib/graphql_spec.rb index b2a9ed477..bc491f6ba 100644 --- a/spec/lib/graphql_spec.rb +++ b/spec/lib/graphql_spec.rb @@ -37,7 +37,6 @@ describe 'ConsulSchema' do end it "returns has_one associations" do - skip "Organizations are not being exposed yet" organization = create(:organization) response = execute("{ user(id: #{organization.user_id}) { organization { name } } }") expect(dig(response, 'data.user.organization.name')).to eq(organization.name) @@ -61,7 +60,6 @@ describe 'ConsulSchema' do end it "executes deeply nested queries" do - skip "Organizations are not being exposed yet" org_user = create(:user) organization = create(:organization, user: org_user) org_proposal = create(:proposal, author: org_user) From 0c00b5376565f629323b12b0b62b553198ae9b1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 26 Jan 2017 10:59:46 +0100 Subject: [PATCH 125/155] Add Vote vote_flag field to API --- config/api.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/api.yml b/config/api.yml index d11a94bd3..669602611 100644 --- a/config/api.yml +++ b/config/api.yml @@ -83,6 +83,7 @@ Vote: votable_id: integer votable_type: string public_timestamp: string + vote_flag: boolean Organization: fields: id: integer From 14798deab0686f3948e0c19089bee2405b21f19c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 26 Jan 2017 12:00:01 +0100 Subject: [PATCH 126/155] Fix bug in singular_association resolver --- lib/graph_ql/api_types_creator.rb | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/graph_ql/api_types_creator.rb b/lib/graph_ql/api_types_creator.rb index 4cb3b18c4..2142a9b1e 100644 --- a/lib/graph_ql/api_types_creator.rb +++ b/lib/graph_ql/api_types_creator.rb @@ -49,7 +49,14 @@ module GraphQL field(field_name, SCALAR_TYPES[field_type]) when :singular_association field(field_name, -> { api_types_creator.created_types[field_type] }) do - resolve -> (object, arguments, context) { field_type.public_for_api.find_by(id: object.id) } + resolve -> (object, arguments, context) do + association_target = object.send(field_name) + if association_target.nil? + nil + else + field_type.public_for_api.find_by(id: association_target.id) + end + end end when :multiple_association field_type = field_type.first From 7cbd8da9941e75c50f2cddce52911fc235a1d8ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 26 Jan 2017 12:26:42 +0100 Subject: [PATCH 127/155] Move outdated Tag specs to ActsAsTaggableOn::Tag specs file --- spec/lib/acts_as_taggable_on_spec.rb | 20 ++++++++++++++++++++ spec/models/tag_spec.rb | 24 ------------------------ 2 files changed, 20 insertions(+), 24 deletions(-) delete mode 100644 spec/models/tag_spec.rb diff --git a/spec/lib/acts_as_taggable_on_spec.rb b/spec/lib/acts_as_taggable_on_spec.rb index dbf76d52b..59a469f88 100644 --- a/spec/lib/acts_as_taggable_on_spec.rb +++ b/spec/lib/acts_as_taggable_on_spec.rb @@ -64,6 +64,26 @@ 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" do + tag = create(:tag, kind: nil) + + expect(ActsAsTaggableOn::Tag.public_for_api).to include(tag) + end + + it "returns tags whose kind is 'category'" do + tag = create(:tag, kind: 'category') + + expect(ActsAsTaggableOn::Tag.public_for_api).to include(tag) + end + + it "blocks other kinds of tags" do + tag = create(:tag, kind: 'foo') + + expect(ActsAsTaggableOn::Tag.public_for_api).not_to include(tag) + end + end end end diff --git a/spec/models/tag_spec.rb b/spec/models/tag_spec.rb deleted file mode 100644 index fa26b7622..000000000 --- a/spec/models/tag_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -require 'rails_helper' - -describe 'Tag' do - - describe "public_for_api scope" do - it "returns tags whose kind is NULL" do - tag = create(:tag, kind: nil) - - expect(Tag.public_for_api).to include(tag) - end - - it "returns tags whose kind is 'category'" do - tag = create(:tag, kind: 'category') - - expect(Tag.public_for_api).to include(tag) - end - - it "blocks other kinds of tags" do - tag = create(:tag, kind: 'foo') - - expect(Tag.public_for_api).not_to include(tag) - end - end -end From 4b7cebf68647f3d53f7e96616b7fb28af51da001 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 26 Jan 2017 18:29:59 +0100 Subject: [PATCH 128/155] Improved public_for_api scopes and corresponding specs --- config/initializers/acts_as_taggable_on.rb | 17 ++++++- spec/lib/acts_as_taggable_on_spec.rb | 53 +++++++++++++++++++++- spec/models/comment_spec.rb | 9 +++- spec/models/debate_spec.rb | 12 +++++ spec/models/proposal_spec.rb | 12 +++++ 5 files changed, 98 insertions(+), 5 deletions(-) diff --git a/config/initializers/acts_as_taggable_on.rb b/config/initializers/acts_as_taggable_on.rb index 45b6e706c..719f1271f 100644 --- a/config/initializers/acts_as_taggable_on.rb +++ b/config/initializers/acts_as_taggable_on.rb @@ -21,7 +21,7 @@ module ActsAsTaggableOn Tag.class_eval do include Graphqlable - + def increment_custom_counter_for(taggable_type) Tag.increment_counter(custom_counter_field_name_for(taggable_type), id) end @@ -45,7 +45,20 @@ module ActsAsTaggableOn end def self.public_for_api - where("kind IS NULL OR kind = 'category'") + find_by_sql(%| + SELECT * + FROM tags + WHERE (tags.kind IS NULL OR tags.kind = 'category') AND tags.id IN ( + SELECT tag_id + FROM ( + SELECT COUNT(taggings.id) AS taggings_count, tag_id + FROM ((taggings FULL OUTER JOIN proposals ON taggable_type = 'Proposal' AND taggable_id = proposals.id) FULL OUTER JOIN debates ON taggable_type = 'Debate' AND taggable_id = debates.id) + WHERE (taggable_type = 'Proposal' AND proposals.hidden_at IS NULL) OR (taggable_type = 'Debate' AND debates.hidden_at IS NULL) + GROUP BY tag_id + ) AS tag_taggings_count_relation + WHERE taggings_count > 0 + ) + |) end private diff --git a/spec/lib/acts_as_taggable_on_spec.rb b/spec/lib/acts_as_taggable_on_spec.rb index 59a469f88..d261bbad2 100644 --- a/spec/lib/acts_as_taggable_on_spec.rb +++ b/spec/lib/acts_as_taggable_on_spec.rb @@ -66,23 +66,72 @@ describe 'ActsAsTaggableOn' do end describe "public_for_api scope" do - it "returns tags whose kind is NULL" 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'" do + 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 diff --git a/spec/models/comment_spec.rb b/spec/models/comment_spec.rb index db68a9e4e..993710c13 100644 --- a/spec/models/comment_spec.rb +++ b/spec/models/comment_spec.rb @@ -134,7 +134,7 @@ describe Comment do end end - describe "public_for_api" do + describe "public_for_api scope" do it "returns comments" do comment = create(:comment) @@ -174,5 +174,12 @@ describe Comment do expect(Comment.public_for_api).not_to include(comment) end + + it 'does not return comments on budget investments' do + budget_investment = create(:budget_investment) + comment = create(:comment, commentable: budget_investment) + + expect(Comment.public_for_api).not_to include(comment) + end end end diff --git a/spec/models/debate_spec.rb b/spec/models/debate_spec.rb index 5272b6e7a..bae8a5f16 100644 --- a/spec/models/debate_spec.rb +++ b/spec/models/debate_spec.rb @@ -700,4 +700,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 diff --git a/spec/models/proposal_spec.rb b/spec/models/proposal_spec.rb index 7c8d3fb41..b2bb63140 100644 --- a/spec/models/proposal_spec.rb +++ b/spec/models/proposal_spec.rb @@ -842,4 +842,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 From 6101848f7affd5e43c597ae5f85fa8f1b29d2209 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 27 Jan 2017 11:54:02 +0100 Subject: [PATCH 129/155] Run pending migration --- db/schema.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/schema.rb b/db/schema.rb index fb563c8db..b332f05d7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -578,7 +578,7 @@ ActiveRecord::Schema.define(version: 20170114154421) 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-02 13:51:14', null: false + t.datetime "password_changed_at", default: '2016-12-21 17:55:08', null: false t.boolean "created_from_signature", default: false end From 073ce38a8d190e08f9ac08885a447541a6476cb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 27 Jan 2017 12:39:44 +0100 Subject: [PATCH 130/155] Improved API docs autogeneration --- app/models/concerns/graphqlable.rb | 8 ++++---- config/initializers/acts_as_taggable_on.rb | 12 ++++++++++++ lib/graph_ql/api_types_creator.rb | 2 +- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/app/models/concerns/graphqlable.rb b/app/models/concerns/graphqlable.rb index 0e8fdb2de..b62d57808 100644 --- a/app/models/concerns/graphqlable.rb +++ b/app/models/concerns/graphqlable.rb @@ -7,14 +7,14 @@ module Graphqlable self.name.gsub('::', '_').underscore.to_sym end - def graphql_pluralized_field_name - self.name.gsub('::', '_').underscore.pluralize.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 diff --git a/config/initializers/acts_as_taggable_on.rb b/config/initializers/acts_as_taggable_on.rb index 719f1271f..68dad364e 100644 --- a/config/initializers/acts_as_taggable_on.rb +++ b/config/initializers/acts_as_taggable_on.rb @@ -61,6 +61,18 @@ module ActsAsTaggableOn |) 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" diff --git a/lib/graph_ql/api_types_creator.rb b/lib/graph_ql/api_types_creator.rb index 2142a9b1e..ddad7ca04 100644 --- a/lib/graph_ql/api_types_creator.rb +++ b/lib/graph_ql/api_types_creator.rb @@ -46,7 +46,7 @@ module GraphQL fields.each do |field_name, field_type| case ApiTypesCreator.type_kind(field_type) when :scalar - field(field_name, SCALAR_TYPES[field_type]) + field(field_name, SCALAR_TYPES[field_type], model.human_attribute_name(field_name)) when :singular_association field(field_name, -> { api_types_creator.created_types[field_type] }) do resolve -> (object, arguments, context) do From 620c83fb69143878d562843b457cb69f265caaa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 27 Jan 2017 12:54:33 +0100 Subject: [PATCH 131/155] Simplify the way QueryTypeCreator is used --- app/controllers/graphql_controller.rb | 4 ++-- lib/graph_ql/query_type_creator.rb | 12 ++---------- spec/lib/graph_ql/query_type_creator_spec.rb | 12 ++++-------- spec/lib/graphql_spec.rb | 5 ++--- 4 files changed, 10 insertions(+), 23 deletions(-) diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index 98ceb3111..ddc143e62 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -20,8 +20,8 @@ class GraphqlController < ApplicationController private def consul_schema - api_types = GraphQL::ApiTypesCreator.new(API_TYPE_DEFINITIONS).create - query_type = GraphQL::QueryTypeCreator.new(api_types).create + api_types = GraphQL::ApiTypesCreator.new(API_TYPE_DEFINITIONS).create + query_type = GraphQL::QueryTypeCreator.create(api_types) GraphQL::Schema.define do query query_type diff --git a/lib/graph_ql/query_type_creator.rb b/lib/graph_ql/query_type_creator.rb index 79020950e..36e6626b9 100644 --- a/lib/graph_ql/query_type_creator.rb +++ b/lib/graph_ql/query_type_creator.rb @@ -3,20 +3,12 @@ require 'graphql' module GraphQL class QueryTypeCreator - attr_accessor :created_api_types - - def initialize(created_api_types) - @created_api_types = created_api_types - end - - def create - query_type_creator = self - + def self.create(api_types) GraphQL::ObjectType.define do name 'QueryType' description 'The root query for the schema' - query_type_creator.created_api_types.each do |model, created_type| + api_types.each do |model, created_type| if created_type.fields['id'] field model.graphql_field_name do type created_type diff --git a/spec/lib/graph_ql/query_type_creator_spec.rb b/spec/lib/graph_ql/query_type_creator_spec.rb index 9669de0c1..7412ae0da 100644 --- a/spec/lib/graph_ql/query_type_creator_spec.rb +++ b/spec/lib/graph_ql/query_type_creator_spec.rb @@ -7,19 +7,16 @@ describe GraphQL::QueryTypeCreator do Proposal => { fields: { id: :integer, title: :string } } } end - let(:api_types_creator) { GraphQL::ApiTypesCreator.new(api_type_definitions) } - let(:created_api_types) { api_types_creator.create } - let(:query_type_creator) { GraphQL::QueryTypeCreator.new(created_api_types) } + let(:api_types) { GraphQL::ApiTypesCreator.new(api_type_definitions).create } describe "::create" do - let(:query_type) { query_type_creator.create } + 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'] - proposal_type = query_type_creator.created_api_types[Proposal] expect(field).to be_a(GraphQL::Field) - expect(field.type).to eq(proposal_type) + expect(field.type).to eq(api_types[Proposal]) expect(field.name).to eq('proposal') end @@ -29,10 +26,9 @@ describe GraphQL::QueryTypeCreator do it "creates a QueryType with connections to retrieve collections of objects" do connection = query_type.fields['proposals'] - proposal_type = query_type_creator.created_api_types[Proposal] expect(connection).to be_a(GraphQL::Field) - expect(connection.type).to eq(proposal_type.connection_type) + expect(connection.type).to eq(api_types[Proposal].connection_type) expect(connection.name).to eq('proposals') end end diff --git a/spec/lib/graphql_spec.rb b/spec/lib/graphql_spec.rb index bc491f6ba..2d976a608 100644 --- a/spec/lib/graphql_spec.rb +++ b/spec/lib/graphql_spec.rb @@ -1,8 +1,7 @@ require 'rails_helper' -api_types = GraphQL::ApiTypesCreator.new(API_TYPE_DEFINITIONS).create -query_type = GraphQL::QueryTypeCreator.new(api_types).create - +api_types = GraphQL::ApiTypesCreator.new(API_TYPE_DEFINITIONS).create +query_type = GraphQL::QueryTypeCreator.create(api_types) ConsulSchema = GraphQL::Schema.define do query query_type max_depth 12 From c52602e04b4b28237cd469087478a3c8dad8f832 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 27 Jan 2017 13:13:19 +0100 Subject: [PATCH 132/155] Simplify the way ApiTypesCreator is used --- app/controllers/graphql_controller.rb | 2 +- lib/graph_ql/api_types_creator.rb | 25 +++++++------------- spec/lib/graph_ql/api_types_creator_spec.rb | 18 +++++++------- spec/lib/graph_ql/query_type_creator_spec.rb | 2 +- spec/lib/graphql_spec.rb | 2 +- 5 files changed, 20 insertions(+), 29 deletions(-) diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index ddc143e62..ea1decdb5 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -20,7 +20,7 @@ class GraphqlController < ApplicationController private def consul_schema - api_types = GraphQL::ApiTypesCreator.new(API_TYPE_DEFINITIONS).create + api_types = GraphQL::ApiTypesCreator.create(API_TYPE_DEFINITIONS) query_type = GraphQL::QueryTypeCreator.create(api_types) GraphQL::Schema.define do diff --git a/lib/graph_ql/api_types_creator.rb b/lib/graph_ql/api_types_creator.rb index ddad7ca04..c82a5e25e 100644 --- a/lib/graph_ql/api_types_creator.rb +++ b/lib/graph_ql/api_types_creator.rb @@ -10,16 +10,10 @@ module GraphQL string: GraphQL::STRING_TYPE } - attr_accessor :created_types - - def initialize(api_type_definitions) - @api_type_definitions = api_type_definitions - @created_types = {} - end - - def create - @api_type_definitions.each do |model, info| - self.create_type(model, info[:fields]) + 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 @@ -34,10 +28,9 @@ module GraphQL end end - def create_type(model, fields) - api_types_creator = self + def self.create_type(model, fields, created_types) - created_type = GraphQL::ObjectType.define do + created_types[model] = GraphQL::ObjectType.define do name model.graphql_type_name description model.graphql_type_description @@ -48,7 +41,7 @@ module GraphQL when :scalar field(field_name, SCALAR_TYPES[field_type], model.human_attribute_name(field_name)) when :singular_association - field(field_name, -> { api_types_creator.created_types[field_type] }) do + field(field_name, -> { created_types[field_type] }) do resolve -> (object, arguments, context) do association_target = object.send(field_name) if association_target.nil? @@ -60,15 +53,13 @@ module GraphQL end when :multiple_association field_type = field_type.first - connection(field_name, -> { api_types_creator.created_types[field_type].connection_type }) do + connection(field_name, -> { created_types[field_type].connection_type }) do resolve -> (object, arguments, context) { field_type.public_for_api & object.send(field_name) } end end end end - created_types[model] = created_type - return created_type # GraphQL::ObjectType end def self.parse_api_config_file(file) diff --git a/spec/lib/graph_ql/api_types_creator_spec.rb b/spec/lib/graph_ql/api_types_creator_spec.rb index 10b43ba28..637c7f1fe 100644 --- a/spec/lib/graph_ql/api_types_creator_spec.rb +++ b/spec/lib/graph_ql/api_types_creator_spec.rb @@ -1,11 +1,11 @@ require 'rails_helper' describe GraphQL::ApiTypesCreator do - let(:api_types_creator) { GraphQL::ApiTypesCreator.new( {} ) } + let(:created_types) { {} } describe "::create_type" do it "creates fields for Int attributes" do - debate_type = api_types_creator.create_type(Debate, { id: :integer }) + 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) @@ -14,7 +14,7 @@ describe GraphQL::ApiTypesCreator do end it "creates fields for String attributes" do - debate_type = api_types_creator.create_type(Debate, { title: :string }) + 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) @@ -23,8 +23,8 @@ describe GraphQL::ApiTypesCreator do end it "creates connections for :belongs_to associations" do - user_type = api_types_creator.create_type(User, { id: :integer }) - debate_type = api_types_creator.create_type(Debate, { author: User }) + 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'] @@ -34,8 +34,8 @@ describe GraphQL::ApiTypesCreator do end it "creates connections for :has_one associations" do - user_type = api_types_creator.create_type(User, { organization: Organization }) - organization_type = api_types_creator.create_type(Organization, { id: :integer }) + 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'] @@ -45,8 +45,8 @@ describe GraphQL::ApiTypesCreator do end it "creates connections for :has_many associations" do - comment_type = api_types_creator.create_type(Comment, { id: :integer }) - debate_type = api_types_creator.create_type(Debate, { comments: [Comment] }) + 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'] diff --git a/spec/lib/graph_ql/query_type_creator_spec.rb b/spec/lib/graph_ql/query_type_creator_spec.rb index 7412ae0da..6d3fe7a2a 100644 --- a/spec/lib/graph_ql/query_type_creator_spec.rb +++ b/spec/lib/graph_ql/query_type_creator_spec.rb @@ -7,7 +7,7 @@ describe GraphQL::QueryTypeCreator do Proposal => { fields: { id: :integer, title: :string } } } end - let(:api_types) { GraphQL::ApiTypesCreator.new(api_type_definitions).create } + let(:api_types) { GraphQL::ApiTypesCreator.create(api_type_definitions) } describe "::create" do let(:query_type) { GraphQL::QueryTypeCreator.create(api_types) } diff --git a/spec/lib/graphql_spec.rb b/spec/lib/graphql_spec.rb index 2d976a608..06f89a356 100644 --- a/spec/lib/graphql_spec.rb +++ b/spec/lib/graphql_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -api_types = GraphQL::ApiTypesCreator.new(API_TYPE_DEFINITIONS).create +api_types = GraphQL::ApiTypesCreator.create(API_TYPE_DEFINITIONS) query_type = GraphQL::QueryTypeCreator.create(api_types) ConsulSchema = GraphQL::Schema.define do query query_type From cb06075c682686bd1ef7a8e76c2db9ddb8cfcc81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 27 Jan 2017 13:47:09 +0100 Subject: [PATCH 133/155] Clarify spec message --- spec/models/comment_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/models/comment_spec.rb b/spec/models/comment_spec.rb index 993710c13..a86566734 100644 --- a/spec/models/comment_spec.rb +++ b/spec/models/comment_spec.rb @@ -175,7 +175,7 @@ describe Comment do expect(Comment.public_for_api).not_to include(comment) end - it 'does not return comments on budget investments' do + 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) From 6b47ce065f18fc3579db17d00d2722e1b6e0a029 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 27 Jan 2017 14:27:28 +0100 Subject: [PATCH 134/155] Fix buggy associations --- app/models/comment.rb | 2 -- app/models/debate.rb | 1 - app/models/proposal.rb | 1 - config/api.yml | 6 +++--- 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/app/models/comment.rb b/app/models/comment.rb index 269dac25d..16bb207d3 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -19,8 +19,6 @@ class Comment < ActiveRecord::Base belongs_to :commentable, -> { with_hidden }, polymorphic: true, counter_cache: true belongs_to :user, -> { with_hidden } - has_many :votes, -> { for_comments }, foreign_key: 'votable_id' - before_save :calculate_confidence_score scope :for_render, -> { with_hidden.includes(user: :organization) } diff --git a/app/models/debate.rb b/app/models/debate.rb index 56adb4f80..8254bdc10 100644 --- a/app/models/debate.rb +++ b/app/models/debate.rb @@ -17,7 +17,6 @@ class Debate < ActiveRecord::Base belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' belongs_to :geozone has_many :comments, as: :commentable - has_many :votes, -> { for_debates }, foreign_key: 'votable_id' validates :title, presence: true validates :description, presence: true diff --git a/app/models/proposal.rb b/app/models/proposal.rb index 09741f5f9..c69fe4194 100644 --- a/app/models/proposal.rb +++ b/app/models/proposal.rb @@ -19,7 +19,6 @@ class Proposal < ActiveRecord::Base belongs_to :geozone has_many :comments, as: :commentable has_many :proposal_notifications - has_many :votes, -> { for_proposals }, foreign_key: 'votable_id' validates :title, presence: true validates :question, presence: true diff --git a/config/api.yml b/config/api.yml index 669602611..d007202cb 100644 --- a/config/api.yml +++ b/config/api.yml @@ -22,7 +22,7 @@ Debate: geozone: Geozone comments: [Comment] public_author: User - votes: [Vote] + votes_for: [Vote] tags: ["ActsAsTaggableOn::Tag"] Proposal: fields: @@ -45,7 +45,7 @@ Proposal: comments: [Comment] proposal_notifications: [ProposalNotification] public_author: User - votes: [Vote] + votes_for: [Vote] tags: ["ActsAsTaggableOn::Tag"] Comment: fields: @@ -60,7 +60,7 @@ Comment: ancestry: string confidence_score: integer public_author: User - votes: [Vote] + votes_for: [Vote] Geozone: fields: id: integer From fd240b2fc7531a059e3a315400ffd2df76a5db85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 27 Jan 2017 14:46:26 +0100 Subject: [PATCH 135/155] Rescue all exceptions in production --- app/controllers/graphql_controller.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index ea1decdb5..ba8edc2b4 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -14,6 +14,8 @@ class GraphqlController < ApplicationController render json: { message: 'Query string not present' }, 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 From 70a1dbde9401e4862efaf0948e802e79c3adc85c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Tue, 31 Jan 2017 13:52:25 +0100 Subject: [PATCH 136/155] Fix bug when parsing query variables sent by the GraphiQL desktop client --- app/controllers/graphql_controller.rb | 8 +++++- spec/controllers/graphql_controller_spec.rb | 29 +++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index ba8edc2b4..ce8f093db 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -12,6 +12,8 @@ class GraphqlController < ApplicationController 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 @@ -40,6 +42,10 @@ class GraphqlController < ApplicationController end def query_variables - params[:variables].blank? ? {} : JSON.parse(params[:variables]) + if params[:variables].blank? || params[:variables] == 'null' + {} + else + JSON.parse(params[:variables]) + end end end diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb index e95bdd3c9..f96392b73 100644 --- a/spec/controllers/graphql_controller_spec.rb +++ b/spec/controllers/graphql_controller_spec.rb @@ -8,18 +8,21 @@ describe GraphqlController, type: :request do 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(:bad_request) expect(JSON.parse(response.body)['message']).to eq('Query string is not valid JSON') 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 @@ -30,6 +33,7 @@ describe GraphqlController, type: :request do 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 @@ -37,20 +41,45 @@ describe GraphqlController, type: :request do 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(:bad_request) expect(JSON.parse(response.body)['message']).to eq('Query string is not valid JSON') 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 From cad66ea85ac668d95c9a8f16b6e426b870b1d0e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garce=CC=81s?= Date: Thu, 11 May 2017 22:29:21 +0200 Subject: [PATCH 137/155] Update GraphQL and GraphiQL gem --- Gemfile | 4 ++-- Gemfile.lock | 8 ++++---- spec/controllers/graphql_controller_spec.rb | 14 ++++++++++---- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/Gemfile b/Gemfile index d7f212e86..3cd21e0eb 100644 --- a/Gemfile +++ b/Gemfile @@ -67,7 +67,7 @@ gem 'redcarpet', '~> 3.4.0' gem 'paperclip' -gem 'graphql', '~> 1.3.0' +gem 'graphql', '~> 1.5.12' group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console @@ -104,7 +104,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' + gem 'graphiql-rails', '~> 1.4.1' end eval_gemfile './Gemfile_custom' diff --git a/Gemfile.lock b/Gemfile.lock index 585295501..b0d2ddc22 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -174,9 +174,9 @@ GEM geocoder (1.4.3) globalid (0.3.7) activesupport (>= 4.1.0) - graphiql-rails (1.3.0) + graphiql-rails (1.4.1) rails - graphql (1.3.0) + graphql (1.5.12) groupdate (3.2.0) gyoku (1.3.1) builder (>= 2.1.2) @@ -499,8 +499,8 @@ DEPENDENCIES foundation-rails (~> 6.2.4.0) foundation_rails_helper (~> 2.0.0) fuubar - graphiql-rails - graphql (~> 1.3.0) + graphiql-rails (~> 1.4.1) + graphql (~> 1.5.12) groupdate (~> 3.2.0) i18n-tasks (~> 0.9.15) initialjs-rails (= 0.2.0.4) diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb index f96392b73..1b8c64793 100644 --- a/spec/controllers/graphql_controller_spec.rb +++ b/spec/controllers/graphql_controller_spec.rb @@ -2,6 +2,12 @@ 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) } @@ -16,8 +22,8 @@ describe GraphqlController, type: :request do specify "with malformed query string" do get '/graphql', query: 'Malformed query string' - expect(response).to have_http_status(:bad_request) - expect(JSON.parse(response.body)['message']).to eq('Query string is not valid JSON') + expect(response).to have_http_status(:ok) + expect(parser_error_raised?(response)).to be_truthy end specify "without query string" do @@ -49,8 +55,8 @@ describe GraphqlController, type: :request do specify "with malformed query string" do post '/graphql', { query: "Malformed query string" }.to_json, json_headers - expect(response).to have_http_status(:bad_request) - expect(JSON.parse(response.body)['message']).to eq('Query string is not valid JSON') + expect(response).to have_http_status(:ok) + expect(parser_error_raised?(response)).to be_truthy end it "without query string" do From 3c7f60d62547d36ccdb50574bda4da48336ae032 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garce=CC=81s?= Date: Thu, 11 May 2017 22:42:43 +0200 Subject: [PATCH 138/155] Fix initializer --- config/initializers/graphql.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index 05cd2333c..2731e1887 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -1,2 +1,4 @@ -api_config = YAML.load_file('./config/api.yml') -API_TYPE_DEFINITIONS = GraphQL::ApiTypesCreator::parse_api_config_file(api_config) +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 From ad8aba07399feae2afc1a969109c5a28782edf02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garce=CC=81s?= Date: Mon, 15 May 2017 20:13:20 +0200 Subject: [PATCH 139/155] Revised public fields, wrote more exhaustive specs --- app/models/concerns/graphqlable.rb | 4 + app/models/user.rb | 12 + app/models/vote.rb | 16 +- config/api.yml | 44 +- lib/graph_ql/api_types_creator.rb | 6 +- spec/lib/graphql_spec.rb | 596 +++++++++++++++++++++- spec/models/comment_spec.rb | 6 + spec/models/proposal_notification_spec.rb | 8 +- spec/models/vote_spec.rb | 25 +- 9 files changed, 665 insertions(+), 52 deletions(-) diff --git a/app/models/concerns/graphqlable.rb b/app/models/concerns/graphqlable.rb index b62d57808..651a285b9 100644 --- a/app/models/concerns/graphqlable.rb +++ b/app/models/concerns/graphqlable.rb @@ -29,4 +29,8 @@ module Graphqlable end + def public_created_at + self.created_at.change(min: 0) + end + end diff --git a/app/models/user.rb b/app/models/user.rb index 1c6d3d2eb..f5a6586fd 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -286,6 +286,18 @@ class User < ActiveRecord::Base end 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 def clean_document_number diff --git a/app/models/vote.rb b/app/models/vote.rb index eae53e9c3..49cc4cd86 100644 --- a/app/models/vote.rb +++ b/app/models/vote.rb @@ -1,15 +1,19 @@ class Vote < ActsAsVotable::Vote include Graphqlable - + def self.public_for_api 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 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") - end - - def public_timestamp - self.created_at.change(min: 0) + 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) AND \ + ( \ + (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 diff --git a/config/api.yml b/config/api.yml index d007202cb..0c668eac2 100644 --- a/config/api.yml +++ b/config/api.yml @@ -2,24 +2,22 @@ User: fields: id: integer username: string - debates: [Debate] - proposals: [Proposal] - comments: [Comment] - organization: Organization + public_debates: [Debate] + public_proposals: [Proposal] + public_comments: [Comment] +# organization: Organization Debate: fields: id: integer title: string description: string - created_at: 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 - geozone_id: integer - geozone: Geozone comments: [Comment] public_author: User votes_for: [Vote] @@ -34,7 +32,7 @@ Proposal: comments_count: integer hot_score: integer confidence_score: integer - created_at: string + public_created_at: string summary: string video_url: string geozone_id: integer @@ -53,7 +51,7 @@ Comment: commentable_id: integer commentable_type: string body: string - created_at: string + public_created_at: string cached_votes_total: integer cached_votes_up: integer cached_votes_down: integer @@ -67,11 +65,11 @@ Geozone: name: string ProposalNotification: fields: - title: string - body: string - proposal_id: integer - created_at: string - proposal: Proposal + title: string + body: string + proposal_id: integer + public_created_at: string + proposal: Proposal ActsAsTaggableOn::Tag: fields: id: integer @@ -80,12 +78,12 @@ ActsAsTaggableOn::Tag: kind: string Vote: fields: - votable_id: integer - votable_type: string - public_timestamp: string - vote_flag: boolean -Organization: - fields: - id: integer - user_id: integer - name: string + votable_id: integer + votable_type: string + public_created_at: string + vote_flag: boolean +# Organization: +# fields: +# id: integer +# user_id: integer +# name: string diff --git a/lib/graph_ql/api_types_creator.rb b/lib/graph_ql/api_types_creator.rb index c82a5e25e..acf3e2d20 100644 --- a/lib/graph_ql/api_types_creator.rb +++ b/lib/graph_ql/api_types_creator.rb @@ -44,11 +44,7 @@ module GraphQL field(field_name, -> { created_types[field_type] }) do resolve -> (object, arguments, context) do association_target = object.send(field_name) - if association_target.nil? - nil - else - field_type.public_for_api.find_by(id: association_target.id) - end + association_target.present? ? field_type.public_for_api.find_by(id: association_target.id) : nil end end when :multiple_association diff --git a/spec/lib/graphql_spec.rb b/spec/lib/graphql_spec.rb index 06f89a356..8bf12245f 100644 --- a/spec/lib/graphql_spec.rb +++ b/spec/lib/graphql_spec.rb @@ -21,32 +21,46 @@ def hidden_field?(response, field_name) 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 + 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 + 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 - it "returns has_one associations" do + 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 + 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 + 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) @@ -58,7 +72,7 @@ describe 'ConsulSchema' do expect(comment_bodies).to match_array([comment_1.body, comment_2.body]) end - it "executes deeply nested queries" do + xit 'executes deeply nested queries' do org_user = create(:user) organization = create(:organization, user: 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) 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 } }") expect(hidden_field?(response, 'failed_census_calls_count')).to be_truthy 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 } }") expect(hidden_field?(response, 'encrypted_password')).to be_truthy end - it "hides confidential has_one associations" do + 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 + 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 + 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 + 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.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 diff --git a/spec/models/comment_spec.rb b/spec/models/comment_spec.rb index a86566734..a060ef59c 100644 --- a/spec/models/comment_spec.rb +++ b/spec/models/comment_spec.rb @@ -181,5 +181,11 @@ describe Comment do 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 diff --git a/spec/models/proposal_notification_spec.rb b/spec/models/proposal_notification_spec.rb index cd8bf346a..88a98ebee 100644 --- a/spec/models/proposal_notification_spec.rb +++ b/spec/models/proposal_notification_spec.rb @@ -30,12 +30,18 @@ describe ProposalNotification do expect(ProposalNotification.public_for_api).to include(notification) end - it "blocks notifications whose proposal is hidden" do + 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 diff --git a/spec/models/vote_spec.rb b/spec/models/vote_spec.rb index 8e5db0ce7..25fd069f2 100644 --- a/spec/models/vote_spec.rb +++ b/spec/models/vote_spec.rb @@ -84,18 +84,33 @@ describe 'Vote' do 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 - end - describe '#public_timestamp' do - it "truncates created_at timestamp up to minutes" do - 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')) + 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 From d4752491f82af39ea9a62392c6bc8c32a092093b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garce=CC=81s?= Date: Mon, 15 May 2017 20:39:59 +0200 Subject: [PATCH 140/155] Fix timezones --- spec/lib/graphql_spec.rb | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/spec/lib/graphql_spec.rb b/spec/lib/graphql_spec.rb index 8bf12245f..725cc109a 100644 --- a/spec/lib/graphql_spec.rb +++ b/spec/lib/graphql_spec.rb @@ -196,13 +196,13 @@ describe 'ConsulSchema' do 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) + 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(received_timestamps.first).to include('09:00:00') + 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 @@ -258,13 +258,13 @@ describe 'ConsulSchema' do 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) + 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(received_timestamps.first).to include('09:00:00') + 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 @@ -378,13 +378,13 @@ describe 'ConsulSchema' do 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) + 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(received_timestamps.first).to include('09:00:00') + expect(Time.zone.parse(received_timestamps.first)).to eq Time.zone.parse("2017-12-31 9:00:00") end end @@ -426,13 +426,13 @@ describe 'ConsulSchema' do 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) + 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(received_timestamps.first).to include('09:00:00') + 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 @@ -660,13 +660,13 @@ describe 'ConsulSchema' do 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) + 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(received_timestamps.first).to include('09:00:00') + expect(Time.zone.parse(received_timestamps.first)).to eq Time.zone.parse("2017-12-31 9:00:00") end end From 18db68a4305cd07b68476a7ca4f0346a70673c0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garce=CC=81s?= Date: Mon, 29 May 2017 09:47:39 +0200 Subject: [PATCH 141/155] Force pagination, limit query depth and complexity --- app/controllers/graphql_controller.rb | 3 ++- lib/graph_ql/api_types_creator.rb | 2 +- lib/graph_ql/query_type_creator.rb | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index ce8f093db..762239dc8 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -29,7 +29,8 @@ class GraphqlController < ApplicationController GraphQL::Schema.define do query query_type - max_depth 12 + max_depth 8 + max_complexity 2500 end end diff --git a/lib/graph_ql/api_types_creator.rb b/lib/graph_ql/api_types_creator.rb index acf3e2d20..4e548748c 100644 --- a/lib/graph_ql/api_types_creator.rb +++ b/lib/graph_ql/api_types_creator.rb @@ -49,7 +49,7 @@ module GraphQL end when :multiple_association field_type = field_type.first - connection(field_name, -> { created_types[field_type].connection_type }) do + connection(field_name, -> { created_types[field_type].connection_type }, max_page_size: 50, complexity: 1000) do resolve -> (object, arguments, context) { field_type.public_for_api & object.send(field_name) } end end diff --git a/lib/graph_ql/query_type_creator.rb b/lib/graph_ql/query_type_creator.rb index 36e6626b9..71beb8033 100644 --- a/lib/graph_ql/query_type_creator.rb +++ b/lib/graph_ql/query_type_creator.rb @@ -18,7 +18,7 @@ module GraphQL end end - connection model.graphql_pluralized_field_name, created_type.connection_type do + 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 From 9ec8b166d7758947f4be0cb9854162640c25ea1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garce=CC=81s?= Date: Thu, 1 Jun 2017 20:04:51 +0200 Subject: [PATCH 142/155] Use scopes for better query performance --- app/models/comment.rb | 3 +-- app/models/debate.rb | 1 + app/models/geozone.rb | 2 ++ app/models/proposal.rb | 1 + app/models/proposal_notification.rb | 4 +--- app/models/user.rb | 7 ++++--- app/models/vote.rb | 3 ++- config/initializers/active_record_extensions.rb | 1 - config/initializers/acts_as_taggable_on.rb | 2 +- lib/active_record_extensions.rb | 12 ------------ lib/graph_ql/api_types_creator.rb | 2 +- 11 files changed, 14 insertions(+), 24 deletions(-) delete mode 100644 config/initializers/active_record_extensions.rb delete mode 100644 lib/active_record_extensions.rb diff --git a/app/models/comment.rb b/app/models/comment.rb index ec90553aa..1bb72af7e 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -26,8 +26,7 @@ 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) } - - def self.public_for_api + scope :public_for_api, -> do joins("FULL OUTER JOIN debates ON commentable_type = 'Debate' AND commentable_id = debates.id"). joins("FULL OUTER JOIN proposals ON commentable_type = 'Proposal' AND commentable_id = proposals.id"). where("commentable_type = 'Proposal' AND proposals.hidden_at IS NULL OR commentable_type = 'Debate' AND debates.hidden_at IS NULL") diff --git a/app/models/debate.rb b/app/models/debate.rb index 8254bdc10..95940b3c3 100644 --- a/app/models/debate.rb +++ b/app/models/debate.rb @@ -39,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 diff --git a/app/models/geozone.rb b/app/models/geozone.rb index 9ef4065ef..ad0fe9cd5 100644 --- a/app/models/geozone.rb +++ b/app/models/geozone.rb @@ -8,6 +8,8 @@ class Geozone < ActiveRecord::Base has_many :users validates :name, presence: true + scope :public_for_api, -> { all } + def self.names Geozone.pluck(:name) end diff --git a/app/models/proposal.rb b/app/models/proposal.rb index 9ed84f9d9..f37dff3d5 100644 --- a/app/models/proposal.rb +++ b/app/models/proposal.rb @@ -53,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 diff --git a/app/models/proposal_notification.rb b/app/models/proposal_notification.rb index 7faa0fec1..25ae40883 100644 --- a/app/models/proposal_notification.rb +++ b/app/models/proposal_notification.rb @@ -10,9 +10,7 @@ class ProposalNotification < ActiveRecord::Base validates :proposal, presence: true validate :minimum_interval - def self.public_for_api - joins(:proposal).where("proposals.hidden_at IS NULL") - end + scope :public_for_api, -> { joins(:proposal).where("proposals.hidden_at IS NULL") } def minimum_interval return true if proposal.try(:notifications).blank? diff --git a/app/models/user.rb b/app/models/user.rb index f5a6586fd..fc34490a7 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -59,6 +59,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 @@ -287,15 +288,15 @@ class User < ActiveRecord::Base delegate :can?, :cannot?, to: :ability def public_proposals - public_activity? ? proposals : [] + public_activity? ? proposals : User.none end def public_debates - public_activity? ? debates : [] + public_activity? ? debates : User.none end def public_comments - public_activity? ? comments : [] + public_activity? ? comments : User.none end private diff --git a/app/models/vote.rb b/app/models/vote.rb index 49cc4cd86..b390a733a 100644 --- a/app/models/vote.rb +++ b/app/models/vote.rb @@ -2,7 +2,7 @@ class Vote < ActsAsVotable::Vote include Graphqlable - def self.public_for_api + scope :public_for_api, -> do 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 comments ON votable_type = 'Comment' AND votable_id = comments.id"). @@ -16,4 +16,5 @@ class Vote < ActsAsVotable::Vote ) \ )") end + end diff --git a/config/initializers/active_record_extensions.rb b/config/initializers/active_record_extensions.rb deleted file mode 100644 index 57659f192..000000000 --- a/config/initializers/active_record_extensions.rb +++ /dev/null @@ -1 +0,0 @@ -require 'active_record_extensions' diff --git a/config/initializers/acts_as_taggable_on.rb b/config/initializers/acts_as_taggable_on.rb index 68dad364e..98cef9a0f 100644 --- a/config/initializers/acts_as_taggable_on.rb +++ b/config/initializers/acts_as_taggable_on.rb @@ -44,7 +44,7 @@ module ActsAsTaggableOn ActsAsTaggableOn::Tag.where('taggings.taggable_type' => 'SpendingProposal').includes(:taggings).order(:name).uniq end - def self.public_for_api + scope :public_for_api, -> do find_by_sql(%| SELECT * FROM tags diff --git a/lib/active_record_extensions.rb b/lib/active_record_extensions.rb deleted file mode 100644 index 1dd9add87..000000000 --- a/lib/active_record_extensions.rb +++ /dev/null @@ -1,12 +0,0 @@ -module PublicForApi - - extend ActiveSupport::Concern - - class_methods do - def public_for_api - all - end - end -end - -ActiveRecord::Base.send(:include, PublicForApi) diff --git a/lib/graph_ql/api_types_creator.rb b/lib/graph_ql/api_types_creator.rb index 4e548748c..f8f78355e 100644 --- a/lib/graph_ql/api_types_creator.rb +++ b/lib/graph_ql/api_types_creator.rb @@ -50,7 +50,7 @@ module GraphQL 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) { field_type.public_for_api & object.send(field_name) } + resolve -> (object, arguments, context) { object.send(field_name).public_for_api } end end end From 0ad1ff5845e1f9913968c57ad215b3f29509f37e Mon Sep 17 00:00:00 2001 From: kikito Date: Tue, 13 Jun 2017 12:11:20 +0200 Subject: [PATCH 143/155] changes the way public_for_api is calculated in comment --- app/models/comment.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/models/comment.rb b/app/models/comment.rb index c285ab0d8..ffa02d09c 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -27,9 +27,10 @@ class Comment < ActiveRecord::Base 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 - joins("FULL OUTER JOIN debates ON commentable_type = 'Debate' AND commentable_id = debates.id"). - joins("FULL OUTER JOIN proposals ON commentable_type = 'Proposal' AND commentable_id = proposals.id"). - where("commentable_type = 'Proposal' AND proposals.hidden_at IS NULL OR commentable_type = 'Debate' AND debates.hidden_at IS NULL") + 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) } From 92633195d978db174537c93c9de785dcfceea412 Mon Sep 17 00:00:00 2001 From: kikito Date: Tue, 13 Jun 2017 12:19:02 +0200 Subject: [PATCH 144/155] refactors proposal_notification.public_for_api --- app/models/proposal_notification.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/proposal_notification.rb b/app/models/proposal_notification.rb index 25ae40883..9f36679a4 100644 --- a/app/models/proposal_notification.rb +++ b/app/models/proposal_notification.rb @@ -1,7 +1,7 @@ class ProposalNotification < ActiveRecord::Base include Graphqlable - + belongs_to :author, class_name: 'User', foreign_key: 'author_id' belongs_to :proposal @@ -10,7 +10,7 @@ class ProposalNotification < ActiveRecord::Base validates :proposal, presence: true validate :minimum_interval - scope :public_for_api, -> { joins(:proposal).where("proposals.hidden_at IS NULL") } + scope :public_for_api, -> { where(proposal_id: Proposal.public_for_api.pluck(:id)) } def minimum_interval return true if proposal.try(:notifications).blank? From a580e607869fc914cd794fa8f9ee894fbc5b7895 Mon Sep 17 00:00:00 2001 From: kikito Date: Tue, 13 Jun 2017 16:14:24 +0200 Subject: [PATCH 145/155] refactor public_for_api acts_as_taggable scopes --- config/initializers/acts_as_taggable_on.rb | 32 ++++++++++------------ 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/config/initializers/acts_as_taggable_on.rb b/config/initializers/acts_as_taggable_on.rb index 98cef9a0f..e570e08de 100644 --- a/config/initializers/acts_as_taggable_on.rb +++ b/config/initializers/acts_as_taggable_on.rb @@ -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 @@ -22,6 +31,12 @@ module ActsAsTaggableOn 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 @@ -44,23 +59,6 @@ module ActsAsTaggableOn ActsAsTaggableOn::Tag.where('taggings.taggable_type' => 'SpendingProposal').includes(:taggings).order(:name).uniq end - scope :public_for_api, -> do - find_by_sql(%| - SELECT * - FROM tags - WHERE (tags.kind IS NULL OR tags.kind = 'category') AND tags.id IN ( - SELECT tag_id - FROM ( - SELECT COUNT(taggings.id) AS taggings_count, tag_id - FROM ((taggings FULL OUTER JOIN proposals ON taggable_type = 'Proposal' AND taggable_id = proposals.id) FULL OUTER JOIN debates ON taggable_type = 'Debate' AND taggable_id = debates.id) - WHERE (taggable_type = 'Proposal' AND proposals.hidden_at IS NULL) OR (taggable_type = 'Debate' AND debates.hidden_at IS NULL) - GROUP BY tag_id - ) AS tag_taggings_count_relation - WHERE taggings_count > 0 - ) - |) - end - def self.graphql_field_name :tag end From 430e020a6af4918606f5d197123530e6810dd874 Mon Sep 17 00:00:00 2001 From: kikito Date: Tue, 13 Jun 2017 16:27:09 +0200 Subject: [PATCH 146/155] refactors public_for_api in vote --- app/models/vote.rb | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/app/models/vote.rb b/app/models/vote.rb index b390a733a..14b11a68d 100644 --- a/app/models/vote.rb +++ b/app/models/vote.rb @@ -3,18 +3,12 @@ class Vote < ActsAsVotable::Vote include Graphqlable scope :public_for_api, -> do - 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 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) AND \ - ( \ - (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))) \ - ) \ - )") + 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 From aa29905e3191ad229adb2d623882b8a31c703b49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Baza=CC=81n?= Date: Wed, 14 Jun 2017 10:44:33 +0200 Subject: [PATCH 147/155] updates graphql --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index f3355313c..b737e0080 100644 --- a/Gemfile +++ b/Gemfile @@ -71,7 +71,7 @@ gem 'rails-assets-markdown-it', source: 'https://rails-assets.org' gem 'cocoon' -gem 'graphql', '~> 1.5.12' +gem 'graphql', '~> 1.6.3' group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console diff --git a/Gemfile.lock b/Gemfile.lock index fdfe79413..9a37210bc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -178,7 +178,7 @@ GEM activesupport (>= 4.1.0) graphiql-rails (1.4.1) rails - graphql (1.5.12) + graphql (1.6.3) groupdate (3.2.0) gyoku (1.3.1) builder (>= 2.1.2) @@ -507,7 +507,7 @@ DEPENDENCIES foundation_rails_helper (~> 2.0.0) fuubar graphiql-rails (~> 1.4.1) - graphql (~> 1.5.12) + graphql (~> 1.6.3) groupdate (~> 3.2.0) i18n-tasks (~> 0.9.15) initialjs-rails (= 0.2.0.4) From 3a50cc3a99d6f152bd35532b2884669ab63eaeca Mon Sep 17 00:00:00 2001 From: rgarcia Date: Thu, 15 Jun 2017 12:12:41 +0200 Subject: [PATCH 148/155] Enables GraphQL UI in production --- Gemfile | 2 +- config/routes.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile b/Gemfile index b737e0080..9a659ab1c 100644 --- a/Gemfile +++ b/Gemfile @@ -72,6 +72,7 @@ gem 'rails-assets-markdown-it', source: 'https://rails-assets.org' gem 'cocoon' gem 'graphql', '~> 1.6.3' +gem 'graphiql-rails', '~> 1.4.1' group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console @@ -108,7 +109,6 @@ 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' diff --git a/config/routes.rb b/config/routes.rb index a8bb7afc4..5c5284a91 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -406,9 +406,9 @@ Rails.application.routes.draw do if Rails.env.development? mount LetterOpenerWeb::Engine, at: "/letter_opener" - mount GraphiQL::Rails::Engine, at: '/graphiql', graphql_path: '/graphql' end + mount GraphiQL::Rails::Engine, at: '/graphiql', graphql_path: '/graphql' mount Tolk::Engine => '/translate', :as => 'tolk' # more info pages From d1a050fa4d2bfc106f72d4fa3ea4941fb1f6b147 Mon Sep 17 00:00:00 2001 From: Bertocq Date: Fri, 16 Jun 2017 00:56:15 +0200 Subject: [PATCH 149/155] Fix shared example group rspec warning because of file name ended with _spec --- .../concerns/{has_public_author_spec.rb => has_public_author.rb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename spec/models/concerns/{has_public_author_spec.rb => has_public_author.rb} (100%) diff --git a/spec/models/concerns/has_public_author_spec.rb b/spec/models/concerns/has_public_author.rb similarity index 100% rename from spec/models/concerns/has_public_author_spec.rb rename to spec/models/concerns/has_public_author.rb From e1573e55b5f159c7b17dfc3f646da2604c0d651e Mon Sep 17 00:00:00 2001 From: Bertocq Date: Mon, 12 Jun 2017 17:45:43 +0200 Subject: [PATCH 150/155] Add scss-lint gem for sass linting --- Gemfile | 2 ++ Gemfile.lock | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/Gemfile b/Gemfile index 9a659ab1c..9208d27c2 100644 --- a/Gemfile +++ b/Gemfile @@ -108,7 +108,9 @@ end group :development do # Access an IRB console on exception pages or by using <%= console %> in views + gem 'scss_lint', require: false gem 'web-console', '3.3.0' + end eval_gemfile './Gemfile_custom' diff --git a/Gemfile.lock b/Gemfile.lock index 9a37210bc..77f7a2576 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -397,6 +397,9 @@ GEM nokogiri (>= 1.4.0) nori (~> 2.4) wasabi (~> 3.4) + scss_lint (0.53.0) + rake (>= 0.9, < 13) + sass (~> 3.4.20) simplecov (0.14.1) docile (~> 1.1.0) json (>= 1.8, < 3) @@ -541,6 +544,7 @@ DEPENDENCIES rvm1-capistrano3 sass-rails (~> 5.0, >= 5.0.4) savon + scss_lint sitemap_generator (~> 5.3.1) social-share-button (~> 0.10) spring From 450227d5e5f07792007f501d8f05df041a749f1f Mon Sep 17 00:00:00 2001 From: Bertocq Date: Mon, 12 Jun 2017 17:46:25 +0200 Subject: [PATCH 151/155] Add base .scss-lint.yml config file based on https://sass-guidelin.es/es/#scss-lint guide --- .scss-lint.yml | 212 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 .scss-lint.yml diff --git a/.scss-lint.yml b/.scss-lint.yml new file mode 100644 index 000000000..6c5785249 --- /dev/null +++ b/.scss-lint.yml @@ -0,0 +1,212 @@ +linters: + + BangFormat: + enabled: true + space_before_bang: true + space_after_bang: false + + BorderZero: + enabled: true + convention: zero + + ChainedClasses: + enabled: false + + ColorKeyword: + enabled: true + + ColorVariable: + enabled: false + + Comment: + enabled: false + + DebugStatement: + enabled: true + + DeclarationOrder: + enabled: true + + DisableLinterReason: + enabled: true + + DuplicateProperty: + enabled: false + + ElsePlacement: + enabled: true + style: same_line + + EmptyLineBetweenBlocks: + enabled: true + ignore_single_line_blocks: true + + EmptyRule: + enabled: true + + ExtendDirective: + enabled: false + + FinalNewline: + enabled: true + present: true + + HexLength: + enabled: true + style: short + + HexNotation: + enabled: true + style: lowercase + + HexValidation: + enabled: true + + IdSelector: + enabled: true + + ImportantRule: + enabled: false + + ImportPath: + enabled: true + leading_underscore: false + filename_extension: false + + Indentation: + enabled: true + allow_non_nested_indentation: true + character: space + width: 2 + + LeadingZero: + enabled: true + style: include_zero + + MergeableSelector: + enabled: false + force_nesting: false + + NameFormat: + enabled: true + convention: hyphenated_lowercase + allow_leading_underscore: true + + NestingDepth: + enabled: true + max_depth: 1 + + PlaceholderInExtend: + enabled: true + + PrivateNamingConvention: + enabled: true + prefix: _ + + PropertyCount: + enabled: false + + PropertySortOrder: + enabled: false + + PropertySpelling: + enabled: true + extra_properties: [] + + PropertyUnits: + enabled: false + + PseudoElement: + enabled: true + + QualifyingElement: + enabled: true + allow_element_with_attribute: false + allow_element_with_class: false + allow_element_with_id: false + + SelectorDepth: + enabled: true + max_depth: 3 + + SelectorFormat: + enabled: true + convention: hyphenated_lowercase + class_convention: '^(?:u|is|has)\-[a-z][a-zA-Z0-9]*$|^(?!u|is|has)[a-zA-Z][a-zA-Z0-9]*(?:\-[a-z][a-zA-Z0-9]*)?(?:\-\-[a-z][a-zA-Z0-9]*)?$' + + Shorthand: + enabled: true + + SingleLinePerProperty: + enabled: true + allow_single_line_rule_sets: false + + SingleLinePerSelector: + enabled: true + + SpaceAfterComma: + enabled: true + + SpaceAfterPropertyColon: + enabled: true + style: one_space + + SpaceAfterPropertyName: + enabled: true + + SpaceAfterVariableColon: + enabled: true + style: at_least_one_space + + SpaceAfterVariableName: + enabled: true + + SpaceAroundOperator: + enabled: true + style: one_space + + SpaceBeforeBrace: + enabled: true + style: space + allow_single_line_padding: true + + SpaceBetweenParens: + enabled: true + spaces: 0 + + StringQuotes: + enabled: true + style: single_quotes + + TrailingSemicolon: + enabled: true + + TrailingZero: + enabled: true + + TransitionAll: + enabled: false + + UnnecessaryMantissa: + enabled: true + + UnnecessaryParentReference: + enabled: true + + UrlFormat: + enabled: false + + UrlQuotes: + enabled: true + + VariableForProperty: + enabled: false + + VendorPrefixes: + enabled: true + identifier_list: base + include: [] + exclude: [] + + ZeroUnit: + enabled: true From 76b7f66fb35b3a1a46067622ed0f7b7e590e972a Mon Sep 17 00:00:00 2001 From: decabeza Date: Tue, 13 Jun 2017 19:07:44 +0200 Subject: [PATCH 152/155] fixes scss lint warnings --- .scss-lint.yml | 5 +- app/assets/stylesheets/_settings.scss | 60 +- app/assets/stylesheets/admin.scss | 542 ++++---- .../stylesheets/annotator_overrides.scss | 66 +- app/assets/stylesheets/application.scss | 34 +- .../stylesheets/datepicker_overrides.scss | 84 +- app/assets/stylesheets/fonts.scss | 10 +- app/assets/stylesheets/icons.scss | 284 ++-- app/assets/stylesheets/ie.scss | 423 ++++-- app/assets/stylesheets/layout.scss | 1147 +++++++++-------- app/assets/stylesheets/legislation.scss | 34 +- .../stylesheets/legislation_process.scss | 287 ++--- app/assets/stylesheets/mixins.scss | 6 +- app/assets/stylesheets/pages.scss | 20 +- app/assets/stylesheets/participation.scss | 636 ++++----- .../legislation/draft_versions/_form.html.erb | 2 +- .../content_blocks/index.html.erb | 2 +- .../site_customization/pages/index.html.erb | 2 +- app/views/layouts/_admin_header.html.erb | 2 +- .../draft_versions/_comments_panel.html.erb | 2 +- .../legislation/processes/_header.html.erb | 2 +- .../processes/_header_full.html.erb | 2 +- app/views/management/proposals/index.html.erb | 2 +- app/views/management/proposals/print.html.erb | 2 +- app/views/sandbox/admin_cms.html.erb | 2 +- .../admin_legislation_draft_edit.html.erb | 464 +++---- .../sandbox/admin_legislation_info.html.erb | 2 +- 27 files changed, 2246 insertions(+), 1878 deletions(-) diff --git a/.scss-lint.yml b/.scss-lint.yml index 6c5785249..dfa46907b 100644 --- a/.scss-lint.yml +++ b/.scss-lint.yml @@ -94,7 +94,7 @@ linters: NestingDepth: enabled: true - max_depth: 1 + max_depth: 4 PlaceholderInExtend: enabled: true @@ -127,12 +127,11 @@ linters: SelectorDepth: enabled: true - max_depth: 3 + max_depth: 5 SelectorFormat: enabled: true convention: hyphenated_lowercase - class_convention: '^(?:u|is|has)\-[a-z][a-zA-Z0-9]*$|^(?!u|is|has)[a-zA-Z][a-zA-Z0-9]*(?:\-[a-z][a-zA-Z0-9]*)?(?:\-\-[a-z][a-zA-Z0-9]*)?$' Shorthand: enabled: true diff --git a/app/assets/stylesheets/_settings.scss b/app/assets/stylesheets/_settings.scss index 613b1d123..00568ff51 100644 --- a/app/assets/stylesheets/_settings.scss +++ b/app/assets/stylesheets/_settings.scss @@ -53,53 +53,53 @@ $small-font-size: rem-calc(14); $line-height: rem-calc(24); $tiny-font-size: rem-calc(12); -$brand: #004A83; +$brand: #004a83; $dark: darken($brand, 10%); -$text: #222222; +$text: #222; $text-medium: #515151; -$text-light: #BFBFBF; +$text-light: #bfbfbf; -$border: #DEE0E3; +$border: #dee0e3; $link: $brand; $link-hover: darken($link, 20%); $debates: $brand; -$like: #7BD2A8; -$unlike: #EF8585; +$like: #7bd2a8; +$unlike: #ef8585; -$delete: #F04124; -$check: #46DB91; +$delete: #f04124; +$check: #46db91; -$proposals: #FFA42D; +$proposals: #ffa42d; $proposals-dark: #794500; -$budget: #7E328A; -$budget-hover: #7571BF; +$budget: #7e328a; +$budget-hover: #7571bf; -$highlight: #E7F2FC; -$light: #F5F7FA; -$featured: #FFDC5C; +$highlight: #e7f2fc; +$light: #f5f7fa; +$featured: #ffdc5c; -$footer-border: #BFC1C3; +$footer-border: #bfc1c3; -$success-bg: #DFF0D8; -$success-border: #D6E9C6; -$color-success: #3C763D; +$success-bg: #dff0d8; +$success-border: #d6e9c6; +$color-success: #3c763d; -$info-bg: #D9EDF7; -$info-border: #BCE8F1; -$color-info: #31708F; +$info-bg: #d9edf7; +$info-border: #bce8f1; +$color-info: #31708f; -$warning-bg: #FCF8E3; -$warning-border: #FAEBCC; -$color-warning: #8A6D3B; +$warning-bg: #fcf8e3; +$warning-border: #faebcc; +$color-warning: #8a6d3b; -$alert-bg: #F2DEDE; -$alert-border: #EBCCD1; -$color-alert: #A94442; +$alert-bg: #f2dede; +$alert-border: #ebccd1; +$color-alert: #a94442; // 1. Global @@ -118,8 +118,8 @@ $foundation-palette: ( $light-gray: #e6e6e6; $medium-gray: #cacaca; $dark-gray: #8a8a8a; -$black: #222222; -$white: #ffffff; +$black: #222; +$white: #fff; $body-background: $white; $body-font-color: $black; $body-font-family: 'Source Sans Pro', 'Helvetica', 'Arial', sans-serif !important; @@ -573,7 +573,7 @@ $tab-background: $white; $tab-background-active: $white; $tab-item-font-size: $base-font-size; $tab-item-background-hover: $white; -$tab-item-padding: $line-height/2 0; +$tab-item-padding: $line-height / 2 0; $tab-expand-max: 6; $tab-content-background: $white; $tab-content-border: $border; diff --git a/app/assets/stylesheets/admin.scss b/app/assets/stylesheets/admin.scss index 06d3e1983..9eff0e273 100644 --- a/app/assets/stylesheets/admin.scss +++ b/app/assets/stylesheets/admin.scss @@ -13,21 +13,21 @@ // 01. Global styles // ----------------- -$admin-color: #CF3638; +$admin-color: #cf3638; -body.admin { +.admin { - header { + .header { border: 0; + } - .top-links { - background: darken($admin-color, 15%); - } + .top-links { + background: darken($admin-color, 15%); + } - .back-web { - padding-top: $line-height/4; - text-decoration: underline; - } + .back-web { + padding-top: $line-height / 4; + text-decoration: underline; } .top-bar { @@ -42,62 +42,46 @@ body.admin { } } - form { + .fieldset { - .button { - margin-top: 0; - - &.margin-top { - margin-top: $line-height; - } + select { + height: $line-height * 2; } - input[type="text"], textarea { - width: 100%; + [type="text"] { + border-radius: 0; + margin-bottom: 0 !important; + } + } + + th { + text-align: left; + + &.text-center { + text-align: center; } - .fieldset { + &.text-right { + padding-right: $line-height; + text-align: right; + } - select { - height: $line-height*2; - } + &.with-button { + line-height: $line-height * 2; + } + } - .input-group input[type="text"] { - border-radius: 0; - margin-bottom: 0 !important; - } + tr { + background: #fff; + border: 1px solid $border; + + &:hover { + background: #f3f6f7; } } table { - - th { - text-align: left; - - &.text-center { - text-align: center; - } - - &.text-right { - padding-right: $line-height; - text-align: right; - } - - &.with-button { - line-height: $line-height*2; - } - } - - tr { - background: white; - border: 1px solid $border; - - &:hover { - background: #f3f6f7; - } - } - - td.break { + .break { word-break: break-word; } @@ -105,14 +89,15 @@ body.admin { table-layout: fixed; } - input[type="submit"] ~ a, a ~ a { + [type="submit"] ~ a, + a ~ a { margin-left: 0; margin-right: 0; - margin-top: $line-height/2; + margin-top: $line-height / 2; @include breakpoint(medium) { - margin-left: $line-height/2; - margin-right: $line-height/2; + margin-left: $line-height / 2; + margin-right: $line-height / 2; margin-top: 0; } } @@ -122,7 +107,7 @@ body.admin { max-width: none; } - .menu.simple li.active { + .menu.simple .active { border-bottom: 2px solid $admin-color; color: $admin-color; } @@ -132,10 +117,6 @@ body.admin { padding-right: 0; } - #proposals { - width: 100% !important; - } - .accordion-title { font-size: $base-font-size; } @@ -183,7 +164,7 @@ body.admin { &:hover .on-hover-block { display: block; margin: 0; - margin-top: $line-height/2; + margin-top: $line-height / 2; width: 100%; } } @@ -231,57 +212,53 @@ body.admin { display: inline-block; font-size: rem-calc(24); line-height: $line-height; - padding: $line-height/2 $line-height/4; + padding: $line-height / 2 $line-height / 4; padding-left: 0; vertical-align: middle; } + } - li { - background: white; - margin: 0; - outline: 0; + li { + background: #fff; + margin: 0; + outline: 0; - ul { - margin-left: $line-height/1.5; - border-left: 1px solid $border; - padding-left: $line-height/2; - } - - &.section-title { - border-bottom: 1px solid $border; - } - - &.active a { - background: #f3f6f7; - border-radius: rem-calc(6); - -moz-border-radius: rem-calc(6); - -webkit-border-radius: rem-calc(6); - color: $admin-color; - font-weight: bold; - } + ul { + margin-left: $line-height / 1.5; + border-left: 1px solid $border; + padding-left: $line-height / 2; } - li a { - color: $text; - display: block; - line-height: rem-calc(48); - padding-left: rem-calc(12); - vertical-align: top; + &.section-title { + border-bottom: 1px solid $border; + } - &:hover { - background: #f3f6f7; - border-radius: rem-calc(6); - -moz-border-radius: rem-calc(6); - -webkit-border-radius: rem-calc(6); - color: $admin-color; - text-decoration: none; - } + &.active a { + background: #f3f6f7; + border-radius: rem-calc(6); + color: $admin-color; + font-weight: bold; + } + } + + li a { + color: $text; + display: block; + line-height: rem-calc(48); + padding-left: rem-calc(12); + vertical-align: top; + + &:hover { + background: #f3f6f7; + border-radius: rem-calc(6); + color: $admin-color; + text-decoration: none; } } .is-accordion-submenu-parent { - & > a::after { + > a::after { border-color: $admin-color transparent transparent; } } @@ -291,11 +268,11 @@ body.admin { margin-left: $line-height; li:first-child { - padding-top: $line-height/2; + padding-top: $line-height / 2; } li:last-child { - padding-bottom: $line-height/2; + padding-bottom: $line-height / 2; } a { @@ -308,12 +285,14 @@ body.admin { // ----------------- .delete { - border-bottom: 1px dotted #CF2A0E; + border-bottom: 1px dotted #cf2a0e; color: $delete; font-size: $small-font-size; - &:hover, &:active, &:focus { - border-bottom: 1px dotted white; + &:hover, + &:active, + &:focus { + border-bottom: 1px dotted #fff; color: #cf2a0e; } } @@ -376,8 +355,6 @@ body.admin { &:hover { max-height: rem-calc(1000); transition: max-height 0.9s; - -moz-transition: max-height 0.9s; - -webkit-transition: max-height 0.9s; } } @@ -385,7 +362,7 @@ body.admin { // --------- .stats { - background: white; + background: #fff; } .stats-numbers { @@ -417,34 +394,36 @@ body.admin { ul { list-style-type: none; margin-left: 0; + } - li { - font-size: rem-calc(14); - margin-bottom: rem-calc(12); + li { + font-size: rem-calc(14); + margin-bottom: rem-calc(12); - span { - color: $text-medium; - font-size: rem-calc(12); - } + span { + color: $text-medium; + font-size: rem-calc(12); + } - .icon-check { - color: $check; - } + .icon-check { + color: $check; + } - .icon-x { - color: $delete; - } + .icon-x { + color: $delete; } } } -.account-info, .login-as, .spending-proposal-info { +.account-info, +.login-as, +.spending-proposal-info { background-color: #e7e7e7; border-radius: rem-calc(3); font-size: rem-calc(16); font-weight: normal; margin: $line-height; - padding: $line-height/2; + padding: $line-height / 2; strong { font-size: rem-calc(18); @@ -455,40 +434,42 @@ body.admin { margin-bottom: 0; } -body.admin { +.admin { .investment-projects-list.medium-9 { width: 100%; } +} - .investment-projects-summary { +.investment-projects-summary { - th, td { - text-align: center; + th, + td { + text-align: center; - &:first-child { - font-weight: bold; - text-align: left; - } - - &:last-child { - font-weight: bold; - } + &:first-child { + font-weight: bold; + text-align: left; } - tr { - &:nth-child(even) td:last-child { - background: $success-border; - } + &:last-child { + font-weight: bold; + } + } - &:nth-child(odd) td:last-child { - background: $success-bg; - } + tr { + &:nth-child(even) td:last-child { + background: $success-border; + } + + &:nth-child(odd) td:last-child { + background: $success-bg; } } } -.admin-content .select-geozone, .admin-content .select-heading { +.admin-content .select-geozone, +.admin-content .select-heading { a { display: block; @@ -501,14 +482,14 @@ body.admin { } } -table.investment-projects-summary { +.investment-projects-summary { - td.total-price { + .total-price { white-space: nowrap; } } -body.admin { +.admin { .geozone { background: #ececec; @@ -516,8 +497,8 @@ body.admin { color: $text; display: inline-block; font-size: $small-font-size; - margin-bottom: $line-height/3; - padding: $line-height/4 $line-height/3; + margin-bottom: $line-height / 3; + padding: $line-height / 4 $line-height / 3; text-decoration: none; &:hover { @@ -538,9 +519,9 @@ body.admin { table { .callout { - height: $line-height*2; - line-height: $line-height*2; - padding: 0 $line-height/2; + height: $line-height * 2; + line-height: $line-height * 2; + padding: 0 $line-height / 2; } } @@ -551,15 +532,15 @@ table { // --------------- .markdown-editor { - background-color: white; + background-color: #fff; .markdown-area, - #markdown-preview { + .markdown-preview { display: none; } } -.markdown-editor #markdown-preview { +.markdown-editor .markdown-preview { overflow-y: auto; height: 15em; } @@ -577,11 +558,11 @@ table { left: 0; } -.markdown-editor.fullscreen #markdown-preview { +.markdown-editor.fullscreen .markdown-preview { height: 99%; } -.edit_legislation_draft_version .row { +.edit-legislation-draft-version .row { margin-bottom: 2rem; } @@ -614,7 +595,7 @@ table { // 08. CMS // -------------- -.cms_page_list { +.cms-page-list { [class^="icon-"] { padding-right: $menu-icon-spacing; @@ -624,17 +605,18 @@ table { .legislation-process-edit { - .edit_legislation_process { + .edit-legislation-process { small { color: $text-medium; } - input[type]:not([type="submit"]):not([type="file"]):not([type="checkbox"]):not([type="radio"]) { + [type]:not([type="submit"]):not([type="file"]):not([type="checkbox"]):not([type="radio"]) { background: $white; } - .legislation-process-start, .legislation-process-end { + .legislation-process-start, + .legislation-process-end { @include breakpoint(medium) { line-height: 3rem; } @@ -664,7 +646,7 @@ table { .legislation-questions-form { - input[type]:not([type="submit"]):not([type="file"]):not([type="checkbox"]):not([type="radio"]) { + [type]:not([type="submit"]):not([type="file"]):not([type="checkbox"]):not([type="radio"]) { background: $white; margin-bottom: 0; @@ -673,53 +655,113 @@ table { } } - input::-webkit-input-placeholder { - font-style: italic; - } - - input:-moz-placeholder { /* Firefox 18- */ - font-style: italic; - } - - input::-moz-placeholder { /* Firefox 19+ */ - font-style: italic; - } - - input:-ms-input-placeholder { + input::placeholder { font-style: italic; } .legislation-questions-answers { margin-bottom: 1rem; } +} - .field { - margin-bottom: 1rem; +.field { + margin-bottom: 1rem; - @include breakpoint(medium) { - margin-bottom: 0; + @include breakpoint(medium) { + margin-bottom: 0; + } + + a { + line-height: 3rem; + color: $delete; + + span { + text-decoration: underline; } + .icon-x { + vertical-align: sub; + text-decoration: none; + line-height: 0; + } + + &:active, + &:focus, + &:hover { + text-decoration: none; + } + } +} + +.fullscreen-container { + + a { + line-height: 8rem; + + &:active, + &:focus, + &:hover { + text-decoration: none; + } + } +} + +.fullscreen { + + .fullscreen-container { + a { line-height: 3rem; - color: $delete; - span { - text-decoration: underline; + @include breakpoint(medium) { + float: right; } + } - .icon-x { - vertical-align: sub; - text-decoration: none; - line-height: 0; - } + .markdown-editor-header { + vertical-align: top; + display: inline-block; + color: $white; - &:active, - &:focus, - &:hover { - text-decoration: none; + @include breakpoint(medium) { + line-height: 3rem; } } + + .truncate { + @include breakpoint(medium) { + width: 40vw; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + + .markdown-editor-buttons { + display: block; + + @include breakpoint(medium) { + display: inline-block; + float: right; + padding-left: 1rem; + } + } + + input { + font-size: $small-font-size; + padding: 0.5em 1em; + margin-left: 0; + margin-bottom: 0; + margin-top: 1rem; + + @include breakpoint(medium) { + margin: 0.5rem; + } + } + + a { + color: $white; + } } } @@ -731,7 +773,7 @@ table { } } - input[type]:not([type="submit"]):not([type="file"]):not([type="checkbox"]):not([type="radio"]) { + [type]:not([type="submit"]):not([type="file"]):not([type="checkbox"]):not([type="radio"]) { background: $white; } @@ -761,30 +803,20 @@ table { display: none; } - a { - line-height: 8rem; + span { + text-decoration: none; + font-size: $small-font-size; + } - span { - text-decoration: none; - font-size: $small-font-size; - } - - .icon-expand { - margin-left: 0.25rem; - vertical-align: sub; - text-decoration: none; - line-height: 0; - } - - &:active, - &:focus, - &:hover { - text-decoration: none; - } + .icon-expand { + margin-left: 0.25rem; + vertical-align: sub; + text-decoration: none; + line-height: 0; } } - #legislation_draft_version_body { + .legislation-draft-version-body { font-family: $font-family-serif; background: #f5f5f5; height: 16em; @@ -795,12 +827,17 @@ table { } } - #markdown-preview { + .markdown-preview { font-family: $font-family-serif; border: 1px solid #cacaca; margin-bottom: 2rem; - h1, h2, h3, h4, h5, h6 { + h1, + h2, + h3, + h4, + h5, + h6 { font-family: $font-family-serif !important; font-size: 1rem; line-height: 1.625rem; @@ -816,7 +853,7 @@ table { .fullscreen { .markdown-area, - #markdown-preview { + .markdown-preview { display: block; } @@ -829,78 +866,21 @@ table { background: $admin-color; padding: 0.5rem 1rem; margin-bottom: 0; - - a { - line-height: 3rem; - - @include breakpoint(medium) { - float: right; - } - } - - .markdown-editor-header { - vertical-align: top; - display: inline-block; - color: $white; - - @include breakpoint(medium) { - line-height: 3rem; - } - } - - .truncate { - @include breakpoint(medium) { - width: 40vw; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - } - - .markdown-editor-buttons { - display: block; - - @include breakpoint(medium) { - display: inline-block; - float: right; - padding-left: 1rem; - } - - input { - font-size: $small-font-size; - padding: 0.5em 1em; - margin-left: 0; - margin-bottom: 0; - margin-top: 1rem; - - @include breakpoint(medium) { - margin: 0.5rem; - } - } - } - - a { - color: $white; - } } - #legislation_draft_version_body { + .legislation-draft-version-body { border-radius: 0; padding: 1rem; - border: none; + border: 0; @include breakpoint(medium) { padding: 1rem 2rem; } - - &:focus { - border: none; - } } - #markdown-preview { + .markdown-preview { padding: 1rem; - border: none; + border: 0; @include breakpoint(medium) { padding: 1rem 2rem; @@ -908,3 +888,9 @@ table { } } } + +.legislation-draft-version-body { + &:focus { + border: 0; + } +} diff --git a/app/assets/stylesheets/annotator_overrides.scss b/app/assets/stylesheets/annotator_overrides.scss index 5132d68eb..9bf27f22c 100644 --- a/app/assets/stylesheets/annotator_overrides.scss +++ b/app/assets/stylesheets/annotator_overrides.scss @@ -2,59 +2,59 @@ // .annotator-editor .annotator-controls, -.annotator-filter, .annotator-filter -.annotator-filter-navigation button { - background: #F3F3F3; +.annotator-filter, +.annotator-filter .annotator-filter-navigation button { + background: #f3f3f3; background-image: none; border: 0; border-radius: 0; border-top: 1px solid $border; box-shadow: none; - padding: $line-height/2 $line-height/4; + padding: $line-height / 2 $line-height / 4; } .annotator-adder { - background-image: image-url("annotator_adder.png"); + background-image: image-url('annotator_adder.png'); margin-top: -52px; } .annotator-widget { - background: white; + background: #fff; border: 1px solid $border; border-radius: 0; bottom: $line-height; - box-shadow: 0 0 5px rgba(0,0,0,0.05); + box-shadow: 0 0 5px rgba(0, 0, 0, 0.05); font-family: $body-font-family; font-size: $base-font-size; line-height: $line-height; - min-width: $line-height*13; + min-width: $line-height * 13; p { color: $text; - padding: $line-height/2; + padding: $line-height / 2; } } .annotator-item, .annotator-editor .annotator-item input:focus, .annotator-editor .annotator-item textarea:focus { - background: white; + background: #fff; } -.annotator-widget:after, -.annotator-editor.annotator-invert-y .annotator-widget:after { - background-image: image-url("annotator_items.png"); +.annotator-widget::after, +.annotator-editor.annotator-invert-y .annotator-widget::after { + background-image: image-url('annotator_items.png'); } .annotator-editor a, .annotator-filter .annotator-filter-property label { - padding: 0 $line-height/4; + padding: 0 $line-height / 4; } .annotator-editor a { background: none; background-image: none; - border: none; + border: 0; box-shadow: none; color: $link; font-family: $body-font-family; @@ -63,7 +63,9 @@ text-shadow: none; padding: 0; - &:hover, &:active, &:focus { + &:hover, + &:active, + &:focus { background: none; background-image: none; color: $link-hover; @@ -71,28 +73,28 @@ text-shadow: none; } - &:after { + &::after { content: none; } &.annotator-cancel { - background: #F04124; - color: white; - padding: $line-height/4 $line-height/2; + background: #f04124; + color: #fff; + padding: $line-height / 4 $line-height / 2; &:hover { - background: darken(#F04124, 20); + background: darken(#f04124, 20); text-decoration: none; } } &.annotator-save { - background: #43AC6A; - color: white; - padding: $line-height/4 $line-height/2; + background: #43ac6a; + color: #fff; + padding: $line-height / 4 $line-height / 2; &:hover { - background: darken(#43AC6A, 20); + background: darken(#43ac6a, 20); text-decoration: none; } } @@ -104,19 +106,23 @@ } .annotator-hl.weight-1 { - background: #FFF9DA; + background: #fff9da; } + .annotator-hl.weight-2 { - background: #FFF5BC; + background: #fff5bc; } + .annotator-hl.weight-3 { - background: #FFF1A2; + background: #fff1a2; } + .annotator-hl.weight-4 { - background: #FFEF8C; + background: #ffef8c; } + .annotator-hl.weight-5 { - background: #FFE95F; + background: #ffe95f; } .current-annotation { diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index c67d85471..a14cf3383 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -1,17 +1,17 @@ -@import "social-share-button"; -@import "foundation_and_overrides"; -@import "fonts"; -@import "icons"; -@import "mixins"; -@import "admin"; -@import "layout"; -@import "participation"; -@import "pages"; -@import "legislation"; -@import "legislation_process"; -@import "custom"; -@import "c3"; -@import "annotator.min"; -@import "annotator_overrides"; -@import "jquery-ui/datepicker"; -@import "datepicker_overrides"; +@import 'social-share-button'; +@import 'foundation_and_overrides'; +@import 'fonts'; +@import 'icons'; +@import 'mixins'; +@import 'admin'; +@import 'layout'; +@import 'participation'; +@import 'pages'; +@import 'legislation'; +@import 'legislation_process'; +@import 'custom'; +@import 'c3'; +@import 'annotator.min'; +@import 'annotator_overrides'; +@import 'jquery-ui/datepicker'; +@import 'datepicker_overrides'; diff --git a/app/assets/stylesheets/datepicker_overrides.scss b/app/assets/stylesheets/datepicker_overrides.scss index dbbab4bd5..d4c07802e 100644 --- a/app/assets/stylesheets/datepicker_overrides.scss +++ b/app/assets/stylesheets/datepicker_overrides.scss @@ -3,7 +3,7 @@ .ui-datepicker-header { background: $brand; - color: white; + color: #fff; font-weight: bold; } @@ -14,9 +14,17 @@ color: $text; } - .ui-state-hover, .ui-state-active { + .ui-state-hover, + .ui-state-active { background: $brand; - color: white; + color: #fff; + } + + thead { + + tr th { + color: $dark; + } } } @@ -32,8 +40,9 @@ right: 12px; } - .ui-datepicker-prev, .ui-datepicker-next { - color: white; + .ui-datepicker-prev, + .ui-datepicker-next { + color: #fff; cursor: pointer; font-weight: normal; font-size: $small-font-size; @@ -44,51 +53,42 @@ table { border: 1px solid $border; border-top: 0; + } - thead { - background: $dark; - border-left: 1px solid $dark; - border-right: 1px solid $dark; + tr { + border-bottom: 1px solid $border; - tr th { - color: $dark; - } + &:last-child { + border-bottom: 0; } - tr { - border-bottom: 1px solid $border; + &:nth-child(odd) { + background: none; + } + } - &:last-child { - border-bottom: 0px; - } + td { + padding: 0; + border-right: 1px solid $border; - &:nth-child(odd) { - background: none; - } + &:last-child { + border-right: 0; } - td { - padding: 0; - border-right: 1px solid $border; - - &:last-child { - border-right: 0px; - } - - span, a { - text-align: center; - line-height: $line-height; - color: $text; - } - - &.ui-datepicker-unselectable.ui-state-disabled { - background: white; - - .ui-state-default { - background: #F8F8F8; - color: $text-medium; - } - } + span, + a { + text-align: center; + line-height: $line-height; + color: $text; } } } + +.ui-datepicker-unselectable.ui-state-disabled { + background: #fff; + + .ui-state-default { + background: #f8f8f8; + color: $text-medium; + } +} diff --git a/app/assets/stylesheets/fonts.scss b/app/assets/stylesheets/fonts.scss index e8fe6a4bf..61429e800 100644 --- a/app/assets/stylesheets/fonts.scss +++ b/app/assets/stylesheets/fonts.scss @@ -16,7 +16,7 @@ font-url('sourcesanspro-light-webfont.woff2') format('woff2'), font-url('sourcesanspro-light-webfont.woff') format('woff'), font-url('sourcesanspro-light-webfont.ttf') format('truetype'), - font-url('sourcesanspro-light-webfont.svg#source_sans_prolight') format('svg') + font-url('sourcesanspro-light-webfont.svg#source_sans_prolight') format('svg'); } @font-face { @@ -67,7 +67,7 @@ font-url('lato-light.ttf') format('truetype'), font-url('lato-light.svg#latolight') format('svg'); font-weight: lighter; - font-style: normal + font-style: normal; } @font-face { @@ -79,7 +79,7 @@ font-url('lato-regular.ttf') format('truetype'), font-url('lato-regular.svg#latoregular') format('svg'); font-weight: normal; - font-style: normal + font-style: normal; } @font-face { @@ -91,5 +91,5 @@ font-url('lato-bold.ttf') format('truetype'), font-url('lato-bold.svg#latobold') format('svg'); font-weight: bold; - font-style: normal -} \ No newline at end of file + font-style: normal; +} diff --git a/app/assets/stylesheets/icons.scss b/app/assets/stylesheets/icons.scss index 5805fb090..782b5cdb2 100644 --- a/app/assets/stylesheets/icons.scss +++ b/app/assets/stylesheets/icons.scss @@ -1,4 +1,5 @@ @charset "UTF-8"; + @font-face { font-family: 'icons'; src: font-url('icons.eot'); @@ -9,7 +10,8 @@ font-weight: normal; font-style: normal; } -[data-icon]:before { + +[data-icon]::before { font-family: "icons" !important; content: attr(data-icon); font-style: normal !important; @@ -21,8 +23,9 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } -[class^="icon-"]:before, -[class*=" icon-"]:before { + +[class^="icon-"]::before, +[class*=" icon-"]::before { font-family: "icons" !important; font-style: normal !important; font-weight: normal !important; @@ -33,168 +36,223 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } -.icon-angle-down:before { - content: "\61"; + +.icon-angle-down::before { + content: '\61'; } -.icon-angle-left:before { - content: "\62"; + +.icon-angle-left::before { + content: '\62'; } -.icon-angle-right:before { - content: "\63"; + +.icon-angle-right::before { + content: '\63'; } -.icon-angle-up:before { - content: "\64"; + +.icon-angle-up::before { + content: '\64'; } -.icon-comments:before { - content: "\65"; + +.icon-comments::before { + content: '\65'; } -.icon-twitter:before { - content: "\66"; + +.icon-twitter::before { + content: '\66'; } -.icon-calendar:before { - content: "\67"; + +.icon-calendar::before { + content: '\67'; } -.icon-debates:before { - content: "\69"; + +.icon-debates::before { + content: '\69'; } -.icon-unlike:before { - content: "\6a"; + +.icon-unlike::before { + content: '\6a'; } -.icon-like:before { - content: "\6b"; + +.icon-like::before { + content: '\6b'; } -.icon-check:before { - content: "\6c"; + +.icon-check::before { + content: '\6c'; } -.icon-edit:before { - content: "\6d"; + +.icon-edit::before { + content: '\6d'; } -.icon-user:before { - content: "\6f"; + +.icon-user::before { + content: '\6f'; } -.icon-settings:before { - content: "\71"; + +.icon-settings::before { + content: '\71'; } -.icon-stats:before { - content: "\72"; + +.icon-stats::before { + content: '\72'; } -.icon-proposals:before { - content: "\68"; + +.icon-proposals::before { + content: '\68'; } -.icon-organizations:before { - content: "\73"; + +.icon-organizations::before { + content: '\73'; } -.icon-deleted:before { - content: "\74"; + +.icon-deleted::before { + content: '\74'; } -.icon-tag:before { - content: "\75"; + +.icon-tag::before { + content: '\75'; } -.icon-eye:before { - content: "\70"; + +.icon-eye::before { + content: '\70'; } -.icon-x:before { - content: "\76"; + +.icon-x::before { + content: '\76'; } -.icon-flag:before { - content: "\77"; + +.icon-flag::before { + content: '\77'; } -.icon-comment:before { - content: "\79"; + +.icon-comment::before { + content: '\79'; } -.icon-reply:before { - content: "\7a"; + +.icon-reply::before { + content: '\7a'; } -.icon-facebook:before { - content: "\41"; + +.icon-facebook::before { + content: '\41'; } -.icon-google-plus:before { - content: "\42"; + +.icon-google-plus::before { + content: '\42'; } -.icon-search:before { - content: "\45"; + +.icon-search::before { + content: '\45'; } -.icon-external:before { - content: "\46"; + +.icon-external::before { + content: '\46'; } -.icon-video:before { - content: "\44"; + +.icon-video::before { + content: '\44'; } -.icon-document:before { - content: "\47"; + +.icon-document::before { + content: '\47'; } -.icon-print:before { - content: "\48"; + +.icon-print::before { + content: '\48'; } -.icon-blog:before { - content: "\4a"; + +.icon-blog::before { + content: '\4a'; } -.icon-box:before { - content: "\49"; + +.icon-box::before { + content: '\49'; } -.icon-youtube:before { - content: "\4b"; + +.icon-youtube::before { + content: '\4b'; } -.icon-letter:before { - content: "\4c"; + +.icon-letter::before { + content: '\4c'; } -.icon-circle:before { - content: "\43"; + +.icon-circle::before { + content: '\43'; } -.icon-circle-o:before { - content: "\4d"; + +.icon-circle-o::before { + content: '\4d'; } -.icon-help:before { - content: "\4e"; + +.icon-help::before { + content: '\4e'; } -.icon-budget:before { - content: "\53"; + +.icon-budget::before { + content: '\53'; } -.icon-notification:before { - content: "\6e"; + +.icon-notification::before { + content: '\6e'; } -.icon-no-notification:before { - content: "\78"; + +.icon-no-notification::before { + content: '\78'; } -.icon-whatsapp:before { - content: "\50"; + +.icon-whatsapp::before { + content: '\50'; } -.icon-zip:before { - content: "\4f"; + +.icon-zip::before { + content: '\4f'; } -.icon-banner:before { - content: "\51"; + +.icon-banner::before { + content: '\51'; } -.icon-arrow-down:before { - content: "\52"; + +.icon-arrow-down::before { + content: '\52'; } -.icon-arrow-left:before { - content: "\54"; + +.icon-arrow-left::before { + content: '\54'; } -.icon-arrow-right:before { - content: "\55"; + +.icon-arrow-right::before { + content: '\55'; } -.icon-check-circle:before { - content: "\56"; + +.icon-check-circle::before { + content: '\56'; } -.icon-arrow-top:before { - content: "\57"; + +.icon-arrow-top::before { + content: '\57'; } -.icon-checkmark-circle:before { - content: "\59"; + +.icon-checkmark-circle::before { + content: '\59'; } -.icon-minus-square:before { - content: "\58"; + +.icon-minus-square::before { + content: '\58'; } -.icon-plus-square:before { - content: "\5a"; + +.icon-plus-square::before { + content: '\5a'; } -.icon-expand:before { - content: "\30"; + +.icon-expand::before { + content: '\30'; } -.icon-telegram:before { - content: "\31"; + +.icon-telegram::before { + content: '\31'; } -.icon-instagram:before { - content: "\32"; + +.icon-instagram::before { + content: '\32'; } diff --git a/app/assets/stylesheets/ie.scss b/app/assets/stylesheets/ie.scss index 2c494747b..46f5916b2 100644 --- a/app/assets/stylesheets/ie.scss +++ b/app/assets/stylesheets/ie.scss @@ -8,7 +8,9 @@ // 01. Global styles // ----------------- -*, *:before, *:after { +*, +*::before, +*::after { box-sizing: border-box !important; } @@ -23,96 +25,344 @@ display: block !important; } -.column, .columns { +.column, +.columns { display: inline-block !important; float: none !important; box-sizing: border-box !important; } -.small-1, .row .small-1 { width: 7.33333%; } -.small-2, .row .small-2 { width: 15.66667%; } -.small-3, .row .small-3 { width: 24%; } -.small-4, .row .small-4 { width: 32.33333%; } -.small-5, .row .small-5 { width: 40.66667%; } -.small-6, .row .small-6 { width: 49%; } -.small-7, .row .small-7 { width: 57.33333%; } -.small-8, .row .small-8 { width: 65.66667%; } -.small-9, .row .small-9 { width: 74%; } -.small-10, .row .small-10 { width: 82.33333%; } -.small-11, .row .small-11 { width: 90.66667%; } -.small-12, .row .small-12 { width: 99%; } +.small-1, +.row .small-1 { + width: 7.33333%; +} -.medium-1, .row .medium-1 { width: 7.33333%; } -.medium-2, .row .medium-2 { width: 15.66667%; } -.medium-3, .row .medium-3 { width: 24%; } -.medium-4, .row .medium-4 { width: 32.33333%; } -.medium-5, .row .medium-5 { width: 40.66667%; } -.medium-6, .row .medium-6 { width: 49%; } -.medium-7, .row .medium-7 { width: 57.33333%; } -.medium-8, .row .medium-8 { width: 65.66667%; } -.medium-9, .row .medium-9 { width: 74%; } -.medium-10, .row .medium-10 { width: 82.33333%; } -.medium-11, .row .medium-11 { width: 90.66667%; } -.medium-12, .row .medium-12 { width: 99%; } +.small-2, +.row .small-2 { + width: 15.66667%; +} -.large-1, .row .large-1 { width: 7.33333%; } -.large-2, .row .large-2 { width: 15.66667%; } -.large-3, .row .large-3 { width: 24%; } -.large-4, .row .large-4 { width: 32.33333%; } -.large-5, .row .large-5 { width: 40.66667%; } -.large-6, .row .large-6 { width: 49%; } -.large-7, .row .large-7 { width: 57.33333%; } -.large-8, .row .large-8 { width: 65.66667%; } -.large-9, .row .large-9 { width: 74%; } -.large-10, .row .large-10 { width: 82.33333%; } -.large-11, .row .large-11 { width: 90.66667%; } -.large-12, .row .large-12 { width: 99%; } +.small-3, +.row .small-3 { + width: 24%; +} -.row .small-offset-1 { margin-left: 7.33333%; } -.row .small-offset-2 { margin-left: 15.66667%; } -.row .small-offset-3 { margin-left: 24%; } -.row .small-offset-4 { margin-left: 32.33333%; } -.row .small-offset-5 { margin-left: 40.66667%; } -.row .small-offset-6 { margin-left: 49%; } -.row .small-offset-7 { margin-left: 57.33333%; } -.row .small-offset-8 { margin-left: 65.66667%; } -.row .small-offset-9 { margin-left: 74%; } -.row .small-offset-10 { margin-left: 82.33333%; } -.row .small-offset-11 { margin-left: 90.66667%; } -.row .small-offset-12 { margin-left: 99%; } +.small-4, +.row .small-4 { + width: 32.33333%; +} -.row .medium-offset-1 { margin-left: 7.33333%; } -.row .medium-offset-2 { margin-left: 15.66667%; } -.row .medium-offset-3 { margin-left: 24%; } -.row .medium-offset-4 { margin-left: 32.33333%; } -.row .medium-offset-5 { margin-left: 40.66667%; } -.row .medium-offset-6 { margin-left: 49%; } -.row .medium-offset-7 { margin-left: 57.33333%; } -.row .medium-offset-8 { margin-left: 65.66667%; } -.row .medium-offset-9 { margin-left: 74%; } -.row .medium-offset-10 { margin-left: 82.33333%; } -.row .medium-offset-11 { margin-left: 90.66667%; } -.row .medium-offset-12 { margin-left: 99%; } +.small-5, +.row .small-5 { + width: 40.66667%; +} -.row .large-offset-1 { margin-left: 7.33333%; } -.row .large-offset-2 { margin-left: 15.66667%; } -.row .large-offset-3 { margin-left: 24%; } -.row .large-offset-4 { margin-left: 32.33333%; } -.row .large-offset-5 { margin-left: 40.66667%; } -.row .large-offset-6 { margin-left: 49%; } -.row .large-offset-7 { margin-left: 57.33333%; } -.row .large-offset-8 { margin-left: 65.66667%; } -.row .large-offset-9 { margin-left: 74%; } -.row .large-offset-10 { margin-left: 82.33333%; } -.row .large-offset-11 { margin-left: 90.66667%; } -.row .large-offset-12 { margin-left: 99%; } +.small-6, +.row .small-6 { + width: 49%; +} + +.small-7, +.row .small-7 { + width: 57.33333%; +} + +.small-8, +.row .small-8 { + width: 65.66667%; +} + +.small-9, +.row .small-9 { + width: 74%; +} + +.small-10, +.row .small-10 { + width: 82.33333%; +} + +.small-11, +.row .small-11 { + width: 90.66667%; +} + +.small-12, +.row .small-12 { + width: 99%; +} + +.medium-1, +.row .medium-1 { + width: 7.33333%; +} + +.medium-2, +.row .medium-2 { + width: 15.66667%; +} + +.medium-3, +.row .medium-3 { + width: 24%; +} + +.medium-4, +.row .medium-4 { + width: 32.33333%; +} + +.medium-5, +.row .medium-5 { + width: 40.66667%; +} + +.medium-6, +.row .medium-6 { + width: 49%; +} + +.medium-7, +.row .medium-7 { + width: 57.33333%; +} + +.medium-8, +.row .medium-8 { + width: 65.66667%; +} + +.medium-9, +.row .medium-9 { + width: 74%; +} + +.medium-10, +.row .medium-10 { + width: 82.33333%; +} + +.medium-11, +.row .medium-11 { + width: 90.66667%; +} + +.medium-12, +.row .medium-12 { + width: 99%; +} + +.large-1, +.row .large-1 { + width: 7.33333%; +} + +.large-2, +.row .large-2 { + width: 15.66667%; +} + +.large-3, +.row .large-3 { + width: 24%; +} + +.large-4, +.row .large-4 { + width: 32.33333%; +} + +.large-5, +.row .large-5 { + width: 40.66667%; +} + +.large-6, +.row .large-6 { + width: 49%; +} + +.large-7, +.row .large-7 { + width: 57.33333%; +} + +.large-8, +.row .large-8 { + width: 65.66667%; +} + +.large-9, +.row .large-9 { + width: 74%; +} + +.large-10, +.row .large-10 { + width: 82.33333%; +} + +.large-11, +.row .large-11 { + width: 90.66667%; +} + +.large-12, +.row .large-12 { + width: 99%; +} + +.row .small-offset-1 { + margin-left: 7.33333%; +} + +.row .small-offset-2 { + margin-left: 15.66667%; +} + +.row .small-offset-3 { + margin-left: 24%; +} + +.row .small-offset-4 { + margin-left: 32.33333%; +} + +.row .small-offset-5 { + margin-left: 40.66667%; +} + +.row .small-offset-6 { + margin-left: 49%; +} + +.row .small-offset-7 { + margin-left: 57.33333%; +} + +.row .small-offset-8 { + margin-left: 65.66667%; +} + +.row .small-offset-9 { + margin-left: 74%; +} + +.row .small-offset-10 { + margin-left: 82.33333%; +} + +.row .small-offset-11 { + margin-left: 90.66667%; +} + +.row .small-offset-12 { + margin-left: 99%; +} + +.row .medium-offset-1 { + margin-left: 7.33333%; +} + +.row .medium-offset-2 { + margin-left: 15.66667%; +} + +.row .medium-offset-3 { + margin-left: 24%; +} + +.row .medium-offset-4 { + margin-left: 32.33333%; +} + +.row .medium-offset-5 { + margin-left: 40.66667%; +} + +.row .medium-offset-6 { + margin-left: 49%; +} + +.row .medium-offset-7 { + margin-left: 57.33333%; +} + +.row .medium-offset-8 { + margin-left: 65.66667%; +} + +.row .medium-offset-9 { + margin-left: 74%; +} + +.row .medium-offset-10 { + margin-left: 82.33333%; +} + +.row .medium-offset-11 { + margin-left: 90.66667%; +} + +.row .medium-offset-12 { + margin-left: 99%; +} + +.row .large-offset-1 { + margin-left: 7.33333%; +} + +.row .large-offset-2 { + margin-left: 15.66667%; +} + +.row .large-offset-3 { + margin-left: 24%; +} + +.row .large-offset-4 { + margin-left: 32.33333%; +} + +.row .large-offset-5 { + margin-left: 40.66667%; +} + +.row .large-offset-6 { + margin-left: 49%; +} + +.row .large-offset-7 { + margin-left: 57.33333%; +} + +.row .large-offset-8 { + margin-left: 65.66667%; +} + +.row .large-offset-9 { + margin-left: 74%; +} + +.row .large-offset-10 { + margin-left: 82.33333%; +} + +.row .large-offset-11 { + margin-left: 90.66667%; +} + +.row .large-offset-12 { + margin-left: 99%; +} .top-bar { clear: both !important; height: 100px !important; } -.locale, .external-links { +.locale, +.external-links { background: #002d50 !important; } @@ -121,11 +371,12 @@ } .external-links { - color: white !important; + color: #fff !important; float: right !important; } -.top-bar-title, .top-bar-title a, .top-bar-title a { +.top-bar-title, +.top-bar-title a { display: inline-block !important; float: none !important; } @@ -140,28 +391,29 @@ margin: 0 !important; position: inherit !important; - &:after { + &::after { content: none !important; } } form { - input, textarea { + input, + textarea { height: 48px !important; line-height: 48px !important; margin-bottom: 24px !important; width: 100% !important; } - input[type="checkbox"], - input[type="radio"] { + [type="checkbox"], + [type="radio"] { height: auto !important; line-height: inherit !important; width: auto !important; } - input[type="radio"] { + [type="radio"] { width: 18px !important; } } @@ -184,16 +436,17 @@ form { // 02. Admin // --------- -body.admin form { +.admin form { - input[type="text"], textarea { + [type="text"], + textarea { height: 48px !important; line-height: 48px !important; margin-bottom: 24px !important; } } -.admin-sidebar ul [class^="icon-"] { +.admin-sidebar [class^="icon-"] { padding-left: 12px !important; padding-right: 12px !important; } diff --git a/app/assets/stylesheets/layout.scss b/app/assets/stylesheets/layout.scss index bf62319c4..1e420231b 100644 --- a/app/assets/stylesheets/layout.scss +++ b/app/assets/stylesheets/layout.scss @@ -24,7 +24,7 @@ // ----------------- ::selection { - color: white; + color: #fff; background-color: $brand; } @@ -32,7 +32,12 @@ body { font-size: $base-font-size; } -h1, h2, h3, h4, h5, h6 { +h1, +h2, +h3, +h4, +h5, +h6 { clear: both; font-weight: 700; } @@ -46,11 +51,17 @@ p { a { color: $link; - &:hover, &:active { + &:hover, + &:active { color: $link-hover; text-decoration: underline; - h1, h2, h3, h4, h5, h6 { + h1, + h2, + h3, + h4, + h5, + h6 { color: $link-hover; } } @@ -69,8 +80,9 @@ a { text-decoration: none !important; } - &.warning, &.warning:hover { - color: black; + &.warning, + &.warning:hover { + color: #000; } } @@ -113,8 +125,9 @@ a { } } - li:hover, .f-dropdown li:focus { - background: white; + li:hover, + .f-dropdown li:focus { + background: #fff; } &.open { @@ -162,14 +175,15 @@ a { .wrapper { min-height: 100%; - margin: 0 auto (-$line-height)*12; + margin: 0 auto (-$line-height) * 12; height: auto !important; height: 100%; } -.footer, .push { +.footer, +.push { clear: both; - min-height: $line-height*12; + min-height: $line-height * 12; } .ie-callout { @@ -177,7 +191,7 @@ a { top: 0; width: 100%; - a.close { + .close { font-size: rem-calc(34); top: 20%; } @@ -202,7 +216,7 @@ a { } .menu.vertical { - background: white; + background: #fff; margin: $line-height 0; padding: $line-height; @@ -247,10 +261,11 @@ a { .close-button { color: $text; - top: $line-height/2; + top: $line-height / 2; } -.back, .icon-angle-left { +.back, +.icon-angle-left { clear: both; color: $text-medium; float: left; @@ -281,7 +296,8 @@ a { } } - h2, h3 { + h2, + h3 { font-size: $base-font-size; } } @@ -291,7 +307,7 @@ a { } .button.float-right ~ .button.float-right { - margin: 0 $line-height/2; + margin: 0 $line-height / 2; } .pagination .current { @@ -307,13 +323,13 @@ header { margin-bottom: $line-height; .selected { - border-bottom: 1px solid white; + border-bottom: 1px solid #fff; } .locale { float: left; - height: $line-height*1.5; - margin-left: $line-height/2; + height: $line-height * 1.5; + margin-left: $line-height / 2; } .external-links { @@ -329,9 +345,9 @@ header { .top-bar { background: $brand !important; - color: white; - height: $line-height*2; - line-height: $line-height*2; + color: #fff; + height: $line-height * 2; + line-height: $line-height * 2; padding-bottom: 0; padding-top: 0; @@ -351,33 +367,33 @@ header { ul { background: none; padding-right: rem-calc(15); + } - &.menu > li { - display: block; + .menu > li { + display: block; + + @include breakpoint(medium) { + display: table-cell; + height: $line-height * 3.5; + } + + a { + color: #fff; + padding-left: 0; @include breakpoint(medium) { - display: table-cell; - height: $line-height*3.5; + font-size: $small-font-size; + padding: rem-calc(11) rem-calc(16); } + } - a { - color: white; - padding-left: 0; + .button { + text-align: left; - @include breakpoint(medium) { - font-size: $small-font-size; - padding: rem-calc(11) rem-calc(16); - } - - &.button { - text-align: left; - - @include breakpoint(medium) { - background: white; - color: $brand; - text-align: center; - } - } + @include breakpoint(medium) { + background: #fff; + color: $brand; + text-align: center; } } } @@ -385,21 +401,22 @@ header { .menu-icon.dark { height: 20px; - &::after, &:hover::after { - background: white; - box-shadow: 0 7px 0 white, 0 14px 0 white; + &::after, + &:hover::after { + background: #fff; + box-shadow: 0 7px 0 #fff, 0 14px 0 #fff; } } } .top-links { background: $dark; - color: white; + color: #fff; font-size: $small-font-size; - padding-right: $line-height/2; + padding-right: $line-height / 2; a { - color: white; + color: #fff; margin: 0 rem-calc(6); &:hover { @@ -409,22 +426,20 @@ header { ul { margin-bottom: 0; + } - li { - display: block; - } + li { + display: block; @include breakpoint(medium) { - li { - display: inline-block; + display: inline-block; - &:after { - content: "|"; - } + &::after { + content: '|'; + } - &:last-child:after { - content: none; - } + &:last-child::after { + content: none; } } } @@ -433,7 +448,7 @@ header { .subnavigation { @include breakpoint(medium) { - background: white; + background: #fff; padding-bottom: 0; } @@ -446,40 +461,40 @@ header { display: inline-block; margin-right: rem-calc(20); } + } + } - a { - color: white; - display: inline-block; - line-height: $line-height*2; - position: relative; - text-align: left; - width: 100%; + a { + color: #fff; + display: inline-block; + line-height: $line-height * 2; + position: relative; + text-align: left; + width: 100%; - @include breakpoint(medium) { - color: $text; - display: block; - font-weight: bold; - width: auto; + @include breakpoint(medium) { + color: $text; + display: block; + font-weight: bold; + width: auto; - &:hover { - color: $link; - } - } + &:hover { + color: $link; + } + } - &.active { - color: white; + &.active { + color: #fff; - @include breakpoint(medium) { - border-bottom: 2px solid $brand; - color: $brand; - } - } + @include breakpoint(medium) { + border-bottom: 2px solid $brand; + color: $brand; } } } .input-group { - padding-top: $line-height/4; + padding-top: $line-height / 4; @include breakpoint(medium) { margin-bottom: 0; @@ -491,7 +506,7 @@ header { } .input-group-button { - line-height: $line-height*1.5; + line-height: $line-height * 1.5; padding-bottom: 0; button { @@ -499,14 +514,14 @@ header { border: 1px solid #ccc; border-left: 0; color: $text; - height: $line-height*1.5; - line-height: $line-height*1.5; + height: $line-height * 1.5; + line-height: $line-height * 1.5; padding-top: 0; } } input { - height: $line-height*1.5 !important; + height: $line-height * 1.5 !important; margin-bottom: 0; margin-right: 0; width: 100%; @@ -516,31 +531,31 @@ header { .submenu { border-bottom: 1px solid $border; clear: both; - margin-bottom: $line-height/2; + margin-bottom: $line-height / 2; a { color: $text; display: inline-block; font-weight: bold; - margin-right: $line-height/2; + margin-right: $line-height / 2; position: relative; text-align: left; @include breakpoint(medium) { - margin-right: $line-height*1.5; + margin-right: $line-height * 1.5; } &:hover { color: $link; } + } - &.active { - border-bottom: 2px solid $brand; - color: $brand; + .active { + border-bottom: 2px solid $brand; + color: $brand; - &:hover { - text-decoration: none; - } + &:hover { + text-decoration: none; } } @@ -549,7 +564,7 @@ header { } } -.search-form-header input[type=text] { +.search-form-header [type=text] { max-width: none; } @@ -576,7 +591,9 @@ footer { } } - a, a:active, a:focus { + a, + a:active, + a:focus { color: $text; text-decoration: underline; @@ -589,7 +606,7 @@ footer { padding-left: 0; } - a.title { + .title { font-weight: bold; text-decoration: none; } @@ -598,28 +615,32 @@ footer { .footer { background: $border; border-top: 6px solid $brand; - margin-top: $line-height*2; + margin-top: $line-height * 2; padding-top: $line-height; } .subfooter { border-top: 1px solid $text-light; font-size: $small-font-size; - padding-top: $line-height/2; + padding-top: $line-height / 2; } // 04. Tags // -------- -.tags a , .tag-cloud a, .categories a, .geozone a, .sidebar-links a, +.tags a , +.tag-cloud a, +.categories a, +.geozone a, +.sidebar-links a, .tags span { background: #ececec; border-radius: rem-calc(6); color: $text; display: inline-block; font-size: $small-font-size; - margin-bottom: $line-height/3; - padding: $line-height/4 $line-height/3; + margin-bottom: $line-height / 3; + padding: $line-height / 4 $line-height / 3; text-decoration: none; &:hover { @@ -627,13 +648,14 @@ footer { } } -.categories a, .geozone a { +.categories a, +.geozone a { background: $highlight; color: $link; &:hover { background: $brand; - color: white; + color: #fff; } } @@ -642,8 +664,8 @@ footer { display: inline-block; font-size: rem-calc(16); font-weight: bold; - margin: -1px 0 $line-height/2; - padding-top: $line-height/4; + margin: -1px 0 $line-height / 2; + padding-top: $line-height / 4; text-transform: uppercase; } @@ -653,17 +675,17 @@ footer { .auth-page { .wrapper { - margin: 0 auto (-$line-height)*14; + margin: 0 auto (-$line-height) * 14; } } .auth-image { - background: $brand image-url("auth_bg.jpg"); + background: $brand image-url('auth_bg.jpg'); background-repeat: no-repeat; background-size: cover; @include breakpoint(medium) { - min-height: $line-height*42; + min-height: $line-height * 42; } h1 { @@ -675,7 +697,7 @@ footer { } a { - color: white; + color: #fff; display: block; line-height: rem-calc(80); // Same as logo image height text-align: center; @@ -691,10 +713,12 @@ footer { .auth-form { @include breakpoint(medium) { - padding-top: $line-height*4; + padding-top: $line-height * 4; } - p, a, .checkbox { + p, + a, + .checkbox { font-size: $small-font-size; } } @@ -706,52 +730,52 @@ footer { text-align: center; span { - background: white; + background: #fff; font-weight: bold; - padding: 0 $line-height/2; + padding: 0 $line-height / 2; } } // 06. Forms // --------- -form.locale-form { +.locale-form { display: inline-block; position: relative; label { - color: white; + color: #fff; font-size: $small-font-size; font-weight: normal; } select { - background-image: image-url("language_select.png"); + background-image: image-url('language_select.png'); background-origin: border-box; background-position: right; background-size: 24px 24px; option { - background: white; + background: #fff; color: $text; border: 0; outline: none; } + } - &.locale-switcher { - background-color: transparent; - border: 0; - color: white; - font-size: $small-font-size; - margin-bottom: 0; - outline: none; - padding-left: rem-calc(3); - padding-right: $line-height; - width: auto; + .locale-switcher { + background-color: transparent; + border: 0; + color: #fff; + font-size: $small-font-size; + margin-bottom: 0; + outline: none; + padding-left: rem-calc(3); + padding-right: $line-height; + width: auto; - &:focus { - outline: 3px solid #ffbf47; - } + &:focus { + outline: 3px solid #ffbf47; } } } @@ -768,7 +792,7 @@ form { font-weight: bold; } - input[type="radio"] { + [type="radio"] { height: $line-height !important; vertical-align: top; @@ -781,9 +805,9 @@ form { } } - input[type]:not([type=submit]):not([type=file]):not([type=checkbox]):not([type=radio]) { - background: #F8F8F8; - height: $line-height*2; + [type]:not([type=submit]):not([type=file]):not([type=checkbox]):not([type=radio]) { + background: #f8f8f8; + height: $line-height * 2; margin-bottom: rem-calc(16); &.error { @@ -791,36 +815,38 @@ form { } } - input[type="checkbox"] + label, - input[type="radio"] + label { + [type="checkbox"] + label, + [type="radio"] + label { margin-right: 0; } - input[type=file] { - margin: $line-height/2 0 $line-height/2 $line-height/4; + [type=file] { + margin: $line-height / 2 0 $line-height / 2 $line-height / 4; } - .note, .note-marked { + .note, + .note-marked { display: block; font-size: rem-calc(13); - margin-bottom: $line-height/2; + margin-bottom: $line-height / 2; } .note-marked { - background: yellow; + background: #ff0; display: inline-block; em { - background: white; + background: #fff; display: block; } } .ckeditor { - min-height: $line-height*13; + min-height: $line-height * 13; } - .checkbox, .radio { + .checkbox, + .radio { display: inline-block; font-weight: normal; line-height: $line-height; @@ -833,47 +859,29 @@ form { .callout-slide { animation-duration: 1s; - -webkit-animation-duration: 1s; animation-fill-mode: both; - -webkit-animation-fill-mode: both; animation-name: slide; - -webkit-animation-name: slide; -} - -@-webkit-keyframes slide { - from { - -webkit-transform: translate3d(100%, 0, 0); - transform: translate3d(100%, 0, 0); - visibility: visible; - } - - to { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } } @keyframes slide { from { - -webkit-transform: translate3d(100%, 0, 0); transform: translate3d(100%, 0, 0); visibility: visible; } to { - -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); } } .notice-container { - min-width: $line-height*12; + min-width: $line-height * 12; position: absolute; right: 24px; top: 24px; .notice { - height: $line-height*4; + height: $line-height * 4; .notice-text { width: 95%; @@ -889,7 +897,8 @@ form { text-decoration: underline; } - &.success, &.notice { + &.success, + &.notice { background-color: $success-bg; border-color: $success-border; color: $color-success; @@ -907,7 +916,8 @@ form { color: $color-warning; } - &.alert, &.error { + &.alert, + &.error { background-color: $alert-bg; border-color: $alert-border; color: $color-alert; @@ -918,25 +928,20 @@ form { } } -span.error, small.error { - background: $alert-bg; - color: $color-alert; -} - -span.no-error, small.no-error { +.no-error { background: $success-bg; color: $color-success; } -.error small.error { +.error { background: $alert-bg; color: $color-alert; display: inline-block; - margin: 0 $line-height/4; -} + margin: 0 $line-height / 4; -label.error, label.error a { - color: $color-alert; + a { + color: $color-alert; + } } // 08. User account @@ -945,12 +950,12 @@ label.error, label.error a { .account { select { - height: $line-height*2; - margin-right: $line-height/2; + height: $line-height * 2; + margin-right: $line-height / 2; } .verify-account { - padding-right: $line-height/2; + padding-right: $line-height / 2; } .final-votes-info { @@ -958,14 +963,14 @@ label.error, label.error a { border: 1px solid $warning-border; color: $color-warning; margin-top: $line-height; - padding: $line-height/2; + padding: $line-height / 2; ul li { - margin: $line-height/2 0; + margin: $line-height / 2 0; } .icon-box { - color: #D4A26D; + color: #d4a26d; font-size: rem-calc(120); line-height: rem-calc(120); vertical-align: top; @@ -973,13 +978,17 @@ label.error, label.error a { } } -img.avatar, img.admin-avatar, img.moderator-avatar, img.initialjs-avatar { +.avatar, +.admin-avatar, +.moderator-avatar, +.initialjs-avatar { border-radius: rem-calc(1000); position: relative; } -.author-deleted, .user-deleted { - color: rgba(0,0,0,.4); +.author-deleted, +.user-deleted { + color: rgba(0, 0, 0, 0.4); display: inline-block; font-size: rem-calc(32); line-height: rem-calc(32); @@ -1000,23 +1009,23 @@ img.avatar, img.admin-avatar, img.moderator-avatar, img.initialjs-avatar { list-style-type: none; margin-bottom: 0; margin-left: 0; + } - li { - font-size: $small-font-size; - margin-bottom: $line-height/2; + li { + font-size: $small-font-size; + margin-bottom: $line-height / 2; - span { - color: $text-medium; - font-size: rem-calc(12); - } + span { + color: $text-medium; + font-size: rem-calc(12); + } - .icon-check { - color: $check; - } + .icon-check { + color: $check; + } - .icon-x { - color: $delete; - } + .icon-x { + color: $delete; } } } @@ -1042,7 +1051,7 @@ img.avatar, img.admin-avatar, img.moderator-avatar, img.initialjs-avatar { } } -.notifications-list:before { +.notifications-list::before { background: $border; content: ''; height: 100%; @@ -1054,7 +1063,7 @@ img.avatar, img.admin-avatar, img.moderator-avatar, img.initialjs-avatar { .notification { display: block; - padding: $line-height/2 0 $line-height/2 $line-height*1.5; + padding: $line-height / 2 0 $line-height / 2 $line-height * 1.5; position: relative; &:hover { @@ -1067,15 +1076,15 @@ img.avatar, img.admin-avatar, img.moderator-avatar, img.initialjs-avatar { color: $link; } - &:before { - content: "\43"; + &::before { + content: '\43'; } } - &:before { - background: white; + &::before { + background: #fff; color: $brand; - content: "\4d"; + content: '\4d'; font-family: "icons" !important; left: 0; position: absolute; @@ -1103,7 +1112,7 @@ img.avatar, img.admin-avatar, img.moderator-avatar, img.initialjs-avatar { @include breakpoint(medium) { float: right; margin-bottom: 0; - margin-top: $line-height/4; + margin-top: $line-height / 4; position: absolute; right: 0; z-index: 2; @@ -1112,44 +1121,50 @@ img.avatar, img.admin-avatar, img.moderator-avatar, img.initialjs-avatar { .advanced-search-form { - select { - height: $line-height*2; - } - @include breakpoint(medium) { - & > .column { + > .column { padding: 0; } } + + select { + height: $line-height * 2; + } } // 10. Officials levels // -------------------- -.level-1, .level-2, .level-3, -.level-4, .level-5, -.is-author, .is-association { - color: black; +.level-1, +.level-2, +.level-3, +.level-4, +.level-5, +.is-author, +.is-association { + color: #000; } .is-author { - background: #00A5F1; + background: #00a5f1; } .is-association { - background: #E38D83; + background: #e38d83; } .level-1 { - background: #1ABC9C; + background: #1abc9c; } -.level-2, .level-3, .level-4 { - background: #43AC6A; +.level-2, +.level-3, +.level-4 { + background: #43ac6a; } .level-5 { - background: #F08A24; + background: #f08a24; } // 11. Tables @@ -1169,11 +1184,11 @@ table { td { line-height: $line-height; - padding: $line-height/2 $line-height/4; + padding: $line-height / 2 $line-height / 4; } &:nth-child(odd) { - background: white; + background: #fff; } &:nth-child(even) { @@ -1189,7 +1204,8 @@ table { .table-for-mobile { @include breakpoint(medium down) { - th, td { + th, + td { display: block; text-align: left; } @@ -1203,26 +1219,26 @@ table { .button.button-facebook, .button.button-google, .button.button-telegram { - background: white; + background: #fff; color: $text; font-weight: bold; - height: $line-height*2; - line-height: $line-height*2; + height: $line-height * 2; + line-height: $line-height * 2; padding: 0; position: relative; } .button.button-twitter { - background: #ECF7FC; - border-left: 3px solid #45B0E3; + background: #ecf7fc; + border-left: 3px solid #45b0e3; - &:before { - color: #45B0E3; - content: "f"; + &::before { + color: #45b0e3; + content: 'f'; font-family: "icons" !important; font-size: rem-calc(24); left: 0; - line-height: $line-height*2; + line-height: $line-height * 2; padding: 0 rem-calc(20); position: absolute; top: 0; @@ -1230,41 +1246,42 @@ table { } .ssb-twitter { - background: #45B0E3; + background: #45b0e3; background-image: none !important; - color: white; - height: $line-height*2 !important; + color: #fff; + height: $line-height * 2 !important; position: relative; - width: $line-height*2 !important; + width: $line-height * 2 !important; - &:before { - content: "f"; + &::before { + content: 'f'; font-family: "icons" !important; font-size: rem-calc(24); left: 50%; - line-height: $line-height*2; + line-height: $line-height * 2; margin-left: rem-calc(-11); position: absolute; top: 0; } - &:hover, &:focus { - background: white; - color: #40A2D1; + &:hover, + &:focus { + background: #fff; + color: #40a2d1; } } .button.button-facebook { - background: #EBEEF4; - border-left: 3px solid #3B5998; + background: #ebeef4; + border-left: 3px solid #3b5998; - &:before { - color: #3B5998; - content: "A"; + &::before { + color: #3b5998; + content: 'A'; font-family: "icons" !important; font-size: rem-calc(24); left: 0; - line-height: $line-height*2; + line-height: $line-height * 2; padding: 0 rem-calc(20); position: absolute; top: 0; @@ -1272,83 +1289,85 @@ table { } .ssb-facebook { - background: #3B5998; + background: #3b5998; background-image: none !important; - color: white; + color: #fff; height: rem-calc(48) !important; position: relative; width: rem-calc(48) !important; - &:before { - content: "A"; + &::before { + content: 'A'; font-family: "icons" !important; font-size: rem-calc(24); left: 50%; - line-height: $line-height*2; + line-height: $line-height * 2; margin-left: rem-calc(-11); position: absolute; top: 0; } - &:hover, &:focus { - background: white; - color: #354F88; + &:hover, + &:focus { + background: #fff; + color: #354f88; } } .button.button-google { - background: #FCEDEA; - border-left: 3px solid #DE4C34; + background: #fcedea; + border-left: 3px solid #de4c34; - &:before { - color: #DE4C34; - content: "B"; + &::before { + color: #de4c34; + content: 'B'; font-family: "icons" !important; font-size: rem-calc(24); left: 0; - line-height: $line-height*2; + line-height: $line-height * 2; padding: 0 rem-calc(20); position: absolute; top: 0; } } -.ssb-google_plus { - background: #DE4C34; +[class^="ssb-icon ssb-google"] { + background: #de4c34; background-image: none !important; - color: white; - height: $line-height*2 !important; + color: #fff; + height: $line-height * 2 !important; position: relative; - width: $line-height*2 !important; + width: $line-height * 2 !important; - &:before { - content: "B"; + &::before { + content: 'B'; font-family: "icons" !important; font-size: rem-calc(24); left: 50%; - line-height: $line-height*2; + line-height: $line-height * 2; margin-left: rem-calc(-11); position: absolute; top: 0; } - &:hover, &:focus { - background: white; - color: #CE3E26; + &:hover, + &:focus { + background: #fff; + color: #ce3e26; } } .button.button-telegram { - background: #ECF7FC; - border-left: 3px solid #0088cc; + background: #ecf7fc; + border-left: 3px solid #08c; - &:before { - color: #0088cc; - content: "1"; + &::before { + color: #08c; + content: '1'; font-family: "icons" !important; font-size: rem-calc(24); left: 0; - line-height: $line-height*2; + line-height: $line-height * 2; padding: 0 rem-calc(20); position: absolute; top: 0; @@ -1356,33 +1375,35 @@ table { } .ssb-telegram { - background: #0088cc; + background: #08c; background-image: none !important; - color: white; - height: $line-height*2 !important; + color: #fff; + height: $line-height * 2 !important; position: relative; - width: $line-height*2 !important; + width: $line-height * 2 !important; - &:before { - content: "1"; + &::before { + content: '1'; font-family: "icons" !important; font-size: rem-calc(24); left: 50%; - line-height: $line-height*2; + line-height: $line-height * 2; margin-left: rem-calc(-11); position: absolute; top: 0; } - &:hover, &:focus { - background: white; - color: #40A2D1; + &:hover, + &:focus { + background: #fff; + color: #40a2d1; } } @include breakpoint(medium) { - .button.button-telegram, .ssb-telegram { + .button.button-telegram, + .ssb-telegram { display: none !important; } } @@ -1391,7 +1412,7 @@ table { a { font-size: rem-calc(24); - margin: 0 $line-height/2; + margin: 0 $line-height / 2; text-decoration: none; &:hover { @@ -1408,98 +1429,102 @@ table { } .ssb-twitter { - background: #45B0E3; - color: white; + background: #45b0e3; + color: #fff; height: $line-height; position: relative; - width: $line-height*2; + width: $line-height * 2; - &:before { - content: "f"; + &::before { + content: 'f'; font-family: "icons" !important; font-size: rem-calc(24); left: 50%; - line-height: $line-height*2; + line-height: $line-height * 2; margin-left: rem-calc(-11); position: absolute; top: 0; } - &:hover, &:focus { - background: white; - color: #40A2D1; + &:hover, + &:focus { + background: #fff; + color: #40a2d1; } } .ssb-facebook { - background: #3B5998; - color: white; + background: #3b5998; + color: #fff; height: rem-calc(24); position: relative; width: rem-calc(48); - &:before { - content: "A"; + &::before { + content: 'A'; font-family: "icons" !important; font-size: rem-calc(24); left: 50%; - line-height: $line-height*2; + line-height: $line-height * 2; margin-left: rem-calc(-11); position: absolute; top: 0; } - &:hover, &:focus { - background: white; - color: #354F88; + &:hover, + &:focus { + background: #fff; + color: #354f88; } } - .ssb-google_plus { - background: #DE4C34; - color: white; + [class^="ssb-icon ssb-google"] { + background: #de4c34; + color: #fff; height: rem-calc(24); position: relative; width: rem-calc(48); - &:before { - content: "B"; + &::before { + content: 'B'; font-family: "icons" !important; font-size: rem-calc(24); left: 50%; - line-height: $line-height*2; + line-height: $line-height * 2; margin-left: rem-calc(-11); position: absolute; top: 0; } - &:hover, &:focus { - background: white; - color: #CE3E26; + &:hover, + &:focus { + background: #fff; + color: #ce3e26; } } .ssb-telegram { - background: #0088cc; - color: white; + background: #08c; + color: #fff; height: $line-height; position: relative; - width: $line-height*2; + width: $line-height * 2; - &:before { - content: "1"; + &::before { + content: '1'; font-family: "icons" !important; font-size: rem-calc(24); left: 50%; - line-height: $line-height*2; + line-height: $line-height * 2; margin-left: rem-calc(-11); position: absolute; top: 0; } - &:hover, &:focus { - background: white; - color: #40A2D1; + &:hover, + &:focus { + background: #fff; + color: #40a2d1; } } } @@ -1509,45 +1534,42 @@ table { .more-information { - ul { + li { + border-bottom: 1px solid $border; + font-weight: bold; + margin-left: rem-calc(-12); + position: relative; - li { - border-bottom: 1px solid $border; - font-weight: bold; - margin-left: rem-calc(-12); - position: relative; + span { + color: $text; + display: inline-block; + font-weight: normal; + } - span { - color: $text; - display: inline-block; - font-weight: normal; + &::after { + content: none; + color: $text-medium; + font-family: "icons" !important; + position: absolute; + right: 0; + top: 24px; + + @include breakpoint(medium) { + content: 'c'; } + } + } - a { - display: block; - padding: $line-height/2; + a { + display: block; + padding: $line-height / 2; + } - &:hover { - background: $highlight; + a:hover { + background: $highlight; - span { - text-decoration: none; - } - } - } - - &:after { - content: none; - color: $text-medium; - font-family: "icons" !important; - position: absolute; - right: 0; - top: 24px; - - @include breakpoint(medium) { - content: 'c'; - } - } + span { + text-decoration: none; } } } @@ -1559,7 +1581,7 @@ table { @include breakpoint(medium) { .left + .left { - margin-left: $line-height/2; + margin-left: $line-height / 2; } } @@ -1576,10 +1598,10 @@ table { } .progress { - height: $line-height*3; + height: $line-height * 3; @include breakpoint(medium) { - height: $line-height*2; + height: $line-height * 2; } &.success .meter { @@ -1591,12 +1613,12 @@ table { background: #f6f6f6; font-weight: bold; line-height: rem-calc(20); - padding-top: $line-height/4; + padding-top: $line-height / 4; text-transform: uppercase; vertical-align: top; @include breakpoint(medium) { - line-height: $line-height*2; + line-height: $line-height * 2; padding: 0; } @@ -1604,9 +1626,9 @@ table { display: block; @include breakpoint(medium) { - background-color: white; + background-color: #fff; border-radius: rem-calc(30); - color: #43AC6A; + color: #43ac6a; display: inline-block; height: rem-calc(30); line-height: rem-calc(30); @@ -1616,17 +1638,8 @@ table { } &.active { - background: #43AC6A; - color: white; - } - - &.completed { - background: #43AC6A; - color: rgba(255,255,255,.5); - - .number { - opacity: .5; - } + background: #43ac6a; + color: #fff; } } @@ -1641,7 +1654,7 @@ table { .button + form { display: inline-block; - margin-left: $line-height/2; + margin-left: $line-height / 2; } .verification-list { @@ -1651,17 +1664,26 @@ table { span { display: inline-block; - min-width: $line-height*12; + min-width: $line-height * 12; } } } +.verification-step .completed { + background: #43ac6a; + color: rgba(255, 255, 255, 0.5); + + .number { + opacity: 0.5; + } +} + .verify-account { - padding-right: $line-height/2; + padding-right: $line-height / 2; .already-verified { color: $check; - line-height: $line-height*2; + line-height: $line-height * 2; .icon-check { font-size: rem-calc(24); @@ -1672,7 +1694,7 @@ table { } .verify { - margin-bottom: $line-height*2; + margin-bottom: $line-height * 2; margin-top: $line-height; h1 { @@ -1685,18 +1707,18 @@ table { } .letter-divider { - border-top: 1px solid #DDDDDD; + border-top: 1px solid #ddd; clear: both; height: 0; margin: rem-calc(24) 0; position: relative; .icon-letter { - background: white; + background: #fff; color: $color-info; font-size: rem-calc(24); margin-left: -27px; - padding: 0 $line-height/2; + padding: 0 $line-height / 2; position: absolute; top: -12px; } @@ -1707,9 +1729,9 @@ table { // ------------ .comments { - background: $white; + background: #fff; background-repeat: repeat-x; - padding-bottom: $line-height*4; + padding-bottom: $line-height * 4; h2 { margin: 0; @@ -1718,127 +1740,133 @@ table { span { font-size: rem-calc(18); font-weight: normal; - opacity: .8; + opacity: 0.8; } } .comment { - margin: $line-height/4 0; + margin: $line-height / 4 0; position: relative; p { margin-bottom: 0; } - .comment-votes { - color: $text-medium; - font-size: $small-font-size; - line-height: $line-height; + [class^="icon-"] { + font-size: $base-font-size; + vertical-align: sub; + } + } +} - a { - color: $text-light; - display: inline-block; - vertical-align: top; +.comment-body { - &:hover { - color: $text-medium; - text-decoration: none; + img { + margin-right: $line-height / 2; + } - .icon-like { - color: $like; - } + .reply { + background: #fff; + border: 1px solid $border; + border-left: 0; + border-right: 0; + font-size: $small-font-size; + margin: $line-height / 4 0; + padding: $line-height / 4; + position: relative; - .icon-unlike { - color: $unlike; - } - } - } - - [class^="icon-"] { - font-size: $base-font-size; - vertical-align: sub; - } + .relative, + [class^="icon-arrow"] { + padding-left: $line-height / 2; } - .comment-body { - - img { - margin-right: $line-height/2; - } - - .reply { - background: white; - border: 1px solid $border; - border-left: 0; - border-right: 0; - font-size: $small-font-size; - margin: $line-height/4 0; - padding: $line-height/4; - position: relative; - - a.relative, [class^="icon-arrow"] { - padding-left: $line-height/2; - } - - [class^="icon-arrow"] { - font-size: $base-font-size; - left: -20px; - position: absolute; - top: -1px; - } - - .divider { - color: $text-light; - } - - form { - margin-top: $line-height/2; - } - } - - .comment-user { - margin-top: $line-height/4; - padding: $line-height/4 0; - overflow: hidden; - - &.level-1, &.level-2, &.level-3, &.level-4, &.level-5, - &.is-author, &.is-admin, &.is-moderator { - background: rgba(70,219,145,.3); - padding: $line-height/4 $line-height/2; - } - - &.level-1 { - background: none; - } - - &.level-5 { - background: rgba(255,241,204,1); - } - - &.is-author, &.is-admin, &.is-moderator { - background: rgba(45,144,248,.15); - } - } + [class^="icon-arrow"] { + font-size: $base-font-size; + left: -20px; + position: absolute; + top: -1px; } - .comment-children { - border-left: 1px dashed $border; - display: inline-block; - margin-left: rem-calc(16); - padding-left: rem-calc(8); - width: 100%; + .divider { + color: $text-light; } - .comment-info { - color: $text-medium; - display: inline-block; - font-size: $small-font-size; - line-height: rem-calc(32); // Same as avatar height + form { + margin-top: $line-height / 2; + } + } - span.user-name { - color: $text; - font-weight: bold; - } + .comment-user { + margin-top: $line-height / 4; + padding: $line-height / 4 0; + overflow: hidden; + + &.level-1, + &.level-2, + &.level-3, + &.level-4, + &.level-5, + &.is-author, + &.is-admin, + &.is-moderator { + background: rgba(70, 219, 145, 0.3); + padding: $line-height / 4 $line-height / 2; + } + + &.level-1 { + background: none; + } + + &.level-5 { + background: rgba(255, 241, 204, 1); + } + + &.is-author, + &.is-admin, + &.is-moderator { + background: rgba(45, 144, 248, 0.15); + } + } +} + +.comment-children { + border-left: 1px dashed $border; + display: inline-block; + margin-left: rem-calc(16); + padding-left: rem-calc(8); + width: 100%; +} + +.comment-info { + color: $text-medium; + display: inline-block; + font-size: $small-font-size; + line-height: rem-calc(32); // Same as avatar height + + .user-name { + color: $text; + font-weight: bold; + } +} + +.comment-votes { + + a { + color: $text-light; + display: inline-block; + vertical-align: top; + } + + a:hover { + color: $text-medium; + text-decoration: none; + + .icon-like { + color: $like; + } + + .icon-unlike { + color: $unlike; } } } @@ -1857,7 +1885,7 @@ table { } label { - padding-right: $line-height/2; + padding-right: $line-height / 2; float: none; @include breakpoint(medium) { @@ -1883,7 +1911,8 @@ table { } } -.flag-disable, .flag-active { +.flag-disable, +.flag-active { line-height: 0; vertical-align: sub; } @@ -1904,54 +1933,55 @@ table { // ------------ .activity { - margin-bottom: $line-height*2; + margin-bottom: $line-height * 2; table { border: 0; + } - td { - position: relative; + td { + position: relative; - &:first-child { - padding-left: $line-height*1.5; - width: 75%; - } - - &:before { - color: $brand; - font-family: "icons" !important; - font-size: rem-calc(24); - left: 4px; - position: absolute; - } + &:first-child { + padding-left: $line-height * 1.5; + width: 75%; } - &.activity-comments td:first-child:before { - content: "e"; + &::before { + color: $brand; + font-family: "icons" !important; + font-size: rem-calc(24); + left: 4px; + position: absolute; + } + } + + .activity-comments td:first-child::before { + content: 'e'; + top: 18px; + } + + .activity-debates td:first-child::before { + content: 'i'; + top: 14px; + } + + .activity-proposals { + + td:first-child::before { + content: 'h'; top: 18px; } - &.activity-debates td:first-child:before { - content: "i"; - top: 14px; + .retired { + text-decoration: line-through; } + } - &.activity-proposals { - - td:first-child:before { - content: "h"; - top: 18px; - } - - .retired { - text-decoration: line-through; - } - } - - &.activity-investment-projects td:first-child:before, &.activity-ballot td:first-child:before { - content: "\53"; - top: 10px; - } + .activity-investment-projects td:first-child::before, + .activity-ballot td:first-child::before { + content: '\53'; + top: 10px; } } @@ -1967,7 +1997,7 @@ table { } .banner-style-three { - background-color: #33DADF; + background-color: #33dadf; } @include breakpoint(large) { @@ -1985,38 +2015,45 @@ table { } } -.banner-img-one, .banner-img-two, .banner-img-three { +.banner-img-one, +.banner-img-two, +.banner-img-three { background-position: bottom right; background-repeat: no-repeat; } -.banner-style-one, .banner-style-two, .banner-style-three { +.banner-style-one, +.banner-style-two, +.banner-style-three { margin: 0; margin-bottom: $line-height; - h2, h3, a { - color: #eaeaf2; - } - - h2 { - padding: $line-height/2; - padding-bottom: 0; - } - - h3 { - padding: $line-height/2; - padding-top: 0; - } - - a:hover h2, a:hover h3 { - color: #eaeaf2 !important; - text-decoration: none; - } - @include breakpoint(large) { h3 { width: 80%; } } + + h2, + h3, + a { + color: #eaeaf2; + } + + h2 { + padding: $line-height / 2; + padding-bottom: 0; + } + + h3 { + padding: $line-height / 2; + padding-top: 0; + } + + a:hover h2, + a:hover h3 { + color: #eaeaf2 !important; + text-decoration: none; + } } diff --git a/app/assets/stylesheets/legislation.scss b/app/assets/stylesheets/legislation.scss index c3917ad8c..60f6d0030 100644 --- a/app/assets/stylesheets/legislation.scss +++ b/app/assets/stylesheets/legislation.scss @@ -18,7 +18,7 @@ h4 { font-weight: 400; text-align: center; - color: white; + color: #fff; } } } @@ -28,7 +28,7 @@ .legislation-categories { .menu.simple { - border-bottom: none; + border-bottom: 0; list-style: none; padding-left: 0; margin-left: 0; @@ -37,19 +37,19 @@ @include breakpoint(medium) { margin: 1.5rem 0; } + } - li { - display: block; - cursor: pointer; - margin-bottom: 1rem; + li { + display: block; + cursor: pointer; + margin-bottom: 1rem; - @include breakpoint(medium) { - margin-bottom: 2rem; - max-width: 80%; - } + @include breakpoint(medium) { + margin-bottom: 2rem; + max-width: 80%; } - li.active { + &.active { font-weight: 700; } } @@ -58,14 +58,14 @@ // 03. Legislation cards // ----------------- .legislation { - margin: 0 0 $line-height 0; - background: white; + margin: 0 0 $line-height; + background: #fff; border: 1px solid; border-color: #e5e6e9 #dfe0e4 #d0d1d5; border-radius: 0; - box-shadow: 0px 1px 3px 0 #DEE0E3; + box-shadow: 0 1px 3px 0 #dee0e3; min-height: 12rem; - padding: 2rem 0 0 0; + padding: 2rem 0 0; } .legislation-text { @@ -83,13 +83,13 @@ } .legislation-calendar { - background: #E5ECF2; + background: #e5ecf2; padding-top: 1rem; h5 { margin-left: 0.25rem; margin-bottom: 0; - color: #61686E; + color: #61686e; @include breakpoint(medium) { margin-left: 0; diff --git a/app/assets/stylesheets/legislation_process.scss b/app/assets/stylesheets/legislation_process.scss index c9f4a4739..15846943c 100644 --- a/app/assets/stylesheets/legislation_process.scss +++ b/app/assets/stylesheets/legislation_process.scss @@ -16,7 +16,7 @@ // ----------------- .grey-heading { - background: #E6E6E6; + background: #e6e6e6; } $epigraph-font-size: rem-calc(15); @@ -39,15 +39,15 @@ $epigraph-line-height: rem-calc(22); list-style: none; margin-left: 0; - li:before { + li::before { vertical-align: text-bottom; padding-right: 0.5rem; - content: "■"; - color: #8AA8BE; + content: '■'; + color: #8aa8be; } } - #debate-show { + .legislation-debate-show { display: none; } @@ -82,12 +82,9 @@ $epigraph-line-height: rem-calc(22); } .half-gradient { - /* Permalink - use to edit and share this gradient: http://colorzilla.com/gradient-editor/#e6e6e6+0,e6e6e6+50,ffffff+50 */ - background: #e6e6e6; /* Old browsers */ - background: -moz-linear-gradient(top, #e6e6e6 0%, #e6e6e6 50%, #ffffff 50%); /* FF3.6-15 */ - background: -webkit-linear-gradient(top, #e6e6e6 0%,#e6e6e6 50%,#ffffff 50%); /* Chrome10-25,Safari5.1-6 */ - background: linear-gradient(to bottom, #e6e6e6 0%,#e6e6e6 50%,#ffffff 50%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */ - filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#e6e6e6', endColorstr='#ffffff',GradientType=0 ); /* IE6-9 */ + background: #e6e6e6; + background: linear-gradient(to bottom, #e6e6e6 0%, #e6e6e6 50%, #fff 50%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#e6e6e6', endColorstr='#fff', GradientType=0); } .text-center .button { @@ -115,14 +112,14 @@ $epigraph-line-height: rem-calc(22); ul { font-size: $epigraph-font-size; line-height: $epigraph-line-height; + } - li { - margin-bottom: 1rem; + li { + margin-bottom: 1rem; - p { - display: inline; - margin-bottom: 0; - } + p { + display: inline; + margin-bottom: 0; } } @@ -134,6 +131,11 @@ $epigraph-line-height: rem-calc(22); .button-subscribe { margin-top: 1rem; + @include breakpoint(medium) { + padding: 0.5em 1em; + margin-top: 3rem; + } + h3 { margin-bottom: 0; } @@ -144,12 +146,7 @@ $epigraph-line-height: rem-calc(22); } &:hover h3 { - color: white; - } - - @include breakpoint(medium) { - padding: 0.5em 1em; - margin-top: 3rem; + color: #fff; } } } @@ -161,7 +158,7 @@ $epigraph-line-height: rem-calc(22); .legislation-process-list { border-bottom: 1px solid $medium-gray; - margin: 0 1rem 1rem 1rem; + margin: 0 1rem 1rem; padding-top: 4rem; @include breakpoint(medium) { @@ -190,53 +187,54 @@ $epigraph-line-height: rem-calc(22); left: -1rem; } } + } - li { - cursor: pointer; - display: inline-block; - margin: 0 1rem 1rem 0; - transition: all 0.4s; - border-bottom: 2px solid transparent; + li { + cursor: pointer; + display: inline-block; + margin: 0 1rem 1rem 0; + transition: all 0.4s; + border-bottom: 2px solid transparent; - &:first-of-type { - margin-left: 0; - } - - &:hover, - &:active, - &:focus { - border-bottom: 2px solid $brand; - } - - @media (min-width: 950px) { - margin: 0 0 0 3rem; - } - - a, - h4 { - display: block; - color: #6D6D6D; - margin-bottom: 0; - } - - a { - &:hover, &:active { - text-decoration: none; - } - - p { - margin-bottom: 0; - - @include breakpoint(medium) { - margin-bottom: 1rem; - } - } - } + &:first-of-type { + margin-left: 0; } - .active { + &:hover, + &:active, + &:focus { border-bottom: 2px solid $brand; } + + @media (min-width: 950px) { + margin: 0 0 0 3rem; + } + + a, + h4 { + display: block; + color: #6d6d6d; + margin-bottom: 0; + } + + a { + &:hover, + &:active { + text-decoration: none; + } + + p { + margin-bottom: 0; + + @include breakpoint(medium) { + margin-bottom: 1rem; + } + } + } + } + + .active { + border-bottom: 2px solid $brand; } } } @@ -269,21 +267,21 @@ $epigraph-line-height: rem-calc(22); .debate-title a { color: $brand; } + } - .debate-meta, - .debate-meta a { - font-size: $small-font-size; - color: #6D6D6D; + .debate-meta, + .debate-meta a { + font-size: $small-font-size; + color: #6d6d6d; - .icon-comments { - margin-right: 0.2rem; - } + .icon-comments { + margin-right: 0.2rem; } } .debate-info { padding: 1rem; - background: #F4F4F4; + background: #f4f4f4; } } @@ -297,13 +295,14 @@ $epigraph-line-height: rem-calc(22); .quiz-header { margin-bottom: 2rem; - .quiz-title, .quiz-next { + .quiz-title, + .quiz-next { padding: 1rem; height: 6rem; } .quiz-title { - background: #E5ECF2; + background: #e5ecf2; .quiz-header-title { margin-bottom: 0; @@ -324,12 +323,13 @@ $epigraph-line-height: rem-calc(22); .quiz-next-link { display: block; - &:hover, &:active { + &:hover, + &:active { text-decoration: none; } .quiz-next { - background: #CCDBE5; + background: #ccdbe5; font-weight: 700; color: $brand; font-size: $small-font-size; @@ -341,13 +341,14 @@ $epigraph-line-height: rem-calc(22); vertical-align: sub; } - &:hover, &:active { + &:hover, + &:active { text-decoration: none; background: $brand; - color: white; + color: #fff; .icon-angle-right { - color: white; + color: #fff; } } } @@ -380,8 +381,8 @@ $epigraph-line-height: rem-calc(22); } .active { - background: #CCDBE6; - border: none; + background: #ccdbe6; + border: 0; } .control input { @@ -392,7 +393,7 @@ $epigraph-line-height: rem-calc(22); .control input:checked ~ .control-indicator { background-color: $brand; - border: none; + border: 0; } .radio .control-indicator { @@ -409,11 +410,8 @@ $epigraph-line-height: rem-calc(22); line-height: 1rem; font-size: 65%; text-align: center; - border: 2px solid #9C9C9C; + border: 2px solid #9c9c9c; pointer-events: none; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; user-select: none; } } @@ -422,9 +420,9 @@ $epigraph-line-height: rem-calc(22); // 06. Legislation draft // ----------------- .debate-draft { - padding: 10rem 2rem 15rem 2rem; + padding: 10rem 2rem 15rem; display: block; - background: #F2F2F2; + background: #f2f2f2; button { height: 90px; @@ -445,7 +443,7 @@ $epigraph-line-height: rem-calc(22); .legislation-allegation { padding-top: 1rem; - #debate-show { + .legislation-debate-show { margin-top: 2rem; } @@ -459,7 +457,7 @@ $epigraph-line-height: rem-calc(22); .button-circle { line-height: 0; - padding: 0em; + padding: 0; width: 30px; height: 30px; border-radius: 50%; @@ -467,7 +465,7 @@ $epigraph-line-height: rem-calc(22); span { padding-left: 1px; - &:before { + &::before { line-height: 1.55; } } @@ -482,8 +480,12 @@ $epigraph-line-height: rem-calc(22); .button-subscribed { margin-top: 1rem; - border: 1px solid #D1D1D1; - background: #F2F2F2; + border: 1px solid #d1d1d1; + background: #f2f2f2; + + @include breakpoint(medium) { + padding: 0.5em 1em; + } h3 { display: inline-block; @@ -499,10 +501,6 @@ $epigraph-line-height: rem-calc(22); &:hover h3 { color: $text; } - - @include breakpoint(medium) { - padding: 0.5em 1em; - } } } @@ -547,7 +545,7 @@ $epigraph-line-height: rem-calc(22); a { text-decoration: underline; - color: $text-medium + color: $text-medium; } } } @@ -585,12 +583,12 @@ $epigraph-line-height: rem-calc(22); } .calc-text { - width: calc(65% - 25px) + width: calc(65% - 25px); } .calc-comments { cursor: pointer; - background: #F2F2F2; + background: #f2f2f2; width: 50px; .draft-panel { @@ -618,7 +616,7 @@ $epigraph-line-height: rem-calc(22); font-weight: 700; padding: 0.5rem 1rem; color: #696969; - background: #F2F2F2; + background: #f2f2f2; font-size: $small-font-size; .icon-comments { @@ -647,21 +645,23 @@ $epigraph-line-height: rem-calc(22); li { margin-bottom: 1rem; } + .open::before { cursor: pointer; position: absolute; margin-left: -1.25rem; - font-family: "icons"; - content: "\58"; + font-family: 'icons'; + content: '\58'; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } + .closed::before { cursor: pointer; position: absolute; margin-left: -1.25rem; - font-family: "icons"; - content: "\5a"; + font-family: 'icons'; + content: '\5a'; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } @@ -689,7 +689,7 @@ $epigraph-line-height: rem-calc(22); .anchor::before { display: none; - content: "#"; + content: '#'; color: $text-medium; position: absolute; left: 0; @@ -732,10 +732,7 @@ $epigraph-line-height: rem-calc(22); font-weight: 700; color: #696969; margin-top: 4rem; - -webkit-transform: rotate(-90deg); - -moz-transform: rotate(-90deg); - -ms-transform: rotate(-90deg); - -o-transform: rotate(-90deg); + transform: rotate(-90deg); filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=3); } } @@ -746,7 +743,7 @@ $epigraph-line-height: rem-calc(22); .comments-on { .calc-index { width: 50px; - background: #F2F2F2; + background: #f2f2f2; cursor: pointer; .panel-title { @@ -766,10 +763,7 @@ $epigraph-line-height: rem-calc(22); font-weight: 700; color: #696969; margin-top: $line-height; - -webkit-transform: rotate(-90deg); - -moz-transform: rotate(-90deg); - -ms-transform: rotate(-90deg); - -o-transform: rotate(-90deg); + transform: rotate(-90deg); filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=3); .panel-title { @@ -781,11 +775,11 @@ $epigraph-line-height: rem-calc(22); .calc-text { width: calc(65% - 25px); - border-right: none; + border-right: 0; .show-comments { width: 105%; - background: #FAFAFA; + background: #fafafa; padding: 0.25rem 2.5rem 0.25rem 0.25rem; border: 1px solid $border; margin-bottom: 1rem; @@ -798,7 +792,7 @@ $epigraph-line-height: rem-calc(22); } .calc-comments { - background: white; + background: #fff; cursor: auto; width: calc(35% - 25px); @@ -810,7 +804,7 @@ $epigraph-line-height: rem-calc(22); display: none; } - #comments-box { + .comments-box-container { position: absolute; top: 230px; } @@ -818,7 +812,7 @@ $epigraph-line-height: rem-calc(22); .comment-box { width: 375px; padding: 1rem; - background: #F9F9F9; + background: #f9f9f9; border: 1px solid $border; display: block; margin-bottom: 2rem; @@ -868,7 +862,7 @@ $epigraph-line-height: rem-calc(22); .participation-not-allowed { font-size: 0.875rem; height: 50px; - padding: .85rem 0.75rem; + padding: 0.85rem 0.75rem; top: -18px; } } @@ -880,31 +874,31 @@ $epigraph-line-height: rem-calc(22); border-bottom: 1px solid $border; .comment-advice { - border-top: 1px solid #D0D0D0; - border-right: 1px solid #D0D0D0; - border-left: 1px solid #D0D0D0; + border-top: 1px solid #d0d0d0; + border-right: 1px solid #d0d0d0; + border-left: 1px solid #d0d0d0; width: 100%; padding: 0.5rem; display: inline-block; font-size: $small-font-size; - background: #DFDFDF; + background: #dfdfdf; .icon-proposals { color: #838383; } a { - color: #87A3B9; - text-decoration: underline; - } + color: #87a3b9; + text-decoration: underline; + } } textarea { border-radius: 0; box-shadow: none; - border-bottom: 1px solid #D0D0D0; - border-right: 1px solid #D0D0D0; - border-left: 1px solid #D0D0D0; + border-bottom: 1px solid #d0d0d0; + border-right: 1px solid #d0d0d0; + border-left: 1px solid #d0d0d0; width: 100%; height: 200px; margin-bottom: 0.5rem; @@ -912,7 +906,7 @@ $epigraph-line-height: rem-calc(22); .comment-actions { .cancel-comment { - color: #87A3B9; + color: #87a3b9; text-decoration: underline; font-size: $small-font-size; display: inline-block; @@ -948,10 +942,11 @@ $epigraph-line-height: rem-calc(22); display: inline-block; &::after { - content: "|"; + content: '|'; color: #838383; } } + .comment-replies { display: inline-block; } @@ -968,14 +963,14 @@ $epigraph-line-height: rem-calc(22); &::after { margin-left: 0.25rem; - content: "|"; + content: '|'; } } .icon-like, .icon-unlike { cursor: pointer; - color: #C7C7C7; + color: #c7c7c7; &:hover, &:active, @@ -993,8 +988,8 @@ $epigraph-line-height: rem-calc(22); } .draft-panel { - background: #E5E5E5; - border-left: 1px solid #D4D4D4; + background: #e5e5e5; + border-left: 1px solid #d4d4d4; .panel-title { display: inline-block; @@ -1022,7 +1017,7 @@ $epigraph-line-height: rem-calc(22); &::before { margin-right: 0.25rem; - content: "—" + content: '—'; } .changes-link { @@ -1041,7 +1036,7 @@ $epigraph-line-height: rem-calc(22); .icon-external { text-decoration: none; - color: #999999; + color: #999; line-height: 0; vertical-align: sub; margin-left: 0.5rem; @@ -1067,9 +1062,9 @@ $epigraph-line-height: rem-calc(22); } .comment-section { - background: #FAFAFA; + background: #fafafa; padding: 1rem; - border: 1px solid #DEE0E3; + border: 1px solid #dee0e3; margin-top: 0.25rem; margin-bottom: 1rem; } @@ -1085,7 +1080,7 @@ $epigraph-line-height: rem-calc(22); .icon-expand, .icon-comments { text-decoration: none; - color: #999999; + color: #999; line-height: 0; } @@ -1119,9 +1114,9 @@ $epigraph-line-height: rem-calc(22); } .comment-section { - background: #FAFAFA; + background: #fafafa; padding: 1rem; - border: 1px solid #DEE0E3; + border: 1px solid #dee0e3; margin-top: 0.25rem; margin-bottom: 1rem; } @@ -1137,7 +1132,7 @@ $epigraph-line-height: rem-calc(22); .icon-expand, .icon-comments { text-decoration: none; - color: #999999; + color: #999; line-height: 0; } diff --git a/app/assets/stylesheets/mixins.scss b/app/assets/stylesheets/mixins.scss index 5ffc534cb..401a2ec81 100644 --- a/app/assets/stylesheets/mixins.scss +++ b/app/assets/stylesheets/mixins.scss @@ -7,14 +7,14 @@ // -------- @mixin logo { - color: white; + color: #fff; display: inline-block; font-family: 'Lato' !important; font-size: rem-calc(24); font-weight: lighter; @include breakpoint(medium) { - line-height: $line-height*2; + line-height: $line-height * 2; margin-top: 0; } @@ -24,7 +24,7 @@ @include breakpoint(medium) { height: 80px; - margin-right: $line-height/2; + margin-right: $line-height / 2; margin-top: 0; width: 80px; } diff --git a/app/assets/stylesheets/pages.scss b/app/assets/stylesheets/pages.scss index f8bf6c7cd..d3db7e9b2 100644 --- a/app/assets/stylesheets/pages.scss +++ b/app/assets/stylesheets/pages.scss @@ -16,7 +16,7 @@ padding-top: $line-height; &.light { - background: #ECF0F1; + background: #ecf0f1; } } @@ -36,7 +36,7 @@ @include breakpoint(medium) { display: inline-block; - margin-right: $line-height/2; + margin-right: $line-height / 2; } } } @@ -50,7 +50,7 @@ color: $brand; } - .additional-info { + .additional-info { margin-bottom: $line-height; } @@ -68,23 +68,23 @@ } } - ul.features { + .features { list-style-type: circle; margin-left: $line-height; @include breakpoint(medium) { - margin: $line-height 0 $line-height $line-height*2.5; + margin: $line-height 0 $line-height $line-height * 2.5; } li { - margin-bottom: $line-height + margin-bottom: $line-height; } } .section-content { border-top: 1px solid $medium-gray; - padding-bottom: $line-height*2; - padding-top: $line-height*2; + padding-bottom: $line-height * 2; + padding-top: $line-height * 2; &:first-child { border-top: 0; @@ -101,10 +101,10 @@ .sidebar-card { border: 1px solid $border; margin-bottom: $line-height; - padding: $line-height/2; + padding: $line-height / 2; &.light { - background: #ECF0F1; + background: #ecf0f1; border: 0; } } diff --git a/app/assets/stylesheets/participation.scss b/app/assets/stylesheets/participation.scss index 75bbba32c..a9cde9f55 100644 --- a/app/assets/stylesheets/participation.scss +++ b/app/assets/stylesheets/participation.scss @@ -16,7 +16,7 @@ @mixin votes { border-top: 1px solid $border; margin-top: $line-height; - padding: $line-height/2 0; + padding: $line-height / 2 0; position: relative; @include breakpoint(medium) { @@ -25,8 +25,9 @@ margin-top: 0; } - .icon-like, .icon-unlike { - background: white; + .icon-like, + .icon-unlike { + background: #fff; border: 2px solid $text-light; border-radius: rem-calc(3); color: $text-light; @@ -36,8 +37,9 @@ padding: rem-calc(3) rem-calc(6); position: relative; - &:hover, &:active { - color: white; + &:hover, + &:active { + color: #fff; cursor: pointer; opacity: 1 !important; } @@ -45,7 +47,8 @@ .icon-like { - &:hover, &:active { + &:hover, + &:active { background: $like; border: 2px solid $like; } @@ -53,23 +56,25 @@ .icon-unlike { - &:hover, &:active { + &:hover, + &:active { background: $unlike; border: 2px solid $unlike; } } - .like, .unlike { + .like, + .unlike { line-height: rem-calc(48); vertical-align: super; text-decoration: none; - span.percentage { + .percentage { color: $text; display: inline-block; font-size: $small-font-size; - line-height: $line-height*2; - padding-right: $line-height/2; + line-height: $line-height * 2; + padding-right: $line-height / 2; vertical-align: top; @include breakpoint(medium) { @@ -82,8 +87,9 @@ .voted { - .icon-like, .icon-unlike { - color: white; + .icon-like, + .icon-unlike { + color: #fff; } .icon-like { @@ -99,15 +105,16 @@ .no-voted { - .icon-like, .icon-unlike { - opacity: .3; + .icon-like, + .icon-unlike { + opacity: 0.3; } } .total-votes { font-weight: bold; float: right; - line-height: $line-height*2; + line-height: $line-height * 2; @include breakpoint(medium) { display: block; @@ -136,7 +143,7 @@ border-bottom-right-radius: rem-calc(3); border-top-right-radius: rem-calc(3); display: block; - height: $line-height/2; + height: $line-height / 2; } } @@ -153,7 +160,7 @@ color: $text-medium; &[title] { - border-bottom: none; + border-bottom: 0; } } @@ -164,7 +171,8 @@ font-size: $small-font-size; margin-top: rem-calc(12); - &:hover, &:active { + &:hover, + &:active { background: lighten($proposals, 25%); cursor: pointer; } @@ -208,7 +216,7 @@ left: 0; line-height: $line-height; min-height: 100%; - padding: $line-height $line-height/2; + padding: $line-height $line-height / 2; position: absolute; text-align: center; top: 0; @@ -229,8 +237,8 @@ } .reply .participation-not-allowed { - padding-right: $line-height/2; - padding-top: $line-height/6; + padding-right: $line-height / 2; + padding-top: $line-height / 6; text-align: right; } @@ -242,10 +250,12 @@ .budget-investment-form, .spending-proposal-form { - .icon-debates, .icon-proposals, .icon-budget { + .icon-debates, + .icon-proposals, + .icon-budget { font-size: rem-calc(50); line-height: $line-height; - opacity: .5; + opacity: 0.5; } .icon-debates { @@ -259,34 +269,34 @@ .icon-budget { color: $budget; } +} - .recommendations { - list-style-type: none; - margin-left: 0; - margin-top: $line-height; +.recommendations { + list-style-type: none; + margin-left: 0; + margin-top: $line-height; - li { - font-size: $small-font-size; - margin: $line-height/2 0; + li { + font-size: $small-font-size; + margin: $line-height / 2 0; - &:before { - content: "l "; - font-family: "icons" !important; - } + &::before { + content: 'l '; + font-family: "icons" !important; } } } .debate-form { - .recommendations li:before { + .recommendations li::before { color: $debates; } } .proposal-form { - .recommendations li:before { + .recommendations li::before { color: $proposals; } } @@ -312,27 +322,31 @@ } .social-share-full .social-share-button { - display:inline; + display: inline; } - .whatsapp:before { + .whatsapp::before { background-color: #43d854; - color: white; + color: #fff; font-size: 1.7em; margin-left: rem-calc(0.5); padding: rem-calc(9.5) rem-calc(9.8); vertical-align: rem-calc(10); } - .edit-debate, .edit-proposal { + .edit-debate, + .edit-proposal { margin-bottom: 0; } - .debate-info, .proposal-info, .investment-project-info, .budget-investment-show { + .debate-info, + .proposal-info, + .investment-project-info, + .budget-investment-show { clear: both; color: $text-medium; font-size: $small-font-size; - margin-bottom: $line-height/2; + margin-bottom: $line-height / 2; position: relative; span:not(.label) { @@ -350,24 +364,26 @@ } } - .debate-description, .proposal-description { + .debate-description, + .proposal-description { font-size: rem-calc(15); line-height: rem-calc(30); } - ul, ol { - margin: rem-calc(12) 0; + ul, + ol { li { font-size: rem-calc(15); margin-bottom: rem-calc(15); } + } - &.tags, &.geozone { + &.tags, + &.geozone { - li { - margin-bottom: 0; - } + li { + margin-bottom: 0; } } @@ -383,10 +399,6 @@ a { color: $link !important; - - &:hover { - color: $link-hover !important; - } } } @@ -403,27 +415,28 @@ } blockquote { - color: #4C4C4C; + color: #4c4c4c; margin-top: rem-calc(12); padding-top: 0; font-size: rem-calc(15); line-height: rem-calc(30); } - .document-link, .video-link { + .document-link, + .video-link { border: 1px solid $border; display: block; - margin: $line-height/2 0; - padding: $line-height/2; + margin: $line-height / 2 0; + padding: $line-height / 2; position: relative; a { padding-left: rem-calc(24); } - :before { - color: #007BB7; - content: "G"; + ::before { + color: #007bb7; + content: 'G'; font-family: "icons" !important; font-size: rem-calc(24); left: rem-calc(6); @@ -435,9 +448,9 @@ .video-link { - :before { - color: #CC181E; - content: "D"; + ::before { + color: #cc181e; + content: 'D'; } } @@ -463,14 +476,17 @@ color: $text; } -.investment-project-show p, .budget-investment-show p { +.investment-project-show p, +.budget-investment-show p { word-break: break-word; } -.proposal-show, .investment-project-show, .budget-investment-show { +.proposal-show, +.investment-project-show, +.budget-investment-show { .supports { - padding: $line-height/2 0 0; + padding: $line-height / 2 0 0; } .share-supported { @@ -481,141 +497,151 @@ // 04. List participation // ---------------------- -.debates-list, .proposals-list, .investment-projects-list, .budget-investments-list { +.debates-list, +.proposals-list, +.investment-projects-list, +.budget-investments-list { @include breakpoint(medium) { margin-bottom: rem-calc(48); } } -.investment-projects-list, .budget-investments-list { +.investment-projects-list, +.budget-investments-list { @include breakpoint(medium) { - min-height: $line-height*15; + min-height: $line-height * 15; } } -.debate, .proposal, .investment-project, .budget-investment, .legislation { - margin: $line-height/4 0; +.debate, +.proposal, +.investment-project, +.budget-investment, +.legislation { + margin: $line-height / 4 0; .panel { - background: white; + background: #fff; border: 1px solid; border-color: #e5e6e9 #dfe0e4 #d0d1d5; border-radius: 0; - box-shadow: 0px 1px 3px 0 $border; + box-shadow: 0 1px 3px 0 $border; margin-bottom: rem-calc(12); min-height: rem-calc(192); - padding: rem-calc(12) rem-calc(12) 0 rem-calc(12); + padding: rem-calc(12) rem-calc(12) 0; @include breakpoint(medium) { margin-bottom: rem-calc(-1); padding-bottom: rem-calc(12); } + @include breakpoint(medium) { + .divider { + display: inline-block; + } + } + h3 { font-weight: bold; - margin-top: $line-height/2; + margin-top: $line-height / 2; a { color: $text; } } + } - .debate-content, .proposal-content, - .investment-project-content, .budget-investment-content { - margin: 0; - min-height: rem-calc(180); - position: relative; + .debate-content, + .proposal-content, + .investment-project-content, + .budget-investment-content { + margin: 0; + min-height: rem-calc(180); + position: relative; + } - .tags { - display: block; - } + .tags { + display: block; + } + + .icon-debates, + .icon-proposals, + .icon-budget { + font-size: rem-calc(18); + line-height: $line-height; + margin-left: rem-calc(6); + top: 0; + } + + .icon-debates { + color: $debates; + } + + .icon-proposals { + color: $proposals-dark; + } + + .icon-budget { + color: $budget; + font-size: $small-font-size; + } + + .debate-info, + .proposal-info, + .investment-project-info, + .budget-investment-info { + color: $text-medium; + font-size: $small-font-size; + margin: rem-calc(6) 0 0; + + .icon-comments { + font-size: rem-calc(16); + vertical-align: top; } - .icon-debates, .icon-proposals, .icon-budget { - font-size: rem-calc(18); - line-height: $line-height; - margin-left: rem-calc(6); - top: 0; - } - - .icon-debates { - color: $debates; - } - - .icon-proposals { - color: $proposals-dark; - } - - .icon-budget { - color: $budget; - font-size: $small-font-size; - } - - .debate-info, .proposal-info, .investment-project-info, .budget-investment-info { + a { color: $text-medium; - font-size: $small-font-size; - margin: rem-calc(6) 0 0; - - .icon-comments { - font-size: rem-calc(16); - vertical-align: top; - } - - a { - color: $text-medium; - } } + } - .debate-description, .proposal-description, .investment-project-description, .budget-investment-description { - color: $text; - font-size: rem-calc(13); - height: rem-calc(72); - line-height: $line-height; - margin-bottom: rem-calc(12); - margin-top: 0; - overflow: hidden; - position: relative; + .debate-description, + .proposal-description, + .investment-project-description, + .budget-investment-description { + color: $text; + font-size: rem-calc(13); + height: rem-calc(72); + line-height: $line-height; + margin-bottom: rem-calc(12); + margin-top: 0; + overflow: hidden; + position: relative; - a { - color: $text; - } - - ul, ol { - - li { - font-size: rem-calc(13); - margin-bottom: rem-calc(12); - } - } - } - - .truncate { - background: image-url('truncate.png'); - background-repeat: repeat-x; - bottom: 0; - height: 24px; - position: absolute; - width: 100%; - } - - p { + a { color: $text; } } - .divider { - display: none; + .truncate { + background: image-url('truncate.png'); + background-repeat: repeat-x; + bottom: 0; + height: 24px; + position: absolute; + width: 100%; } - @include breakpoint(medium) { - .divider { - display: inline-block; - } + p { + color: $text; } } +.divider { + display: none; +} + .more-info { clear: both; color: $text-medium; @@ -626,24 +652,25 @@ } } -.debate, .debate-show { +.debate, +.debate-show { .votes { @include votes; - .against { - margin-left: $line-height/4; - } - @include breakpoint(medium) { text-align: center; } + + .against { + margin-left: $line-height / 4; + } } } .debate-show .votes { border: 0; - padding: $line-height/2 0; + padding: $line-height / 2 0; } .proposal { @@ -653,8 +680,10 @@ } } -.investment-project, .investment-project-show, -.budget-investment, .budget-investment-show { +.investment-project, +.investment-project-show, +.budget-investment, +.budget-investment-show { .supports { @include supports; @@ -669,16 +698,16 @@ .button-support { background: $budget; - color: white; + color: #fff; &:hover { background: $budget-hover; - color: white; + color: #fff; cursor: pointer; } &:active { - opacity: .75; + opacity: 0.75; } } @@ -714,7 +743,7 @@ .budget-investment .supports .total-supports.no-button, .budget-investment-show .supports .total-supports.no-button { display: block; - margin-top: $line-height*1.5; + margin-top: $line-height * 1.5; } .budget-investment-show { @@ -745,9 +774,10 @@ // 05. Featured // ------------ -.featured-debates, .featured-proposals, +.featured-debates, +.featured-proposals, .enquiries-list { - padding: $line-height/2 0; + padding: $line-height / 2 0; @include breakpoint(medium) { margin-left: 0 !important; @@ -769,11 +799,11 @@ } } - a, .info { + a, + .info { color: lighten($text, 15%); font-size: $small-font-size; } - } .featured-debates { @@ -784,7 +814,7 @@ background: $featured; .proposal-featured { - min-height: $line-height*3.5; + min-height: $line-height * 3.5; } .supports { @@ -794,7 +824,7 @@ padding-bottom: 0; padding-top: 0; - &:after { + &::after { content: none; } @@ -808,7 +838,7 @@ margin-top: 0; &:hover { - background: white; + background: #fff; color: $text; } } @@ -833,21 +863,21 @@ margin-top: 0; font-size: $small-font-size; } + } - .share-supported { + .share-supported { - .ssb-twitter, - .ssb-facebook, - .ssb-google_plus { - background: none; - color: $text; - height: rem-calc(33) !important; + .ssb-twitter, + .ssb-facebook, + [class^="ssb-icon ssb-google"] { + background: none; + color: $text; + height: rem-calc(33) !important; - &:before { - font-size: rem-calc(18); - line-height: rem-calc(33); - } + &::before { + font-size: rem-calc(18); + line-height: rem-calc(33); } } } @@ -859,26 +889,26 @@ .expanded.budget { background: $budget; - h1, h2, p, a.back, .icon-angle-left { - color: white; + h1, + h2, + p, + .back, + .icon-angle-left { + color: #fff; } .button { - background: white; + background: #fff; color: $budget; } .info { - background: #6A2A72; + background: #6a2a72; p { margin-bottom: 0; text-transform: uppercase; } - - @include breakpoint(medium) { - border-top: rem-calc(6) solid #54225C; - } } } @@ -887,15 +917,20 @@ border-bottom: 1px solid $budget; &.budget-heading { - min-height: $line-height*10; + min-height: $line-height * 10; } h1 { margin-bottom: 0; } - h1, h2, .back, .icon-angle-left, p, a { - color: white; + h1, + h2, + .back, + .icon-angle-left, + p, + a { + color: #fff; } .callout.warning { @@ -914,46 +949,46 @@ .spending-proposal-timeline { padding-top: $line-height; + } - ul li { - margin-right: $line-height; - padding-top: $line-height/2; + ul li { + margin-right: $line-height; + padding-top: $line-height / 2; - .icon-calendar { - display: none; - } + .icon-calendar { + display: none; } } } a { text-decoration: underline; + } - &.button { - background: white; - color: $brand; - margin-bottom: rem-calc(3); - text-decoration: none; - } + .button { + background: #fff; + color: $brand; + margin-bottom: rem-calc(3); + text-decoration: none; } .social-share-button a { - color: white; + color: #fff; &.social-share-button-twitter:hover { - color: #40A2D1; + color: #40a2d1; } &.social-share-button-facebook:hover { - color: #354F88; + color: #354f88; } - &.social-share-button-google_plus:hover { - color: #CE3E26; + &[class^="social-share-button-google"] { + color: #ce3e26; } &.social-share-button-telegram:hover { - color: #CE3E26; + color: #ce3e26; } } } @@ -969,7 +1004,6 @@ .progress-meter { background: #fdcb10; border-radius: 0; - -webkit-transition: width 2s; transition: width 2s; } @@ -979,7 +1013,7 @@ } .spent-amount-text { - color: white; + color: #fff; font-size: $base-font-size; font-weight: normal; position: absolute; @@ -988,9 +1022,9 @@ top: 16px; width: 100%; - &:before { + &::before { color: #a5a1ff; - content: "\57"; + content: '\57'; font-family: 'icons'; font-size: $small-font-size; position: absolute; @@ -1000,7 +1034,7 @@ } .total-amount { - color: white; + color: #fff; font-size: rem-calc(18); font-weight: bold; float: right; @@ -1030,7 +1064,8 @@ .ballot { - h2, h3 { + h2, + h3 { font-weight: normal; span { @@ -1040,21 +1075,21 @@ } .ballot-content { - border: 2px solid #F9F9F9; + border: 2px solid #f9f9f9; border-radius: rem-calc(6); - padding: $line-height/2; + padding: $line-height / 2; } .subtitle { border-left: 2px solid $budget; - margin: $line-height/2 0; - padding-left: $line-height/2; + margin: $line-height / 2 0; + padding-left: $line-height / 2; } .amount-spent { background: $success-bg; font-weight: normal; - padding: $line-height/2; + padding: $line-height / 2; span { font-size: rem-calc(24); @@ -1063,15 +1098,15 @@ } } -ul.ballot-list { +.ballot-list { list-style: none; margin-left: 0; li { background: #f9f9f9; line-height: $line-height; - margin-bottom: $line-height/4; - padding: $line-height $line-height/2; + margin-bottom: $line-height / 4; + padding: $line-height $line-height / 2; position: relative; a { @@ -1090,7 +1125,7 @@ ul.ballot-list { .icon-x { color: #9f9f9f; font-size: rem-calc(24); - line-height: $line-height/2; + line-height: $line-height / 2; position: absolute; right: 6px; text-decoration: none; @@ -1104,16 +1139,17 @@ ul.ballot-list { &:hover { background: $budget; - color: white; + color: #fff; - a, span { - color: white; + a, + span { + color: #fff; outline: 0; text-decoration: none; } .remove-investment-project .icon-x { - color: white; + color: #fff; } } } @@ -1121,8 +1157,8 @@ ul.ballot-list { .select-district a { display: inline-block; - margin: $line-height/4 0; - padding: $line-height/4; + margin: $line-height / 4 0; + padding: $line-height / 4; } .select-district .active a { @@ -1131,9 +1167,9 @@ ul.ballot-list { color: $budget; font-weight: bold; - &:after { - content: "\56"; - font-family: "icons"; + &::after { + content: '\56'; + font-family: 'icons'; font-size: $small-font-size; font-weight: normal; line-height: $line-height; @@ -1153,13 +1189,9 @@ ul.ballot-list { @include breakpoint(medium) { background-color: $budget; - -webkit-transition: height 0.3s; - -moz-transition: height 0.3s; transition: height 0.3s; h1 { - -webkit-transition: font-size 0.3s; - -moz-transition: font-size 0.3s; transition: font-size 0.3s; } @@ -1177,8 +1209,6 @@ ul.ballot-list { h1 { font-size: rem-calc(24); - -webkit-transition: font-size 0.3s; - -moz-transition: font-size 0.3s; transition: font-size 0.3s; } } @@ -1189,8 +1219,8 @@ ul.ballot-list { // ------------------------- .dark-heading { - background: #2D3E50; - color: white; + background: #2d3e50; + color: #fff; @include breakpoint(medium) { padding-bottom: $line-height; @@ -1199,7 +1229,7 @@ ul.ballot-list { p { &.title { - color: #FFD200; + color: #ffd200; } &.title-date { @@ -1213,40 +1243,42 @@ ul.ballot-list { padding-top: $line-height; @include breakpoint(medium) { - border-top: rem-calc(6) solid #FFD200; + border-top: rem-calc(6) solid #ffd200; } } } -.featured-proposals-ballot-banner, .sucessfull-proposals-banner { - background: #2D3E50 image-url("ballot_tiny.gif") no-repeat; +.featured-proposals-ballot-banner, +.sucessfull-proposals-banner { + background: #2d3e50 image-url('ballot_tiny.gif') no-repeat; background-position: 75% 0; position: relative; - h2, a:hover h2 { - color: #FFD200 !important; - } - - p { - color: white; - } - @include breakpoint(medium) { margin-left: 0 !important; margin-right: 0 !important; } @include breakpoint(large) { - background: #2D3E50 image-url("ballot.gif") no-repeat; + background: #2d3e50 image-url('ballot.gif') no-repeat; background-position: 90% 0; } + + h2, + a:hover h2 { + color: #ffd200 !important; + } + + p { + color: #fff; + } } .sucessfull-proposals-banner, .successful .panel { .icon-successful { - border-right: 60px solid #FFD200; + border-right: 60px solid #ffd200; border-top: 0; border-bottom: 60px solid transparent; height: 0; @@ -1255,9 +1287,9 @@ ul.ballot-list { top: 0; width: 0; - &:after { - color: #1B254C; - content: "\59"; + &::after { + color: #1b254c; + content: '\59'; font-family: "icons" !important; left: 34px; position: absolute; @@ -1292,15 +1324,15 @@ ul.ballot-list { // ---------------------- .dark-heading { - background: #2D3E50; - color: white; + background: #2d3e50; + color: #fff; .title { - color: #92BA48; + color: #92ba48; } .button { - background: white; + background: #fff; color: $brand; } @@ -1324,28 +1356,30 @@ ul.ballot-list { padding: $line-height; @include breakpoint(medium) { - border-top: rem-calc(6) solid #92BA48; + border-top: rem-calc(6) solid #92ba48; } } a:not(.button) { - color: white; + color: #fff; text-decoration: underline; } - .back, .icon-angle-left { - color: white; + .back, + .icon-angle-left { + color: #fff; } &.polls-show-header { - min-height: $line-height*8; + min-height: $line-height * 8; } } -.poll, .poll-question { - background: white; +.poll, +.poll-question { + background: #fff; border-radius: rem-calc(6); - margin-bottom: $line-height/2; + margin-bottom: $line-height / 2; } .poll { @@ -1361,11 +1395,11 @@ ul.ballot-list { top: 0; width: 0; - &.can-answer:after, - &.cant-answer:after, - &.not-logged-in:after, - &.already-answer:after, - &.unverified:after { + &.can-answer::after, + &.cant-answer::after, + &.not-logged-in::after, + &.already-answer::after, + &.unverified::after { font-family: "icons" !important; left: 34px; position: absolute; @@ -1375,45 +1409,45 @@ ul.ballot-list { &.can-answer { border-right: 60px solid $info-bg; - &:after { + &::after { color: $color-info; - content: "\6c"; + content: '\6c'; } } &.cant-answer { border-right: 60px solid $alert-bg; - &:after { + &::after { color: $color-alert; - content: "\74"; + content: '\74'; } } &.not-logged-in { border-right: 60px solid $info-bg; - &:after { + &::after { color: $color-info; - content: "\6f"; + content: '\6f'; } } &.unverified { border-right: 60px solid $warning-bg; - &:after { + &::after { color: $color-warning; - content: "\6f"; + content: '\6f'; } } &.already-answer { border-right: 60px solid $success-bg; - &:after { + &::after { color: $color-success; - content: "\59"; + content: '\59'; } } } @@ -1421,12 +1455,12 @@ ul.ballot-list { .dates { color: $text-medium; font-size: $small-font-size; - margin-bottom: $line-height/2; + margin-bottom: $line-height / 2; } h4 { font-size: rem-calc(30); - line-height: $line-height*1.5; + line-height: $line-height * 1.5; a { color: $text; @@ -1434,11 +1468,11 @@ ul.ballot-list { } } -h2.questions-callout { +.questions-callout { font-size: $base-font-size; } -h3.section-title-divider { +.section-title-divider { border-bottom: 2px solid $brand; color: $brand; margin-bottom: $line-height; @@ -1459,23 +1493,23 @@ h3.section-title-divider { .poll-question-answers { .button { - margin-right: $line-height/4; + margin-right: $line-height / 4; min-width: rem-calc(168); &.answered { - background: #F4F8EC; - border: 2px solid #92BA48; + background: #f4f8ec; + border: 2px solid #92ba48; color: $text; position: relative; - &:after { - background: #92BA48; + &::after { + background: #92ba48; border-radius: rem-calc(20); - content: "\6c"; - color: white; + content: '\6c'; + color: #fff; font-family: "icons" !important; font-size: rem-calc(12); - padding: $line-height/4; + padding: $line-height / 4; position: absolute; right: -6px; top: -6px; diff --git a/app/views/admin/legislation/draft_versions/_form.html.erb b/app/views/admin/legislation/draft_versions/_form.html.erb index af96412a8..5dfd86e33 100644 --- a/app/views/admin/legislation/draft_versions/_form.html.erb +++ b/app/views/admin/legislation/draft_versions/_form.html.erb @@ -79,7 +79,7 @@
<%= f.text_area :body, label: false, placeholder: t('admin.legislation.draft_versions.form.body_placeholder') %>
-
+
diff --git a/app/views/admin/site_customization/content_blocks/index.html.erb b/app/views/admin/site_customization/content_blocks/index.html.erb index e9fc664d1..455ac634f 100644 --- a/app/views/admin/site_customization/content_blocks/index.html.erb +++ b/app/views/admin/site_customization/content_blocks/index.html.erb @@ -6,7 +6,7 @@

<%= t("admin.site_customization.content_blocks.index.title") %>

- +
diff --git a/app/views/admin/site_customization/pages/index.html.erb b/app/views/admin/site_customization/pages/index.html.erb index 7be817acb..18ba5a9b7 100644 --- a/app/views/admin/site_customization/pages/index.html.erb +++ b/app/views/admin/site_customization/pages/index.html.erb @@ -8,7 +8,7 @@ <% if @pages.any? %>

<%= page_entries_info @pages %>

-
<%= t("admin.site_customization.content_blocks.content_block.name") %>
+
diff --git a/app/views/layouts/_admin_header.html.erb b/app/views/layouts/_admin_header.html.erb index 8597b41de..8207ca2f1 100644 --- a/app/views/layouts/_admin_header.html.erb +++ b/app/views/layouts/_admin_header.html.erb @@ -1,4 +1,4 @@ -
+
<%= t("admin.site_customization.pages.page.title") %>
+
diff --git a/app/views/sandbox/admin_legislation_draft_edit.html.erb b/app/views/sandbox/admin_legislation_draft_edit.html.erb index 3443fdddc..3ca991c1d 100644 --- a/app/views/sandbox/admin_legislation_draft_edit.html.erb +++ b/app/views/sandbox/admin_legislation_draft_edit.html.erb @@ -4,7 +4,7 @@ Volver - +

Licencias urbanísticas, declaraciones responsables y comunicaciones previas

- +
From 65fa1209297d5056134afe4fb134a992cd8b32bf Mon Sep 17 00:00:00 2001 From: decabeza Date: Fri, 16 Jun 2017 12:22:21 +0200 Subject: [PATCH 153/155] adds scss-lint to readme files --- README.md | 7 ++++++- README_ES.md | 6 ++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6a1bb19ac..bdc528de5 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,6 @@ RAILS_ENV=test rake db:setup Run the app locally: ``` bin/rails s - ``` Prerequisites for testing: install PhantomJS >= 1.9.8 @@ -58,6 +57,12 @@ Run the tests with: bin/rspec ``` +If you add SCSS code you can check it with: + +``` +scss-lint +``` + You can use the default admin user from the seeds file: **user:** admin@consul.dev diff --git a/README_ES.md b/README_ES.md index bbe821608..dd6c23ee6 100644 --- a/README_ES.md +++ b/README_ES.md @@ -58,6 +58,12 @@ Para ejecutar los tests: bin/rspec ``` +Si añades código SCSS puedes revisarlo con: + +``` +scss-lint +``` + Puedes usar el usuario administrador por defecto del fichero seeds: **user:** admin@consul.dev From 2e0b17afcf004753ddd588331ab58ccfb6eccd60 Mon Sep 17 00:00:00 2001 From: decabeza Date: Fri, 16 Jun 2017 13:23:11 +0200 Subject: [PATCH 154/155] updates specs --- spec/features/management/proposals_spec.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/features/management/proposals_spec.rb b/spec/features/management/proposals_spec.rb index baa5efcc6..1bf32eeb3 100644 --- a/spec/features/management/proposals_spec.rb +++ b/spec/features/management/proposals_spec.rb @@ -97,7 +97,7 @@ feature 'Proposals' do expect(current_path).to eq(management_proposals_path) - within("#proposals") do + within(".proposals-list") do expect(page).to have_css('.proposal', count: 1) expect(page).to have_content(proposal1.title) expect(page).to have_content(proposal1.summary) @@ -124,7 +124,7 @@ feature 'Proposals' do expect(page).to have_content "#{user.document_number}" end - within("#proposals") do + within(".proposals-list") do expect(page).to have_css('.proposal', count: 2) expect(page).to have_css("a[href='#{management_proposal_path(proposal1)}']", text: proposal1.title) expect(page).to have_content(proposal1.summary) @@ -143,7 +143,7 @@ feature 'Proposals' do click_link "Support proposals" - within("#proposals") do + within(".proposals-list") do find('.in-favor a').click expect(page).to have_content "1 support" @@ -160,7 +160,7 @@ feature 'Proposals' do click_link "Support proposals" - within("#proposals") do + within(".proposals-list") do click_link proposal.title end @@ -205,7 +205,7 @@ feature 'Proposals' do expect(page).to have_selector('.js-order-selector[data-order="confidence_score"]') - within '#proposals' do + within(".proposals-list") do expect('Best proposal').to appear_before('Medium proposal') expect('Medium proposal').to appear_before('Worst proposal') end @@ -217,7 +217,7 @@ feature 'Proposals' do expect(current_url).to include('order=created_at') expect(current_url).to include('page=1') - within '#proposals' do + within(".proposals-list") do expect('Medium proposal').to appear_before('Best proposal') expect('Best proposal').to appear_before('Worst proposal') end From d165116b5e0e00b18f406e2a30c83102cddca41f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Baza=CC=81n?= Date: Fri, 16 Jun 2017 15:19:38 +0200 Subject: [PATCH 155/155] fixes draft spec --- spec/features/admin/legislation/draft_versions_spec.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/spec/features/admin/legislation/draft_versions_spec.rb b/spec/features/admin/legislation/draft_versions_spec.rb index 439d18d0c..6f33c265f 100644 --- a/spec/features/admin/legislation/draft_versions_spec.rb +++ b/spec/features/admin/legislation/draft_versions_spec.rb @@ -86,15 +86,18 @@ feature 'Admin legislation draft versions' do click_link 'Version 1' + fill_in 'legislation_draft_version_title', with: 'Version 1b' + click_link 'Launch text editor' - fill_in 'legislation_draft_version_title', with: 'Version 1b' fill_in 'legislation_draft_version_body', with: '# Version 1 body\r\n\r\nParagraph\r\n\r\n>Quote' within('.fullscreen') do - click_button 'Save changes' + click_link 'Close text editor' end + click_button 'Save changes' + expect(page).to have_content 'Version 1b' end end
Nombre