Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Revert product to a previous revision (API + upcoming website integration for moderators) #9800

Merged
merged 24 commits into from
Mar 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
3c614e2
feat: revert product to previous revision #1405
stephanegigandet Feb 14, 2024
41d788a
API to revert a product to a previous revision
stephanegigandet Feb 14, 2024
92ece77
fix permission issue, update tests
stephanegigandet Feb 15, 2024
12a4044
product revert
stephanegigandet Feb 16, 2024
a19a42c
update tests
stephanegigandet Feb 16, 2024
9bf1abd
merged main, fixed conflicts
stephanegigandet Feb 16, 2024
c3e6a44
fix lint issues
stephanegigandet Feb 19, 2024
db56e09
/api/v3/product_revert
stephanegigandet Feb 19, 2024
687cb6b
Update lib/ProductOpener/Permissions.pm
stephanegigandet Feb 26, 2024
b31fad2
update tests, suggestion from code review
stephanegigandet Feb 29, 2024
446bede
suggestions from code review
stephanegigandet Feb 29, 2024
9a9e113
update tests
stephanegigandet Feb 29, 2024
7882cbe
status codes
stephanegigandet Feb 29, 2024
4e0c586
update tests
stephanegigandet Feb 29, 2024
ec51531
check status code on setup test cases
stephanegigandet Mar 6, 2024
7161db6
check status code on setup test cases
stephanegigandet Mar 6, 2024
5a87136
Merge branch 'main' into revert-product
alexgarel Mar 6, 2024
dff17df
fix test
stephanegigandet Mar 6, 2024
80ff0ce
Merge branch 'main' into revert-product
stephanegigandet Mar 6, 2024
a9dfd7f
Merge branch 'main' into revert-product
stephanegigandet Mar 8, 2024
38ea0c3
update test
stephanegigandet Mar 8, 2024
b925bfa
update tests
stephanegigandet Mar 8, 2024
6ad1fb2
delete status_code
stephanegigandet Mar 8, 2024
02b9a08
update tests results
stephanegigandet Mar 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cgi/product_jqm_multilingual.pl
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ =head1 DESCRIPTION

$code = normalize_code($code);

if ($code !~ /^\d{4,24}$/) {
if (not is_valid_code($code)) {

$log->info("invalid code", {code => $code, original_code => $original_code}) if $log->is_info();
$response{status} = 0;
Expand Down
29 changes: 15 additions & 14 deletions cgi/product_multilingual.pl
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ ($product_ref)
# Response structure to keep track of warnings and errors
# Note: currently some warnings and errors are added,
# but we do not yet do anything with them
my $response_ref = get_initialized_response();
my $response_ref = ProductOpener::API::get_initialized_response();

my $type = single_param('type') || 'search_or_add';
my $action = single_param('action') || 'display';
Expand Down Expand Up @@ -220,7 +220,7 @@ ($product_ref)
if ((not defined $code) or ($code eq "")) {
$code = process_search_image_form(\$filename);
}
elsif ($code !~ /^\d{4,24}$/) {
elsif (not is_valid_code($code)) {
display_error_and_exit($Lang{invalid_barcode}{$lang}, 403);
}

Expand Down Expand Up @@ -315,7 +315,7 @@ ($product_ref)
if ((not defined $code) or ($code eq '')) {
display_error_and_exit($Lang{missing_barcode}{$lang}, 403);
}
elsif ($code !~ /^\d{4,24}$/) {
elsif (not is_valid_code($code)) {
display_error_and_exit($Lang{invalid_barcode}{$lang}, 403);
}
else {
Expand Down Expand Up @@ -777,19 +777,20 @@ ($product_ref, $field, $language)
;

$scripts .= <<HTML
<script type="text/javascript" src="/js/dist/webcomponentsjs/webcomponents-loader.js"></script>
<script type="text/javascript" src="/js/dist/cropper.js"></script>
<script type="text/javascript" src="/js/dist/jquery-cropper.js"></script>
<script type="text/javascript" src="/js/dist/jquery.form.js"></script>
<script type="text/javascript" src="/js/dist/tagify.min.js"></script>
<script type="text/javascript" src="/js/dist/jquery.iframe-transport.js"></script>
<script type="text/javascript" src="/js/dist/jquery.fileupload.js"></script>
<script type="text/javascript" src="/js/dist/load-image.all.min.js"></script>
<script type="text/javascript" src="/js/dist/canvas-to-blob.js"></script>
<script type="text/javascript" src="$static_subdomain/js/dist/webcomponentsjs/webcomponents-loader.js"></script>
<script type="text/javascript" src="$static_subdomain/js/dist/cropper.js"></script>
<script type="text/javascript" src="$static_subdomain/js/dist/jquery-cropper.js"></script>
<script type="text/javascript" src="$static_subdomain/js/dist/jquery.form.js"></script>
<script type="text/javascript" src="$static_subdomain/js/dist/tagify.min.js"></script>
<script type="text/javascript" src="$static_subdomain/js/dist/jquery.iframe-transport.js"></script>
<script type="text/javascript" src="$static_subdomain/js/dist/jquery.fileupload.js"></script>
<script type="text/javascript" src="$static_subdomain/js/dist/load-image.all.min.js"></script>
<script type="text/javascript" src="$static_subdomain/js/dist/canvas-to-blob.js"></script>
<script type="text/javascript">
var admin = $moderator;
</script>
<script type="text/javascript" src="/js/dist/product-multilingual.js?v=$file_timestamps{'js/dist/product-multilingual.js'}"></script>
<script type="text/javascript" src="$static_subdomain/js/dist/product-multilingual.js?v=$file_timestamps{'js/dist/product-multilingual.js'}"></script>
<script type="text/javascript" src="$static_subdomain/js/dist/product-history.js"></script>

HTML
;
Expand Down Expand Up @@ -1479,7 +1480,7 @@ ($product_ref, $field, $language)
$template_data_ref_display->{param_fields} = single_param("fields");
$template_data_ref_display->{type} = $type;
$template_data_ref_display->{code} = $code;
$template_data_ref_display->{display_product_history} = display_product_history($code, $product_ref);
$template_data_ref_display->{display_product_history} = display_product_history($request_ref, $code, $product_ref);
$template_data_ref_display->{product} = $product_ref;

process_template('web/pages/product_edit/product_edit_form_display.tt.html', $template_data_ref_display, \$html)
Expand Down
33 changes: 33 additions & 0 deletions docs/api/ref/api-v3.yml
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,39 @@ paths:

Categories:
- Packaging stats for a category
/api/v3/product_revert:
parameters: []
post:
summary: Revert a product to a previous revision
tags: []
responses:
'200':
description: OK
content:
application/json:
schema:
allOf:
- $ref: ./responses/response-status/response_status.yaml
operationId: post-api-v3-product_revert
description: |-
For moderators only, revert a product to a previous revision.
requestBody:
content:
application/json:
schema:
allOf:
- $ref: ./requestBodies/fields_tags_lc.yaml
- type: object
properties:
code:
type: string
description: Barcode of the product
rev:
type: integer
description: Revision number to revert to
description: |
The code and rev fields are mandatory.
parameters: []
components:
parameters:
cc:
Expand Down
2 changes: 1 addition & 1 deletion gulpfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const sass = gulpSass(sassLib);

const jsSrc = [
"./html/js/display*.js",
"./html/js/product-multilingual.js",
"./html/js/product-*.js",
"./html/js/search.js",
"./html/js/hc-sticky.js",
"./html/js/stikelem.js",
Expand Down
60 changes: 60 additions & 0 deletions html/js/product-history.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// This file is part of Product Opener.
//
// Product Opener
// Copyright (C) 2011-2024 Association Open Food Facts
// Contact: [email protected]
// Address: 21 rue des Iles, 94100 Saint-Maur des Fossés, France
//
// Product Opener is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

/*global revert_confirm_message*/
/*exported activate_product_revert_buttons_in_history*/

function activate_product_revert_buttons_in_history () {
$('#history_list a.product_revert_button').on('click', function() {
const code = $(this).data('code');
const rev = $(this).data('rev');
// using confirm, could be replaced with some JS dialog / modal
const confirm = window.confirm(revert_confirm_message); // eslint-disable-line no-alert
if (confirm) {
$.ajax({
url: '/api/v3/product_revert',
type: 'POST',
contentType: "application/json; charset=utf-8",
dataType: "json",
data: JSON.stringify({
code: code,
rev: rev,
fields: "rev"
// we don't pass cc and lc, as they will get the right default value from the subdomain
}),
success: function(data) {
let message = data.status;
if (data.status === 'success') {
message = message + ' - <a href="/product/' + code +'">' + data.result.lc_name + '</a>';
}
else {
message = message + ' - ' + data.result.lc_name;
}
$('#revert_result_' + rev).html(message);
}
});
}
});
}

$(function() {
activate_product_revert_buttons_in_history();
});

92 changes: 89 additions & 3 deletions lib/ProductOpener/API.pm
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ BEGIN {
&decode_json_request_body
&normalize_requested_code
&customize_response_for_product
&check_user_permission
); # symbols to export on request
%EXPORT_TAGS = (all => [@EXPORT_OK]);
}
Expand All @@ -71,9 +72,11 @@ use ProductOpener::Attributes qw/:all/;
use ProductOpener::KnowledgePanels qw/:all/;
use ProductOpener::Ecoscore qw/localize_ecoscore/;
use ProductOpener::Packaging qw/:all/;
use ProductOpener::Permissions qw/:all/;

use ProductOpener::APIProductRead qw/:all/;
use ProductOpener::APIProductWrite qw/:all/;
use ProductOpener::APIProductRevert qw/:all/;
use ProductOpener::APIProductServices qw/:all/;
use ProductOpener::APITagRead qw/:all/;
use ProductOpener::APITaxonomySuggestions qw/:all/;
Expand Down Expand Up @@ -106,8 +109,29 @@ sub add_warning ($response_ref, $warning_ref) {
return;
}

sub add_error ($response_ref, $error_ref) {
=head2 add_error ($response_ref, $error_ref, $status_code = 400)

Add an error to the response object.

=head3 Parameters

=head4 $response_ref (input)

Reference to the response object.

=head4 $error_ref (input)

Reference to the error object.

=head4 $status_code (input)

HTTP status code to return in the response, defaults to 400 bad request.

=cut

sub add_error ($response_ref, $error_ref, $status_code = 400) {
push @{$response_ref->{errors}}, $error_ref;
$response_ref->{status_code} = $status_code;
return;
}

Expand All @@ -124,7 +148,8 @@ sub add_invalid_method_error ($response_ref, $request_ref) {
api_action => $request_ref->{api_action},
},
impact => {id => "failure"},
}
},
405
);
return;
}
Expand Down Expand Up @@ -318,7 +343,8 @@ Reference to the customized product object.

sub send_api_response ($request_ref) {

my $status_code = $request_ref->{status_code} || "200";
my $status_code = $request_ref->{api_response}{status_code} || $request_ref->{status_code} || "200";
delete $request_ref->{api_response}{status_code};

my $json = JSON::PP->new->allow_nonref->canonical->utf8->encode($request_ref->{api_response});

Expand Down Expand Up @@ -389,6 +415,17 @@ sub process_api_request ($request_ref) {
add_invalid_method_error($response_ref, $request_ref);
}
}
# Product revert
elsif ($request_ref->{api_action} eq "product_revert") {

# Check that the method is POST (GET may be dangerous: it would allow to revert a product by just clicking or loading a link)
if ($request_ref->{api_method} eq "POST") {
revert_product_api($request_ref);
}
else {
add_invalid_method_error($response_ref, $request_ref);
}
}
# Product services
elsif ($request_ref->{api_action} eq "product_services") {

Expand Down Expand Up @@ -833,4 +870,53 @@ sub customize_response_for_product ($request_ref, $product_ref, $fields_comma_se
return $customized_product_ref;
}

=head2 check_user_permission ($request_ref, $response_ref, $permission)

Check the user has a specific permission, before processing an API request.
If the user does not have the permission, an error is added to the response.

=head3 Parameters

=head4 $request_ref (input)

Reference to the request object.

=head4 $response_ref (output)

Reference to the response object.

=head4 $permission (input)

Permission to check.

=head3 Return value

1 if the user does not have the permission, 0 otherwise.

=cut

sub check_user_permission ($request_ref, $response_ref, $permission) {

# We will return an error equal to 1 if the user does not have the permission
my $error = 0;

# Check if the user has permission
if (not has_permission($request_ref, $permission)) {
$error = 1;
$log->error("check_user_permission - user does not have permission", {permission => $permission})
if $log->is_error();
add_error(
$response_ref,
{
message => {id => "no_permission"},
field => {id => "permission", value => $permission},
impact => {id => "failure"},
},
403
);
}

return $error;
}

1;
7 changes: 2 additions & 5 deletions lib/ProductOpener/APIProductRead.pm
Original file line number Diff line number Diff line change
Expand Up @@ -124,17 +124,14 @@ sub read_product_api ($request_ref) {

# Return an error if we could not find a product

if ($request_ref->{api_version} >= 1) {
$request_ref->{status_code} = 404;
}

add_error(
$response_ref,
{
message => {id => "product_not_found"},
field => {id => "code", value => $code},
impact => {id => "failure"},
}
},
404
);
$response_ref->{result} = {id => "product_not_found"};
}
Expand Down
Loading
Loading