From 74b2c0beea74057c85ae0e94f8b65bd4736a1f37 Mon Sep 17 00:00:00 2001 From: Sawy Date: Thu, 18 Jun 2026 17:54:12 +0300 Subject: [PATCH 1/3] CV2-6653: validate article fields length --- app/models/concerns/article.rb | 12 +++++++++++- config/locales/en.yml | 1 + 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/app/models/concerns/article.rb b/app/models/concerns/article.rb index de3371085..40b1ef1c9 100644 --- a/app/models/concerns/article.rb +++ b/app/models/concerns/article.rb @@ -14,9 +14,11 @@ module Article before_validation :set_author, on: :create before_validation :set_user before_validation :set_channel, on: :create, unless: -> { self.is_a?(ClaimDescription) } - validates_presence_of :user + validates_presence_of :user validates :channel, inclusion: { in: ARTICLE_CHANNELS.keys }, unless: -> { self.is_a?(ClaimDescription) } + validate :max_article_fields_length + enum channel: ARTICLE_CHANNELS after_commit :update_elasticsearch_data, :send_to_alegre, :notify_bots, on: [:create, :update] @@ -66,6 +68,14 @@ def notify_bots BotUser.enqueue_event(event, self.project_media.team_id, self) unless self.project_media.nil? end + private + + def max_article_fields_length + max_length = self.is_a(Explainer) ? CheckConfig.get(:explainer_max_fields_length, 4096, :integer) : CheckConfig.get(:fact_check_max_fields_length, 900, :integer) + length = [self.title, self.summary, self.url].compact.sum(&:length) + errors.add(:base, I18n.t(:"errors.messages.article_character_limit_reached")) if length > max_length + end + protected def index_in_elasticsearch(pm_id, data) diff --git a/config/locales/en.yml b/config/locales/en.yml index 5d44bf80b..5a4508b5e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -114,6 +114,7 @@ en: invalid_feed_saved_search_list_type: The saved search list type is invalid. platform_allowed_values_error: 'cannot be of type %{type}, allowed types: %{allowed_types}' restrict_registration_to_invited_users_only: 'Looks like you don’t have an invitation yet. Please request one from your workspace.' + article_character_limit_reached: Character Limit Reached. activerecord: models: link: Link From d7ccc1a71566c22d955f3c09987b36f77a6ca91a Mon Sep 17 00:00:00 2001 From: Sawy Date: Thu, 18 Jun 2026 18:26:09 +0300 Subject: [PATCH 2/3] CV2-6653: add unit tests --- app/models/concerns/article.rb | 12 +----------- app/models/explainer.rb | 7 ++++++- app/models/fact_check.rb | 7 ++++++- test/models/explainer_test.rb | 13 +++++++++++++ test/models/fact_check_test.rb | 13 +++++++++++++ 5 files changed, 39 insertions(+), 13 deletions(-) diff --git a/app/models/concerns/article.rb b/app/models/concerns/article.rb index 40b1ef1c9..de3371085 100644 --- a/app/models/concerns/article.rb +++ b/app/models/concerns/article.rb @@ -14,11 +14,9 @@ module Article before_validation :set_author, on: :create before_validation :set_user before_validation :set_channel, on: :create, unless: -> { self.is_a?(ClaimDescription) } - validates_presence_of :user - validates :channel, inclusion: { in: ARTICLE_CHANNELS.keys }, unless: -> { self.is_a?(ClaimDescription) } - validate :max_article_fields_length + validates :channel, inclusion: { in: ARTICLE_CHANNELS.keys }, unless: -> { self.is_a?(ClaimDescription) } enum channel: ARTICLE_CHANNELS after_commit :update_elasticsearch_data, :send_to_alegre, :notify_bots, on: [:create, :update] @@ -68,14 +66,6 @@ def notify_bots BotUser.enqueue_event(event, self.project_media.team_id, self) unless self.project_media.nil? end - private - - def max_article_fields_length - max_length = self.is_a(Explainer) ? CheckConfig.get(:explainer_max_fields_length, 4096, :integer) : CheckConfig.get(:fact_check_max_fields_length, 900, :integer) - length = [self.title, self.summary, self.url].compact.sum(&:length) - errors.add(:base, I18n.t(:"errors.messages.article_character_limit_reached")) if length > max_length - end - protected def index_in_elasticsearch(pm_id, data) diff --git a/app/models/explainer.rb b/app/models/explainer.rb index f72334326..6805d1f49 100644 --- a/app/models/explainer.rb +++ b/app/models/explainer.rb @@ -12,7 +12,7 @@ class Explainer < ApplicationRecord before_validation :set_team, :set_language validates_format_of :url, with: URI.regexp, allow_blank: true, allow_nil: true validates_presence_of :team, :title, :description - validate :language_in_allowed_values + validate :language_in_allowed_values, :max_fields_length after_save :update_paragraphs_in_alegre after_update :detach_explainer_if_trashed @@ -179,6 +179,11 @@ def language_in_allowed_values errors.add(:language, I18n.t(:"errors.messages.invalid_article_language_value")) unless allowed_languages.include?(self.language) end + def max_fields_length + length = [self.title, self.description, self.url].compact.sum(&:length) + errors.add(:base, I18n.t(:"errors.messages.article_character_limit_reached")) if length > CheckConfig.get(:explainer_max_fields_length, 4096, :integer) + end + def detach_explainer_if_trashed if self.trashed && !self.trashed_before_last_save self.project_medias = [] diff --git a/app/models/fact_check.rb b/app/models/fact_check.rb index f7d080c54..81dba3833 100644 --- a/app/models/fact_check.rb +++ b/app/models/fact_check.rb @@ -19,7 +19,7 @@ class FactCheck < ApplicationRecord validates_presence_of :claim_description validates_uniqueness_of :claim_description_id validates_format_of :url, with: URI.regexp, allow_blank: true, allow_nil: true - validate :language_in_allowed_values, :title_or_summary_exists, :rating_in_allowed_values + validate :language_in_allowed_values, :title_or_summary_exists, :rating_in_allowed_values, :max_fields_length before_save :clean_fact_check_tags after_save :update_report, unless: proc { |fc| fc.skip_report_update || !DynamicAnnotation::AnnotationType.where(annotation_type: 'report_design').exists? || fc.project_media.blank? } @@ -128,6 +128,11 @@ def title_or_summary_exists errors.add(:base, I18n.t(:"errors.messages.fact_check_empty_title_and_summary")) if self.title.blank? && self.summary.blank? end + def max_fields_length + length = [self.title, self.summary, self.url].compact.sum(&:length) + errors.add(:base, I18n.t(:"errors.messages.article_character_limit_reached")) if length > CheckConfig.get(:fact_check_max_fields_length, 900, :integer) + end + def update_report pm = self.project_media reports = pm.get_dynamic_annotation('report_design') || Dynamic.new(annotation_type: 'report_design', annotated: pm) diff --git a/test/models/explainer_test.rb b/test/models/explainer_test.rb index 4df10544d..4b2e02e50 100644 --- a/test/models/explainer_test.rb +++ b/test/models/explainer_test.rb @@ -48,6 +48,19 @@ def teardown end end + test "should validate max length" do + stub_configs({ 'explainer_max_fields_length' => 20 }) do + assert_difference 'Explainer.count' do + create_explainer title: random_string(5), description: random_string(12), url: nil + end + assert_no_difference 'Explainer.count' do + assert_raises ActiveRecord::RecordInvalid do + create_explainer title: random_string(15), description: random_string(12), url: nil + end + end + end + end + test "should belong to user and team" do u = create_user t = create_team diff --git a/test/models/fact_check_test.rb b/test/models/fact_check_test.rb index 9d019dd9a..e887feff9 100644 --- a/test/models/fact_check_test.rb +++ b/test/models/fact_check_test.rb @@ -329,6 +329,19 @@ def setup end end + test "should validate max length" do + stub_configs({ 'fact_check_max_fields_length' => 20 }) do + assert_difference 'FactCheck.count' do + create_fact_check claim_description: create_claim_description, title: random_string(5), summary: random_string(12), url: nil + end + assert_no_difference 'FactCheck.count' do + assert_raises ActiveRecord::RecordInvalid do + create_fact_check claim_description: create_claim_description, title: random_string(15), summary: random_string(12), url: nil + end + end + end + end + test "should create many fact-checks without signature" do assert_difference 'FactCheck.count', 2 do create_fact_check signature: nil, claim_description: create_claim_description From c7facf39e09281ffcef8d100ce55ca7357faf3be Mon Sep 17 00:00:00 2001 From: Sawy Date: Tue, 23 Jun 2026 18:41:09 +0300 Subject: [PATCH 3/3] CV2-6653: add a validation per field --- app/models/concerns/article.rb | 26 ++++++++++++++++++++++++++ app/models/explainer.rb | 6 +----- app/models/fact_check.rb | 6 +----- test/models/explainer_test.rb | 15 +++++++++++++-- test/models/fact_check_test.rb | 19 +++++++++++++++++-- 5 files changed, 58 insertions(+), 14 deletions(-) diff --git a/app/models/concerns/article.rb b/app/models/concerns/article.rb index de3371085..9b4e5be32 100644 --- a/app/models/concerns/article.rb +++ b/app/models/concerns/article.rb @@ -66,6 +66,32 @@ def notify_bots BotUser.enqueue_event(event, self.project_media.team_id, self) unless self.project_media.nil? end + def field_max_length(field, default) + CheckConfig.get(:"article_max_#{field}_length", default, :integer) + end + + def article_get_validation_length_fields + fields = { title: 800, url: 600 } + extra = self.is_a?(FactCheck) ? { summary: 3956 } : { description: 3956 } + fields.merge(extra) + end + + def max_fields_length + article_get_validation_length_fields.each do |field, default| + max = field_max_length(field, default) + length = self[field]&.length || 0 + errors.add(field, I18n.t(:"errors.messages.article_character_limit_reached")) if length > max + end + end + + def truncate_fields_for_bot_user + article_get_validation_length_fields.each do |field, default| + max = field_max_length(field, default) + length = self[field]&.length || 0 + self[field] = self[field][0...max] if length > max + end + end + protected def index_in_elasticsearch(pm_id, data) diff --git a/app/models/explainer.rb b/app/models/explainer.rb index 6805d1f49..f84eeb5c8 100644 --- a/app/models/explainer.rb +++ b/app/models/explainer.rb @@ -10,6 +10,7 @@ class Explainer < ApplicationRecord has_many :project_medias, through: :explainer_items before_validation :set_team, :set_language + before_validation :truncate_fields_for_bot_user, if: proc { |fc| fc.user.is_a?(BotUser) } validates_format_of :url, with: URI.regexp, allow_blank: true, allow_nil: true validates_presence_of :team, :title, :description validate :language_in_allowed_values, :max_fields_length @@ -179,11 +180,6 @@ def language_in_allowed_values errors.add(:language, I18n.t(:"errors.messages.invalid_article_language_value")) unless allowed_languages.include?(self.language) end - def max_fields_length - length = [self.title, self.description, self.url].compact.sum(&:length) - errors.add(:base, I18n.t(:"errors.messages.article_character_limit_reached")) if length > CheckConfig.get(:explainer_max_fields_length, 4096, :integer) - end - def detach_explainer_if_trashed if self.trashed && !self.trashed_before_last_save self.project_medias = [] diff --git a/app/models/fact_check.rb b/app/models/fact_check.rb index 81dba3833..b7587e301 100644 --- a/app/models/fact_check.rb +++ b/app/models/fact_check.rb @@ -15,6 +15,7 @@ class FactCheck < ApplicationRecord before_validation :set_imported, on: :create before_validation :set_claim_description, on: :create, unless: proc { |fc| fc.claim_description.present? } before_validation :set_original_claim_for_published_articles, on: :create, if: proc { |fc| fc.publish_report && fc.set_original_claim.blank? } + before_validation :truncate_fields_for_bot_user, if: proc { |fc| fc.user.is_a?(BotUser) } validates_presence_of :claim_description validates_uniqueness_of :claim_description_id @@ -128,11 +129,6 @@ def title_or_summary_exists errors.add(:base, I18n.t(:"errors.messages.fact_check_empty_title_and_summary")) if self.title.blank? && self.summary.blank? end - def max_fields_length - length = [self.title, self.summary, self.url].compact.sum(&:length) - errors.add(:base, I18n.t(:"errors.messages.article_character_limit_reached")) if length > CheckConfig.get(:fact_check_max_fields_length, 900, :integer) - end - def update_report pm = self.project_media reports = pm.get_dynamic_annotation('report_design') || Dynamic.new(annotation_type: 'report_design', annotated: pm) diff --git a/test/models/explainer_test.rb b/test/models/explainer_test.rb index 4b2e02e50..068c74cb5 100644 --- a/test/models/explainer_test.rb +++ b/test/models/explainer_test.rb @@ -49,15 +49,26 @@ def teardown end test "should validate max length" do - stub_configs({ 'explainer_max_fields_length' => 20 }) do + stub_configs({ 'article_max_title_length' => 20, 'article_max_url_length' => 30, 'article_max_description_length' => 20 }) do assert_difference 'Explainer.count' do create_explainer title: random_string(5), description: random_string(12), url: nil end assert_no_difference 'Explainer.count' do assert_raises ActiveRecord::RecordInvalid do - create_explainer title: random_string(15), description: random_string(12), url: nil + create_explainer title: random_string(15), description: random_string(25), url: random_url end + assert_raises ActiveRecord::RecordInvalid do + create_explainer title: random_string(25), description: random_string(20), url: random_url + end + end + team = create_team + bot_user = create_bot_user team: team, team_author_id: team.id + ex = nil + assert_difference 'Explainer.count' do + ex = create_explainer team: team, user: bot_user, title: random_string(30), description: random_string(30), url: nil end + assert_equal 20, ex.title.length + assert_equal 20, ex.description.length end end diff --git a/test/models/fact_check_test.rb b/test/models/fact_check_test.rb index e887feff9..92e4ee8b3 100644 --- a/test/models/fact_check_test.rb +++ b/test/models/fact_check_test.rb @@ -330,15 +330,30 @@ def setup end test "should validate max length" do - stub_configs({ 'fact_check_max_fields_length' => 20 }) do + stub_configs({ 'article_max_title_length' => 20, 'article_max_url_length' => 30, 'article_max_summary_length' => 20 }) do assert_difference 'FactCheck.count' do create_fact_check claim_description: create_claim_description, title: random_string(5), summary: random_string(12), url: nil end assert_no_difference 'FactCheck.count' do assert_raises ActiveRecord::RecordInvalid do - create_fact_check claim_description: create_claim_description, title: random_string(15), summary: random_string(12), url: nil + create_fact_check claim_description: create_claim_description, title: random_string(15), summary: random_string(25), url: random_url + end + assert_raises ActiveRecord::RecordInvalid do + create_fact_check claim_description: create_claim_description, title: random_string(25), summary: random_string(15), url: random_url + end + end + # should pass for autmation import (user is a Bot) + team = create_team + pm = create_project_media team: team + bot_user = create_bot_user team: team, team_author_id: team.id + fc = nil + with_current_user_and_team(bot_user, team) do + assert_difference 'FactCheck.count' do + fc = create_fact_check claim_description: create_claim_description(project_media: pm), title: random_string(30), summary: random_string(30), url: random_url end end + assert_equal 20, fc.title.length + assert_equal 20, fc.summary.length end end