diff --git a/admin/sql/CreateConstraints.sql b/admin/sql/CreateConstraints.sql index 5df42837219..5720281fd90 100644 --- a/admin/sql/CreateConstraints.sql +++ b/admin/sql/CreateConstraints.sql @@ -170,6 +170,17 @@ ADD CONSTRAINT group_type_implies_null_gender CHECK ( OR type IS NULL ); +ALTER TABLE artist +ADD CONSTRAINT character_type_implies_no_end CHECK ( + type != 4 OR ( + end_date_day IS NULL AND + end_date_month IS NULL AND + end_date_year IS NULL AND + end_area IS NULL AND + ended = FALSE + ) +); + ALTER TABLE release_label ADD CHECK (catalog_number IS NOT NULL OR label IS NOT NULL); diff --git a/admin/sql/updates/20241104-mbs-9885.sql b/admin/sql/updates/20241104-mbs-9885.sql new file mode 100644 index 00000000000..4edded422ea --- /dev/null +++ b/admin/sql/updates/20241104-mbs-9885.sql @@ -0,0 +1,27 @@ +\set ON_ERROR_STOP 1 + +BEGIN; + +UPDATE artist +SET end_date_day = NULL, + end_date_month = NULL, + end_date_year = NULL, + end_area = NULL, + ended = FALSE +WHERE type = 4; + +ALTER TABLE artist +DROP CONSTRAINT IF EXISTS character_type_implies_no_end; + +ALTER TABLE artist +ADD CONSTRAINT character_type_implies_no_end CHECK ( + type != 4 OR ( + end_date_day IS NULL AND + end_date_month IS NULL AND + end_date_year IS NULL AND + end_area IS NULL AND + ended = FALSE + ) +); + +COMMIT; diff --git a/admin/sql/updates/schema-change/30.master_and_standalone.sql b/admin/sql/updates/schema-change/30.master_and_standalone.sql new file mode 100644 index 00000000000..4214fd3d6c5 --- /dev/null +++ b/admin/sql/updates/schema-change/30.master_and_standalone.sql @@ -0,0 +1,33 @@ +-- Generated by CompileSchemaScripts.pl from: +-- 20241104-mbs-9885.sql +\set ON_ERROR_STOP 1 +BEGIN; +SET search_path = musicbrainz, public; +SET LOCAL statement_timeout = 0; +-------------------------------------------------------------------------------- +SELECT '20241104-mbs-9885.sql'; + + +UPDATE artist +SET end_date_day = NULL, + end_date_month = NULL, + end_date_year = NULL, + end_area = NULL, + ended = FALSE +WHERE type = 4; + +ALTER TABLE artist +DROP CONSTRAINT IF EXISTS character_type_implies_no_end; + +ALTER TABLE artist +ADD CONSTRAINT character_type_implies_no_end CHECK ( + type != 4 OR ( + end_date_day IS NULL AND + end_date_month IS NULL AND + end_date_year IS NULL AND + end_area IS NULL AND + ended = FALSE + ) +); + +COMMIT; diff --git a/lib/MusicBrainz/Server/Constants.pm b/lib/MusicBrainz/Server/Constants.pm index 1c71142e40d..b92b1d22b4d 100644 --- a/lib/MusicBrainz/Server/Constants.pm +++ b/lib/MusicBrainz/Server/Constants.pm @@ -67,7 +67,7 @@ our @EXPORT_OK = ( $DLABEL_ID $DARTIST_ID $VARTIST_ID $VARTIST_GID $NOLABEL_ID $NOLABEL_GID $COVERART_FRONT_TYPE $COVERART_BACK_TYPE $AREA_TYPE_COUNTRY $AREA_TYPE_CITY - $ARTIST_TYPE_PERSON $ARTIST_TYPE_GROUP + $ARTIST_TYPE_PERSON $ARTIST_TYPE_GROUP $ARTIST_TYPE_CHARACTER $INSTRUMENT_ROOT_ID $VOCAL_ROOT_ID $REQUIRED_VOTES $OPEN_EDIT_DURATION $MINIMUM_RESPONSE_PERIOD $MINIMUM_VOTING_PERIOD @@ -395,6 +395,7 @@ Readonly our $AREA_TYPE_CITY => 3; Readonly our $ARTIST_TYPE_PERSON => 1; Readonly our $ARTIST_TYPE_GROUP => 2; +Readonly our $ARTIST_TYPE_CHARACTER => 4; Readonly our $REQUIRED_VOTES => 3; Readonly our $OPEN_EDIT_DURATION => DateTime::Duration->new(days => 7); diff --git a/lib/MusicBrainz/Server/Data/Artist.pm b/lib/MusicBrainz/Server/Data/Artist.pm index 0b632236b3f..4ddd7d953cd 100644 --- a/lib/MusicBrainz/Server/Data/Artist.pm +++ b/lib/MusicBrainz/Server/Data/Artist.pm @@ -4,7 +4,10 @@ use namespace::autoclean; use Carp; use List::AllUtils qw( any ); -use MusicBrainz::Server::Constants qw( $ARTIST_TYPE_GROUP ); +use MusicBrainz::Server::Constants qw( + $ARTIST_TYPE_CHARACTER + $ARTIST_TYPE_GROUP +); use MusicBrainz::Server::Entity::Artist; use MusicBrainz::Server::Entity::PartialDate; use MusicBrainz::Server::Data::ArtistCredit; @@ -19,6 +22,7 @@ use MusicBrainz::Server::Data::Utils qw( load_subobjects merge_table_attributes merge_date_period + merge_partial_date order_by ); use MusicBrainz::Server::Data::Utils::Cleanup qw( used_in_relationship ); @@ -389,12 +393,21 @@ sub merge # that's arguably more annoying for the editor. See MBS-10187. my %dropped_columns; unless (is_special_artist($new_id)) { - my $merge_columns = [ qw( area begin_area end_area ) ]; - my $target_row = $self->sql->select_single_row_hash('SELECT gender, type FROM artist WHERE id = ?', $new_id); + my $merge_columns = [ qw( area begin_area ) ]; + my $target_row = $self->sql->select_single_row_hash( + 'SELECT gender, type, ended, end_area, end_date_year, end_date_month, end_date_day FROM artist WHERE id = ?', + $new_id, + ); my $target_type = $target_row->{type}; my $target_gender = $target_row->{gender}; + my $target_ended = $target_row->{ended}; + my $target_end_date = MusicBrainz::Server::Entity::PartialDate->new_from_row($target_row, 'end_date_'); + my $target_end_area = $target_row->{end_area}; my $merged_type = $target_type; my $merged_gender = $target_gender; + my $merged_ended = $target_ended; + my $merged_end_date = $target_end_date; + my $merged_end_area = $target_end_area; if (!$merged_type) { my ($query, $args) = conditional_merge_column_query( @@ -408,6 +421,38 @@ sub merge $merged_gender = $self->c->sql->select_single_value($query, @$args); } + if (!$merged_ended) { + my ($query, $args) = conditional_merge_column_query( + 'artist', 'ended', $new_id, [$new_id, @$old_ids], '= TRUE'); + $merged_ended = $self->c->sql->select_single_value($query, @$args) // 0; + } + + if ($merged_end_date->is_empty) { + my ($query, $args) = conditional_merge_column_query( + 'artist', 'end_date_year', $new_id, [$new_id, @$old_ids], 'IS NOT NULL'); + my $merged_end_year = $self->c->sql->select_single_value($query, @$args); + + ($query, $args) = conditional_merge_column_query( + 'artist', 'end_date_month', $new_id, [$new_id, @$old_ids], 'IS NOT NULL'); + my $merged_end_month = $self->c->sql->select_single_value($query, @$args); + + ($query, $args) = conditional_merge_column_query( + 'artist', 'end_date_day', $new_id, [$new_id, @$old_ids], 'IS NOT NULL'); + my $merged_end_day = $self->c->sql->select_single_value($query, @$args); + + $merged_end_date = MusicBrainz::Server::Entity::PartialDate->new( + year => $merged_end_year, + month => $merged_end_month, + day => $merged_end_day, + ); + } + + if (!$merged_end_area) { + my ($query, $args) = conditional_merge_column_query( + 'artist', 'end_area', $new_id, [$new_id, @$old_ids], 'IS NOT NULL'); + $merged_end_area = $self->c->sql->select_single_value($query, @$args); + } + my $group_types = $self->sql->select_single_column_array(q{ WITH RECURSIVE atp(id) AS ( VALUES (?::int) @@ -422,23 +467,61 @@ sub merge defined $merged_type && contains_string($group_types, $merged_type); - if ($merged_type_is_group && $merged_gender) { - my $target_type_is_group = - defined $target_type && - contains_string($group_types, $target_type); - - if ($target_type_is_group) { - $dropped_columns{gender} = $merged_gender; - push @$merge_columns, 'type'; - } elsif ($target_gender) { - $dropped_columns{type} = $merged_type; + my $merged_type_is_character = + defined $merged_type && $merged_type == $ARTIST_TYPE_CHARACTER; + + my $merge_ended = 0; + + if ($merged_type_is_group || $merged_type_is_character) { + if ($merged_type_is_group && $merged_gender) { + my $target_type_is_group = + defined $target_type && + contains_string($group_types, $target_type); + + if ($target_type_is_group) { + $dropped_columns{gender} = $merged_gender; + push @$merge_columns, 'type'; + } elsif ($target_gender) { + $dropped_columns{type} = $merged_type; + push @$merge_columns, 'gender'; + } else { + $dropped_columns{gender} = $merged_gender; + $dropped_columns{type} = $merged_type; + } + } else { push @$merge_columns, 'gender'; + } + + if ( + $merged_type_is_character && ( + $merged_ended || + $merged_end_area || + !($merged_end_date->is_empty) + ) + ) { + my $target_type_is_character = + defined $target_type && $target_type == $ARTIST_TYPE_CHARACTER; + + if ($target_type_is_character) { + $dropped_columns{ended} = $merged_ended; + $dropped_columns{end_area} = $merged_end_area; + $dropped_columns{end_date} = $merged_end_date->format; + push @$merge_columns, 'type'; + } elsif ($target_ended || $target_end_area || !($target_end_date->is_empty)) { + $dropped_columns{type} = $merged_type; + push @$merge_columns, 'end_area'; + $merge_ended = 1; + } else { + $dropped_columns{ended} = $merged_ended; + $dropped_columns{type} = $merged_type; + } } else { - $dropped_columns{gender} = $merged_gender; - $dropped_columns{type} = $merged_type; + push @$merge_columns, 'end_area'; + $merge_ended = 1; } } else { - push @$merge_columns, qw( gender type ); + push @$merge_columns, qw( type gender end_area ); + $merge_ended = 1; } merge_table_attributes( @@ -450,13 +533,24 @@ sub merge ), ); - merge_date_period( - $self->sql => ( - table => 'artist', - old_ids => $old_ids, - new_id => $new_id, - ), - ); + if ($merge_ended) { + merge_date_period( + $self->sql => ( + table => 'artist', + old_ids => $old_ids, + new_id => $new_id, + ), + ); + } else { + merge_partial_date( + $self->sql => ( + table => 'artist', + old_ids => $old_ids, + new_id => $new_id, + ), + field => 'begin_date', + ); + } } $self->_delete_and_redirect_gids('artist', $new_id, @$old_ids); diff --git a/lib/MusicBrainz/Server/Edit/Artist/Edit.pm b/lib/MusicBrainz/Server/Edit/Artist/Edit.pm index 7d2d6ffb6de..06d3a6afe50 100644 --- a/lib/MusicBrainz/Server/Edit/Artist/Edit.pm +++ b/lib/MusicBrainz/Server/Edit/Artist/Edit.pm @@ -3,6 +3,7 @@ use 5.10.0; use Moose; use MusicBrainz::Server::Constants qw( + $ARTIST_TYPE_CHARACTER $ARTIST_TYPE_GROUP $EDIT_ARTIST_CREATE $EDIT_ARTIST_EDIT @@ -272,6 +273,12 @@ around merge_changes => sub { my $artist = $self->current_instance; my $gender_id = exists $merged->{gender_id} ? $merged->{gender_id} : $artist->gender_id; + my $end_date = exists $merged->{end_date} ? + PartialDate->new($merged->{end_date}) : $artist->end_date; + my $ended = (exists $merged->{ended} ? + $merged->{ended} : $artist->ended) // 0; + my $end_area_id = exists $merged->{end_area_id} ? + $merged->{end_area_id} : $artist->end_area_id; my $type_id = exists $merged->{type_id} ? $merged->{type_id} : $artist->type_id; @@ -284,6 +291,24 @@ around merge_changes => sub { ); } + if (defined $end_date && !$end_date->is_empty && defined $type_id) { + MusicBrainz::Server::Edit::Exceptions::GeneralError->throw( + 'Characters cannot have an end date.', + ) if ($type_id == $ARTIST_TYPE_CHARACTER); + } + + if (defined $end_area_id && defined $type_id) { + MusicBrainz::Server::Edit::Exceptions::GeneralError->throw( + 'Characters cannot have an end area.', + ) if ($type_id == $ARTIST_TYPE_CHARACTER); + } + + if ($ended && defined $type_id) { + MusicBrainz::Server::Edit::Exceptions::GeneralError->throw( + 'Characters cannot be marked as ended.', + ) if ($type_id == $ARTIST_TYPE_CHARACTER); + } + return $merged; }; diff --git a/lib/MusicBrainz/Server/Edit/Artist/Merge.pm b/lib/MusicBrainz/Server/Edit/Artist/Merge.pm index 4ba2615a6f6..c4ff18df794 100644 --- a/lib/MusicBrainz/Server/Edit/Artist/Merge.pm +++ b/lib/MusicBrainz/Server/Edit/Artist/Merge.pm @@ -65,20 +65,75 @@ sub do_merge my $dropped_type = $self->c->model('ArtistType')->get_by_id($dropped_columns->{type}); + if ($dropped_type->id == 4) { + $self->c->model('EditNote')->add_note( + $self->id => { + editor_id => $EDITOR_MODBOT, + text => localized_note( + N_l('The following data has not been added to the destination ' . + 'artist because it conflicted with other data: ' . + '{dropped_data}. {reason}'), + vars => { + dropped_data => localized_note( + N_l('Type: {artist_type}'), + vars => { + artist_type => localized_note( + $dropped_type->name, + function => 'lp', + domain => 'attributes', + args => ['artist_type'], + ), + }, + ), + reason => localized_note( + N_l('Characters cannot have an end date nor area ' . + 'nor be marked as ended.'), + ), + }, + ), + }, + ); + } else { + $self->c->model('EditNote')->add_note( + $self->id => { + editor_id => $EDITOR_MODBOT, + text => localized_note( + N_l('The “{artist_type}” type has not been added to the ' . + 'destination artist because it conflicted with the ' . + 'gender setting of one of the artists here. Group ' . + 'artists cannot have a gender.'), + vars => { + artist_type => localized_note( + $dropped_type->name, + function => 'lp', + domain => 'attributes', + args => ['artist_type'], + ), + }, + ), + }, + ); + } + } + + if ($dropped_columns->{gender}) { + my $dropped_gender = + $self->c->model('Gender')->get_by_id($dropped_columns->{gender}); + $self->c->model('EditNote')->add_note( $self->id => { editor_id => $EDITOR_MODBOT, text => localized_note( - N_l('The “{artist_type}” type has not been added to the ' . + N_l('The “{gender}” gender has not been added to the ' . 'destination artist because it conflicted with the ' . - 'gender setting of one of the artists here. Group ' . - 'artists cannot have a gender.'), + 'group type of one of the artists here. Group artists ' . + 'cannot have a gender.'), vars => { - artist_type => localized_note( - $dropped_type->name, + gender => localized_note( + $dropped_gender->name, function => 'lp', domain => 'attributes', - args => ['artist_type'], + args => ['gender'], ), }, ), @@ -86,24 +141,53 @@ sub do_merge ); } - if ($dropped_columns->{gender}) { - my $dropped_gender = - $self->c->model('Gender')->get_by_id($dropped_columns->{gender}); + my $dropped_ended = defined $dropped_columns->{ended}; + + if ($dropped_ended || + $dropped_columns->{end_date} || + $dropped_columns->{end_area} + ) { + my $dropped_area = $dropped_columns->{end_area} + ? $self->c->model('Area')->get_by_id($dropped_columns->{end_area}) + : undef; + + my $dropped_data = []; + + if ($dropped_ended) { + push @$dropped_data, localized_note( N_l('Ended: Yes') ); + } + + if ($dropped_columns->{end_date}) { + push @$dropped_data, localized_note( + N_l('End date: {end_date}'), + vars => { end_date => $dropped_columns->{end_date} }, + ); + } + + if ($dropped_area) { + push @$dropped_data, localized_note( + N_l('End area: {end_area}'), + vars => { end_area => $dropped_area->name }, + ); + } $self->c->model('EditNote')->add_note( $self->id => { editor_id => $EDITOR_MODBOT, text => localized_note( - N_l('The “{gender}” gender has not been added to the ' . - 'destination artist because it conflicted with the ' . - 'group type of one of the artists here. Group artists ' . - 'cannot have a gender.'), + N_l('The following data has not been added to the destination ' . + 'artist because it conflicted with other data: ' . + '{dropped_data}. {reason}'), vars => { - gender => localized_note( - $dropped_gender->name, - function => 'lp', - domain => 'attributes', - args => ['gender'], + dropped_data => localized_note( + undef, + function => 'comma_list', + domain => 'mb_server', + args => $dropped_data, + ), + reason => localized_note( + N_l('Characters cannot have an end date nor area ' . + 'nor be marked as ended.'), ), }, ), diff --git a/lib/MusicBrainz/Server/Entity/EditNote.pm b/lib/MusicBrainz/Server/Entity/EditNote.pm index 7adde8cd0db..46d18b476a7 100644 --- a/lib/MusicBrainz/Server/Entity/EditNote.pm +++ b/lib/MusicBrainz/Server/Entity/EditNote.pm @@ -7,7 +7,10 @@ use Moose; use namespace::autoclean; use MusicBrainz::Server::Constants qw( $EDITOR_MODBOT ); -use MusicBrainz::Server::Data::Utils qw( datetime_to_iso8601 ); +use MusicBrainz::Server::Data::Utils qw( + contains_string + datetime_to_iso8601 +); use MusicBrainz::Server::Entity::Types; use MusicBrainz::Server::Entity::Util::JSON qw( to_json_object ); use MusicBrainz::Server::Filters qw( format_editnote ); @@ -75,17 +78,22 @@ sub _localize_text { my $version = $source->{version} // 0; my $fn_package = 'MusicBrainz::Server::Translation'; my $fn_name = 'l'; - my @args = ($source->{message} // ''); + my @args; my $source_vars; if ($version == 0) { + @args = ($source->{message} // ''); # old versions of `localized_note` passed substitution # variables as `args`. $source_vars = $source->{args}; } elsif ($version == 1) { - push @args, @{ $source->{args} // [] }; + my $no_message_functions = [ qw ( comma_list comma_only_list ) ]; $fn_name = $source->{function} // 'l'; + @args = ($source->{message} // '') + unless contains_string($no_message_functions, $fn_name); + push @args, map { _localize_text($_, $depth + 1) } @{ $source->{args} // [] }; + $source_vars = $source->{vars}; my $domain = $source->{domain}; diff --git a/lib/MusicBrainz/Server/Form/Artist.pm b/lib/MusicBrainz/Server/Form/Artist.pm index e552ee9e6f1..f0b05ba6c35 100644 --- a/lib/MusicBrainz/Server/Form/Artist.pm +++ b/lib/MusicBrainz/Server/Form/Artist.pm @@ -89,6 +89,21 @@ sub validate { $self->field('gender_id')->add_error(l('Choirs cannot have a gender.')); } } + + if ($self->field('type_id')->value && + $self->field('type_id')->value == 4) { + if ($self->field('period.end_date.year')->value || + $self->field('period.end_date.month')->value || + $self->field('period.end_date.day')->value) { + $self->field('period.end_date')->add_error(l('Characters cannot have an end date.')); + } + if ($self->field('period.ended')->value) { + $self->field('period.ended')->add_error(l('Characters cannot be marked as ended.')); + } + if ($self->field('end_area_id')->value) { + $self->field('end_area.name')->add_error(l('Characters cannot have an end area.')); + } + } } 1; diff --git a/root/artist/edit_form.tt b/root/artist/edit_form.tt index dabe29f0c35..9f3d0f126b7 100644 --- a/root/artist/edit_form.tt +++ b/root/artist/edit_form.tt @@ -60,20 +60,22 @@ [% field_errors(r.form, 'begin_area.name') %] [% END %] - [% form_row_date(r, 'period.end_date', add_colon(l('End date'))) %] - [% too_short_year_error('too_short_end_year') %] - [% form_row_checkbox(r, 'period.ended', l('This artist has ended.')) %] - [% WRAPPER form_row %] - [% end_area_field = form.field('end_area.name') %] - - - [% React.embed(c, 'static/scripts/common/components/SearchIcon') %] - [% r.hidden(form.field('end_area').field('gid'), { class => 'gid' }) %] - [% r.hidden('end_area_id', class => 'id') %] - [% r.text(end_area_field, class => 'name') %] - - [% field_errors(r.form, 'end_area.name') %] - [% END %] +