158 lines
3.7 KiB
Go
158 lines
3.7 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/csv"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
type ImportResult struct {
|
|
Inserted int `json:"inserted"`
|
|
Grouped int `json:"grouped"`
|
|
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, noteIdx int
|
|
hasEmail, hasTicketID, hasTicketType, hasNote bool
|
|
)
|
|
|
|
if idx, ok := colIndex["patron name"]; ok {
|
|
// CrowdWork / ticketing platform format
|
|
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
|
|
}
|
|
if idx, ok := colIndex["note"]; ok {
|
|
noteIdx, hasNote = 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
|
|
}
|
|
|
|
a := Attendee{Name: name}
|
|
if hasEmail {
|
|
a.Email = strings.TrimSpace(csvGet(record, emailIdx))
|
|
}
|
|
if hasTicketID {
|
|
a.TicketID = strings.TrimSpace(csvGet(record, ticketIDIdx))
|
|
}
|
|
if hasTicketType {
|
|
a.TicketType = strings.TrimSpace(csvGet(record, ticketTypeIdx))
|
|
}
|
|
if hasNote {
|
|
a.Note = strings.TrimSpace(csvGet(record, noteIdx))
|
|
}
|
|
|
|
_, err = app.createAttendee(a)
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
|
|
// CrowdWork exports one row per ticket under the purchaser's name.
|
|
// If we have a ticket_id and the same (name, ticket_id) already exists,
|
|
// increment party_size instead of skipping.
|
|
if hasTicketID && a.TicketID != "" {
|
|
merged, mergeErr := app.incrementPartySize(a.Name, a.TicketID)
|
|
if mergeErr == nil && merged {
|
|
result.Grouped++
|
|
continue
|
|
}
|
|
}
|
|
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 ""
|
|
}
|