Created Turnpike, event attendee and volunteer management
Built after prototype, Traverse, an attendee and volunteer list maintainer.
This commit is contained in:
commit
5d56ba8112
59 changed files with 8663 additions and 0 deletions
158
handle_import.go
Normal file
158
handle_import.go
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
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 ""
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue