Skip to content

Commit 6f2c5c5

Browse files
committed
name on signups
1 parent 333b618 commit 6f2c5c5

9 files changed

Lines changed: 83 additions & 9 deletions

File tree

cli.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ def signups():
262262
conn = duckdb.connect(str(db_path), read_only=True)
263263
rows = conn.execute(
264264
"""
265-
SELECT id, timestamp, email, notify_plow, notify_projects,
265+
SELECT id, timestamp, name, email, notify_plow, notify_projects,
266266
notify_siliconharbour, note, ip, user_agent
267267
FROM signups
268268
ORDER BY timestamp DESC
@@ -271,6 +271,7 @@ def signups():
271271
columns = [
272272
"id",
273273
"timestamp",
274+
"name",
274275
"email",
275276
"notify_plow",
276277
"notify_projects",
@@ -327,10 +328,11 @@ def _bool_badge(val):
327328
f"</div>"
328329
)
329330

331+
name_str = _esc(r["name"]) if r["name"] else ""
330332
cards_html.append(f"""\
331333
<div class="card">
332334
<div class="card-header">
333-
<span class="email">{_esc(r["email"])}</span>
335+
<span class="email">{name_str + " &lt;" + _esc(r["email"]) + "&gt;" if name_str else _esc(r["email"])}</span>
334336
<span class="id">#{r["id"]}</span>
335337
</div>
336338
<div class="meta">

src/where_the_plow/db.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,7 @@ def insert_viewport(
453453
def insert_signup(
454454
self,
455455
email: str,
456+
name: str,
456457
ip: str | None = None,
457458
user_agent: str | None = None,
458459
notify_plow: bool = False,
@@ -463,11 +464,12 @@ def insert_signup(
463464
"""Record an email signup."""
464465
self._cursor().execute(
465466
"""
466-
INSERT INTO signups (email, ip, user_agent, notify_plow, notify_projects, notify_siliconharbour, note)
467-
VALUES (?, ?, ?, ?, ?, ?, ?)
467+
INSERT INTO signups (email, name, ip, user_agent, notify_plow, notify_projects, notify_siliconharbour, note)
468+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
468469
""",
469470
[
470471
email,
472+
name,
471473
ip,
472474
user_agent,
473475
notify_plow,
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""Add name column to signups table.
2+
3+
Stores the user's name alongside their email. NOT NULL with DEFAULT ''
4+
so existing rows get an empty string.
5+
6+
DuckDB doesn't support ALTER TABLE ADD COLUMN with constraints, so we
7+
recreate the table (same pattern as migration 002).
8+
"""
9+
10+
import duckdb
11+
12+
13+
def _has_column(conn: duckdb.DuckDBPyConnection, table: str, column: str) -> bool:
14+
rows = conn.execute(
15+
"SELECT column_name FROM information_schema.columns "
16+
"WHERE table_name=? AND column_name=?",
17+
[table, column],
18+
).fetchall()
19+
return len(rows) > 0
20+
21+
22+
def upgrade(conn: duckdb.DuckDBPyConnection) -> None:
23+
if not _has_column(conn, "signups", "name"):
24+
conn.execute("CREATE SEQUENCE IF NOT EXISTS signups_mig_seq")
25+
conn.execute("""
26+
CREATE TABLE signups_new (
27+
id BIGINT DEFAULT nextval('signups_mig_seq') PRIMARY KEY,
28+
timestamp TIMESTAMPTZ NOT NULL DEFAULT now(),
29+
name VARCHAR NOT NULL DEFAULT '',
30+
email VARCHAR NOT NULL,
31+
ip VARCHAR,
32+
user_agent VARCHAR,
33+
notify_plow BOOLEAN NOT NULL DEFAULT FALSE,
34+
notify_projects BOOLEAN NOT NULL DEFAULT FALSE,
35+
notify_siliconharbour BOOLEAN NOT NULL DEFAULT FALSE,
36+
note VARCHAR
37+
)
38+
""")
39+
conn.execute("""
40+
INSERT INTO signups_new
41+
(id, timestamp, name, email, ip, user_agent,
42+
notify_plow, notify_projects, notify_siliconharbour, note)
43+
SELECT id, timestamp, '', email, ip, user_agent,
44+
notify_plow, notify_projects, notify_siliconharbour, note
45+
FROM signups
46+
""")
47+
conn.execute("DROP TABLE signups")
48+
conn.execute("ALTER TABLE signups_new RENAME TO signups")

src/where_the_plow/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ class StatsResponse(BaseModel):
120120

121121
class SignupRequest(BaseModel):
122122
email: str = Field(..., description="Email address", min_length=3, max_length=320)
123+
name: str = Field(..., description="User's name", min_length=1, max_length=200)
123124
notify_plow: bool = Field(False, description="Notify when plow visits street")
124125
notify_projects: bool = Field(False, description="Notify about other projects")
125126
notify_siliconharbour: bool = Field(

src/where_the_plow/routes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,7 @@ def signup(request: Request, body: SignupRequest):
430430
db = request.app.state.db
431431
db.insert_signup(
432432
email=body.email,
433+
name=body.name,
433434
ip=ip,
434435
user_agent=user_agent,
435436
notify_plow=body.notify_plow,

src/where_the_plow/static/app.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ const welcomeCloseBtn = document.getElementById("welcome-close");
142142
const welcomeDismissBtn = document.getElementById("welcome-dismiss");
143143
const welcomeCtaBtn = document.getElementById("welcome-cta");
144144
const signupPanel = document.getElementById("welcome-signup");
145+
const signupName = document.getElementById("signup-name");
145146
const signupEmail = document.getElementById("signup-email");
146147
const signupProjects = document.getElementById("signup-projects");
147148
const signupSH = document.getElementById("signup-siliconharbour");
@@ -183,7 +184,7 @@ welcomeOverlay.addEventListener("click", (e) => {
183184
welcomeCtaBtn.addEventListener("click", () => {
184185
welcomeCtaBtn.style.display = "none";
185186
signupPanel.style.display = "block";
186-
signupEmail.focus();
187+
signupName.focus();
187188
});
188189

189190
// Note toggle
@@ -195,6 +196,13 @@ signupNoteToggle.addEventListener("click", () => {
195196

196197
// Submit signup
197198
signupSubmitBtn.addEventListener("click", async () => {
199+
const name = signupName.value.trim();
200+
if (!name) {
201+
signupStatus.textContent = "Please enter your name.";
202+
signupStatus.className = "error";
203+
return;
204+
}
205+
198206
const email = signupEmail.value.trim();
199207
if (!email || !email.includes("@")) {
200208
signupStatus.textContent = "Please enter a valid email address.";
@@ -212,6 +220,7 @@ signupSubmitBtn.addEventListener("click", async () => {
212220
headers: { "Content-Type": "application/json" },
213221
body: JSON.stringify({
214222
email,
223+
name,
215224
notify_plow: true,
216225
notify_projects: signupProjects.checked,
217226
notify_siliconharbour: signupSH.checked,
@@ -228,6 +237,7 @@ signupSubmitBtn.addEventListener("click", async () => {
228237
signupStatus.textContent =
229238
"You're signed up! I don't have a newsletter system just yet, but when I do, you'll be the first to know!";
230239
signupStatus.className = "success";
240+
signupName.disabled = true;
231241
signupEmail.disabled = true;
232242
signupProjects.disabled = true;
233243
signupSH.disabled = true;

src/where_the_plow/static/index.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,12 @@ <h4>About</h4>
378378
email, I'll let you know when it, and other new
379379
features, are ready.
380380
</p>
381+
<input
382+
type="text"
383+
id="signup-name"
384+
placeholder="Your name"
385+
autocomplete="name"
386+
/>
381387
<input
382388
type="email"
383389
id="signup-email"

src/where_the_plow/static/style.css

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -949,6 +949,7 @@ body.controls-left .maplibregl-ctrl-bottom-right .maplibregl-ctrl {
949949
font-size: 12px;
950950
}
951951

952+
#signup-name,
952953
#signup-email {
953954
width: 100%;
954955
padding: 8px 10px;
@@ -962,6 +963,7 @@ body.controls-left .maplibregl-ctrl-bottom-right .maplibregl-ctrl {
962963
margin-bottom: 8px;
963964
}
964965

966+
#signup-name:focus,
965967
#signup-email:focus {
966968
outline: none;
967969
border-color: var(--color-accent);

tests/test_migrate.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ def test_001_fresh_db_creates_tables(tmp_path):
347347
}
348348
assert "ip" in su_cols
349349
assert "user_agent" in su_cols
350+
assert "name" in su_cols
350351

351352
conn.close()
352353

@@ -373,7 +374,7 @@ def test_002_migrates_prod_db(tmp_path):
373374
)
374375
run_migrations(conn, migrations_dir)
375376

376-
assert get_version(conn) == 2
377+
assert get_version(conn) == 3
377378

378379
# Vehicles should have source column with composite PK
379380
veh_cols = {
@@ -485,7 +486,8 @@ def test_already_migrated_db_gets_stamped(tmp_path):
485486
notify_plow BOOLEAN NOT NULL DEFAULT FALSE,
486487
notify_projects BOOLEAN NOT NULL DEFAULT FALSE,
487488
notify_siliconharbour BOOLEAN NOT NULL DEFAULT FALSE,
488-
note VARCHAR
489+
note VARCHAR,
490+
name VARCHAR NOT NULL DEFAULT ''
489491
)
490492
""")
491493

@@ -494,6 +496,6 @@ def test_already_migrated_db_gets_stamped(tmp_path):
494496
)
495497
run_migrations(conn, migrations_dir)
496498

497-
# Should be stamped at version 2 with no errors
498-
assert get_version(conn) == 2
499+
# Should be stamped at version 3 with no errors
500+
assert get_version(conn) == 3
499501
conn.close()

0 commit comments

Comments
 (0)