Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Shattering #84

Merged
merged 118 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
118 commits
Select commit Hold shift + click to select a range
50dd897
refactor out tiles for now
raphaellaude Sep 9, 2024
6d81538
shattering models
raphaellaude Sep 9, 2024
d91b544
parent child procedure and management commands
raphaellaude Sep 9, 2024
e76a9e1
clean up cli
raphaellaude Sep 10, 2024
05ad503
start testing cli
raphaellaude Sep 13, 2024
507ab52
show cli coverage too
raphaellaude Sep 13, 2024
98b87ea
reorg args and allow for child layer to be null
raphaellaude Sep 13, 2024
aeb6ae3
materialize unioned gerrydb views
raphaellaude Sep 13, 2024
8cc7fc0
refactor districtmap to use only gerrydb table names rather than uuids
raphaellaude Sep 14, 2024
ae5397b
fix tests
raphaellaude Sep 14, 2024
b43b1e5
update procedures and udf api
raphaellaude Sep 15, 2024
feb30c2
fix silly issue
raphaellaude Sep 15, 2024
b6ca1d6
getting close
raphaellaude Sep 15, 2024
324266d
fix by just forcing gerrydb table name in districtrmap to be unique
raphaellaude Sep 16, 2024
23ffe27
Add event listener, context menu information in store
nofurtherinformation Sep 19, 2024
7e599df
Draft Context Menu component
nofurtherinformation Sep 19, 2024
1e44fe1
update models to match
raphaellaude Sep 20, 2024
1c7e129
partition parent child edges on districtrmap
raphaellaude Sep 20, 2024
6ce61f7
move tileset commands to pipelines
raphaellaude Sep 21, 2024
3e4036b
begin to refactor fe
raphaellaude Sep 23, 2024
e50387c
add blocks if there is a live document
raphaellaude Sep 23, 2024
4b07a6a
paint on startup
raphaellaude Sep 23, 2024
ed75eb0
use context menu from themes
raphaellaude Sep 23, 2024
d5b9f04
clean up
raphaellaude Sep 23, 2024
58335f7
progress on fixing tests
raphaellaude Sep 23, 2024
b6f8db7
passing tests
raphaellaude Sep 23, 2024
65672eb
SHATTERING ON THE FE
raphaellaude Sep 23, 2024
d6b0d39
send parents back
raphaellaude Sep 23, 2024
e5530bb
making progress
raphaellaude Sep 23, 2024
33b7692
document how to set up shatterable districtr maps
raphaellaude Sep 24, 2024
6af1bb5
Docker compose config and data load script (#88)
nofurtherinformation Sep 23, 2024
937874e
Add shatter IDs to store, subscribe and mutate map
nofurtherinformation Sep 24, 2024
d79cb20
Add parent/child layer rendering
nofurtherinformation Sep 24, 2024
fb1d9e4
Add shatter state change to context menu
nofurtherinformation Sep 24, 2024
feffb61
clean up layers
nofurtherinformation Sep 24, 2024
790fb23
Cleanup log
nofurtherinformation Sep 24, 2024
f807f1a
cleanup unused source id
nofurtherinformation Sep 24, 2024
b9a39c8
remove boolean parameter
raphaellaude Sep 25, 2024
5ce9c1d
Refactor zustand accessors
nofurtherinformation Sep 25, 2024
d9c2422
Additional zustand accessor refactor
nofurtherinformation Sep 25, 2024
3694115
refactor zustand state accessors - map
nofurtherinformation Sep 25, 2024
86209b3
Add events to child layers
nofurtherinformation Sep 25, 2024
0719df0
Refactor hover state to store
nofurtherinformation Sep 25, 2024
6a26d4a
Refactor map zone assignment rendering to mapStore
nofurtherinformation Sep 25, 2024
55deda4
Clean up hover events
nofurtherinformation Sep 25, 2024
67c09fe
Enhance set shatter Ids
nofurtherinformation Sep 25, 2024
7bfca34
Child layer interactivity and zone assignment cleanup
nofurtherinformation Sep 25, 2024
c5ded83
Update mapStore.ts
nofurtherinformation Sep 25, 2024
3a1a8b4
Performance - clean up when zone assignments run
nofurtherinformation Sep 25, 2024
96312e6
Optimize zone assignments rendering logic
nofurtherinformation Sep 26, 2024
dcd74a7
Add map lock on shatter
nofurtherinformation Sep 26, 2024
0261d28
Style fix for map container
nofurtherinformation Sep 26, 2024
d712ab0
fix layer order
nofurtherinformation Sep 26, 2024
53f781c
clean up shatter events to remove parents
nofurtherinformation Sep 26, 2024
3e92c2c
Return parent path with assignments
nofurtherinformation Sep 30, 2024
9a7c791
Load shatter IDs into state on document load
nofurtherinformation Sep 30, 2024
d32bcdf
Also return parents
nofurtherinformation Sep 30, 2024
c9f3742
Formatting
nofurtherinformation Sep 30, 2024
0aae53a
Shatter assignment bug fix
nofurtherinformation Sep 30, 2024
83da9b3
Fix remove layer order
nofurtherinformation Sep 30, 2024
e35da2e
Only allow context menu on pan
nofurtherinformation Sep 30, 2024
4dd026b
Clean up map updates
nofurtherinformation Oct 1, 2024
3993be4
Revert color while painting
nofurtherinformation Oct 2, 2024
485f25a
reorganize mutations, queries
nofurtherinformation Oct 2, 2024
e16b5f0
reorganize subscriptions
nofurtherinformation Oct 2, 2024
fc2eb90
Remove mutation logic from components
nofurtherinformation Oct 2, 2024
44e2d08
move to global query client
nofurtherinformation Oct 2, 2024
6249070
update layers and helpers to track rendering state
nofurtherinformation Oct 2, 2024
1f451fe
clean up
nofurtherinformation Oct 2, 2024
770f2b2
clean up
nofurtherinformation Oct 2, 2024
987fc85
nextjs type safety
nofurtherinformation Oct 2, 2024
367da46
Remove tracking map rendering state
nofurtherinformation Oct 2, 2024
145e78e
Revert "Remove tracking map rendering state"
nofurtherinformation Oct 2, 2024
b09e265
Add where condition for districtr map uuid
nofurtherinformation Oct 3, 2024
64be91d
materialized view should actually use column data
raphaellaude Oct 8, 2024
989919d
try using specific python version
raphaellaude Oct 8, 2024
5e6cb7d
Subscribe shatter filters to mapRenderingState
nofurtherinformation Oct 8, 2024
24801ef
clean up subscriptions and filtering
nofurtherinformation Oct 8, 2024
e43cc92
merge main backend app changes
raphaellaude Oct 9, 2024
a0832d4
sort out alembic history
raphaellaude Oct 9, 2024
746955e
Merge branch 'main' into shattering
nofurtherinformation Oct 9, 2024
a00b9d3
ruff
nofurtherinformation Oct 9, 2024
bfe6f53
update load data script
nofurtherinformation Oct 9, 2024
8bcb1db
Zone can be none after erased
nofurtherinformation Oct 9, 2024
bc8241b
erase events cleanup (on mouse move)
nofurtherinformation Oct 9, 2024
911ecbd
Add paintable IDs and bbox to state on shatter
nofurtherinformation Oct 9, 2024
b4495bd
change map fill opacity on shatter, helper
nofurtherinformation Oct 9, 2024
f6bd236
query features filtered to captive ids
nofurtherinformation Oct 9, 2024
c90cc5e
add exit shatter view
nofurtherinformation Oct 9, 2024
533952d
turf bbox
nofurtherinformation Oct 9, 2024
e742977
refactor handleShatter to features, not IDs
nofurtherinformation Oct 9, 2024
93639d5
Only paint/select parents when not shattered
nofurtherinformation Oct 9, 2024
33da1a0
wip dive back in to shattered
nofurtherinformation Oct 9, 2024
ab2e2d8
clean up
nofurtherinformation Oct 9, 2024
88b8cfe
remove children when painting
nofurtherinformation Oct 9, 2024
5b4cc31
Revert "remove children when painting"
nofurtherinformation Oct 9, 2024
18d1691
Revert "clean up"
nofurtherinformation Oct 9, 2024
248247b
Revert "wip dive back in to shattered"
nofurtherinformation Oct 9, 2024
8670d72
Revert "Only paint/select parents when not shattered"
nofurtherinformation Oct 9, 2024
d8d553b
Revert "refactor handleShatter to features, not IDs"
nofurtherinformation Oct 9, 2024
b22393d
Revert "turf bbox"
nofurtherinformation Oct 9, 2024
5467492
Revert "add exit shatter view"
nofurtherinformation Oct 9, 2024
c5b39de
Revert "query features filtered to captive ids"
nofurtherinformation Oct 9, 2024
a21a89e
Revert "change map fill opacity on shatter, helper"
nofurtherinformation Oct 9, 2024
2dac210
Revert "Add paintable IDs and bbox to state on shatter"
nofurtherinformation Oct 9, 2024
1bccead
update script
raphaellaude Oct 10, 2024
e830ef1
handle non shattering maps
raphaellaude Oct 10, 2024
9f01d14
disable shattering if no child layer
raphaellaude Oct 10, 2024
5e9ef4d
optionals
raphaellaude Oct 10, 2024
03d365d
ssr safety
nofurtherinformation Oct 10, 2024
a1302cc
Clear state on create document
nofurtherinformation Oct 10, 2024
1bc972c
swap .union for new set
nofurtherinformation Oct 10, 2024
7bbfa55
missing brackets
nofurtherinformation Oct 11, 2024
1c85564
make load data script better for future refactor
nofurtherinformation Oct 14, 2024
58a2e38
ruff
nofurtherinformation Oct 15, 2024
378da62
UDF Fix for shattering null parents
nofurtherinformation Oct 15, 2024
3ee9595
Swap onClick for onSelect
nofurtherinformation Oct 15, 2024
a1c7de2
Bump limit to 30
nofurtherinformation Oct 15, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test-backend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ jobs:
working-directory: backend

- name: Run tests
run: pytest -v --cov=app
run: pytest -v --cov=.
working-directory: backend
env:
DOMAIN: postgres
Expand Down
2 changes: 1 addition & 1 deletion backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ If needed, create a user for yourself.

### Testing

`pytest --cov=app --cov-report=html`
`pytest --cov=.`

Or with full coverage report:

Expand Down
118 changes: 118 additions & 0 deletions backend/app/alembic/versions/ccb2a6b81a8b_shattering.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
"""shattering

Revision ID: ccb2a6b81a8b
Revises: 8437ce954087
Create Date: 2024-09-13 09:44:34.534198

"""

from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa
import app.models
import sqlmodel.sql.sqltypes
from pathlib import Path

SQL_PATH = Path(__file__).parent.parent.parent / "sql"


# revision identifiers, used by Alembic.
revision: str = "ccb2a6b81a8b"
down_revision: Union[str, None] = "8437ce954087"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"districtrmap",
sa.Column(
"created_at",
sa.TIMESTAMP(timezone=True),
server_default=sa.text("CURRENT_TIMESTAMP"),
nullable=False,
),
sa.Column(
"updated_at",
sa.TIMESTAMP(timezone=True),
server_default=sa.text("CURRENT_TIMESTAMP"),
nullable=False,
),
sa.Column("uuid", app.models.UUIDType(), nullable=False),
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column(
"gerrydb_table_name", sqlmodel.sql.sqltypes.AutoString(), nullable=True
),
sa.Column("num_districts", sa.Integer(), nullable=True),
sa.Column("tiles_s3_path", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column("parent_layer", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("child_layer", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.ForeignKeyConstraint(
["child_layer"],
["gerrydbtable.name"],
),
sa.ForeignKeyConstraint(
["parent_layer"],
["gerrydbtable.name"],
),
sa.PrimaryKeyConstraint("uuid"),
sa.UniqueConstraint("uuid"),
sa.UniqueConstraint("gerrydb_table_name"),
)
op.create_table(
"parentchildedges",
sa.Column(
"created_at",
sa.TIMESTAMP(timezone=True),
server_default=sa.text("CURRENT_TIMESTAMP"),
nullable=False,
),
sa.Column(
"updated_at",
sa.TIMESTAMP(timezone=True),
server_default=sa.text("CURRENT_TIMESTAMP"),
nullable=False,
),
sa.Column("districtr_map", app.models.UUIDType(), nullable=False),
sa.Column("parent_path", sa.String(), nullable=False),
sa.Column("child_path", sa.String(), nullable=False),
sa.ForeignKeyConstraint(
["districtr_map"],
["districtrmap.uuid"],
),
sa.PrimaryKeyConstraint("districtr_map", "parent_path", "child_path"),
)
op.drop_column("gerrydbtable", "tiles_s3_path")
# ### end Alembic commands ###

for file_name in [
"parent_child_relationships.sql",
"create_shatterable_gerrydb_view.sql",
"create_districtr_map_udf.sql",
"shatter_parent.sql",
]:
with open(SQL_PATH / file_name, "r") as f:
sql = f.read()
op.execute(sql)


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"gerrydbtable",
sa.Column("tiles_s3_path", sa.VARCHAR(), autoincrement=False, nullable=True),
)
op.drop_table("parentchildedges")
op.drop_table("districtrmap")
# ### end Alembic commands ###
op.execute("DROP PROCEDURE IF EXISTS add_parent_child_relationships(TEXT)")
op.execute(
"DROP PROCEDURE IF EXISTS create_shatterable_gerrydb_view(TEXT, TEXT, TEXT)"
)
for func_name in [
"create_districtr_map",
"shatter_parent",
]:
op.execute(f"DROP FUNCTION IF EXISTS {func_name}")
67 changes: 45 additions & 22 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,27 @@
from pydantic import UUID4
from sqlalchemy import text
from sqlalchemy.exc import ProgrammingError
from sqlmodel import Session, select
from sqlmodel import Session, String, select
from starlette.middleware.cors import CORSMiddleware
from sqlalchemy.dialects.postgresql import insert
import logging
from sqlalchemy import bindparam
from sqlmodel import ARRAY

import sentry_sdk
from app.core.db import engine
from app.core.config import settings
from app.models import (
Assignments,
AssignmentsCreate,
DistrictrMap,
Document,
DocumentCreate,
DocumentPublic,
GEOIDS,
UUIDType,
ZonePopulation,
GerryDBTable,
GerryDBViewPublic,
DistrictrMapPublic,
)

if settings.ENVIRONMENT == "production":
Expand Down Expand Up @@ -82,18 +86,9 @@ async def create_document(
{"gerrydb_table_name": data.gerrydb_table},
)
document_id = results.one()[0] # should be only one row, one column of results
stmt = (
select(
Document.document_id,
Document.created_at,
Document.updated_at,
Document.gerrydb_table,
GerryDBTable.tiles_s3_path,
)
.where(Document.document_id == document_id)
.join(GerryDBTable, Document.gerrydb_table == GerryDBTable.name, isouter=True)
.limit(1)
)
stmt = select(Document).where(Document.document_id == document_id).limit(1)
# Document id has a unique constraint so I'm not sure we need to hit the DB again here
# more valuable would be to check that the assignments table
doc = session.exec(
stmt
).one() # again if we've got more than one, we have problems.
Expand Down Expand Up @@ -139,11 +134,39 @@ async def update_assignments(
stmt = stmt.on_conflict_do_update(
constraint=Assignments.__table__.primary_key, set_={"zone": stmt.excluded.zone}
)
session.exec(stmt)
session.execute(stmt)
session.commit()
return {"assignments_upserted": len(data.assignments)}


@app.patch(
"/api/update_assignments/{document_id}/shatter_parents",
response_model=list[Assignments],
)
async def shatter_parent(
document_id: str, data: GEOIDS, session: Session = Depends(get_session)
):
stmt = text("""SELECT *
FROM shatter_parent(:input_document_id, :parent_geoids)""").bindparams(
bindparam(key="input_document_id", type_=UUIDType),
bindparam(key="parent_geoids", type_=ARRAY(String)),
)
results = session.execute(
statement=stmt,
params={
"input_document_id": document_id,
"parent_geoids": data.geoids,
},
)
# :( was getting validation errors so am just going to loop
assignments = [
Assignments(document_id=str(document_id), geo_id=geo_id, zone=zone)
for document_id, geo_id, zone in results
]
session.commit()
return assignments


# called by getAssignments in apiHandlers.ts
@app.get("/api/get_assignments/{document_id}", response_model=list[Assignments])
async def get_assignments(document_id: str, session: Session = Depends(get_session)):
Expand All @@ -160,10 +183,10 @@ async def get_document(document_id: str, session: Session = Depends(get_session)
Document.created_at,
Document.gerrydb_table,
Document.updated_at,
GerryDBTable.tiles_s3_path.label("tiles_s3_path"),
)
DistrictrMap.tiles_s3_path.label("tiles_s3_path"), # pyright: ignore
) # pyright: ignore
.where(Document.document_id == document_id)
.join(GerryDBTable, Document.gerrydb_table == GerryDBTable.name, isouter=True)
.join(DistrictrMap, Document.gerrydb_table == DistrictrMap.name, isouter=True)
.limit(1)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(pp) not caused by this PR, but just noticing- limit(1) is going to prevent run time errors if we do for some reason violate uniqueness constraints, but, uh, we have uniqueness constraints on the database. I think we should be consistent-- pair result.one() with a .limit(1) each time.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally but I'd prefer to actually remove the limit and handle the runtime error, allowing us to send an informative error and catch bugs earlier. Bugs ofc not caused by erroneous db state-because we have our constraints in place-but in our queries

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With that said, let's address this in a separate mini pr

)
result = session.exec(stmt)
Expand Down Expand Up @@ -195,16 +218,16 @@ async def get_total_population(
)


@app.get("/api/gerrydb/views", response_model=list[GerryDBViewPublic])
@app.get("/api/gerrydb/views", response_model=list[DistrictrMapPublic])
async def get_projects(
*,
session: Session = Depends(get_session),
offset: int = 0,
limit: int = Query(default=100, le=100),
):
gerrydb_views = session.exec(
select(GerryDBTable)
.order_by(GerryDBTable.created_at.asc())
select(DistrictrMap)
.order_by(DistrictrMap.created_at.asc()) # pyright: ignore
.offset(offset)
.limit(limit)
).all()
Expand Down
57 changes: 53 additions & 4 deletions backend/app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
from pydantic import UUID4, BaseModel
from sqlmodel import (
Field,
ForeignKey,
SQLModel,
UUID,
TIMESTAMP,
UniqueConstraint,
text,
Column,
MetaData,
String,
)
from app.constants import DOCUMENT_SCHEMA

Expand Down Expand Up @@ -40,15 +42,58 @@ class TimeStampMixin(SQLModel):
)


