Add self-service person-level opt-out with full removal at load

This commit is contained in:
Ian Gulliver
2026-08-16 11:34:32 -07:00
parent 4d70d6c2a8
commit 4abe9d281f
7 changed files with 145 additions and 69 deletions
+48 -61
View File
@@ -1,4 +1,4 @@
// Command oneoff drops the duplicated trailing import column and reshapes the Change Log header.
// Command oneoff appends the Opted Out column to the Overrides and Change Log tabs.
package main
import (
@@ -12,17 +12,6 @@ import (
"google.golang.org/api/sheets/v4"
)
var changeLogHeader = []string{
"Timestamp", "Actor",
"Email", "Added",
"Full Name", "Legal Name", "Preferred Name",
"Is Student", "Is Parent", "Is Staff",
"New to Helios", "Pronouns", "Facts",
"Grade", "Classroom", "Crew",
"Phone", "Job Title", "Department", "Grade Band", "Room Parent",
"Address", "Family Phone", "Family Photo Caption",
}
func main() {
sheet := flag.String("sheet", "", "spreadsheet id")
flag.Parse()
@@ -35,60 +24,58 @@ func main() {
if err != nil {
log.Fatalf("[ERROR] create sheets client: %v", err)
}
meta, err := svc.Spreadsheets.Get(*sheet).Fields("sheets(properties(sheetId,title))").Do()
meta, err := svc.Spreadsheets.Get(*sheet).Fields("sheets(properties(sheetId,title,gridProperties(columnCount)))").Do()
if err != nil {
log.Fatalf("[ERROR] get spreadsheet: %v", err)
}
importID := int64(-1)
for _, s := range meta.Sheets {
if s.Properties.Title == "Veracross Import" {
importID = s.Properties.SheetId
}
}
if importID < 0 {
log.Fatal("[ERROR] no Veracross Import tab")
}
resp, err := svc.Spreadsheets.Values.Get(*sheet, "'Veracross Import'!AD1:AD1000").Do()
if err != nil {
log.Fatalf("[ERROR] read column AD: %v", err)
}
for i, row := range resp.Values {
if i == 0 {
if len(row) == 0 || fmt.Sprint(row[0]) != "household_2_person_2_phone_business" {
log.Fatalf("[ERROR] column AD header is %v, not the expected duplicate", row)
for _, tab := range []string{"Overrides", "Change Log"} {
var props *sheets.SheetProperties
for _, s := range meta.Sheets {
if s.Properties.Title == tab {
props = s.Properties
}
continue
}
if len(row) > 0 && fmt.Sprint(row[0]) != "" {
log.Fatalf("[ERROR] column AD row %d has data %q; not deleting", i+1, row[0])
if props == nil {
log.Fatalf("[ERROR] no %s tab", tab)
}
resp, err := svc.Spreadsheets.Values.Get(*sheet, fmt.Sprintf("'%s'!1:1", tab)).Do()
if err != nil {
log.Fatalf("[ERROR] read %s header: %v", tab, err)
}
width := len(resp.Values[0])
for _, cell := range resp.Values[0] {
if fmt.Sprint(cell) == "Opted Out" {
log.Fatalf("[ERROR] %s already has an Opted Out column", tab)
}
}
if int64(width) >= props.GridProperties.ColumnCount {
_, err = svc.Spreadsheets.BatchUpdate(*sheet, &sheets.BatchUpdateSpreadsheetRequest{
Requests: []*sheets.Request{{AppendDimension: &sheets.AppendDimensionRequest{
SheetId: props.SheetId,
Dimension: "COLUMNS",
Length: 1,
}}},
}).Do()
if err != nil {
log.Fatalf("[ERROR] widen %s: %v", tab, err)
}
}
cell := fmt.Sprintf("'%s'!%s1", tab, columnName(width))
_, err = svc.Spreadsheets.Values.Update(*sheet, cell, &sheets.ValueRange{
Values: [][]interface{}{{"Opted Out"}},
}).ValueInputOption("RAW").Do()
if err != nil {
log.Fatalf("[ERROR] write %s header: %v", tab, err)
}
log.Printf("added Opted Out to %s at %s", tab, cell)
}
_, err = svc.Spreadsheets.BatchUpdate(*sheet, &sheets.BatchUpdateSpreadsheetRequest{
Requests: []*sheets.Request{{DeleteDimension: &sheets.DeleteDimensionRequest{
Range: &sheets.DimensionRange{
SheetId: importID,
Dimension: "COLUMNS",
StartIndex: 29,
EndIndex: 30,
},
}}},
}).Do()
if err != nil {
log.Fatalf("[ERROR] delete column AD: %v", err)
}
log.Print("deleted duplicate import column AD")
values := make([]interface{}, len(changeLogHeader))
for i, h := range changeLogHeader {
values[i] = h
}
_, err = svc.Spreadsheets.Values.Update(*sheet, "'Change Log'!1:1", &sheets.ValueRange{
Values: [][]interface{}{values},
}).ValueInputOption("RAW").Do()
if err != nil {
log.Fatalf("[ERROR] rewrite change log header: %v", err)
}
log.Printf("rewrote Change Log header with %d columns", len(changeLogHeader))
}
func columnName(idx int) string {
name := ""
for idx >= 0 {
name = string(rune('A'+idx%26)) + name
idx = idx/26 - 1
}
return name
}