housing#
Cape Town neighbourhoods explorer with safety scores, rental & property pricing, historical trends, ward mapping, and integration with crime/loadshedding data.
Models#
Model |
Description |
|---|---|
|
Geographic grouping (Atlantic Seaboard, City Bowl, etc.) |
|
Core neighbourhood record — safety, rental, property, Airbnb data |
|
Key-value highlight cards (icon + label + value) |
|
Point-in-time rental/property data for trend charts |
|
Municipal electoral ward (2021 boundaries) |
|
Quarterly crime/safety snapshot per ward |
|
Through-table mapping neighbourhoods → wards with weights |
URL Routes#
Path |
View |
Description |
|---|---|---|
|
|
Housing insights landing page |
|
|
Long-term + Airbnb/STR + housing impact |
|
|
Property prices, districts, types |
|
|
Hotel occupancy, ADR, segments |
|
|
Grid + Leaflet map of all neighbourhoods |
|
|
Single neighbourhood detail page |
Management Commands#
Command |
Description |
|---|---|
|
Ingest |
|
Ingest |
|
Report on neighbourhood data quality and completeness |
|
Create / update neighbourhood records from data files |
|
Refresh rental, property, and Airbnb metrics |
|
Generate a safety-guide blog post for a neighbourhood from existing data |
Data Flow#
How to Add a Neighbourhood#
The housing portal is fully data-driven — no code changes needed. Update these 5 data files
and re-run populate_neighbourhoods. Optionally, scaffold a safety guide blog post (step 6).
1. housing/data/neighbourhoods.json#
Add an entry with the next unique order value (check current max). Fields:
Field |
Notes |
|---|---|
|
URL-safe identifier, e.g. |
|
Display name |
|
Must match an existing district slug |
|
Short tagline (2–4 words) |
|
1–2 sentence overview |
|
Comma-separated audience tags |
|
Pipe-delimited ( |
|
Array of ward numbers (may be empty for out-of-metro areas) |
|
Centre point as strings |
|
BlogPost lookup key (empty string if none) |
|
BlogPost URL path (empty string if none) |
|
Manual safety override or |
|
Reporting period, e.g. |
|
Sort order within district |
2. housing/data/neighbourhood_metrics.json#
Add a key under neighbourhoods with rent/property medians:
"new-slug": {
"rent_1br": 9000,
"rent_2br": 15500,
"rent_sample_size": 8,
"rent_std_dev": 3500,
"price_1br": null,
"price_sample_size": null,
"price_std_dev": null,
"listing_url": "https://www.property24.com/to-rent/..."
}
Use null for fields with no data.
3. housing/data/district_mappings.json#
Append the slug to the appropriate group in each of the three mapping arrays:
rental— e.g."Woodstock / Observatory"real_estate— e.g."Woodstock"airbnb— e.g."Woodstock / Observatory"
4. static/geo/neighbourhoods.geojson#
Add a GeoJSON Feature with a Polygon geometry. Simplify to ~15 points.
Source boundaries from OpenStreetMap.
{
"type": "Feature",
"properties": { "slug": "new-slug" },
"geometry": {
"type": "Polygon",
"coordinates": [[ [lng, lat], ... ]]
}
}
5. housing/fixtures/neighbourhood_translations.json#
Add an entry with translations for all 9 languages (de, es, fr, it, ja, nl, pt, ru, en).
Translated fields: name, description, vibe, best_for, pros, cons, safety_reasoning.
The safety_reasoning translations are ward-level — reuse from a sibling neighbourhood in the
same ward. Insert the entry after the sibling neighbourhood entry in the fixture.
6. (Optional) Scaffold a safety guide blog post#
USE_SQLITE=1 python manage.py scaffold_neighbourhood_blogpost --slug new-slug
This creates a BlogPost with Markdown content assembled from the neighbourhood’s safety data,
ward research, and housing metrics. It links the post via FK and updates neighbourhoods.json.
DB is the source of truth; the JSON sync is best-effort.
Options: --dry-run (preview only), --overwrite (replace existing), --access subscribed,
--year 2026. Translation stubs are set as [TODO:XX] for manual translation later.
Verify#
USE_SQLITE=1 python manage.py populate_neighbourhoods
USE_SQLITE=1 python manage.py load_neighbourhood_translations
USE_SQLITE=1 python manage.py neighbourhood_overview
USE_SQLITE=1 python manage.py test housing products.tests.test_scaffold_blogpost --verbosity=2
Check /housing/neighbourhoods/ (map + table) and /housing/neighbourhoods/<slug>/ (detail page).
If a blog post was created, check /products/blogpost/<pk>/ and the “Full Safety Guide” CTA on the
neighbourhood detail page.
What NOT to change#
safety/ward_mapping.pyWARD_NEIGHBORHOODS— cosmetic only, updated separately wheningest_ward_safetyruns.No model or migration changes needed.
Production Deployment#
The housing/data/*.json seed files are not git-tracked (gitignored).
The PostgreSQL database is the production source of truth.
After adding a neighbourhood locally and verifying it works, deploy to production:
1. Trigger the housing workflow#
Run the Update Housing Data workflow (housing.yml) via GitHub Actions.
This runs populate_neighbourhoods against the production DB, which creates the
new neighbourhood record from the seed files bundled in the checkout.
Note: The seed files must be present in the CI checkout. If they are not, run
populate_neighbourhoodsmanually against the production DB with the appropriateDATABASE_URL/ Postgres env vars set.
2. Scaffold the blog post (if applicable)#
Blog posts are created against the production DB manually:
python manage.py scaffold_neighbourhood_blogpost --slug <slug>
This must run with production database credentials. The command creates the
BlogPost, links it to the Neighbourhood, and updates neighbourhoods.json
(best-effort).
3. Load neighbourhood translations#
The translation fixture is git-tracked (housing/fixtures/neighbourhood_translations.json).
It loads automatically when the workflow runs, or manually:
python manage.py load_neighbourhood_translations
4. Apply blog post translations#
Blog post translations (title + content in 8 languages) are stored directly on the
BlogPost model. These are not in any fixture — they must be applied via Django
admin, a data migration, or a script run against the production DB.
R2 data#
Only neighbourhood_metrics.json syncs to R2 (data/housing/neighbourhood_metrics.json).
Upload it via:
python manage.py update_neighbourhood_metrics
The other seed files (neighbourhoods.json, district_mappings.json) are local only.
Key Design Decisions#
Ward-to-neighbourhood mapping uses a weighted through-table (
NeighbourhoodWard) so safety scores are aggregated proportionally when a neighbourhood spans multiple wards.PropertySnapshot records are append-only — one per neighbourhood per
data_date— enabling year-over-year trend visualisation.Safety data flows from the
safetyapp viaWardSafetySnapshot; theNeighbourhoodmodel stores a denormalisedsafety_scorefor fast reads.