manage rank file update check

clubs
Raz 1 year ago
parent 40762f90b2
commit 5d7a81f7ef
  1. 4
      PadelClub.xcodeproj/project.pbxproj
  2. 17
      PadelClub/Data/MonthData.swift
  3. 2
      PadelClub/Data/PlayerRegistration.swift
  4. 2
      PadelClub/Data/TeamRegistration.swift
  5. 2
      PadelClub/Extensions/Date+Extensions.swift
  6. 37
      PadelClub/Extensions/URL+Extensions.swift
  7. 2
      PadelClub/Utils/FileImportManager.swift
  8. 43
      PadelClub/Utils/Network/NetworkManager.swift
  9. 2
      PadelClub/Utils/Network/NetworkManagerError.swift
  10. 16
      PadelClub/Utils/SourceFileManager.swift
  11. 2
      PadelClub/Views/Components/StepperView.swift
  12. 29
      PadelClub/Views/Navigation/MainView.swift
  13. 10
      PadelClub/Views/Navigation/Umpire/PadelClubView.swift

@ -1939,7 +1939,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 6; CURRENT_PROJECT_VERSION = 7;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
@ -1989,7 +1989,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 6; CURRENT_PROJECT_VERSION = 7;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;

@ -19,7 +19,7 @@ final class MonthData : ModelObject, Storable {
private(set) var id: String = Store.randomId() private(set) var id: String = Store.randomId()
private(set) var monthKey: String private(set) var monthKey: String
var creationDate: Date private(set) var creationDate: Date
var maleUnrankedValue: Int? = nil var maleUnrankedValue: Int? = nil
var femaleUnrankedValue: Int? = nil var femaleUnrankedValue: Int? = nil
var maleCount: Int? = nil var maleCount: Int? = nil
@ -32,6 +32,10 @@ final class MonthData : ModelObject, Storable {
self.creationDate = Date() self.creationDate = Date()
} }
fileprivate func _updateCreationDate() {
self.creationDate = Date()
}
required init(from decoder: Decoder) throws { required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self) let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: ._id) id = try container.decode(String.self, forKey: ._id)
@ -52,8 +56,11 @@ final class MonthData : ModelObject, Storable {
static func calculateCurrentUnrankedValues(fromDate: Date) async { 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 incompleteMode = fftImportingUncomplete != nil
let lastDataSourceMaleUnranked = await FederalPlayer.lastRank(mostRecentDateAvailable: fromDate, man: true) let lastDataSourceMaleUnranked = await FederalPlayer.lastRank(mostRecentDateAvailable: fromDate, man: true)
@ -62,8 +69,8 @@ final class MonthData : ModelObject, Storable {
await MainActor.run { await MainActor.run {
let lastDataSource = URL.importDateFormatter.string(from: fromDate) let lastDataSource = URL.importDateFormatter.string(from: fromDate)
let currentMonthData : MonthData = DataStore.shared.monthData.first(where: { $0.monthKey == lastDataSource }) ?? MonthData(monthKey: lastDataSource) let currentMonthData : MonthData = DataStore.shared.monthData.first(where: { $0.monthKey == lastDataSource }) ?? MonthData(monthKey: lastDataSource)
currentMonthData.creationDate = Date() currentMonthData._updateCreationDate()
currentMonthData.maleUnrankedValue = incompleteMode ? 60000 : lastDataSourceMaleUnranked?.0 currentMonthData.maleUnrankedValue = incompleteMode ? fftImportingMaleUnrankValue : lastDataSourceMaleUnranked?.0
currentMonthData.incompleteMode = incompleteMode currentMonthData.incompleteMode = incompleteMode
currentMonthData.maleCount = incompleteMode ? fftImportingUncomplete : lastDataSourceMaleUnranked?.1 currentMonthData.maleCount = incompleteMode ? fftImportingUncomplete : lastDataSourceMaleUnranked?.1
currentMonthData.femaleUnrankedValue = lastDataSourceFemaleUnranked?.0 currentMonthData.femaleUnrankedValue = lastDataSourceFemaleUnranked?.0

@ -281,7 +281,7 @@ final class PlayerRegistration: ModelObject, Storable {
} }
func setComputedRank(in tournament: Tournament) { 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 { switch tournament.tournamentCategory {
case .men: case .men:
computedRank = isMalePlayer() ? currentRank : currentRank + PlayerRegistration.addon(for: currentRank, manMax: tournament.maleUnrankedValue ?? 0, womanMax: tournament.femaleUnrankedValue ?? 0) computedRank = isMalePlayer() ? currentRank : currentRank + PlayerRegistration.addon(for: currentRank, manMax: tournament.maleUnrankedValue ?? 0, womanMax: tournament.femaleUnrankedValue ?? 0)

@ -478,7 +478,7 @@ final class TeamRegistration: ModelObject, Storable {
} }
func unrankValue(for malePlayer: Bool) -> Int { func unrankValue(for malePlayer: Bool) -> Int {
return tournamentObject()?.unrankValue(for: malePlayer) ?? 100_000 return tournamentObject()?.unrankValue(for: malePlayer) ?? 70_000
} }
func groupStageObject() -> GroupStage? { func groupStageObject() -> GroupStage? {

@ -215,7 +215,7 @@ extension Date {
extension Date { extension Date {
func isEarlierThan(_ date: Date) -> Bool { func isEarlierThan(_ date: Date) -> Bool {
self < date Calendar.current.compare(self, to: date, toGranularity: .minute) == .orderedAscending
} }
} }

@ -51,6 +51,43 @@ extension URL {
} }
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? { func fftImportingUncomplete() -> Int? {
// Read the contents of the file // Read the contents of the file
guard let fileContents = try? String(contentsOfFile: path(), encoding: .utf8) else { guard let fileContents = try? String(contentsOfFile: path(), encoding: .utf8) else {

@ -147,7 +147,7 @@ class FileImportManager {
} }
let significantPlayerCount = 2 let significantPlayerCount = 2
let pl = players.prefix(significantPlayerCount).map { $0.computedRank } 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,+) self.weight = pl.reduce(0,+) + missingPl.reduce(0,+)
} else { } else {
self.weight = players.map { $0.computedRank }.reduce(0,+) self.weight = players.map { $0.computedRank }.reduce(0,+)

@ -18,6 +18,32 @@ class NetworkManager {
try? FileManager.default.removeItem(at: destinationFileUrl) 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 { func downloadRankingData(lastDateString: String, fileName: String) async throws {
let dateString = ["CLASSEMENT-PADEL", fileName, lastDateString].joined(separator: "-") + ".csv" let dateString = ["CLASSEMENT-PADEL", fileName, lastDateString].joined(separator: "-") + ".csv"
@ -26,15 +52,16 @@ class NetworkManager {
let destinationFileUrl = documentsUrl.appendingPathComponent("\(dateString)") let destinationFileUrl = documentsUrl.appendingPathComponent("\(dateString)")
let fileURL = URL(string: "https://xlr.alwaysdata.net/static/rankings/\(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!) var request = URLRequest(url:fileURL!)
request.addValue("attachment;filename=\(dateString)", forHTTPHeaderField:"Content-Disposition") 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") request.addValue("text/csv", forHTTPHeaderField: "Content-Type")
let task = try await URLSession.shared.download(for: request) let task = try await URLSession.shared.download(for: request)
if let urlResponse = task.1 as? HTTPURLResponse { if let urlResponse = task.1 as? HTTPURLResponse {
print(dateString, urlResponse.statusCode)
if urlResponse.statusCode == 200 { 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 //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) try FileManager.default.copyItem(at: task.0, to: destinationFileUrl)
print("dl rank data ok", lastDateString, fileName) print("dl rank data ok", lastDateString, fileName)
} else if urlResponse.statusCode == 404 && fileName == "MESSIEURS" { } 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 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)
} }
} }
} }

@ -14,6 +14,8 @@ enum NetworkManagerError: LocalizedError {
case mailNotSent //no network no error case mailNotSent //no network no error
case messageFailed case messageFailed
case messageNotSent //no network no error case messageNotSent //no network no error
case fileNotModified
case fileNotDownloaded(Int)
var errorDescription: String? { var errorDescription: String? {
switch self { switch self {

@ -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 lastStringDate = URL.importDateFormatter.string(from: current)
let files = ["MESSIEURS", "MESSIEURS-2", "MESSIEURS-3", "MESSIEURS-4", "DAMES"] let files = ["MESSIEURS", "MESSIEURS-2", "MESSIEURS-3", "MESSIEURS-4", "DAMES"]
@ -101,6 +102,7 @@ class SourceFileManager {
// await fetchData(fromDate: nextCurrent) // await fetchData(fromDate: nextCurrent)
// } // }
// } // }
return true
} catch { } catch {
print("downloadRankingData", error) print("downloadRankingData", error)
@ -109,6 +111,8 @@ class SourceFileManager {
await fetchData(fromDate: previousDate) await fetchData(fromDate: previousDate)
} }
} }
return false
} }
} }
@ -180,7 +184,7 @@ class SourceFileManager {
url.pathExtension == "csv" 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] { func allFiles(_ isManPlayer: Bool) -> [URL] {
@ -192,6 +196,14 @@ class SourceFileManager {
func allFilesSortedByDate(_ isManPlayer: Bool) -> [URL] { func allFilesSortedByDate(_ isManPlayer: Bool) -> [URL] {
return allFiles(isManPlayer) 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 { enum SourceFile: String, CaseIterable {

@ -67,7 +67,7 @@ struct StepperView: View {
} }
fileprivate func _plusIsDisabled() -> Bool { fileprivate func _plusIsDisabled() -> Bool {
count >= (maximum ?? 100_000) count >= (maximum ?? 70_000)
} }
fileprivate func _add() { fileprivate func _add() {

@ -231,26 +231,37 @@ struct MainView: View {
} }
private func _checkingDataIntegrity() { 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 { Task {
await self._checkSourceFileAvailability() 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 { Task {
await self._checkSourceFileAvailability() 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) 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 { Task {
await _checkSourceFileAvailability() 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)
}
}
}
} }
} }
} }

@ -39,7 +39,11 @@ struct PadelClubView: View {
if let currentMonth = monthData.first, currentMonth.incompleteMode { if let currentMonth = monthData.first, currentMonth.incompleteMode {
Section { 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.") 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 { LabeledContent {
if let maleUnrankedValue = monthData.maleUnrankedValue { if let maleUnrankedValue = monthData.maleUnrankedValue {
Text(maleUnrankedValue.formatted()) Text(maleUnrankedValue.formatted())
} else {
Text(70_000.formatted())
} }
} label: { } label: {
Text("Rang d'un non classé") Text("Rang d'un non classé")
if monthData.incompleteMode { if monthData.incompleteMode && monthData.maleUnrankedValue == nil {
Text("Messieurs (estimation car incomplet)") Text("Messieurs (estimation car incomplet)")
} else { } else {
Text("Messieurs") Text("Messieurs")

Loading…
Cancel
Save