diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 5e28c4c..64a33c4 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -1939,7 +1939,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 7; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -1989,7 +1989,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 7; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; diff --git a/PadelClub/Data/MonthData.swift b/PadelClub/Data/MonthData.swift index c1e8137..9be70f5 100644 --- a/PadelClub/Data/MonthData.swift +++ b/PadelClub/Data/MonthData.swift @@ -19,7 +19,7 @@ final class MonthData : ModelObject, Storable { private(set) var id: String = Store.randomId() private(set) var monthKey: String - var creationDate: Date + private(set) var creationDate: Date var maleUnrankedValue: Int? = nil var femaleUnrankedValue: Int? = nil var maleCount: Int? = nil @@ -32,6 +32,10 @@ final class MonthData : ModelObject, Storable { self.creationDate = Date() } + fileprivate func _updateCreationDate() { + self.creationDate = Date() + } + required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(String.self, forKey: ._id) @@ -52,8 +56,11 @@ final class MonthData : ModelObject, Storable { static func calculateCurrentUnrankedValues(fromDate: Date) async { - let fftImportingUncomplete = SourceFileManager.shared.allFiles(true).first(where: { $0.dateFromPath == fromDate })?.fftImportingUncomplete() - + let fileURL = SourceFileManager.shared.allFiles(true).first(where: { $0.dateFromPath == fromDate && $0.index == 0 }) + print("calculateCurrentUnrankedValues", fromDate.monthYearFormatted, fileURL?.path()) + let fftImportingUncomplete = fileURL?.fftImportingUncomplete() + let fftImportingMaleUnrankValue = fileURL?.fftImportingMaleUnrankValue() + let incompleteMode = fftImportingUncomplete != nil let lastDataSourceMaleUnranked = await FederalPlayer.lastRank(mostRecentDateAvailable: fromDate, man: true) @@ -62,8 +69,8 @@ final class MonthData : ModelObject, Storable { await MainActor.run { let lastDataSource = URL.importDateFormatter.string(from: fromDate) let currentMonthData : MonthData = DataStore.shared.monthData.first(where: { $0.monthKey == lastDataSource }) ?? MonthData(monthKey: lastDataSource) - currentMonthData.creationDate = Date() - currentMonthData.maleUnrankedValue = incompleteMode ? 60000 : lastDataSourceMaleUnranked?.0 + currentMonthData._updateCreationDate() + currentMonthData.maleUnrankedValue = incompleteMode ? fftImportingMaleUnrankValue : lastDataSourceMaleUnranked?.0 currentMonthData.incompleteMode = incompleteMode currentMonthData.maleCount = incompleteMode ? fftImportingUncomplete : lastDataSourceMaleUnranked?.1 currentMonthData.femaleUnrankedValue = lastDataSourceFemaleUnranked?.0 diff --git a/PadelClub/Data/PlayerRegistration.swift b/PadelClub/Data/PlayerRegistration.swift index 8746cb3..91dec26 100644 --- a/PadelClub/Data/PlayerRegistration.swift +++ b/PadelClub/Data/PlayerRegistration.swift @@ -281,7 +281,7 @@ final class PlayerRegistration: ModelObject, Storable { } func setComputedRank(in tournament: Tournament) { - let currentRank = rank ?? tournament.unrankValue(for: isMalePlayer()) ?? 100_000 + let currentRank = rank ?? tournament.unrankValue(for: isMalePlayer()) ?? 70_000 switch tournament.tournamentCategory { case .men: computedRank = isMalePlayer() ? currentRank : currentRank + PlayerRegistration.addon(for: currentRank, manMax: tournament.maleUnrankedValue ?? 0, womanMax: tournament.femaleUnrankedValue ?? 0) diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index cb298b8..c5db5d1 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -478,7 +478,7 @@ final class TeamRegistration: ModelObject, Storable { } func unrankValue(for malePlayer: Bool) -> Int { - return tournamentObject()?.unrankValue(for: malePlayer) ?? 100_000 + return tournamentObject()?.unrankValue(for: malePlayer) ?? 70_000 } func groupStageObject() -> GroupStage? { diff --git a/PadelClub/Extensions/Date+Extensions.swift b/PadelClub/Extensions/Date+Extensions.swift index 1a31a36..2758f48 100644 --- a/PadelClub/Extensions/Date+Extensions.swift +++ b/PadelClub/Extensions/Date+Extensions.swift @@ -215,7 +215,7 @@ extension Date { extension Date { func isEarlierThan(_ date: Date) -> Bool { - self < date + Calendar.current.compare(self, to: date, toGranularity: .minute) == .orderedAscending } } diff --git a/PadelClub/Extensions/URL+Extensions.swift b/PadelClub/Extensions/URL+Extensions.swift index 4f5b92d..5cddc1b 100644 --- a/PadelClub/Extensions/URL+Extensions.swift +++ b/PadelClub/Extensions/URL+Extensions.swift @@ -51,6 +51,43 @@ extension URL { } extension URL { + func creationDate() -> Date? { + // Use FileManager to retrieve the file attributes + do { + let fileAttributes = try FileManager.default.attributesOfItem(atPath: self.path()) + + // Access the creationDate from the file attributes + if let creationDate = fileAttributes[.creationDate] as? Date { + print("File creationDate: \(creationDate)") + return creationDate + } else { + print("creationDate not found.") + } + } catch { + print("Error retrieving file attributes: \(error.localizedDescription)") + } + + return nil + } + + func fftImportingMaleUnrankValue() -> Int? { + // Read the contents of the file + guard let fileContents = try? String(contentsOfFile: path(), encoding: .utf8) else { + return nil + } + + // Split the contents by newline characters + let lines = fileContents.components(separatedBy: .newlines) + + if let line = lines.first(where: { + $0.hasPrefix("unrank-male-value:") + }) { + return Int(line.replacingOccurrences(of: "unrank-male-value:", with: "")) + } + + return nil + } + func fftImportingUncomplete() -> Int? { // Read the contents of the file guard let fileContents = try? String(contentsOfFile: path(), encoding: .utf8) else { diff --git a/PadelClub/Utils/FileImportManager.swift b/PadelClub/Utils/FileImportManager.swift index b888f95..7ee5ced 100644 --- a/PadelClub/Utils/FileImportManager.swift +++ b/PadelClub/Utils/FileImportManager.swift @@ -147,7 +147,7 @@ class FileImportManager { } let significantPlayerCount = 2 let pl = players.prefix(significantPlayerCount).map { $0.computedRank } - let missingPl = (missing.map { tournament.unrankValue(for: $0 == 1 ? true : false ) ?? ($0 == 1 ? 100_000 : 10_000) }).prefix(significantPlayerCount) + let missingPl = (missing.map { tournament.unrankValue(for: $0 == 1 ? true : false ) ?? ($0 == 1 ? 70_000 : 10_000) }).prefix(significantPlayerCount) self.weight = pl.reduce(0,+) + missingPl.reduce(0,+) } else { self.weight = players.map { $0.computedRank }.reduce(0,+) diff --git a/PadelClub/Utils/Network/NetworkManager.swift b/PadelClub/Utils/Network/NetworkManager.swift index a86596b..914adb1 100644 --- a/PadelClub/Utils/Network/NetworkManager.swift +++ b/PadelClub/Utils/Network/NetworkManager.swift @@ -18,6 +18,32 @@ class NetworkManager { try? FileManager.default.removeItem(at: destinationFileUrl) } +// func headerDataRankingData(lastDateString: String, fileName: String) async throws { +// let dateString = ["CLASSEMENT-PADEL", fileName, lastDateString].joined(separator: "-") + ".csv" +// +// let documentsUrl: URL = SourceFileManager.shared.rankingSourceDirectory +// let destinationFileUrl = documentsUrl.appendingPathComponent("\(dateString)") +// let fileURL = URL(string: "https://xlr.alwaysdata.net/static/rankings/\(dateString)") +// +// var request = URLRequest(url:fileURL!) +// request.httpMethod = "HEAD" +// request.addValue("attachment;filename=\(dateString)", forHTTPHeaderField:"Content-Disposition") +// request.addValue("text/csv", forHTTPHeaderField: "Content-Type") +// let task = try await URLSession.shared.dataTask(with: request) +// if let urlResponse = task.1 as? HTTPURLResponse { +// print(urlResponse.allHeaderFields) +// } +// } +// + func formatDateForHTTPHeader(_ date: Date) -> String { + let formatter = DateFormatter() + formatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss 'GMT'" + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.timeZone = TimeZone(secondsFromGMT: 0) // GMT timezone + + return formatter.string(from: date) + } + func downloadRankingData(lastDateString: String, fileName: String) async throws { let dateString = ["CLASSEMENT-PADEL", fileName, lastDateString].joined(separator: "-") + ".csv" @@ -26,15 +52,16 @@ class NetworkManager { let destinationFileUrl = documentsUrl.appendingPathComponent("\(dateString)") let fileURL = URL(string: "https://xlr.alwaysdata.net/static/rankings/\(dateString)") - if FileManager.default.fileExists(atPath: destinationFileUrl.path()) { - return - } var request = URLRequest(url:fileURL!) request.addValue("attachment;filename=\(dateString)", forHTTPHeaderField:"Content-Disposition") + if FileManager.default.fileExists(atPath: destinationFileUrl.path()), let modificationDate = destinationFileUrl.creationDate() { + request.addValue(formatDateForHTTPHeader(modificationDate), forHTTPHeaderField: "If-Modified-Since") + } request.addValue("text/csv", forHTTPHeaderField: "Content-Type") let task = try await URLSession.shared.download(for: request) if let urlResponse = task.1 as? HTTPURLResponse { + print(dateString, urlResponse.statusCode) if urlResponse.statusCode == 200 { //todo à voir si on en a besoin, permet de re-télécharger un csv si on détecte qu'il a été mis à jour @@ -47,12 +74,18 @@ class NetworkManager { // } // } // } - + try? FileManager.default.removeItem(at: destinationFileUrl) try FileManager.default.copyItem(at: task.0, to: destinationFileUrl) print("dl rank data ok", lastDateString, fileName) } else if urlResponse.statusCode == 404 && fileName == "MESSIEURS" { - print("dl rank data failed", lastDateString, fileName) + print("dl rank data failedm fileNotYetAvailable", lastDateString, fileName) throw NetworkManagerError.fileNotYetAvailable + } else if urlResponse.statusCode == 304 { + print("dl rank data failed, fileNotModified", lastDateString, fileName) + throw NetworkManagerError.fileNotModified + } else { + print("dl rank data failed, fileNotDownloaded", lastDateString, fileName, urlResponse.statusCode) + throw NetworkManagerError.fileNotDownloaded(urlResponse.statusCode) } } } diff --git a/PadelClub/Utils/Network/NetworkManagerError.swift b/PadelClub/Utils/Network/NetworkManagerError.swift index 4b3eb5b..970af60 100644 --- a/PadelClub/Utils/Network/NetworkManagerError.swift +++ b/PadelClub/Utils/Network/NetworkManagerError.swift @@ -14,6 +14,8 @@ enum NetworkManagerError: LocalizedError { case mailNotSent //no network no error case messageFailed case messageNotSent //no network no error + case fileNotModified + case fileNotDownloaded(Int) var errorDescription: String? { switch self { diff --git a/PadelClub/Utils/SourceFileManager.swift b/PadelClub/Utils/SourceFileManager.swift index c9ca7c7..0f7bf05 100644 --- a/PadelClub/Utils/SourceFileManager.swift +++ b/PadelClub/Utils/SourceFileManager.swift @@ -80,7 +80,8 @@ class SourceFileManager { } } - func fetchData(fromDate current: Date) async { + @discardableResult + func fetchData(fromDate current: Date) async -> Bool { let lastStringDate = URL.importDateFormatter.string(from: current) let files = ["MESSIEURS", "MESSIEURS-2", "MESSIEURS-3", "MESSIEURS-4", "DAMES"] @@ -101,6 +102,7 @@ class SourceFileManager { // await fetchData(fromDate: nextCurrent) // } // } + return true } catch { print("downloadRankingData", error) @@ -109,6 +111,8 @@ class SourceFileManager { await fetchData(fromDate: previousDate) } } + + return false } } @@ -180,7 +184,7 @@ class SourceFileManager { url.pathExtension == "csv" }) - return (allFiles + (Bundle.main.urls(forResourcesWithExtension: "csv", subdirectory: nil) ?? [])).sorted(by: \.dateFromPath).reversed() + return (allFiles + (Bundle.main.urls(forResourcesWithExtension: "csv", subdirectory: nil) ?? [])).sorted { $0.dateFromPath == $1.dateFromPath ? $0.index < $1.index : $0.dateFromPath > $1.dateFromPath } } func allFiles(_ isManPlayer: Bool) -> [URL] { @@ -192,6 +196,14 @@ class SourceFileManager { func allFilesSortedByDate(_ isManPlayer: Bool) -> [URL] { return allFiles(isManPlayer) } + + static func isDateAfterUrlImportDate(date: Date, dateString: String) -> Bool { + guard let importDate = URL.importDateFormatter.date(from: dateString) else { + return false + } + + return importDate.isEarlierThan(date) + } } enum SourceFile: String, CaseIterable { diff --git a/PadelClub/Views/Components/StepperView.swift b/PadelClub/Views/Components/StepperView.swift index 638ec99..23f3108 100644 --- a/PadelClub/Views/Components/StepperView.swift +++ b/PadelClub/Views/Components/StepperView.swift @@ -67,7 +67,7 @@ struct StepperView: View { } fileprivate func _plusIsDisabled() -> Bool { - count >= (maximum ?? 100_000) + count >= (maximum ?? 70_000) } fileprivate func _add() { diff --git a/PadelClub/Views/Navigation/MainView.swift b/PadelClub/Views/Navigation/MainView.swift index dc87d3b..5db1726 100644 --- a/PadelClub/Views/Navigation/MainView.swift +++ b/PadelClub/Views/Navigation/MainView.swift @@ -231,26 +231,37 @@ struct MainView: View { } private func _checkingDataIntegrity() { - if lastDataSource == nil, importObserver.checkingFiles == false, importObserver.isImportingFile() == false { + guard importObserver.checkingFiles == false, importObserver.isImportingFile() == false else { + return + } + if lastDataSource == nil { Task { await self._checkSourceFileAvailability() } - } else if let lastDataSource, lastDataSource != URL.importDateFormatter.string(from: Date()), importObserver.checkingFiles == false, importObserver.isImportingFile() == false { + } else if let lastDataSource, lastDataSource != URL.importDateFormatter.string(from: Date()) { Task { await self._checkSourceFileAvailability() } - } else if let lastDataSource, lastDataSource == "08-2024" { + } else if let lastDataSource, let mostRecentDateImported = URL.importDateFormatter.date(from: lastDataSource), SourceFileManager.isDateAfterUrlImportDate(date:mostRecentDateImported, dateString: "07-2024") { + let monthData = dataStore.monthData.sorted(by: \.creationDate) - if let current = monthData.last, current.monthKey == "08-2024", current.incompleteMode == false { - Task { - await _calculateMonthData(dataSource: current.monthKey) - } - } - if monthData.first(where: { $0.monthKey == "07-2024" }) == nil, importObserver.isImportingFile() == false, importObserver.checkingFiles == false { + if monthData.first(where: { $0.monthKey == "07-2024" }) == nil { Task { await _checkSourceFileAvailability() } + } else { + if let current = monthData.last { + Task { + let updated = await SourceFileManager.shared.fetchData(fromDate: mostRecentDateImported) + print("file updated", updated) + if updated { + await _startImporting(importingDate: mostRecentDateImported) + } else if current.incompleteMode == false { + await _calculateMonthData(dataSource: current.monthKey) + } + } + } } } } diff --git a/PadelClub/Views/Navigation/Umpire/PadelClubView.swift b/PadelClub/Views/Navigation/Umpire/PadelClubView.swift index ef8f31c..ea4cb32 100644 --- a/PadelClub/Views/Navigation/Umpire/PadelClubView.swift +++ b/PadelClub/Views/Navigation/Umpire/PadelClubView.swift @@ -39,7 +39,11 @@ struct PadelClubView: View { if let currentMonth = monthData.first, currentMonth.incompleteMode { Section { - Text("Attention, depuis Août 2024, les données fédérales publiques des joueurs (messieurs) récupérables sont incomplètes car limité au 40.000 premiers joueurs. Le rang d'un joueur non-classé n'est donc pas calculable pour le moment, Padel Club utilisera une valeur par défaut de de 60.000.") + Text("Attention, depuis Août 2024, les données fédérales publiques des joueurs (messieurs) récupérables sont incomplètes car limité au 40.000 premiers joueurs.") + + if currentMonth.maleUnrankedValue == nil { + Text("Le rang d'un joueur non-classé n'est donc pas calculable pour le moment, Padel Club utilisera une valeur par défaut de de 70.000.") + } Text("Un classement souligné comme ci-dessous indiquera que l'information provient d'un mois précédent.") @@ -158,10 +162,12 @@ struct PadelClubView: View { LabeledContent { if let maleUnrankedValue = monthData.maleUnrankedValue { Text(maleUnrankedValue.formatted()) + } else { + Text(70_000.formatted()) } } label: { Text("Rang d'un non classé") - if monthData.incompleteMode { + if monthData.incompleteMode && monthData.maleUnrankedValue == nil { Text("Messieurs (estimation car incomplet)") } else { Text("Messieurs")