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: 617 - new "save packagings V3" feature #640

Merged
merged 2 commits into from
Dec 16, 2022
Merged
Changes from all commits
Commits
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
10 changes: 9 additions & 1 deletion lib/model/Product.dart
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ import 'package:openfoodfacts/model/Attribute.dart';
import 'package:openfoodfacts/model/AttributeGroup.dart';
import 'package:openfoodfacts/model/KnowledgePanels.dart';
import 'package:openfoodfacts/model/ProductImage.dart';
import 'package:openfoodfacts/model/ProductPackaging.dart';
import 'package:openfoodfacts/utils/JsonHelper.dart';
import 'package:openfoodfacts/utils/LanguageHelper.dart';
import 'package:openfoodfacts/utils/ProductFields.dart';
@@ -311,8 +312,14 @@ class Product extends JsonObject {
includeIfNull: false)
Map<OpenFoodFactsLanguage, List<String>>? labelsTagsInLanguages;

// TODO: deprecated from 2022-12-16; remove when old enough
@Deprecated('User packagingS instead')
Comment on lines +315 to +316
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// TODO: deprecated from 2022-12-16; remove when old enough
@Deprecated('User packagingS instead')
// TODO: deprecated from 2022-12-16; remove when old enough
@Deprecated('Use [packagings] instead')

@JsonKey(name: 'packaging', includeIfNull: false)
String? packaging;

@JsonKey(name: 'packagings', includeIfNull: false)
List<ProductPackaging>? packagings;