class GerryDBTable(TimeStampMixin, SQLModel, table=True):
class DistrictrMap(TimeStampMixin, SQLModel, table=True):
uuid: str = Field(sa_column=Column(UUIDType, unique=True, primary_key=True))
name: str = Field(nullable=False, unique=True)
name: str = Field(nullable=False)
# This is intentionally not a foreign key on `GerryDBTable` because in some cases
# this may be the GerryDBTable but in others the pop table may be a materialized
# view of two GerryDBTables in the case of shatterable maps.
# We'll want to enforce the constraint tha the gerrydb_table_name is either in
# GerrydbTable.name or a materialized view of two GerryDBTables some other way.
gerrydb_table_name: str | None = Field(nullable=True, unique=True)
# Null means default number of districts? Should we have a sensible default?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(O) Sensible default hard to set globally, IMO, maybe later we are more geo-aware and can default to the num of congressional districts in a state but for now I think null is fine.

num_districts: int | None = Field(nullable=True, default=None)
tiles_s3_path: str | None = Field(nullable=True)
parent_layer: str = Field(
sa_column=Column(UUIDType, ForeignKey("gerrydbtable.uuid"), nullable=False)
)
child_layer: str | None = Field(
sa_column=Column(
UUIDType, ForeignKey("gerrydbtable.uuid"), default=None, nullable=True
)
)
# schema? will need to contrain the schema
# where does this go?
# when you create the view, pull the columns that you need
# we'll want discrete management steps


