diff --git a/.codeclimate.yml b/.codeclimate.yml index 9534d44f17..773f85a550 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -46,7 +46,7 @@ plugins: config: languages: ruby: - mass_threshold: 30 + mass_threshold: 36 bundler-audit: enabled: true exclude_patterns: diff --git a/app/graph/mutations/destroy_mutation.rb b/app/graph/mutations/destroy_mutation.rb index 2eb350d157..18668f4fcf 100644 --- a/app/graph/mutations/destroy_mutation.rb +++ b/app/graph/mutations/destroy_mutation.rb @@ -19,6 +19,8 @@ def define_behavior(subclass, mutation_target, parents_mapping) subclass.define_method :resolve do |**inputs| ::GraphqlCrudOperations.destroy(inputs, context, parents_mapping) end + + type_class end end end diff --git a/app/graph/mutations/duplicate_team_mutation.rb b/app/graph/mutations/duplicate_team_mutation.rb index 52777b736b..b4d9a3dafd 100644 --- a/app/graph/mutations/duplicate_team_mutation.rb +++ b/app/graph/mutations/duplicate_team_mutation.rb @@ -10,9 +10,7 @@ def resolve(team_id:, custom_slug: nil, custom_name: nil) user = User.current ability = Ability.new(user) team = GraphqlCrudOperations.load_if_can(Team, id, context) - if ability.cannot?(:duplicate, team) - raise I18n.t("team_clone.user_not_authorized") - end + raise I18n.t("team_clone.user_not_authorized") if ability.cannot?(:duplicate, team) new_team = Team.duplicate( team, diff --git a/app/graph/mutations/graphql_crud_operations.rb b/app/graph/mutations/graphql_crud_operations.rb index 700a281a6a..d4f5ef209e 100644 --- a/app/graph/mutations/graphql_crud_operations.rb +++ b/app/graph/mutations/graphql_crud_operations.rb @@ -1,8 +1,6 @@ class GraphqlCrudOperations def self.safe_save(obj, attrs, parent_names = []) - if User.current.nil? && ApiKey.current.nil? - raise "This operation must be done by a signed-in user" - end + raise 'This operation must be done by a signed-in user' if User.current.nil? && ApiKey.current.nil? attrs.each do |key, value| method = key == "clientMutationId" ? "client_mutation_id=" : "#{key}=" obj.send(method, value) if obj.respond_to?(method) @@ -96,7 +94,7 @@ def self.create(type, inputs, ctx, parents_mapping = {}) self.safe_save(obj, attrs, parents_mapping.keys) end - def self.update_from_single_id(graphql_id, obj, inputs, ctx, parent_names) + def self.update_from_single_id(_graphql_id, obj, inputs, ctx, parent_names) obj.file = ctx[:file] unless ctx[:file].blank? attrs = inputs.keys.inject({}) do |memo, key| diff --git a/app/graph/types/default_object.rb b/app/graph/types/default_object.rb index 8de44c4e95..9e954a6971 100644 --- a/app/graph/types/default_object.rb +++ b/app/graph/types/default_object.rb @@ -6,7 +6,7 @@ def inherited(subclass) subclass.global_id_field :id end end - + field :permissions, GraphQL::Types::String, null: true def permissions diff --git a/app/graph/types/project_media_type.rb b/app/graph/types/project_media_type.rb index cf757cbf8a..aaed722128 100644 --- a/app/graph/types/project_media_type.rb +++ b/app/graph/types/project_media_type.rb @@ -172,6 +172,12 @@ def log(event_types: nil, field_names: nil, annotation_types: nil, who_dunnit: n object.get_versions_log(event_types, field_names, annotation_types, who_dunnit, include_related) end + field :flags, FlagType.connection_type, null: true + + def flags + object.get_annotations('flag').map(&:load) + end + field :tags, TagType.connection_type, null: true def tags @@ -254,18 +260,18 @@ def assignments(user_id:, annotation_type:) end DynamicAnnotation::AnnotationType.pluck(:annotation_type).each do |type| - field "dynamic_annotations_#{type}".to_sym, DynamicType.connection_type, null: true + field "dynamic_annotations_#{type}".to_sym, DynamicType.connection_type, null: true - define_method("dynamic_annotations_#{type}".to_sym) do |**_inputs| - object.get_annotations(type) - end + define_method("dynamic_annotations_#{type}".to_sym) do |**_inputs| + object.get_annotations(type) + end - field "dynamic_annotation_#{type}".to_sym, DynamicType, null: true + field "dynamic_annotation_#{type}".to_sym, DynamicType, null: true - define_method("dynamic_annotation_#{type}".to_sym) do |**_inputs| - object.get_dynamic_annotation(type) - end + define_method("dynamic_annotation_#{type}".to_sym) do |**_inputs| + object.get_dynamic_annotation(type) end + end field :suggested_similar_relationships, RelationshipType.connection_type, null: true diff --git a/app/graph/types/public_team_type.rb b/app/graph/types/public_team_type.rb index e6026c5177..a7c9794d66 100644 --- a/app/graph/types/public_team_type.rb +++ b/app/graph/types/public_team_type.rb @@ -38,14 +38,6 @@ def spam_count private def archived_count(team) - team.private && - (!User.current || - (!User.current.is_admin && - TeamUser - .where(team_id: team.id, user_id: User.current.id) - .last - .nil? - ) - ) + team.private && (!User.current || (!User.current.is_admin && TeamUser.where(team_id: team.id, user_id: User.current.id).last.nil?)) end end diff --git a/app/graph/types/query_type.rb b/app/graph/types/query_type.rb index 773611f310..09d8ac04bc 100644 --- a/app/graph/types/query_type.rb +++ b/app/graph/types/query_type.rb @@ -79,7 +79,7 @@ def me argument :random, GraphQL::Types::String, required: false end - def team(id: nil, slug: nil, random: nil) + def team(id: nil, slug: nil, _random: nil) tid = id.to_i if !slug.blank? team = Team.where(slug: slug).first diff --git a/app/graph/types/source_type.rb b/app/graph/types/source_type.rb index f602fd2757..c9932e24d1 100644 --- a/app/graph/types/source_type.rb +++ b/app/graph/types/source_type.rb @@ -29,7 +29,7 @@ def account_sources field :medias, ProjectMediaType.connection_type, null: true def medias - object.media + object.medias end field :medias_count, GraphQL::Types::Int, null: true diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 53bd0a5839..fa9e4212ce 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -15,15 +15,15 @@ def get_from_and_to_values(values, tz) if ['less_than', 'more_than'].include?(condition_type) period = values.dig('period').to_i period_date = case values.dig('period_type').downcase - when 'd' - Time.now - period.day - when 'w' - Time.now - period.week - when 'm' - Time.now - period.month - when 'y' - Time.now - period.year - end + when 'd' + Time.now - period.day + when 'w' + Time.now - period.week + when 'm' + Time.now - period.month + when 'y' + Time.now - period.year + end condition_date = period_date.blank? ? nil : format_time_with_timezone(period_date.to_s, tz) if condition_type == 'less_than' from = condition_date diff --git a/lib/relay.idl b/lib/relay.idl index 29b3ca7eb9..1112349207 100644 --- a/lib/relay.idl +++ b/lib/relay.idl @@ -8532,6 +8532,42 @@ type Flag implements Node { version: Version } +""" +The connection type for Flag. +""" +type FlagConnection { + """ + A list of edges. + """ + edges: [FlagEdge] + + """ + A list of nodes. + """ + nodes: [Flag] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + totalCount: Int +} + +""" +An edge in a connection. +""" +type FlagEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Flag +} + """ Autogenerated input type of GenerateTwoFactorBackupCodes """ @@ -11100,6 +11136,27 @@ type ProjectMedia implements Node { ): DynamicConnection feed_columns_values: JsonStringType field_value(annotation_type_field_name: String!): String + flags( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): FlagConnection full_url: String id: ID! is_confirmed: Boolean diff --git a/public/relay.json b/public/relay.json index d24f262c4b..08a1dd63dd 100644 --- a/public/relay.json +++ b/public/relay.json @@ -46592,6 +46592,132 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "FlagConnection", + "description": "The connection type for Flag.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "FlagEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Flag", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "totalCount", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "FlagEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "Flag", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "GenerateTwoFactorBackupCodesInput", @@ -58297,6 +58423,67 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "flags", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "FlagConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "full_url", "description": null, diff --git a/test/controllers/graphql_controller_10_test.rb b/test/controllers/graphql_controller_10_test.rb index f29256787a..36a8f0d76e 100644 --- a/test/controllers/graphql_controller_10_test.rb +++ b/test/controllers/graphql_controller_10_test.rb @@ -72,21 +72,22 @@ def setup assert_equal 'foo', data['tag_texts']['edges'][0]['node']['text'] end - test "should get team team_users" do + test "should get team users and bots" do t = create_team u = create_user u2 = create_user + u3 = create_bot_user team: t create_team_user team: t, user: u, role: 'admin' create_team_user team: t, user: u2 authenticate_with_user(u) - query = "query GetById { team(id: \"#{t.id}\") { team_users { edges { node { user { dbid } } } } } }" + query = "query GetById { team(id: \"#{t.id}\") { team_users { edges { node { user { dbid, get_send_email_notifications, get_send_successful_login_notifications, get_send_failed_login_notifications, source { medias(first: 1) { edges { node { id } } } }, annotations(first: 1) { edges { node { id } } }, team_users(first: 1) { edges { node { id } } }, bot { get_description, get_role, get_version, get_source_code_url } } } } } } }" post :create, params: { query: query, team: t.slug } assert_response :success data = JSON.parse(@response.body)['data']['team']['team_users']['edges'] ids = data.collect{ |i| i['node']['user']['dbid'] } - assert_equal 2, data.size - assert_equal [u.id, u2.id], ids.sort + assert_equal 3, data.size + assert_equal [u.id, u2.id, u3.id], ids.sort end test "should get team tasks" do diff --git a/test/controllers/graphql_controller_5_test.rb b/test/controllers/graphql_controller_5_test.rb index fd4468b83a..834f756074 100644 --- a/test/controllers/graphql_controller_5_test.rb +++ b/test/controllers/graphql_controller_5_test.rb @@ -72,7 +72,7 @@ def setup pm = create_project_media team: t authenticate_with_user(u) # verify create - query = 'mutation create { createDynamic(input: { annotation_type: "flag", clientMutationId: "1", annotated_type: "ProjectMedia", annotated_id: "' + pm.id.to_s + '", set_fields: "{\"flags\":{\"adult\":3,\"spoof\":2,\"medical\":1,\"violence\":3,\"racy\":4,\"spam\":0},\"show_cover\":false}" }) { dynamic { dbid } } }' + query = 'mutation create { createDynamic(input: { annotation_type: "flag", clientMutationId: "1", annotated_type: "ProjectMedia", annotated_id: "' + pm.id.to_s + '", set_fields: "{\"flags\":{\"adult\":3,\"spoof\":2,\"medical\":1,\"violence\":3,\"racy\":4,\"spam\":0},\"show_cover\":false}" }) { dynamic { dbid, id } } }' post :create, params: { query: query, team: t.slug } assert_response :success assert JSON.parse(@response.body).dig('errors').blank? diff --git a/test/controllers/graphql_controller_8_test.rb b/test/controllers/graphql_controller_8_test.rb index 70dd52ea81..3c4034fdb9 100644 --- a/test/controllers/graphql_controller_8_test.rb +++ b/test/controllers/graphql_controller_8_test.rb @@ -207,7 +207,7 @@ def setup t = create_team private: true create_team_user(user: u, team: t) f = create_feed - query = "query { team(slug: \"#{t.slug}\") { feed(dbid: #{f.id}) { current_feed_team { dbid } } } }" + query = "query { team(slug: \"#{t.slug}\") { feed(dbid: #{f.id}) { current_feed_team { dbid, requests_filters } } } }" post :create, params: { query: query, team: t.slug } assert_nil JSON.parse(@response.body).dig('data', 'team', 'feed') @@ -537,6 +537,7 @@ def setup id trash_count unconfirmed_count + spam_count } search { id @@ -870,4 +871,13 @@ def setup assert_response :success assert_nil JSON.parse(@response.body)['data']['dynamic_annotation_field'] end + + test "should get team settings fields" do + u = create_user is_admin: true + authenticate_with_user(u) + t = create_team + fields = %w(get_slack_notifications_enabled get_slack_webhook get_embed_whitelist get_report_design_image_template get_status_target_turnaround get_rules get_languages get_language get_report get_data_report_url get_outgoing_urls_utm_code get_shorten_outgoing_urls) + post :create, params: { query: "query Team { team { join_requests(first: 10) { edges { node { id } } }, #{fields.join(', ')} } }", team: t.slug } + assert_response :success + end end diff --git a/test/controllers/graphql_controller_test.rb b/test/controllers/graphql_controller_test.rb index 179bc3bc6a..512244115a 100644 --- a/test/controllers/graphql_controller_test.rb +++ b/test/controllers/graphql_controller_test.rb @@ -127,6 +127,16 @@ def setup assert_graphql_create('project_media', { media_id: m.id, project_id: p.id }) end + test "should read project media flag and source" do + u = create_user is_admin: true + authenticate_with_user(u) + pm = create_project_media + create_flag annotated: pm + query = "query GetById { project_media(ids: \"#{pm.id},nil,#{pm.team_id}\") { source { id }, flags(first: 10) { edges { node { id } } }, annotation(annotation_type: \"flag\") { permissions, medias(first: 1) { edges { node { id } } } project_media { id } }, annotations(annotation_type: \"flag\") { edges { node { ... on Flag { id } } } } } }" + post :create, params: { query: query, team: pm.team.slug } + assert_response :success + end + test "should read project medias" do authenticate_with_user p = create_project team: @team @@ -906,4 +916,12 @@ def setup assert_response :success end + test "should have a base interface for GraphQL types" do + assert_nothing_raised do + class TestType < BaseObject + implements BaseInterface + field :test, ::BaseEnum.connection_type, null: false + end + end + end end diff --git a/test/models/project_test.rb b/test/models/project_test.rb index 1ca951b413..39362aa68d 100644 --- a/test/models/project_test.rb +++ b/test/models/project_test.rb @@ -680,4 +680,10 @@ def setup end end end + + test "should be inactive if team is inactive" do + t = create_team inactive: true + p = create_project team: t + assert p.inactive + end end