Merge branch 'main'

Conflicts:
	PadelClub.xcodeproj/project.pbxproj
clubs
Raz 1 year ago
commit 70f398e226
  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. 56
      PadelClub/Extensions/URL+Extensions.swift
  7. 2
      PadelClub/Utils/FileImportManager.swift
  8. 48
      PadelClub/Utils/Network/NetworkManager.swift
  9. 2
      PadelClub/Utils/Network/NetworkManagerError.swift
  10. 41
      PadelClub/Utils/SourceFileManager.swift
  11. 2
      PadelClub/Views/Components/StepperView.swift
  12. 36
      PadelClub/Views/Navigation/MainView.swift
  13. 30
      PadelClub/Views/Navigation/Umpire/PadelClubView.swift
  14. 37
      PadelClub/Views/Tournament/Screen/AddTeamView.swift

@ -1939,7 +1939,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2;
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 = 2;
CURRENT_PROJECT_VERSION = 7;
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6;

@ -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

@ -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)

@ -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? {

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

@ -51,6 +51,62 @@ 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 fftImportingStatus() -> 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)
//0 means no need to reimport, just recalc
//1 or missing means re-import
if let line = lines.first(where: {
$0.hasPrefix("import-status:")
}) {
return Int(line.replacingOccurrences(of: "import-status:", with: ""))
}
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 {

@ -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,+)

@ -18,7 +18,34 @@ class NetworkManager {
try? FileManager.default.removeItem(at: destinationFileUrl)
}
func downloadRankingData(lastDateString: String, fileName: String) async throws {
// 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)
}
@discardableResult
func downloadRankingData(lastDateString: String, fileName: String) async throws -> Int? {
let dateString = ["CLASSEMENT-PADEL", fileName, lastDateString].joined(separator: "-") + ".csv"
@ -26,15 +53,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,14 +75,22 @@ class NetworkManager {
// }
// }
// }
try? FileManager.default.removeItem(at: destinationFileUrl)
try FileManager.default.copyItem(at: task.0, to: destinationFileUrl)
print("dl rank data ok", lastDateString, fileName)
return destinationFileUrl.fftImportingStatus() ?? 1
} 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)
}
}
return nil
}
func checkFileCreationDate(filePath: String) throws -> Date? {

@ -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 {

@ -80,16 +80,39 @@ class SourceFileManager {
}
}
func fetchData(fromDate current: Date) async {
actor SourceFileDownloadTracker {
var _downloadedFileStatus : Int? = nil
func updateIfNecessary(with successState: Int?) {
if successState != nil && (_downloadedFileStatus == nil || _downloadedFileStatus == 0) {
_downloadedFileStatus = successState
}
}
func getDownloadedFileStatus() -> Int? {
return _downloadedFileStatus
}
}
//return nil if no new files
//return 1 if new file to import
//return 0 if new file just to re-calc static data, no need to re-import
@discardableResult
func fetchData(fromDate current: Date) async -> Int? {
let lastStringDate = URL.importDateFormatter.string(from: current)
let files = ["MESSIEURS", "MESSIEURS-2", "MESSIEURS-3", "MESSIEURS-4", "DAMES"]
let sourceFileDownloadTracker = SourceFileDownloadTracker()
do {
try await withThrowingTaskGroup(of: Void.self) { group in // Mark 1
for file in files {
group.addTask {
try await NetworkManager.shared.downloadRankingData(lastDateString: lastStringDate, fileName: file)
group.addTask { [sourceFileDownloadTracker] in
let success = try await NetworkManager.shared.downloadRankingData(lastDateString: lastStringDate, fileName: file)
await sourceFileDownloadTracker.updateIfNecessary(with: success)
}
}
@ -110,7 +133,9 @@ class SourceFileManager {
}
}
}
let downloadedFileStatus = await sourceFileDownloadTracker.getDownloadedFileStatus()
return downloadedFileStatus
}
func getAllFiles(initialDate: String = "08-2022") async {
@ -180,7 +205,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 +217,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 {

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

@ -11,7 +11,8 @@ import StoreKit
struct MainView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(\.requestReview) var requestReview
//TODO: IOS BUG
//@Environment(\.requestReview) var requestReview
@Environment(NavigationViewModel.self) private var navigation: NavigationViewModel
@Environment(ImportObserver.self) private var importObserver: ImportObserver
@ -70,7 +71,7 @@ struct MainView: View {
#if DEBUG
#else
_requestReviewIfAppropriated()
//_requestReviewIfAppropriated()
#endif
}
@ -146,7 +147,7 @@ struct MainView: View {
let isConnected = StoreCenter.main.userId != nil
let numberOfSignificantTournaments = dataStore.tournaments.filter({ $0.isDeleted == false && $0.endDate != nil }).count
if isConnected || numberOfSignificantTournaments > 0 {
requestReview()
//requestReview()
}
}
@ -230,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 let updated, updated == 1 {
await _startImporting(importingDate: mostRecentDateImported)
} else if current.incompleteMode == false || updated == 0 {
await _calculateMonthData(dataSource: current.monthKey)
}
}
}
}
}
}

@ -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")
@ -196,14 +202,6 @@ struct PadelClubView: View {
}
}
.id(uuid)
.task {
await self._checkSourceFileAvailability()
}
.refreshable {
Task {
await self._checkSourceFileAvailability()
}
}
.headerProminence(.increased)
.navigationTitle("Données fédérales")
}
@ -226,18 +224,6 @@ struct PadelClubView: View {
}
}
private func _checkSourceFileAvailability() async {
print("check internet")
print("check files on internet")
print("check if any files on internet are more recent than here")
importObserver.checkingFiles = true
await SourceFileManager.shared.fetchData()
importObserver.checkingFilesAttempt += 1
importObserver.checkingFiles = false
//uuid = UUID()
}
private func _startImporting() {
let importingDate = SourceFileManager.shared.mostRecentDateAvailable
importObserver.currentImportDate = importingDate

@ -149,7 +149,7 @@ struct AddTeamView: View {
@ViewBuilder
private func _managementView() -> some View {
Section {
RowButtonView("Rechercher dans la base fédérale") {
RowButtonView("Ajouter via la base fédérale") {
presentPlayerSearch = true
}
} footer: {
@ -365,6 +365,22 @@ struct AddTeamView: View {
}
}
}
if editedTeam == nil {
if createdPlayerIds.isEmpty {
RowButtonView("Bloquer une place") {
_createTeam(checkDuplicates: false)
}
} else {
RowButtonView("Ajouter l'équipe") {
_createTeam(checkDuplicates: true)
}
}
} else {
RowButtonView("Confirmer") {
_updateTeam(checkDuplicates: false)
editedTeam = nil
}
}
} header: {
let _currentSelection = _currentSelection()
let selectedSortedTeams = tournament.selectedSortedTeams()
@ -393,25 +409,6 @@ struct AddTeamView: View {
}
Section {
if editedTeam == nil {
if createdPlayerIds.isEmpty {
RowButtonView("Bloquer une place") {
_createTeam(checkDuplicates: false)
}
} else {
RowButtonView("Ajouter l'équipe") {
_createTeam(checkDuplicates: true)
}
}
} else {
RowButtonView("Modifier l'équipe") {
_updateTeam(checkDuplicates: false)
editedTeam = nil
}
}
}
if let pasteString {
if fetchPlayers.isEmpty {
ContentUnavailableView {

Loading…
Cancel
Save