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 8 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
4 changes: 2 additions & 2 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 @@ -1479,7 +1479,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 @@ -349,6 +349,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
54 changes: 54 additions & 0 deletions html/js/product-history.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// 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.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"
}),
alexgarel marked this conversation as resolved.
Show resolved Hide resolved
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);
}
});
}
});
}
62 changes: 62 additions & 0 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 @@ -389,6 +392,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 +847,52 @@ 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"},
}
);
}

return $error;
}

1;
181 changes: 181 additions & 0 deletions lib/ProductOpener/APIProductRevert.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
# This file is part of Product Opener.
#
# Product Opener
# Copyright (C) 2011-2023 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 <http://www.gnu.org/licenses/>.

=head1 NAME

ProductOpener::APIProductRevert - implementation of API to revert a product to a specific revision
=head1 DESCRIPTION

=cut

package ProductOpener::APIProductRevert;

use ProductOpener::PerlStandards;
use Exporter qw< import >;

use Log::Any qw($log);

BEGIN {
use vars qw(@ISA @EXPORT_OK %EXPORT_TAGS);
@EXPORT_OK = qw(
&revert_product_api
); # symbols to export on request
%EXPORT_TAGS = (all => [@EXPORT_OK]);
}

use vars @EXPORT_OK;

use ProductOpener::Config qw/:all/;
use ProductOpener::Display qw/:all/;
use ProductOpener::Users qw/:all/;
use ProductOpener::Lang qw/:all/;
use ProductOpener::Products qw/:all/;
use ProductOpener::API qw/:all/;
use ProductOpener::Text qw/:all/;
use ProductOpener::Tags qw/:all/;
use ProductOpener::Mail qw/:all/;

use Encode;

=head2 revert_product_api()

Process API v3 requests to revert a product to a specific revision.

=cut

sub revert_product_api ($request_ref) {

$log->debug("revert_product_api - start", {request => $request_ref}) if $log->is_debug();

my $response_ref = $request_ref->{api_response};

# Set the result field for the API response, will be updated later if the product is successfully reverted
$response_ref->{result} = {id => "product_not_reverted"};

my $error = 0;

my $request_body_ref = $request_ref->{body_json};

# Check we have all needed input parameters
foreach my $field ("code", "rev") {
if (not defined $request_body_ref->{$field}) {
$log->error("revert_product_api - missing input $field", {request => $request_ref})
if $log->is_error();
add_error(
$response_ref,
{
message => {id => "missing_field"},
field => {id => $field},
impact => {id => "failure"},
}
);
$error = 1;
}
}

# Check that the user has permission (is an admin or a moderator, or we are on the producers platform)

$error += check_user_permission($request_ref, $response_ref, "product_revert");

if (not $error) {

# Load the product
my ($code, $ai_data_string) = &normalize_requested_code($request_body_ref->{code}, $response_ref);
my $rev = $request_body_ref->{rev};

# Check if the code is valid
if ($code !~ /^\d{4,24}$/) {
alexgarel marked this conversation as resolved.
Show resolved Hide resolved

$log->info("invalid code", {code => $code, original_code => $request_body_ref->{code}}) if $log->is_info();
add_error(
$response_ref,
{
message => {id => "invalid_code"},
field => {id => "code", value => $request_body_ref->{code}},
impact => {id => "failure"},
}
);
$error = 1;
}
else {
my $product_id = product_id_for_owner($Owner_id, $code);
my $product_ref = retrieve_product($product_id);

if (not defined $product_ref) {
$log->info("product not found", {code => $code}) if $log->is_info();
add_error(
$response_ref,
{
message => {id => "product_not_found"},
field => {id => "code", value => $code},
impact => {id => "failure"},
}
);
$error = 1;
}
else {
# Check if the revision exists
my $revision_ref = retrieve_product_rev($product_id, $rev);

if (not defined $revision_ref) {
$log->info("revision not found", {code => $code, rev => $rev}) if $log->is_info();
add_error(
$response_ref,
{
message => {id => "revision_not_found"},
field => {id => "rev", value => $rev},
impact => {id => "failure"},
}
);
$error = 1;
}
else {
# Save the product revision as a new revision
my $comment = "API v3 - revert to revision $rev";
my $user_comment = request_param($request_ref, "comment");
if ((defined $user_comment) and ($user_comment !~ /^\s*$/)) {
$comment .= " - $user_comment";
}
store_product($User_id, $revision_ref, $comment);

# Set the result field for the API response
$response_ref->{result} = {id => "product_reverted"};

# Select / compute only the fields requested by the caller, default to all fields
$response_ref->{product} = customize_response_for_product($request_ref, $revision_ref,
request_param($request_ref, 'fields') || "all");

# Send an email to admins - TODO: replace with an event or something once we have a better system in place
my $email_subject = "Product $code reverted to revision $rev";
my $email_body = "Product $code has been reverted to revision $rev by user $User_id\n";
$email_body .= "Comment: $comment\n";
$email_body .= "Product: " . product_name_brand_quantity($revision_ref) . "\n";
send_email_to_admin($email_subject, $email_body);
}
}
}
}

$log->debug("revert_product_api - stop", {request => $request_ref}) if $log->is_debug();

return;
}

1;
Loading
Loading