From baa1258c71e38dfa194b568c5a20c70ee0dc6c6a Mon Sep 17 00:00:00 2001 From: monsieurtanuki Date: Fri, 17 May 2024 14:22:56 +0200 Subject: [PATCH 1/5] feat: 831 - prices methods getProofs, getProof and getUsers + fixed uploadProof (#926) * feat: 831 - prices methods getProofs, getProof and getUsers + fixed uploadProof New files: * `flavor.dart`: Flavor is used to refer to a specific Open*Facts project. * `get_price_count_parameters_helper.dart`: Helper class for API query parameters with price count filters. * `get_proofs_order.dart`: Field for the "order by" clause of "get proofs". * `get_proofs_parameters.dart`: Parameters for the "get proofs" API query. * `get_proofs_result.dart`: List of proof objects returned by the "get proofs" method. * `get_proofs_result.g.dart`: generated * `get_users_order.dart`: Field for the "order by" clause of "get users". * `get_users_parameters.dart`: Parameters for the "get users" API query. * `get_users_result.dart`: List of price user objects returned by the "get users" method. * `get_users_result.g.dart`: generated * `price_user.dart`: Price user object. * `price_user.g.dart`: generated Impacted files: * `api_prices_test.dart`: added tests for new methods `getProofs`, `getProof` and `getUsers`; fixed test of method `uploadProof` * `badge_base.g.dart`: unrelated file generation change * `events_base.g.dart`: unrelated file generation change * `get_locations_parameters.dart`: refactoring * `get_locations_result.g.dart`: unrelated file generation change * `get_parameters_helper.dart`: refactoring * `get_prices_parameters.dart`: refactoring * `get_prices_result.g.dart`: unrelated file generation change * `key_stats.g.dart`: unrelated file generation change * `knowledge_panel_element.g.dart`: unrelated file generation change * `leaderboard_entry.g.dart`: unrelated file generation change * `location.dart`: new field `priceCount` * `location.g.dart`: generated * `ocr_ingredients_result.g.dart`: unrelated file generation change * `ocr_packaging_result.g.dart`: unrelated file generation change * `old_product_result.g.dart`: unrelated file generation change * `open_prices_api_client.dart`: new methods `getProofs`, `getProof` and `getUsers`; fixed method `uploadProof` * `openfoodfacts.dart`: exported the new files * `price.dart`: new fields `proof`, `location` and `product` * `price.g.dart`: generated * `price_product.dart`: new fields * `price_product.g.dart`: generated * `product.g.dart`: unrelated file generation change * `product_stats.g.dart`: unrelated file generation change * `product_tag.g.dart`: unrelated file generation change * `proof.dart`: removed old field (that provoked test failure anyway) * `proof.g.dart`: generated * `pubspec.yaml`: upgraded packages to more recent versions - while not too recent either * `spelling_corrections.g.dart`: unrelated file generation change * Unrelated unit test fixes --- lib/openfoodfacts.dart | 9 + lib/src/model/badge_base.g.dart | 2 +- lib/src/model/events_base.g.dart | 2 +- lib/src/model/key_stats.g.dart | 4 +- lib/src/model/knowledge_panel_element.g.dart | 4 +- lib/src/model/leaderboard_entry.g.dart | 2 +- lib/src/model/ocr_ingredients_result.g.dart | 2 +- lib/src/model/ocr_packaging_result.g.dart | 2 +- lib/src/model/old_product_result.g.dart | 2 +- lib/src/model/product.g.dart | 5 +- lib/src/model/product_stats.g.dart | 4 +- lib/src/model/product_tag.g.dart | 2 +- lib/src/model/spelling_corrections.g.dart | 4 +- lib/src/open_prices_api_client.dart | 89 ++++++++- lib/src/prices/flavor.dart | 36 ++++ lib/src/prices/get_locations_parameters.dart | 22 +-- lib/src/prices/get_locations_result.g.dart | 8 +- lib/src/prices/get_parameters_helper.dart | 13 +- .../get_price_count_parameters_helper.dart | 20 ++ lib/src/prices/get_prices_parameters.dart | 13 +- lib/src/prices/get_prices_result.g.dart | 8 +- lib/src/prices/get_proofs_order.dart | 11 ++ lib/src/prices/get_proofs_parameters.dart | 21 ++ lib/src/prices/get_proofs_result.dart | 33 ++++ lib/src/prices/get_proofs_result.g.dart | 26 +++ lib/src/prices/get_users_order.dart | 12 ++ lib/src/prices/get_users_parameters.dart | 8 + lib/src/prices/get_users_result.dart | 33 ++++ lib/src/prices/get_users_result.g.dart | 26 +++ lib/src/prices/location.dart | 4 + lib/src/prices/location.g.dart | 6 +- lib/src/prices/price.dart | 15 ++ lib/src/prices/price.g.dart | 22 ++- lib/src/prices/price_product.dart | 38 +++- lib/src/prices/price_product.g.dart | 40 +++- lib/src/prices/price_user.dart | 30 +++ lib/src/prices/price_user.g.dart | 18 ++ lib/src/prices/proof.dart | 8 +- lib/src/prices/proof.g.dart | 6 +- pubspec.yaml | 14 +- test/api_events_test.dart | 2 +- test/api_get_product_test.dart | 19 -- test/api_get_robotoff_test.dart | 6 +- test/api_get_save_product_test.dart | 4 +- test/api_get_suggestions_test.dart | 2 +- .../api_get_taxonomy_origins_server_test.dart | 5 +- test/api_not_food_get_product_test.dart | 2 +- test/api_prices_test.dart | 187 +++++++++++++++++- 48 files changed, 728 insertions(+), 123 deletions(-) create mode 100644 lib/src/prices/flavor.dart create mode 100644 lib/src/prices/get_price_count_parameters_helper.dart create mode 100644 lib/src/prices/get_proofs_order.dart create mode 100644 lib/src/prices/get_proofs_parameters.dart create mode 100644 lib/src/prices/get_proofs_result.dart create mode 100644 lib/src/prices/get_proofs_result.g.dart create mode 100644 lib/src/prices/get_users_order.dart create mode 100644 lib/src/prices/get_users_parameters.dart create mode 100644 lib/src/prices/get_users_result.dart create mode 100644 lib/src/prices/get_users_result.g.dart create mode 100644 lib/src/prices/price_user.dart create mode 100644 lib/src/prices/price_user.g.dart diff --git a/lib/openfoodfacts.dart b/lib/openfoodfacts.dart index 89aa078813..d79b99f602 100644 --- a/lib/openfoodfacts.dart +++ b/lib/openfoodfacts.dart @@ -89,6 +89,7 @@ export 'src/personalized_search/preference_importance.dart'; export 'src/personalized_search/product_preferences_manager.dart'; export 'src/personalized_search/product_preferences_selection.dart'; export 'src/prices/currency.dart'; +export 'src/prices/flavor.dart'; export 'src/prices/get_locations_order.dart'; export 'src/prices/get_locations_parameters.dart'; export 'src/prices/get_locations_result.dart'; @@ -96,6 +97,13 @@ export 'src/prices/get_locations_result.dart'; export 'src/prices/get_prices_order.dart'; export 'src/prices/get_prices_parameters.dart'; export 'src/prices/get_prices_result.dart'; +export 'src/prices/get_price_count_parameters_helper.dart'; +export 'src/prices/get_proofs_order.dart'; +export 'src/prices/get_proofs_parameters.dart'; +export 'src/prices/get_proofs_result.dart'; +export 'src/prices/get_users_order.dart'; +export 'src/prices/get_users_parameters.dart'; +export 'src/prices/get_users_result.dart'; export 'src/prices/location.dart'; export 'src/prices/location_osm_type.dart'; export 'src/prices/maybe_error.dart'; @@ -103,6 +111,7 @@ export 'src/prices/order_by.dart'; export 'src/prices/price.dart'; export 'src/prices/price_per.dart'; export 'src/prices/price_product.dart'; +export 'src/prices/price_user.dart'; export 'src/prices/proof.dart'; export 'src/prices/proof_type.dart'; export 'src/prices/session.dart'; diff --git a/lib/src/model/badge_base.g.dart b/lib/src/model/badge_base.g.dart index 2bda7a6bee..c4d0a54ce1 100644 --- a/lib/src/model/badge_base.g.dart +++ b/lib/src/model/badge_base.g.dart @@ -8,7 +8,7 @@ part of 'badge_base.dart'; BadgeBase _$BadgeBaseFromJson(Map json) => BadgeBase( badgeName: json['badge_name'] as String, - level: json['level'] as int, + level: (json['level'] as num).toInt(), userId: json['user_id'] as String?, ); diff --git a/lib/src/model/events_base.g.dart b/lib/src/model/events_base.g.dart index d9da3532c9..3dc6cbd0d3 100644 --- a/lib/src/model/events_base.g.dart +++ b/lib/src/model/events_base.g.dart @@ -11,7 +11,7 @@ EventsBase _$EventsBaseFromJson(Map json) => EventsBase( timestamp: JsonHelper.nullableStringTimestampToDate(json['timestamp']), userId: json['user_id'] as String?, barcode: json['barcode'] as String?, - points: json['points'] as int?, + points: (json['points'] as num?)?.toInt(), ); Map _$EventsBaseToJson(EventsBase instance) => diff --git a/lib/src/model/key_stats.g.dart b/lib/src/model/key_stats.g.dart index 60fc8fa80f..71086b918a 100644 --- a/lib/src/model/key_stats.g.dart +++ b/lib/src/model/key_stats.g.dart @@ -8,8 +8,8 @@ part of 'key_stats.dart'; KeyStats _$KeyStatsFromJson(Map json) => KeyStats( key: json['k'] as String, - count: json['count'] as int, - values: json['values'] as int, + count: (json['count'] as num).toInt(), + values: (json['values'] as num).toInt(), ); Map _$KeyStatsToJson(KeyStats instance) => { diff --git a/lib/src/model/knowledge_panel_element.g.dart b/lib/src/model/knowledge_panel_element.g.dart index 6e07b94b5d..d3130d077d 100644 --- a/lib/src/model/knowledge_panel_element.g.dart +++ b/lib/src/model/knowledge_panel_element.g.dart @@ -41,8 +41,8 @@ KnowledgePanelImageElement _$KnowledgePanelImageElementFromJson( Map json) => KnowledgePanelImageElement( url: json['url'] as String, - width: json['width'] as int?, - height: json['height'] as int?, + width: (json['width'] as num?)?.toInt(), + height: (json['height'] as num?)?.toInt(), altText: json['alt'] as String?, linkUrl: json['link_url'] as String?, ); diff --git a/lib/src/model/leaderboard_entry.g.dart b/lib/src/model/leaderboard_entry.g.dart index 2cb345e22b..01d2f19a25 100644 --- a/lib/src/model/leaderboard_entry.g.dart +++ b/lib/src/model/leaderboard_entry.g.dart @@ -8,7 +8,7 @@ part of 'leaderboard_entry.dart'; LeaderboardEntry _$LeaderboardEntryFromJson(Map json) => LeaderboardEntry( - score: json['score'] as int, + score: (json['score'] as num).toInt(), userId: json['user_id'] as String?, ); diff --git a/lib/src/model/ocr_ingredients_result.g.dart b/lib/src/model/ocr_ingredients_result.g.dart index f784ec3315..5342b4dde3 100644 --- a/lib/src/model/ocr_ingredients_result.g.dart +++ b/lib/src/model/ocr_ingredients_result.g.dart @@ -9,7 +9,7 @@ part of 'ocr_ingredients_result.dart'; OcrIngredientsResult _$OcrIngredientsResultFromJson( Map json) => OcrIngredientsResult( - status: json['status'] as int?, + status: (json['status'] as num?)?.toInt(), ingredientsTextFromImageOrig: json['ingredients_text_from_image_orig'] as String?, ingredientsTextFromImage: json['ingredients_text_from_image'] as String?, diff --git a/lib/src/model/ocr_packaging_result.g.dart b/lib/src/model/ocr_packaging_result.g.dart index 0867584efd..cde2d5f15d 100644 --- a/lib/src/model/ocr_packaging_result.g.dart +++ b/lib/src/model/ocr_packaging_result.g.dart @@ -8,7 +8,7 @@ part of 'ocr_packaging_result.dart'; OcrPackagingResult _$OcrPackagingResultFromJson(Map json) => OcrPackagingResult( - status: json['status'] as int?, + status: (json['status'] as num?)?.toInt(), textFromImageOrig: json['packaging_text_from_image_orig'] as String?, textFromImage: json['packaging_text_from_image'] as String?, ); diff --git a/lib/src/model/old_product_result.g.dart b/lib/src/model/old_product_result.g.dart index 3a6cb5df26..049d197480 100644 --- a/lib/src/model/old_product_result.g.dart +++ b/lib/src/model/old_product_result.g.dart @@ -8,7 +8,7 @@ part of 'old_product_result.dart'; OldProductResult _$OldProductResultFromJson(Map json) => OldProductResult( - status: json['status'] as int?, + status: (json['status'] as num?)?.toInt(), barcode: json['code'] as String?, statusVerbose: json['status_verbose'] as String?, product: json['product'] == null diff --git a/lib/src/model/product.g.dart b/lib/src/model/product.g.dart index 0ca116f3d7..11c285d991 100644 --- a/lib/src/model/product.g.dart +++ b/lib/src/model/product.g.dart @@ -113,7 +113,8 @@ Product _$ProductFromJson(Map json) => Product( (k, e) => MapEntry( $enumDecode(_$OpenFoodFactsLanguageEnumMap, k), (e as Map).map( - (k, e) => MapEntry($enumDecode(_$ImageFieldEnumMap, k), e as int), + (k, e) => MapEntry( + $enumDecode(_$ImageFieldEnumMap, k), (e as num).toInt()), )), ) ..ingredientsAnalysisTagsInLanguages = @@ -167,7 +168,7 @@ Product _$ProductFromJson(Map json) => Product( ..embCodes = json['emb_codes'] as String? ..manufacturingPlaces = json['manufacturing_places'] as String? ..origins = json['origins'] as String? - ..novaGroup = json['nova_group'] as int? + ..novaGroup = (json['nova_group'] as num?)?.toInt() ..website = json['link'] as String? ..obsolete = JsonHelper.checkboxFromJSON(json['obsolete']) ..expirationDate = json['expiration_date'] as String?; diff --git a/lib/src/model/product_stats.g.dart b/lib/src/model/product_stats.g.dart index e19b68aed3..1eadd21f47 100644 --- a/lib/src/model/product_stats.g.dart +++ b/lib/src/model/product_stats.g.dart @@ -8,8 +8,8 @@ part of 'product_stats.dart'; ProductStats _$ProductStatsFromJson(Map json) => ProductStats( barcode: json['product'] as String, - numberOfKeys: json['keys'] as int, - numberOfEditors: json['editors'] as int, + numberOfKeys: (json['keys'] as num).toInt(), + numberOfEditors: (json['editors'] as num).toInt(), lastEdit: JsonHelper.stringTimestampToDate(json['last_edit']), ); diff --git a/lib/src/model/product_tag.g.dart b/lib/src/model/product_tag.g.dart index 7f65abd6d8..b622682b1a 100644 --- a/lib/src/model/product_tag.g.dart +++ b/lib/src/model/product_tag.g.dart @@ -11,7 +11,7 @@ ProductTag _$ProductTagFromJson(Map json) => ProductTag( key: json['k'] as String, value: json['v'] as String, owner: json['owner'] as String, - version: json['version'] as int, + version: (json['version'] as num).toInt(), editor: json['editor'] as String, lastEdit: JsonHelper.stringTimestampToDate(json['last_edit']), comment: json['comment'] as String, diff --git a/lib/src/model/spelling_corrections.g.dart b/lib/src/model/spelling_corrections.g.dart index 5a43862585..3824cf1d21 100644 --- a/lib/src/model/spelling_corrections.g.dart +++ b/lib/src/model/spelling_corrections.g.dart @@ -47,8 +47,8 @@ Map _$TermCorrectionsToJson(TermCorrections instance) => Correction _$CorrectionFromJson(Map json) => Correction( json['correction'] as String?, json['original'] as String?, - json['start_offset'] as int?, - json['end_offset'] as int?, + (json['start_offset'] as num?)?.toInt(), + (json['end_offset'] as num?)?.toInt(), json['is_valid'] as bool?, ); diff --git a/lib/src/open_prices_api_client.dart b/lib/src/open_prices_api_client.dart index 1bc05bc701..cbc99fdb26 100644 --- a/lib/src/open_prices_api_client.dart +++ b/lib/src/open_prices_api_client.dart @@ -11,6 +11,10 @@ import 'prices/get_locations_result.dart'; import 'prices/get_parameters_helper.dart'; import 'prices/get_prices_parameters.dart'; import 'prices/get_prices_result.dart'; +import 'prices/get_proofs_parameters.dart'; +import 'prices/get_proofs_result.dart'; +import 'prices/get_users_parameters.dart'; +import 'prices/get_users_result.dart'; import 'prices/location.dart'; import 'prices/location_osm_type.dart'; import 'prices/price_product.dart'; @@ -383,9 +387,37 @@ class OpenPricesAPIClient { return MaybeError.responseError(response); } + /// Get user proofs. + static Future> getProofs( + final GetProofsParameters parameters, { + final UriProductHelper uriHelper = uriHelperFoodProd, + required final String bearerToken, + }) async { + final Uri uri = uriHelper.getUri( + path: '/api/v1/proofs', + queryParameters: parameters.getQueryParameters(), + forcedHost: _getHost(uriHelper), + ); + final Response response = await HttpHelper().doGetRequest( + uri, + uriHelper: uriHelper, + bearerToken: bearerToken, + ); + if (response.statusCode == 200) { + try { + final dynamic decodedResponse = HttpHelper().jsonDecodeUtf8(response); + return MaybeError.value( + GetProofsResult.fromJson(decodedResponse), + ); + } catch (e) { + // + } + } + return MaybeError.responseError(response); + } + static Future> uploadProof({ required final ProofType proofType, - required final bool isPublic, required final Uri imageUri, required final MediaType mediaType, required final String bearerToken, @@ -404,7 +436,6 @@ class OpenPricesAPIClient { request.fields.addAll( { 'type': proofType.offTag, - 'is_public': isPublic ? 'true' : 'false', }, ); final List fileBytes = await UriReader.instance!.readAsBytes(imageUri); @@ -436,6 +467,32 @@ class OpenPricesAPIClient { ); } + /// Get user proof by id. + static Future> getProof( + final int proofId, { + final UriProductHelper uriHelper = uriHelperFoodProd, + required final String bearerToken, + }) async { + final Uri uri = uriHelper.getUri( + path: '/api/v1/proofs/$proofId', + forcedHost: _getHost(uriHelper), + ); + final Response response = await HttpHelper().doGetRequest( + uri, + uriHelper: uriHelper, + bearerToken: bearerToken, + ); + if (response.statusCode == 200) { + try { + final dynamic decodedResponse = HttpHelper().jsonDecodeUtf8(response); + return MaybeError.value(Proof.fromJson(decodedResponse)); + } catch (e) { + // + } + } + return MaybeError.responseError(response); + } + /// Deletes a proof. /// A user can delete only owned proofs. Can delete only proofs that are not associated with prices. A moderator can delete not owned proofs. /// Returns true if successful. @@ -458,4 +515,32 @@ class OpenPricesAPIClient { } return MaybeError.responseError(response); } + + static Future> getUsers( + final GetUsersParameters parameters, { + final UriProductHelper uriHelper = uriHelperFoodProd, + final String? bearerToken, + }) async { + final Uri uri = uriHelper.getUri( + path: '/api/v1/users', + queryParameters: parameters.getQueryParameters(), + forcedHost: _getHost(uriHelper), + ); + final Response response = await HttpHelper().doGetRequest( + uri, + uriHelper: uriHelper, + bearerToken: bearerToken, + ); + if (response.statusCode == 200) { + try { + final dynamic decodedResponse = HttpHelper().jsonDecodeUtf8(response); + return MaybeError.value( + GetUsersResult.fromJson(decodedResponse), + ); + } catch (e) { + // + } + } + return MaybeError.responseError(response); + } } diff --git a/lib/src/prices/flavor.dart b/lib/src/prices/flavor.dart new file mode 100644 index 0000000000..52f50f067a --- /dev/null +++ b/lib/src/prices/flavor.dart @@ -0,0 +1,36 @@ +import 'package:json_annotation/json_annotation.dart'; +import '../model/off_tagged.dart'; +import '../utils/server_type.dart'; + +/// Flavor is used to refer to a specific Open*Facts project. +/// +/// cf. `Flavor` in https://prices.openfoodfacts.org/api/docs +/// Somehow redundant with [ServerType]. +enum Flavor implements OffTagged { + /// Open Food Facts + @JsonValue('off') + openFoodFacts(offTag: 'off'), + + /// Open Beauty Facts + @JsonValue('obf') + openBeautyFacts(offTag: 'obf'), + + /// Open Pet Food Facts + @JsonValue('opff') + openPetFoodFacts(offTag: 'opff'), + + /// Open Product Facts + @JsonValue('opf') + openProductFacts(offTag: 'opf'), + + /// Open Product Facts (Pro platform) + @JsonValue('off-pro') + openFoodProductFactsPro(offTag: 'off-pro'); + + const Flavor({ + required this.offTag, + }); + + @override + final String offTag; +} diff --git a/lib/src/prices/get_locations_parameters.dart b/lib/src/prices/get_locations_parameters.dart index 8305439a1a..2ffe423ce4 100644 --- a/lib/src/prices/get_locations_parameters.dart +++ b/lib/src/prices/get_locations_parameters.dart @@ -1,19 +1,15 @@ import 'get_locations_order.dart'; -import 'order_by.dart'; -import 'get_parameters_helper.dart'; +import 'get_price_count_parameters_helper.dart'; /// Parameters for the "get locations" API query. /// /// cf. https://prices.openfoodfacts.org/api/docs -class GetLocationsParameters extends GetParametersHelper { +class GetLocationsParameters + extends GetPriceCountParametersHelper { String? osmNameLike; String? osmCityLike; String? osmPostcodeLike; String? osmCountryLike; - int? priceCount; - int? priceCountGte; - int? priceCountLte; - List>? orderBy; @override Map getQueryParameters() { @@ -22,18 +18,6 @@ class GetLocationsParameters extends GetParametersHelper { addNonNullString(osmCityLike, 'osm_address_city__like'); addNonNullString(osmPostcodeLike, 'osm_address_postcode__like'); addNonNullString(osmCountryLike, 'osm_address_country__like'); - addNonNullInt(priceCount, 'price_count'); - addNonNullInt(priceCountGte, 'price_count__gte'); - addNonNullInt(priceCountLte, 'price_count__lte'); - if (orderBy != null) { - final List orders = []; - for (final OrderBy order in orderBy!) { - orders.add(order.offTag); - } - if (orders.isNotEmpty) { - addNonNullString(orders.join(','), 'order_by'); - } - } return result; } } diff --git a/lib/src/prices/get_locations_result.g.dart b/lib/src/prices/get_locations_result.g.dart index 78c6078dbe..5b0a7dbba4 100644 --- a/lib/src/prices/get_locations_result.g.dart +++ b/lib/src/prices/get_locations_result.g.dart @@ -11,10 +11,10 @@ GetLocationsResult _$GetLocationsResultFromJson(Map json) => ..items = (json['items'] as List?) ?.map((e) => Location.fromJson(e as Map)) .toList() - ..total = json['total'] as int? - ..pageNumber = json['page'] as int? - ..pageSize = json['size'] as int? - ..numberOfPages = json['pages'] as int?; + ..total = (json['total'] as num?)?.toInt() + ..pageNumber = (json['page'] as num?)?.toInt() + ..pageSize = (json['size'] as num?)?.toInt() + ..numberOfPages = (json['pages'] as num?)?.toInt(); Map _$GetLocationsResultToJson(GetLocationsResult instance) => { diff --git a/lib/src/prices/get_parameters_helper.dart b/lib/src/prices/get_parameters_helper.dart index a0c90458be..a996f94245 100644 --- a/lib/src/prices/get_parameters_helper.dart +++ b/lib/src/prices/get_parameters_helper.dart @@ -1,9 +1,11 @@ import 'package:meta/meta.dart'; +import 'order_by.dart'; /// Helper class for API query parameters. -abstract class GetParametersHelper { +abstract class GetParametersHelper { int? pageSize; int? pageNumber; + List>? orderBy; final Map _result = {}; @@ -17,6 +19,15 @@ abstract class GetParametersHelper { _result.clear(); addNonNullInt(pageNumber, 'page'); addNonNullInt(pageSize, 'size'); + if (orderBy != null) { + final List orders = []; + for (final OrderBy order in orderBy!) { + orders.add(order.offTag); + } + if (orders.isNotEmpty) { + addNonNullString(orders.join(','), 'order_by'); + } + } return _result; } diff --git a/lib/src/prices/get_price_count_parameters_helper.dart b/lib/src/prices/get_price_count_parameters_helper.dart new file mode 100644 index 0000000000..ce20b813ad --- /dev/null +++ b/lib/src/prices/get_price_count_parameters_helper.dart @@ -0,0 +1,20 @@ +import 'get_parameters_helper.dart'; +import 'order_by.dart'; + +/// Helper class for API query parameters with price count filters. +abstract class GetPriceCountParametersHelper + extends GetParametersHelper { + int? priceCount; + int? priceCountGte; + int? priceCountLte; + + /// Returns the parameters as a query parameter map. + @override + Map getQueryParameters() { + super.getQueryParameters(); + addNonNullInt(priceCount, 'price_count'); + addNonNullInt(priceCountGte, 'price_count__gte'); + addNonNullInt(priceCountLte, 'price_count__lte'); + return result; + } +} diff --git a/lib/src/prices/get_prices_parameters.dart b/lib/src/prices/get_prices_parameters.dart index 82017212fd..099c05cba2 100644 --- a/lib/src/prices/get_prices_parameters.dart +++ b/lib/src/prices/get_prices_parameters.dart @@ -1,13 +1,12 @@ import 'currency.dart'; import 'get_parameters_helper.dart'; import 'get_prices_order.dart'; -import 'order_by.dart'; import 'location_osm_type.dart'; /// Parameters for the "get prices" API query. /// /// cf. https://prices.openfoodfacts.org/api/docs -class GetPricesParameters extends GetParametersHelper { +class GetPricesParameters extends GetParametersHelper { String? productCode; int? productId; bool? productIdIsNull; @@ -25,7 +24,6 @@ class GetPricesParameters extends GetParametersHelper { DateTime? dateLte; String? owner; DateTime? createdGte; - List>? orderBy; @override Map getQueryParameters() { @@ -47,15 +45,6 @@ class GetPricesParameters extends GetParametersHelper { addNonNullDate(dateLte, 'date__lte', dayOnly: true); addNonNullString(owner, 'owner'); addNonNullDate(createdGte, 'created__gte', dayOnly: false); - if (orderBy != null) { - final List orders = []; - for (final OrderBy order in orderBy!) { - orders.add(order.offTag); - } - if (orders.isNotEmpty) { - addNonNullString(orders.join(','), 'order_by'); - } - } return result; } } diff --git a/lib/src/prices/get_prices_result.g.dart b/lib/src/prices/get_prices_result.g.dart index 6e4bea82a9..5f87abb3f2 100644 --- a/lib/src/prices/get_prices_result.g.dart +++ b/lib/src/prices/get_prices_result.g.dart @@ -11,10 +11,10 @@ GetPricesResult _$GetPricesResultFromJson(Map json) => ..items = (json['items'] as List?) ?.map((e) => Price.fromJson(e as Map)) .toList() - ..total = json['total'] as int? - ..pageNumber = json['page'] as int? - ..pageSize = json['size'] as int? - ..numberOfPages = json['pages'] as int?; + ..total = (json['total'] as num?)?.toInt() + ..pageNumber = (json['page'] as num?)?.toInt() + ..pageSize = (json['size'] as num?)?.toInt() + ..numberOfPages = (json['pages'] as num?)?.toInt(); Map _$GetPricesResultToJson(GetPricesResult instance) => { diff --git a/lib/src/prices/get_proofs_order.dart b/lib/src/prices/get_proofs_order.dart new file mode 100644 index 0000000000..4efd306b1e --- /dev/null +++ b/lib/src/prices/get_proofs_order.dart @@ -0,0 +1,11 @@ +import 'order_by.dart'; + +/// Field for the "order by" clause of "get proofs". +enum GetProofsOrderField implements OrderByField { + created(offTag: 'created'); + + const GetProofsOrderField({required this.offTag}); + + @override + final String offTag; +} diff --git a/lib/src/prices/get_proofs_parameters.dart b/lib/src/prices/get_proofs_parameters.dart new file mode 100644 index 0000000000..2bfe6e1a86 --- /dev/null +++ b/lib/src/prices/get_proofs_parameters.dart @@ -0,0 +1,21 @@ +import 'get_price_count_parameters_helper.dart'; +import 'get_proofs_order.dart'; +import 'proof_type.dart'; + +/// Parameters for the "get proofs" API query. +/// +/// cf. https://prices.openfoodfacts.org/api/docs +class GetProofsParameters + extends GetPriceCountParametersHelper { + String? owner; + ProofType? type; + + /// Returns the parameters as a query parameter map. + @override + Map getQueryParameters() { + super.getQueryParameters(); + addNonNullString(owner, 'owner'); + addNonNullString(type?.offTag, 'type'); + return result; + } +} diff --git a/lib/src/prices/get_proofs_result.dart b/lib/src/prices/get_proofs_result.dart new file mode 100644 index 0000000000..b34d9ce20e --- /dev/null +++ b/lib/src/prices/get_proofs_result.dart @@ -0,0 +1,33 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'proof.dart'; +import '../interface/json_object.dart'; + +part 'get_proofs_result.g.dart'; + +/// List of proof objects returned by the "get proofs" method. +@JsonSerializable() +class GetProofsResult extends JsonObject { + @JsonKey() + List? items; + + @JsonKey() + int? total; + + @JsonKey(name: 'page') + int? pageNumber; + + @JsonKey(name: 'size') + int? pageSize; + + @JsonKey(name: 'pages') + int? numberOfPages; + + GetProofsResult(); + + factory GetProofsResult.fromJson(Map json) => + _$GetProofsResultFromJson(json); + + @override + Map toJson() => _$GetProofsResultToJson(this); +} diff --git a/lib/src/prices/get_proofs_result.g.dart b/lib/src/prices/get_proofs_result.g.dart new file mode 100644 index 0000000000..6779bd171d --- /dev/null +++ b/lib/src/prices/get_proofs_result.g.dart @@ -0,0 +1,26 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'get_proofs_result.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +GetProofsResult _$GetProofsResultFromJson(Map json) => + GetProofsResult() + ..items = (json['items'] as List?) + ?.map((e) => Proof.fromJson(e as Map)) + .toList() + ..total = (json['total'] as num?)?.toInt() + ..pageNumber = (json['page'] as num?)?.toInt() + ..pageSize = (json['size'] as num?)?.toInt() + ..numberOfPages = (json['pages'] as num?)?.toInt(); + +Map _$GetProofsResultToJson(GetProofsResult instance) => + { + 'items': instance.items, + 'total': instance.total, + 'page': instance.pageNumber, + 'size': instance.pageSize, + 'pages': instance.numberOfPages, + }; diff --git a/lib/src/prices/get_users_order.dart b/lib/src/prices/get_users_order.dart new file mode 100644 index 0000000000..d0399f3cec --- /dev/null +++ b/lib/src/prices/get_users_order.dart @@ -0,0 +1,12 @@ +import 'order_by.dart'; + +/// Field for the "order by" clause of "get users". +enum GetUsersOrderField implements OrderByField { + priceCount(offTag: 'price_count'), + userId(offTag: 'user_id'); + + const GetUsersOrderField({required this.offTag}); + + @override + final String offTag; +} diff --git a/lib/src/prices/get_users_parameters.dart b/lib/src/prices/get_users_parameters.dart new file mode 100644 index 0000000000..e7b44f44b3 --- /dev/null +++ b/lib/src/prices/get_users_parameters.dart @@ -0,0 +1,8 @@ +import 'get_price_count_parameters_helper.dart'; +import 'get_users_order.dart'; + +/// Parameters for the "get users" API query. +/// +/// cf. https://prices.openfoodfacts.org/api/docs +class GetUsersParameters + extends GetPriceCountParametersHelper {} diff --git a/lib/src/prices/get_users_result.dart b/lib/src/prices/get_users_result.dart new file mode 100644 index 0000000000..d56d0582ac --- /dev/null +++ b/lib/src/prices/get_users_result.dart @@ -0,0 +1,33 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'price_user.dart'; +import '../interface/json_object.dart'; + +part 'get_users_result.g.dart'; + +/// List of price user objects returned by the "get users" method. +@JsonSerializable() +class GetUsersResult extends JsonObject { + @JsonKey() + List? items; + + @JsonKey() + int? total; + + @JsonKey(name: 'page') + int? pageNumber; + + @JsonKey(name: 'size') + int? pageSize; + + @JsonKey(name: 'pages') + int? numberOfPages; + + GetUsersResult(); + + factory GetUsersResult.fromJson(Map json) => + _$GetUsersResultFromJson(json); + + @override + Map toJson() => _$GetUsersResultToJson(this); +} diff --git a/lib/src/prices/get_users_result.g.dart b/lib/src/prices/get_users_result.g.dart new file mode 100644 index 0000000000..7feb4cd38a --- /dev/null +++ b/lib/src/prices/get_users_result.g.dart @@ -0,0 +1,26 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'get_users_result.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +GetUsersResult _$GetUsersResultFromJson(Map json) => + GetUsersResult() + ..items = (json['items'] as List?) + ?.map((e) => PriceUser.fromJson(e as Map)) + .toList() + ..total = (json['total'] as num?)?.toInt() + ..pageNumber = (json['page'] as num?)?.toInt() + ..pageSize = (json['size'] as num?)?.toInt() + ..numberOfPages = (json['pages'] as num?)?.toInt(); + +Map _$GetUsersResultToJson(GetUsersResult instance) => + { + 'items': instance.items, + 'total': instance.total, + 'page': instance.pageNumber, + 'size': instance.pageSize, + 'pages': instance.numberOfPages, + }; diff --git a/lib/src/prices/location.dart b/lib/src/prices/location.dart index 2ebd24cc39..1d808c7604 100644 --- a/lib/src/prices/location.dart +++ b/lib/src/prices/location.dart @@ -23,6 +23,10 @@ class Location extends JsonObject { @JsonKey(name: 'osm_type') late LocationOSMType type; + /// Number of prices for this location. + @JsonKey(name: 'price_count') + late int priceCount; + /// ID in the Prices API. @JsonKey(name: 'id') late int locationId; diff --git a/lib/src/prices/location.g.dart b/lib/src/prices/location.g.dart index 59efae4304..d7e1d3d5c7 100644 --- a/lib/src/prices/location.g.dart +++ b/lib/src/prices/location.g.dart @@ -7,9 +7,10 @@ part of 'location.dart'; // ************************************************************************** Location _$LocationFromJson(Map json) => Location() - ..osmId = json['osm_id'] as int + ..osmId = (json['osm_id'] as num).toInt() ..type = $enumDecode(_$LocationOSMTypeEnumMap, json['osm_type']) - ..locationId = json['id'] as int + ..priceCount = (json['price_count'] as num).toInt() + ..locationId = (json['id'] as num).toInt() ..name = json['osm_name'] as String? ..displayName = json['osm_display_name'] as String? ..postcode = json['osm_address_postcode'] as String? @@ -23,6 +24,7 @@ Location _$LocationFromJson(Map json) => Location() Map _$LocationToJson(Location instance) => { 'osm_id': instance.osmId, 'osm_type': _$LocationOSMTypeEnumMap[instance.type]!, + 'price_count': instance.priceCount, 'id': instance.locationId, 'osm_name': instance.name, 'osm_display_name': instance.displayName, diff --git a/lib/src/prices/price.dart b/lib/src/prices/price.dart index d0fd7cbd68..4c1a320021 100644 --- a/lib/src/prices/price.dart +++ b/lib/src/prices/price.dart @@ -1,8 +1,11 @@ import 'package:json_annotation/json_annotation.dart'; import 'currency.dart'; +import 'location.dart'; import 'location_osm_type.dart'; import 'price_per.dart'; +import 'price_product.dart'; +import 'proof.dart'; import '../interface/json_object.dart'; import '../utils/json_helper.dart'; @@ -112,6 +115,18 @@ class Price extends JsonObject { @JsonKey(name: 'location_id') int? locationId; + /// Proof. + @JsonKey() + Proof? proof; + + /// Location. + @JsonKey() + Location? location; + + /// Product. + @JsonKey() + PriceProduct? product; + /// Owner. @JsonKey() late String owner; diff --git a/lib/src/prices/price.g.dart b/lib/src/prices/price.g.dart index 4b70672c32..47563d76cb 100644 --- a/lib/src/prices/price.g.dart +++ b/lib/src/prices/price.g.dart @@ -19,14 +19,23 @@ Price _$PriceFromJson(Map json) => Price() ..priceWithoutDiscount = json['price_without_discount'] as num? ..pricePer = $enumDecodeNullable(_$PricePerEnumMap, json['price_per']) ..currency = $enumDecode(_$CurrencyEnumMap, json['currency']) - ..locationOSMId = json['location_osm_id'] as int + ..locationOSMId = (json['location_osm_id'] as num).toInt() ..locationOSMType = $enumDecode(_$LocationOSMTypeEnumMap, json['location_osm_type']) ..date = JsonHelper.stringTimestampToDate(json['date']) - ..proofId = json['proof_id'] as int? - ..id = json['id'] as int - ..productId = json['product_id'] as int? - ..locationId = json['location_id'] as int? + ..proofId = (json['proof_id'] as num?)?.toInt() + ..id = (json['id'] as num).toInt() + ..productId = (json['product_id'] as num?)?.toInt() + ..locationId = (json['location_id'] as num?)?.toInt() + ..proof = json['proof'] == null + ? null + : Proof.fromJson(json['proof'] as Map) + ..location = json['location'] == null + ? null + : Location.fromJson(json['location'] as Map) + ..product = json['product'] == null + ? null + : PriceProduct.fromJson(json['product'] as Map) ..owner = json['owner'] as String ..created = JsonHelper.stringTimestampToDate(json['created']); @@ -48,6 +57,9 @@ Map _$PriceToJson(Price instance) => { 'id': instance.id, 'product_id': instance.productId, 'location_id': instance.locationId, + 'proof': instance.proof, + 'location': instance.location, + 'product': instance.product, 'owner': instance.owner, 'created': instance.created.toIso8601String(), }; diff --git a/lib/src/prices/price_product.dart b/lib/src/prices/price_product.dart index 1084842433..79dfe30e40 100644 --- a/lib/src/prices/price_product.dart +++ b/lib/src/prices/price_product.dart @@ -1,5 +1,6 @@ import 'package:json_annotation/json_annotation.dart'; +import 'flavor.dart'; import '../interface/json_object.dart'; import '../utils/json_helper.dart'; @@ -7,17 +8,23 @@ part 'price_product.g.dart'; /// Product object in the Prices API. /// -/// cf. `ProductBase` in https://prices.openfoodfacts.net/docs +/// cf. `ProductFull` in https://prices.openfoodfacts.net/docs @JsonSerializable() class PriceProduct extends JsonObject { + /// Barcode (EAN) of the product, as a string. @JsonKey() late String code; + /// Number of prices for this product. + @JsonKey(name: 'price_count') + late int priceCount; + @JsonKey(name: 'id') late int productId; + /// Source of data. @JsonKey() - String? source; + Flavor? source; @JsonKey(name: 'product_name') String? name; @@ -25,9 +32,36 @@ class PriceProduct extends JsonObject { @JsonKey(name: 'product_quantity') int? quantity; + @JsonKey(name: 'product_quantity_unit') + String? quantityUnit; + + @JsonKey(name: 'categories_tags') + late List categoriesTags; + + @JsonKey() + String? brands; + + @JsonKey(name: 'brands_tags') + late List brandsTags; + + @JsonKey(name: 'labels_tags') + late List labelsTags; + @JsonKey(name: 'image_url') String? imageURL; + @JsonKey(name: 'nutriscore_grade') + String? nutriscoreGrade; + + @JsonKey(name: 'ecoscore_grade') + String? ecoscoreGrade; + + @JsonKey(name: 'nova_group') + int? novaGroup; + + @JsonKey(name: 'unique_scans_n') + late int uniqueScansNumber; + @JsonKey(fromJson: JsonHelper.stringTimestampToDate) late DateTime created; diff --git a/lib/src/prices/price_product.g.dart b/lib/src/prices/price_product.g.dart index fb68ba5097..8ad8c5c658 100644 --- a/lib/src/prices/price_product.g.dart +++ b/lib/src/prices/price_product.g.dart @@ -8,22 +8,54 @@ part of 'price_product.dart'; PriceProduct _$PriceProductFromJson(Map json) => PriceProduct() ..code = json['code'] as String - ..productId = json['id'] as int - ..source = json['source'] as String? + ..priceCount = (json['price_count'] as num).toInt() + ..productId = (json['id'] as num).toInt() + ..source = $enumDecodeNullable(_$FlavorEnumMap, json['source']) ..name = json['product_name'] as String? - ..quantity = json['product_quantity'] as int? + ..quantity = (json['product_quantity'] as num?)?.toInt() + ..quantityUnit = json['product_quantity_unit'] as String? + ..categoriesTags = (json['categories_tags'] as List) + .map((e) => e as String) + .toList() + ..brands = json['brands'] as String? + ..brandsTags = + (json['brands_tags'] as List).map((e) => e as String).toList() + ..labelsTags = + (json['labels_tags'] as List).map((e) => e as String).toList() ..imageURL = json['image_url'] as String? + ..nutriscoreGrade = json['nutriscore_grade'] as String? + ..ecoscoreGrade = json['ecoscore_grade'] as String? + ..novaGroup = (json['nova_group'] as num?)?.toInt() + ..uniqueScansNumber = (json['unique_scans_n'] as num).toInt() ..created = JsonHelper.stringTimestampToDate(json['created']) ..updated = JsonHelper.nullableStringTimestampToDate(json['updated']); Map _$PriceProductToJson(PriceProduct instance) => { 'code': instance.code, + 'price_count': instance.priceCount, 'id': instance.productId, - 'source': instance.source, + 'source': _$FlavorEnumMap[instance.source], 'product_name': instance.name, 'product_quantity': instance.quantity, + 'product_quantity_unit': instance.quantityUnit, + 'categories_tags': instance.categoriesTags, + 'brands': instance.brands, + 'brands_tags': instance.brandsTags, + 'labels_tags': instance.labelsTags, 'image_url': instance.imageURL, + 'nutriscore_grade': instance.nutriscoreGrade, + 'ecoscore_grade': instance.ecoscoreGrade, + 'nova_group': instance.novaGroup, + 'unique_scans_n': instance.uniqueScansNumber, 'created': instance.created.toIso8601String(), 'updated': instance.updated?.toIso8601String(), }; + +const _$FlavorEnumMap = { + Flavor.openFoodFacts: 'off', + Flavor.openBeautyFacts: 'obf', + Flavor.openPetFoodFacts: 'opff', + Flavor.openProductFacts: 'opf', + Flavor.openFoodProductFactsPro: 'off-pro', +}; diff --git a/lib/src/prices/price_user.dart b/lib/src/prices/price_user.dart new file mode 100644 index 0000000000..9f2a6147eb --- /dev/null +++ b/lib/src/prices/price_user.dart @@ -0,0 +1,30 @@ +import 'package:json_annotation/json_annotation.dart'; + +import '../interface/json_object.dart'; + +part 'price_user.g.dart'; + +/// Price user object. +/// +/// cf. `UserBase` in https://prices.openfoodfacts.org/api/docs +@JsonSerializable() +class PriceUser extends JsonObject { + @JsonKey(name: 'user_id') + late String userId; + + /// Number of prices for this user. + @JsonKey(name: 'price_count') + late int priceCount; + + /// Number of prices for this user. + @JsonKey(name: 'is_moderator') + late bool isModerator; + + PriceUser(); + + factory PriceUser.fromJson(Map json) => + _$PriceUserFromJson(json); + + @override + Map toJson() => _$PriceUserToJson(this); +} diff --git a/lib/src/prices/price_user.g.dart b/lib/src/prices/price_user.g.dart new file mode 100644 index 0000000000..f72ba6687a --- /dev/null +++ b/lib/src/prices/price_user.g.dart @@ -0,0 +1,18 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'price_user.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +PriceUser _$PriceUserFromJson(Map json) => PriceUser() + ..userId = json['user_id'] as String + ..priceCount = (json['price_count'] as num).toInt() + ..isModerator = json['is_moderator'] as bool; + +Map _$PriceUserToJson(PriceUser instance) => { + 'user_id': instance.userId, + 'price_count': instance.priceCount, + 'is_moderator': instance.isModerator, + }; diff --git a/lib/src/prices/proof.dart b/lib/src/prices/proof.dart index a2bdf25a9b..214ece5548 100644 --- a/lib/src/prices/proof.dart +++ b/lib/src/prices/proof.dart @@ -8,7 +8,7 @@ part 'proof.g.dart'; /// Proof of a price. /// -/// cf. ProofFull in https://prices.openfoodfacts.org/api/docs +/// cf. `ProofFull` in https://prices.openfoodfacts.org/api/docs @JsonSerializable() class Proof extends JsonObject { @JsonKey() @@ -23,12 +23,6 @@ class Proof extends JsonObject { @JsonKey() ProofType? type; - /// if true, the proof is public and is included in the API response. - /// - /// Set false only if the proof contains personal information. - @JsonKey(name: 'is_public') - late bool isPublic; - /// Number of prices for this proof. @JsonKey(name: 'price_count') late int priceCount; diff --git a/lib/src/prices/proof.g.dart b/lib/src/prices/proof.g.dart index d3ed2a0c15..4ef52c90a0 100644 --- a/lib/src/prices/proof.g.dart +++ b/lib/src/prices/proof.g.dart @@ -7,12 +7,11 @@ part of 'proof.dart'; // ************************************************************************** Proof _$ProofFromJson(Map json) => Proof() - ..id = json['id'] as int + ..id = (json['id'] as num).toInt() ..filePath = json['file_path'] as String? ..mimetype = json['mimetype'] as String ..type = $enumDecodeNullable(_$ProofTypeEnumMap, json['type']) - ..isPublic = json['is_public'] as bool - ..priceCount = json['price_count'] as int + ..priceCount = (json['price_count'] as num).toInt() ..owner = json['owner'] as String ..created = JsonHelper.stringTimestampToDate(json['created']); @@ -21,7 +20,6 @@ Map _$ProofToJson(Proof instance) => { 'file_path': instance.filePath, 'mimetype': instance.mimetype, 'type': _$ProofTypeEnumMap[instance.type], - 'is_public': instance.isPublic, 'price_count': instance.priceCount, 'owner': instance.owner, 'created': instance.created.toIso8601String(), diff --git a/pubspec.yaml b/pubspec.yaml index 13e332afa1..053f61421d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,19 +8,19 @@ environment: sdk: '>=2.17.0 <4.0.0' dependencies: - json_annotation: ^4.8.1 + json_annotation: ^4.9.0 http: '>=0.13.5 <2.0.0' http_parser: ^4.0.2 - path: ^1.8.2 # 1.8.2 for flutter_test as of 2023-04-06 + path: ^1.9.0 meta: ^1.8.0 dev_dependencies: - analyzer: ^6.0.0 - build_runner: ^2.4.6 - json_serializable: ^6.7.1 + analyzer: ^6.2.0 + build_runner: ^2.4.9 + json_serializable: ^6.8.0 lints: ">=3.0.0 <5.0.0" - test: ^1.21.4 - coverage: ^1.6.2 + test: ^1.25.5 + coverage: ^1.8.0 funding: - "https://donate.openfoodfacts.org/" diff --git a/test/api_events_test.dart b/test/api_events_test.dart index ab49158fe0..44181adbf8 100644 --- a/test/api_events_test.dart +++ b/test/api_events_test.dart @@ -213,5 +213,5 @@ void main() { expect(knownSum, knownTotal); expect(nullSum, nullTotal); }); - }); + }, skip: 'Unstable EVENTS server'); } diff --git a/test/api_get_product_test.dart b/test/api_get_product_test.dart index 769b76b116..10d01d856b 100644 --- a/test/api_get_product_test.dart +++ b/test/api_get_product_test.dart @@ -333,25 +333,6 @@ void main() { nutriments.getValue(Nutrient.omega6, PerSize.serving), 9.1, ); - - result = await OpenFoodAPIClient.getProductV3( - ProductQueryConfiguration( - '5000159461122', - language: language, - fields: fields, - version: ProductQueryVersion.v3, - ), - ); - expect(result.product!.nutriments, isNotNull); - nutriments = result.product!.nutriments!; - expect( - nutriments.getValue(Nutrient.transFat, PerSize.oneHundredGrams), - 0.1, - ); - expect( - nutriments.getValue(Nutrient.transFat, PerSize.serving), - 0.05, - ); }); test('get product Confiture Rhubarbe Fraises extra', () async { diff --git a/test/api_get_robotoff_test.dart b/test/api_get_robotoff_test.dart index 45fc4b15e2..bacc1edc79 100644 --- a/test/api_get_robotoff_test.dart +++ b/test/api_get_robotoff_test.dart @@ -158,7 +158,7 @@ void main() { } // highly probable expect(germanBarcodes2, isNot(frenchBarcodes1)); - }); + }, timeout: Timeout(Duration(seconds: 90))); test('get 2 random questions with no specific type', () async { final RobotoffQuestionResult result = @@ -178,16 +178,14 @@ void main() { group('$OpenFoodAPIClient get robotoff insights', () { test('get random insight', () async { final InsightsResult result = await RobotoffAPIClient.getRandomInsights( - type: InsightType.CATEGORY, countries: [OpenFoodFactsCountry.FRANCE], ); expect(result.status, isNotNull); expect(result.status, 'found'); - expect(result.insights![0].type, InsightType.CATEGORY); expect(result.insights![0].id, isNotNull); expect(result.insights![0].barcode, isNotNull); - }); + }, timeout: Timeout(Duration(seconds: 90))); test('get product insights (found)', () async { final InsightsResult result1 = await RobotoffAPIClient.getRandomInsights( diff --git a/test/api_get_save_product_test.dart b/test/api_get_save_product_test.dart index 096baa18f4..b8826deb8a 100644 --- a/test/api_get_save_product_test.dart +++ b/test/api_get_save_product_test.dart @@ -150,8 +150,8 @@ void main() { expect(product.additives!.names[4], 'E950'); expect(product.images, isNotNull); - expect(product.images!, hasLength(7)); - expect(product.getRawImages(), hasLength(3)); + expect(product.images!, hasLength(10)); + expect(product.getRawImages(), hasLength(6)); expect(product.getMainImages(), hasLength(4)); expect(product.countries, 'Frankreich,Deutschland'); }); diff --git a/test/api_get_suggestions_test.dart b/test/api_get_suggestions_test.dart index bc44382e01..a3c2cc994b 100644 --- a/test/api_get_suggestions_test.dart +++ b/test/api_get_suggestions_test.dart @@ -359,7 +359,7 @@ void main() { language: OpenFoodFactsLanguage.UKRAINIAN, input: 'D', )); - }); + }, skip: 'Skipping while the issue is not fixed on the server side'); test('Suggestions empty fields', () async { List result = await OpenFoodAPIClient.getSuggestions( TagType.ALLERGENS, diff --git a/test/api_get_taxonomy_origins_server_test.dart b/test/api_get_taxonomy_origins_server_test.dart index 3640dcdacb..0096c9684f 100644 --- a/test/api_get_taxonomy_origins_server_test.dart +++ b/test/api_get_taxonomy_origins_server_test.dart @@ -1,4 +1,5 @@ import 'package:openfoodfacts/openfoodfacts.dart'; +import 'package:test/expect.dart'; import 'package:test/test.dart'; import 'test_constants.dart'; @@ -20,6 +21,8 @@ void main() { 'en:england', 'en:scotland', 'en:wales', + 'en:cornwall', + 'en:yorkshire', }; const Set expectedParents = { 'en:united-kingdom', @@ -36,7 +39,7 @@ void main() { expect(value.synonyms![OpenFoodFactsLanguage.FRENCH]!, contains(expectedNameFrench)); expect(value.parents, unorderedEquals(expectedParents)); - expect(value.children, unorderedEquals(expectedChildren)); + expect(value.children, containsAll(expectedChildren)); } test('get an origin', () async { diff --git a/test/api_not_food_get_product_test.dart b/test/api_not_food_get_product_test.dart index 1baa76373f..4be4e4d682 100644 --- a/test/api_not_food_get_product_test.dart +++ b/test/api_not_food_get_product_test.dart @@ -17,7 +17,7 @@ void main() { domain: 'openpetfoodfacts.org', ); - const String beautyBarcode = '3600550964738'; + const String beautyBarcode = '3600551054476'; const String productsBarcode = '7898927451035'; const String petFoodBarcode = '3564700266809'; diff --git a/test/api_prices_test.dart b/test/api_prices_test.dart index 09312b1930..d180a53e5a 100644 --- a/test/api_prices_test.dart +++ b/test/api_prices_test.dart @@ -505,9 +505,115 @@ void main() { } }); + test('get proofs', () async { + const int pageNumber = 1; + const int pageSize = 20; + const GetProofsOrderField orderField = GetProofsOrderField.created; + const ProofType proofType = ProofType.receipt; + + late GetProofsResult result; + + final MaybeError token = + await OpenPricesAPIClient.getAuthenticationToken( + username: user.userId, + password: user.password, + uriHelper: uriHelper, + ); + expect(token.isError, isFalse); + expect(token.value, isNotEmpty); + final String bearerToken = token.value; + + // oldest first + GetProofsParameters parameters = GetProofsParameters() + ..orderBy = >[ + OrderBy(field: orderField, ascending: true), + ] + ..type = proofType + ..pageSize = pageSize + ..pageNumber = pageNumber; + MaybeError maybeResults; + try { + maybeResults = await OpenPricesAPIClient.getProofs( + parameters, + uriHelper: uriHelper, + bearerToken: bearerToken, + ); + } catch (e) { + if (e.toString().contains(TestConstants.badGatewayError)) { + return; + } + rethrow; + } + expect(maybeResults.isError, isFalse); + result = maybeResults.value; + expect(result.pageSize, pageSize); + expect(result.pageNumber, pageNumber); + expect(result.total, isNotNull); + expect(result.numberOfPages, (result.total! / result.pageSize!).ceil()); + expect(result.items, isNotNull); + expect(result.items, hasLength(pageSize)); + for (final Proof proof in result.items!) { + expect(proof.type, proofType); + } + final DateTime oldestDate = result.items!.first.created; + + // newest first + parameters = GetProofsParameters() + ..orderBy = >[ + OrderBy(field: orderField, ascending: false), + ] + ..type = proofType + ..pageSize = pageSize + ..pageNumber = pageNumber; + try { + maybeResults = await OpenPricesAPIClient.getProofs( + parameters, + uriHelper: uriHelper, + bearerToken: bearerToken, + ); + } catch (e) { + if (e.toString().contains(TestConstants.badGatewayError)) { + return; + } + rethrow; + } + expect(maybeResults.isError, isFalse); + result = maybeResults.value; + expect(result.pageSize, pageSize); + expect(result.pageNumber, pageNumber); + expect(result.total, isNotNull); + expect(result.numberOfPages, (result.total! / result.pageSize!).ceil()); + expect(result.items, isNotNull); + expect(result.items, hasLength(pageSize)); + for (final Proof proof in result.items!) { + expect(proof.type, proofType); + } + final DateTime newestDate = result.items!.first.created; + + expect( + newestDate.millisecondsSinceEpoch, + greaterThan(oldestDate.millisecondsSinceEpoch), + ); + + // Trying to get the same single result, from an item. + final Proof proof = result.items!.first; + final MaybeError maybeProof = await OpenPricesAPIClient.getProof( + proof.id, + uriHelper: uriHelper, + bearerToken: bearerToken, + ); + expect(maybeProof.isError, isFalse); + expect(maybeProof.value.id, proof.id); + expect(maybeProof.value.type, proof.type); + expect(maybeProof.value.owner, proof.owner); + expect(maybeProof.value.priceCount, proof.priceCount); + expect(maybeProof.value.mimetype, proof.mimetype); + expect(maybeProof.value.created, proof.created); + expect(maybeProof.value.filePath, proof.filePath); + }); + test('upload', () async { final ProofType initialProofType = ProofType.receipt; - final bool initialIsPublic = true; // TODO(monsieurtanuki): more relevant image if possible final Uri initialImageUri = Uri.file('test/test_assets/ingredients_en.jpg'); @@ -519,7 +625,6 @@ void main() { // failing proof upload with invalid token MaybeError uploadProof = await OpenPricesAPIClient.uploadProof( proofType: initialProofType, - isPublic: initialIsPublic, imageUri: initialImageUri, mediaType: initialMediaType, bearerToken: bearerToken, @@ -543,7 +648,6 @@ void main() { // successful proof upload with valid token uploadProof = await OpenPricesAPIClient.uploadProof( proofType: initialProofType, - isPublic: initialIsPublic, imageUri: initialImageUri, mediaType: initialMediaType, bearerToken: bearerToken, @@ -554,7 +658,6 @@ void main() { expect(uploadProof.value.owner, user.userId); expect(uploadProof.value.id, isNotNull); expect(uploadProof.value.priceCount, 0); - expect(uploadProof.value.isPublic, initialIsPublic); expect(uploadProof.value.mimetype, initialMediaType.toString()); final int proofId = uploadProof.value.id; @@ -588,4 +691,80 @@ void main() { expect(closedSession.value, isTrue); }); }); + + group('$OpenPricesAPIClient Users', () { + test('get users', () async { + const int pageNumber = 1; + const int pageSize = 20; + const GetUsersOrderField orderField = GetUsersOrderField.priceCount; + + late GetUsersResult result; + + int? priceCountMax; + int? priceCountMin; + + // highest first + GetUsersParameters parameters = GetUsersParameters() + ..orderBy = >[ + OrderBy(field: orderField, ascending: false), + ] + ..pageSize = pageSize + ..pageNumber = pageNumber; + MaybeError maybeResults; + try { + maybeResults = await OpenPricesAPIClient.getUsers( + parameters, + uriHelper: uriHelperFoodProd, + ); + } catch (e) { + if (e.toString().contains(TestConstants.badGatewayError)) { + return; + } + rethrow; + } + expect(maybeResults.isError, isFalse); + result = maybeResults.value; + expect(result.pageSize, pageSize); + expect(result.pageNumber, pageNumber); + expect(result.total, isNotNull); + expect(result.numberOfPages, (result.total! / result.pageSize!).ceil()); + expect(result.items, isNotNull); + expect(result.items, hasLength(pageSize)); + for (final PriceUser user in result.items!) { + priceCountMax ??= user.priceCount; + } + + // lowest first + parameters = GetUsersParameters() + ..orderBy = >[ + OrderBy(field: orderField, ascending: true), + ] + ..pageSize = pageSize + ..pageNumber = pageNumber; + try { + maybeResults = await OpenPricesAPIClient.getUsers( + parameters, + uriHelper: uriHelperFoodProd, + ); + } catch (e) { + if (e.toString().contains(TestConstants.badGatewayError)) { + return; + } + rethrow; + } + expect(maybeResults.isError, isFalse); + result = maybeResults.value; + expect(result.pageSize, pageSize); + expect(result.pageNumber, pageNumber); + expect(result.total, isNotNull); + expect(result.numberOfPages, (result.total! / result.pageSize!).ceil()); + expect(result.items, isNotNull); + expect(result.items, hasLength(pageSize)); + for (final PriceUser user in result.items!) { + priceCountMin ??= user.priceCount; + } + + expect(priceCountMax!, greaterThan(priceCountMin!)); + }); + }); } From 299ba1edd93ac29f5d3546626a81238cdbfe1cfe Mon Sep 17 00:00:00 2001 From: Open Food Facts Bot <119524357+openfoodfacts-bot@users.noreply.github.com> Date: Sun, 19 May 2024 09:55:12 +0200 Subject: [PATCH 2/5] chore(master): release 3.9.0 (#931) --- CHANGELOG.md | 8 ++++++++ pubspec.yaml | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6394898ee6..fe0340a9a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [3.9.0](https://github.com/openfoodfacts/openfoodfacts-dart/compare/v3.8.0...v3.9.0) (2024-05-17) + + +### Features + +* 831 - prices methods getProofs, getProof and getUsers + fixed uploadProof ([#926](https://github.com/openfoodfacts/openfoodfacts-dart/issues/926)) ([baa1258](https://github.com/openfoodfacts/openfoodfacts-dart/commit/baa1258c71e38dfa194b568c5a20c70ee0dc6c6a)) +* add localized additives and allergens ([#929](https://github.com/openfoodfacts/openfoodfacts-dart/issues/929)) ([9d17073](https://github.com/openfoodfacts/openfoodfacts-dart/commit/9d1707309ab035e3360740e5ef0772915c95120e)) + ## [3.8.0](https://github.com/openfoodfacts/openfoodfacts-dart/compare/v3.7.0...v3.8.0) (2024-05-10) diff --git a/pubspec.yaml b/pubspec.yaml index 053f61421d..51f5d4bc04 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: openfoodfacts description: Dart package for the Open Food Facts API, a food products database made by everyone, for everyone. # The version is automatically, temporarily increased by the release workflow, changing it manually has no effect. -version: 3.8.0 +version: 3.9.0 homepage: https://github.com/openfoodfacts/openfoodfacts-dart environment: From 6f417be01733ec9b2f88743c68833321ce07a21d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 May 2024 12:57:24 +0200 Subject: [PATCH 3/5] chore(deps): bump JamesIves/github-pages-deploy-action (#932) Bumps [JamesIves/github-pages-deploy-action](https://github.com/jamesives/github-pages-deploy-action) from 4.6.0 to 4.6.1. - [Release notes](https://github.com/jamesives/github-pages-deploy-action/releases) - [Commits](https://github.com/jamesives/github-pages-deploy-action/compare/v4.6.0...v4.6.1) --- updated-dependencies: - dependency-name: JamesIves/github-pages-deploy-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/dartdoc.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dartdoc.yml b/.github/workflows/dartdoc.yml index dcdb4969ec..fedd92dec8 100644 --- a/.github/workflows/dartdoc.yml +++ b/.github/workflows/dartdoc.yml @@ -21,7 +21,7 @@ jobs: run: dart doc . - name: Deploy API documentation to Github Pages - uses: JamesIves/github-pages-deploy-action@v4.6.0 + uses: JamesIves/github-pages-deploy-action@v4.6.1 with: BRANCH: gh-pages FOLDER: doc/api/ From f653f6ab049e6ec30c0726cfae576ede57e5ce46 Mon Sep 17 00:00:00 2001 From: monsieurtanuki Date: Wed, 22 May 2024 16:55:15 +0200 Subject: [PATCH 4/5] feat: 933 - added osm location fields (countryCode, osmKey, osmValue) (#934) * feat: 933 - added osm location fields (countryCode, osmKey, osmValue) Impacted files: * `api_get_product_test.dart`: unrelated minor test fix * `api_get_robotoff_test.dart`: unrelated minor test fix * `api_matched_product_v1_test.dart`: unrelated minor test fix * `location.dart`: added fields `countryCode`, `osmKey` and `osmValue` * `location.g.dart`: generated * Minor typo fix --- lib/src/prices/location.dart | 9 +++++++++ lib/src/prices/location.g.dart | 6 ++++++ test/api_get_product_test.dart | 6 +++--- test/api_get_robotoff_test.dart | 4 +++- test/api_matched_product_v1_test.dart | 8 +++++--- 5 files changed, 26 insertions(+), 7 deletions(-) diff --git a/lib/src/prices/location.dart b/lib/src/prices/location.dart index 1d808c7604..4372e6c842 100644 --- a/lib/src/prices/location.dart +++ b/lib/src/prices/location.dart @@ -46,6 +46,15 @@ class Location extends JsonObject { @JsonKey(name: 'osm_address_country') String? country; + @JsonKey(name: 'osm_address_country_code') + String? countryCode; + + @JsonKey(name: 'osm_tag_key') + String? osmKey; + + @JsonKey(name: 'osm_tag_value') + String? osmValue; + @JsonKey(name: 'osm_lat') double? latitude; diff --git a/lib/src/prices/location.g.dart b/lib/src/prices/location.g.dart index d7e1d3d5c7..a60dbcf781 100644 --- a/lib/src/prices/location.g.dart +++ b/lib/src/prices/location.g.dart @@ -16,6 +16,9 @@ Location _$LocationFromJson(Map json) => Location() ..postcode = json['osm_address_postcode'] as String? ..city = json['osm_address_city'] as String? ..country = json['osm_address_country'] as String? + ..countryCode = json['osm_address_country_code'] as String? + ..osmKey = json['osm_tag_key'] as String? + ..osmValue = json['osm_tag_value'] as String? ..latitude = (json['osm_lat'] as num?)?.toDouble() ..longitude = (json['osm_lon'] as num?)?.toDouble() ..created = JsonHelper.stringTimestampToDate(json['created']) @@ -31,6 +34,9 @@ Map _$LocationToJson(Location instance) => { 'osm_address_postcode': instance.postcode, 'osm_address_city': instance.city, 'osm_address_country': instance.country, + 'osm_address_country_code': instance.countryCode, + 'osm_tag_key': instance.osmKey, + 'osm_tag_value': instance.osmValue, 'osm_lat': instance.latitude, 'osm_lon': instance.longitude, 'created': instance.created.toIso8601String(), diff --git a/test/api_get_product_test.dart b/test/api_get_product_test.dart index 10d01d856b..86267879a6 100644 --- a/test/api_get_product_test.dart +++ b/test/api_get_product_test.dart @@ -628,13 +628,13 @@ void main() { expect(nutritionalQuality.first.title, 'Nutri-Score D'); expect(nutritionalQuality.first.name, 'Nutri-Score'); expect(nutritionalQuality.first.match, - greaterThan(29)); // 20230602: 29.4444444444444 + greaterThan(27)); // 20240522: 27.3333333333333 expect(nutritionalQuality.first.status, 'known'); expect(nutritionalQuality[1].id, 'low_salt'); expect(nutritionalQuality[2].id, 'low_fat'); expect(nutritionalQuality[3].id, 'low_sugars'); expect(nutritionalQuality[4].id, 'low_saturated_fat'); - expect(nutritionalQuality.first.panelId, 'nutriscore'); + expect(nutritionalQuality.first.panelId, 'nutriscore_2023'); group = result.product!.attributeGroups! .singleWhere((element) => element.id == 'processing'); @@ -857,7 +857,7 @@ void main() { 'environment_card', 'health_card', 'ingredients', - 'nutriscore', + 'nutriscore_2023', 'root', }; final ProductResultV3 productResult = diff --git a/test/api_get_robotoff_test.dart b/test/api_get_robotoff_test.dart index bacc1edc79..985bf60385 100644 --- a/test/api_get_robotoff_test.dart +++ b/test/api_get_robotoff_test.dart @@ -158,7 +158,9 @@ void main() { } // highly probable expect(germanBarcodes2, isNot(frenchBarcodes1)); - }, timeout: Timeout(Duration(seconds: 90))); + }, + skip: 'a bit prone to 502 Bad Gateway', + timeout: Timeout(Duration(seconds: 90))); test('get 2 random questions with no specific type', () async { final RobotoffQuestionResult result = diff --git a/test/api_matched_product_v1_test.dart b/test/api_matched_product_v1_test.dart index e676ad0ff8..8b17245cbf 100644 --- a/test/api_matched_product_v1_test.dart +++ b/test/api_matched_product_v1_test.dart @@ -75,8 +75,9 @@ void main() { MatchedProduct matchedProduct; matchedProduct = MatchedProduct(result.product!, manager); - expect(matchedProduct.score, greaterThan(151)); - expect(matchedProduct.status, MatchedProductStatus.YES); + expect(matchedProduct.score, + greaterThan(50)); // 20240522: was 59.2727272727272 + expect(matchedProduct.status, MatchedProductStatus.NO); await manager.setImportance(attributeId1, importanceId2); expect( @@ -88,7 +89,8 @@ void main() { expect(refreshCounter, 4); matchedProduct = MatchedProduct(result.product!, manager); - expect(matchedProduct.score, greaterThan(37.5)); + expect(matchedProduct.score, + greaterThan(14)); // 20240522: was 14.8181818181818 expect(matchedProduct.status, MatchedProductStatus.NO); await manager.clearImportances(); // no attribute parameters at all From 1256fe64ec21dda26a6352fea826bcf43043717d Mon Sep 17 00:00:00 2001 From: Open Food Facts Bot <119524357+openfoodfacts-bot@users.noreply.github.com> Date: Sat, 25 May 2024 15:24:44 +0200 Subject: [PATCH 5/5] chore(master): release 3.10.0 (#935) --- CHANGELOG.md | 7 +++++++ pubspec.yaml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe0340a9a8..800631a06b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [3.10.0](https://github.com/openfoodfacts/openfoodfacts-dart/compare/v3.9.0...v3.10.0) (2024-05-22) + + +### Features + +* 933 - added osm location fields (countryCode, osmKey, osmValue) ([#934](https://github.com/openfoodfacts/openfoodfacts-dart/issues/934)) ([f653f6a](https://github.com/openfoodfacts/openfoodfacts-dart/commit/f653f6ab049e6ec30c0726cfae576ede57e5ce46)) + ## [3.9.0](https://github.com/openfoodfacts/openfoodfacts-dart/compare/v3.8.0...v3.9.0) (2024-05-17) diff --git a/pubspec.yaml b/pubspec.yaml index 51f5d4bc04..f060ae0689 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: openfoodfacts description: Dart package for the Open Food Facts API, a food products database made by everyone, for everyone. # The version is automatically, temporarily increased by the release workflow, changing it manually has no effect. -version: 3.9.0 +version: 3.10.0 homepage: https://github.com/openfoodfacts/openfoodfacts-dart environment: