675601869b
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 19s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 46s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 12s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
Upgrades the existing "Pupils" stat to include a compact split bar and
percentage hint for mixed schools (single-sex schools already carry a
"Boys's/Girls's school" badge, so the split would be redundant).
Wires fact_pupil_characteristics into the API: new SQLAlchemy model and
a real census block in /api/schools/{urn} replacing the prior null stub.
On the primary detail page the inline "Pupils: 241" text is replaced by
a richer block (display number + bar + "52% girls · 48% boys"). On the
secondary detail page the existing "Total pupils" hero stat card grows
the bar and hint beneath the number. Both fall back to the previous
text-only rendering when census gender data is missing.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
258 lines
8.4 KiB
Python
258 lines
8.4 KiB
Python
"""
|
|
SQLAlchemy models — all tables live in the marts schema, built by dbt.
|
|
Read-only: the pipeline writes to these tables; the backend only reads.
|
|
"""
|
|
|
|
from sqlalchemy import Column, Integer, String, Float, Boolean, Date, Text, Index
|
|
|
|
from .database import Base
|
|
|
|
MARTS = {"schema": "marts"}
|
|
|
|
|
|
class DimSchool(Base):
|
|
"""Canonical school dimension — one row per active URN."""
|
|
__tablename__ = "dim_school"
|
|
__table_args__ = MARTS
|
|
|
|
urn = Column(Integer, primary_key=True)
|
|
school_name = Column(String(255), nullable=False)
|
|
phase = Column(String(100))
|
|
school_type = Column(String(100))
|
|
academy_trust_name = Column(String(255))
|
|
academy_trust_uid = Column(String(20))
|
|
religious_character = Column(String(100))
|
|
gender = Column(String(20))
|
|
age_range = Column(String(20))
|
|
capacity = Column(Integer)
|
|
total_pupils = Column(Integer)
|
|
headteacher_name = Column(String(200))
|
|
website = Column(String(255))
|
|
telephone = Column(String(30))
|
|
status = Column(String(50))
|
|
nursery_provision = Column(Boolean)
|
|
admissions_policy = Column(String(50))
|
|
# Denormalised Ofsted summary (updated by monthly pipeline)
|
|
ofsted_grade = Column(Integer)
|
|
ofsted_date = Column(Date)
|
|
ofsted_framework = Column(String(20))
|
|
|
|
|
|
class DimLocation(Base):
|
|
"""School location — address, lat/lng from easting/northing (BNG→WGS84)."""
|
|
__tablename__ = "dim_location"
|
|
__table_args__ = MARTS
|
|
|
|
urn = Column(Integer, primary_key=True)
|
|
address_line1 = Column(String(255))
|
|
address_line2 = Column(String(255))
|
|
town = Column(String(100))
|
|
county = Column(String(100))
|
|
postcode = Column(String(20))
|
|
local_authority_code = Column(Integer)
|
|
local_authority_name = Column(String(100))
|
|
parliamentary_constituency = Column(String(100))
|
|
urban_rural = Column(String(50))
|
|
easting = Column(Integer)
|
|
northing = Column(Integer)
|
|
latitude = Column(Float)
|
|
longitude = Column(Float)
|
|
# geom is a PostGIS geometry — not mapped to SQLAlchemy (accessed via raw SQL)
|
|
|
|
|
|
class KS2Performance(Base):
|
|
"""KS2 attainment — one row per URN per year (includes predecessor data)."""
|
|
__tablename__ = "fact_ks2_performance"
|
|
__table_args__ = (
|
|
Index("ix_ks2_urn_year", "urn", "year"),
|
|
MARTS,
|
|
)
|
|
|
|
urn = Column(Integer, primary_key=True)
|
|
year = Column(Integer, primary_key=True)
|
|
source_urn = Column(Integer)
|
|
total_pupils = Column(Integer)
|
|
eligible_pupils = Column(Integer)
|
|
# Core attainment
|
|
rwm_expected_pct = Column(Float)
|
|
rwm_high_pct = Column(Float)
|
|
reading_expected_pct = Column(Float)
|
|
reading_high_pct = Column(Float)
|
|
reading_avg_score = Column(Float)
|
|
reading_progress = Column(Float)
|
|
writing_expected_pct = Column(Float)
|
|
writing_high_pct = Column(Float)
|
|
writing_progress = Column(Float)
|
|
maths_expected_pct = Column(Float)
|
|
maths_high_pct = Column(Float)
|
|
maths_avg_score = Column(Float)
|
|
maths_progress = Column(Float)
|
|
gps_expected_pct = Column(Float)
|
|
gps_high_pct = Column(Float)
|
|
gps_avg_score = Column(Float)
|
|
science_expected_pct = Column(Float)
|
|
# Absence
|
|
reading_absence_pct = Column(Float)
|
|
writing_absence_pct = Column(Float)
|
|
maths_absence_pct = Column(Float)
|
|
gps_absence_pct = Column(Float)
|
|
science_absence_pct = Column(Float)
|
|
# Gender
|
|
rwm_expected_boys_pct = Column(Float)
|
|
rwm_high_boys_pct = Column(Float)
|
|
rwm_expected_girls_pct = Column(Float)
|
|
rwm_high_girls_pct = Column(Float)
|
|
# Disadvantaged
|
|
rwm_expected_disadvantaged_pct = Column(Float)
|
|
rwm_expected_non_disadvantaged_pct = Column(Float)
|
|
disadvantaged_gap = Column(Float)
|
|
# Context
|
|
disadvantaged_pct = Column(Float)
|
|
eal_pct = Column(Float)
|
|
sen_support_pct = Column(Float)
|
|
sen_ehcp_pct = Column(Float)
|
|
stability_pct = Column(Float)
|
|
|
|
|
|
class FactOfstedInspection(Base):
|
|
"""Full Ofsted inspection history — one row per inspection."""
|
|
__tablename__ = "fact_ofsted_inspection"
|
|
__table_args__ = (
|
|
Index("ix_ofsted_urn_date", "urn", "inspection_date"),
|
|
MARTS,
|
|
)
|
|
|
|
urn = Column(Integer, primary_key=True)
|
|
inspection_date = Column(Date, primary_key=True)
|
|
inspection_type = Column(String(100))
|
|
framework = Column(String(20))
|
|
overall_effectiveness = Column(Integer)
|
|
quality_of_education = Column(Integer)
|
|
behaviour_attitudes = Column(Integer)
|
|
personal_development = Column(Integer)
|
|
leadership_management = Column(Integer)
|
|
early_years_provision = Column(Integer)
|
|
sixth_form_provision = Column(Integer)
|
|
rc_safeguarding_met = Column(Boolean)
|
|
rc_inclusion = Column(Integer)
|
|
rc_curriculum_teaching = Column(Integer)
|
|
rc_achievement = Column(Integer)
|
|
rc_attendance_behaviour = Column(Integer)
|
|
rc_personal_development = Column(Integer)
|
|
rc_leadership_governance = Column(Integer)
|
|
rc_early_years = Column(Integer)
|
|
rc_sixth_form = Column(Integer)
|
|
report_url = Column(Text)
|
|
|
|
|
|
class FactParentView(Base):
|
|
"""Ofsted Parent View survey — latest per school."""
|
|
__tablename__ = "fact_parent_view"
|
|
__table_args__ = MARTS
|
|
|
|
urn = Column(Integer, primary_key=True)
|
|
survey_date = Column(Date)
|
|
total_responses = Column(Integer)
|
|
q_happy_pct = Column(Float)
|
|
q_safe_pct = Column(Float)
|
|
q_behaviour_pct = Column(Float)
|
|
q_bullying_pct = Column(Float)
|
|
q_communication_pct = Column(Float)
|
|
q_progress_pct = Column(Float)
|
|
q_teaching_pct = Column(Float)
|
|
q_information_pct = Column(Float)
|
|
q_curriculum_pct = Column(Float)
|
|
q_future_pct = Column(Float)
|
|
q_leadership_pct = Column(Float)
|
|
q_wellbeing_pct = Column(Float)
|
|
q_recommend_pct = Column(Float)
|
|
|
|
|
|
class FactAdmissions(Base):
|
|
"""School admissions — one row per URN per year."""
|
|
__tablename__ = "fact_admissions"
|
|
__table_args__ = (
|
|
Index("ix_admissions_urn_year", "urn", "year"),
|
|
MARTS,
|
|
)
|
|
|
|
urn = Column(Integer, primary_key=True)
|
|
year = Column(Integer, primary_key=True)
|
|
school_phase = Column(String(50))
|
|
places_offered = Column(Integer)
|
|
total_applications = Column(Integer)
|
|
first_preference_applications = Column(Integer)
|
|
first_preference_offers = Column(Integer)
|
|
first_preference_offer_pct = Column(Float)
|
|
oversubscription_ratio = Column(Float)
|
|
oversubscribed = Column(Boolean)
|
|
admissions_policy = Column(String(100))
|
|
|
|
|
|
class FactPupilCharacteristics(Base):
|
|
"""School pupil composition from EES census — one row per URN per year."""
|
|
__tablename__ = "fact_pupil_characteristics"
|
|
__table_args__ = (
|
|
Index("ix_pupil_chars_urn_year", "urn", "year"),
|
|
MARTS,
|
|
)
|
|
|
|
urn = Column(Integer, primary_key=True)
|
|
year = Column(Integer, primary_key=True)
|
|
phase_type_grouping = Column(String(50))
|
|
total_pupils = Column(Integer)
|
|
female_pupils = Column(Integer)
|
|
male_pupils = Column(Integer)
|
|
fsm_pct = Column(Float)
|
|
eal_pct = Column(Float)
|
|
|
|
|
|
class FactDeprivation(Base):
|
|
"""IDACI deprivation index — one row per URN."""
|
|
__tablename__ = "fact_deprivation"
|
|
__table_args__ = MARTS
|
|
|
|
urn = Column(Integer, primary_key=True)
|
|
lsoa_code = Column(String(20))
|
|
idaci_score = Column(Float)
|
|
idaci_decile = Column(Integer)
|
|
|
|
|
|
class FactFinance(Base):
|
|
"""FBIT financial benchmarking — one row per URN per year."""
|
|
__tablename__ = "fact_finance"
|
|
__table_args__ = (
|
|
Index("ix_finance_urn_year", "urn", "year"),
|
|
MARTS,
|
|
)
|
|
|
|
urn = Column(Integer, primary_key=True)
|
|
year = Column(Integer, primary_key=True)
|
|
per_pupil_spend = Column(Float)
|
|
staff_cost_pct = Column(Float)
|
|
teacher_cost_pct = Column(Float)
|
|
support_staff_cost_pct = Column(Float)
|
|
premises_cost_pct = Column(Float)
|
|
|
|
|
|
class Ks2NationalAverage(Base):
|
|
"""Official DfE KS2 national headline averages — one row per academic year."""
|
|
__tablename__ = "fact_ks2_national_averages"
|
|
__table_args__ = MARTS
|
|
|
|
year = Column(Integer, primary_key=True)
|
|
rwm_expected_pct = Column(Float)
|
|
rwm_high_pct = Column(Float)
|
|
reading_expected_pct = Column(Float)
|
|
reading_high_pct = Column(Float)
|
|
reading_avg_score = Column(Float)
|
|
writing_expected_pct = Column(Float)
|
|
writing_gd_pct = Column(Float)
|
|
maths_expected_pct = Column(Float)
|
|
maths_high_pct = Column(Float)
|
|
maths_avg_score = Column(Float)
|
|
gps_expected_pct = Column(Float)
|
|
gps_high_pct = Column(Float)
|
|
gps_avg_score = Column(Float)
|
|
science_expected_pct = Column(Float)
|