artists: fix editing invalid urls in artist entries (fix #3720, #3927, #3781)

Convert to an autosave association on urls. This ensures that when we
save the artist we only validate the added urls, not bad urls that we're
trying to remove, and that url validation errors are propagated up to
the artist object.

This also fixes invalid urls being saved in the artist history despite
validation failing (#3720).
This commit is contained in:
evazion
2018-10-04 17:38:44 -05:00
parent c78dece411
commit 03cc3dfa50
4 changed files with 49 additions and 45 deletions

View File

@@ -2,19 +2,17 @@ class Artist < ApplicationRecord
extend Memoist extend Memoist
class RevertError < Exception ; end class RevertError < Exception ; end
attribute :url_string, :string, default: nil attr_accessor :url_string_was
before_validation :normalize_name before_validation :normalize_name
after_save :create_version after_save :create_version
after_save :categorize_tag after_save :categorize_tag
after_save :update_wiki after_save :update_wiki
after_save :save_urls
validates_associated :urls
validates :name, tag_name: true, uniqueness: true validates :name, tag_name: true, uniqueness: true
validate :validate_wiki, :on => :create validate :validate_wiki, :on => :create
after_validation :merge_validation_errors
belongs_to_creator belongs_to_creator
has_many :members, :class_name => "Artist", :foreign_key => "group_name", :primary_key => "name" has_many :members, :class_name => "Artist", :foreign_key => "group_name", :primary_key => "name"
has_many :urls, :dependent => :destroy, :class_name => "ArtistUrl" has_many :urls, :dependent => :destroy, :class_name => "ArtistUrl", :autosave => true
has_many :versions, -> {order("artist_versions.id ASC")}, :class_name => "ArtistVersion" has_many :versions, -> {order("artist_versions.id ASC")}, :class_name => "ArtistVersion"
has_one :wiki_page, :foreign_key => "title", :primary_key => "name" has_one :wiki_page, :foreign_key => "title", :primary_key => "name"
has_one :tag_alias, :foreign_key => "antecedent_name", :primary_key => "name" has_one :tag_alias, :foreign_key => "antecedent_name", :primary_key => "name"
@@ -175,18 +173,24 @@ class Artist < ApplicationRecord
end end
def url_array def url_array
urls.map(&:url) urls.map(&:to_s)
end end
def save_urls def url_string
if url_string && saved_change_to_url_string? url_array.sort.join("\n")
Artist.transaction do end
self.urls.clear
self.urls = url_string.scan(/[^[:space:]]+/).uniq.map do |url| def url_string=(string)
self.urls.find_or_create_by(url: url) self.url_string_was = url_string
end
end self.urls = string.to_s.scan(/[^[:space:]]+/).map do |url|
end is_active, url = ArtistUrl.parse_prefix(url)
self.urls.find_or_initialize_by(url: url, is_active: is_active)
end.uniq(&:url)
end
def url_string_changed?
url_string_was != url_string
end end
def map_domain(x) def map_domain(x)
@@ -253,7 +257,7 @@ class Artist < ApplicationRecord
module VersionMethods module VersionMethods
def create_version(force=false) def create_version(force=false)
if saved_change_to_name? || saved_change_to_url_string? || saved_change_to_is_active? || saved_change_to_is_banned? || saved_change_to_other_names? || saved_change_to_group_name? || saved_change_to_notes? || force if saved_change_to_name? || url_string_changed? || saved_change_to_is_active? || saved_change_to_is_banned? || saved_change_to_other_names? || saved_change_to_group_name? || saved_change_to_notes? || force
if merge_version? if merge_version?
merge_version merge_version
else else
@@ -268,7 +272,7 @@ class Artist < ApplicationRecord
:name => name, :name => name,
:updater_id => CurrentUser.id, :updater_id => CurrentUser.id,
:updater_ip_addr => CurrentUser.ip_addr, :updater_ip_addr => CurrentUser.ip_addr,
:url_string => url_string || url_array.join("\n"), :url_string => url_string,
:is_active => is_active, :is_active => is_active,
:is_banned => is_banned, :is_banned => is_banned,
:other_names => other_names, :other_names => other_names,
@@ -280,7 +284,7 @@ class Artist < ApplicationRecord
prev = versions.last prev = versions.last
prev.update_attributes( prev.update_attributes(
:name => name, :name => name,
:url_string => url_string || url_array.join("\n"), :url_string => url_string,
:is_active => is_active, :is_active => is_active,
:is_banned => is_banned, :is_banned => is_banned,
:other_names => other_names, :other_names => other_names,
@@ -546,13 +550,6 @@ class Artist < ApplicationRecord
extend SearchMethods extend SearchMethods
include ApiMethods include ApiMethods
def merge_validation_errors
errors[:urls].clear
urls.select(&:invalid?).each do |url|
errors[:url] << url.errors.full_messages.join("; ")
end
end
def status def status
if is_banned? && is_active? if is_banned? && is_active?
"Banned" "Banned"

View File

@@ -1,5 +1,4 @@
class ArtistUrl < ApplicationRecord class ArtistUrl < ApplicationRecord
before_validation :parse_prefix
before_validation :initialize_normalized_url, on: :create before_validation :initialize_normalized_url, on: :create
before_validation :normalize before_validation :normalize
validates :url, presence: true, uniqueness: { scope: :artist_id } validates :url, presence: true, uniqueness: { scope: :artist_id }
@@ -9,12 +8,11 @@ class ArtistUrl < ApplicationRecord
scope :url_matches, ->(url) { url_attribute_matches(:url, url) } scope :url_matches, ->(url) { url_attribute_matches(:url, url) }
scope :normalized_url_matches, ->(url) { url_attribute_matches(:normalized_url, url) } scope :normalized_url_matches, ->(url) { url_attribute_matches(:normalized_url, url) }
def self.strip_prefixes(url) def self.parse_prefix(url)
url.sub(/^[-]+/, "") prefix, url = url.match(/\A(-)?(.*)/)[1, 2]
end is_active = prefix.nil?
def self.is_active?(url) [is_active, url]
url !~ /^-/
end end
def self.normalize(url) def self.normalize(url)
@@ -88,14 +86,6 @@ class ArtistUrl < ApplicationRecord
end end
end end
def parse_prefix
case url
when /^-/
self.url = url[1..-1]
self.is_active = false
end
end
def priority def priority
if normalized_url =~ /pixiv\.net\/member\.php/ if normalized_url =~ /pixiv\.net\/member\.php/
10 10
@@ -132,8 +122,8 @@ class ArtistUrl < ApplicationRecord
def validate_url_format def validate_url_format
uri = Addressable::URI.parse(url) uri = Addressable::URI.parse(url)
errors[:url] << "#{uri} must begin with http:// or https://" if !uri.scheme.in?(%w[http https]) errors[:url] << "'#{uri}' must begin with http:// or https:// " if !uri.scheme.in?(%w[http https])
rescue Addressable::URI::InvalidURIError => error rescue Addressable::URI::InvalidURIError => error
errors[:url] << "is malformed: #{error}" errors[:url] << "'#{uri}' is malformed: #{error}"
end end
end end

View File

@@ -166,9 +166,19 @@ class ArtistTest < ActiveSupport::TestCase
end end
should "not allow invalid urls" do should "not allow invalid urls" do
artist = FactoryBot.create(:artist, :url_string => "blah") artist = FactoryBot.build(:artist, :url_string => "blah")
assert_equal(false, artist.valid?) assert_equal(false, artist.valid?)
assert_equal([" blah must begin with http:// or https://"], artist.errors[:url]) assert_equal(["'blah' must begin with http:// or https:// "], artist.errors["urls.url"])
end
should "allow fixing invalid urls" do
artist = FactoryBot.build(:artist)
artist.urls << FactoryBot.build(:artist_url, url: "www.example.com", normalized_url: "www.example.com")
artist.save(validate: false)
artist.update(url_string: "http://www.example.com")
assert_equal(true, artist.valid?)
assert_equal("http://www.example.com", artist.urls.map(&:to_s).join)
end end
should "make sure old urls are deleted" do should "make sure old urls are deleted" do

View File

@@ -17,10 +17,17 @@ class ArtistUrlTest < ActiveSupport::TestCase
end end
should "allow urls to be marked as inactive" do should "allow urls to be marked as inactive" do
url = FactoryBot.create(:artist_url, :url => "-http://monet.com") url = FactoryBot.create(:artist_url, url: "http://monet.com", is_active: false)
assert_equal("http://monet.com", url.url) assert_equal("http://monet.com", url.url)
assert_equal("http://monet.com/", url.normalized_url) assert_equal("http://monet.com/", url.normalized_url)
refute(url.is_active?) assert_equal("-http://monet.com", url.to_s)
end
should "disallow invalid urls" do
url = FactoryBot.build(:artist_url, url: "www.example.com")
assert_equal(false, url.valid?)
assert_match(/must begin with http/, url.errors.full_messages.join)
end end
should "always add a trailing slash when normalized" do should "always add a trailing slash when normalized" do