Move Ticket Name to the Participant model.

This commit is contained in:
Pen Anderson 2026-03-05 17:15:41 -06:00
parent cc4dd76438
commit e722ef055e
5 changed files with 27 additions and 27 deletions

30
db.go
View file

@ -201,6 +201,7 @@ func migrateV2(db *sql.DB) error {
// and links volunteers to participants via participant_id. // and links volunteers to participants via participant_id.
func migrateV3(db *sql.DB) error { func migrateV3(db *sql.DB) error {
addColumnIfMissing(db, "volunteers", "participant_id INTEGER REFERENCES participants(id)") addColumnIfMissing(db, "volunteers", "participant_id INTEGER REFERENCES participants(id)")
addColumnIfMissing(db, "participants", "ticket_name TEXT NOT NULL DEFAULT ''")
// Seed participants from volunteers first (better name data: preferred_name). // Seed participants from volunteers first (better name data: preferred_name).
db.Exec(` db.Exec(`
@ -401,7 +402,6 @@ type Volunteer struct {
AttendeeID *int `json:"attendee_id,omitempty"` // deprecated; kept for migration compat AttendeeID *int `json:"attendee_id,omitempty"` // deprecated; kept for migration compat
Name string `json:"name"` Name string `json:"name"`
PreferredName string `json:"preferred_name"` PreferredName string `json:"preferred_name"`
TicketName string `json:"ticket_name"`
Email string `json:"email"` Email string `json:"email"`
Phone string `json:"phone"` Phone string `json:"phone"`
Pronouns string `json:"pronouns"` Pronouns string `json:"pronouns"`
@ -424,6 +424,7 @@ type Participant struct {
ID int `json:"id"` ID int `json:"id"`
Email string `json:"email"` Email string `json:"email"`
PreferredName string `json:"preferred_name"` PreferredName string `json:"preferred_name"`
TicketName string `json:"ticket_name"`
Phone string `json:"phone"` Phone string `json:"phone"`
Pronouns string `json:"pronouns"` Pronouns string `json:"pronouns"`
Note string `json:"note"` Note string `json:"note"`
@ -860,7 +861,7 @@ func (app *App) attendeeCounts() (total, checkedIn int, err error) {
// --- Participants --- // --- Participants ---
const participantCols = `id, email, preferred_name, phone, pronouns, note, created_at, updated_at, deleted_at` const participantCols = `id, email, preferred_name, ticket_name, phone, pronouns, note, created_at, updated_at, deleted_at`
func (app *App) listParticipants(search, since string) ([]Participant, error) { func (app *App) listParticipants(search, since string) ([]Participant, error) {
var q string var q string
@ -900,8 +901,8 @@ func (app *App) getParticipantByEmail(email string) (*Participant, error) {
func (app *App) createParticipant(p Participant) (*Participant, error) { func (app *App) createParticipant(p Participant) (*Participant, error) {
res, err := app.db.Exec( res, err := app.db.Exec(
`INSERT INTO participants (email, preferred_name, phone, pronouns, note, updated_at) VALUES (?, ?, ?, ?, ?, ?)`, `INSERT INTO participants (email, preferred_name, ticket_name, phone, pronouns, note, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)`,
strings.ToLower(p.Email), p.PreferredName, p.Phone, p.Pronouns, p.Note, now(), strings.ToLower(p.Email), p.PreferredName, p.TicketName, p.Phone, p.Pronouns, p.Note, now(),
) )
if err != nil { if err != nil {
return nil, err return nil, err
@ -912,9 +913,9 @@ func (app *App) createParticipant(p Participant) (*Participant, error) {
func (app *App) updateParticipant(p Participant) error { func (app *App) updateParticipant(p Participant) error {
_, err := app.db.Exec( _, err := app.db.Exec(
`UPDATE participants SET email=?, preferred_name=?, phone=?, pronouns=?, note=?, updated_at=? `UPDATE participants SET email=?, preferred_name=?, ticket_name=?, phone=?, pronouns=?, note=?, updated_at=?
WHERE id=? AND deleted_at IS NULL`, WHERE id=? AND deleted_at IS NULL`,
strings.ToLower(p.Email), p.PreferredName, p.Phone, p.Pronouns, p.Note, now(), p.ID, strings.ToLower(p.Email), p.PreferredName, p.TicketName, p.Phone, p.Pronouns, p.Note, now(), p.ID,
) )
return err return err
} }
@ -957,7 +958,7 @@ func queryParticipants(db *sql.DB, q string, args ...any) ([]Participant, error)
for rows.Next() { for rows.Next() {
var p Participant var p Participant
if err := rows.Scan( if err := rows.Scan(
&p.ID, &p.Email, &p.PreferredName, &p.Phone, &p.Pronouns, &p.Note, &p.ID, &p.Email, &p.PreferredName, &p.TicketName, &p.Phone, &p.Pronouns, &p.Note,
&p.CreatedAt, &p.UpdatedAt, &p.DeletedAt, &p.CreatedAt, &p.UpdatedAt, &p.DeletedAt,
); err != nil { ); err != nil {
return nil, err return nil, err
@ -1199,7 +1200,6 @@ func queryDepartments(db *sql.DB, q string, args ...any) ([]Department, error) {
const volunteerSelect = `v.id, v.participant_id, v.attendee_id, const volunteerSelect = `v.id, v.participant_id, v.attendee_id,
COALESCE(NULLIF(p.preferred_name,''), NULLIF(v.preferred_name,''), v.name), COALESCE(NULLIF(p.preferred_name,''), NULLIF(v.preferred_name,''), v.name),
COALESCE(NULLIF(p.preferred_name,''), NULLIF(v.preferred_name,''), v.name), COALESCE(NULLIF(p.preferred_name,''), NULLIF(v.preferred_name,''), v.name),
v.ticket_name,
COALESCE(NULLIF(p.email,''), v.email), COALESCE(NULLIF(p.email,''), v.email),
COALESCE(NULLIF(p.phone,''), v.phone), COALESCE(NULLIF(p.phone,''), v.phone),
COALESCE(NULLIF(p.pronouns,''), v.pronouns), COALESCE(NULLIF(p.pronouns,''), v.pronouns),
@ -1210,7 +1210,7 @@ const volunteerSelect = `v.id, v.participant_id, v.attendee_id,
const volunteerFrom = `FROM volunteers v LEFT JOIN participants p ON p.id = v.participant_id` const volunteerFrom = `FROM volunteers v LEFT JOIN participants p ON p.id = v.participant_id`
// volunteerCols is kept for backward-compat references that expect unqualified column names. // volunteerCols is kept for backward-compat references that expect unqualified column names.
const volunteerCols = `id, attendee_id, name, preferred_name, ticket_name, email, phone, pronouns, department_id, is_lead, checked_in, checked_in_at, email_confirmed, confirmation_token, note, created_at, updated_at, deleted_at` const volunteerCols = `id, attendee_id, name, preferred_name, email, phone, pronouns, department_id, is_lead, checked_in, checked_in_at, email_confirmed, confirmation_token, note, created_at, updated_at, deleted_at`
func (app *App) listVolunteers(search string, deptID *int, since string) ([]Volunteer, error) { func (app *App) listVolunteers(search string, deptID *int, since string) ([]Volunteer, error) {
q := `SELECT ` + volunteerSelect + ` ` + volunteerFrom + ` WHERE 1=1` q := `SELECT ` + volunteerSelect + ` ` + volunteerFrom + ` WHERE 1=1`
@ -1263,9 +1263,9 @@ func (app *App) getVolunteerByParticipantID(participantID int) (*Volunteer, erro
func (app *App) createVolunteer(v Volunteer) (*Volunteer, error) { func (app *App) createVolunteer(v Volunteer) (*Volunteer, error) {
res, err := app.db.Exec( res, err := app.db.Exec(
`INSERT INTO volunteers (participant_id, attendee_id, name, preferred_name, ticket_name, email, phone, pronouns, department_id, is_lead, email_confirmed, confirmation_token, note, updated_at) `INSERT INTO volunteers (participant_id, attendee_id, name, preferred_name, email, phone, pronouns, department_id, is_lead, email_confirmed, confirmation_token, note, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
v.ParticipantID, v.AttendeeID, v.Name, v.PreferredName, v.TicketName, v.Email, v.Phone, v.Pronouns, v.ParticipantID, v.AttendeeID, v.Name, v.PreferredName, v.Email, v.Phone, v.Pronouns,
v.DepartmentID, boolInt(v.IsLead), boolInt(v.EmailConfirmed), v.ConfirmationToken, v.Note, now(), v.DepartmentID, boolInt(v.IsLead), boolInt(v.EmailConfirmed), v.ConfirmationToken, v.Note, now(),
) )
if err != nil { if err != nil {
@ -1277,9 +1277,9 @@ func (app *App) createVolunteer(v Volunteer) (*Volunteer, error) {
func (app *App) updateVolunteer(v Volunteer) error { func (app *App) updateVolunteer(v Volunteer) error {
_, err := app.db.Exec( _, err := app.db.Exec(
`UPDATE volunteers SET participant_id=?, attendee_id=?, name=?, preferred_name=?, ticket_name=?, email=?, phone=?, pronouns=?, department_id=?, is_lead=?, note=?, updated_at=? `UPDATE volunteers SET participant_id=?, attendee_id=?, name=?, preferred_name=?, email=?, phone=?, pronouns=?, department_id=?, is_lead=?, note=?, updated_at=?
WHERE id=? AND deleted_at IS NULL`, WHERE id=? AND deleted_at IS NULL`,
v.ParticipantID, v.AttendeeID, v.Name, v.PreferredName, v.TicketName, v.Email, v.Phone, v.Pronouns, v.ParticipantID, v.AttendeeID, v.Name, v.PreferredName, v.Email, v.Phone, v.Pronouns,
v.DepartmentID, boolInt(v.IsLead), v.Note, now(), v.ID, v.DepartmentID, boolInt(v.IsLead), v.Note, now(), v.ID,
) )
return err return err
@ -1340,7 +1340,7 @@ func queryVolunteers(db *sql.DB, q string, args ...any) ([]Volunteer, error) {
var isLead, checkedIn, confirmed, emailConfirmed int var isLead, checkedIn, confirmed, emailConfirmed int
var confirmationToken, confirmedAt, kioskCode sql.NullString var confirmationToken, confirmedAt, kioskCode sql.NullString
if err := rows.Scan( if err := rows.Scan(
&v.ID, &participantID, &attendeeID, &v.Name, &v.PreferredName, &v.TicketName, &v.ID, &participantID, &attendeeID, &v.Name, &v.PreferredName,
&v.Email, &v.Phone, &v.Pronouns, &deptID, &v.Email, &v.Phone, &v.Pronouns, &deptID,
&isLead, &checkedIn, &v.CheckedInAt, &isLead, &checkedIn, &v.CheckedInAt,
&confirmed, &confirmedAt, &confirmed, &confirmedAt,

View file

@ -63,6 +63,7 @@
return ($allTickets ?? []).filter(t => t.participant_id === participantId) return ($allTickets ?? []).filter(t => t.participant_id === participantId)
} }
function checkedInCount(participantId) { function checkedInCount(participantId) {
return ticketsFor(participantId).filter(t => t.checked_in_at).length return ticketsFor(participantId).filter(t => t.checked_in_at).length
} }
@ -367,6 +368,9 @@
{#if p.pronouns} {#if p.pronouns}
<span class="text-muted" style="font-size:0.78rem"> · {p.pronouns}</span> <span class="text-muted" style="font-size:0.78rem"> · {p.pronouns}</span>
{/if} {/if}
{#if p.ticket_name && p.ticket_name !== p.preferred_name}
<div class="text-muted" style="font-size:0.78rem">Ticket: {p.ticket_name}</div>
{/if}
{#if p.note} {#if p.note}
<div class="text-muted" style="font-size:0.78rem">{p.note}</div> <div class="text-muted" style="font-size:0.78rem">{p.note}</div>
{/if} {/if}

View file

@ -299,9 +299,6 @@
{:else if !participantHasTickets(v.participant_id)} {:else if !participantHasTickets(v.participant_id)}
<span class="badge badge-partial" style="margin-left:0.4rem" title="No ticket on file">No ticket</span> <span class="badge badge-partial" style="margin-left:0.4rem" title="No ticket on file">No ticket</span>
{/if} {/if}
{#if v.ticket_name && v.ticket_name !== v.name}
<div class="text-muted" style="font-size:0.78rem">Ticket: {v.ticket_name}</div>
{/if}
{#if v.email} {#if v.email}
<div class="text-muted" style="font-size:0.78rem">{v.email}</div> <div class="text-muted" style="font-size:0.78rem">{v.email}</div>
{/if} {/if}

View file

@ -75,12 +75,13 @@ func (app *App) handlePublicSignup(w http.ResponseWriter, r *http.Request) {
return return
} }
// Update participant's personal details if they signed up with more info. // Update participant's personal details if they signed up with more info.
if body.Phone != "" || body.Pronouns != "" { if body.Phone != "" || body.Pronouns != "" || body.TicketName != "" {
app.db.Exec(`UPDATE participants SET app.db.Exec(`UPDATE participants SET
phone = CASE WHEN phone = '' THEN ? ELSE phone END, phone = CASE WHEN phone = '' THEN ? ELSE phone END,
pronouns = CASE WHEN pronouns = '' THEN ? ELSE pronouns END, pronouns = CASE WHEN pronouns = '' THEN ? ELSE pronouns END,
ticket_name = CASE WHEN ticket_name = '' THEN ? ELSE ticket_name END,
updated_at = ? updated_at = ?
WHERE id = ?`, body.Phone, body.Pronouns, now(), participant.ID) WHERE id = ?`, body.Phone, body.Pronouns, body.TicketName, now(), participant.ID)
} }
confirmToken, err := generateConfirmationToken() confirmToken, err := generateConfirmationToken()
@ -93,7 +94,6 @@ func (app *App) handlePublicSignup(w http.ResponseWriter, r *http.Request) {
ParticipantID: &participant.ID, ParticipantID: &participant.ID,
Name: body.PreferredName, Name: body.PreferredName,
PreferredName: body.PreferredName, PreferredName: body.PreferredName,
TicketName: body.TicketName,
Email: body.Email, Email: body.Email,
Phone: body.Phone, Phone: body.Phone,
Pronouns: body.Pronouns, Pronouns: body.Pronouns,

View file

@ -337,8 +337,8 @@ func TestPublicSignupTicketNameDoesNotOverwritePreferredName(t *testing.T) {
if p.PreferredName != "Titania" { if p.PreferredName != "Titania" {
t.Errorf("participant preferred_name = %q, want %q (not ticket_name)", p.PreferredName, "Titania") t.Errorf("participant preferred_name = %q, want %q (not ticket_name)", p.PreferredName, "Titania")
} }
if vol.TicketName != "Titania Fairweather" { if p.TicketName != "Titania Fairweather" {
t.Errorf("vol.TicketName = %q, want %q", vol.TicketName, "Titania Fairweather") t.Errorf("participant.TicketName = %q, want %q", p.TicketName, "Titania Fairweather")
} }
} }
@ -349,12 +349,11 @@ func TestConfirmEmailAssignsKioskCode(t *testing.T) {
app.db.Exec(`INSERT OR REPLACE INTO config (key, value) VALUES ('shift_signups_open', 'true')`) app.db.Exec(`INSERT OR REPLACE INTO config (key, value) VALUES ('shift_signups_open', 'true')`)
app.baseURL = "https://example.com" app.baseURL = "https://example.com"
participant, _ := app.createParticipant(Participant{PreferredName: "Titania", Email: "titania@example.com"}) participant, _ := app.createParticipant(Participant{PreferredName: "Titania", TicketName: "Titania Fairweather", Email: "titania@example.com"})
token := "abc123def456" token := "abc123def456"
app.createVolunteer(Volunteer{ app.createVolunteer(Volunteer{
Name: "Titania", Name: "Titania",
PreferredName: "Titania", PreferredName: "Titania",
TicketName: "Titania Fairweather",
Email: "titania@example.com", Email: "titania@example.com",
ParticipantID: &participant.ID, ParticipantID: &participant.ID,
ConfirmationToken: &token, ConfirmationToken: &token,