@JsonKey(name: 'packaging_tags', includeIfNull: false)
List<String>? packagingTags;
@JsonKey(
@@ -468,7 +475,8 @@ class Product extends JsonObject {
this.labels,
this.labelsTags,
this.labelsTagsInLanguages,
this.packaging,
// TODO: deprecated from 2022-12-16; remove when old enough
@Deprecated('Use packagingS field instead') this.packaging,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@Deprecated('Use packagingS field instead') this.packaging,
@Deprecated('Use [packagings] field instead') this.packaging,

Feel free to leave it with the capital S

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The square bracket syntax is unfortunately not detected in strings, only in /// comments.

this.packagingTags,
this.miscTags,
this.statesTags,
4 changes: 4 additions & 0 deletions lib/model/Product.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 38 additions & 0 deletions lib/model/ProductPackaging.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:openfoodfacts/model/LocalizedTag.dart';
import '../interface/JsonObject.dart';

part 'ProductPackaging.g.dart';

/// Packaging component for a product, e.g. recyclable bottle made of glass.
@JsonSerializable()
class ProductPackaging extends JsonObject {
@JsonKey(includeIfNull: false)
LocalizedTag? shape;

@JsonKey(includeIfNull: false)
LocalizedTag? material;

@JsonKey(includeIfNull: false)
LocalizedTag? recycling;

@JsonKey(
name: 'number_of_units',
includeIfNull: false,
fromJson: JsonObject.parseInt,
)
int? numberOfUnits;

ProductPackaging();

factory ProductPackaging.fromJson(dynamic json) =>
_$ProductPackagingFromJson(json);

@override
Map<String, dynamic> toJson() => _$ProductPackagingToJson(this);

Map<String, String> toServerData() => JsonObject.toDataStatic(toJson());

@override
String toString() => toServerData().toString();
}
36 changes: 36 additions & 0 deletions lib/model/ProductPackaging.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 43 additions & 0 deletions lib/openfoodfacts.dart
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ import 'dart:developer';

import 'package:http/http.dart';
import 'package:openfoodfacts/interface/JsonObject.dart';
import 'package:openfoodfacts/model/ProductPackaging.dart';
import 'package:openfoodfacts/model/ProductResultV3.dart';
import 'package:openfoodfacts/model/KnowledgePanels.dart';
import 'package:openfoodfacts/model/LoginStatus.dart';
@@ -147,6 +148,48 @@ class OpenFoodAPIClient {
return Status.fromApiResponse(response.body);
}

/// Temporary: saves product packagings V3 style.
///
/// For the moment that's the only field supported in WRITE by API V3.
/// Long term target is of course more something like [saveProduct].
static Future<ProductResultV3> temporarySaveProductV3(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
static Future<ProductResultV3> temporarySaveProductV3(
static Future<ProductResultV3> temporarySaveProductPackagingsV3(

Just a suggestion

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could make sense but in the long run I expect more fields to match V3 (e.g. one by month) : I'm not going to create a different method for each.

final User user,
final String barcode, {
final List<ProductPackaging>? packagings,
final QueryType? queryType,
final OpenFoodFactsCountry? country,
final OpenFoodFactsLanguage? language,
}) async {
final Map<String, dynamic> parameterMap = <String, dynamic>{};
parameterMap.addAll(user.toData());
if (packagings == null) {
// For the moment it's the only purpose of this method: saving packagings.
throw Exception('packagings cannot be null');
}
parameterMap['product'] = {};
parameterMap['product']['packagings'] = packagings;
if (language != null) {
parameterMap['lc'] = language.offTag;
parameterMap['tags_lc'] = language.offTag;
}
if (country != null) {
parameterMap['cc'] = country.offTag;
}

var productUri = UriHelper.getPatchUri(
path: '/api/v3/product/$barcode',
queryType: queryType,
);

final Response response = await HttpHelper().doPatchRequest(
productUri,
parameterMap,
user,
queryType: queryType,
);
return ProductResultV3.fromJson(jsonDecode(response.body));
}

/// Send one image to the server.
/// The image will be added to the product specified in the SendImage
/// Returns a Status object as result.
2 changes: 2 additions & 0 deletions lib/utils/AbstractQueryConfiguration.dart
Original file line number Diff line number Diff line change
@@ -75,6 +75,8 @@ abstract class AbstractQueryConfiguration {
if (queryLanguages.isNotEmpty) {
result.putIfAbsent(
'lc', () => queryLanguages.map((e) => e.offTag).join(','));
// the result looks like crap if we put several languages
result.putIfAbsent('tags_lc', () => queryLanguages.first.offTag);
}

final String? countryCode = computeCountryCode();
46 changes: 32 additions & 14 deletions lib/utils/HttpHelper.dart
Original file line number Diff line number Diff line change
@@ -29,10 +29,10 @@ class HttpHelper {
static const String FROM = 'anonymous';

/// Adds user agent data to parameters, for statistics purpose
static Map<String, String>? addUserAgentParameters(
Map<String, String>? map,
static Map<String, dynamic>? addUserAgentParameters(
Map<String, dynamic>? map,
) {
map ??= <String, String>{};
map ??= <String, dynamic>{};
Comment on lines +32 to +35
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why dynamic

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the new method doPatchRequest the input can be Map<String, dynamic>, and not Map<String, String>.
More specifically in that case:

    parameterMap['product']['packagings'] = packagings;

if (OpenFoodAPIConfiguration.userAgent?.name != null) {
map['app_name'] = OpenFoodAPIConfiguration.userAgent!.name!;
}
@@ -70,9 +70,7 @@ class HttpHelper {
headers: _buildHeaders(
user: user,
isTestModeActive:
OpenFoodAPIConfiguration.getQueryType(queryType) == QueryType.PROD
? false
: true,
OpenFoodAPIConfiguration.getQueryType(queryType) != QueryType.PROD,
),
);

@@ -105,16 +103,37 @@ class HttpHelper {
headers: _buildHeaders(
user: user,
isTestModeActive:
OpenFoodAPIConfiguration.getQueryType(queryType) == QueryType.PROD
? false
: true,
OpenFoodAPIConfiguration.getQueryType(queryType) != QueryType.PROD,
addCredentialsToHeader: addCredentialsToHeader,
),
body: addUserAgentParameters(body),
);
return response;
}

static const String userInfoForTest = 'off:off';

/// Send a http PATCH request to the specified uri.
///
/// The data / body of the request has to be provided as map. (key, value)
/// The result of the request will be returned as string.
Future<http.Response> doPatchRequest(
final Uri uri,
final Map<String, dynamic> body,
final User? user, {
final QueryType? queryType,
}) async =>
http.patch(
uri,
headers: _buildHeaders(
user: user,
isTestModeActive: OpenFoodAPIConfiguration.getQueryType(queryType) !=
QueryType.PROD,
addCredentialsToHeader: false,
),
body: jsonEncode(addUserAgentParameters(body)),
);

/// Send a multipart post request to the specified uri.
/// The data / body of the request has to be provided as map. (key, value)
/// The files to send have to be provided as map containing the source file uri.
@@ -132,14 +151,13 @@ class HttpHelper {
_buildHeaders(
user: user,
isTestModeActive:
OpenFoodAPIConfiguration.getQueryType(queryType) == QueryType.PROD
? false
: true,
OpenFoodAPIConfiguration.getQueryType(queryType) != QueryType.PROD,
) as Map<String, String>,
);

request.headers.addAll({'Content-Type': 'multipart/form-data'});
request.fields.addAll(addUserAgentParameters(body)!);
addUserAgentParameters(body);
request.fields.addAll(body);
if (user != null) {
request.fields.addAll(user.toData());
}
@@ -192,7 +210,7 @@ class HttpHelper {
});

if (isTestModeActive && !addCredentialsToHeader) {
var token = 'Basic ${base64Encode(utf8.encode('off:off'))}';
var token = 'Basic ${base64Encode(utf8.encode(userInfoForTest))}';
headers.addAll({'Authorization': token});
}

6 changes: 6 additions & 0 deletions lib/utils/ProductFields.dart
Original file line number Diff line number Diff line change
@@ -51,7 +51,10 @@ enum ProductField implements OffTagged {
LABELS(offTag: 'labels'),
LABELS_TAGS(offTag: 'labels_tags'),
LABELS_TAGS_IN_LANGUAGES(offTag: 'labels_tags_'),
// TODO: deprecated from 2022-12-16; remove when old enough
@Deprecated('Use packagingS field instead')
PACKAGING(offTag: 'packaging'),
PACKAGINGS(offTag: 'packagings'),
PACKAGING_TAGS(offTag: 'packaging_tags'),
PACKAGING_TEXT_IN_LANGUAGES(offTag: 'packaging_text_'),
PACKAGING_TEXT_ALL_LANGUAGES(offTag: 'packaging_text_languages'),
@@ -85,6 +88,9 @@ enum ProductField implements OffTagged {
ORIGINS(offTag: 'origins'),
NOVA_GROUP(offTag: 'nova_group'),
WEBSITE(offTag: 'link'),

/// All data as RAW from the server. E.g. packagings are only Strings there.
RAW(offTag: 'raw'),
ALL(offTag: '');

const ProductField({
8 changes: 5 additions & 3 deletions lib/utils/ProductSearchQueryConfiguration.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import 'package:openfoodfacts/interface/Parameter.dart';
import 'package:openfoodfacts/openfoodfacts.dart';
import 'package:openfoodfacts/utils/AbstractQueryConfiguration.dart';
import 'package:openfoodfacts/utils/CountryHelper.dart';
import 'package:openfoodfacts/utils/LanguageHelper.dart';
import 'package:openfoodfacts/utils/ProductFields.dart';

/// Query Configuration for search parameters
class ProductSearchQueryConfiguration extends AbstractQueryConfiguration {
@@ -14,6 +12,7 @@ class ProductSearchQueryConfiguration extends AbstractQueryConfiguration {
final OpenFoodFactsCountry? country,
final List<ProductField>? fields,
required List<Parameter> parametersList,
this.version = ProductQueryVersion.v3,
}) : super(
language: language,
languages: languages,
@@ -22,6 +21,8 @@ class ProductSearchQueryConfiguration extends AbstractQueryConfiguration {
additionalParameters: parametersList,
);

final ProductQueryVersion version;

List<String> getFieldsKeys() {
List<String> result = [];

@@ -38,6 +39,7 @@ class ProductSearchQueryConfiguration extends AbstractQueryConfiguration {
result.putIfAbsent('search_terms', () => '');
// explicit json output
result.putIfAbsent('json', () => '1');
result.putIfAbsent('api_version', () => '${version.version}');
return result;
}

16 changes: 16 additions & 0 deletions lib/utils/UriHelper.dart
Original file line number Diff line number Diff line change
@@ -18,6 +18,7 @@ class UriHelper {
final Map<String, String>? queryParameters,
final QueryType? queryType,
final bool addUserAgentParameters = true,
final String? userInfo,
}) =>
Uri(
scheme: OpenFoodAPIConfiguration.uriScheme,
@@ -28,6 +29,7 @@ class UriHelper {
queryParameters: addUserAgentParameters
? HttpHelper.addUserAgentParameters(queryParameters)
: queryParameters,
userInfo: userInfo,
);

static Uri getPostUri({
@@ -36,6 +38,20 @@ class UriHelper {
}) =>
getUri(path: path, queryType: queryType, addUserAgentParameters: false);

static Uri getPatchUri({
required final String path,
final QueryType? queryType,
}) =>
getUri(
path: path,
queryType: queryType,
addUserAgentParameters: false,
userInfo:
OpenFoodAPIConfiguration.getQueryType(queryType) == QueryType.PROD
? null
: HttpHelper.userInfoForTest,
);

///Returns a OFF-Robotoff uri with the in the [OpenFoodAPIConfiguration] specified settings
static Uri getRobotoffUri({
required final String path,
Loading