diff --git a/.eslintignore b/.eslintignore index c54c6c23f11..910f0037d4d 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,5 +1,6 @@ .cpanm/**/*.js flow-typed/npm/@sentry/*.js +flow-typed/npm/he_*.js flow-typed/npm/react-dom_*.js flow-typed/npm/redux_*.js perl_modules/**/*.js diff --git a/.eslintrc.unfixed.yaml b/.eslintrc.unfixed.yaml index c63d6e911bc..33be5391237 100644 --- a/.eslintrc.unfixed.yaml +++ b/.eslintrc.unfixed.yaml @@ -1,13 +1,10 @@ rules: no-await-in-loop: error - no-extra-semi: warn no-unused-vars: error consistent-return: error eqeqeq: [warn, smart] no-else-return: warn - no-multi-spaces: [error, {ignoreEOLComments: true}] wrap-iife: warn - array-element-newline: [warn, consistent] camelcase: [warn, {properties: never, allow: [ "l_attributes", @@ -38,22 +35,13 @@ rules: "N_ln", "N_lp", ]}] - comma-dangle: [warn, {arrays: always-multiline, - objects: always-multiline, - imports: always-multiline, - exports: always-multiline, - functions: always-multiline}] comma-spacing: [warn, {before: false, after: true}] computed-property-spacing: [warn, never, {"enforceForClassMembers": true}] consistent-this: [warn, self] - function-paren-newline: [warn, consistent] jsx-quotes: [warn, prefer-double] key-spacing: [warn, {mode: minimum}] - multiline-comment-style: [warn, starred-block] newline-per-chained-call: [warn, {ignoreChainWithDepth: 3}] no-lonely-if: warn - no-multiple-empty-lines: [warn, {max: 2, maxBOF: 0, maxEOF: 0}] - no-trailing-spaces: warn object-curly-newline: [warn, {multiline: true, consistent: true}] object-curly-spacing: [warn, never] operator-assignment: [warn, always] @@ -72,7 +60,6 @@ rules: no-var: warn prefer-const: warn import/first: warn - import/newline-after-import: [warn, {count: 1}] import/no-commonjs: error import/order: [warn, {newlines-between: always}] react/no-access-state-in-setstate: error @@ -85,7 +72,6 @@ rules: lifecycle, everything-else, render]}] - react/jsx-boolean-value: [warn, never] react/jsx-closing-bracket-location: [error, tag-aligned] react/jsx-first-prop-new-line: [error, multiline-multiprop] react/jsx-key: warn diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 8303317991f..643066935da 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -81,6 +81,7 @@ rules: no-duplicate-case: error no-empty-character-class: error no-empty: [error, {allowEmptyCatch: true}] + no-extra-semi: warn no-irregular-whitespace: warn no-misleading-character-class: error no-obj-calls: error @@ -107,6 +108,7 @@ rules: no-eq-null: off no-floating-decimal: warn no-global-assign: error + no-multi-spaces: [error, {ignoreEOLComments: true}] no-useless-catch: warn radix: warn @@ -120,11 +122,18 @@ rules: # Stylistic Issues array-bracket-newline: [warn, consistent] array-bracket-spacing: [warn, never] + array-element-newline: [warn, consistent] block-spacing: [warn, always] brace-style: [warn, 1tbs] + comma-dangle: [warn, {arrays: always-multiline, + objects: always-multiline, + imports: always-multiline, + exports: always-multiline, + functions: always-multiline}] comma-style: [warn, last] eol-last: [warn, always] func-call-spacing: [warn, never] + function-paren-newline: [warn, consistent] implicit-arrow-linebreak: [warn, beside] indent: [warn, 2, {CallExpression: {arguments: first}, SwitchCase: 1, ignoredNodes: ["JSXElement", "ArrowFunctionExpression"]}] keyword-spacing: [warn, {before: true, after: true}] @@ -136,16 +145,19 @@ rules: ignoreTemplateLiterals: false, ignoreRegExpLiterals: true}] max-statements-per-line: [warn, {max: 1}] + multiline-comment-style: [warn, starred-block] multiline-ternary: off new-cap: off new-parens: warn no-mixed-spaces-and-tabs: warn no-multi-assign: off + no-multiple-empty-lines: [warn, {max: 2, maxBOF: 0, maxEOF: 0}] no-negated-condition: warn no-nested-ternary: off no-plusplus: off no-tabs: warn no-ternary: off + no-trailing-spaces: warn no-underscore-dangle: off no-unneeded-ternary: warn no-whitespace-before-property: warn @@ -185,6 +197,7 @@ rules: import/export: error import/extensions: [warn, never] + import/newline-after-import: [warn, {count: 1}] import/no-duplicates: warn import/no-dynamic-require: error import/no-unresolved: error @@ -238,6 +251,7 @@ rules: react/void-dom-elements-no-children: error # JSX-specific rules + react/jsx-boolean-value: [warn, never] react/jsx-closing-tag-location: error react/jsx-curly-spacing: [error, {when: never, children: true}] react/jsx-equals-spacing: [error, never] diff --git a/lib/MusicBrainz/Server.pm b/lib/MusicBrainz/Server.pm index 7992f48a1cf..b89b33f9e2f 100644 --- a/lib/MusicBrainz/Server.pm +++ b/lib/MusicBrainz/Server.pm @@ -257,9 +257,12 @@ sub redirect_back { if ( $returnto eq '' || - # Check that we weren't given an external URL. Only relative - # URLs are allowed. - $returnto->authority + # Check that we weren't given an external URL. Only URLs relative to + # the current domain are allowed. + ( + $returnto->authority && + $returnto->authority ne $c->req->uri->authority + ) ) { $returnto->path_query('/'); $returnto->fragment(undef); @@ -500,19 +503,24 @@ around 'finalize_error' => sub { $c->$orig(@args); if (!$c->debug && scalar @{ $c->error }) { - $c->stash->{errors} = $errors; - $c->stash->{template} = $timed_out ? - 'main/timeout.tt' : 'main/500.tt'; try { $c->stash->{hostname} = hostname; } catch {}; + $c->stash( + component_path => $timed_out + ? 'main/error/TimeoutError' + : 'main/error/Error500', + component_props => { + $c->stash->{edit} ? (edits => [ $c->stash->{edit} ]) : (), + formattedErrors => $c->stash->{formatted_errors}, + hostname => $c->stash->{hostname}, + useLanguages => boolean_to_json($c->stash->{use_languages}), + }, + current_view => 'Node', + ); $c->clear_errors; if ($c->stash->{error_body_in_stash}) { $c->res->{body} = $c->stash->{body}; $c->res->{status} = $c->stash->{status}; } else { - if (($c->stash->{current_view} // '') eq 'Node') { - # Remove once error pages are converted to React. - $c->stash(current_view => 'Default'); - } $c->view->process($c); # Catalyst::Engine::finalize_error unsets $c->encoding. [1] # We're rendering our own error page here, not using theirs, @@ -741,6 +749,7 @@ sub TO_JSON { # Whitelist of keys that we use in the templates. my @stash_keys = qw( + can_delete collaborative_collections commons_image containment @@ -830,6 +839,7 @@ sub TO_JSON { debug => boolean_to_json($self->debug), relative_uri => $self->relative_uri, req => { + body_params => $req->body_params, headers => \%headers, query_params => $req->query_params, secure => boolean_to_json($req->secure), diff --git a/lib/MusicBrainz/Server/Controller.pm b/lib/MusicBrainz/Server/Controller.pm index 55e91f5ebb5..d36196eba9c 100644 --- a/lib/MusicBrainz/Server/Controller.pm +++ b/lib/MusicBrainz/Server/Controller.pm @@ -153,6 +153,9 @@ sub edit_action $form_args{init_object} = $opts{item} if exists $opts{item}; my $form = $c->form( form => $opts{form}, ctx => $c, %form_args ); + $c->stash->{component_props}{form} = $form; + $opts{pre_validation}->($form) if exists $opts{pre_validation}; + if ($c->form_posted_and_valid($form, $c->req->body_params)) { return if exists $opts{pre_creation} && !$opts{pre_creation}->($form); diff --git a/lib/MusicBrainz/Server/Controller/Artist.pm b/lib/MusicBrainz/Server/Controller/Artist.pm index ffb89e8e2a6..1ebd5b7b18b 100644 --- a/lib/MusicBrainz/Server/Controller/Artist.pm +++ b/lib/MusicBrainz/Server/Controller/Artist.pm @@ -179,41 +179,58 @@ sub show : PathPart('') Chained('load') }) }; my $has_filter = %filter ? 1 : 0; + my $has_default = $c->model('ReleaseGroup')->has_by_artist($artist->id, 0); + my $has_extra = $c->model('ReleaseGroup')->has_by_artist($artist->id, 1); + my $has_va = $c->model('ReleaseGroup')->has_by_track_artist($artist->id, 0); + my $has_va_extra = $c->model('ReleaseGroup')->has_by_track_artist($artist->id, 1); + my $want_va_only = $c->req->query_params->{va}; my $want_all_statuses = $c->req->query_params->{all}; my $including_all_statuses; my $showing_va_only; + my $has_release_groups = $has_default || $has_extra || $has_va || $has_va_extra; + my $force_release_groups = $want_va_only || $want_all_statuses || %filter; + my $make_attempt = sub { my ($all, $va) = @_; my $method = $va ? 'find_by_track_artist' : 'find_by_artist'; return $self->_load_paged($c, sub { - $c->model('ReleaseGroup')->$method($c->stash->{artist}->id, $all, shift, shift, filter => \%filter); + if (!$all && !$va) { + return ([], 0) unless $has_default; + } elsif ($all && !$va) { + return ([], 0) unless ($has_default || $has_extra); + } elsif (!$all && $va) { + return ([], 0) unless $has_va; + } elsif ($all && $va) { + return ([], 0) unless ($has_va || $has_va_extra); + } + return $c->model('ReleaseGroup')->$method($c->stash->{artist}->id, $all, shift, shift, filter => \%filter); }); }; - # Attempt from official non-va, to all non-va, to official va, to all va; - # filter out any attempt that contradicts a preference from a query param - my @attempts = grep { - ($_->[0] || !$want_all_statuses) && - ($_->[1] || !$want_va_only) - } ([0,0], [1,0], [0,1], [1,1]); - - for my $attempt (@attempts) { - my $all = $attempt->[0]; - my $va = $attempt->[1]; - $release_groups = $make_attempt->($all, $va); - # If filtering, only make one attempt - # otherwise, attempt until we find RGs or exhaust the possibilities - if (scalar @$release_groups || %filter) { + if ($has_release_groups || $force_release_groups) { + # Attempt from official non-va, to all non-va, to official va, to all va; + # filter out any attempt that contradicts a preference from a query param + my @attempts = grep { + ($_->[0] || !$want_all_statuses) && + ($_->[1] || !$want_va_only) + } ([0,0], [1,0], [0,1], [1,1]); + + for my $attempt (@attempts) { + my $all = $attempt->[0]; + my $va = $attempt->[1]; + $release_groups = $make_attempt->($all, $va); $including_all_statuses = $all; $showing_va_only = $va; - last; + # If filtering, only make one attempt + # otherwise, attempt until we find RGs or exhaust the possibilities + if (scalar @$release_groups || %filter) { + last; + } } - } - - # If there is no expressed preference (va, filter) and no RGs, find recordings - if (!$showing_va_only && !%filter && scalar @$release_groups == 0) { + } else { + # If there is no expressed preference (va, filter) and no RGs, find recordings $recordings = $self->_load_paged($c, sub { $c->model('Recording')->find_standalone($artist->id, shift, shift); }); @@ -289,8 +306,12 @@ sub show : PathPart('') Chained('load') ajaxFilterFormUrl => $c->uri_for_action('/ajax/filter_artist_release_groups_form', { artist_id => $artist->id }), artist => $artist, filterForm => $c->stash->{filter_form}, + hasDefault => boolean_to_json($has_default), + hasExtra => boolean_to_json($has_extra), hasFilter => boolean_to_json($has_filter), - includingAllStatuses => $including_all_statuses, + hasVariousArtists => boolean_to_json($has_va), + hasVariousArtistsExtra => boolean_to_json($has_va_extra), + includingAllStatuses => boolean_to_json($including_all_statuses), legalName => $legal_name, legalNameAliases => $legal_name_aliases, legalNameArtistAliases => $legal_name_artist_aliases, @@ -299,9 +320,7 @@ sub show : PathPart('') Chained('load') pager => serialize_pager($c->stash->{pager}), recordings => $recordings, releaseGroups => $release_groups, - showingVariousArtistsOnly => $showing_va_only, - wantAllStatuses => boolean_to_json($want_all_statuses), - wantVariousArtistsOnly => boolean_to_json($want_va_only), + showingVariousArtistsOnly => boolean_to_json($showing_va_only), wikipediaExtract => $c->stash->{wikipedia_extract}, }, ); diff --git a/lib/MusicBrainz/Server/Controller/ArtistCredit.pm b/lib/MusicBrainz/Server/Controller/ArtistCredit.pm new file mode 100644 index 00000000000..5eb586bff6d --- /dev/null +++ b/lib/MusicBrainz/Server/Controller/ArtistCredit.pm @@ -0,0 +1,96 @@ +package MusicBrainz::Server::Controller::ArtistCredit; +use Moose; +use Moose::Util qw( find_meta ); + +BEGIN { extends 'MusicBrainz::Server::Controller' } + +use MusicBrainz::Server::Data::Utils qw( type_to_model ); +use MusicBrainz::Server::Constants qw( %ENTITIES entities_with ); +use MusicBrainz::Server::ControllerUtils::JSON qw( serialize_pager ); + +__PACKAGE__->config( + namespace => 'artist_credit', +); + +with 'MusicBrainz::Server::Controller::Role::Load' => { + model => 'ArtistCredit', +}; + +sub base : Chained('/') PathPart('artist-credit') CaptureArgs(0) { } + +sub _load +{ + my ($self, $c, $id) = @_; + my $artist_credit = $c->model('ArtistCredit')->get_by_id($id); + return $artist_credit; +} + +sub show : Chained('load') PathPart('') +{ + my ($self, $c) = @_; + my $artist_credit = $c->stash->{artist_credit}; + $c->stash( + current_view => 'Node', + component_path => 'artist_credit/ArtistCreditIndex', + component_props => { + %{$c->stash->{component_props}}, + artistCredit => $artist_credit, + creditedEntities => { + map { + my ($entities, $total) = $c->model(type_to_model($_))->find_by_artist_credit($artist_credit->id, 10, 0); + + ("$_" => { + count => $total, + entities => $entities, + }) + } entities_with('artist_credits') + }, + }, + + ); +} + +map { + my $entity_type = $_; + my $entity_properties = $ENTITIES{$entity_type}; + my $url = $entity_properties->{url}; + + my $method = sub { + my ($self, $c) = @_; + + my $artist_credit = $c->stash->{artist_credit}; + + my $entities = $self->_load_paged($c, sub { + $c->model($entity_properties->{model})->find_by_artist_credit($artist_credit->id, shift, shift); + }); + + $c->stash( + current_view => 'Node', + component_path => 'artist_credit/EntityList', + component_props => { + %{$c->stash->{component_props}}, + entities => $entities, + entityType => $entity_type, + page => "/$url", + pager => serialize_pager($c->stash->{pager}), + artistCredit => $artist_credit, + }, + ); + }; + + find_meta(__PACKAGE__)->add_method($_ => $method); + find_meta(__PACKAGE__)->register_method_attributes($method, ["Chained('load')", "PathPart('$url')"]); +} entities_with('artist_credits'); + +1; + +=head1 COPYRIGHT AND LICENSE + +Copyright (C) 2020 MetaBrainz Foundation + +This file is part of MusicBrainz, the open internet music database, +and is licensed under the GPL version 2, or (at your option) any +later version: http://www.gnu.org/licenses/gpl-2.0.txt + +=cut + diff --git a/lib/MusicBrainz/Server/Controller/OtherLookup.pm b/lib/MusicBrainz/Server/Controller/OtherLookup.pm index 0b0e76d8021..c180053e948 100644 --- a/lib/MusicBrainz/Server/Controller/OtherLookup.pm +++ b/lib/MusicBrainz/Server/Controller/OtherLookup.pm @@ -18,7 +18,11 @@ sub lookup_handler { $self->$code($c, $form->field($name)->value); } else { - $c->stash( template => 'otherlookup/index.tt' ); + $c->stash( + current_view => 'Node', + component_path => 'otherlookup/OtherLookupIndex', + component_props => {form => $form}, + ); } }; @@ -88,26 +92,8 @@ lookup_handler 'isrc' => sub { lookup_handler 'iswc' => sub { my ($self, $c, $iswc) = @_; - my @works = $c->model('Work')->find_by_iswc($iswc); - if (@works == 1) { - my $work = $works[0]; - $c->response->redirect( - $c->uri_for_action( - $c->controller('Work')->action_for('show'), - [ $work->gid ])); - $c->detach; - } - elsif (@works > 1) { - $c->model('Work')->load_writers(@works); - $c->model('Work')->load_recording_artists(@works); - $c->stash( - works => \@works, - template => 'otherlookup/results-work.tt' - ); - } - else { - $self->not_found($c); - } + $c->response->redirect($c->uri_for_action('/iswc/show', [ $iswc ])); + $c->detach; }; lookup_handler 'artist-ipi' => sub { @@ -191,8 +177,9 @@ lookup_handler 'freedbid' => sub { $c->model('ReleaseGroupType')->load(map { $_->release_group } @releases); $c->stash( - results => \@releases, - template => 'otherlookup/results-release.tt' + current_view => 'Node', + component_path => 'otherlookup/OtherLookupReleaseResults', + component_props => {results => \@releases}, ) }; @@ -200,6 +187,12 @@ sub index : Path('') { my ($self, $c) = @_; my $form = $c->form( other_lookup => 'OtherLookup' ); + + $c->stash( + current_view => 'Node', + component_path => 'otherlookup/OtherLookupIndex', + component_props => {form => $form}, + ); } 1; diff --git a/lib/MusicBrainz/Server/Controller/Role/Alias.pm b/lib/MusicBrainz/Server/Controller/Role/Alias.pm index a635400d2db..bb747109d51 100644 --- a/lib/MusicBrainz/Server/Controller/Role/Alias.pm +++ b/lib/MusicBrainz/Server/Controller/Role/Alias.pm @@ -70,8 +70,21 @@ sub add_alias : Chained('load') PathPart('add-alias') Edit my ($self, $c) = @_; my $type = $self->{entity_name}; my $entity = $c->stash->{ $type }; + my $form_type = 'add'; my $alias_model = $c->model( $self->{model} )->alias; - $c->stash( template => 'entity/alias/add.tt' ); + + my %props = ( + type => $type, + entity => $entity, + formType => $form_type, + ); + + $c->stash( + component_path => 'entity/alias/AddOrEditAlias', + component_props => \%props, + current_view => 'Node', + ); + $self->edit_action($c, form => 'Alias', form_args => { @@ -87,7 +100,13 @@ sub add_alias : Chained('load') PathPart('add-alias') Edit name => $entity->name, id => $entity->id }, - on_creation => sub { $self->_redir_to_aliases($c) } + on_creation => sub { $self->_redir_to_aliases($c) }, + pre_validation => sub { + my $form = shift; + $props{form} = $form; + $props{aliasTypes} = $form->options_type_id; + $props{locales} = $form->options_locale; + } ); } @@ -95,8 +114,22 @@ sub delete_alias : Chained('alias') PathPart('delete') Edit { my ($self, $c) = @_; my $alias = $c->stash->{alias}; + my $type = $self->{entity_name}; + my $entity = $c->stash->{ $type }; my $edit = $c->model('Edit')->find_creation_edit($model_to_edit_type{add}->{ $self->{model} }, $alias->id, id_field => 'alias_id'); - $c->stash( template => 'entity/alias/delete.tt' ); + + my %props = ( + alias => $alias, + entity => $entity, + type => $type, + ); + + $c->stash( + component_path => 'entity/alias/DeleteAlias', + component_props => \%props, + current_view => 'Node', + ); + cancel_or_action($c, $edit, $self->_aliases_url($c), sub { $self->edit_action($c, form => 'Confirm', @@ -106,7 +139,11 @@ sub delete_alias : Chained('alias') PathPart('delete') Edit alias => $alias, entity => $c->stash->{ $self->{entity_name} } }, - on_creation => sub { $self->_redir_to_aliases($c) } + on_creation => sub { $self->_redir_to_aliases($c) }, + pre_validation => sub { + my $form = shift; + $props{form} = $form; + } ); }); } @@ -117,8 +154,20 @@ sub edit_alias : Chained('alias') PathPart('edit') Edit my $alias = $c->stash->{alias}; my $type = $self->{entity_name}; my $entity = $c->stash->{ $type }; + my $form_type = 'edit'; my $alias_model = $c->model( $self->{model} )->alias; - $c->stash( template => 'entity/alias/edit.tt' ); + my %props = ( + type => $type, + entity => $entity, + formType => $form_type + ); + + $c->stash( + component_path => 'entity/alias/AddOrEditAlias', + component_props => \%props, + current_view => 'Node', + ); + $self->edit_action($c, form => 'Alias', form_args => { @@ -133,7 +182,13 @@ sub edit_alias : Chained('alias') PathPart('edit') Edit alias => $alias, entity => $c->stash->{ $self->{entity_name} } }, - on_creation => sub { $self->_redir_to_aliases($c) } + on_creation => sub { $self->_redir_to_aliases($c) }, + pre_validation => sub { + my $form = shift; + $props{form} = $form; + $props{aliasTypes} = $form->options_type_id; + $props{locales} = $form->options_locale; + } ); } diff --git a/lib/MusicBrainz/Server/Controller/Role/Delete.pm b/lib/MusicBrainz/Server/Controller/Role/Delete.pm index c40c3cec2ee..9b7e028759d 100644 --- a/lib/MusicBrainz/Server/Controller/Role/Delete.pm +++ b/lib/MusicBrainz/Server/Controller/Role/Delete.pm @@ -25,7 +25,7 @@ role { ($params->create_edit_type ? (create_edit_type => $params->create_edit_type) : ()) ); - after 'show' => sub { + after 'load' => sub { my ($self, $c) = @_; my $entity_name = $self->{entity_name}; my $entity = $c->stash->{ $entity_name }; diff --git a/lib/MusicBrainz/Server/Controller/Root.pm b/lib/MusicBrainz/Server/Controller/Root.pm index b5a221b8156..95f14bb7505 100644 --- a/lib/MusicBrainz/Server/Controller/Root.pm +++ b/lib/MusicBrainz/Server/Controller/Root.pm @@ -12,7 +12,7 @@ BEGIN { extends 'Catalyst::Controller' } use DBDefs; use MusicBrainz::Server::Constants qw( $VARTIST_GID $CONTACT_URL ); use MusicBrainz::Server::ControllerUtils::SSL qw( ensure_ssl ); -use MusicBrainz::Server::Data::Utils qw( type_to_model ); +use MusicBrainz::Server::Data::Utils qw( boolean_to_json type_to_model ); use MusicBrainz::Server::Log qw( log_debug ); use MusicBrainz::Server::Replication ':replication_type'; use aliased 'MusicBrainz::Server::Translation'; @@ -140,8 +140,18 @@ sub error_400 : Private my ($self, $c) = @_; $c->response->status(400); - $c->stash->{template} = 'main/400.tt'; - $c->detach; + + my %props = ( + hostname => $c->stash->{hostname}, + message => $c->stash->{message}, + useLanguages => boolean_to_json($c->stash->{use_languages}), + ); + + $c->stash( + component_path => 'main/error/Error400', + component_props => \%props, + current_view => 'Node', + ); } sub error_401 : Private @@ -149,8 +159,10 @@ sub error_401 : Private my ($self, $c) = @_; $c->response->status(401); - $c->stash->{template} = 'main/401.tt'; - $c->detach; + $c->stash( + component_path => 'main/error/Error401', + current_view => 'Node', + ); } sub error_403 : Private @@ -158,15 +170,21 @@ sub error_403 : Private my ($self, $c) = @_; $c->response->status(403); - $c->stash->{template} = 'main/403.tt'; + $c->stash( + component_path => 'main/error/Error403', + current_view => 'Node', + ); } sub error_404 : Private { my ($self, $c, $message) = @_; $c->response->status(404); - $c->stash->{current_view} = 'Node'; - $c->stash->{component_props}{message} = $message; + $c->stash( + component_path => 'main/error/Error404', + component_props => { message => $message }, + current_view => 'Node', + ); } sub error_500 : Private @@ -174,8 +192,13 @@ sub error_500 : Private my ($self, $c) = @_; $c->response->status(500); - $c->stash->{template} = 'main/500.tt'; - $c->detach; + $c->stash( + component_path => 'main/error/Error500', + component_props => { + useLanguages => boolean_to_json($c->stash->{use_languages}), + }, + current_view => 'Node', + ); } sub error_503 : Private @@ -183,8 +206,10 @@ sub error_503 : Private my ($self, $c) = @_; $c->response->status(503); - $c->stash->{template} = 'main/503.tt'; - $c->detach; + $c->stash( + component_path => 'main/error/Error503', + current_view => 'Node', + ); } sub error_mirror : Private @@ -192,8 +217,10 @@ sub error_mirror : Private my ($self, $c) = @_; $c->response->status(403); - $c->stash->{template} = 'main/mirror.tt'; - $c->detach; + $c->stash( + component_path => 'main/error/MirrorError403', + current_view => 'Node', + ); } sub error_mirror_404 : Private @@ -201,8 +228,10 @@ sub error_mirror_404 : Private my ($self, $c) = @_; $c->response->status(404); - $c->stash->{template} = 'main/mirror_404.tt'; - $c->detach; + $c->stash( + component_path => 'main/error/MirrorError404', + current_view => 'Node', + ); } sub begin : Private @@ -361,19 +390,19 @@ sub begin : Private if (exists $attributes->{Edit} && $c->user_exists && (!$c->user->has_confirmed_email_address || $c->user->is_editing_disabled)) { - $c->forward('/error_401'); + $c->detach('/error_401'); } if (DBDefs->DB_READ_ONLY && (exists $attributes->{Edit} || exists $attributes->{DenyWhenReadonly})) { $c->stash( message => 'The server is currently in read only mode and is not accepting edits'); - $c->forward('/error_400'); + $c->detach('/error_400'); } # Update the tagger port if (defined $c->req->query_params->{tport}) { my ($tport) = $c->req->query_params->{tport} =~ /^([0-9]{1,5})$/ - or $c->forward('/error_400'); + or $c->detach('/error_400'); $c->session->{tport} = $tport; } diff --git a/lib/MusicBrainz/Server/Controller/Search.pm b/lib/MusicBrainz/Server/Controller/Search.pm index f0a77f7aa43..c48eb849f9a 100644 --- a/lib/MusicBrainz/Server/Controller/Search.pm +++ b/lib/MusicBrainz/Server/Controller/Search.pm @@ -80,7 +80,15 @@ sub search : Path('') } else { - $c->stash( template => 'search/index.tt' ); + $c->stash( + component_path => 'search/SearchIndex', + component_props => { + otherLookupForm => $c->stash->{otherlookup}, + searchForm => $c->stash->{form}, + tagLookupForm => $c->stash->{taglookup}, + }, + current_view => 'Node', + ); } } @@ -125,6 +133,7 @@ sub direct : Private when ('release') { $c->model('Language')->load(@entities); $c->model('Release')->load_related_info(@entities); + $c->model('Release')->load_meta(@entities); $c->model('Script')->load(@entities); $c->model('ReleaseStatus')->load(@entities); $c->model('ReleaseGroup')->load(@entities); diff --git a/lib/MusicBrainz/Server/Controller/WS/js/Edit.pm b/lib/MusicBrainz/Server/Controller/WS/js/Edit.pm index 2bec63f7f2b..0f44459d51e 100644 --- a/lib/MusicBrainz/Server/Controller/WS/js/Edit.pm +++ b/lib/MusicBrainz/Server/Controller/WS/js/Edit.pm @@ -42,7 +42,12 @@ use MusicBrainz::Server::Data::Utils qw( ); use MusicBrainz::Server::Renderer qw( render_component ); use MusicBrainz::Server::Translation qw( comma_list comma_only_list l ); -use MusicBrainz::Server::Validation qw( is_guid is_valid_url is_valid_partial_date ); +use MusicBrainz::Server::Validation qw( + is_database_row_id + is_guid + is_valid_url + is_valid_partial_date +); use MusicBrainz::Server::View::Base; use Readonly; use Scalar::Util qw( looks_like_number ); @@ -244,7 +249,7 @@ sub process_release_events { sub process_artist_credits { my ($c, $loader, @artist_credits) = @_; - my @artist_gids; + my @artist_ids; for my $ac (@artist_credits) { my @names = @{ $ac->{names} }; @@ -261,26 +266,34 @@ sub process_artist_credits { trim_string($name, 'name'); trim_string($artist, 'name'); - if (!$artist->{id} && is_guid($artist->{gid})) { - push @artist_gids, $artist->{gid}; + if (is_database_row_id($artist->{id})) { + push @artist_ids, $artist->{id}; + } elsif (is_guid($artist->{gid})) { + push @artist_ids, $artist->{gid}; } $i++; } } - return unless @artist_gids; - - my $artists = $c->model('Artist')->get_by_gids(@artist_gids); + my $artists = $c->model('Artist')->get_by_any_ids(@artist_ids); for my $ac (@artist_credits) { my @names = @{ $ac->{names} }; for my $name (@names) { my $artist = $name->{artist}; - my $gid = delete $artist->{gid}; + my $given_id = $artist->{id}; + my $given_gid = $artist->{gid}; + my $entity = + (defined $given_id ? $artists->{$given_id} : undef) // + (defined $given_gid ? $artists->{$given_gid} : undef); - if ($gid and my $entity = $artists->{$gid}) { + if (defined $entity) { $artist->{id} = $entity->id; + $artist->{gid} = $entity->gid; + $artist->{name} = $entity->name; + $name->{name} = $entity->name + unless non_empty($name->{name}); } } } diff --git a/lib/MusicBrainz/Server/Data/Artist.pm b/lib/MusicBrainz/Server/Data/Artist.pm index fe770b26dde..86a4fa12ce9 100644 --- a/lib/MusicBrainz/Server/Data/Artist.pm +++ b/lib/MusicBrainz/Server/Data/Artist.pm @@ -3,8 +3,13 @@ use Moose; use namespace::autoclean; use Carp; -use List::MoreUtils qw( uniq ); -use MusicBrainz::Server::Constants qw( $VARTIST_ID $DARTIST_ID $STATUS_OPEN ); +use List::MoreUtils qw( any uniq ); +use MusicBrainz::Server::Constants qw( + $VARTIST_ID + $DARTIST_ID + $STATUS_OPEN + $ARTIST_TYPE_GROUP +); use MusicBrainz::Server::Entity::Artist; use MusicBrainz::Server::Entity::PartialDate; use MusicBrainz::Server::Data::ArtistCredit; @@ -12,6 +17,7 @@ use MusicBrainz::Server::Data::Edit; use MusicBrainz::Server::Data::Utils qw( is_special_artist add_partial_date_to_row + conditional_merge_column_query defined_hash get_area_containment_query hash_to_row @@ -334,18 +340,66 @@ sub merge $self->c->model('Collection')->merge_entities('artist', $new_id, @$old_ids); $self->c->model('Relationship')->merge_entities('artist', $new_id, $old_ids, rename_credits => $opts{rename}); + # We detect cases where a merged artist type or gender is dropped due to + # it conflicting with a type or gender on the target or elsewhere (since + # only persons can have a gender). In Edit::Artist::Merge, we use the + # result of %dropped_columns to inform users of what information was + # dropped. This is done as opposed to failing the edit outright, since + # 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 type ) ]; - my $artist_type = $self->sql->select_single_value('SELECT type FROM artist WHERE id = ?', $new_id); - my $group_type = 2; - my $orchestra_type = 5; - my $choir_type = 6; - if ( - !defined $artist_type || - ($artist_type != $group_type && $artist_type != $orchestra_type && $artist_type != $choir_type) - ) { - push @$merge_columns, 'gender'; + 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 $target_type = $target_row->{type}; + my $target_gender = $target_row->{gender}; + my $merged_type = $target_type; + my $merged_gender = $target_gender; + + if (!$merged_type) { + my ($query, $args) = conditional_merge_column_query( + 'artist', 'type', $new_id, [$new_id, @$old_ids], 'IS NOT NULL'); + $merged_type = $self->c->sql->select_single_value($query, @$args); + } + + if (!$merged_gender) { + my ($query, $args) = conditional_merge_column_query( + 'artist', 'gender', $new_id, [$new_id, @$old_ids], 'IS NOT NULL'); + $merged_gender = $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) + UNION + SELECT artist_type.id + FROM artist_type + JOIN atp ON atp.id = artist_type.parent + ) SELECT * FROM atp + }, $ARTIST_TYPE_GROUP); + + my $merged_type_is_group = + defined $merged_type && + any { $merged_type eq $_ } @$group_types; + + if ($merged_type_is_group && $merged_gender) { + my $target_type_is_group = + defined $target_type && + any { $target_type eq $_ } @$group_types; + + 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, qw( gender type ); + } + merge_table_attributes( $self->sql => ( table => 'artist', @@ -365,7 +419,7 @@ sub merge } $self->_delete_and_redirect_gids('artist', $new_id, @$old_ids); - return 1; + return (1, \%dropped_columns); } sub _hash_to_row diff --git a/lib/MusicBrainz/Server/Data/Editor.pm b/lib/MusicBrainz/Server/Data/Editor.pm index 8498a2049be..d6cce038f16 100644 --- a/lib/MusicBrainz/Server/Data/Editor.pm +++ b/lib/MusicBrainz/Server/Data/Editor.pm @@ -651,33 +651,42 @@ sub added_entities_counts { return $cached_result if defined $cached_result; my %result = map { $_ => 0 } - qw( artist release cover_art event label place series work other ); + qw( artist release area cover_art event instrument label place recording + releasegroup series work other ); my $query = q{SELECT CASE WHEN type = ? THEN 'artist' WHEN type IN (?, ?) THEN 'release' + WHEN type = ? THEN 'area' WHEN type = ? THEN 'cover_art' WHEN type = ? THEN 'event' + WHEN type = ? THEN 'instrument' WHEN type = ? THEN 'label' WHEN type = ? THEN 'place' + WHEN type = ? THEN 'recording' + WHEN type = ? THEN 'releasegroup' WHEN type = ? THEN 'series' WHEN type = ? THEN 'work' ELSE 'other' END AS type, COUNT(*) AS count FROM edit - WHERE editor = ? + WHERE edit.status = ? + AND editor = ? GROUP BY type}; - my @params = ($EDIT_ARTIST_CREATE, $EDIT_RELEASE_CREATE, $EDIT_HISTORIC_ADD_RELEASE, - $EDIT_RELEASE_ADD_COVER_ART, $EDIT_EVENT_CREATE, $EDIT_LABEL_CREATE, - $EDIT_PLACE_CREATE, $EDIT_SERIES_CREATE, $EDIT_WORK_CREATE); + my @params = ($EDIT_ARTIST_CREATE, $EDIT_RELEASE_CREATE, + $EDIT_HISTORIC_ADD_RELEASE, $EDIT_AREA_CREATE, $EDIT_RELEASE_ADD_COVER_ART, + $EDIT_EVENT_CREATE, $EDIT_INSTRUMENT_CREATE, $EDIT_LABEL_CREATE, + $EDIT_PLACE_CREATE, $EDIT_RECORDING_CREATE, $EDIT_RELEASEGROUP_CREATE, + $EDIT_SERIES_CREATE, $EDIT_WORK_CREATE, $STATUS_APPLIED); my $rows = $self->sql->select_list_of_lists($query, @params, $editor_id); for my $row (@$rows) { my ($type, $count) = @$row; - $result{$type} = $count; + # We just ignore any edits that are not one of the desired types + $result{$type} = $count unless $type eq 'other'; } $self->c->cache->set($cache_key, \%result, 60 * 60 * 24); diff --git a/lib/MusicBrainz/Server/Data/Recording.pm b/lib/MusicBrainz/Server/Data/Recording.pm index 1671dfe2501..5d02be57605 100644 --- a/lib/MusicBrainz/Server/Data/Recording.pm +++ b/lib/MusicBrainz/Server/Data/Recording.pm @@ -112,6 +112,18 @@ sub find_by_artist $self->query_to_list_limited($query, \@where_args, $limit, $offset); } +sub find_by_artist_credit +{ + my ($self, $artist_credit_id, $limit, $offset) = @_; + + my $query = "SELECT " . $self->_columns . ", + name COLLATE musicbrainz AS name_collate + FROM " . $self->_table . " + WHERE artist_credit = ? + ORDER BY name COLLATE musicbrainz"; + $self->query_to_list_limited($query, [$artist_credit_id], $limit, $offset); +} + sub find_by_instrument { my ($self, $instrument_id, $limit, $offset) = @_; diff --git a/lib/MusicBrainz/Server/Data/Release.pm b/lib/MusicBrainz/Server/Data/Release.pm index bcdf611f77d..47114cb6ed2 100644 --- a/lib/MusicBrainz/Server/Data/Release.pm +++ b/lib/MusicBrainz/Server/Data/Release.pm @@ -245,6 +245,18 @@ sub find_by_artist $self->query_to_list_limited($query, $params, $limit, $offset, undef, cache_hits => 1); } +sub find_by_artist_credit +{ + my ($self, $artist_credit_id, $limit, $offset) = @_; + + my $query = "SELECT " . $self->_columns . ", + release.name COLLATE musicbrainz AS name_collate + FROM " . $self->_table . " + WHERE artist_credit = ? + ORDER BY release.name COLLATE musicbrainz"; + $self->query_to_list_limited($query, [$artist_credit_id], $limit, $offset); +} + sub find_by_instrument { my ($self, $instrument_id, $limit, $offset, %args) = @_; @@ -1089,7 +1101,7 @@ sub determine_recording_merges { return (0, { message => $RELEASE_MERGE_ERRORS{ambiguous_recording_merge}, - args => { + vars => { source_recording => _link_recording($source), target_recordings => comma_list(map { _link_recording($_) } @{$target}), }, @@ -1117,7 +1129,7 @@ sub determine_recording_merges { if ($new_id == $old_id) { return (0, { message => $RELEASE_MERGE_ERRORS{recording_merge_cycle}, - args => { + vars => { recording1 => _link_recording($old_recording), recording2 => _link_recording($new_recording), }, @@ -1349,6 +1361,31 @@ sub _hash_to_row return $row; } +=method load_ids + +Load internal IDs for release objects that only have GIDs. + +=cut + +sub load_ids +{ + my ($self, @releases) = @_; + + my @gids = map { $_->gid } @releases; + return () unless @gids; + + my $query = " + SELECT gid, id FROM release + WHERE gid IN (" . placeholders(@gids) . ") + "; + my %map = map { $_->[0] => $_->[1] } + @{ $self->sql->select_list_of_lists($query, @gids) }; + + for my $release (@releases) { + $release->id($map{$release->gid}) if exists $map{$release->gid}; + } +} + sub load_meta { my $self = shift; diff --git a/lib/MusicBrainz/Server/Data/ReleaseGroup.pm b/lib/MusicBrainz/Server/Data/ReleaseGroup.pm index 6150a2e7fc3..fc4084bae07 100644 --- a/lib/MusicBrainz/Server/Data/ReleaseGroup.pm +++ b/lib/MusicBrainz/Server/Data/ReleaseGroup.pm @@ -127,6 +127,59 @@ sub find_artist_credits_by_artist return $self->c->model('ArtistCredit')->find_by_ids($ids); } +sub pick_status_condition +{ + my ($self, $query_extra_only) = @_; + + if ($query_extra_only) { + return ' + AND ( + NOT EXISTS ( + SELECT 1 FROM release + WHERE release.release_group = rg.id + AND release.status = 1 + ) AND EXISTS ( + SELECT 1 FROM release + WHERE release.release_group = rg.id + AND release.status IS NOT NULL + ) + ) + '; + } else { + return ' + AND ( + EXISTS ( + SELECT 1 FROM release + WHERE release.release_group = rg.id + AND release.status = 1 + ) OR NOT EXISTS ( + SELECT 1 FROM release + WHERE release.release_group = rg.id + AND release.status IS NOT NULL + ) + ) + '; + } +} + +sub has_by_artist +{ + my ($self, $artist_id, $query_extra_only) = @_; + + my $status_condition = $self->pick_status_condition($query_extra_only); + + my $query =" + SELECT EXISTS ( + SELECT 1 + FROM release_group rg + JOIN artist_credit_name acn + ON acn.artist_credit = rg.artist_credit + WHERE acn.artist = ? + $status_condition + )"; + $self->sql->select_single_value($query, $artist_id); +} + sub find_by_artist { my ($self, $artist_id, $show_all, $limit, $offset, %args) = @_; @@ -184,6 +237,36 @@ sub find_by_artist ); } +sub has_by_track_artist +{ + my ($self, $artist_id, $query_extra_only) = @_; + + my $status_condition = $self->pick_status_condition($query_extra_only); + + my $query =" + SELECT EXISTS ( + SELECT 1 + FROM release_group rg + WHERE rg.id IN ( + SELECT release_group FROM release + JOIN medium + ON medium.release = release.id + JOIN track tr + ON tr.medium = medium.id + JOIN artist_credit_name acn + ON acn.artist_credit = tr.artist_credit + WHERE acn.artist = ? + ) + AND rg.id NOT IN ( + SELECT id FROM release_group + JOIN artist_credit_name acn + ON release_group.artist_credit = acn.artist_credit + WHERE acn.artist = ?) + $status_condition + )"; + $self->sql->select_single_value($query, $artist_id, $artist_id); +} + sub find_by_track_artist { my ($self, $artist_id, $show_all, $limit, $offset) = @_; @@ -252,6 +335,18 @@ sub find_by_track_artist ); } +sub find_by_artist_credit +{ + my ($self, $artist_credit_id, $limit, $offset) = @_; + + my $query = "SELECT " . $self->_columns . ", + rg.name COLLATE musicbrainz AS name_collate + FROM " . $self->_table . " + WHERE rg.artist_credit = ? + ORDER BY rg.name COLLATE musicbrainz"; + $self->query_to_list_limited($query, [$artist_credit_id], $limit, $offset); +} + sub find_by_release { my ($self, $release_id, $limit, $offset) = @_; diff --git a/lib/MusicBrainz/Server/Data/Role/IPI.pm b/lib/MusicBrainz/Server/Data/Role/IPI.pm index 5664779b2e7..2ce54b0470a 100644 --- a/lib/MusicBrainz/Server/Data/Role/IPI.pm +++ b/lib/MusicBrainz/Server/Data/Role/IPI.pm @@ -21,6 +21,31 @@ role value_type => 'ipi', }; + method find_reused_ipis => sub { + my ($self, @ipis) = @_; + my $query = "SELECT 'artist' AS entity_type, artist_ipi.ipi, COUNT(*) AS count + FROM artist + JOIN artist_ipi ON artist.id = artist_ipi.artist + WHERE artist_ipi.ipi = any(?) + GROUP BY artist_ipi.ipi + UNION ALL + SELECT 'label' AS entity_type, label_ipi.ipi, COUNT(*) AS count + FROM label + JOIN label_ipi ON label.id = label_ipi.label + WHERE label_ipi.ipi = any(?) + GROUP BY label_ipi.ipi"; + my $results = $self->sql->select_list_of_hashes($query, \@ipis, \@ipis); + my %reused_ipis; + for my $result (@$results) { + my $ipi = $result->{ipi}; + my $entity_count = $result->{count}; + my $entity_type = $result->{entity_type}; + $reused_ipis{$ipi}{$entity_type} = $entity_count; + } + + return \%reused_ipis; + }; + }; no Moose::Role; diff --git a/lib/MusicBrainz/Server/Data/Role/ISNI.pm b/lib/MusicBrainz/Server/Data/Role/ISNI.pm index 2672178825d..bfdbd20e16c 100644 --- a/lib/MusicBrainz/Server/Data/Role/ISNI.pm +++ b/lib/MusicBrainz/Server/Data/Role/ISNI.pm @@ -21,6 +21,31 @@ role value_type => 'isni', }; + method find_reused_isnis => sub { + my ($self, @isnis) = @_; + my $query = "SELECT 'artist' AS entity_type, artist_isni.isni, COUNT(*) AS count + FROM artist + JOIN artist_isni ON artist.id = artist_isni.artist + WHERE artist_isni.isni = any(?) + GROUP BY artist_isni.isni + UNION ALL + SELECT 'label' AS entity_type, label_isni.isni, COUNT(*) AS count + FROM label + JOIN label_isni ON label.id = label_isni.label + WHERE label_isni.isni = any(?) + GROUP BY label_isni.isni"; + my $results = $self->sql->select_list_of_hashes($query, \@isnis, \@isnis); + my %reused_isnis; + for my $result (@$results) { + my $isni = $result->{isni}; + my $entity_count = $result->{count}; + my $entity_type = $result->{entity_type}; + $reused_isnis{$isni}{$entity_type} = $entity_count; + } + + return \%reused_isnis; + }; + }; no Moose::Role; diff --git a/lib/MusicBrainz/Server/Data/Search.pm b/lib/MusicBrainz/Server/Data/Search.pm index f0982a109c2..ef901b676e7 100644 --- a/lib/MusicBrainz/Server/Data/Search.pm +++ b/lib/MusicBrainz/Server/Data/Search.pm @@ -904,6 +904,13 @@ sub external_search $self->c->model('Event')->load_areas(@entities); } + if ($type eq 'release') + { + my @entities = map { $_->entity } @results; + $self->c->model('Release')->load_ids(@entities); + $self->c->model('Release')->load_meta(@entities); + } + my $pager = Data::Page->new; $pager->current_page($page); $pager->entries_per_page($limit); diff --git a/lib/MusicBrainz/Server/Data/Series.pm b/lib/MusicBrainz/Server/Data/Series.pm index ec7da1f3551..8612e8c844c 100644 --- a/lib/MusicBrainz/Server/Data/Series.pm +++ b/lib/MusicBrainz/Server/Data/Series.pm @@ -154,10 +154,16 @@ sub update { my $series = $self->c->model('Series')->get_by_id($series_id); $self->c->model('SeriesType')->load($series); - if (defined($row->{type}) && $series->type_id != $row->{type}) { - my ($items, $hits) = $self->c->model('Series')->get_entities($series, 1, 0); + if (defined($row->{type})) { + my $existing_entity_type = $series->type->item_entity_type; + my $new_series_type = $self->c->model('SeriesType')->get_by_id($row->{type}); + my $new_entity_type = $new_series_type->item_entity_type; - die "Cannot change the type of a non-empty series" if scalar(@$items); + if ($existing_entity_type ne $new_entity_type) { + my ($items, $hits) = $self->c->model('Series')->get_entities($series, 1, 0); + + die "Cannot change the entity type of a non-empty series" if scalar(@$items); + } } $self->sql->update_row('series', $row, { id => $series_id }) if %$row; diff --git a/lib/MusicBrainz/Server/Data/Track.pm b/lib/MusicBrainz/Server/Data/Track.pm index fe6de2d2ba4..0ab40c22ea2 100644 --- a/lib/MusicBrainz/Server/Data/Track.pm +++ b/lib/MusicBrainz/Server/Data/Track.pm @@ -87,6 +87,18 @@ sub load_for_mediums } } +sub find_by_artist_credit +{ + my ($self, $artist_credit_id, $limit, $offset) = @_; + + my $query = "SELECT " . $self->_columns . ", + name COLLATE musicbrainz AS name_collate + FROM " . $self->_table . " + WHERE artist_credit = ? + ORDER BY name COLLATE musicbrainz"; + $self->query_to_list_limited($query, [$artist_credit_id], $limit, $offset); +} + sub find_by_recording { my ($self, $recording_id, $limit, $offset) = @_; diff --git a/lib/MusicBrainz/Server/Data/URL.pm b/lib/MusicBrainz/Server/Data/URL.pm index 5c32ef62d21..8487b04cbc5 100644 --- a/lib/MusicBrainz/Server/Data/URL.pm +++ b/lib/MusicBrainz/Server/Data/URL.pm @@ -50,8 +50,6 @@ my %URL_SPECIALIZATIONS = ( 'DAHR' => qr{^https?://adp\.library\.ucsb\.edu/}i, 'Dailymotion' => qr{^https?://(?:www\.)?dailymotion\.com/}i, 'DanceDB' => qr{^https?://(?:www\.)?tedcrane\.com/DanceDB/}i, - # Temporarily grey Decoda out as it has been reported to be compromised - # 'Decoda' => qr{^https?://(?:www\.)?decoda\.com/}i, 'Deezer' => qr{^https?://(?:www\.)?deezer\.com/}i, 'DHHU' => qr{^https?://(?:www\.)?dhhu\.dk/}i, 'Directlyrics' => qr{^https?://(?:www\.)?directlyrics\.com/}i, @@ -127,6 +125,7 @@ my %URL_SPECIALIZATIONS = ( 'OpenLibrary' => qr{^https?://(?:www\.)?openlibrary\.org/}i, 'Operabase' => qr{^https?://(?:www\.)?operabase\.com/}i, 'Operadis' => qr{^https?://(?:www\.)?operadis-opera-discography\.org\.uk/}i, + 'Overture' => qr{^https?://overture\.doremus\.org/}i, 'Ozon' => qr{^https?://(?:www\.)?ozon\.ru/}i, 'Patreon' => qr{^https?://(?:www\.)?patreon\.com/}i, 'PayPalMe' => qr{^https?://(?:www\.)?paypal\.me/}i, @@ -137,7 +136,8 @@ my %URL_SPECIALIZATIONS = ( 'PsyDB' => qr{^https?://(?:www\.)?psydb\.net/}i, 'QuebecInfoMusique' => qr{^https?://(?:www\.)?qim\.com/}i, 'Rateyourmusic' => qr{^https?://(?:www\.)?rateyourmusic\.com/}i, - 'ResidentAdvisor' => qr{^https?://(?:www\.)?residentadvisor\.net/}i, + # Remove residentadvisor.net once MBBE-31 is done + 'ResidentAdvisor' => qr{^https?://(?:www\.)?(ra\.co|residentadvisor\.net)/}i, 'ReverbNation' => qr{^https?://(?:www\.)?reverbnation\.com/}i, 'RockComAr' => qr{^https?://(?:www\.)?rock\.com\.ar/}i, 'RockensDanmarkskort' => qr{^https?://(?:www\.)?rockensdanmarkskort\.dk/}i, diff --git a/lib/MusicBrainz/Server/Data/Utils.pm b/lib/MusicBrainz/Server/Data/Utils.pm index 46d4cfc774b..eeb6525386e 100644 --- a/lib/MusicBrainz/Server/Data/Utils.pm +++ b/lib/MusicBrainz/Server/Data/Utils.pm @@ -42,6 +42,7 @@ our @EXPORT_OK = qw( boolean_from_json boolean_to_json check_data + conditional_merge_column_query copy_escape coordinates_to_hash datetime_to_iso8601 @@ -510,32 +511,49 @@ sub _merge_attributes { $sql->do($query_generator->($table, $new_id, $old_ids, $all_ids, \%named_params)); } +sub conditional_merge_column_query { + my ($table, $column, $new_id, $all_ids, $condition, $default) = @_; + + my @args = ($new_id, $all_ids); + my $query = + "(SELECT new_val + FROM (SELECT (id = ?) AS first, $column AS new_val + FROM $table + WHERE $column $condition + AND id = any(?) + ORDER BY first DESC + LIMIT 1) s)"; + if (defined $default) { + $query = "coalesce($query, ?)"; + push @args, $default; + } + return ($query, \@args); +} sub _conditional_merge { my ($condition, %opts) = @_; - my $wrap_coalesce = sub { - my ($inner, $wrap) = @_; - if ($wrap) { return "coalesce(" . $inner . ",?)" } - else { return $inner } - }; - return sub { - my ($table, $new_id, $old_ids, $all_ids, $named_params) = @_; - my $columns = $named_params->{columns} or confess 'Missing parameter columns'; - ("UPDATE $table SET " . - join(',', map { - "$_ = " . $wrap_coalesce->("(SELECT new_val FROM ( - SELECT (id = ?) AS first, $_ AS new_val - FROM $table - WHERE $_ $condition - AND id IN (" . placeholders(@$all_ids) . ") - ORDER BY first DESC - LIMIT 1 - ) s)", exists $opts{default}); - } @$columns) . ' - WHERE id = ?', - (@$all_ids, $new_id) x @$columns, (exists $opts{default} ? $opts{default} : ()), $new_id)} + my ($table, $new_id, $old_ids, $all_ids, $named_params) = @_; + my $columns = $named_params->{columns} or confess 'Missing parameter columns'; + my @assignment_args; + my $column_assignments = join(', ', map { + my $column = $_; + my ($column_query, $column_args) = + conditional_merge_column_query( + $table, + $column, + $new_id, + $all_ids, + $condition, + $opts{default}, + ); + push @assignment_args, @{$column_args}; + "$column = ($column_query)" + } @$columns); + ("UPDATE $table SET $column_assignments WHERE id = ?", + @assignment_args, $new_id); + }; } sub merge_table_attributes { @@ -689,12 +707,13 @@ sub datetime_to_iso8601 { } sub localized_note { - my ($message, $args) = @_; + my ($message, %opts) = @_; state $json = JSON::XS->new; 'localize:' . $json->encode({ message => $message, - defined $args ? (args => $args) : (), + version => 1, + %opts, }); } diff --git a/lib/MusicBrainz/Server/Edit.pm b/lib/MusicBrainz/Server/Edit.pm index 916b4010a25..5c3af6541ed 100644 --- a/lib/MusicBrainz/Server/Edit.pm +++ b/lib/MusicBrainz/Server/Edit.pm @@ -337,6 +337,7 @@ sub TO_JSON { expires_time => datetime_to_iso8601($self->expires_time), historic_type => $self->can('historic_type') ? $self->historic_type + 0 : undef, id => $self->id + 0, + is_loaded => boolean_to_json($self->is_loaded), is_open => boolean_to_json($self->is_open), $can_preview ? (preview => boolean_to_json($self->preview)) : (), status => $self->status + 0, diff --git a/lib/MusicBrainz/Server/Edit/Artist/Edit.pm b/lib/MusicBrainz/Server/Edit/Artist/Edit.pm index 9bd973a1225..0067bf96bc5 100644 --- a/lib/MusicBrainz/Server/Edit/Artist/Edit.pm +++ b/lib/MusicBrainz/Server/Edit/Artist/Edit.pm @@ -193,9 +193,33 @@ around allow_auto_edit => sub { return 0 if exists $self->data->{old}{end_area_id} && defined($self->data->{old}{end_area_id}) && $self->data->{old}{end_area_id} != 0; - return 0 if $self->data->{new}{ipi_codes}; + if (defined $self->data->{new}{ipi_codes}) { + # If there's already IPIs for the artist, not an autoedit + if (@{ $self->data->{old}{ipi_codes} // [] }) { + return 0; + } + + # If there's already an entity with any of the IPIs, not an autoedit + my $reused_ipis = $self->reused_ipis; - return 0 if $self->data->{new}{isni_codes}; + if (%$reused_ipis) { + return 0; + } + } + + if (defined $self->data->{new}{isni_codes}) { + # If there's already ISNIs for the artist, not an autoedit + if (@{ $self->data->{old}{isni_codes} // [] }) { + return 0; + } + + # If there's already an entity with any of the ISNIs, not an autoedit + my $reused_isnis = $self->reused_isnis; + + if (%$reused_isnis) { + return 0; + } + } return $self->$orig(@args); }; diff --git a/lib/MusicBrainz/Server/Edit/Artist/EditArtistCredit.pm b/lib/MusicBrainz/Server/Edit/Artist/EditArtistCredit.pm index d3b05cfde2a..693c2be752f 100644 --- a/lib/MusicBrainz/Server/Edit/Artist/EditArtistCredit.pm +++ b/lib/MusicBrainz/Server/Edit/Artist/EditArtistCredit.pm @@ -90,6 +90,12 @@ sub build_display_data old => artist_credit_from_loaded_definition($loaded, $self->data->{old}{artist_credit}) }; + my $old_ac_id = $self->c->model('ArtistCredit')->find($self->data->{old}{artist_credit}); + + if ($old_ac_id) { + $data->{artist_credit}{old}{id} = $old_ac_id; + } + return $data; } diff --git a/lib/MusicBrainz/Server/Edit/Artist/Merge.pm b/lib/MusicBrainz/Server/Edit/Artist/Merge.pm index 950e39d6d41..bd67365ce0f 100644 --- a/lib/MusicBrainz/Server/Edit/Artist/Merge.pm +++ b/lib/MusicBrainz/Server/Edit/Artist/Merge.pm @@ -1,10 +1,15 @@ package MusicBrainz::Server::Edit::Artist::Merge; +use utf8; use Moose; use MooseX::Types::Moose qw( ArrayRef Bool Int Str ); use MooseX::Types::Structured qw( Dict ); -use MusicBrainz::Server::Constants qw( $EDIT_ARTIST_MERGE ); -use MusicBrainz::Server::Data::Utils qw( boolean_to_json ); +use MusicBrainz::Server::Constants qw( $EDIT_ARTIST_MERGE $EDITOR_MODBOT ); +use MusicBrainz::Server::Data::Utils qw( + boolean_to_json + conditional_merge_column_query + localized_note +); use MusicBrainz::Server::Translation qw( N_l ); use Hash::Merge qw( merge ); @@ -51,11 +56,65 @@ sub do_merge { my $self = shift; - $self->c->model('Artist')->merge( - $self->new_entity->{id}, - [ $self->_old_ids ], + my $new_id = $self->new_entity->{id}; + my @old_ids = $self->_old_ids; + my $all_ids = [$new_id, @old_ids]; + + my (undef, $dropped_columns) = $self->c->model('Artist')->merge( + $new_id, + \@old_ids, rename => $self->data->{rename} ); + + if ($dropped_columns->{type}) { + my $dropped_type = + $self->c->model('ArtistType')->get_by_id($dropped_columns->{type}); + + $self->c->model('EditNote')->add_note( + $self->id => { + editor_id => $EDITOR_MODBOT, + text => localized_note( + '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( + '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.', + vars => { + gender => localized_note( + $dropped_gender->name, + function => 'lp', + domain => 'attributes', + args => ['gender'], + ), + }, + ), + }, + ); + } }; around _build_related_entities => sub { diff --git a/lib/MusicBrainz/Server/Edit/Label/Edit.pm b/lib/MusicBrainz/Server/Edit/Label/Edit.pm index 13bc959fb32..3079e6a7dfb 100644 --- a/lib/MusicBrainz/Server/Edit/Label/Edit.pm +++ b/lib/MusicBrainz/Server/Edit/Label/Edit.pm @@ -171,9 +171,33 @@ around allow_auto_edit => sub { # Don't allow an autoedit if the area changed return 0 if defined $self->data->{old}{area_id}; - return 0 if $self->data->{new}{ipi_codes}; + if (defined $self->data->{new}{ipi_codes}) { + # If there's already IPIs for the label, not an autoedit + if (@{ $self->data->{old}{ipi_codes} // [] }) { + return 0; + } + + # If there's already an entity with any of the IPIs, not an autoedit + my $reused_ipis = $self->reused_ipis; - return 0 if $self->data->{new}{isni_codes}; + if (%$reused_ipis) { + return 0; + } + } + + if (defined $self->data->{new}{isni_codes}) { + # If there's already ISNIs for the label, not an autoedit + if (@{ $self->data->{old}{isni_codes} // [] }) { + return 0; + } + + # If there's already an entity with any of the ISNIs, not an autoedit + my $reused_isnis = $self->reused_isnis; + + if (%$reused_isnis) { + return 0; + } + } return $self->$orig(@args); }; diff --git a/lib/MusicBrainz/Server/Edit/Medium/Edit.pm b/lib/MusicBrainz/Server/Edit/Medium/Edit.pm index f4092f4bf36..3ec495813ea 100644 --- a/lib/MusicBrainz/Server/Edit/Medium/Edit.pm +++ b/lib/MusicBrainz/Server/Edit/Medium/Edit.pm @@ -51,6 +51,8 @@ sub edit_type { $EDIT_MEDIUM_EDIT } sub edit_name { N_l('Edit medium') } sub edit_kind { 'edit' } sub _edit_model { 'Medium' } +sub edit_template_react { 'EditMedium' } + sub entity_id { shift->data->{entity_id} } sub medium_id { shift->entity_id } sub release_id { shift->data->{release}{id} } @@ -242,10 +244,16 @@ sub build_display_data my $data = { }; my $release = $loaded->{Release}{ $self->data->{release}{id} } // - Release->new( name => $self->data->{release}{name} ); + Release->new( + id => $self->data->{release}{id}, + name => $self->data->{release}{name}, + ); $data->{medium} = $loaded->{Medium}{ $self->data->{entity_id} } // - Medium->new( release => $release ); + Medium->new( + release_id => $self->data->{release}{id}, + release => $release, + ); if (exists $self->data->{new}{format_id}) { $data->{format} = { @@ -306,7 +314,12 @@ sub build_display_data } if (any {$_->[0] ne 'u' || $_->[1]->number ne $_->[2]->number } @$tracklist_changes) { - $data->{tracklist_changes} = $tracklist_changes; + my @mapped_tracklist_changes = map +{ + change_type => $_->[0], + old_track => $_->[1] eq '' ? undef : $_->[1], + new_track => $_->[2] eq '' ? undef : $_->[2], + }, @$tracklist_changes; + $data->{tracklist_changes} = \@mapped_tracklist_changes; } # Edits that predate track mbids do not store track ids at all. @@ -317,6 +330,11 @@ sub build_display_data } $data->{artist_credit_changes} = [ + map +{ + change_type => $_->[0], + old_track => $_->[1] eq '' ? undef : $_->[1], + new_track => $_->[2], + }, grep { ($_->[1] && hash_artist_credit_without_join_phrases($_->[1]->artist_credit)) ne @@ -325,6 +343,7 @@ sub build_display_data grep { $_->[0] ne '-' } @$tracklist_changes ]; + # Generate a map of track id => old recording id, for edits that store # track ids, to detect if recordings have changed. @@ -334,6 +353,11 @@ sub build_display_data @changes_with_track_ids; $data->{recording_changes} = [ + map +{ + change_type => $_->[0], + old_track => $_->[1] eq '' ? undef : $_->[1], + new_track => $_->[2], + }, grep { my $old = $_->[1]; my $new = $_->[2]; @@ -555,6 +579,9 @@ sub allow_auto_edit return 0 if exists $self->data->{old}{position}; if ($self->data->{old}{tracklist}) { + # If there's no old tracklist, allow adding one as an autoedit + return 1 if scalar @{ $self->data->{old}{tracklist} } == 0; + my @changes = grep { $_->[0] ne 'u' } @{ sdiff( @@ -616,6 +643,12 @@ before restore => sub { $data->{new}{name} //= ''; $data->{old}{name} //= ''; } + + for my $track (@{ $data->{new}{tracklist} }, @{ $data->{old}{tracklist} }) { + for my $artist_credit_name (@{ $track->{artist_credit}{names} }) { + $artist_credit_name->{join_phrase} //= ''; + } + } }; __PACKAGE__->meta->make_immutable; diff --git a/lib/MusicBrainz/Server/Edit/Relationship/Delete.pm b/lib/MusicBrainz/Server/Edit/Relationship/Delete.pm index d0781196c58..6661f8c70e1 100644 --- a/lib/MusicBrainz/Server/Edit/Relationship/Delete.pm +++ b/lib/MusicBrainz/Server/Edit/Relationship/Delete.pm @@ -282,8 +282,7 @@ sub accept { 'as an example of its relationship type in the documentation. ' . 'If you still think this should be removed, please ' . '{contact_url|contact us}.'), - {contact_url => $CONTACT_URL}, - + vars => {contact_url => $CONTACT_URL}, ); MusicBrainz::Server::Edit::Exceptions::GeneralError->throw($error); } diff --git a/lib/MusicBrainz/Server/Edit/Release/AddCoverArt.pm b/lib/MusicBrainz/Server/Edit/Release/AddCoverArt.pm index a227ff928aa..70cdb299afa 100644 --- a/lib/MusicBrainz/Server/Edit/Release/AddCoverArt.pm +++ b/lib/MusicBrainz/Server/Edit/Release/AddCoverArt.pm @@ -104,7 +104,11 @@ sub build_display_data { my ($self, $loaded) = @_; my $release = $loaded->{Release}{ $self->data->{entity}{id} } || - Release->new( name => $self->data->{entity}{name} ); + Release->new( + gid => $self->data->{entity}{mbid}, + id => $self->data->{entity}{id}, + name => $self->data->{entity}{name}, + ); my $suffix = $self->data->{cover_art_mime_type} ? $self->c->model('CoverArt')->image_type_suffix($self->data->{cover_art_mime_type}) diff --git a/lib/MusicBrainz/Server/Edit/Release/Merge.pm b/lib/MusicBrainz/Server/Edit/Release/Merge.pm index 1a8594f9e4b..e44a066b81a 100644 --- a/lib/MusicBrainz/Server/Edit/Release/Merge.pm +++ b/lib/MusicBrainz/Server/Edit/Release/Merge.pm @@ -181,6 +181,16 @@ sub initialize { $self->data(\%opts); } +sub alter_edit_pending +{ + my $self = shift; + my @recording_ids = map { $_->{id} } map { $_->{destination}, @{ $_->{sources} } } @{ $self->recording_merges // [] }; + return { + Release => [ $self->release_ids ], + @recording_ids ? (Recording => [ @recording_ids ]) : (), + } +} + override build_display_data => sub { my ($self, $loaded) = @_; @@ -322,10 +332,12 @@ sub do_merge unless ($can_merge) { my $error = localized_note( N_l('These releases could not be merged: {reason}'), - {reason => localized_note( - $cannot_merge_reason->{message}, - $cannot_merge_reason->{args}, - )}, + vars => { + reason => localized_note( + $cannot_merge_reason->{message}, + vars => $cannot_merge_reason->{vars}, + ), + }, ); MusicBrainz::Server::Edit::Exceptions::GeneralError->throw($error); } diff --git a/lib/MusicBrainz/Server/Edit/Role/IPI.pm b/lib/MusicBrainz/Server/Edit/Role/IPI.pm index d3ec25e1bef..d5f3c23271c 100644 --- a/lib/MusicBrainz/Server/Edit/Role/IPI.pm +++ b/lib/MusicBrainz/Server/Edit/Role/IPI.pm @@ -1,6 +1,17 @@ package MusicBrainz::Server::Edit::Role::IPI; use 5.10.0; use Moose::Role; +use utf8; + +use MusicBrainz::Server::Constants qw( $EDITOR_MODBOT ); +use MusicBrainz::Server::Data::Utils qw( localized_note type_to_model ); +use MusicBrainz::Server::Translation qw( N_l ); +use Set::Scalar; + +has 'reused_ipis' => ( + isa => 'HashRef', + is => 'rw' +); with 'MusicBrainz::Server::Edit::Role::ValueSet' => { prop_name => 'ipi_codes', @@ -12,6 +23,68 @@ with 'MusicBrainz::Server::Edit::Role::ValueSet' => { extract_value => sub { shift->ipi } }; +after initialize => sub { + my ($self, %opts) = @_; + + my $old_ipis = $self->data->{old}{ipi_codes} // []; + my $new_ipis = $self->data->{new}{ipi_codes} // []; + my $added_ipis_set = Set::Scalar->new(@$new_ipis) - Set::Scalar->new(@$old_ipis); + my @added_ipis = $added_ipis_set->members; + $self->reused_ipis($self->c->model($self->_edit_model)->find_reused_ipis(@added_ipis)); +}; + +after post_insert => sub { + my $self = shift; + + for my $ipi (keys %{ $self->reused_ipis }) { + my $artist_dupe_count = $self->reused_ipis->{$ipi}->{artist}; + my $label_dupe_count = $self->reused_ipis->{$ipi}->{label}; + my $edit_note; + + if ($artist_dupe_count) { + $edit_note = localized_note( + 'The IPI {ipi} is already in use on {artist_count} artist. Please check {artist_search|all uses of this IPI}.', + function => 'ln', + args => ['The IPI {ipi} is already in use on {artist_count} artists. Please check {artist_search|all uses of this IPI}.', $artist_dupe_count], + vars => { + artist_count => $artist_dupe_count, + artist_search => "/search?query=ipi%3A$ipi&advanced=1&type=artist", + ipi => $ipi, + } + ); + + $self->c->model('EditNote')->add_note( + $self->{id}, + { + editor_id => $EDITOR_MODBOT, + text => $edit_note + } + ); + } + + if ($label_dupe_count) { + $edit_note = localized_note( + 'The IPI {ipi} is already in use on {label_count} label. Please check {label_search|all uses of this IPI}.', + function => 'ln', + args => ['The IPI {ipi} is already in use on {label_count} labels. Please check {label_search|all uses of this IPI}.', $label_dupe_count], + vars => { + ipi => $ipi, + label_search => "/search?query=ipi%3A$ipi&advanced=1&type=label", + label_count => $label_dupe_count, + } + ); + + $self->c->model('EditNote')->add_note( + $self->{id}, + { + editor_id => $EDITOR_MODBOT, + text => $edit_note + } + ); + } + } +}; + no Moose; 1; diff --git a/lib/MusicBrainz/Server/Edit/Role/ISNI.pm b/lib/MusicBrainz/Server/Edit/Role/ISNI.pm index bbd76d11785..faad1f481c6 100644 --- a/lib/MusicBrainz/Server/Edit/Role/ISNI.pm +++ b/lib/MusicBrainz/Server/Edit/Role/ISNI.pm @@ -1,6 +1,17 @@ package MusicBrainz::Server::Edit::Role::ISNI; use 5.10.0; use Moose::Role; +use utf8; + +use MusicBrainz::Server::Constants qw( $EDITOR_MODBOT ); +use MusicBrainz::Server::Data::Utils qw( localized_note type_to_model ); +use MusicBrainz::Server::Translation qw( N_l ); +use Set::Scalar; + +has 'reused_isnis' => ( + isa => 'HashRef', + is => 'rw' +); with 'MusicBrainz::Server::Edit::Role::ValueSet' => { prop_name => 'isni_codes', @@ -12,6 +23,68 @@ with 'MusicBrainz::Server::Edit::Role::ValueSet' => { extract_value => sub { shift->isni } }; +after initialize => sub { + my ($self, %opts) = @_; + + my $old_isnis = $self->data->{old}{isni_codes} // []; + my $new_isnis = $self->data->{new}{isni_codes} // []; + my $added_isnis_set = Set::Scalar->new(@$new_isnis) - Set::Scalar->new(@$old_isnis); + my @added_isnis = $added_isnis_set->members; + $self->reused_isnis($self->c->model($self->_edit_model)->find_reused_isnis(@added_isnis)); +}; + +after post_insert => sub { + my $self = shift; + + for my $isni (keys %{ $self->reused_isnis }) { + my $artist_dupe_count = $self->reused_isnis->{$isni}->{artist}; + my $label_dupe_count = $self->reused_isnis->{$isni}->{label}; + my $edit_note; + + if ($artist_dupe_count) { + $edit_note = localized_note( + 'The ISNI {isni} is already in use on {artist_count} artist. Please check {artist_search|all uses of this ISNI}.', + function => 'ln', + args => ['The ISNI {isni} is already in use on {artist_count} artists. Please check {artist_search|all uses of this ISNI}.', $artist_dupe_count], + vars => { + artist_count => $artist_dupe_count, + artist_search => "/search?query=isni%3A$isni&advanced=1&type=artist", + isni => $isni, + } + ); + + $self->c->model('EditNote')->add_note( + $self->{id}, + { + editor_id => $EDITOR_MODBOT, + text => $edit_note + } + ); + } + + if ($label_dupe_count) { + $edit_note = localized_note( + 'The ISNI {isni} is already in use on {label_count} label. Please check {label_search|all uses of this ISNI}.', + function => 'ln', + args => ['The ISNI {isni} is already in use on {label_count} labels. Please check {label_search|all uses of this ISNI}.', $label_dupe_count], + vars => { + isni => $isni, + label_search => "/search?query=isni%3A$isni&advanced=1&type=label", + label_count => $label_dupe_count, + } + ); + + $self->c->model('EditNote')->add_note( + $self->{id}, + { + editor_id => $EDITOR_MODBOT, + text => $edit_note + } + ); + } + } +}; + no Moose; 1; diff --git a/lib/MusicBrainz/Server/Entity/EditNote.pm b/lib/MusicBrainz/Server/Entity/EditNote.pm index d0676c90192..14e62625ee7 100644 --- a/lib/MusicBrainz/Server/Entity/EditNote.pm +++ b/lib/MusicBrainz/Server/Entity/EditNote.pm @@ -9,7 +9,6 @@ use namespace::autoclean; use MusicBrainz::Server::Constants qw( $EDITOR_MODBOT ); use MusicBrainz::Server::Entity::Types; use MusicBrainz::Server::Filters qw( format_editnote ); -use MusicBrainz::Server::Translation qw( l ); use MusicBrainz::Server::Types qw( DateTime ); has 'editor_id' => ( @@ -44,6 +43,17 @@ has 'post_time' => ( coerce => 1 ); +my %domain_classes = ( + 'attributes' => 'Attributes', + 'countries' => 'Countries', + 'instrument_descriptions' => 'InstrumentDescriptions', + 'instruments' => 'Instruments', + 'languages' => 'Languages', + 'relationships' => 'Relationships', + 'scripts' => 'Scripts', + 'statistics' => 'Statistics', +); + sub _localize_text { my ($text, $depth) = @_; @@ -52,13 +62,41 @@ sub _localize_text { if (my ($source) = ($text =~ m/^localize:(.+)$/)) { $source = $json->decode($source); - my $source_args = $source->{args} // {}; - my %args = map { - my $value = $source_args->{$_}; - $_ => (ref($value) ? $value : _localize_text($value // '', $depth + 1)) - } keys %{$source_args}; - - $text = l($source->{message} // '', \%args); + my $version = $source->{version} // 0; + my $fn_package = 'MusicBrainz::Server::Translation'; + my $fn_name = 'l'; + my @args = ($source->{message} // ''); + my $source_vars; + + if ($version == 0) { + # old versions of `localized_note` passed substitution + # variables as `args`. + $source_vars = $source->{args}; + } elsif ($version == 1) { + push @args, @{ $source->{args} // [] }; + + $fn_name = $source->{function} // 'l'; + $source_vars = $source->{vars}; + + my $domain = $source->{domain}; + if (defined $domain && $domain ne 'mb_server') { + $fn_package .= ('::' . $domain_classes{$domain}); + } + } + + if (defined $source_vars) { + my %vars = map { + my $value = $source_vars->{$_}; + $_ => (ref($value) ? $value : _localize_text($value // '', $depth + 1)) + } keys %{$source_vars}; + push @args, \%vars; + } + + my $fn_package_path = ($fn_package =~ s/::/\//gr) . '.pm'; + require $fn_package_path; + + my $function = \&{ "${fn_package}::${fn_name}" }; + $text = $function->(@args); } elsif ($depth == 0) { # Otherwise, assume this message uses edit note syntax. $text = format_editnote($text); diff --git a/lib/MusicBrainz/Server/Entity/URL/Decoda.pm b/lib/MusicBrainz/Server/Entity/URL/Overture.pm similarity index 75% rename from lib/MusicBrainz/Server/Entity/URL/Decoda.pm rename to lib/MusicBrainz/Server/Entity/URL/Overture.pm index 5ee138224dd..d19fc0a8d47 100644 --- a/lib/MusicBrainz/Server/Entity/URL/Decoda.pm +++ b/lib/MusicBrainz/Server/Entity/URL/Overture.pm @@ -1,11 +1,11 @@ -package MusicBrainz::Server::Entity::URL::Decoda; +package MusicBrainz::Server::Entity::URL::Overture; use Moose; extends 'MusicBrainz::Server::Entity::URL'; with 'MusicBrainz::Server::Entity::URL::Sidebar'; -sub sidebar_name { 'Decoda' } +sub sidebar_name { 'Overture' } __PACKAGE__->meta->make_immutable; no Moose; @@ -13,7 +13,7 @@ no Moose; =head1 COPYRIGHT AND LICENSE -Copyright (C) 2019 MetaBrainz Foundation +Copyright (C) 2020 MetaBrainz Foundation This file is part of MusicBrainz, the open internet music database, and is licensed under the GPL version 2, or (at your option) any diff --git a/lib/MusicBrainz/Server/Entity/URL/Spotify.pm b/lib/MusicBrainz/Server/Entity/URL/Spotify.pm index ed2bcb24ceb..000c498005a 100644 --- a/lib/MusicBrainz/Server/Entity/URL/Spotify.pm +++ b/lib/MusicBrainz/Server/Entity/URL/Spotify.pm @@ -10,7 +10,7 @@ with 'MusicBrainz::Server::Entity::URL::Sidebar'; sub sidebar_name { my $self = shift; - if ($self->url =~ m{^(?:https?:)?//(?:[^/]+.)?spotify.com/user/[^/?&#]+/?}i) { + if ($self->url =~ m{^(?:https?:)?//(?:[^/]+.)?spotify.com/user/[^/?#]+/?}i) { return l('Playlists at Spotify'); } else { return l('Stream at Spotify'); diff --git a/lib/MusicBrainz/Server/Form/Alias.pm b/lib/MusicBrainz/Server/Form/Alias.pm index c4cfc332335..72a455d2faf 100644 --- a/lib/MusicBrainz/Server/Form/Alias.pm +++ b/lib/MusicBrainz/Server/Form/Alias.pm @@ -89,7 +89,10 @@ sub options_locale { map { my $code = $_; my $locale = $ALIAS_LOCALES{$code}; - $code => indentation($code =~ /_/ ? 1 : 0) . _locale_name_special_cases($locale) + { + value => $code, + label => indentation($code =~ /_/ ? 1 : 0) . _locale_name_special_cases($locale), + }; } sort_by { $ALIAS_LOCALES{$_}->name } keys %ALIAS_LOCALES diff --git a/lib/MusicBrainz/Server/Form/Field/DatePeriod.pm b/lib/MusicBrainz/Server/Form/Field/DatePeriod.pm index a743594dba3..f1782cdd1e0 100644 --- a/lib/MusicBrainz/Server/Form/Field/DatePeriod.pm +++ b/lib/MusicBrainz/Server/Form/Field/DatePeriod.pm @@ -37,7 +37,7 @@ after 'validate' => sub { return 1 unless $begin->{year} && $end->{year}; unless (is_date_range_valid($begin, $end)) { - return $self->field('end_date')->add_error(l('The end date cannot precede the begin date.')); + return $self->add_error(l('The end date cannot precede the begin date.')); } return 1; diff --git a/lib/MusicBrainz/Server/Form/Field/OAuthRedirectURI.pm b/lib/MusicBrainz/Server/Form/Field/OAuthRedirectURI.pm index 2df21abef61..769297c6c37 100644 --- a/lib/MusicBrainz/Server/Form/Field/OAuthRedirectURI.pm +++ b/lib/MusicBrainz/Server/Form/Field/OAuthRedirectURI.pm @@ -18,10 +18,5 @@ sub validate $self->_set_value($url->as_string); } -sub deflate { - my ($self, $value) = @_; - return $value->as_string; -} - __PACKAGE__->meta->make_immutable; 1; diff --git a/lib/MusicBrainz/Server/Form/Field/URL.pm b/lib/MusicBrainz/Server/Form/Field/URL.pm index 7aef731046c..29b2b4b9b03 100644 --- a/lib/MusicBrainz/Server/Form/Field/URL.pm +++ b/lib/MusicBrainz/Server/Form/Field/URL.pm @@ -7,6 +7,10 @@ use MusicBrainz::Server::Validation qw( is_valid_url ); extends 'HTML::FormHandler::Field::Text'; +has '+deflate_method' => ( + default => sub { \&deflate_url } +); + my %ALLOWED_PROTOCOLS = map { $_ => 1 } qw( http https ftp ); sub validate @@ -27,9 +31,10 @@ sub validate $self->_set_value($url->as_string); } -sub deflate { +sub deflate_url { my ($self, $value) = @_; - return $value->as_string; + + return $value->as_iri; } __PACKAGE__->meta->make_immutable; diff --git a/lib/MusicBrainz/Server/Form/OtherLookup.pm b/lib/MusicBrainz/Server/Form/OtherLookup.pm index 9ccb1d1f6f9..968af3460cc 100644 --- a/lib/MusicBrainz/Server/Form/OtherLookup.pm +++ b/lib/MusicBrainz/Server/Form/OtherLookup.pm @@ -10,6 +10,12 @@ has_field 'catno' => ( has_field 'barcode' => ( type => '+MusicBrainz::Server::Form::Field::Barcode', + trim => { transform => sub { + my $string = shift; + # Remove all spaces for barcode search since we don't store them + $string =~ s/\s+//g; + return $string; + } } ); has_field 'url' => ( @@ -28,19 +34,19 @@ has_field 'iswc' => ( type => '+MusicBrainz::Server::Form::Field::ISWC', ); -has_field 'artist-ipi' => ( +has_field 'artist-ipi' => ( type => '+MusicBrainz::Server::Form::Field::IPI', ); -has_field 'artist-isni' => ( +has_field 'artist-isni' => ( type => '+MusicBrainz::Server::Form::Field::ISNI', ); -has_field 'label-ipi' => ( +has_field 'label-ipi' => ( type => '+MusicBrainz::Server::Form::Field::IPI', ); -has_field 'label-isni' => ( +has_field 'label-isni' => ( type => '+MusicBrainz::Server::Form::Field::ISNI', ); diff --git a/lib/MusicBrainz/Server/Form/Role/ToJSON.pm b/lib/MusicBrainz/Server/Form/Role/ToJSON.pm index 15e6d7698e5..817dffb44e6 100644 --- a/lib/MusicBrainz/Server/Form/Role/ToJSON.pm +++ b/lib/MusicBrainz/Server/Form/Role/ToJSON.pm @@ -23,6 +23,7 @@ sub TO_JSON { if ($is_form) { $field_id_counter = 0; $json->{name} = $self->name; + $json->{type} = 'form'; } if ($self->isa('HTML::FormHandler::Field')) { @@ -30,6 +31,7 @@ sub TO_JSON { $json->{errors} = $self->errors; $json->{html_name} = $self->html_name; $json->{id} = ++$field_id_counter; + $json->{type} = 'field'; } if ($self->can('fields')) { @@ -37,9 +39,11 @@ sub TO_JSON { $json->{field} = []; $json->{field}[$_->name] = TO_JSON($_) for $self->fields; $json->{last_index} = scalar(@{ $json->{field} }) - 1; + $json->{type} = 'repeatable_field'; } else { $json->{field} = {}; $json->{field}{$_->name} = TO_JSON($_) for $self->fields; + $json->{type} = 'compound_field'; } } else { if ($self->isa('HTML::FormHandler::Field::Checkbox')) { diff --git a/lib/MusicBrainz/Server/Report/CDTOCDubiousLength.pm b/lib/MusicBrainz/Server/Report/CDTOCDubiousLength.pm index a6c26cabf1d..ae94505b333 100644 --- a/lib/MusicBrainz/Server/Report/CDTOCDubiousLength.pm +++ b/lib/MusicBrainz/Server/Report/CDTOCDubiousLength.pm @@ -20,7 +20,14 @@ sub query { JOIN medium ON medium_cdtoc.medium = medium.id JOIN medium_format ON medium.format = medium_format.id WHERE - leadout_offset > 75 * 60 * 100 -- cutoff 100 minutes + -- default cutoff 88 minutes + (leadout_offset > 75 * 60 * 88 + -- no limit for CD-R + AND medium_format.id != 33) + OR + -- custom cutoff 30 minutes for 8cm CDs + (medium_format.id = 34 + AND leadout_offset > 75 * 60 * 30) ORDER BY medium_format.name, cdtoc.leadout_offset DESC diff --git a/lib/MusicBrainz/Server/Report/CDTOCNotApplied.pm b/lib/MusicBrainz/Server/Report/CDTOCNotApplied.pm new file mode 100644 index 00000000000..7b41efb2d00 --- /dev/null +++ b/lib/MusicBrainz/Server/Report/CDTOCNotApplied.pm @@ -0,0 +1,62 @@ +package MusicBrainz::Server::Report::CDTOCNotApplied; +use Moose; + +with 'MusicBrainz::Server::Report::CDTOCReport', + 'MusicBrainz::Server::Report::FilterForEditor::ReleaseID'; + +sub table { 'cd_toc_not_applied' } +sub component_name { 'CDTocNotApplied' } + +sub query { + q{ + WITH + mc AS ( + SELECT + UNNEST(ARRAY_AGG(cdtoc)) AS cdtoc, + medium + FROM + medium_cdtoc + GROUP BY + medium + HAVING + COUNT(medium) = 1 + ), + disc AS ( + SELECT DISTINCT + c.id AS cdtoc_id, + c.discid, + mc.medium + FROM + cdtoc AS c + JOIN mc ON c.id = mc.cdtoc + JOIN track AS t ON t.medium = mc.medium + WHERE + t.length IS NULL + AND NOT t.is_data_track + ) + SELECT + cdtoc_id, + r.id AS release_id, + row_number() OVER (ORDER BY r.name) + FROM + disc + JOIN medium AS m ON m.id = disc.medium + JOIN release AS r ON r.id = m.release + ORDER BY + r.name + } +} + +__PACKAGE__->meta->make_immutable; +no Moose; +1; + +=head1 COPYRIGHT AND LICENSE + +Copyright (C) 2021 Jerome Roy + +This file is part of MusicBrainz, the open internet music database, +and is licensed under the GPL version 2, or (at your option) any +later version: http://www.gnu.org/licenses/gpl-2.0.txt + +=cut diff --git a/lib/MusicBrainz/Server/Report/CDTOCReport.pm b/lib/MusicBrainz/Server/Report/CDTOCReport.pm index afe337b9b55..18749ba0a8c 100644 --- a/lib/MusicBrainz/Server/Report/CDTOCReport.pm +++ b/lib/MusicBrainz/Server/Report/CDTOCReport.pm @@ -13,10 +13,17 @@ around inflate_rows => sub { map { $_->{cdtoc_id} } @$items ); + my $releases = $self->c->model('Release')->get_by_ids( + map { $_->{release_id} } @$items + ); + + $self->c->model('ArtistCredit')->load(values %$releases); + return [ map +{ %$_, cdtoc => $cdtocs->{ $_->{cdtoc_id} }, + release => $releases->{ $_->{release_id} } }, @$items ]; }; diff --git a/lib/MusicBrainz/Server/Report/DeprecatedRelationshipReport.pm b/lib/MusicBrainz/Server/Report/DeprecatedRelationshipReport.pm index 2b936bef398..18c6507b949 100644 --- a/lib/MusicBrainz/Server/Report/DeprecatedRelationshipReport.pm +++ b/lib/MusicBrainz/Server/Report/DeprecatedRelationshipReport.pm @@ -10,6 +10,7 @@ sub query { my ($self) = @_; my $entity_type = $self->entity_type; my $name_sort = $entity_type ne 'url' ? 'entity.name COLLATE musicbrainz' : 'entity.url'; + my $extra_conditions = $entity_type eq 'url' ? ' AND link.ended IS FALSE' : ''; my @tables = $self->c->model('Relationship')->generate_table_list($entity_type); my $query = "SELECT l.name AS link_name, l.gid AS link_gid, entity.id AS ${entity_type}_id, row_number() OVER (ORDER BY l.name, $name_sort)" . "FROM $entity_type AS entity JOIN ("; @@ -25,7 +26,8 @@ sub query { FROM link_type JOIN link ON link.link_type = link_type.id JOIN $table l_table ON l_table.link = link.id - WHERE link_type.is_deprecated OR link_type.description = ''"; + WHERE (link_type.is_deprecated OR link_type.description = '') + $extra_conditions"; } $query .= ") l ON l.entity = entity.id"; return $query; diff --git a/lib/MusicBrainz/Server/Report/InstrumentsWithoutAnImage.pm b/lib/MusicBrainz/Server/Report/InstrumentsWithoutAnImage.pm index 1fe7a14d312..6786cccce95 100644 --- a/lib/MusicBrainz/Server/Report/InstrumentsWithoutAnImage.pm +++ b/lib/MusicBrainz/Server/Report/InstrumentsWithoutAnImage.pm @@ -10,7 +10,7 @@ sub query q{ SELECT i.id AS instrument_id, - row_number() OVER (ORDER BY i.type, i.name COLLATE musicbrainz) + row_number() OVER (ORDER BY i.name COLLATE musicbrainz, i.type) FROM instrument i WHERE NOT EXISTS ( SELECT 1 diff --git a/lib/MusicBrainz/Server/Report/InstrumentsWithoutWikidata.pm b/lib/MusicBrainz/Server/Report/InstrumentsWithoutWikidata.pm index a26afefd437..594071f3e4c 100644 --- a/lib/MusicBrainz/Server/Report/InstrumentsWithoutWikidata.pm +++ b/lib/MusicBrainz/Server/Report/InstrumentsWithoutWikidata.pm @@ -10,7 +10,7 @@ sub query q{ SELECT i.id AS instrument_id, - row_number() OVER (ORDER BY i.type, i.name COLLATE musicbrainz) + row_number() OVER (ORDER BY i.name COLLATE musicbrainz, i.type) FROM instrument i WHERE NOT EXISTS ( SELECT 1 diff --git a/lib/MusicBrainz/Server/ReportFactory.pm b/lib/MusicBrainz/Server/ReportFactory.pm index 079e268a554..600f47949ff 100644 --- a/lib/MusicBrainz/Server/ReportFactory.pm +++ b/lib/MusicBrainz/Server/ReportFactory.pm @@ -23,6 +23,7 @@ use MusicBrainz::Server::PagedReport; CatNoLooksLikeASIN CatNoLooksLikeLabelCode CDTOCDubiousLength + CDTOCNotApplied CollaborationRelationships CoverArtRelationships DeprecatedRelationshipArtists @@ -109,6 +110,7 @@ use MusicBrainz::Server::Report::BadAmazonURLs; use MusicBrainz::Server::Report::CatNoLooksLikeASIN; use MusicBrainz::Server::Report::CatNoLooksLikeLabelCode; use MusicBrainz::Server::Report::CDTOCDubiousLength; +use MusicBrainz::Server::Report::CDTOCNotApplied; use MusicBrainz::Server::Report::CollaborationRelationships; use MusicBrainz::Server::Report::CoverArtRelationships; use MusicBrainz::Server::Report::DeprecatedRelationshipArtists; diff --git a/package.json b/package.json index 3c12fa38419..1bf9cf7a95c 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "pg": "8.3.3", "pg-cursor": "2.3.3", "po2json": "0.4.1", + "punycode": "2.1.1", "querystring": "0.2.0", "react": "17.0.1", "react-dom": "17.0.1", diff --git a/po/attributes.fr.po b/po/attributes.fr.po index 17ddbf8a870..42ee1768ba5 100644 --- a/po/attributes.fr.po +++ b/po/attributes.fr.po @@ -29,7 +29,7 @@ msgid "" msgstr "" "Project-Id-Version: MusicBrainz\n" -"PO-Revision-Date: 2021-01-08 08:39+0000\n" +"PO-Revision-Date: 2021-01-15 15:00+0000\n" "Last-Translator: yvanz\n" "Language-Team: French (http://www.transifex.com/musicbrainz/musicbrainz/language/fr/)\n" "MIME-Version: 1.0\n" @@ -4136,7 +4136,7 @@ msgstr "Lenk Fahte" #: DB:cover_art_archive.art_type/name:12 msgctxt "cover_art_type" msgid "Liner" -msgstr "Pochette" +msgstr "Enveloppe" #: DB:release_group_secondary_type/name:6 msgctxt "release_group_secondary_type" diff --git a/po/instrument_descriptions.de.po b/po/instrument_descriptions.de.po index 5c99dc2bb4d..e4ba83319b9 100644 --- a/po/instrument_descriptions.de.po +++ b/po/instrument_descriptions.de.po @@ -13,8 +13,8 @@ msgid "" msgstr "" "Project-Id-Version: MusicBrainz\n" -"PO-Revision-Date: 2021-01-07 10:59+0000\n" -"Last-Translator: yvanz\n" +"PO-Revision-Date: 2021-02-01 16:47+0000\n" +"Last-Translator: Nicolás Tamargo