Simplify school types and persist selected schools
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m1s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m1s
- Add runtime normalization of cryptic school type codes to user-friendly names (e.g., AC/ACC/ACCS -> "Academy", CY/CYS -> "Community") - Update SCHOOL_TYPE_MAP in schemas.py with consolidated mappings - Add normalize_school_type() and get_school_type_codes_for_filter() helpers - Persist selected schools in localStorage across page refreshes - Move "Add to Compare" button from modal footer to header Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,11 +17,39 @@ from sqlalchemy.orm import joinedload, Session
|
|||||||
from .config import settings
|
from .config import settings
|
||||||
from .database import SessionLocal, get_db_session
|
from .database import SessionLocal, get_db_session
|
||||||
from .models import School, SchoolResult
|
from .models import School, SchoolResult
|
||||||
|
from .schemas import SCHOOL_TYPE_MAP
|
||||||
|
|
||||||
# Cache for user search postcode geocoding (not for school data)
|
# Cache for user search postcode geocoding (not for school data)
|
||||||
_postcode_cache: Dict[str, Tuple[float, float]] = {}
|
_postcode_cache: Dict[str, Tuple[float, float]] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_school_type(school_type: Optional[str]) -> Optional[str]:
|
||||||
|
"""Convert cryptic school type codes to user-friendly names."""
|
||||||
|
if not school_type:
|
||||||
|
return None
|
||||||
|
# Check if it's a code that needs mapping
|
||||||
|
code = school_type.strip().upper()
|
||||||
|
if code in SCHOOL_TYPE_MAP:
|
||||||
|
return SCHOOL_TYPE_MAP[code]
|
||||||
|
# Return original if already a friendly name or unknown code
|
||||||
|
return school_type
|
||||||
|
|
||||||
|
|
||||||
|
def get_school_type_codes_for_filter(school_type: str) -> List[str]:
|
||||||
|
"""Get all database codes that map to a given friendly name."""
|
||||||
|
if not school_type:
|
||||||
|
return []
|
||||||
|
school_type_lower = school_type.lower()
|
||||||
|
# Collect all codes that map to this friendly name
|
||||||
|
codes = []
|
||||||
|
for code, friendly_name in SCHOOL_TYPE_MAP.items():
|
||||||
|
if friendly_name.lower() == school_type_lower:
|
||||||
|
codes.append(code.lower())
|
||||||
|
# Also include the school_type itself (case-insensitive) in case it's stored as-is
|
||||||
|
codes.append(school_type_lower)
|
||||||
|
return codes
|
||||||
|
|
||||||
|
|
||||||
def geocode_single_postcode(postcode: str) -> Optional[Tuple[float, float]]:
|
def geocode_single_postcode(postcode: str) -> Optional[Tuple[float, float]]:
|
||||||
"""Geocode a single postcode using postcodes.io API."""
|
"""Geocode a single postcode using postcodes.io API."""
|
||||||
if not postcode:
|
if not postcode:
|
||||||
@@ -115,7 +143,7 @@ def get_available_local_authorities(db: Session = None) -> List[str]:
|
|||||||
|
|
||||||
|
|
||||||
def get_available_school_types(db: Session = None) -> List[str]:
|
def get_available_school_types(db: Session = None) -> List[str]:
|
||||||
"""Get list of available school types."""
|
"""Get list of available school types (normalized to user-friendly names)."""
|
||||||
close_db = db is None
|
close_db = db is None
|
||||||
if db is None:
|
if db is None:
|
||||||
db = get_db()
|
db = get_db()
|
||||||
@@ -124,9 +152,15 @@ def get_available_school_types(db: Session = None) -> List[str]:
|
|||||||
result = db.query(School.school_type)\
|
result = db.query(School.school_type)\
|
||||||
.filter(School.school_type.isnot(None))\
|
.filter(School.school_type.isnot(None))\
|
||||||
.distinct()\
|
.distinct()\
|
||||||
.order_by(School.school_type)\
|
|
||||||
.all()
|
.all()
|
||||||
return [r[0] for r in result if r[0]]
|
# Normalize codes to friendly names and deduplicate
|
||||||
|
normalized = set()
|
||||||
|
for r in result:
|
||||||
|
if r[0]:
|
||||||
|
friendly_name = normalize_school_type(r[0])
|
||||||
|
if friendly_name:
|
||||||
|
normalized.add(friendly_name)
|
||||||
|
return sorted(normalized)
|
||||||
finally:
|
finally:
|
||||||
if close_db:
|
if close_db:
|
||||||
db.close()
|
db.close()
|
||||||
@@ -174,7 +208,10 @@ def get_schools(
|
|||||||
query = query.filter(func.lower(School.local_authority) == local_authority.lower())
|
query = query.filter(func.lower(School.local_authority) == local_authority.lower())
|
||||||
|
|
||||||
if school_type:
|
if school_type:
|
||||||
query = query.filter(func.lower(School.school_type) == school_type.lower())
|
# Filter by all codes that map to this friendly name
|
||||||
|
type_codes = get_school_type_codes_for_filter(school_type)
|
||||||
|
if type_codes:
|
||||||
|
query = query.filter(func.lower(School.school_type).in_(type_codes))
|
||||||
|
|
||||||
# Get total count
|
# Get total count
|
||||||
total = query.count()
|
total = query.count()
|
||||||
@@ -222,7 +259,10 @@ def get_schools_near_location(
|
|||||||
query = query.filter(func.lower(School.local_authority) == local_authority.lower())
|
query = query.filter(func.lower(School.local_authority) == local_authority.lower())
|
||||||
|
|
||||||
if school_type:
|
if school_type:
|
||||||
query = query.filter(func.lower(School.school_type) == school_type.lower())
|
# Filter by all codes that map to this friendly name
|
||||||
|
type_codes = get_school_type_codes_for_filter(school_type)
|
||||||
|
if type_codes:
|
||||||
|
query = query.filter(func.lower(School.school_type).in_(type_codes))
|
||||||
|
|
||||||
# Get all matching schools and calculate distances
|
# Get all matching schools and calculate distances
|
||||||
all_schools = query.all()
|
all_schools = query.all()
|
||||||
@@ -335,7 +375,7 @@ def school_to_dict(school: School, include_results: bool = False) -> dict:
|
|||||||
"urn": school.urn,
|
"urn": school.urn,
|
||||||
"school_name": school.school_name,
|
"school_name": school.school_name,
|
||||||
"local_authority": school.local_authority,
|
"local_authority": school.local_authority,
|
||||||
"school_type": school.school_type,
|
"school_type": normalize_school_type(school.school_type),
|
||||||
"address": school.address,
|
"address": school.address,
|
||||||
"town": school.town,
|
"town": school.town,
|
||||||
"postcode": school.postcode,
|
"postcode": school.postcode,
|
||||||
@@ -422,7 +462,7 @@ def load_school_data_as_dataframe(db: Session = None) -> pd.DataFrame:
|
|||||||
"urn": school.urn,
|
"urn": school.urn,
|
||||||
"school_name": school.school_name,
|
"school_name": school.school_name,
|
||||||
"local_authority": school.local_authority,
|
"local_authority": school.local_authority,
|
||||||
"school_type": school.school_type,
|
"school_type": normalize_school_type(school.school_type),
|
||||||
"address": school.address,
|
"address": school.address,
|
||||||
"town": school.town,
|
"town": school.town,
|
||||||
"postcode": school.postcode,
|
"postcode": school.postcode,
|
||||||
|
|||||||
@@ -101,16 +101,24 @@ NUMERIC_COLUMNS = [
|
|||||||
"maths_avg_3yr",
|
"maths_avg_3yr",
|
||||||
]
|
]
|
||||||
|
|
||||||
# School type code to name mapping
|
# School type code to user-friendly name mapping
|
||||||
SCHOOL_TYPE_MAP = {
|
SCHOOL_TYPE_MAP = {
|
||||||
|
# Academies
|
||||||
"AC": "Academy",
|
"AC": "Academy",
|
||||||
"ACC": "Academy Converter",
|
"ACC": "Academy",
|
||||||
"ACS": "Academy Sponsor Led",
|
"ACCS": "Academy",
|
||||||
"CY": "Community School",
|
"ACS": "Academy (Sponsor Led)",
|
||||||
|
# Community Schools
|
||||||
|
"CY": "Community",
|
||||||
|
"CYS": "Community",
|
||||||
|
# Voluntary Schools
|
||||||
"VA": "Voluntary Aided",
|
"VA": "Voluntary Aided",
|
||||||
"VC": "Voluntary Controlled",
|
"VC": "Voluntary Controlled",
|
||||||
|
# Foundation Schools
|
||||||
"FD": "Foundation",
|
"FD": "Foundation",
|
||||||
"F": "Foundation",
|
"F": "Foundation",
|
||||||
|
"FDS": "Foundation",
|
||||||
|
# Free Schools
|
||||||
"FS": "Free School",
|
"FS": "Free School",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -345,6 +345,9 @@ function handleRoute() {
|
|||||||
document.addEventListener("DOMContentLoaded", init);
|
document.addEventListener("DOMContentLoaded", init);
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
|
// Load selected schools from localStorage first
|
||||||
|
loadSelectedSchoolsFromStorage();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Load filters and metrics in parallel (single request for filters)
|
// Load filters and metrics in parallel (single request for filters)
|
||||||
const [filtersData, metricsData] = await Promise.all([
|
const [filtersData, metricsData] = await Promise.all([
|
||||||
@@ -376,6 +379,9 @@ async function init() {
|
|||||||
// Initialize tooltip manager
|
// Initialize tooltip manager
|
||||||
tooltipManager = new TooltipManager();
|
tooltipManager = new TooltipManager();
|
||||||
|
|
||||||
|
// Render any previously selected schools
|
||||||
|
renderSelectedSchools();
|
||||||
|
|
||||||
// Handle initial route
|
// Handle initial route
|
||||||
handleRoute();
|
handleRoute();
|
||||||
|
|
||||||
@@ -1544,14 +1550,38 @@ function addToComparison(school) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
state.selectedSchools.push(school);
|
state.selectedSchools.push(school);
|
||||||
|
saveSelectedSchoolsToStorage();
|
||||||
renderSelectedSchools();
|
renderSelectedSchools();
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeFromComparison(urn) {
|
function removeFromComparison(urn) {
|
||||||
state.selectedSchools = state.selectedSchools.filter((s) => s.urn !== urn);
|
state.selectedSchools = state.selectedSchools.filter((s) => s.urn !== urn);
|
||||||
|
saveSelectedSchoolsToStorage();
|
||||||
renderSelectedSchools();
|
renderSelectedSchools();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function saveSelectedSchoolsToStorage() {
|
||||||
|
try {
|
||||||
|
localStorage.setItem("selectedSchools", JSON.stringify(state.selectedSchools));
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Failed to save to localStorage:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSelectedSchoolsFromStorage() {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem("selectedSchools");
|
||||||
|
if (stored) {
|
||||||
|
const schools = JSON.parse(stored);
|
||||||
|
if (Array.isArray(schools)) {
|
||||||
|
state.selectedSchools = schools;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Failed to load from localStorage:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function showEmptyState(container, message) {
|
function showEmptyState(container, message) {
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
|
|||||||
@@ -330,6 +330,7 @@
|
|||||||
<h2 id="modal-school-name"></h2>
|
<h2 id="modal-school-name"></h2>
|
||||||
<div class="modal-meta" id="modal-meta"></div>
|
<div class="modal-meta" id="modal-meta"></div>
|
||||||
<div class="modal-details" id="modal-details"></div>
|
<div class="modal-details" id="modal-details"></div>
|
||||||
|
<button class="btn btn-primary" id="add-to-compare">Add to Compare</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="modal-chart-container">
|
<div class="modal-chart-container">
|
||||||
@@ -341,9 +342,6 @@
|
|||||||
<div class="modal-map" id="modal-map"></div>
|
<div class="modal-map" id="modal-map"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
|
||||||
<button class="btn btn-primary" id="add-to-compare">Add to Compare</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1088,6 +1088,10 @@ body {
|
|||||||
margin-bottom: 0.25rem;
|
margin-bottom: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-header .btn {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.modal-details .modal-age-range {
|
.modal-details .modal-age-range {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
@@ -1165,12 +1169,6 @@ body {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-footer {
|
|
||||||
padding: 1.5rem 2rem;
|
|
||||||
border-top: 1px solid var(--border-color);
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Buttons */
|
/* Buttons */
|
||||||
.btn {
|
.btn {
|
||||||
|
|||||||
Reference in New Issue
Block a user