diff --git a/Sources/Helpers/OFFUrlsHelper.swift b/Sources/Helpers/OFFUrlsHelper.swift index 380ca613a97..aff2bcf7d47 100644 --- a/Sources/Helpers/OFFUrlsHelper.swift +++ b/Sources/Helpers/OFFUrlsHelper.swift @@ -46,6 +46,10 @@ class OFFUrlsHelper: NSObject { return URL(string: "\(baseUrl())/nucleotide/\(nucleotide.code)")! } + static func url(forLabel label: Label) -> URL { + return URL(string: "\(baseUrl())/label/\(label.code)")! + } + // Not sure if there is a taxonomy for this /* static func url(forOther other: OtherNutritionalSubstance) -> URL { diff --git a/Sources/Models/API/OFFReadAPIkeysJSON.swift b/Sources/Models/API/OFFReadAPIkeysJSON.swift index 3e1a232e789..1f7fc430b88 100644 --- a/Sources/Models/API/OFFReadAPIkeysJSON.swift +++ b/Sources/Models/API/OFFReadAPIkeysJSON.swift @@ -299,6 +299,7 @@ struct OFFJson { //static let IngredientsIdsDebugKey = "ingredients_ids_debug" //static let IngredientsThatMayBeFromPalmOilNKey = "ingredients_that_may_be_from_palm_oil_n" static let LabelsKey = "labels" + static let LabelsTagsKey = "labels_tags" //static let LabelsPrevHierarchyKey = "labels_prev_hierarchy" //static let LcKey = "lc" static let MineralsTagsKey = "minerals_tags" @@ -375,7 +376,8 @@ struct OFFJson { OFFJson.IngredientsTextKey, OFFJson.IngredientsThatMayBeFromPalmOilTagsKey, // OFFJson.LabelsHierarchyKey, - // OFFJson.LabelsTagsKey, + OFFJson.LabelsKey, + OFFJson.LabelsTagsKey, OFFJson.LangKey, OFFJson.ProductNameLanguagesKey, OFFJson.GenericNameLanguagesKey, diff --git a/Sources/Models/API/Product.swift b/Sources/Models/API/Product.swift index 978eb4f35b1..5bebcf90a09 100644 --- a/Sources/Models/API/Product.swift +++ b/Sources/Models/API/Product.swift @@ -122,6 +122,7 @@ struct Product: Mappable { var manufacturingPlaces: String? var origins: String? var labels: [String]? + var labelsTags: [String]? var citiesTags: [String]? var embCodesTags: [String]? var stores: [String]? @@ -303,6 +304,7 @@ struct Product: Mappable { manufacturingPlaces <- map[OFFJson.ManufacturingPlacesKey] origins <- map[OFFJson.OriginsKey] labels <- (map[OFFJson.LabelsKey], ArrayTransform()) + labelsTags <- map[OFFJson.LabelsTagsKey] citiesTags <- map[OFFJson.CitiesTagsKey] // countries <- (map[OFFJson.CountriesKey], ArrayTransform()) countriesTags <- map[OFFJson.CountriesTagsKey] @@ -316,7 +318,6 @@ struct Product: Mappable { imageUrl <- map[OFFJson.ImageUrlKey] ingredientsImageUrlDecoded <- map[OFFJson.ImageIngredientsUrlKey] ingredientsListDecoded <- map[OFFJson.IngredientsKey] - labels <- (map[OFFJson.LabelsKey], ArrayTransform()) lang <- map[OFFJson.LangKey] languageCodes <- map[OFFJson.LanguageCodesKey] manufacturingPlaces <- map[OFFJson.ManufacturingPlacesKey] diff --git a/Sources/Models/API/Taxonomies/Label.swift b/Sources/Models/API/Taxonomies/Label.swift new file mode 100644 index 00000000000..416f99fa51b --- /dev/null +++ b/Sources/Models/API/Taxonomies/Label.swift @@ -0,0 +1,49 @@ +// +// Label.swift +// OpenFoodFacts +// +// Created by Alexander Scott Beaty on 8/23/20. +// + +import Foundation +import RealmSwift +import ObjectMapper + +class Label: Object { + + @objc dynamic var code = "" + + let parents = List() + let children = List() + let names = List() + + @objc dynamic var mainName = "" // name in the language of the app, for sorting + @objc dynamic var indexedNames = "" // all names concatenated, for search + + convenience init(code: String, parents: [String], children: [String], names: [Tag]) { + self.init() + self.code = code + + self.parents.removeAll() + self.parents.append(objectsIn: parents) + + self.children.removeAll() + self.children.append(objectsIn: children) + + self.names.removeAll() + self.names.append(objectsIn: names) + + self.mainName = names.chooseForCurrentLanguage()?.value ?? "" + self.indexedNames = names.map({ (tag) -> String in + return tag.languageCode.appending(":").appending(tag.value) + }).joined(separator: " ||| ") // group all names to be able to query on only one field, independently of language + } + + override static func primaryKey() -> String? { + return "code" + } + + override static func indexedProperties() -> [String] { + return ["mainName", "indexedNames"] + } +} diff --git a/Sources/Models/DataManager.swift b/Sources/Models/DataManager.swift index 9555b089bdf..e29299195ee 100644 --- a/Sources/Models/DataManager.swift +++ b/Sources/Models/DataManager.swift @@ -50,6 +50,7 @@ protocol DataManagerProtocol { func ingredientsAnalysis(forProduct product: Product) -> [IngredientsAnalysisDetail] func ingredientsAnalysis(forTag tag: String) -> IngredientsAnalysis? func ingredientsAnalysisConfig(forTag tag: String) -> IngredientsAnalysisConfig? + func label(forTag: String) -> Label? func getTagline(_ callback: @escaping (_: Tagline?) -> Void) @@ -257,6 +258,11 @@ class DataManager: DataManagerProtocol { taxonomiesApi.getTagline(callback) } + func label(forTag tag: String) -> Label? { + let myLabel = persistenceManager.label(forCode: tag) + return myLabel + } + // MARK: - Settings func addAllergy(toAllergen: Allergen) { persistenceManager.addAllergy(toAllergen: toAllergen) diff --git a/Sources/Models/PersistenceManager.swift b/Sources/Models/PersistenceManager.swift index 781cfc78f00..2c597ad750f 100644 --- a/Sources/Models/PersistenceManager.swift +++ b/Sources/Models/PersistenceManager.swift @@ -65,6 +65,10 @@ protocol PersistenceManagerProtocol { func tagLine() -> Tagline? var additivesIsEmpty: Bool { get } + func save(labels: [Label]) + func label(forCode: String) -> Label? + var labelsIsEmpty: Bool { get } + // Offline func save(offlineProducts: [RealmOfflineProduct]) func getOfflineProduct(forCode: String) -> RealmOfflineProduct? @@ -365,6 +369,20 @@ class PersistenceManager: PersistenceManagerProtocol { return getRealm().object(ofType: IngredientsAnalysisConfig.self, forPrimaryKey: code) } + func save(labels: [Label]) { + saveOrUpdate(objects: labels) + log.info("Saved \(labels.count) labels in taxonomy database") + } + + func label(forCode code: String) -> Label? { + return getRealm().object(ofType: Label.self, forPrimaryKey: code) + } + + var labelsIsEmpty: Bool { + getRealm().objects(Label.self).isEmpty + } + + // Offline Products func save(offlineProducts: [RealmOfflineProduct]) { saveOrUpdate(objects: offlineProducts) } diff --git a/Sources/Models/Transforms/TagTransform.swift b/Sources/Models/Transforms/TagTransform.swift index 46febb85e82..624fc505a15 100644 --- a/Sources/Models/Transforms/TagTransform.swift +++ b/Sources/Models/Transforms/TagTransform.swift @@ -22,7 +22,7 @@ public class Tag: Object { /// choose the most appropriate tags based on the language passed in parameters, default to english if not found static func choose(inTags tags: [Tag], forLanguageCode languageCode: String? = nil, defaultToFirst: Bool = false) -> Tag? { - let lang = languageCode ?? Bundle.main.preferredLocalizations.first ?? "en" + let lang = languageCode ?? Bundle.main.currentLocalization if let tag = tags.first(where: { (tag: Tag) -> Bool in return tag.languageCode == lang diff --git a/Sources/Network/TaxonomiesParser.swift b/Sources/Network/TaxonomiesParser.swift index a138ea168af..8036df1d203 100644 --- a/Sources/Network/TaxonomiesParser.swift +++ b/Sources/Network/TaxonomiesParser.swift @@ -106,6 +106,19 @@ struct TaxonomiesParser: TaxonomiesParserProtocol { return ingredientsAnalysisConfig } + func parseLabels(data: [String: Any]) -> [Label] { + let labels = data.compactMap({ (labelCode: String, value: Any) -> Label? in + let tags = parseTags(value: value) + let parents = parseParents(value: value) + let children = parseChildren(value: value) + return Label(code: labelCode, + parents: parents, + children: children, + names: tags) + }) + return labels + } + // MARK: - Private Helper Methods private func parseTags(value: Any) -> [Tag] { diff --git a/Sources/Network/TaxonomiesRequest.swift b/Sources/Network/TaxonomiesRequest.swift index 96b02f091b3..4b840556c4b 100644 --- a/Sources/Network/TaxonomiesRequest.swift +++ b/Sources/Network/TaxonomiesRequest.swift @@ -30,6 +30,8 @@ struct TaxonomiesRequest: URLRequestConvertible { return route.rawValue + "/data/" + Endpoint.get case (.post, _): return Endpoint.post + route.rawValue + case (.get, _): + return Endpoint.get + "/data/" + route.rawValue default: return "" } diff --git a/Sources/Network/TaxonomiesService.swift b/Sources/Network/TaxonomiesService.swift index eef34519ae0..da693ef5dc3 100644 --- a/Sources/Network/TaxonomiesService.swift +++ b/Sources/Network/TaxonomiesService.swift @@ -21,6 +21,7 @@ enum TaxonomiesRoute: String { case getMinerals = "taxonomies/minerals.json" case getNucleotides = "taxonomies/nucleotides.json" case getInvalidBarcodes = "invalid-barcodes.json" + case getLabels = "taxonomies/labels.json" } enum FilesRouter: URLRequestConvertible { @@ -357,6 +358,27 @@ class TaxonomiesService: TaxonomiesApi { } } + fileprivate func refreshLabels(_ callback: @escaping (_: Bool) -> Void) { + do { + let request = try TaxonomiesRequest(route: .getLabels, requestType: .get).asURLRequest() + Alamofire.request(request) + .responseJSON { (response) in + switch response.result { + case .success(let responseBody): + if let json = responseBody as? [String: Any] { + let labels = self.taxonomiesParser.parseLabels(data: json) + self.persistenceManager.save(labels: labels) + callback(true) + } + case .failure(let error): + AnalyticsManager.record(error: error) + callback(false) + } + } + } catch { + callback(false) + } + } // swiftlint:disable identifier_name /// increment last number each time you want to force a refresh. Useful if you add a new refresh method or a new field @@ -380,7 +402,8 @@ class TaxonomiesService: TaxonomiesApi { persistenceManager.nucleotidesIsEmpty || // persistenceManager.otherNutritionalSubstancesIsEmpty || persistenceManager.ingredientsAnalysisIsEmpty || - persistenceManager.ingredientsAnalysisConfigIsEmpty + persistenceManager.ingredientsAnalysisConfigIsEmpty || + persistenceManager.labelsIsEmpty if shouldDownload { downloadTaxonomies() } else { @@ -454,12 +477,17 @@ class TaxonomiesService: TaxonomiesApi { }) group.enter() - self.refreshInvalidBarcodes { (success) in allSuccess = allSuccess && success group.leave() } + group.enter() + self.refreshLabels({ (success) in + allSuccess = allSuccess && success + group.leave() + }) + group.wait() if allSuccess { diff --git a/Sources/Protocols/TaxonomiesParserProtocol.swift b/Sources/Protocols/TaxonomiesParserProtocol.swift index 1cee3e0c519..17640fcc6ae 100644 --- a/Sources/Protocols/TaxonomiesParserProtocol.swift +++ b/Sources/Protocols/TaxonomiesParserProtocol.swift @@ -17,4 +17,5 @@ protocol TaxonomiesParserProtocol { func parseAdditives(data: [String: Any]) -> [Additive] func parseIngredientsAnalysis(data: [String: Any]) -> [IngredientsAnalysis] func parseIngredientsAnalysisConfig(data: [String: Any]) -> [IngredientsAnalysisConfig] + func parseLabels(data: [String: Any]) -> [Label] } diff --git a/Sources/ViewControllers/Products/Detail/ProductDetailViewController.swift b/Sources/ViewControllers/Products/Detail/ProductDetailViewController.swift index 0fd810e05cb..7e4cd68df60 100644 --- a/Sources/ViewControllers/Products/Detail/ProductDetailViewController.swift +++ b/Sources/ViewControllers/Products/Detail/ProductDetailViewController.swift @@ -218,7 +218,15 @@ class ProductDetailViewController: ButtonBarPagerTabStripViewController, DataMan return NSAttributedString(string: categoryTag) }), label: InfoRowKey.categories.localizedString) - createFormRow(with: &rows, item: product.labels, label: InfoRowKey.labels.localizedString) + createFormRow(with: &rows, item: product.labelsTags?.map({ (labelTag: String) -> NSAttributedString in + if let label = dataManager.label(forTag: labelTag) { + if let name = Tag.choose(inTags: Array(label.names)) { + return NSAttributedString(string: name.value, attributes: [NSAttributedString.Key.link : OFFUrlsHelper.url(forLabel: label)]) + } + } + return NSAttributedString(string: labelTag) + }), label: InfoRowKey.labels.localizedString) + createFormRow(with: &rows, item: product.citiesTags, label: InfoRowKey.citiesTags.localizedString) createFormRow(with: &rows, item: product.embCodesTags?.map({ (tag: String) -> NSAttributedString in diff --git a/Tests/Models/PersistenceManagerMock.swift b/Tests/Models/PersistenceManagerMock.swift index e61d933a403..80244d94cbb 100644 --- a/Tests/Models/PersistenceManagerMock.swift +++ b/Tests/Models/PersistenceManagerMock.swift @@ -256,6 +256,13 @@ class PersistenceManagerMock: PersistenceManagerProtocol { return nil } + func save(labels: [Label]) { + } + + func label(forCode: String) -> Label? { + return nil + } + func save(offlineProducts: [RealmOfflineProduct]) { }