package main import ( "encoding/csv" "fmt" "io" "net/http" "strings" ) type ImportResult struct { Inserted int `json:"inserted"` Skipped int `json:"skipped"` Errors []string `json:"errors"` } func (app *App) handleImport(w http.ResponseWriter, r *http.Request) { if err := r.ParseMultipartForm(10 << 20); err != nil { writeError(w, "invalid form", http.StatusBadRequest) return } file, _, err := r.FormFile("csv") if err != nil { writeError(w, "csv file required", http.StatusBadRequest) return } defer file.Close() result, err := app.importCSV(file) if err != nil { writeError(w, err.Error(), http.StatusBadRequest) return } if result.Errors == nil { result.Errors = []string{} } writeJSON(w, result) } func (app *App) importCSV(r io.Reader) (ImportResult, error) { reader := csv.NewReader(r) reader.TrimLeadingSpace = true reader.LazyQuotes = true header, err := reader.Read() if err != nil { return ImportResult{}, fmt.Errorf("reading header: %w", err) } if len(header) > 0 { header[0] = strings.TrimPrefix(header[0], "\xef\xbb\xbf") // strip BOM } colIndex := make(map[string]int) for i, h := range header { colIndex[strings.ToLower(strings.TrimSpace(h))] = i } var ( nameIdx, emailIdx, ticketIDIdx, ticketTypeIdx int hasEmail, hasTicketID, hasTicketType bool isCrowdWork bool ) if idx, ok := colIndex["patron name"]; ok { // CrowdWork / ticketing platform format isCrowdWork = true nameIdx = idx if idx, ok := colIndex["patron email"]; ok { emailIdx, hasEmail = idx, true } if idx, ok := colIndex["tier name"]; ok { ticketTypeIdx, hasTicketType = idx, true } if idx, ok := colIndex["order number"]; ok { ticketIDIdx, hasTicketID = idx, true } } else if idx, ok := colIndex["name"]; ok { // Generic format nameIdx = idx if idx, ok := colIndex["email"]; ok { emailIdx, hasEmail = idx, true } if idx, ok := colIndex["ticket_id"]; ok { ticketIDIdx, hasTicketID = idx, true } if idx, ok := colIndex["ticket_type"]; ok { ticketTypeIdx, hasTicketType = idx, true } } else { return ImportResult{}, fmt.Errorf("CSV must have a 'name' or 'patron name' column") } var result ImportResult lineNum := 1 for { record, err := reader.Read() if err == io.EOF { break } lineNum++ if err != nil { result.Errors = append(result.Errors, fmt.Sprintf("line %d: %v", lineNum, err)) continue } name := strings.TrimSpace(csvGet(record, nameIdx)) if name == "" { continue } email := "" if hasEmail { email = strings.TrimSpace(csvGet(record, emailIdx)) } externalID := "" if hasTicketID { externalID = strings.TrimSpace(csvGet(record, ticketIDIdx)) } ticketType := "" if hasTicketType { ticketType = strings.TrimSpace(csvGet(record, ticketTypeIdx)) } source := "manual" orderID := "" if isCrowdWork { source = "crowdwork" orderID = externalID } // Find or create participant when email is present. var participantID *int if email != "" { p, _, err := app.upsertParticipant(email, name) if err != nil { result.Errors = append(result.Errors, fmt.Sprintf("line %d (%s): participant: %v", lineNum, name, err)) continue } if p != nil { participantID = &p.ID } } _, err = app.createTicket(Ticket{ ParticipantID: participantID, Name: name, TicketType: ticketType, Source: source, ExternalID: externalID, OrderID: orderID, }) if err != nil { if strings.Contains(err.Error(), "UNIQUE constraint failed") { result.Skipped++ } else { result.Errors = append(result.Errors, fmt.Sprintf("line %d (%s): %v", lineNum, name, err)) } continue } result.Inserted++ } return result, nil } func csvGet(record []string, idx int) string { if idx < len(record) { return record[idx] } return "" }