deep-linking-sample/Apps/Wikipedia/WMF Framework/MWKDataStore+LanguageVariantMigration.swift
Javier Cicchelli 9bcdaa697b [Setup] Basic project structure (#1)
This PR contains all the work related to setting up this project as required to implement the [Assignment](https://repo.rock-n-code.com/rock-n-code/deep-linking-assignment/wiki/Assignment) on top, as intended.

To summarise this work:
- [x] created a new **Xcode** project;
- [x] cloned the `Wikipedia` app and inserted it into the **Xcode** project;
- [x] created the `Locations` app and also, its `Libraries` package;
- [x] created the `Shared` package to share dependencies between the apps;
- [x] added a `Makefile` file and implemented some **environment** and **help** commands.

Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
Reviewed-on: rock-n-code/deep-linking-assignment#1
2023-04-08 18:37:13 +00:00

203 lines
10 KiB
Swift

import Foundation
import CocoaLumberjackSwift
/* Whenever a language that previously did not have variants becomes a language with variants, a migration must happen.
*
* There are two parts to the migration:
* 1. Updating persisted items like settings and Core Data records
* 2. Presenting alerts to the user to make them aware of the new variants
*
* 1. Migrating persistent items
* The first part of this process updates the various settings and user defaults that reference languages and ensure
* that the correct language variant is set. So, a value such as "zh" for Chinese is replaced with a variant such as "zh-hans".
*
* Note that once a language is converted, the 'plain' language code is a valid value meaning to use the 'mixed'
* content for that site. This is the content as entered into that site without converting to any variant.
* Because the plain language code means one thing before migration (the language itself) and another thing after
* migration (the mixed or untransformed variant of the language), migration should only happen once for a given
* language.
*
* If additional languages add variants in the future, a new library version should be used and a new entry mapping the
* library version to the newly variant-aware languages should be added to -newlyAddedVariantLanguageCodes(for:).
* The migration code itself should call migrateToLanguageVariants(for:in:) with the new library version.
*
*
* 2. Presenting informational alerts
* When migrating to use language variants, if the user's preferred languages include languages which have
* received variant support, an alert is presented to tell the user about variants. An alert is only presented for
* newly variant-aware languges that also are preferred languages of the user. Multiple alerts shown
* sequentially are possible, but expected to be rare.
*
* The method languageCodesNeedingVariantAlerts(since:) returns all language codes requiring a variant alert since the
* provided library version. Note that while migrating data is done library version by library version, this API can handle
* multiple library version updates at once. Also note that the method only returns those language codes that are also
* in the user's list of preferred languages. So, it is expected that this method will return an empty array for a user
* with no variant-aware languages in their list of preferred languages.
*/
extension MWKDataStore {
@objc(migrateToLanguageVariantsForLibraryVersion:inManagedObjectContext:)
public func migrateToLanguageVariants(for libraryVersion: Int, in moc: NSManagedObjectContext) {
let languageCodes = newlyAddedVariantLanguageCodes(for: libraryVersion)
// Map all languages with variants being migrated to the user's preferred variant
// Note that even if the user does not have any preferred languages that match,
// the user could have chosen to read or save an article in any language.
// The variant is therefore determined for all langauges being migrated.
let migrationMapping = languageCodes.reduce(into: [String:String]()) { (result, languageCode) in
guard let languageVariantCode = NSLocale.wmf_bestLanguageVariantCodeForLanguageCode(languageCode) else {
assertionFailure("No variant found for language code \(languageCode). Every language migrating to use language variants should return a language variant code")
return
}
result[languageCode] = languageVariantCode
}
// Ensure any settings that currently use 'nb' are updated to use 'no'
var languageCodeMigrationMapping = migrationMapping
languageCodeMigrationMapping["nb"] = "no"
languageLinkController.migratePreferredLanguages(toLanguageVariants: languageCodeMigrationMapping, in: moc)
feedContentController.migrateExploreFeedSettings(toLanguageVariants: languageCodeMigrationMapping, in: moc)
migrateSearchLanguageSetting(toLanguageVariants: languageCodeMigrationMapping)
migrateWikipediaEntities(toLanguageVariants: migrationMapping, in: moc)
}
private func migrateSearchLanguageSetting(toLanguageVariants languageMapping: [String:String]) {
let defaults = UserDefaults.standard
if let url = defaults.url(forKey: WMFSearchURLKey),
let languageCode = url.wmf_languageCode {
let searchLanguageCode = languageMapping[languageCode] ?? languageCode
defaults.wmf_setCurrentSearchContentLanguageCode(searchLanguageCode)
defaults.removeObject(forKey: WMFSearchURLKey)
}
}
private func migrateWikipediaEntities(toLanguageVariants languageMapping: [String:String], in moc: NSManagedObjectContext) {
for (languageCode, languageVariantCode) in languageMapping {
guard let siteURLString = NSURL.wmf_URL(withDefaultSiteAndLanguageCode: languageCode)?.wmf_databaseKey else {
assertionFailure("Could not create URL from language code: '\(languageCode)'")
continue
}
do {
// Update ContentGroups
let contentGroupFetchRequest: NSFetchRequest<WMFContentGroup> = WMFContentGroup.fetchRequest()
contentGroupFetchRequest.predicate = NSPredicate(format: "siteURLString == %@", siteURLString)
let groups = try moc.fetch(contentGroupFetchRequest)
for group in groups {
group.variant = languageVariantCode
}
// Update Talk Pages
let talkPageFetchRequest: NSFetchRequest<TalkPage> = TalkPage.fetchRequest()
talkPageFetchRequest.predicate = NSPredicate(format: "key BEGINSWITH %@", siteURLString)
let talkPages = try moc.fetch(talkPageFetchRequest)
for talkPage in talkPages {
talkPage.variant = languageVariantCode
talkPage.forceRefresh = true
}
// Update Articles and Gather Keys
var articleKeys: Set<String> = []
let articleFetchRequest: NSFetchRequest<WMFArticle> = WMFArticle.fetchRequest()
articleFetchRequest.predicate = NSPredicate(format: "key BEGINSWITH %@", siteURLString)
let articles = try moc.fetch(articleFetchRequest)
for article in articles {
article.variant = languageVariantCode
if let key = article.key {
articleKeys.insert(key)
}
}
// Update Reading List Entries
let entryFetchRequest: NSFetchRequest<ReadingListEntry> = ReadingListEntry.fetchRequest()
entryFetchRequest.predicate = NSPredicate(format: "articleKey IN %@", articleKeys)
let entries = try moc.fetch(entryFetchRequest)
for entry in entries {
entry.variant = languageVariantCode
}
} catch let error {
DDLogError("Error migrating articles to variant '\(languageVariantCode)': \(error)")
}
}
if moc.hasChanges {
do {
try moc.save()
} catch let error {
DDLogError("Error saving articles and readling list entry variant migrations: \(error)")
}
}
}
// Returns any array of language codes of any of the user's preferred languages that have
// added variant support since the indicated library version. For each language, the user
// will be informed of variant support for that language via an alert
@objc public func languageCodesNeedingVariantAlerts(since libraryVersion: Int) -> [String] {
let addedVariantLanguageCodes = allAddedVariantLanguageCodes(since: libraryVersion)
guard !addedVariantLanguageCodes.isEmpty else {
return []
}
var uniqueLanguageCodes: Set<String> = []
return languageLinkController.preferredLanguages
.map { $0.languageCode }
.filter { addedVariantLanguageCodes.contains($0) }
.filter { uniqueLanguageCodes.insert($0).inserted }
}
// Returns an array of language codes for all languages that have added variant support
// since the indicated library version. Used to determine all language codes that might
// need to have an alert presented to inform the user about the added variant support
private func allAddedVariantLanguageCodes(since libraryVersion: Int) -> [String] {
guard libraryVersion < MWKDataStore.currentLibraryVersion else {
return []
}
var languageCodes: [String] = []
for version in libraryVersion...MWKDataStore.currentLibraryVersion {
languageCodes.append(contentsOf: newlyAddedVariantLanguageCodes(for: version))
}
return languageCodes
}
// Returns the language codes for any languages that have added variant support in that library version.
// Returns an empty array if no languages added variant support
private func newlyAddedVariantLanguageCodes(for libraryVersion: Int) -> [String] {
switch libraryVersion {
case 12: return ["crh", "gan", "iu", "kk", "ku", "sr", "tg", "uz", "zh"]
default: return []
}
}
}
public extension TalkPage {
var forceRefresh: Bool {
get {
guard let revisionID = self.revisionId?.intValue else {
return false
}
return UserDefaults.standard.talkPageForceRefreshRevisionIDs?.contains(revisionID) ?? false
}
set {
guard let revisionID = self.revisionId?.intValue else {
assertionFailure("Attempting to set forceRefresh on a talk page that has no revisionID.")
return
}
var revisionIDs = UserDefaults.standard.talkPageForceRefreshRevisionIDs ?? Set<Int>()
if newValue == true {
revisionIDs.insert(revisionID)
} else {
revisionIDs.remove(revisionID)
}
UserDefaults.standard.talkPageForceRefreshRevisionIDs = revisionIDs
}
}
}