Mirror Overrides columns in Change Log with previous values; add tab tooling
This commit is contained in:
+3
-1
@@ -9,7 +9,7 @@ The entities the directory serves, and how they are assembled. Structured data l
|
|||||||
| Veracross Import | import tool | The raw Veracross student export, rewritten wholesale by the import tool on each refresh. Read-only to the serving app; no local edit survives here. |
|
| Veracross Import | import tool | The raw Veracross student export, rewritten wholesale by the import tool on each refresh. Read-only to the serving app; no local edit survives here. |
|
||||||
| Name to Email | hand | Maps a student's name to their school email for import rows where the email cell is empty or wrong, so every person can be keyed by email downstream. |
|
| Name to Email | hand | Maps a student's name to their school email for import rows where the email cell is empty or wrong, so every person can be keyed by email downstream. |
|
||||||
| Overrides | hand + app | The entire local layer: admin corrections, app-written self-service text, and added people, in canonical model columns keyed by email. |
|
| Overrides | hand + app | The entire local layer: admin corrections, app-written self-service text, and added people, in canonical model columns keyed by email. |
|
||||||
| Change Log | app | Append-only audit trail of self-service changes. |
|
| Change Log | app | Append-only audit trail mirroring the Overrides columns: one row per change, holding the previous values. |
|
||||||
|
|
||||||
School structure lives nowhere in the sheet: membership and the classroom and crew names themselves derive from person records, and the remaining fixed structure — band identities, grade progression, department order — is code constants (see Classrooms and grades).
|
School structure lives nowhere in the sheet: membership and the classroom and crew names themselves derive from person records, and the remaining fixed structure — band identities, grade progression, department order — is code constants (see Classrooms and grades).
|
||||||
|
|
||||||
@@ -67,6 +67,8 @@ Flagged rows must supply every field the model requires; unflagged rows can be a
|
|||||||
|
|
||||||
Family-level fields (address, family photo caption, family phone) ride on a parent's row and apply to that parent's household, so a two-household student's families are addressed independently through their respective adults.
|
Family-level fields (address, family photo caption, family phone) ride on a parent's row and apply to that parent's household, so a two-household student's families are addressed independently through their respective adults.
|
||||||
|
|
||||||
|
Every change to Overrides appends a Change Log row: timestamp, actor, the row's email, then the previous value of each column that changed — `-` marking a previously empty cell, untouched columns left blank. Media uploads are not logged here; the drive archive is their history.
|
||||||
|
|
||||||
## Media blobs
|
## Media blobs
|
||||||
|
|
||||||
Photos and pronunciation recordings are files in the media shared drive, named by convention: `<email local part>-photo` and `<email local part>-pronunciation` for people, `<family key hash>-photo` and `<family key hash>-pronunciation` for families. Presence means existence — no sheet cell records a filename — and freshness comes from the file's modified time. Uploads replace the file and archive the previous version; superseded versions stay in an archive folder.
|
Photos and pronunciation recordings are files in the media shared drive, named by convention: `<email local part>-photo` and `<email local part>-pronunciation` for people, `<family key hash>-photo` and `<family key hash>-pronunciation` for families. Presence means existence — no sheet cell records a filename — and freshness comes from the file's modified time. Uploads replace the file and archive the previous version; superseded versions stay in an archive folder.
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
// Command createtabs creates the directory sheet's local-layer tabs with their header rows.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"heliosian/internal/data"
|
||||||
|
"google.golang.org/api/option"
|
||||||
|
"google.golang.org/api/sheets/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
var tabs = []struct {
|
||||||
|
title string
|
||||||
|
header []string
|
||||||
|
}{
|
||||||
|
{"Name to Email", []string{"Name", "Email"}},
|
||||||
|
{"Overrides", []string{
|
||||||
|
"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",
|
||||||
|
}},
|
||||||
|
{"Change Log", []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()
|
||||||
|
if *sheet == "" {
|
||||||
|
log.Fatal("[ERROR] -sheet <spreadsheet id> is required")
|
||||||
|
}
|
||||||
|
svc, err := sheets.NewService(context.Background(),
|
||||||
|
option.WithCredentialsFile(data.KeyFile),
|
||||||
|
option.WithScopes(sheets.SpreadsheetsScope))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("[ERROR] create sheets client: %v", err)
|
||||||
|
}
|
||||||
|
meta, err := svc.Spreadsheets.Get(*sheet).Fields("sheets(properties(title))").Do()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("[ERROR] get spreadsheet: %v", err)
|
||||||
|
}
|
||||||
|
existing := map[string]bool{}
|
||||||
|
for _, s := range meta.Sheets {
|
||||||
|
existing[s.Properties.Title] = true
|
||||||
|
}
|
||||||
|
for _, t := range tabs {
|
||||||
|
if existing[t.title] {
|
||||||
|
log.Fatalf("[ERROR] tab %q already exists", t.title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, t := range tabs {
|
||||||
|
_, err := svc.Spreadsheets.BatchUpdate(*sheet, &sheets.BatchUpdateSpreadsheetRequest{
|
||||||
|
Requests: []*sheets.Request{{AddSheet: &sheets.AddSheetRequest{
|
||||||
|
Properties: &sheets.SheetProperties{Title: t.title},
|
||||||
|
}}},
|
||||||
|
}).Do()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("[ERROR] create tab %q: %v", t.title, err)
|
||||||
|
}
|
||||||
|
values := make([]interface{}, len(t.header))
|
||||||
|
for i, h := range t.header {
|
||||||
|
values[i] = h
|
||||||
|
}
|
||||||
|
quoted := "'" + strings.ReplaceAll(t.title, "'", "''") + "'"
|
||||||
|
_, err = svc.Spreadsheets.Values.Update(*sheet, quoted+"!1:1", &sheets.ValueRange{
|
||||||
|
Values: [][]interface{}{values},
|
||||||
|
}).ValueInputOption("RAW").Do()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("[ERROR] write header of %q: %v", t.title, err)
|
||||||
|
}
|
||||||
|
log.Printf("created tab %q with %d columns", t.title, len(t.header))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
// Command oneoff drops the duplicated trailing import column and reshapes the Change Log header.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"heliosian/internal/data"
|
||||||
|
"google.golang.org/api/option"
|
||||||
|
"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()
|
||||||
|
if *sheet == "" {
|
||||||
|
log.Fatal("[ERROR] -sheet <spreadsheet id> is required")
|
||||||
|
}
|
||||||
|
svc, err := sheets.NewService(context.Background(),
|
||||||
|
option.WithCredentialsFile(data.KeyFile),
|
||||||
|
option.WithScopes(sheets.SpreadsheetsScope))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("[ERROR] create sheets client: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
meta, err := svc.Spreadsheets.Get(*sheet).Fields("sheets(properties(sheetId,title))").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)
|
||||||
|
}
|
||||||
|
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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, 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))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user