diff --git a/cgi/product_jqm_multilingual.pl b/cgi/product_jqm_multilingual.pl index 16afbb472676e..198807f96f397 100755 --- a/cgi/product_jqm_multilingual.pl +++ b/cgi/product_jqm_multilingual.pl @@ -114,6 +114,15 @@ =head1 DESCRIPTION $product_ref = init_product($User_id, $Org_id, $code, $country); $product_ref->{interface_version_created} = $interface_version; } + else { + # There is an existing product + # If the product has a product_type and it is not the product_type of the server, redirect to the correct server + + if ((defined $product_ref->{product_type}) and ($product_ref->{product_type} ne $options{product_type})) { + redirect_to_url($request_ref, 307, + format_subdomain($subdomain, $product_ref->{product_type}) . '/cgi/product_jqm.pl?code=' . $code); + } + } # Process edit rules diff --git a/cgi/product_multilingual.pl b/cgi/product_multilingual.pl index 9c8b9f80281db..d7d065137f5fa 100755 --- a/cgi/product_multilingual.pl +++ b/cgi/product_multilingual.pl @@ -342,6 +342,15 @@ ($product_ref) if (not defined $product_ref) { display_error_and_exit($request_ref, sprintf(lang("no_product_for_barcode"), $code), 404); } + else { + # There is an existing product + # If the product has a product_type and it is not the product_type of the server, redirect to the correct server + # We use a 302 redirect so that browsers issue a GET request to display the form (even if we received a POST request) + if ((defined $product_ref->{product_type}) and ($product_ref->{product_type} ne $options{product_type})) { + redirect_to_url($request_ref, 302, + format_subdomain($subdomain, $product_ref->{product_type}) . '/cgi/product.pl?code=' . $code); + } + } } } @@ -399,11 +408,15 @@ ($product_ref) exists $product_ref->{new_server} and delete $product_ref->{new_server}; - # 26/01/2017 - disallow barcode changes until we fix bug #677 - if ($User{moderator} and (defined single_param("new_code")) and (single_param("new_code") ne "")) { + if ($User{moderator}) { + if ((defined single_param("new_code")) and (single_param("new_code") ne "")) { - change_product_server_or_code($product_ref, single_param("new_code"), \@errors); - $code = $product_ref->{code}; + change_product_server_or_code($product_ref, single_param("new_code"), \@errors); + $code = $product_ref->{code}; + } + if ((defined single_param("product_type")) and (single_param("product_type") ne "")) { + change_product_type($product_ref, single_param("product_type"), \@errors); + } } my @param_fields = (); @@ -848,14 +861,12 @@ ($product_ref, $field, $language, $request_ref) my $label_new_code = $Lang{new_code}{$lc}; - # 26/01/2017 - disallow barcode changes until we fix bug #677 - if ($User{moderator}) { - } - $template_data_ref_display->{org_id} = $Org_id; $template_data_ref_display->{label_new_code} = $label_new_code; $template_data_ref_display->{owner_id} = $Owner_id; + $template_data_ref_display->{product_types} = $options{product_types}; + # obsolete products: restrict to admin on public site # authorize owners on producers platform if ($User{moderator} or $Owner_id) { diff --git a/lib/ProductOpener/APIProductRead.pm b/lib/ProductOpener/APIProductRead.pm index 7e9f287a77b9a..f8a8bec91d338 100644 --- a/lib/ProductOpener/APIProductRead.pm +++ b/lib/ProductOpener/APIProductRead.pm @@ -45,12 +45,13 @@ use vars @EXPORT_OK; use ProductOpener::Config qw/:all/; use ProductOpener::Paths qw/%BASE_DIRS/; -use ProductOpener::Display qw/request_param single_param/; +use ProductOpener::Display qw/$subdomain redirect_to_url request_param single_param/; use ProductOpener::Users qw/$Owner_id/; use ProductOpener::Lang qw/$lc/; use ProductOpener::Products qw/:all/; use ProductOpener::Ingredients qw/flatten_sub_ingredients/; use ProductOpener::API qw/add_error customize_response_for_product normalize_requested_code/; +use ProductOpener::URL qw(format_subdomain); my $cc; @@ -138,6 +139,13 @@ sub read_product_api ($request_ref) { $response_ref->{result} = {id => "product_not_found"}; } else { + # If the product has a product_type and it is not the product_type of the server, redirect to the correct server + + if ((defined $product_ref->{product_type}) and ($product_ref->{product_type} ne $options{product_type})) { + redirect_to_url($request_ref, 302, + format_subdomain($subdomain, $product_ref->{product_type}) . $request_ref->{original_query_string}); + } + $response_ref->{result} = {id => "product_found"}; add_images_urls_to_product($product_ref, $lc); diff --git a/lib/ProductOpener/APIProductWrite.pm b/lib/ProductOpener/APIProductWrite.pm index abe224347b228..8f4a8e9764534 100644 --- a/lib/ProductOpener/APIProductWrite.pm +++ b/lib/ProductOpener/APIProductWrite.pm @@ -45,7 +45,7 @@ BEGIN { use vars @EXPORT_OK; use ProductOpener::Config qw/:all/; -use ProductOpener::Display qw/$country request_param single_param/; +use ProductOpener::Display qw/$subdomain redirect_to_url $country request_param single_param/; use ProductOpener::Users qw/$Org_id $Owner_id $User_id/; use ProductOpener::Lang qw/$lc/; use ProductOpener::Products qw/:all/; @@ -54,6 +54,7 @@ use ProductOpener::Packaging qw/add_or_combine_packaging_component_data get_checked_and_taxonomized_packaging_component_data/; use ProductOpener::Text qw/remove_tags_and_quote/; use ProductOpener::Tags qw/%language_fields %writable_tags_fields add_tags_to_field compute_field_tags/; +use ProductOpener::URL qw(format_subdomain); use Encode; @@ -430,6 +431,15 @@ sub write_product_api ($request_ref) { $product_ref = init_product($User_id, $Org_id, $code, $country); $product_ref->{interface_version_created} = "20221102/api/v3"; } + else { + # There is an existing product + # If the product has a product_type and it is not the product_type of the server, redirect to the correct server + + if ((defined $product_ref->{product_type}) and ($product_ref->{product_type} ne $options{product_type})) { + redirect_to_url($request_ref, 307, + format_subdomain($subdomain, $product_ref->{product_type}) . '/api/v3/product/' . $code); + } + } # Use default request language if we did not get tags_lc if (not defined $request_body_ref->{tags_lc}) { diff --git a/lib/ProductOpener/APITest.pm b/lib/ProductOpener/APITest.pm index 73c1805efe698..ee212d33d4409 100644 --- a/lib/ProductOpener/APITest.pm +++ b/lib/ProductOpener/APITest.pm @@ -428,6 +428,14 @@ sub execute_request ($test_ref, $ua) { my $response; + # For some tests, we don't want to follow redirects. We want to see the 302 responses, not the response to the final destination + if ((defined $test_ref->{expected_status_code}) and (int($test_ref->{expected_status_code} / 100) == 3)) { + $test_ua->max_redirect(0); + } + else { + $test_ua->max_redirect(3); + } + # Send the request if ($method eq 'OPTIONS') { # not yet supported by our (system) version of HTTP::Request::Common @@ -541,9 +549,10 @@ sub check_request_response ($test_ref, $response, $test_id, $test_dir, $expected my $response_content = $response->decoded_content; - # Check that we don't get an errore message generated by the Apache Server + # Check that we don't get an error message generated by the Apache Server # e.g. "Apache/2.4.56 (Debian) Server at world.openfoodfacts.localhost Port 80" - if ($response_content =~ /Apache.*Server/) { + # unless it is a redirect + if (($response_content =~ /Apache.*Server/) and not($response->code =~ /^3\d\d$/)) { fail("Received an Apache Server generated error message for test $test_case"); diag("Response content: " . $response_content); } diff --git a/lib/ProductOpener/Config.pm b/lib/ProductOpener/Config.pm index bd58f65a3b6e0..484946e1a714d 100644 --- a/lib/ProductOpener/Config.pm +++ b/lib/ProductOpener/Config.pm @@ -33,6 +33,141 @@ if (not defined $flavor) { } use Module::Load; + autoload("ProductOpener::Config_$flavor"); +# Add values common to all flavors + +# define the normalization applied to change a string to a tag id (in particular for taxonomies) +# tag ids are also used in URLs. + +# unaccent: +# - useful when accents are sometimes ommited (e.g. in French accents are often not present on capital letters), +# either in print, or when typed by users. +# - dangerous if different words (in the same context like ingredients or category names) have the same unaccented form +# lowercase: +# - useful when the same word appears in lowercase, with a first capital letter, or in all caps. + +# IMPORTANT: if you change it, you need to change $BUILD_TAGS_VERSION in Tags.pm + +%ProductOpener::Config::string_normalization_for_lang = ( + # no_language is used for strings that are not in a specific language (e.g. user names) + no_language => { + unaccent => 1, + lowercase => 1, + }, + # default is used for languages that do not have specified values + default => { + unaccent => 0, + lowercase => 1, + }, + # German umlauts should not be converted (e.g. ä -> ae) as there are many conflicts + de => { + unaccent => 0, + lowercase => 1, + }, + # French has very few actual conflicts caused by unaccenting (one counter example is "pâtes" and "pâtés") + # Accents or often not present in capital letters (beginning of word, or in all caps text). + fr => { + unaccent => 1, + lowercase => 1, + }, + # Same for Spanish, Italian and Portuguese + ca => { + unaccent => 1, + lowercase => 1, + }, + es => { + unaccent => 1, + lowercase => 1, + }, + it => { + unaccent => 1, + lowercase => 1, + }, + nl => { + unaccent => 1, + lowercase => 1, + }, + pt => { + unaccent => 1, + lowercase => 1, + }, + sk => { + unaccent => 1, + lowercase => 1, + }, + # English has very few accented words, and they are very often not accented by users or in ingredients lists etc. + en => { + unaccent => 1, + lowercase => 1, + }, + # xx: language less entries, also deaccent + xx => { + unaccent => 1, + lowercase => 1, + }, +); + +%ProductOpener::Config::admins = map {$_ => 1} qw( + alex-off + cha-delh + charlesnepote + gala-nafikova + hangy + manoncorneille + raphael0202 + stephane + tacinte + teolemon + g123k + valimp +); + +=head2 Available product types and flavors + +=cut + +$ProductOpener::Config::options{product_types} = [qw(food petfood beauty product)]; +$ProductOpener::Config::options{product_types_flavors} = { + food => "off", + petfood => "opff", + beauty => "obf", + product => "opf" +}; + +$ProductOpener::Config::options{flavors_product_types} + = {reverse %{$ProductOpener::Config::options{product_types_flavors}}}; + +$ProductOpener::Config::options{product_types_domains} = { + food => "openfoodfacts.org", + petfood => "openpetfoodfacts.org", + beauty => "openbeautyfacts.org", + product => "openproductsfacts.org" +}; + +$ProductOpener::Config::options{other_servers} = { + obf => { + name => "Open Beauty Facts", + mongodb => "obf", + domain => "openbeautyfacts.org", + }, + off => { + name => "Open Food Facts", + mongodb => "off", + domain => "openfoodfacts.org", + }, + opf => { + name => "Open Products Facts", + mongodb => "opf", + domain => "openproductsfacts.org", + }, + opff => { + prefix => "opff", + name => "Open Pet Food Facts", + mongodb => "opff", + domain => "openpetfoodfacts.org", + } +}; + 1; diff --git a/lib/ProductOpener/Config_off.pm b/lib/ProductOpener/Config_off.pm index 1791e14b26f30..fb7be2d05a378 100644 --- a/lib/ProductOpener/Config_off.pm +++ b/lib/ProductOpener/Config_off.pm @@ -177,21 +177,6 @@ $flavor = 'off'; }, ); -%admins = map {$_ => 1} qw( - alex-off - cha-delh - charlesnepote - gala-nafikova - hangy - manoncorneille - raphael0202 - stephane - tacinte - teolemon - g123k - valimp -); - %options = ( site_name => "Open Food Facts", product_type => "food", @@ -1099,30 +1084,22 @@ $options{current_server} = "off"; $options{other_servers} = { obf => { name => "Open Beauty Facts", - data_root => "/srv/obf", - www_root => "/srv/obf/html", mongodb => "obf", domain => "openbeautyfacts.org", }, off => { name => "Open Food Facts", - data_root => "/srv/off", - www_root => "/srv/off/html", mongodb => "off", domain => "openfoodfacts.org", }, opf => { name => "Open Products Facts", - data_root => "/srv/opf", - www_root => "/srv/opf/html", mongodb => "opf", domain => "openproductsfacts.org", }, opff => { prefix => "opff", name => "Open Pet Food Facts", - data_root => "/srv/opff", - www_root => "/srv/opff/html", mongodb => "opff", domain => "openpetfoodfacts.org", } diff --git a/lib/ProductOpener/Display.pm b/lib/ProductOpener/Display.pm index f9ac54dba4106..893626a9f9d59 100644 --- a/lib/ProductOpener/Display.pm +++ b/lib/ProductOpener/Display.pm @@ -7899,6 +7899,13 @@ JS display_error_and_exit($request_ref, sprintf(lang("no_product_for_barcode"), $code), 404); } + # If the product has a product_type and it is not the product_type of the server, redirect to the correct server + + if ((defined $product_ref->{product_type}) and ($product_ref->{product_type} ne $options{product_type})) { + redirect_to_url($request_ref, 302, + format_subdomain($subdomain, $product_ref->{product_type}) . product_url($product_ref)); + } + $title = product_name_brand_quantity($product_ref); my $titleid = get_string_id_for_lang($lc, product_name_brand($product_ref)); diff --git a/lib/ProductOpener/Paths.pm b/lib/ProductOpener/Paths.pm index dc1b4fd0da056..579978334c31d 100644 --- a/lib/ProductOpener/Paths.pm +++ b/lib/ProductOpener/Paths.pm @@ -290,44 +290,8 @@ my @PRO_ONLY_PATHS = qw(SFTP_HOME); =head1 FUNCTIONS -=head2 products_dir($server_name) - -products directory for a foreign server - -=head3 Arguments - -=head4 $server_name - off/obf/opf/opff… - -=head3 Return - -String of path to base directory containing products sto - =cut -sub products_dir ($server_name) { - my $server_data_root = $options{other_servers}{$server_name}{data_root}; - return "$server_data_root/products"; -} - -=head2 products_images_dir($server_name) - -products images directory for a foreign server - -=head3 Arguments - -=head4 $server_name - off/obf/opf/opff… - -=head3 Return - -String of path to base directory containing products images - -=cut - -sub products_images_dir ($server_name) { - my $server_www_root = $options{other_servers}{$server_name}{www_root}; - return "$server_www_root/images/products"; -} - sub _source_dir() { # compute $src_root my $src_root = abs_path(dirname(__FILE__) . '/../..'); @@ -384,15 +348,6 @@ sub base_paths() { my %paths = (%BASE_DIRS); if (!$server_options{producers_platform}) { # on non pro instances, - # also add foreign projects dirs for products migrations - my $servers_options = $options{other_servers}; - foreach my $server_name (keys %{$servers_options}) { - if ($server_name eq $options{current_server}) { - next; - } - $paths{uc($server_name) . "_PRODUCTS_DIR"} = products_dir($server_name); - $paths{uc($server_name) . "_PRODUCTS_IMAGES_DIR"} = products_images_dir($server_name); - } # remove some paths foreach my $path (@PRO_ONLY_PATHS) { delete $paths{$path}; diff --git a/lib/ProductOpener/Products.pm b/lib/ProductOpener/Products.pm index 8f1781309af76..cc862cd5bd4f4 100644 --- a/lib/ProductOpener/Products.pm +++ b/lib/ProductOpener/Products.pm @@ -107,6 +107,7 @@ BEGIN { &make_sure_numbers_are_stored_as_numbers &change_product_server_or_code + &change_product_type &find_and_replace_user_id_in_products @@ -1024,52 +1025,68 @@ sub retrieve_product_rev ($product_id, $rev, $include_deleted = 0) { sub change_product_server_or_code ($product_ref, $new_code, $errors_ref) { - # Currently only called by admins, can cause issues because of bug #677 + # Currently only called by admins and moderators my $code = $product_ref->{code}; - my $new_server = ""; - my $new_data_root = $data_root; if ($new_code =~ /^([a-z]+)$/) { - $new_server = $1; - if ( (defined $options{other_servers}) - and (defined $options{other_servers}{$new_server}) - and ($options{other_servers}{$new_server}{data_root} ne $data_root)) - { - $new_code = $code; - $new_data_root = $options{other_servers}{$new_server}{data_root}; + my $new_server = $1; + if (defined $options{flavors_product_types}{$new_server}) { + change_product_type($product_ref, $options{flavors_product_types}{$new_server}, $errors_ref); } } - $new_code = normalize_code($new_code); - if (not is_valid_code($new_code)) { - push @$errors_ref, lang("invalid_barcode"); - } else { - # check that the new code is available - if (-e "$new_data_root/products/" . product_path_from_id($new_code)) { - push @{$errors_ref}, lang("error_new_code_already_exists"); - $log->warn( - "cannot change product code, because the new code already exists", - {code => $code, new_code => $new_code, new_server => $new_server} - ) if $log->is_warn(); + $new_code = normalize_code($new_code); + if (not is_valid_code($new_code)) { + push @$errors_ref, lang("invalid_barcode"); } else { - $product_ref->{old_code} = $code; - $code = $new_code; - $product_ref->{code} = $code; - if ($new_server ne '') { - $product_ref->{new_server} = $new_server; - } - $log->info("changing code", - {old_code => $product_ref->{old_code}, code => $code, new_server => $new_server}) - if $log->is_info(); + # check that the new code is available + if (-e "$data_root/products/" . product_path_from_id($new_code) . "/product.sto") { + push @{$errors_ref}, lang("error_new_code_already_exists"); + $log->warn("cannot change product code, because the new code already exists", + {code => $code, new_code => $new_code}) + if $log->is_warn(); + } + else { + $product_ref->{old_code} = $code; + $code = $new_code; + $product_ref->{code} = $code; + $log->info("changing code", {old_code => $product_ref->{old_code}, code => $code}) + if $log->is_info(); + } } } return; } +sub change_product_type ($product_ref, $new_product_type, $errors_ref) { + + # Currently only called by admins and moderators + + my $product_type = $product_ref->{product_type}; + + # Return if the product type is already the new product type, or if the new product type is not defined + if ((not defined $new_product_type) or ($product_type eq $new_product_type)) { + return; + } + + if (not defined $options{product_types_flavors}{$new_product_type}) { + push @$errors_ref, lang("error_invalid_product_type"); + } + else { + $product_ref->{old_product_type} = $product_type; + $product_ref->{product_type} = $new_product_type; + $log->info("changing product type", + {old_product_type => $product_ref->{old_product_type}, product_type => $new_product_type}) + if $log->is_info(); + } + + return; +} + =head2 compute_sort_keys ( $product_ref ) Compute sort keys that are stored in the MongoDB database and used to order results of queries. @@ -1163,22 +1180,53 @@ sub store_product ($user_id, $product_ref, $comment) { } ) if $log->is_debug(); - # In case we need to move a product from OFF to OBF etc. - # the "new_server" value will be set to off, obf etc. - # we first move the existing files (product and images) - # and then store the product with a comment. + my $delete_from_previous_products_collection = 0; # if we have a "server" value (e.g. from an import), # we save the product on the corresponding server but we don't need to move an existing product + if (defined $product_ref->{server}) { + my $new_server = $product_ref->{server}; + # Update the product_type from the server + if (defined $options{flavors_product_types}{$new_server}) { + my $errors_ref = {}; + change_product_type($product_ref, $options{flavors_product_types}{$new_server}, $errors_ref); + } + delete $product_ref->{server}; + } - my $new_data_root = $data_root; - my $new_www_root = $www_root; + # In case we need to move a product from OFF to OBF etc. + # we will have a old_product_type field + + # Get the previous server and collection for the product + my $previous_server + = $options{product_types_flavors}{$product_ref->{old_product_type} + || $product_ref->{product_type} + || $options{product_type}}; # We use the was_obsolete flag so that we can remove the product from its old collection # (either products or products_obsolete) if its obsolete status has changed - my $previous_products_collection = get_products_collection({obsolete => $product_ref->{was_obsolete}}); - my $new_products_collection = get_products_collection({obsolete => $product_ref->{obsolete}}); - my $delete_from_previous_products_collection = 0; + my $previous_products_collection = get_products_collection( + {database => $options{other_servers}{$previous_server}{mongodb}, obsolete => $product_ref->{was_obsolete}}); + + # Change of product type + if (defined $product_ref->{old_product_type}) { + $log->info("changing product type", + {old_product_type => $product_ref->{old_product_type}, product_type => $product_ref->{product_type}}) + if $log->is_info(); + $delete_from_previous_products_collection = 1; + delete $product_ref->{old_product_type}; + } + + # Get the server and collection for the product that we will write + my $new_server = $options{product_types_flavors}{$product_ref->{product_type} || $options{product_type}}; + my $new_products_collection = get_products_collection( + {database => $options{other_servers}{$new_server}{mongodb}, obsolete => $product_ref->{obsolete}}); + + if ($previous_server ne $new_server) { + $log->info("changing server", {old_server => $previous_server, new_server => $new_server, code => $code}) + if $log->is_info(); + $delete_from_previous_products_collection = 1; + } # the obsolete (and was_obsolete) field is either undef or an empty string, or contains "on" if ( ($product_ref->{was_obsolete} and not $product_ref->{obsolete}) @@ -1204,36 +1252,17 @@ sub store_product ($user_id, $product_ref, $comment) { $action = "unarchived"; } } - delete $product_ref->{was_obsolete}; - if ( (defined $product_ref->{server}) - and (defined $options{other_servers}) - and (defined $options{other_servers}{$product_ref->{server}})) - { - my $server = $product_ref->{server}; - $new_data_root = $options{other_servers}{$server}{data_root}; - $new_www_root = $options{other_servers}{$server}{www_root}; - $new_products_collection = get_products_collection( - {database => $options{other_servers}{$server}{mongodb}, obsolete => $product_ref->{obsolete}}); - } + delete $product_ref->{was_obsolete}; + # Change of barcode if (defined $product_ref->{old_code}) { my $old_code = $product_ref->{old_code}; my $old_product_id = product_id_for_owner($Owner_id, $old_code); my $old_path = product_path_from_id($old_product_id); - if (defined $product_ref->{new_server}) { - my $new_server = $product_ref->{new_server}; - $new_data_root = $options{other_servers}{$new_server}{data_root}; - $new_www_root = $options{other_servers}{$new_server}{www_root}; - $new_products_collection = get_products_collection( - {database => $options{other_servers}{$new_server}{mongodb}, obsolete => $product_ref->{obsolete}}); - $product_ref->{server} = $product_ref->{new_server}; - delete $product_ref->{new_server}; - } - - $log->info("moving product", {old_code => $old_code, code => $code, new_data_root => $new_data_root}) + $log->info("moving product", {old_code => $old_code, code => $code}) if $log->is_info(); # Move directory @@ -1247,15 +1276,15 @@ sub store_product ($user_id, $product_ref, $comment) { $log->debug("creating product directories", {path => $path, prefix_path => $prefix_path}) if $log->is_debug(); # Create the directories for the product - ensure_dir_created_or_die("$new_data_root/products/$prefix_path"); - ensure_dir_created_or_die("$new_www_root/images/products/$prefix_path"); + ensure_dir_created_or_die("$data_root/products/$prefix_path"); + ensure_dir_created_or_die("$www_root/images/products/$prefix_path"); # Check if we are updating the product in place: # the code changed, but it is the same path # this can happen if the path is already normalized, but the code is not # in that case we just want to update the code, and remove the old one from MongoDB # we don't need to move the directories - if ("$BASE_DIRS{PRODUCTS}/$old_path" eq "$new_data_root/products/$path") { + if ("$BASE_DIRS{PRODUCTS}/$old_path" eq "$data_root/products/$path") { $log->debug("updating product code in place", {old_code => $old_code, code => $code}) if $log->is_debug(); delete $product_ref->{old_code}; # remove the old product from the previous collection @@ -1269,8 +1298,8 @@ sub store_product ($user_id, $product_ref, $comment) { $product_ref->{_id} = $product_ref->{code} . ''; # treat id as string; } - if ( (!-e "$new_data_root/products/$path") - and (!-e "$new_www_root/images/products/$path")) + if ( (!-e "$data_root/products/$path") + and (!-e "$www_root/images/products/$path")) { # File::Copy move() is intended to move files, not # directories. It does work on directories if the @@ -1289,7 +1318,7 @@ sub store_product ($user_id, $product_ref, $comment) { $log->debug("moving product data", {source => "$BASE_DIRS{PRODUCTS}/$old_path", destination => "$BASE_DIRS{PRODUCTS}/$path"}) if $log->is_debug(); - dirmove("$BASE_DIRS{PRODUCTS}/$old_path", "$new_data_root/products/$path") + dirmove("$BASE_DIRS{PRODUCTS}/$old_path", "$data_root/products/$path") or $log->error( "could not move product data", { @@ -1303,15 +1332,15 @@ sub store_product ($user_id, $product_ref, $comment) { "moving product images", { source => "$BASE_DIRS{PRODUCTS_IMAGES}/$old_path", - destination => "$new_www_root/images/products/$path" + destination => "$www_root/images/products/$path" } ) if $log->is_debug(); - dirmove("$BASE_DIRS{PRODUCTS_IMAGES}/$old_path", "$new_www_root/images/products/$path") + dirmove("$BASE_DIRS{PRODUCTS_IMAGES}/$old_path", "$www_root/images/products/$path") or $log->error( "could not move product images", { source => "$BASE_DIRS{PRODUCTS_IMAGES}/$old_path", - destination => "$new_www_root/images/products/$path", + destination => "$www_root/images/products/$path", error => $! } ); @@ -1329,15 +1358,15 @@ sub store_product ($user_id, $product_ref, $comment) { } else { - (-e "$new_data_root/products/$path") + (-e "$data_root/products/$path") and $log->error("cannot move product data, because the destination already exists", {source => "$BASE_DIRS{PRODUCTS}/$old_path", destination => "$BASE_DIRS{PRODUCTS}/$path"}); - (-e "$new_www_root/products/$path") + (-e "$www_root/products/$path") and $log->error( "cannot move product images data, because the destination already exists", { source => "$BASE_DIRS{PRODUCTS_IMAGES}/$old_path", - destination => "$new_www_root/images/products/$path" + destination => "$www_root/images/products/$path" } ); } @@ -1347,12 +1376,12 @@ sub store_product ($user_id, $product_ref, $comment) { if ($rev < 1) { # Create the directories for the product - ensure_dir_created_or_die("$new_data_root/products/$path"); - ensure_dir_created_or_die("$new_www_root/images/products/$path"); + ensure_dir_created_or_die("$data_root/products/$path"); + ensure_dir_created_or_die("$www_root/images/products/$path"); } # Check lock and previous version - my $changes_ref = retrieve("$new_data_root/products/$path/changes.sto"); + my $changes_ref = retrieve("$data_root/products/$path/changes.sto"); if (not defined $changes_ref) { $changes_ref = []; } @@ -1431,7 +1460,7 @@ sub store_product ($user_id, $product_ref, $comment) { my $blame_ref = {}; - compute_product_history_and_completeness($new_data_root, $product_ref, $changes_ref, $blame_ref); + compute_product_history_and_completeness($product_ref, $changes_ref, $blame_ref); compute_data_sources($product_ref, $changes_ref); @@ -1473,7 +1502,7 @@ sub store_product ($user_id, $product_ref, $comment) { } # First store the product data in a .sto file on disk - store("$new_data_root/products/$path/$rev.sto", $product_ref); + store("$data_root/products/$path/$rev.sto", $product_ref); # Also store the product in MongoDB, unless it was marked as deleted if ($product_ref->{deleted}) { @@ -1489,16 +1518,16 @@ sub store_product ($user_id, $product_ref, $comment) { } # Update link - my $link = "$new_data_root/products/$path/product.sto"; + my $link = "$data_root/products/$path/product.sto"; if (-l $link) { unlink($link) or $log->error("could not unlink old product.sto", {link => $link, error => $!}); } symlink("$rev.sto", $link) or $log->error("could not symlink to new revision", - {source => "$new_data_root/products/$path/$rev.sto", link => $link, error => $!}); + {source => "$data_root/products/$path/$rev.sto", link => $link, error => $!}); - store("$new_data_root/products/$path/changes.sto", $changes_ref); + store("$data_root/products/$path/changes.sto", $changes_ref); log_change($product_ref, $change_ref); $log->debug("store_product - done", {code => $code, product_id => $product_id}) if $log->is_debug(); @@ -2163,7 +2192,7 @@ sub record_user_edit_type ($users_ref, $user_type, $user_id) { return; } -sub compute_product_history_and_completeness ($product_data_root, $current_product_ref, $changes_ref, $blame_ref) { +sub compute_product_history_and_completeness ($current_product_ref, $changes_ref, $blame_ref) { my $code = $current_product_ref->{code}; my $product_id = $current_product_ref->{_id}; @@ -2213,13 +2242,13 @@ sub compute_product_history_and_completeness ($product_data_root, $current_produ # Read all previous versions to see which fields have been added or edited my @fields = ( - 'code', 'lang', - 'product_name', 'generic_name', - @ProductOpener::Config::product_fields, @ProductOpener::Config::product_other_fields, - 'no_nutrition_data', 'nutrition_data_per', - 'nutrition_data_prepared_per', 'serving_size', - 'allergens', 'traces', - 'ingredients_text' + 'product_type', 'code', + 'lang', 'product_name', + 'generic_name', @ProductOpener::Config::product_fields, + @ProductOpener::Config::product_other_fields, 'no_nutrition_data', + 'nutrition_data_per', 'nutrition_data_prepared_per', + 'serving_size', 'allergens', + 'traces', 'ingredients_text' ); my %previous = (uploaded_images => {}, selected_images => {}, fields => {}, nutriments => {}); @@ -2247,7 +2276,7 @@ sub compute_product_history_and_completeness ($product_data_root, $current_produ if (not defined $rev) { $rev = $revs; # was not set before June 2012 } - my $product_ref = retrieve("$product_data_root/products/$path/$rev.sto"); + my $product_ref = retrieve("$data_root/products/$path/$rev.sto"); # if not found, we may be be updating the product, with the latest rev not set yet if ((not defined $product_ref) or ($rev == $current_product_ref->{rev})) { diff --git a/lib/ProductOpener/URL.pm b/lib/ProductOpener/URL.pm index fe253558629cc..a36354e6bf3d9 100644 --- a/lib/ProductOpener/URL.pm +++ b/lib/ProductOpener/URL.pm @@ -62,23 +62,32 @@ use experimental 'smartmatch'; use ProductOpener::Config qw/:all/; use ProductOpener::Paths qw/%BASE_DIRS/; +use Data::DeepAccess qw(deep_get); + =head1 FUNCTIONS -=head2 format_subdomain( SUBDOMAIN ) +=head2 format_subdomain($sd, $product_type = undef)) C returns URL on the basis of subdomain and scheme (http/https) =head3 Arguments +=head4 subdomain + A scalar variable to indicate the subdomain (e.g. "us" or "static") needs to be passed as an argument. +=head4 product_type (optional) + +Defaults to the current server product type. If passed, use the domain for that product type. +(e.g. "beauty" -> "openbeautyfacts.org") + =head3 Return Values The function returns a URL by concatenating scheme, subdomain and server-domain. =cut -sub format_subdomain ($sd) { +sub format_subdomain ($sd, $product_type = undef) { return $sd unless $sd; my $scheme; @@ -89,8 +98,15 @@ sub format_subdomain ($sd) { $scheme = 'http'; } - return $scheme . '://' . $sd . '.' . $server_domain; + my $domain = $server_domain; + # If we have a product_type, different from the product_type of the server, use the domain for that product_type + if ((defined $product_type) and ($product_type ne $options{product_type})) { + + $domain + = deep_get(\%options, "product_types_domains", $product_type || $options{product_type}) || $server_domain; + } + return $scheme . '://' . $sd . '.' . $domain; } =head2 subdomain_supports_https( SUBDOMAIN ) diff --git a/po/common/common.pot b/po/common/common.pot index 23f62f229f7f0..6c543a6ffb8fa 100644 --- a/po/common/common.pot +++ b/po/common/common.pot @@ -7251,3 +7251,27 @@ msgstr "Poor repairability" msgctxt "attribute_repairability_index_france_bad_description_short" msgid "Bad repairability" msgstr "Bad repairability" + +msgctxt "product_type" +msgid "Product type" +msgstr "Product type" + +# Food product type +msgctxt "product_type_food" +msgid "Food" +msgstr "Food" + +# Pet food product type +msgctxt "product_type_petfood" +msgid "Pet food" +msgstr "Pet food" + +# Beauty product type +msgctxt "product_type_beauty" +msgid "Beauty" +msgstr "Beauty" + +# General product product type +msgctxt "product_type_product" +msgid "Product" +msgstr "Product" \ No newline at end of file diff --git a/po/common/en.po b/po/common/en.po index 6a4440b63cd6d..824e40c19ba17 100644 --- a/po/common/en.po +++ b/po/common/en.po @@ -7241,6 +7241,26 @@ msgctxt "attribute_repairability_index_france_bad_description_short" msgid "Bad repairability" msgstr "Bad repairability" -msgctxt "concerned_categories" -msgid "Bad repairability" -msgstr "Bad repairability" +msgctxt "product_type" +msgid "Product type" +msgstr "Product type" + +# Food product type +msgctxt "product_type_food" +msgid "Food" +msgstr "Food" + +# Pet food product type +msgctxt "product_type_petfood" +msgid "Pet food" +msgstr "Pet food" + +# Beauty product type +msgctxt "product_type_beauty" +msgid "Beauty" +msgstr "Beauty" + +# General product product type +msgctxt "product_type_product" +msgid "Product" +msgstr "Product" \ No newline at end of file diff --git a/templates/web/pages/product_edit/product_edit_form_display.tt.html b/templates/web/pages/product_edit/product_edit_form_display.tt.html index 077516be3e12d..a0c42b65076cb 100644 --- a/templates/web/pages/product_edit/product_edit_form_display.tt.html +++ b/templates/web/pages/product_edit/product_edit_form_display.tt.html @@ -67,6 +67,14 @@

[% title %]


[% END %] + [% IF moderator || owner_id %] + + diff --git a/tests/integration/api_v2_product_code_and_product_type_change.t b/tests/integration/api_v2_product_code_and_product_type_change.t new file mode 100644 index 0000000000000..dcc0e0a8f435e --- /dev/null +++ b/tests/integration/api_v2_product_code_and_product_type_change.t @@ -0,0 +1,191 @@ +#!/usr/bin/perl -w + +use ProductOpener::PerlStandards; + +use Test2::V0; +use ProductOpener::APITest qw/:all/; +use ProductOpener::Test qw/remove_all_products remove_all_users/; +use ProductOpener::TestDefaults qw/%admin_user_form %default_user_form %moderator_user_form/; + +use File::Basename "dirname"; + +use Storable qw(dclone); + +remove_all_users(); + +remove_all_products(); + +wait_application_ready(); + +# Create an admin +my $admin_ua = new_client(); +my $resp = create_user($admin_ua, \%admin_user_form); +ok(!html_displays_error($resp)); + +# Create a normal user +my $ua = new_client(); +my %create_user_args = (%default_user_form, (email => 'bob@gmail.com')); +$resp = create_user($ua, \%create_user_args); +ok(!html_displays_error($resp)); + +# Create a moderator +my $moderator_ua = new_client(); +$resp = create_user($moderator_ua, \%moderator_user_form); +ok(!html_displays_error($resp)); + +# Admin gives moderator status +my %moderator_edit_form = ( + %moderator_user_form, + user_group_moderator => "1", + type => "edit", +); +$resp = edit_user($admin_ua, \%moderator_edit_form); +ok(!html_displays_error($resp)); + +# Note: expected results are stored in json files, see execute_api_tests +my $tests_ref = [ + + # Test setup - Create a product + { + setup => 1, + test_case => 'setup-create-product', + method => 'PATCH', + path => '/api/v3/product/1234567890100', + body => '{ + "product": { + "product_name_en": "Test product 1", + "countries_tags": ["en:france"] + } + }', + }, + # Change the barcode + { + test_case => 'change-product-code-not-a-moderator', + method => 'POST', + path => '/cgi/product_jqm_multilingual.pl', + form => { + code => "1234567890100", + new_code => "1234567890101", + } + }, + # Get the product with the initial code + { + test_case => 'get-product-with-initial-code', + method => 'GET', + path => '/api/v3/product/1234567890100', + expected_status_code => 200, + }, + # Change the product with a moderator account + { + test_case => 'change-product-code-moderator', + method => 'POST', + path => '/cgi/product_jqm_multilingual.pl', + form => { + code => "1234567890100", + new_code => "1234567890102", + }, + ua => $moderator_ua, + }, + # Get the product with the new code + { + test_case => 'get-product-with-new-code', + method => 'GET', + path => '/api/v3/product/1234567890102', + expected_status_code => 200, + }, + # Send code=obf to move product to Open Beauty Facts + { + test_case => 'change-product-code-to-obf', + method => 'POST', + path => '/cgi/product_jqm_multilingual.pl', + form => { + code => "1234567890102", + new_code => "obf", + }, + ua => $moderator_ua, + }, + # Get the product with web interface + { + test_case => 'get-obf-product-with-web-interface', + method => 'GET', + path => '/product/1234567890102', + expected_status_code => 302, + expected_type => 'html', + }, + # Get the product with API v3 + { + test_case => 'get-obf-product-with-api-v3', + method => 'GET', + path => '/api/v3/product/1234567890102', + expected_status_code => 302, + expected_type => 'html', + }, + # Edit the product with web interface (display the form) + { + test_case => 'edit-obf-product-with-web-interface-display-form', + method => 'GET', + path => '/cgi/product.pl?type=edit&code=1234567890102', + expected_status_code => 302, + expected_type => 'html', + ua => $ua, + }, + # Edit the product with web interface (process the form) + { + test_case => 'edit-obf-product-with-web-interface-process-form', + method => 'POST', + path => '/cgi/product.pl', + form => { + type => "edit", + action => "process", + code => "1234567890102", + product_name => "Test product 2 - updated", + }, + expected_status_code => 302, + expected_type => 'html', + ua => $ua, + }, + # Create a new product + { + setup => 1, + test_case => 'setup-create-product-2', + method => 'PATCH', + path => '/api/v3/product/1234567890200', + body => '{ + "product": { + "product_name_en": "Test product 2", + "lang": "en", + "countries_tags": ["en:france"] + } + }', + }, + # Change the product_type field to opf + { + test_case => 'change-product-type-to-opf', + method => 'POST', + path => '/cgi/product_jqm_multilingual.pl', + form => { + code => "1234567890200", + new_code => "opf", + }, + ua => $moderator_ua, + }, + # Get the product with API v3 + { + test_case => 'get-product-opf', + method => 'GET', + path => '/api/v3/product/1234567890200', + expected_status_code => 302, + expected_type => 'html', + }, + # Search all products to check moved products are not on the off MongoDB database anymore + { + test_case => 'search-all-products', + method => 'GET', + path => '/cgi/search.pl?action=process&json=1&no_cache=1', + expected_status_code => 200, + }, +]; + +execute_api_tests(__FILE__, $tests_ref); + +done_testing(); diff --git a/tests/integration/expected_test_results/api_v2_product_code_and_product_type_change/change-product-code-moderator.json b/tests/integration/expected_test_results/api_v2_product_code_and_product_type_change/change-product-code-moderator.json new file mode 100644 index 0000000000000..39d4261a9d3f0 --- /dev/null +++ b/tests/integration/expected_test_results/api_v2_product_code_and_product_type_change/change-product-code-moderator.json @@ -0,0 +1,4 @@ +{ + "status" : 1, + "status_verbose" : "fields saved" +} diff --git a/tests/integration/expected_test_results/api_v2_product_code_and_product_type_change/change-product-code-not-a-moderator.json b/tests/integration/expected_test_results/api_v2_product_code_and_product_type_change/change-product-code-not-a-moderator.json new file mode 100644 index 0000000000000..39d4261a9d3f0 --- /dev/null +++ b/tests/integration/expected_test_results/api_v2_product_code_and_product_type_change/change-product-code-not-a-moderator.json @@ -0,0 +1,4 @@ +{ + "status" : 1, + "status_verbose" : "fields saved" +} diff --git a/tests/integration/expected_test_results/api_v2_product_code_and_product_type_change/change-product-code-to-obf.json b/tests/integration/expected_test_results/api_v2_product_code_and_product_type_change/change-product-code-to-obf.json new file mode 100644 index 0000000000000..39d4261a9d3f0 --- /dev/null +++ b/tests/integration/expected_test_results/api_v2_product_code_and_product_type_change/change-product-code-to-obf.json @@ -0,0 +1,4 @@ +{ + "status" : 1, + "status_verbose" : "fields saved" +} diff --git a/tests/integration/expected_test_results/api_v2_product_code_and_product_type_change/change-product-type-to-opf.json b/tests/integration/expected_test_results/api_v2_product_code_and_product_type_change/change-product-type-to-opf.json new file mode 100644 index 0000000000000..39d4261a9d3f0 --- /dev/null +++ b/tests/integration/expected_test_results/api_v2_product_code_and_product_type_change/change-product-type-to-opf.json @@ -0,0 +1,4 @@ +{ + "status" : 1, + "status_verbose" : "fields saved" +} diff --git a/tests/integration/expected_test_results/api_v2_product_code_and_product_type_change/edit-obf-product-with-web-interface-display-form.html b/tests/integration/expected_test_results/api_v2_product_code_and_product_type_change/edit-obf-product-with-web-interface-display-form.html new file mode 100644 index 0000000000000..5fc79a4e65fde --- /dev/null +++ b/tests/integration/expected_test_results/api_v2_product_code_and_product_type_change/edit-obf-product-with-web-interface-display-form.html @@ -0,0 +1,9 @@ + + +302 Found + +

Found

+

The document has moved here.

+
+
Apache/2.4.62 (Debian) Server at world.openfoodfacts.localhost Port 80
+ diff --git a/tests/integration/expected_test_results/api_v2_product_code_and_product_type_change/edit-obf-product-with-web-interface-process-form.html b/tests/integration/expected_test_results/api_v2_product_code_and_product_type_change/edit-obf-product-with-web-interface-process-form.html new file mode 100644 index 0000000000000..5fc79a4e65fde --- /dev/null +++ b/tests/integration/expected_test_results/api_v2_product_code_and_product_type_change/edit-obf-product-with-web-interface-process-form.html @@ -0,0 +1,9 @@ + + +302 Found + +

Found

+

The document has moved here.

+
+
Apache/2.4.62 (Debian) Server at world.openfoodfacts.localhost Port 80
+ diff --git a/tests/integration/expected_test_results/api_v2_product_code_and_product_type_change/get-obf-product-with-api-v3.html b/tests/integration/expected_test_results/api_v2_product_code_and_product_type_change/get-obf-product-with-api-v3.html new file mode 100644 index 0000000000000..f8bbf0a5e4afb --- /dev/null +++ b/tests/integration/expected_test_results/api_v2_product_code_and_product_type_change/get-obf-product-with-api-v3.html @@ -0,0 +1,9 @@ + + +302 Found + +

Found

+

The document has moved here.

+
+
Apache/2.4.62 (Debian) Server at world.openfoodfacts.localhost Port 80
+ diff --git a/tests/integration/expected_test_results/api_v2_product_code_and_product_type_change/get-obf-product-with-web-interface.html b/tests/integration/expected_test_results/api_v2_product_code_and_product_type_change/get-obf-product-with-web-interface.html new file mode 100644 index 0000000000000..b8f908730d61c --- /dev/null +++ b/tests/integration/expected_test_results/api_v2_product_code_and_product_type_change/get-obf-product-with-web-interface.html @@ -0,0 +1,9 @@ + + +302 Found + +

Found

+

The document has moved here.

+
+
Apache/2.4.62 (Debian) Server at world.openfoodfacts.localhost Port 80
+ diff --git a/tests/integration/expected_test_results/api_v2_product_code_and_product_type_change/get-product-opf.html b/tests/integration/expected_test_results/api_v2_product_code_and_product_type_change/get-product-opf.html new file mode 100644 index 0000000000000..501bcd5effc28 --- /dev/null +++ b/tests/integration/expected_test_results/api_v2_product_code_and_product_type_change/get-product-opf.html @@ -0,0 +1,9 @@ + + +302 Found + +

Found

+

The document has moved here.

+
+
Apache/2.4.62 (Debian) Server at world.openfoodfacts.localhost Port 80
+ diff --git a/tests/integration/expected_test_results/api_v2_product_code_and_product_type_change/get-product-with-initial-code.json b/tests/integration/expected_test_results/api_v2_product_code_and_product_type_change/get-product-with-initial-code.json new file mode 100644 index 0000000000000..21d9b467427c5 --- /dev/null +++ b/tests/integration/expected_test_results/api_v2_product_code_and_product_type_change/get-product-with-initial-code.json @@ -0,0 +1,523 @@ +{ + "code" : "1234567890100", + "errors" : [], + "product" : { + "_id" : "1234567890100", + "_keywords" : [ + "product", + "test" + ], + "added_countries_tags" : [], + "allergens" : "", + "allergens_from_ingredients" : "", + "allergens_from_user" : "(en) ", + "allergens_hierarchy" : [], + "allergens_tags" : [], + "categories_properties" : {}, + "categories_properties_tags" : [ + "all-products", + "categories-unknown", + "agribalyse-food-code-unknown", + "agribalyse-proxy-food-code-unknown", + "ciqual-food-code-unknown", + "agribalyse-unknown" + ], + "checkers_tags" : [], + "code" : "1234567890100", + "codes_tags" : [ + "code-13", + "1234567890xxx", + "123456789xxxx", + "12345678xxxxx", + "1234567xxxxxx", + "123456xxxxxxx", + "12345xxxxxxxx", + "1234xxxxxxxxx", + "123xxxxxxxxxx", + "12xxxxxxxxxxx", + "1xxxxxxxxxxxx" + ], + "complete" : 0, + "completeness" : 0.1, + "correctors_tags" : [], + "countries" : "en:france", + "countries_hierarchy" : [ + "en:france" + ], + "countries_lc" : "en", + "countries_tags" : [ + "en:france" + ], + "created_t" : "--ignore--", + "creator" : "openfoodfacts-contributors", + "data_quality_bugs_tags" : [], + "data_quality_errors_tags" : [], + "data_quality_info_tags" : [ + "en:no-packaging-data", + "en:ecoscore-extended-data-not-computed", + "en:food-groups-1-unknown", + "en:food-groups-2-unknown", + "en:food-groups-3-unknown" + ], + "data_quality_tags" : [ + "en:no-packaging-data", + "en:ecoscore-extended-data-not-computed", + "en:food-groups-1-unknown", + "en:food-groups-2-unknown", + "en:food-groups-3-unknown", + "en:ecoscore-origins-of-ingredients-origins-are-100-percent-unknown", + "en:ecoscore-packaging-packaging-data-missing", + "en:ecoscore-production-system-no-label", + "en:ecoscore-threatened-species-ingredients-missing" + ], + "data_quality_warnings_tags" : [ + "en:ecoscore-origins-of-ingredients-origins-are-100-percent-unknown", + "en:ecoscore-packaging-packaging-data-missing", + "en:ecoscore-production-system-no-label", + "en:ecoscore-threatened-species-ingredients-missing" + ], + "ecoscore_data" : { + "adjustments" : { + "origins_of_ingredients" : { + "aggregated_origins" : [ + { + "epi_score" : 0, + "origin" : "en:unknown", + "percent" : 100, + "transportation_score" : 0 + } + ], + "epi_score" : 0, + "epi_value" : -5, + "origins_from_categories" : [ + "en:unknown" + ], + "origins_from_origins_field" : [ + "en:unknown" + ], + "transportation_score" : 0, + "transportation_scores" : { + "ad" : 0, + "al" : 0, + "at" : 0, + "ax" : 0, + "ba" : 0, + "be" : 0, + "bg" : 0, + "ch" : 0, + "cy" : 0, + "cz" : 0, + "de" : 0, + "dk" : 0, + "dz" : 0, + "ee" : 0, + "eg" : 0, + "es" : 0, + "fi" : 0, + "fo" : 0, + "fr" : 0, + "gg" : 0, + "gi" : 0, + "gr" : 0, + "hr" : 0, + "hu" : 0, + "ie" : 0, + "il" : 0, + "im" : 0, + "is" : 0, + "it" : 0, + "je" : 0, + "lb" : 0, + "li" : 0, + "lt" : 0, + "lu" : 0, + "lv" : 0, + "ly" : 0, + "ma" : 0, + "mc" : 0, + "md" : 0, + "me" : 0, + "mk" : 0, + "mt" : 0, + "nl" : 0, + "no" : 0, + "pl" : 0, + "ps" : 0, + "pt" : 0, + "ro" : 0, + "rs" : 0, + "se" : 0, + "si" : 0, + "sj" : 0, + "sk" : 0, + "sm" : 0, + "sy" : 0, + "tn" : 0, + "tr" : 0, + "ua" : 0, + "uk" : 0, + "us" : 0, + "va" : 0, + "world" : 0, + "xk" : 0 + }, + "transportation_value" : 0, + "transportation_values" : { + "ad" : 0, + "al" : 0, + "at" : 0, + "ax" : 0, + "ba" : 0, + "be" : 0, + "bg" : 0, + "ch" : 0, + "cy" : 0, + "cz" : 0, + "de" : 0, + "dk" : 0, + "dz" : 0, + "ee" : 0, + "eg" : 0, + "es" : 0, + "fi" : 0, + "fo" : 0, + "fr" : 0, + "gg" : 0, + "gi" : 0, + "gr" : 0, + "hr" : 0, + "hu" : 0, + "ie" : 0, + "il" : 0, + "im" : 0, + "is" : 0, + "it" : 0, + "je" : 0, + "lb" : 0, + "li" : 0, + "lt" : 0, + "lu" : 0, + "lv" : 0, + "ly" : 0, + "ma" : 0, + "mc" : 0, + "md" : 0, + "me" : 0, + "mk" : 0, + "mt" : 0, + "nl" : 0, + "no" : 0, + "pl" : 0, + "ps" : 0, + "pt" : 0, + "ro" : 0, + "rs" : 0, + "se" : 0, + "si" : 0, + "sj" : 0, + "sk" : 0, + "sm" : 0, + "sy" : 0, + "tn" : 0, + "tr" : 0, + "ua" : 0, + "uk" : 0, + "us" : 0, + "va" : 0, + "world" : 0, + "xk" : 0 + }, + "value" : -5, + "values" : { + "ad" : -5, + "al" : -5, + "at" : -5, + "ax" : -5, + "ba" : -5, + "be" : -5, + "bg" : -5, + "ch" : -5, + "cy" : -5, + "cz" : -5, + "de" : -5, + "dk" : -5, + "dz" : -5, + "ee" : -5, + "eg" : -5, + "es" : -5, + "fi" : -5, + "fo" : -5, + "fr" : -5, + "gg" : -5, + "gi" : -5, + "gr" : -5, + "hr" : -5, + "hu" : -5, + "ie" : -5, + "il" : -5, + "im" : -5, + "is" : -5, + "it" : -5, + "je" : -5, + "lb" : -5, + "li" : -5, + "lt" : -5, + "lu" : -5, + "lv" : -5, + "ly" : -5, + "ma" : -5, + "mc" : -5, + "md" : -5, + "me" : -5, + "mk" : -5, + "mt" : -5, + "nl" : -5, + "no" : -5, + "pl" : -5, + "ps" : -5, + "pt" : -5, + "ro" : -5, + "rs" : -5, + "se" : -5, + "si" : -5, + "sj" : -5, + "sk" : -5, + "sm" : -5, + "sy" : -5, + "tn" : -5, + "tr" : -5, + "ua" : -5, + "uk" : -5, + "us" : -5, + "va" : -5, + "world" : -5, + "xk" : -5 + }, + "warning" : "origins_are_100_percent_unknown" + }, + "packaging" : { + "value" : -15, + "warning" : "packaging_data_missing" + }, + "production_system" : { + "labels" : [], + "value" : 0, + "warning" : "no_label" + }, + "threatened_species" : { + "warning" : "ingredients_missing" + } + }, + "agribalyse" : { + "warning" : "missing_agribalyse_match" + }, + "missing" : { + "categories" : 1, + "ingredients" : 1, + "labels" : 1, + "origins" : 1, + "packagings" : 1 + }, + "missing_agribalyse_match_warning" : 1, + "missing_key_data" : 1, + "scores" : {}, + "status" : "unknown" + }, + "ecoscore_grade" : "unknown", + "ecoscore_tags" : [ + "unknown" + ], + "editors_tags" : [ + "openfoodfacts-contributors" + ], + "entry_dates_tags" : "--ignore--", + "food_groups_tags" : [], + "id" : "1234567890100", + "informers_tags" : [ + "openfoodfacts-contributors" + ], + "ingredients_lc" : "en", + "interface_version_created" : "20221102/api/v3", + "interface_version_modified" : "20150316.jqm2", + "lang" : "en", + "languages" : { + "en:english" : 1 + }, + "languages_codes" : { + "en" : 1 + }, + "languages_hierarchy" : [ + "en:english" + ], + "languages_tags" : [ + "en:english", + "en:1" + ], + "last_edit_dates_tags" : "--ignore--", + "last_modified_t" : "--ignore--", + "last_updated_t" : "--ignore--", + "lc" : "en", + "main_countries_tags" : [], + "misc_tags" : [ + "en:ecoscore-extended-data-not-computed", + "en:ecoscore-not-computed", + "en:nutriscore-missing-category", + "en:nutriscore-missing-nutrition-data", + "en:nutriscore-missing-nutrition-data-energy", + "en:nutriscore-missing-nutrition-data-fat", + "en:nutriscore-missing-nutrition-data-proteins", + "en:nutriscore-missing-nutrition-data-saturated-fat", + "en:nutriscore-missing-nutrition-data-sodium", + "en:nutriscore-missing-nutrition-data-sugars", + "en:nutriscore-not-computed", + "en:nutrition-no-fiber", + "en:nutrition-no-fiber-or-fruits-vegetables-nuts", + "en:nutrition-no-fruits-vegetables-nuts", + "en:nutrition-not-enough-data-to-compute-nutrition-score", + "en:packagings-empty", + "en:packagings-not-complete", + "en:packagings-number-of-components-0", + "en:main-countries-new-product" + ], + "nova_group_debug" : "no nova group when the product does not have ingredients", + "nova_group_error" : "missing_ingredients", + "nova_groups_tags" : [ + "unknown" + ], + "nutrient_levels" : {}, + "nutrient_levels_tags" : [], + "nutriments" : {}, + "nutriscore" : { + "2021" : { + "category_available" : 0, + "data" : { + "energy" : null, + "fiber" : 0, + "fruits_vegetables_nuts_colza_walnut_olive_oils" : 0, + "is_beverage" : 0, + "is_cheese" : 0, + "is_fat" : 0, + "is_water" : 0, + "proteins" : null, + "saturated_fat" : null, + "sodium" : null, + "sugars" : null + }, + "grade" : "unknown", + "nutrients_available" : 0, + "nutriscore_applicable" : 0, + "nutriscore_computed" : 0 + }, + "2023" : { + "category_available" : 0, + "data" : { + "energy" : null, + "fiber" : null, + "fruits_vegetables_legumes" : null, + "is_beverage" : 0, + "is_cheese" : 0, + "is_fat_oil_nuts_seeds" : 0, + "is_red_meat_product" : 0, + "is_water" : 0, + "proteins" : null, + "salt" : null, + "saturated_fat" : null, + "sugars" : null + }, + "grade" : "unknown", + "nutrients_available" : 0, + "nutriscore_applicable" : 0, + "nutriscore_computed" : 0 + } + }, + "nutriscore_2021_tags" : [ + "unknown" + ], + "nutriscore_2023_tags" : [ + "unknown" + ], + "nutriscore_grade" : "unknown", + "nutriscore_tags" : [ + "unknown" + ], + "nutriscore_version" : "2021", + "nutrition_data_per" : "100g", + "nutrition_data_prepared_per" : "100g", + "nutrition_grade_fr" : "unknown", + "nutrition_grades" : "unknown", + "nutrition_grades_tags" : [ + "unknown" + ], + "nutrition_score_beverage" : 0, + "nutrition_score_debug" : "no score when the product does not have a category - missing energy_100g - missing fat_100g - missing saturated-fat_100g - missing sugars_100g - missing sodium_100g - missing proteins_100g", + "nutrition_score_warning_no_fiber" : 1, + "nutrition_score_warning_no_fruits_vegetables_nuts" : 1, + "packaging_materials_tags" : [], + "packaging_recycling_tags" : [], + "packaging_shapes_tags" : [], + "packagings" : [], + "packagings_materials" : {}, + "photographers_tags" : [], + "pnns_groups_1" : "unknown", + "pnns_groups_1_tags" : [ + "unknown", + "missing-category" + ], + "pnns_groups_2" : "unknown", + "pnns_groups_2_tags" : [ + "unknown", + "missing-category" + ], + "popularity_key" : 0, + "product_name" : "Test product 1", + "product_name_en" : "Test product 1", + "product_type" : "food", + "removed_countries_tags" : [], + "rev" : 2, + "states" : "en:to-be-completed, en:nutrition-facts-to-be-completed, en:ingredients-to-be-completed, en:expiration-date-to-be-completed, en:packaging-code-to-be-completed, en:characteristics-to-be-completed, en:origins-to-be-completed, en:categories-to-be-completed, en:brands-to-be-completed, en:packaging-to-be-completed, en:quantity-to-be-completed, en:product-name-completed, en:photos-to-be-uploaded", + "states_hierarchy" : [ + "en:to-be-completed", + "en:nutrition-facts-to-be-completed", + "en:ingredients-to-be-completed", + "en:expiration-date-to-be-completed", + "en:packaging-code-to-be-completed", + "en:characteristics-to-be-completed", + "en:origins-to-be-completed", + "en:categories-to-be-completed", + "en:brands-to-be-completed", + "en:packaging-to-be-completed", + "en:quantity-to-be-completed", + "en:product-name-completed", + "en:photos-to-be-uploaded" + ], + "states_tags" : [ + "en:to-be-completed", + "en:nutrition-facts-to-be-completed", + "en:ingredients-to-be-completed", + "en:expiration-date-to-be-completed", + "en:packaging-code-to-be-completed", + "en:characteristics-to-be-completed", + "en:origins-to-be-completed", + "en:categories-to-be-completed", + "en:brands-to-be-completed", + "en:packaging-to-be-completed", + "en:quantity-to-be-completed", + "en:product-name-completed", + "en:photos-to-be-uploaded" + ], + "traces" : "", + "traces_from_ingredients" : "", + "traces_from_user" : "(en) ", + "traces_hierarchy" : [], + "traces_tags" : [], + "unknown_nutrients_tags" : [], + "weighers_tags" : [] + }, + "result" : { + "id" : "product_found", + "lc_name" : "Product found", + "name" : "Product found" + }, + "status" : "success", + "warnings" : [] +} diff --git a/tests/integration/expected_test_results/api_v2_product_code_and_product_type_change/get-product-with-new-code.json b/tests/integration/expected_test_results/api_v2_product_code_and_product_type_change/get-product-with-new-code.json new file mode 100644 index 0000000000000..582e8fbd512d6 --- /dev/null +++ b/tests/integration/expected_test_results/api_v2_product_code_and_product_type_change/get-product-with-new-code.json @@ -0,0 +1,528 @@ +{ + "code" : "1234567890102", + "errors" : [], + "product" : { + "_id" : "1234567890102", + "_keywords" : [ + "product", + "test" + ], + "added_countries_tags" : [], + "allergens" : "", + "allergens_from_ingredients" : "", + "allergens_from_user" : "(en) ", + "allergens_hierarchy" : [], + "allergens_tags" : [], + "categories_properties" : {}, + "categories_properties_tags" : [ + "all-products", + "categories-unknown", + "agribalyse-food-code-unknown", + "agribalyse-proxy-food-code-unknown", + "ciqual-food-code-unknown", + "agribalyse-unknown" + ], + "checkers_tags" : [], + "code" : "1234567890102", + "codes_tags" : [ + "code-13", + "1234567890xxx", + "123456789xxxx", + "12345678xxxxx", + "1234567xxxxxx", + "123456xxxxxxx", + "12345xxxxxxxx", + "1234xxxxxxxxx", + "123xxxxxxxxxx", + "12xxxxxxxxxxx", + "1xxxxxxxxxxxx" + ], + "complete" : 0, + "completeness" : 0.1, + "correctors_tags" : [ + "moderator" + ], + "countries" : "en:france", + "countries_hierarchy" : [ + "en:france" + ], + "countries_lc" : "en", + "countries_tags" : [ + "en:france" + ], + "created_t" : "--ignore--", + "creator" : "openfoodfacts-contributors", + "data_quality_bugs_tags" : [], + "data_quality_errors_tags" : [], + "data_quality_info_tags" : [ + "en:no-packaging-data", + "en:ecoscore-extended-data-not-computed", + "en:food-groups-1-unknown", + "en:food-groups-2-unknown", + "en:food-groups-3-unknown" + ], + "data_quality_tags" : [ + "en:no-packaging-data", + "en:ecoscore-extended-data-not-computed", + "en:food-groups-1-unknown", + "en:food-groups-2-unknown", + "en:food-groups-3-unknown", + "en:ecoscore-origins-of-ingredients-origins-are-100-percent-unknown", + "en:ecoscore-packaging-packaging-data-missing", + "en:ecoscore-production-system-no-label", + "en:ecoscore-threatened-species-ingredients-missing" + ], + "data_quality_warnings_tags" : [ + "en:ecoscore-origins-of-ingredients-origins-are-100-percent-unknown", + "en:ecoscore-packaging-packaging-data-missing", + "en:ecoscore-production-system-no-label", + "en:ecoscore-threatened-species-ingredients-missing" + ], + "ecoscore_data" : { + "adjustments" : { + "origins_of_ingredients" : { + "aggregated_origins" : [ + { + "epi_score" : 0, + "origin" : "en:unknown", + "percent" : 100, + "transportation_score" : 0 + } + ], + "epi_score" : 0, + "epi_value" : -5, + "origins_from_categories" : [ + "en:unknown" + ], + "origins_from_origins_field" : [ + "en:unknown" + ], + "transportation_score" : 0, + "transportation_scores" : { + "ad" : 0, + "al" : 0, + "at" : 0, + "ax" : 0, + "ba" : 0, + "be" : 0, + "bg" : 0, + "ch" : 0, + "cy" : 0, + "cz" : 0, + "de" : 0, + "dk" : 0, + "dz" : 0, + "ee" : 0, + "eg" : 0, + "es" : 0, + "fi" : 0, + "fo" : 0, + "fr" : 0, + "gg" : 0, + "gi" : 0, + "gr" : 0, + "hr" : 0, + "hu" : 0, + "ie" : 0, + "il" : 0, + "im" : 0, + "is" : 0, + "it" : 0, + "je" : 0, + "lb" : 0, + "li" : 0, + "lt" : 0, + "lu" : 0, + "lv" : 0, + "ly" : 0, + "ma" : 0, + "mc" : 0, + "md" : 0, + "me" : 0, + "mk" : 0, + "mt" : 0, + "nl" : 0, + "no" : 0, + "pl" : 0, + "ps" : 0, + "pt" : 0, + "ro" : 0, + "rs" : 0, + "se" : 0, + "si" : 0, + "sj" : 0, + "sk" : 0, + "sm" : 0, + "sy" : 0, + "tn" : 0, + "tr" : 0, + "ua" : 0, + "uk" : 0, + "us" : 0, + "va" : 0, + "world" : 0, + "xk" : 0 + }, + "transportation_value" : 0, + "transportation_values" : { + "ad" : 0, + "al" : 0, + "at" : 0, + "ax" : 0, + "ba" : 0, + "be" : 0, + "bg" : 0, + "ch" : 0, + "cy" : 0, + "cz" : 0, + "de" : 0, + "dk" : 0, + "dz" : 0, + "ee" : 0, + "eg" : 0, + "es" : 0, + "fi" : 0, + "fo" : 0, + "fr" : 0, + "gg" : 0, + "gi" : 0, + "gr" : 0, + "hr" : 0, + "hu" : 0, + "ie" : 0, + "il" : 0, + "im" : 0, + "is" : 0, + "it" : 0, + "je" : 0, + "lb" : 0, + "li" : 0, + "lt" : 0, + "lu" : 0, + "lv" : 0, + "ly" : 0, + "ma" : 0, + "mc" : 0, + "md" : 0, + "me" : 0, + "mk" : 0, + "mt" : 0, + "nl" : 0, + "no" : 0, + "pl" : 0, + "ps" : 0, + "pt" : 0, + "ro" : 0, + "rs" : 0, + "se" : 0, + "si" : 0, + "sj" : 0, + "sk" : 0, + "sm" : 0, + "sy" : 0, + "tn" : 0, + "tr" : 0, + "ua" : 0, + "uk" : 0, + "us" : 0, + "va" : 0, + "world" : 0, + "xk" : 0 + }, + "value" : -5, + "values" : { + "ad" : -5, + "al" : -5, + "at" : -5, + "ax" : -5, + "ba" : -5, + "be" : -5, + "bg" : -5, + "ch" : -5, + "cy" : -5, + "cz" : -5, + "de" : -5, + "dk" : -5, + "dz" : -5, + "ee" : -5, + "eg" : -5, + "es" : -5, + "fi" : -5, + "fo" : -5, + "fr" : -5, + "gg" : -5, + "gi" : -5, + "gr" : -5, + "hr" : -5, + "hu" : -5, + "ie" : -5, + "il" : -5, + "im" : -5, + "is" : -5, + "it" : -5, + "je" : -5, + "lb" : -5, + "li" : -5, + "lt" : -5, + "lu" : -5, + "lv" : -5, + "ly" : -5, + "ma" : -5, + "mc" : -5, + "md" : -5, + "me" : -5, + "mk" : -5, + "mt" : -5, + "nl" : -5, + "no" : -5, + "pl" : -5, + "ps" : -5, + "pt" : -5, + "ro" : -5, + "rs" : -5, + "se" : -5, + "si" : -5, + "sj" : -5, + "sk" : -5, + "sm" : -5, + "sy" : -5, + "tn" : -5, + "tr" : -5, + "ua" : -5, + "uk" : -5, + "us" : -5, + "va" : -5, + "world" : -5, + "xk" : -5 + }, + "warning" : "origins_are_100_percent_unknown" + }, + "packaging" : { + "value" : -15, + "warning" : "packaging_data_missing" + }, + "production_system" : { + "labels" : [], + "value" : 0, + "warning" : "no_label" + }, + "threatened_species" : { + "warning" : "ingredients_missing" + } + }, + "agribalyse" : { + "warning" : "missing_agribalyse_match" + }, + "missing" : { + "categories" : 1, + "ingredients" : 1, + "labels" : 1, + "origins" : 1, + "packagings" : 1 + }, + "missing_agribalyse_match_warning" : 1, + "missing_key_data" : 1, + "scores" : {}, + "status" : "unknown" + }, + "ecoscore_grade" : "unknown", + "ecoscore_tags" : [ + "unknown" + ], + "editors_tags" : [ + "moderator", + "openfoodfacts-contributors" + ], + "entry_dates_tags" : "--ignore--", + "food_groups_tags" : [], + "id" : "1234567890100", + "informers_tags" : [ + "openfoodfacts-contributors" + ], + "ingredients_lc" : "en", + "interface_version_created" : "20221102/api/v3", + "interface_version_modified" : "20150316.jqm2", + "lang" : "en", + "languages" : { + "en:english" : 1 + }, + "languages_codes" : { + "en" : 1 + }, + "languages_hierarchy" : [ + "en:english" + ], + "languages_tags" : [ + "en:english", + "en:1" + ], + "last_edit_dates_tags" : "--ignore--", + "last_editor" : "moderator", + "last_modified_by" : "moderator", + "last_modified_t" : "--ignore--", + "last_updated_t" : "--ignore--", + "lc" : "en", + "main_countries_tags" : [], + "misc_tags" : [ + "en:ecoscore-extended-data-not-computed", + "en:ecoscore-not-computed", + "en:nutriscore-missing-category", + "en:nutriscore-missing-nutrition-data", + "en:nutriscore-missing-nutrition-data-energy", + "en:nutriscore-missing-nutrition-data-fat", + "en:nutriscore-missing-nutrition-data-proteins", + "en:nutriscore-missing-nutrition-data-saturated-fat", + "en:nutriscore-missing-nutrition-data-sodium", + "en:nutriscore-missing-nutrition-data-sugars", + "en:nutriscore-not-computed", + "en:nutrition-no-fiber", + "en:nutrition-no-fiber-or-fruits-vegetables-nuts", + "en:nutrition-no-fruits-vegetables-nuts", + "en:nutrition-not-enough-data-to-compute-nutrition-score", + "en:packagings-empty", + "en:packagings-not-complete", + "en:packagings-number-of-components-0", + "en:main-countries-new-product" + ], + "nova_group_debug" : "no nova group when the product does not have ingredients", + "nova_group_error" : "missing_ingredients", + "nova_groups_tags" : [ + "unknown" + ], + "nutrient_levels" : {}, + "nutrient_levels_tags" : [], + "nutriments" : {}, + "nutriscore" : { + "2021" : { + "category_available" : 0, + "data" : { + "energy" : null, + "fiber" : 0, + "fruits_vegetables_nuts_colza_walnut_olive_oils" : 0, + "is_beverage" : 0, + "is_cheese" : 0, + "is_fat" : 0, + "is_water" : 0, + "proteins" : null, + "saturated_fat" : null, + "sodium" : null, + "sugars" : null + }, + "grade" : "unknown", + "nutrients_available" : 0, + "nutriscore_applicable" : 0, + "nutriscore_computed" : 0 + }, + "2023" : { + "category_available" : 0, + "data" : { + "energy" : null, + "fiber" : null, + "fruits_vegetables_legumes" : null, + "is_beverage" : 0, + "is_cheese" : 0, + "is_fat_oil_nuts_seeds" : 0, + "is_red_meat_product" : 0, + "is_water" : 0, + "proteins" : null, + "salt" : null, + "saturated_fat" : null, + "sugars" : null + }, + "grade" : "unknown", + "nutrients_available" : 0, + "nutriscore_applicable" : 0, + "nutriscore_computed" : 0 + } + }, + "nutriscore_2021_tags" : [ + "unknown" + ], + "nutriscore_2023_tags" : [ + "unknown" + ], + "nutriscore_grade" : "unknown", + "nutriscore_tags" : [ + "unknown" + ], + "nutriscore_version" : "2021", + "nutrition_data_per" : "100g", + "nutrition_data_prepared_per" : "100g", + "nutrition_grade_fr" : "unknown", + "nutrition_grades" : "unknown", + "nutrition_grades_tags" : [ + "unknown" + ], + "nutrition_score_beverage" : 0, + "nutrition_score_debug" : "no score when the product does not have a category - missing energy_100g - missing fat_100g - missing saturated-fat_100g - missing sugars_100g - missing sodium_100g - missing proteins_100g", + "nutrition_score_warning_no_fiber" : 1, + "nutrition_score_warning_no_fruits_vegetables_nuts" : 1, + "packaging_materials_tags" : [], + "packaging_recycling_tags" : [], + "packaging_shapes_tags" : [], + "packagings" : [], + "packagings_materials" : {}, + "photographers_tags" : [], + "pnns_groups_1" : "unknown", + "pnns_groups_1_tags" : [ + "unknown", + "missing-category" + ], + "pnns_groups_2" : "unknown", + "pnns_groups_2_tags" : [ + "unknown", + "missing-category" + ], + "popularity_key" : 0, + "product_name" : "Test product 1", + "product_name_en" : "Test product 1", + "product_type" : "food", + "removed_countries_tags" : [], + "rev" : 3, + "states" : "en:to-be-completed, en:nutrition-facts-to-be-completed, en:ingredients-to-be-completed, en:expiration-date-to-be-completed, en:packaging-code-to-be-completed, en:characteristics-to-be-completed, en:origins-to-be-completed, en:categories-to-be-completed, en:brands-to-be-completed, en:packaging-to-be-completed, en:quantity-to-be-completed, en:product-name-completed, en:photos-to-be-uploaded", + "states_hierarchy" : [ + "en:to-be-completed", + "en:nutrition-facts-to-be-completed", + "en:ingredients-to-be-completed", + "en:expiration-date-to-be-completed", + "en:packaging-code-to-be-completed", + "en:characteristics-to-be-completed", + "en:origins-to-be-completed", + "en:categories-to-be-completed", + "en:brands-to-be-completed", + "en:packaging-to-be-completed", + "en:quantity-to-be-completed", + "en:product-name-completed", + "en:photos-to-be-uploaded" + ], + "states_tags" : [ + "en:to-be-completed", + "en:nutrition-facts-to-be-completed", + "en:ingredients-to-be-completed", + "en:expiration-date-to-be-completed", + "en:packaging-code-to-be-completed", + "en:characteristics-to-be-completed", + "en:origins-to-be-completed", + "en:categories-to-be-completed", + "en:brands-to-be-completed", + "en:packaging-to-be-completed", + "en:quantity-to-be-completed", + "en:product-name-completed", + "en:photos-to-be-uploaded" + ], + "traces" : "", + "traces_from_ingredients" : "", + "traces_from_user" : "(en) ", + "traces_hierarchy" : [], + "traces_tags" : [], + "unknown_nutrients_tags" : [], + "weighers_tags" : [] + }, + "result" : { + "id" : "product_found", + "lc_name" : "Product found", + "name" : "Product found" + }, + "status" : "success", + "warnings" : [] +} diff --git a/tests/integration/expected_test_results/api_v2_product_code_and_product_type_change/search-all-products.json b/tests/integration/expected_test_results/api_v2_product_code_and_product_type_change/search-all-products.json new file mode 100644 index 0000000000000..8c298a45fd1a0 --- /dev/null +++ b/tests/integration/expected_test_results/api_v2_product_code_and_product_type_change/search-all-products.json @@ -0,0 +1,8 @@ +{ + "count" : 0, + "page" : 1, + "page_count" : 0, + "page_size" : 50, + "products" : [], + "skip" : 0 +} diff --git a/tests/integration/expected_test_results/web_html/fr-edit-product.html b/tests/integration/expected_test_results/web_html/fr-edit-product.html index 4d9ac5aa954ba..f679024163eba 100644 --- a/tests/integration/expected_test_results/web_html/fr-edit-product.html +++ b/tests/integration/expected_test_results/web_html/fr-edit-product.html @@ -448,6 +448,8 @@

+ +