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
release
Razmig Sarkissian 1 year ago
parent 17c4911a89
commit 71822ac204
  1. 2
      PadelClub/Extensions/String+Extensions.swift
  2. 213
      PadelClub/Utils/CloudConvert.swift
  3. 5
      PadelClub/ViewModel/SearchViewModel.swift
  4. 7
      PadelClub/Views/Club/ClubDetailView.swift
  5. 5
      PadelClub/Views/Tournament/Screen/AddTeamView.swift

@ -71,7 +71,7 @@ extension String {
// Join the first letters together into a string // Join the first letters together into a string
let result = String(firstLetters) let result = String(firstLetters)
return result return String(result.prefix(10))
} }
} }

@ -29,196 +29,65 @@ class CloudConvert {
static let manager = CloudConvert() static let manager = CloudConvert()
func uploadFile(_ url: URL) async throws -> String { func uploadFile(_ url: URL) async throws -> String {
let taskResponse = try await createJob(url) return 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
} }
func createJob(_ url: URL) async throws -> TaskResponse { func createJob(_ url: URL) async throws -> String {
guard let taskURL = URL(string: "https://api.convertio.co/convert") else { let apiPath = "https://\(URLs.activationHost.rawValue)/utils/xls-to-csv/"
throw CloudConvertionError.urlNotFound("https://api.convertio.co/convert") guard let taskURL = URL(string: apiPath) else {
throw CloudConvertionError.urlNotFound(apiPath)
} }
var request: URLRequest = URLRequest(url: taskURL) var request: URLRequest = URLRequest(url: taskURL)
let parameters = """ request.httpMethod = "POST"
{"apikey":"d97cf13ef6d163e5e386c381fc8d256f","input":"upload","file":"","filename":"","outputformat":"csv","options":""}
"""
// Create the boundary string for multipart/form-data
let boundary = UUID().uuidString
let postData = parameters.data(using: .utf8) // Set the content type to multipart/form-data with the boundary
request.httpMethod = "POST" request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
request.httpBody = postData
let task = try await URLSession.shared.data(for: request) // The file to upload
//print("tried: \(request.url)") let fileName = url.lastPathComponent
if let errorResponse = try? JSONDecoder().decode(ErrorResponse.self, from: task.0) { let fileURL = url
print("errorResponse.error", errorResponse.error)
throw CloudConvertionError.serviceError(errorResponse)
}
return try JSONDecoder().decode(TaskResponse.self, from: task.0)
}
func uploadFile(_ response: TaskResponse, url: URL) async throws -> UploadResponse { // Construct the body of the request
guard let uploadTaskURL = URL(string: "https://api.convertio.co/convert/\(response.data.id)/\(url.encodedLastPathComponent)") else { var body = Data()
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)
//print("tried: \(uploadRequest.url)") // Start the body with the boundary and content-disposition for the file
if let errorResponse = try? JSONDecoder().decode(ErrorResponse.self, from: uploadTask.0) { body.append("--\(boundary)\r\n".data(using: .utf8)!)
print("errorResponse.error", errorResponse.error) body.append("Content-Disposition: form-data; name=\"file\"; filename=\"\(fileName)\"\r\n".data(using: .utf8)!)
throw CloudConvertionError.serviceError(errorResponse) body.append("Content-Type: application/vnd.ms-excel\r\n\r\n".data(using: .utf8)!)
}
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)
//print("tried: \(request.url)") // Append the file data
if let errorResponse = try? JSONDecoder().decode(ErrorResponse.self, from: task.0) { if let fileData = try? Data(contentsOf: fileURL) {
print("errorResponse.error", errorResponse.error) body.append(fileData)
throw CloudConvertionError.serviceError(errorResponse)
} }
return try JSONDecoder().decode(ProgressResponse.self, from: task.0) // End the body with the boundary
} body.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)
func downloadConvertedFile(id: String) async throws -> String { // Set the body of the request
// try await Task.sleep(nanoseconds: 3_000_000_000) request.httpBody = body
guard let downloadTaskURL = URL(string: "https://api.convertio.co/convert/\(id)/dl/base64") else { let (data, response) = try await URLSession.shared.data(for: request)
throw CloudConvertionError.urlNotFound("https://api.convertio.co/convert/\(id)/dl/base64")
} // Check the response status code
var downloadRequest: URLRequest = URLRequest(url: downloadTaskURL) if let httpResponse = response as? HTTPURLResponse {
downloadRequest.httpMethod = "GET" print("Status code: \(httpResponse.statusCode)")
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
} }
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 // MARK: - ErrorResponse
struct ErrorResponse: Decodable { struct ErrorResponse: Decodable {
let code: Int let code: Int
let status, error: String 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
}
}
}

@ -68,7 +68,10 @@ class SearchViewModel: ObservableObject, Identifiable {
var contentUnavailableMessage: String { var contentUnavailableMessage: String {
var message = ["Vérifiez l'ortographe ou lancez une nouvelle recherche."] var message = ["Vérifiez l'ortographe ou lancez une nouvelle recherche."]
if tokens.isEmpty { 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") return message.joined(separator: "\n")
} }

@ -66,6 +66,11 @@ struct ClubDetailView: View {
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.focused($focusedField, equals: ._name) .focused($focusedField, equals: ._name)
.submitLabel( displayContext == .addition ? .next : .done) .submitLabel( displayContext == .addition ? .next : .done)
.onChange(of: club.name) {
if club.name.count > 50 {
club.name = String(club.name.prefix(50))
}
}
.onSubmit(of: .text) { .onSubmit(of: .text) {
if club.acronym.isEmpty { if club.acronym.isEmpty {
club.acronym = club.name.canonicalVersion.replaceCharactersFromSet(characterSet: .whitespacesAndNewlines).acronym() club.acronym = club.name.canonicalVersion.replaceCharactersFromSet(characterSet: .whitespacesAndNewlines).acronym()
@ -90,7 +95,7 @@ struct ClubDetailView: View {
.focused($focusedField, equals: ._acronym) .focused($focusedField, equals: ._acronym)
.submitLabel(.done) .submitLabel(.done)
.multilineTextAlignment(.trailing) .multilineTextAlignment(.trailing)
.onSubmit(of: .text) { .onChange(of: club.acronym) {
if club.acronym.count > 10 { if club.acronym.count > 10 {
club.acronym = String(club.acronym.prefix(10)) club.acronym = String(club.acronym.prefix(10))
} else if club.acronym.count == 0 { } else if club.acronym.count == 0 {

@ -438,7 +438,7 @@ struct AddTeamView: View {
ContentUnavailableView { ContentUnavailableView {
Label("Aucun résultat", systemImage: "person.2.slash") Label("Aucun résultat", systemImage: "person.2.slash")
} description: { } 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: { } actions: {
RowButtonView("Créer un joueur non classé") { RowButtonView("Créer un joueur non classé") {
presentPlayerCreation = true presentPlayerCreation = true
@ -451,7 +451,8 @@ struct AddTeamView: View {
} else { } else {
Section { 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!) ImportedPlayerView(player: player).tag(player.license!)
} }
} header: { } header: {

Loading…
Cancel
Save