From 71822ac204e85d5c816186e19a8640aa78ad5f45 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Mon, 2 Sep 2024 15:31:31 +0200 Subject: [PATCH] implement custom xls to csv option fix player not found message to account august 2024 situation guard the creation of acronym above 10 characters and club name above 50 characters --- PadelClub/Extensions/String+Extensions.swift | 2 +- PadelClub/Utils/CloudConvert.swift | 213 ++++-------------- PadelClub/ViewModel/SearchViewModel.swift | 5 +- PadelClub/Views/Club/ClubDetailView.swift | 7 +- .../Views/Tournament/Screen/AddTeamView.swift | 5 +- 5 files changed, 55 insertions(+), 177 deletions(-) diff --git a/PadelClub/Extensions/String+Extensions.swift b/PadelClub/Extensions/String+Extensions.swift index 1fe097d..e4d3d81 100644 --- a/PadelClub/Extensions/String+Extensions.swift +++ b/PadelClub/Extensions/String+Extensions.swift @@ -71,7 +71,7 @@ extension String { // Join the first letters together into a string let result = String(firstLetters) - return result + return String(result.prefix(10)) } } diff --git a/PadelClub/Utils/CloudConvert.swift b/PadelClub/Utils/CloudConvert.swift index 6a5a28f..ab1569a 100644 --- a/PadelClub/Utils/CloudConvert.swift +++ b/PadelClub/Utils/CloudConvert.swift @@ -29,196 +29,65 @@ class CloudConvert { static let manager = CloudConvert() func uploadFile(_ url: URL) async throws -> String { - let taskResponse = try await createJob(url) - let uploadResponse = try await uploadFile(taskResponse, url: url) - var fileReady = false - while fileReady == false { - try await Task.sleep(nanoseconds: 3_000_000_000) - let progressResponse = try await checkFile(id: uploadResponse.data.id) - if progressResponse.data.step == "finish" && progressResponse.data.stepPercent == 100 { - fileReady = true - print("progressResponse.data.minutes", progressResponse.data.minutes) - } - } - - let convertedFile = try await downloadConvertedFile(id: uploadResponse.data.id) - return convertedFile + return try await createJob(url) } - func createJob(_ url: URL) async throws -> TaskResponse { - guard let taskURL = URL(string: "https://api.convertio.co/convert") else { - throw CloudConvertionError.urlNotFound("https://api.convertio.co/convert") + func createJob(_ url: URL) async throws -> String { + let apiPath = "https://\(URLs.activationHost.rawValue)/utils/xls-to-csv/" + guard let taskURL = URL(string: apiPath) else { + throw CloudConvertionError.urlNotFound(apiPath) } var request: URLRequest = URLRequest(url: taskURL) - let parameters = """ - {"apikey":"d97cf13ef6d163e5e386c381fc8d256f","input":"upload","file":"","filename":"","outputformat":"csv","options":""} - """ + request.httpMethod = "POST" + // Create the boundary string for multipart/form-data + let boundary = UUID().uuidString - let postData = parameters.data(using: .utf8) - request.httpMethod = "POST" - request.httpBody = postData + // Set the content type to multipart/form-data with the boundary + request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") - let task = try await URLSession.shared.data(for: request) - //print("tried: \(request.url)") - if let errorResponse = try? JSONDecoder().decode(ErrorResponse.self, from: task.0) { - print("errorResponse.error", errorResponse.error) - throw CloudConvertionError.serviceError(errorResponse) - } - - return try JSONDecoder().decode(TaskResponse.self, from: task.0) - } + // The file to upload + let fileName = url.lastPathComponent + let fileURL = url - func uploadFile(_ response: TaskResponse, url: URL) async throws -> UploadResponse { - guard let uploadTaskURL = URL(string: "https://api.convertio.co/convert/\(response.data.id)/\(url.encodedLastPathComponent)") else { - throw CloudConvertionError.urlNotFound("https://api.convertio.co/convert/\(response.data.id)/\(url.encodedLastPathComponent)") - } - var uploadRequest: URLRequest = URLRequest(url: uploadTaskURL) - uploadRequest.httpMethod = "PUT" - let uploadTask = try await URLSession.shared.upload(for: uploadRequest, fromFile: url) + // Construct the body of the request + var body = Data() - //print("tried: \(uploadRequest.url)") - if let errorResponse = try? JSONDecoder().decode(ErrorResponse.self, from: uploadTask.0) { - print("errorResponse.error", errorResponse.error) - throw CloudConvertionError.serviceError(errorResponse) - } - - return try JSONDecoder().decode(UploadResponse.self, from: uploadTask.0) - } - - func checkFile(id: String) async throws -> ProgressResponse { - guard let taskURL = URL(string: "https://api.convertio.co/convert/\(id)/status") else { - throw CloudConvertionError.urlNotFound("https://api.convertio.co/convert/\(id)/status") - } - var request: URLRequest = URLRequest(url: taskURL) - request.httpMethod = "GET" - let task = try await URLSession.shared.data(for: request) + // Start the body with the boundary and content-disposition for the file + body.append("--\(boundary)\r\n".data(using: .utf8)!) + body.append("Content-Disposition: form-data; name=\"file\"; filename=\"\(fileName)\"\r\n".data(using: .utf8)!) + body.append("Content-Type: application/vnd.ms-excel\r\n\r\n".data(using: .utf8)!) - //print("tried: \(request.url)") - if let errorResponse = try? JSONDecoder().decode(ErrorResponse.self, from: task.0) { - print("errorResponse.error", errorResponse.error) - throw CloudConvertionError.serviceError(errorResponse) + // Append the file data + if let fileData = try? Data(contentsOf: fileURL) { + body.append(fileData) } - - return try JSONDecoder().decode(ProgressResponse.self, from: task.0) - } - - func downloadConvertedFile(id: String) async throws -> String { -// try await Task.sleep(nanoseconds: 3_000_000_000) - - guard let downloadTaskURL = URL(string: "https://api.convertio.co/convert/\(id)/dl/base64") else { - throw CloudConvertionError.urlNotFound("https://api.convertio.co/convert/\(id)/dl/base64") - } - var downloadRequest: URLRequest = URLRequest(url: downloadTaskURL) - downloadRequest.httpMethod = "GET" - let downloadTask = try await URLSession.shared.data(for: downloadRequest) - if let errorResponse = try? JSONDecoder().decode(ErrorResponse.self, from: downloadTask.0) { - print("errorResponse.error", errorResponse.error) - throw CloudConvertionError.serviceError(errorResponse) - } - - //print("tried: \(downloadRequest.url)") - let dataResponse = try JSONDecoder().decode(DataResponse.self, from: downloadTask.0) - if let decodedData = Data(base64Encoded: dataResponse.data.content), let string = String(data: decodedData, encoding: .utf8) { - return string + + // End the body with the boundary + body.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!) + + // Set the body of the request + request.httpBody = body + + let (data, response) = try await URLSession.shared.data(for: request) + + // Check the response status code + if let httpResponse = response as? HTTPURLResponse { + print("Status code: \(httpResponse.statusCode)") } - throw CloudConvertionError.unknownError + // Convert the response data to a String + if let responseString = String(data: data, encoding: .utf8) { + return responseString + } else { + let error = ErrorResponse(code: 1, status: "Encodage", error: "Encodage des données de classement invalide") + throw CloudConvertionError.serviceError(error) + } } } -// MARK: - DataResponse -struct DataResponse: Decodable { - let code: Int - let status: String - let data: DataDownloadClass -} - -// MARK: - DataClass -struct DataDownloadClass: Decodable { - let id, encode, content: String -} - - // MARK: - ErrorResponse struct ErrorResponse: Decodable { let code: Int let status, error: String } - - -// MARK: - TaskResponse -struct TaskResponse: Decodable { - let code: Int - let status: String - let data: DataClass -} - -// MARK: - DataClass -struct DataClass: Decodable { - let id: String -} - -// MARK: - ProgressResponse -struct ProgressResponse: Decodable { - let code: Int - let status: String - let data: ProgressDataClass -} - -// MARK: - DataClass -struct ProgressDataClass: Decodable { - let id, step: String - let stepPercent: Int - let minutes: String - - enum CodingKeys: String, CodingKey { - case id, step - case stepPercent = "step_percent" - case minutes - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - id = try container.decode(String.self, forKey: .id) - step = try container.decode(String.self, forKey: .step) - minutes = try container.decode(String.self, forKey: .minutes) - if let value = try? container.decode(String.self, forKey: .stepPercent) { - print(value) - stepPercent = Int(value) ?? 0 - } else { - stepPercent = try container.decode(Int.self, forKey: .stepPercent) - } - } - -} - -// MARK: - Output -struct Output: Decodable { - let url: String - let size: String -} - -// MARK: - UploadResponse -struct UploadResponse: Decodable { - let code: Int - let status: String - let data: UploadDataClass -} - -// MARK: - DataClass -struct UploadDataClass: Decodable { - let id, file: String - let size: Int -} - -extension URL { - var encodedLastPathComponent: String { - if #available(iOS 17.0, *) { - lastPathComponent - } else { - lastPathComponent.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? lastPathComponent - } - - } -} diff --git a/PadelClub/ViewModel/SearchViewModel.swift b/PadelClub/ViewModel/SearchViewModel.swift index ddb8123..631306a 100644 --- a/PadelClub/ViewModel/SearchViewModel.swift +++ b/PadelClub/ViewModel/SearchViewModel.swift @@ -68,7 +68,10 @@ class SearchViewModel: ObservableObject, Identifiable { var contentUnavailableMessage: String { var message = ["Vérifiez l'ortographe ou lancez une nouvelle recherche."] if tokens.isEmpty { - message.append("Il est possible que cette personne n'est joué aucun tournoi depuis les 12 derniers mois. Dans ce pas, Padel Club ne pourra pas le trouver.") + message.append("Il est possible que cette personne n'est joué aucun tournoi depuis les 12 derniers mois, dans ce cas, Padel Club ne pourra pas le trouver.") + if filterOption == .male { + message.append("Depuis août 2024, le classement fédérale disponible est limité aux 40.000 premiers joueurs. Si le joueur n'a pas encore assez de points pour être visible, Padel Club ne pourra pas non plus le trouver.") + } } return message.joined(separator: "\n") } diff --git a/PadelClub/Views/Club/ClubDetailView.swift b/PadelClub/Views/Club/ClubDetailView.swift index 29fe8f7..cb3b23b 100644 --- a/PadelClub/Views/Club/ClubDetailView.swift +++ b/PadelClub/Views/Club/ClubDetailView.swift @@ -66,6 +66,11 @@ struct ClubDetailView: View { .frame(maxWidth: .infinity) .focused($focusedField, equals: ._name) .submitLabel( displayContext == .addition ? .next : .done) + .onChange(of: club.name) { + if club.name.count > 50 { + club.name = String(club.name.prefix(50)) + } + } .onSubmit(of: .text) { if club.acronym.isEmpty { club.acronym = club.name.canonicalVersion.replaceCharactersFromSet(characterSet: .whitespacesAndNewlines).acronym() @@ -90,7 +95,7 @@ struct ClubDetailView: View { .focused($focusedField, equals: ._acronym) .submitLabel(.done) .multilineTextAlignment(.trailing) - .onSubmit(of: .text) { + .onChange(of: club.acronym) { if club.acronym.count > 10 { club.acronym = String(club.acronym.prefix(10)) } else if club.acronym.count == 0 { diff --git a/PadelClub/Views/Tournament/Screen/AddTeamView.swift b/PadelClub/Views/Tournament/Screen/AddTeamView.swift index 56569a4..5ef151f 100644 --- a/PadelClub/Views/Tournament/Screen/AddTeamView.swift +++ b/PadelClub/Views/Tournament/Screen/AddTeamView.swift @@ -438,7 +438,7 @@ struct AddTeamView: View { ContentUnavailableView { Label("Aucun résultat", systemImage: "person.2.slash") } description: { - Text("Aucun joueur classé n'a été trouvé dans ce message.") + Text("Aucun joueur classé n'a été trouvé dans ce message. Attention, si un joueur n'a pas joué de tournoi dans les 12 derniers, Padel Club ne pourra pas le trouver.") } actions: { RowButtonView("Créer un joueur non classé") { presentPlayerCreation = true @@ -451,7 +451,8 @@ struct AddTeamView: View { } else { Section { - ForEach(fetchPlayers.sorted(by: { $0.hitForSearch(pasteString) > $1.hitForSearch(pasteString) })) { player in + let sortedPlayers = fetchPlayers.sorted(by: { $0.hitForSearch(pasteString) > $1.hitForSearch(pasteString) }) + ForEach(sortedPlayers) { player in ImportedPlayerView(player: player).tag(player.license!) } } header: {