Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import requests | |
| import math | |
| # --- Logic Functions --- | |
| def calculate_distance(postal1, postal2, onemap_token=None): | |
| """ | |
| Calculates the Euclidean distance in km between two postal codes using OneMap API. | |
| Returns: | |
| tuple: (distance_km, error_message) | |
| - distance_km (float): Distance in km, or None if error. | |
| - error_message (str): None if success, else specific error description. | |
| """ | |
| def get_coordinates(postal): | |
| try: | |
| # Basic format validation (Singapore postal codes are 6 digits) | |
| if not str(postal).isdigit() or len(str(postal)) != 6: | |
| return None, f"Invalid Postal Format: {postal}" | |
| url = f"https://www.onemap.gov.sg/api/common/elastic/search?searchVal={postal}&returnGeom=Y&getAddrDetails=N&pageNum=1" | |
| headers = {} | |
| if onemap_token: | |
| headers["Authorization"] = onemap_token | |
| response = requests.get(url, headers=headers, timeout=5) | |
| if response.status_code != 200: | |
| return None, f"OneMap API Error: Status {response.status_code}" | |
| data = response.json() | |
| if data['found'] > 0: | |
| result = data['results'][0] | |
| return (float(result['X']), float(result['Y'])), None | |
| return None, f"Invalid Postal Code: {postal}" | |
| except requests.exceptions.RequestException as e: | |
| return None, f"OneMap API Connection Error: {str(e)}" | |
| except Exception as e: | |
| return None, f"Unexpected Error: {str(e)}" | |
| coord1, err1 = get_coordinates(postal1) | |
| if err1: | |
| return None, err1 | |
| coord2, err2 = get_coordinates(postal2) | |
| if err2: | |
| return None, err2 | |
| # Calculate Euclidean distance on SVY21 plane (coordinates are in meters) | |
| distance_meters = math.sqrt((coord1[0] - coord2[0])**2 + (coord1[1] - coord2[1])**2) | |
| return distance_meters / 1000.0, None | |
| def get_distance_category(distance_km): | |
| if distance_km < 1.0: | |
| return 1 # < 1km | |
| elif distance_km < 2.0: | |
| return 2 # 1-2km | |
| else: | |
| return 3 # > 2km | |
| def get_registration_phase(citizenship, user_postal, school_postal, | |
| has_sibling, is_alumni, is_staff, is_mk, | |
| is_volunteer, is_church_clan, is_community_leader, | |
| is_pr=False, onemap_token=None): | |
| """ | |
| Determines the earliest eligible P1 registration phase and priority code. | |
| Args: | |
| citizenship (str): "SCPR" or "International". | |
| user_postal (str): 6-digit postal code of user. | |
| school_postal (str): 6-digit postal code of school. | |
| ... boolean args ... | |
| is_pr (bool): True if Permanent Resident, False if Singapore Citizen. Only relevant if citizenship="SCPR". | |
| Returns: | |
| str: The priority code (e.g. "P1", "P2A-1") OR "ERROR: <Reason>" | |
| """ | |
| # --- Logic --- | |
| if citizenship == "International": | |
| return "P3" | |
| # Phase 1: Guaranteed (Conceptually P1 does not balloting by distance usually, but let's assign P1) | |
| if has_sibling: | |
| return "P1" | |
| # Calculate Distance - Catching specific errors from the helper | |
| dist, error_msg = calculate_distance(user_postal, school_postal, onemap_token) | |
| if error_msg: | |
| return f"ERROR: {error_msg}" | |
| # Helper for priority suffix | |
| def get_suffix(d_km, is_pr_status): | |
| cat = get_distance_category(d_km) | |
| # SC: 1, 2, 3 | |
| # PR: 4, 5, 6 | |
| if is_pr_status: | |
| return f"-{cat + 3}" | |
| else: | |
| return f"-{cat}" | |
| suffix = get_suffix(dist, is_pr) | |
| # Phase 2A | |
| if is_alumni or is_staff or is_mk: | |
| return f"P2A{suffix}" | |
| # Phase 2B | |
| # Check Community Leader specific 2km rule | |
| valid_community_leader = is_community_leader and (dist < 2.0) | |
| if is_volunteer or is_church_clan or valid_community_leader: | |
| return f"P2B{suffix}" | |
| # Phase 2C | |
| return f"P2C{suffix}" | |
| # --- Streamlit App --- | |
| def main(): | |
| st.set_page_config(page_title="P1 Registration Eligibility Checker", page_icon="cP", layout="wide") | |
| st.title("P1 Registration Eligibility Checker") | |
| st.markdown(""" | |
| Check which phase your child is eligible for in the Primary 1 (P1) Registration Exercise. | |
| *Based on 2025 rules for 2026 intake.* | |
| """) | |
| st.divider() | |
| # 1. Citizenship | |
| st.subheader("1. Citizenship") | |
| citizenship_radio = st.radio( | |
| "What is the child's citizenship status?", | |
| ["Singapore Citizen", "Permanent Resident", "International Student"], | |
| horizontal=True | |
| ) | |
| is_pr = False | |
| citizenship_code = "SCPR" | |
| if citizenship_radio == "International Student": | |
| citizenship_code = "International" | |
| st.warning("International Students must register in **Phase 3**.") | |
| st.info("Important: You must submit an 'Indication of Interest' form online (typically in May/June) before you can register in Phase 3 (typically in October).") | |
| return # Stop execution for International students as other questions are irrelevant | |
| elif citizenship_radio == "Permanent Resident": | |
| is_pr = True | |
| citizenship_code = "SCPR" | |
| else: | |
| # Singapore Citizen | |
| is_pr = False | |
| citizenship_code = "SCPR" | |
| st.divider() | |
| # 2. Postal Codes | |
| st.subheader("2. Location (Postal Codes)") | |
| st.info("Distance is calculated using the OneMap API.") | |
| col_postal1, col_postal2 = st.columns(2) | |
| with col_postal1: | |
| user_postal = st.text_input("Home Postal Code", max_chars=6, help="Enter your 6-digit home postal code.") | |
| with col_postal2: | |
| school_postal = st.text_input("School Postal Code", max_chars=6, help="Enter the 6-digit postal code of the school.") | |
| with st.expander("Advanced: OneMap API Token (Optional)"): | |
| onemap_token = st.text_input("API Token", type="password", help="If public API limits are hit, enter a valid OneMap token here.") | |
| if not onemap_token: | |
| onemap_token = None | |
| st.divider() | |
| # 3. School Connections | |
| st.subheader("3. Eligibility Criteria") | |
| st.write("Tick all that apply to see your eligible phase.") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.markdown("**Phase 1**") | |
| has_sibling = st.checkbox( | |
| "Child has a sibling currently studying in the school of choice", | |
| help="For a child who has a sibling currently studying in the primary school." | |
| ) | |
| st.markdown("**Phase 2A**") | |
| is_alumni = st.checkbox( | |
| "Parent/Sibling is an alumni", | |
| help="For a child whose parent or sibling is a former student of the primary school, including those who have joined the alumni association." | |
| ) | |
| is_staff = st.checkbox( | |
| "Parent is a staff member or SAC/SMC member", | |
| help="For a child whose parent is a member of the School Advisory or Management Committee, or a staff member of the primary school." | |
| ) | |
| is_mk = st.checkbox( | |
| "Child attends MOE Kindergarten at the school", | |
| help="For a child from the MOE Kindergarten under the purview of and located within the primary school." | |
| ) | |
| with col2: | |
| st.markdown("**Phase 2B**") | |
| is_volunteer = st.checkbox( | |
| "Parent is a school volunteer", | |
| help="For a child whose parent has joined the primary school as a parent volunteer not later than 1 July of the year before P1 registration and has given at least 40 hours of voluntary service to the school by 30 June of the registration year." | |
| ) | |
| is_church_clan = st.checkbox( | |
| "Parent is a member of endorsed church/clan", | |
| help="For a child whose parent is a member endorsed by the church or clan directly connected with the primary school." | |
| ) | |
| is_community_leader = st.checkbox( | |
| "Parent is an active community leader", | |
| help="For a child whose parent is endorsed as an active community leader. *Note: Must be within 2km of the school to qualify for Phase 2B.*" | |
| ) | |
| st.divider() | |
| # Calculate Button | |
| if st.button("Check Eligibility", type="primary", use_container_width=True): | |
| if not user_postal or len(user_postal) != 6 or not user_postal.isdigit(): | |
| st.error("Please enter a valid 6-digit Home Postal Code.") | |
| elif not school_postal or len(school_postal) != 6 or not school_postal.isdigit(): | |
| st.error("Please enter a valid 6-digit School Postal Code.") | |
| else: | |
| with st.spinner("Calculating distance and eligibility..."): | |
| result = get_registration_phase( | |
| citizenship=citizenship_code, | |
| user_postal=user_postal, | |
| school_postal=school_postal, | |
| has_sibling=has_sibling, | |
| is_alumni=is_alumni, | |
| is_staff=is_staff, | |
| is_mk=is_mk, | |
| is_volunteer=is_volunteer, | |
| is_church_clan=is_church_clan, | |
| is_community_leader=is_community_leader, | |
| is_pr=is_pr, | |
| onemap_token=onemap_token | |
| ) | |
| if result.startswith("ERROR:"): | |
| st.error(result) | |
| else: | |
| st.success(f"**Your eligible Phase & Priority Code: {result}**") | |
| # Explain code | |
| phase = result.split("-")[0] | |
| priority = result.split("-")[1] if "-" in result else "" | |
| explanation = "" | |
| if phase == "P1": | |
| explanation = "Phase 1: Guaranteed admission." | |
| elif "P2A" in phase: | |
| explanation = "Phase 2A: For alumni, staff, and MOE Kindergarten children." | |
| elif "P2B" in phase: | |
| explanation = "Phase 2B: For volunteers, church/clan members, and community leaders." | |
| elif "P2C" in phase: | |
| explanation = "Phase 2C: Open to all eligible children." | |
| if priority: | |
| cat_map = { | |
| "1": "Singapore Citizen < 1km", | |
| "2": "Singapore Citizen 1-2km", | |
| "3": "Singapore Citizen > 2km", | |
| "4": "Permanent Resident < 1km", | |
| "5": "Permanent Resident 1-2km", | |
| "6": "Permanent Resident > 2km" | |
| } | |
| explanation += f"\n\n**Priority Category**: {cat_map.get(priority, 'Unknown')}" | |
| st.info(explanation) | |
| st.caption("Note: This tool is for reference only. Official eligibility is determined by MOE. Distance is calculated via OneMap API (Euclidean/Straight-line).") | |
| if __name__ == "__main__": | |
| main() | |