From 278e15dbd4c0c6d9fb514cd1a803a34b6f0d05a6 Mon Sep 17 00:00:00 2001 From: Jay Joshua <7008757+jayjay-w@users.noreply.github.com> Date: Wed, 3 Jul 2024 17:14:58 +0200 Subject: [PATCH] Support importing original claim with fact check (#1928) * Support importing original claim along with fact-check This update enhances the Check API by allowing the `createProjectMedia` GraphQL mutation to import original claims alongside fact-checks. The new functionality supports creating media from file URLs, regular URLs, and text. A new argument, `set_original_claim`, is introduced to handle the original claim data. - Added `set_original_claim` argument to the `Create` mutation in `project_media_mutations.rb`. - Declared `set_original_claim` as an accessor in `project_media.rb`. - Added `create_original_claim` callback before validation on create in `project_media.rb`. - Implemented `create_original_claim` method in `project_media_creators.rb` to handle URLs (creating `UploadedImage`, `UploadedVideo`, `UploadedAudio`, or `Link` media) and text (creating `Claim` media). - Added unit tests in `project_media_test.rb` to cover all media types: link, image, video, audio, and text. * Increase code coverage and fix whitespace issues Increase code coverage and fix whitespace issues flagged by CodeQL * Update GraphQL relay schema Update GraphQL relay schema * Add reviewer feedback * Split original claim tests into multiple tests * Print test name during setup and teardown * Move original claim tests to new files Move original claim tests to new files. Created graphql_controller_11_test.rb and project_media_7_test.rb --- .../mutations/project_media_mutations.rb | 1 + app/models/concerns/project_media_creators.rb | 58 +++++++++ app/models/project_media.rb | 3 +- lib/relay.idl | 1 + public/relay.json | 12 ++ .../controllers/graphql_controller_11_test.rb | 118 ++++++++++++++++++ test/models/project_media_7_test.rb | 70 +++++++++++ 7 files changed, 262 insertions(+), 1 deletion(-) create mode 100644 test/controllers/graphql_controller_11_test.rb create mode 100644 test/models/project_media_7_test.rb diff --git a/app/graph/mutations/project_media_mutations.rb b/app/graph/mutations/project_media_mutations.rb index 7fc4410523..179b13c252 100644 --- a/app/graph/mutations/project_media_mutations.rb +++ b/app/graph/mutations/project_media_mutations.rb @@ -45,6 +45,7 @@ class Create < Mutations::CreateMutation argument :set_tags, JsonStringType, required: false, camelize: false argument :set_title, GraphQL::Types::String, required: false, camelize: false argument :set_status, GraphQL::Types::String, required: false, camelize: false # Status identifier (for example, "in_progress") + argument :set_original_claim, GraphQL::Types::String, required: false, camelize: false end class Update < Mutations::UpdateMutation diff --git a/app/models/concerns/project_media_creators.rb b/app/models/concerns/project_media_creators.rb index 402c111a6c..1c8e1dbcaf 100644 --- a/app/models/concerns/project_media_creators.rb +++ b/app/models/concerns/project_media_creators.rb @@ -1,4 +1,6 @@ require 'active_support/concern' +require 'open-uri' +require 'uri' module ProjectMediaCreators extend ActiveSupport::Concern @@ -28,6 +30,62 @@ def create_annotation end end + def create_original_claim + claim = self.set_original_claim.strip + if claim.match?(/\A#{URI::DEFAULT_PARSER.make_regexp(['http', 'https'])}\z/) + uri = URI.parse(claim) + content_type = fetch_content_type(uri) + + case content_type + when /^image\// + self.media = create_media_from_url('UploadedImage', claim) + when /^video\// + self.media = create_media_from_url('UploadedVideo', claim) + when /^audio\// + self.media = create_media_from_url('UploadedAudio', claim) + else + self.media = create_link_media(claim) + end + else + self.media = create_claim_media(claim) + end + end + + def fetch_content_type(uri) + response = Net::HTTP.get_response(uri) + response['content-type'] + end + + def create_media_from_url(type, url) + klass = type.constantize + file = download_file(url) + m = klass.new + m.file = file + m.save! + m + end + + def download_file(url) + raise "Invalid URL when creating media from original claim attribute" unless url =~ /\A#{URI::DEFAULT_PARSER.make_regexp(['http', 'https'])}\z/ + + file = Tempfile.new(['download', File.extname(url)]) + file.binmode + file.write(URI(url).open.read) + file.rewind + file + end + + def create_claim_media(text) + Claim.create!(quote: text) + end + + def create_link_media(url) + team = self.team || Team.current + pender_key = team.get_pender_key if team + url_from_pender = Link.normalized(url, pender_key) + Link.find_by(url: url_from_pender) || Link.create!(url: url, pender_key: pender_key) + end + def set_quote_metadata media = self.media case media.type diff --git a/app/models/project_media.rb b/app/models/project_media.rb index 5565d19ca8..5d8ac6e26e 100644 --- a/app/models/project_media.rb +++ b/app/models/project_media.rb @@ -1,5 +1,5 @@ class ProjectMedia < ApplicationRecord - attr_accessor :quote, :quote_attributions, :file, :media_type, :set_annotation, :set_tasks_responses, :previous_project_id, :cached_permissions, :is_being_created, :related_to_id, :skip_rules, :set_claim_description, :set_claim_context, :set_fact_check, :set_tags, :set_title, :set_status + attr_accessor :quote, :quote_attributions, :file, :media_type, :set_annotation, :set_tasks_responses, :previous_project_id, :cached_permissions, :is_being_created, :related_to_id, :skip_rules, :set_claim_description, :set_claim_context, :set_fact_check, :set_tags, :set_title, :set_status, :set_original_claim belongs_to :media has_one :claim_description @@ -32,6 +32,7 @@ class ProjectMedia < ApplicationRecord validates_presence_of :custom_title, if: proc { |pm| pm.title_field == 'custom_title' } before_validation :set_team_id, :set_channel, :set_project_id, on: :create + before_validation :create_original_claim, if: proc { |pm| pm.set_original_claim.present? }, on: :create after_create :create_annotation, :create_metrics_annotation, :send_slack_notification, :create_relationship, :create_team_tasks, :create_claim_description_and_fact_check, :create_tags after_create :add_source_creation_log, unless: proc { |pm| pm.source_id.blank? } after_commit :apply_rules_and_actions_on_create, :set_quote_metadata, :notify_team_bots_create, on: [:create] diff --git a/lib/relay.idl b/lib/relay.idl index 806e05e3cc..25e0255a5f 100644 --- a/lib/relay.idl +++ b/lib/relay.idl @@ -2671,6 +2671,7 @@ input CreateProjectMediaInput { set_annotation: String set_claim_description: String set_fact_check: JsonStringType + set_original_claim: String set_status: String set_tags: JsonStringType set_tasks_responses: JsonStringType diff --git a/public/relay.json b/public/relay.json index e69599cb62..458c1b5863 100644 --- a/public/relay.json +++ b/public/relay.json @@ -16109,6 +16109,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "set_original_claim", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "clientMutationId", "description": "A unique identifier for the client performing the mutation.", diff --git a/test/controllers/graphql_controller_11_test.rb b/test/controllers/graphql_controller_11_test.rb new file mode 100644 index 0000000000..417a3c7fff --- /dev/null +++ b/test/controllers/graphql_controller_11_test.rb @@ -0,0 +1,118 @@ +require_relative '../test_helper' + +class GraphqlController11Test < ActionController::TestCase + def setup + @controller = Api::V1::GraphqlController.new + TestDynamicAnnotationTables.load! + + @u = create_user + @t = create_team + create_team_user team: @t, user: @u, role: 'admin' + end + + def teardown + User.unstub(:current) + Team.unstub(:current) + User.current = nil + Team.current = nil + end + + test "should create media with various types using set_original_claim" do + p = create_project team: @t + authenticate_with_user(@u) + + # Test for creating media with plain text original claim + query_plain_text = <<~GRAPHQL + mutation { + createProjectMedia(input: { project_id: #{p.id}, set_original_claim: "This is an original claim" }) { + project_media { + id + } + } + } + GRAPHQL + + post :create, params: { query: query_plain_text, team: @t.slug } + assert_response :success + data = JSON.parse(response.body)['data']['createProjectMedia'] + assert_not_nil data['project_media']['id'] + + # Prepare mock responses for URLs + pender_url = CheckConfig.get('pender_url_private') + '/api/medias' + url_types = { + audio: 'http://example.com/audio.mp3', + image: 'http://example.com/image.png', + video: 'http://example.com/video.mp4', + generic: 'http://example.com' + } + + url_types.each do |type, url| + response_body = '{"type":"media","data":{"url":"' + url + '","type":"item"}}' + WebMock.stub_request(:get, pender_url).with({ query: { url: url } }).to_return(body: response_body) + end + + # Test for creating media with audio URL original claim + query_audio = <<~GRAPHQL + mutation { + createProjectMedia(input: { project_id: #{p.id}, set_original_claim: "#{url_types[:audio]}" }) { + project_media { + id + } + } + } + GRAPHQL + + post :create, params: { query: query_audio, team: @t.slug } + assert_response :success + data = JSON.parse(response.body)['data']['createProjectMedia'] + assert_not_nil data['project_media']['id'] + + # Test for creating media with image URL original claim + query_image = <<~GRAPHQL + mutation { + createProjectMedia(input: { project_id: #{p.id}, set_original_claim: "#{url_types[:image]}" }) { + project_media { + id + } + } + } + GRAPHQL + + post :create, params: { query: query_image, team: @t.slug } + assert_response :success + data = JSON.parse(response.body)['data']['createProjectMedia'] + assert_not_nil data['project_media']['id'] + + # Test for creating media with video URL original claim + query_video = <<~GRAPHQL + mutation { + createProjectMedia(input: { project_id: #{p.id}, set_original_claim: "#{url_types[:video]}" }) { + project_media { + id + } + } + } + GRAPHQL + + post :create, params: { query: query_video, team: @t.slug } + assert_response :success + data = JSON.parse(response.body)['data']['createProjectMedia'] + assert_not_nil data['project_media']['id'] + + # Test for creating media with generic URL original claim + query_generic = <<~GRAPHQL + mutation { + createProjectMedia(input: { project_id: #{p.id}, set_original_claim: "#{url_types[:generic]}" }) { + project_media { + id + } + } + } + GRAPHQL + + post :create, params: { query: query_generic, team: @t.slug } + assert_response :success + data = JSON.parse(response.body)['data']['createProjectMedia'] + assert_not_nil data['project_media']['id'] + end +end diff --git a/test/models/project_media_7_test.rb b/test/models/project_media_7_test.rb new file mode 100644 index 0000000000..c96ca0a7cc --- /dev/null +++ b/test/models/project_media_7_test.rb @@ -0,0 +1,70 @@ +require_relative '../test_helper' +require 'tempfile' + +class ProjectMedia7Test < ActiveSupport::TestCase + def setup + require 'sidekiq/testing' + Sidekiq::Testing.fake! + super + create_team_bot login: 'keep', name: 'Keep' + create_verification_status_stuff + end + + test "should create media from original claim URL as Link" do + setup_elasticsearch + + # Mock Pender response for Link + link_url = 'https://example.com' + pender_url = CheckConfig.get('pender_url_private') + '/api/medias' + link_response = { + type: 'media', + data: { + url: link_url, + type: 'item' + } + }.to_json + WebMock.stub_request(:get, pender_url).with(query: { url: link_url }).to_return(body: link_response) + pm_link = create_project_media(set_original_claim: link_url) + assert_equal 'Link', pm_link.media.type + assert_equal link_url, pm_link.media.url + end + + test "should create media from original claim URL as UploadedImage" do + Tempfile.create(['test_image', '.jpg']) do |file| + file.write(File.read(File.join(Rails.root, 'test', 'data', 'rails.png'))) + file.rewind + image_url = "http://example.com/#{file.path.split('/').last}" + WebMock.stub_request(:get, image_url).to_return(body: file.read, headers: { 'Content-Type' => 'image/jpeg' }) + pm_image = create_project_media(set_original_claim: image_url) + assert_equal 'UploadedImage', pm_image.media.type + end + end + + test "should create media from original claim URL as UploadedVideo" do + Tempfile.create(['test_video', '.mp4']) do |file| + file.write(File.read(File.join(Rails.root, 'test', 'data', 'rails.mp4'))) + file.rewind + video_url = "http://example.com/#{file.path.split('/').last}" + WebMock.stub_request(:get, video_url).to_return(body: file.read, headers: { 'Content-Type' => 'video/mp4' }) + pm_video = create_project_media(set_original_claim: video_url) + assert_equal 'UploadedVideo', pm_video.media.type + end + end + + test "should create media from original claim URL as UploadedAudio" do + Tempfile.create(['test_audio', '.mp3']) do |file| + file.write(File.read(File.join(Rails.root, 'test', 'data', 'rails.mp3'))) + file.rewind + audio_url = "http://example.com/#{file.path.split('/').last}" + WebMock.stub_request(:get, audio_url).to_return(body: file.read, headers: { 'Content-Type' => 'audio/mp3' }) + pm_audio = create_project_media(set_original_claim: audio_url) + assert_equal 'UploadedAudio', pm_audio.media.type + end + end + + test "should create media from original claim text as Claim" do + pm_claim = create_project_media(set_original_claim: 'This is a claim.') + assert_equal 'Claim', pm_claim.media.type + assert_equal 'This is a claim.', pm_claim.media.quote + end +end