class GerryDBViewPublic(BaseModel):
class DistrictrMapPublic(BaseModel):
name: str
tiles_s3_path: str | None
gerrydb_table_name: str
parent_layer: str
child_layer: str | None = None
tiles_s3_path: str | None = None
num_districts: int | None = None


class GerryDBTable(TimeStampMixin, SQLModel, table=True):
uuid: str = Field(sa_column=Column(UUIDType, unique=True, primary_key=True))
# Must correspond to the layer name in the tileset
name: str = Field(nullable=False, unique=True)


class ParentChildEdges(TimeStampMixin, SQLModel, table=True):
districtr_map: str = Field(
raphaellaude marked this conversation as resolved.
Show resolved Hide resolved
sa_column=Column(
UUIDType,
ForeignKey("districtrmap.uuid"),
nullable=False,
primary_key=True,
)
)
parent_path: str = Field(sa_column=Column(String, nullable=False, primary_key=True))
child_path: str = Field(sa_column=Column(String, nullable=False, primary_key=True))


class Document(TimeStampMixin, SQLModel, table=True):
Expand Down Expand Up @@ -91,6 +136,10 @@ class AssignmentsCreate(BaseModel):
assignments: list[Assignments]


class GEOIDS(BaseModel):
geoids: list[str]


class ZonePopulation(BaseModel):
zone: int
total_pop: int
Loading